diff --git a/projects/components/src/overlay/overlay.service.test.ts b/projects/components/src/overlay/overlay.service.test.ts index bf8901298..9d682d627 100644 --- a/projects/components/src/overlay/overlay.service.test.ts +++ b/projects/components/src/overlay/overlay.service.test.ts @@ -1,55 +1,84 @@ -import { Component } from '@angular/core'; -import { fakeAsync, flush, tick } from '@angular/core/testing'; -import { NavigationService } from '@hypertrace/common'; -import { recordObservable, runFakeRxjs } from '@hypertrace/test-utils'; -import { createServiceFactory, mockProvider, SpectatorService } from '@ngneat/spectator/jest'; -import { Subject } from 'rxjs'; -import { PopoverModule } from '../popover/popover.module'; -import { OverlayService } from './overlay.service'; -import { SheetSize } from './sheet/sheet'; +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { fakeAsync, flush } from '@angular/core/testing'; +import { IconLibraryTestingModule } from '@hypertrace/assets-library'; +import { GLOBAL_HEADER_HEIGHT, NavigationService } from '@hypertrace/common'; +import { OverlayService, SheetRef, SheetSize } from '@hypertrace/components'; +import { createHostFactory, mockProvider } from '@ngneat/spectator/jest'; +import { EMPTY } from 'rxjs'; +import { OverlayModule } from './overlay.module'; +import { SHEET_DATA } from './sheet/sheet'; describe('Overlay service', () => { - const navigation$: Subject = new Subject(); + @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
Test Component Content Data: {{ this.data }}
+ + ` + }) + class TestComponent { + public constructor(@Inject(SHEET_DATA) public readonly data: string, public readonly sheetRef: SheetRef) {} - let spectator: SpectatorService; + public onClose(): void { + this.sheetRef.close(this.data); + } + } - const createService = createServiceFactory({ - service: OverlayService, - imports: [PopoverModule], + const createHost = createHostFactory({ + component: Component({ selector: 'host', template: 'template' })(class {}), + declarations: [TestComponent], + entryComponents: [TestComponent], + imports: [OverlayModule, IconLibraryTestingModule], providers: [ mockProvider(NavigationService, { - navigation$: navigation$ - }) - ] + navigation$: EMPTY + }), + { + provide: GLOBAL_HEADER_HEIGHT, + useValue: 100 + } + ], + template: `` }); - beforeEach(() => { - spectator = createService(); - }); - - test('can close a sheet popover on navigation', fakeAsync(() => { - const popover = spectator.service.createSheet({ - showHeader: false, - size: SheetSize.Medium, - content: Component({ - selector: 'test-component', - template: `
TEST
` - })(class {}) + test('can create a sheet with provided data', fakeAsync(() => { + const spectator = createHost(); + spectator.inject(OverlayService).createSheet({ + content: TestComponent, + size: SheetSize.Small, + title: 'Test title', + showHeader: true, + data: 'custom input' }); - popover.show(); - popover.closeOnNavigation(); - tick(); // CDK overlay is async - expect(popover.closed).toBe(false); + spectator.tick(); + + expect(spectator.query('.test-sheet-content', { root: true })).toContainText( + 'Test Component Content Data: custom input' + ); + })); - runFakeRxjs(({ expectObservable }) => { - expectObservable(popover.closed$).toBe('(x|)', { x: undefined }); - expectObservable(recordObservable(popover.hidden$)).toBe('|'); // Record hidden/shown for test, since they're hot - expectObservable(recordObservable(popover.shown$)).toBe('|'); - navigation$.next(); + test('sheet can be closed and return a result', fakeAsync(() => { + const spectator = createHost(); + const sheet: SheetRef = spectator.inject(OverlayService).createSheet({ + content: TestComponent, + size: SheetSize.Small, + data: 'custom input' }); + let result: string | undefined; + spectator.tick(); + const subscription = sheet.closed$.subscribe(out => (result = out)); + const closeButton = spectator.query('.test-close-button', { root: true })!; + expect(result).toBeUndefined(); + expect(subscription.closed).toBe(false); + + spectator.click(closeButton); + spectator.tick(); + + expect(spectator.query('.test-sheet-content', { root: true })).not.toExist(); + expect(result).toBe('custom input'); + expect(subscription.closed).toBe(true); - expect(popover.closed).toBe(true); - flush(); // CDK cleans up overlay async + flush(); // CDK timer to remove overlay })); }); diff --git a/projects/components/src/overlay/overlay.service.ts b/projects/components/src/overlay/overlay.service.ts index 566613626..b1a425f8a 100644 --- a/projects/components/src/overlay/overlay.service.ts +++ b/projects/components/src/overlay/overlay.service.ts @@ -3,7 +3,8 @@ import { Subscription } from 'rxjs'; import { PopoverFixedPositionLocation, PopoverPositionType } from '../popover/popover'; import { PopoverRef } from '../popover/popover-ref'; import { PopoverService } from '../popover/popover.service'; -import { SheetOverlayConfig, SHEET_DATA } from './sheet/sheet'; +import { DefaultSheetRef } from './sheet/default-sheet-ref'; +import { SheetOverlayConfig, SheetRef, SHEET_DATA } from './sheet/sheet'; import { SheetOverlayComponent } from './sheet/sheet-overlay.component'; @Injectable({ @@ -16,10 +17,14 @@ export class OverlayService { public constructor(private readonly popoverService: PopoverService, private readonly defaultInjector: Injector) {} - public createSheet(config: SheetOverlayConfig, injector: Injector = this.defaultInjector): PopoverRef { + public createSheet( + config: SheetOverlayConfig, + injector: Injector = this.defaultInjector + ): SheetRef { this.activeSheetPopover?.close(); - const metadata = this.buildMetadata(config, injector); + const sheetRef = new DefaultSheetRef(); + const metadata = this.buildMetadata(config, injector, sheetRef); const popover = this.popoverService.drawPopover({ componentOrTemplate: SheetOverlayComponent, parentInjector: injector, @@ -31,13 +36,18 @@ export class OverlayService { }); popover.closeOnNavigation(); + sheetRef.initialize(popover); this.setActiveSheetPopover(popover); - return popover; + return sheetRef as SheetRef; } - private buildMetadata(config: SheetOverlayConfig, parentInjector: Injector): SheetConstructionData { + private buildMetadata( + config: SheetOverlayConfig, + parentInjector: Injector, + sheetRef: SheetRef + ): SheetConstructionData { return { config: config, injector: Injector.create({ @@ -45,6 +55,10 @@ export class OverlayService { { provide: SHEET_DATA, useValue: config.data + }, + { + provide: SheetRef, + useValue: sheetRef } ], parent: parentInjector diff --git a/projects/components/src/overlay/sheet/default-sheet-ref.ts b/projects/components/src/overlay/sheet/default-sheet-ref.ts new file mode 100644 index 000000000..3d7b4d64b --- /dev/null +++ b/projects/components/src/overlay/sheet/default-sheet-ref.ts @@ -0,0 +1,32 @@ +import { Observable, Observer, ReplaySubject } from 'rxjs'; +import { PopoverRef } from '../../popover/popover-ref'; +import { SheetRef } from './sheet'; + +export class DefaultSheetRef extends SheetRef { + public readonly closed$: Observable; + + private readonly closedObserver: Observer; + private popoverRef?: PopoverRef; + + public constructor() { + super(); + const closedSubject = new ReplaySubject(1); + this.closedObserver = closedSubject; + this.closed$ = closedSubject.asObservable(); + } + + public initialize(popoverRef: PopoverRef): void { + this.popoverRef = popoverRef; + this.popoverRef.closed$.subscribe({ + complete: () => this.closedObserver.complete(), + error: err => this.closedObserver.error(err) + }); + } + + public close(result?: unknown): void { + if (result !== undefined) { + this.closedObserver.next(result); + } + this.popoverRef?.close(); + } +} diff --git a/projects/components/src/overlay/sheet/sheet.ts b/projects/components/src/overlay/sheet/sheet.ts index 8309527f4..c6ad3f01f 100644 --- a/projects/components/src/overlay/sheet/sheet.ts +++ b/projects/components/src/overlay/sheet/sheet.ts @@ -1,4 +1,5 @@ import { InjectionToken } from '@angular/core'; +import { Observable } from 'rxjs'; import { OverlayConfig } from './../overlay'; export interface SheetOverlayConfig extends OverlayConfig { @@ -15,3 +16,8 @@ export const enum SheetSize { } export const SHEET_DATA = new InjectionToken('SHEET_DATA'); + +export abstract class SheetRef { + public abstract readonly closed$: Observable; + public abstract close(result?: TResult): void; +} diff --git a/projects/distributed-tracing/src/shared/dashboard/interaction/detail-sheet/detail-sheet-interaction-handler.model.ts b/projects/distributed-tracing/src/shared/dashboard/interaction/detail-sheet/detail-sheet-interaction-handler.model.ts index 1e6c9550b..7b253a52a 100644 --- a/projects/distributed-tracing/src/shared/dashboard/interaction/detail-sheet/detail-sheet-interaction-handler.model.ts +++ b/projects/distributed-tracing/src/shared/dashboard/interaction/detail-sheet/detail-sheet-interaction-handler.model.ts @@ -1,4 +1,4 @@ -import { PopoverRef, SheetSize } from '@hypertrace/components'; +import { SheetRef, SheetSize } from '@hypertrace/components'; import { EnumPropertyTypeInstance, ENUM_TYPE, ModelTemplatePropertyType } from '@hypertrace/dashboards'; import { BOOLEAN_PROPERTY, Model, ModelApi, ModelJson, ModelProperty, STRING_PROPERTY } from '@hypertrace/hyperdash'; import { ModelInject, MODEL_API } from '@hypertrace/hyperdash-angular'; @@ -53,7 +53,7 @@ export class DetailSheetInteractionHandlerModel implements InteractionHandler { @ModelInject(DetailSheetInteractionHandlerService) private readonly handlerService!: DetailSheetInteractionHandlerService; - private popover?: PopoverRef; + private sheet?: SheetRef; public execute(data?: unknown): Observable { if (isEmpty(data)) { @@ -81,7 +81,7 @@ export class DetailSheetInteractionHandlerModel implements InteractionHandler { const title = get(source, this.titlePropertyPath ?? ''); const model = this.getDetailModel(source); - this.popover = this.handlerService.showSheet(model, this.sheetSize, title, this.showHeader); + this.sheet = this.handlerService.showSheet(model, this.sheetSize, title, this.showHeader); } private getDetailModel(source: unknown): object { @@ -92,6 +92,6 @@ export class DetailSheetInteractionHandlerModel implements InteractionHandler { } private clear(): void { - this.popover?.close(); + this.sheet?.close(); } } diff --git a/projects/distributed-tracing/src/shared/dashboard/interaction/detail-sheet/detail-sheet-interaction-handler.service.ts b/projects/distributed-tracing/src/shared/dashboard/interaction/detail-sheet/detail-sheet-interaction-handler.service.ts index bfc582f75..3d50e6c34 100644 --- a/projects/distributed-tracing/src/shared/dashboard/interaction/detail-sheet/detail-sheet-interaction-handler.service.ts +++ b/projects/distributed-tracing/src/shared/dashboard/interaction/detail-sheet/detail-sheet-interaction-handler.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { OverlayService, PopoverRef, SheetSize } from '@hypertrace/components'; +import { OverlayService, SheetRef, SheetSize } from '@hypertrace/components'; import { DetailSheetInteractionContainerComponent } from './container/detail-sheet-interaction-container.component'; @Injectable({ @@ -13,7 +13,7 @@ export class DetailSheetInteractionHandlerService { sheetSize: SheetSize = SheetSize.Medium, title?: string, showHeader: boolean = true - ): PopoverRef { + ): SheetRef { return this.overlayService.createSheet({ content: DetailSheetInteractionContainerComponent, size: sheetSize, diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall-widget-renderer.component.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall-widget-renderer.component.ts index d11ef3b83..3b6a27395 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall-widget-renderer.component.ts +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall-widget-renderer.component.ts @@ -8,7 +8,7 @@ import { ViewChild } from '@angular/core'; import { IconType } from '@hypertrace/assets-library'; -import { ButtonStyle, OverlayService, PopoverRef, SheetSize } from '@hypertrace/components'; +import { ButtonStyle, OverlayService, SheetRef, SheetSize } from '@hypertrace/components'; import { WidgetRenderer } from '@hypertrace/dashboards'; import { Renderer } from '@hypertrace/hyperdash'; import { RendererApi, RENDERER_API } from '@hypertrace/hyperdash-angular'; @@ -83,7 +83,7 @@ export class WaterfallWidgetRendererComponent @ViewChild('sidebarDetails', { static: true }) public sidebarDetails!: TemplateRef; - private popoverRef?: PopoverRef; + private sheet?: SheetRef; public selectedData?: WaterfallData; public constructor( @@ -111,13 +111,10 @@ export class WaterfallWidgetRendererComponent } private openSheet(selected: WaterfallData): void { - if (this.popoverRef !== undefined) { - this.popoverRef.close(); - } - + this.sheet?.close(); this.selectedData = selected; - this.popoverRef = this.overlayService.createSheet({ + this.sheet = this.overlayService.createSheet({ showHeader: false, size: SheetSize.ResponsiveExtraLarge, content: this.sidebarDetails @@ -125,7 +122,7 @@ export class WaterfallWidgetRendererComponent } public closeSheet(): void { - this.popoverRef?.close(); + this.sheet?.close(); this.selectedData = undefined; this.changeDetector.markForCheck(); // Need this for child table to remove selected-row class }