diff --git a/projects/components/src/table/controls/table-controls.component.ts b/projects/components/src/table/controls/table-controls.component.ts index 6289032b4..12d7319dc 100644 --- a/projects/components/src/table/controls/table-controls.component.ts +++ b/projects/components/src/table/controls/table-controls.component.ts @@ -6,7 +6,8 @@ import { IterableDiffer, IterableDiffers, OnChanges, - Output + Output, + TemplateRef } from '@angular/core'; import { IconType } from '@hypertrace/assets-library'; import { TypedSimpleChanges } from '@hypertrace/common'; @@ -14,6 +15,7 @@ import { IconSize } from '../../icon/icon-size'; import { MultiSelectJustify } from '../../multi-select/multi-select-justify'; import { MultiSelectSearchMode, TriggerLabelDisplayMode } from '../../multi-select/multi-select.component'; import { ToggleItem } from '../../toggle-group/toggle-item'; +import { StatefulTableRow } from '../table-api'; import { TableCheckboxChange, TableCheckboxControl, @@ -86,6 +88,13 @@ import { [activeItem]="this.activeViewItem" (activeItemChange)="this.onViewChange($event)" > + + + + + ` @@ -114,6 +123,12 @@ export class TableControlsComponent implements OnChanges { @Input() public activeViewItem?: ToggleItem; + @Input() + public selectedRows?: StatefulTableRow[] = []; + + @Input() + public customControlContent?: TemplateRef<{ selectedRows?: StatefulTableRow[] }>; + @Output() public readonly searchChange: EventEmitter = new EventEmitter(); diff --git a/projects/components/src/table/table.component.test.ts b/projects/components/src/table/table.component.test.ts index fa153597e..2ca704b10 100644 --- a/projects/components/src/table/table.component.test.ts +++ b/projects/components/src/table/table.component.test.ts @@ -60,7 +60,7 @@ describe('Table component', () => { queryParamMap: EMPTY }), mockProvider(DomElementMeasurerService, { - measureHtmlElement: (): ClientRect => ({ + measureHtmlElement: (): Partial => ({ top: 0, left: 0, bottom: 20, @@ -397,37 +397,6 @@ describe('Table component', () => { }); })); - test('should trigger toggle row selection for multi row select config', () => { - const columns = buildColumns(); - const spectator = createHost( - ``, - { - hostProps: { - columnConfigs: columns, - data: buildData(), - selectionMode: TableSelectionMode.Multiple, - mode: TableMode.Flat - } - } - ); - - const row: StatefulTableRow = { - $$state: { - parent: undefined, - expanded: false, - selected: false, - root: false, - leaf: true, - depth: 1 - } - }; - - const spyToggleRowSelection = spyOn(spectator.component, 'toggleRowSelected'); - spectator.component.onDataCellClick(row); - expect(spyToggleRowSelection).toHaveBeenCalledWith(row); - }); - test('should emit selections on toggle select', () => { const mockSelectionsChange = jest.fn(); const columns = buildColumns(); diff --git a/projects/components/src/table/table.component.ts b/projects/components/src/table/table.component.ts index 2788aca62..46b164bba 100644 --- a/projects/components/src/table/table.component.ts +++ b/projects/components/src/table/table.component.ts @@ -272,6 +272,9 @@ export class TableComponent @Input() public pageSize?: number = 50; + @Output() + public readonly rowClicked: EventEmitter = new EventEmitter(); + @Output() public readonly selectionsChange: EventEmitter = new EventEmitter(); @@ -548,9 +551,7 @@ export class TableComponent } public onDataRowClick(row: StatefulTableRow): void { - if (this.hasSelectableRows()) { - this.toggleRowSelected(row); - } + this.rowClicked.emit(row); } public onDataRowMouseEnter(row: StatefulTableRow): void { @@ -695,10 +696,6 @@ export class TableComponent return this.isDetailType() && row.$$state.expanded; } - public hasSelectableRows(): boolean { - return this.hasSingleSelect() || this.hasMultiSelect(); - } - public hasSingleSelect(): boolean { return this.selectionMode === TableSelectionMode.Single; } diff --git a/projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-selection.model.test.ts b/projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-interaction.model.test.ts similarity index 76% rename from projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-selection.model.test.ts rename to projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-interaction.model.test.ts index 296adceae..544b4bfb5 100644 --- a/projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-selection.model.test.ts +++ b/projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-interaction.model.test.ts @@ -1,8 +1,8 @@ -import { TableWidgetRowSelectionModel } from './table-widget-row-selection.model'; +import { TableWidgetRowInteractionModel } from './table-widget-row-interaction.model'; -describe('TableWidgetRowSelectionModel applies to current row depth', () => { +describe('TableWidgetRowInteractionModel applies to current row depth', () => { test('works for default properties', () => { - const model = new TableWidgetRowSelectionModel(); + const model = new TableWidgetRowInteractionModel(); model.handler = { execute: jest.fn() }; @@ -13,7 +13,7 @@ describe('TableWidgetRowSelectionModel applies to current row depth', () => { }); test('works for higher row depth', () => { - const model = new TableWidgetRowSelectionModel(); + const model = new TableWidgetRowInteractionModel(); model.handler = { execute: jest.fn() }; @@ -26,7 +26,7 @@ describe('TableWidgetRowSelectionModel applies to current row depth', () => { }); test('works correctly when applyToChildRows is false', () => { - const model = new TableWidgetRowSelectionModel(); + const model = new TableWidgetRowInteractionModel(); model.handler = { execute: jest.fn() }; diff --git a/projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-selection.model.ts b/projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-interaction.model.ts similarity index 90% rename from projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-selection.model.ts rename to projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-interaction.model.ts index 8d6f00595..47176d1d2 100644 --- a/projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-selection.model.ts +++ b/projects/observability/src/shared/dashboard/widgets/table/selections/table-widget-row-interaction.model.ts @@ -2,10 +2,10 @@ import { BOOLEAN_PROPERTY, Model, ModelProperty, ModelPropertyType, NUMBER_PROPE import { InteractionHandler } from '../../../interaction/interaction-handler'; @Model({ - type: 'table-widget-row-selection', + type: 'table-widget-row-interaction', displayName: 'Row Selection' }) -export class TableWidgetRowSelectionModel { +export class TableWidgetRowInteractionModel { @ModelProperty({ key: 'handler', displayName: 'Selection Handler', diff --git a/projects/observability/src/shared/dashboard/widgets/table/table-widget-base.model.ts b/projects/observability/src/shared/dashboard/widgets/table/table-widget-base.model.ts index cb499423b..e732c85db 100644 --- a/projects/observability/src/shared/dashboard/widgets/table/table-widget-base.model.ts +++ b/projects/observability/src/shared/dashboard/widgets/table/table-widget-base.model.ts @@ -17,7 +17,7 @@ import { } from '@hypertrace/hyperdash'; import { ModelInject, MODEL_API } from '@hypertrace/hyperdash-angular'; import { Observable } from 'rxjs'; -import { TableWidgetRowSelectionModel } from './selections/table-widget-row-selection.model'; +import { TableWidgetRowInteractionModel } from './selections/table-widget-row-interaction.model'; import { SpecificationBackedTableColumnDef } from './table-widget-column.model'; import { TableWidgetControlCheckboxOptionModel } from './table-widget-control-checkbox-option.model'; import { TableWidgetControlSelectOptionModel } from './table-widget-control-select-option.model'; @@ -121,16 +121,11 @@ export abstract class TableWidgetBaseModel extends BaseModel { return this.api.getData>(); } - public getRowSelectionHandlers(_row: TableRow): TableWidgetRowSelectionModel[] { + public getRowSelectionHandlers(_row: TableRow): TableWidgetRowInteractionModel[] { // No-op here, but can be overridden return []; } - public getSelectionMode(): TableSelectionMode { - // No-op here, but can be overridden - return TableSelectionMode.Single; - } - public setView(_view: string): void { // No-op here, but can be overridden return; @@ -141,6 +136,11 @@ export abstract class TableWidgetBaseModel extends BaseModel { return []; } + public abstract getRowClickHandlers(): TableWidgetRowInteractionModel[]; + public abstract getSelectionMode(): TableSelectionMode; + public abstract isCustomControlPresent(): boolean; + public abstract getCustomControlWidgetModel(selectedRows?: TableRow[]): object | undefined; + public getSearchAttribute(): string | undefined { return this.searchAttribute; } diff --git a/projects/observability/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts b/projects/observability/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts index 7e6eb5082..9c04d9b4a 100644 --- a/projects/observability/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts +++ b/projects/observability/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts @@ -22,7 +22,6 @@ import { TableRow, TableSelectChange, TableSelectControl, - TableSelectionMode, TableStyle, ToggleItem, toInFilter @@ -36,6 +35,7 @@ import { filter, map, pairwise, share, startWith, switchMap, take, tap, withLate import { AttributeMetadata, toFilterAttributeType } from '../../../graphql/model/metadata/attribute-metadata'; import { MetadataService } from '../../../services/metadata/metadata.service'; import { InteractionHandler } from '../../interaction/interaction-handler'; +import { TableWidgetRowInteractionModel } from './selections/table-widget-row-interaction.model'; import { TableWidgetBaseModel } from './table-widget-base.model'; import { SpecificationBackedTableColumnDef } from './table-widget-column.model'; import { TableWidgetViewToggleModel } from './table-widget-view-toggle.model'; @@ -61,6 +61,8 @@ import { TableWidgetModel } from './table-widget.model'; [searchPlaceholder]="this.api.model.getSearchPlaceholder()" [selectControls]="this.selectControls$ | async" [checkboxControls]="this.checkboxControls$ | async" + [selectedRows]="this.selectedRows" + [customControlContent]="(this.isCustomControlPresent | htMemoize) ? customControlDetail : undefined" [viewItems]="this.viewItems" (searchChange)="this.onSearchChange($event)" (selectChange)="this.onSelectChange($event)" @@ -83,6 +85,7 @@ import { TableWidgetModel } from './table-widget.model'; [resizable]="this.api.model.isResizable()" [detailContent]="childDetail" [syncWithUrl]="this.syncWithUrl" + (rowClicked)="this.onRowClicked($event)" (selectionsChange)="this.onRowSelection($event)" (columnConfigsChange)="this.onColumnsChange($event)" > @@ -93,6 +96,10 @@ import { TableWidgetModel } from './table-widget.model'; + + + + ` }) export class TableWidgetRendererComponent @@ -107,6 +114,8 @@ export class TableWidgetRendererComponent public columnConfigs$!: Observable; public combinedFilters$!: Observable; + public selectedRows?: StatefulTableRow[] = []; + private readonly toggleFilterSubject: Subject = new BehaviorSubject([]); private readonly searchFilterSubject: Subject = new BehaviorSubject([]); private readonly selectFilterSubject: BehaviorSubject = new BehaviorSubject([]); @@ -116,8 +125,6 @@ export class TableWidgetRendererComponent >({}); public queryProperties$: Observable> = this.queryPropertiesSubject.asObservable(); - private selectedRowInteractionHandler?: InteractionHandler; - public constructor( @Inject(RENDERER_API) api: RendererApi, changeDetectorRef: ChangeDetectorRef, @@ -152,6 +159,11 @@ export class TableWidgetRendererComponent public getChildModel = (row: TableRow): object | undefined => this.model.getChildModel(row); + public isCustomControlPresent = (): boolean => this.model.isCustomControlPresent(); + + public getCustomControlWidgetModel = (selectedRows?: TableRow[]): object | undefined => + this.model.getCustomControlWidgetModel(selectedRows); + protected fetchData(): Observable | undefined> { return this.model.getData().pipe( startWith(undefined), @@ -368,28 +380,30 @@ export class TableWidgetRendererComponent } } + public onRowClicked(row: StatefulTableRow): void { + this.getRowClickInteractionHandler(row)?.execute(row); + } + public onRowSelection(selections: StatefulTableRow[]): void { - if (this.api.model.getSelectionMode() === TableSelectionMode.Single) { - /** - * Execute selection handler for single selection mode only - */ - let selectedRow; - if (selections.length > 0) { - selectedRow = selections[0]; - this.selectedRowInteractionHandler = this.getInteractionHandler(selectedRow); - } - - this.selectedRowInteractionHandler?.execute(selectedRow); - } + this.selectedRows = selections; + /** + * Todo: Stich this with selection handlers + */ + } + + private getRowClickInteractionHandler(selectedRow: StatefulTableRow): InteractionHandler | undefined { + return this.getInteractionHandler(selectedRow, this.api.model.getRowClickHandlers()); } - private getInteractionHandler(selectedRow: StatefulTableRow): InteractionHandler | undefined { - const matchedSelectionHandlers = this.api.model - .getRowSelectionHandlers(selectedRow) - ?.filter(selectionModel => selectionModel.appliesToCurrentRowDepth(selectedRow.$$state.depth)) + private getInteractionHandler( + row: StatefulTableRow, + rowHandlers: TableWidgetRowInteractionModel[] = [] + ): InteractionHandler | undefined { + const matchedHandlers = rowHandlers + .filter(interactionModel => interactionModel.appliesToCurrentRowDepth(row.$$state.depth)) .sort((model1, model2) => model2.rowDepth - model1.rowDepth); - return !isEmpty(matchedSelectionHandlers) ? matchedSelectionHandlers[0].handler : undefined; + return !isEmpty(matchedHandlers) ? matchedHandlers[0].handler : undefined; } private pickPersistColumnProperties(column: TableColumnConfig): Pick { diff --git a/projects/observability/src/shared/dashboard/widgets/table/table-widget-view-toggle.model.ts b/projects/observability/src/shared/dashboard/widgets/table/table-widget-view-toggle.model.ts index d8a1cd484..826d578dd 100644 --- a/projects/observability/src/shared/dashboard/widgets/table/table-widget-view-toggle.model.ts +++ b/projects/observability/src/shared/dashboard/widgets/table/table-widget-view-toggle.model.ts @@ -1,9 +1,9 @@ -import { TableDataSource, TableRow } from '@hypertrace/components'; +import { TableDataSource, TableRow, TableSelectionMode } from '@hypertrace/components'; import { ArrayPropertyTypeInstance } from '@hypertrace/dashboards'; import { ARRAY_PROPERTY, Model, ModelApi, ModelOnInit, ModelProperty, ModelPropertyType } from '@hypertrace/hyperdash'; import { ModelInject, MODEL_API } from '@hypertrace/hyperdash-angular'; import { NEVER, Observable } from 'rxjs'; -import { TableWidgetRowSelectionModel } from './selections/table-widget-row-selection.model'; +import { TableWidgetRowInteractionModel } from './selections/table-widget-row-interaction.model'; import { SpecificationBackedTableColumnDef } from './table-widget-column.model'; import { TableWidgetControlCheckboxOptionModel } from './table-widget-control-checkbox-option.model'; import { TableWidgetControlSelectOptionModel } from './table-widget-control-select-option.model'; @@ -73,7 +73,23 @@ export class TableWidgetViewToggleModel extends TableWidgetModel implements Mode return this.delegateModel && this.delegateModel?.getChildModel(row); } - public getRowSelectionHandlers(row: TableRow): TableWidgetRowSelectionModel[] { + public getSelectionMode(): TableSelectionMode { + return this.delegateModel?.getSelectionMode() ?? TableSelectionMode.Single; + } + + public isCustomControlPresent(): boolean { + return this.delegateModel?.isCustomControlPresent() ?? false; + } + + public getCustomControlWidgetModel(selectedRows?: TableRow[]): object | undefined { + return this.delegateModel && this.delegateModel?.getCustomControlWidgetModel(selectedRows); + } + + public getRowClickHandlers(): TableWidgetRowInteractionModel[] { + return this.delegateModel?.getRowClickHandlers() ?? []; + } + + public getRowSelectionHandlers(row: TableRow): TableWidgetRowInteractionModel[] { return this.delegateModel && this.delegateModel?.getRowSelectionHandlers(row).length > 0 ? this.delegateModel?.getRowSelectionHandlers(row) : []; diff --git a/projects/observability/src/shared/dashboard/widgets/table/table-widget.model.ts b/projects/observability/src/shared/dashboard/widgets/table/table-widget.model.ts index e6b7f8efe..7a6c89958 100644 --- a/projects/observability/src/shared/dashboard/widgets/table/table-widget.model.ts +++ b/projects/observability/src/shared/dashboard/widgets/table/table-widget.model.ts @@ -18,7 +18,7 @@ import { ModelInject } from '@hypertrace/hyperdash-angular'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { InteractionHandler } from '../../interaction/interaction-handler'; -import { TableWidgetRowSelectionModel } from './selections/table-widget-row-selection.model'; +import { TableWidgetRowInteractionModel } from './selections/table-widget-row-interaction.model'; import { TableWidgetColumnsService } from './services/table-widget-columns.service'; import { TableWidgetBaseModel } from './table-widget-base.model'; import { SpecificationBackedTableColumnDef, TableWidgetColumnModel } from './table-widget-column.model'; @@ -63,19 +63,33 @@ export class TableWidgetModel extends TableWidgetBaseModel { }) public selectionHandler?: InteractionHandler; + @ModelProperty({ + key: 'row-click-handlers', + displayName: 'Row click Handlers', + // tslint:disable-next-line: no-object-literal-type-assertion + type: { + key: ARRAY_PROPERTY.type, + subtype: { + key: ModelPropertyType.TYPE, + defaultModelClass: TableWidgetRowInteractionModel + } + } as ArrayPropertyTypeInstance + }) + public rowClickHandlers?: TableWidgetRowInteractionModel[]; + @ModelProperty({ key: 'row-selection-handlers', - displayName: 'Row selection Handlers', + displayName: 'Row selection handlers', // tslint:disable-next-line: no-object-literal-type-assertion type: { key: ARRAY_PROPERTY.type, subtype: { key: ModelPropertyType.TYPE, - defaultModelClass: TableWidgetRowSelectionModel + defaultModelClass: TableWidgetRowInteractionModel } } as ArrayPropertyTypeInstance }) - public rowSelectionHandlers?: TableWidgetRowSelectionModel[]; + public rowSelectionHandlers?: TableWidgetRowInteractionModel[]; @ModelProperty({ key: 'child-template', @@ -83,6 +97,12 @@ export class TableWidgetModel extends TableWidgetBaseModel { }) public childTemplate?: ModelJson; + @ModelProperty({ + key: 'custom-control-widget', + type: ModelTemplatePropertyType.TYPE + }) + public customControlModelJson?: ModelJson; + @ModelProperty({ key: 'fetchEditableColumns', displayName: 'Query for additional columns not provided', @@ -97,10 +117,33 @@ export class TableWidgetModel extends TableWidgetBaseModel { return this.api.getData>(); } - public getRowSelectionHandlers(_row: TableRow): TableWidgetRowSelectionModel[] { + public getRowClickHandlers(): TableWidgetRowInteractionModel[] { + return this.rowClickHandlers ?? []; + } + + public getRowSelectionHandlers(_row: TableRow): TableWidgetRowInteractionModel[] { return this.rowSelectionHandlers ?? []; } + public getSelectionMode(): TableSelectionMode { + return this.selectionMode; + } + + public isCustomControlPresent(): boolean { + return !!this.customControlModelJson; + } + + public getCustomControlWidgetModel(selectedRows?: TableRow[]): object | undefined { + if (this.customControlModelJson) { + const childModel = this.api.createChild(this.customControlModelJson, this); + this.api.setVariable('selectedRows', selectedRows, childModel); + + return childModel; + } + + return undefined; + } + public getColumns(scope?: string): Observable { const modelColumns: Observable = forkJoinSafeEmpty( this.columns.map(column => column.asTableColumnDef(scope)) diff --git a/projects/observability/src/shared/dashboard/widgets/table/table-widget.module.ts b/projects/observability/src/shared/dashboard/widgets/table/table-widget.module.ts index 1360ee236..8059c76a8 100644 --- a/projects/observability/src/shared/dashboard/widgets/table/table-widget.module.ts +++ b/projects/observability/src/shared/dashboard/widgets/table/table-widget.module.ts @@ -11,7 +11,7 @@ import { import { WidgetHeaderModel } from '@hypertrace/dashboards'; import { DashboardCoreModule } from '@hypertrace/hyperdash-angular'; import { TracingTableCellRendererModule } from '../../../components/table/tracing-table-cell-renderer.module'; -import { TableWidgetRowSelectionModel } from './selections/table-widget-row-selection.model'; +import { TableWidgetRowInteractionModel } from './selections/table-widget-row-interaction.model'; import { TableWidgetColumnModel } from './table-widget-column.model'; import { TableWidgetControlCheckboxOptionModel } from './table-widget-control-checkbox-option.model'; import { TableWidgetControlSelectOptionModel } from './table-widget-control-select-option.model'; @@ -27,7 +27,7 @@ import { TableWidgetModel } from './table-widget.model'; models: [ TableWidgetModel, TableWidgetColumnModel, - TableWidgetRowSelectionModel, + TableWidgetRowInteractionModel, TableWidgetControlCheckboxOptionModel, TableWidgetControlSelectOptionModel, WidgetHeaderModel,