This is an automated email from the ASF dual-hosted git repository. zehnder pushed a commit to branch add-asset-infos-to-resource-table in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 34a57d5f2db51ee9a6d56c8acbd35bbe057b68aa Author: Philipp Zehnder <[email protected]> AuthorDate: Wed Mar 11 15:43:13 2026 +0100 feat: First prototype to show asset context in sp-table --- .../components/sp-table/sp-table.component.html | 57 +++++ .../components/sp-table/sp-table.component.scss | 12 + .../lib/components/sp-table/sp-table.component.ts | 274 ++++++++++++++++++++- .../chart-overview-table.component.html | 1 + .../chart-overview-table.component.ts | 7 + .../existing-adapters.component.html | 1 + .../existing-adapters.component.ts | 7 + .../dashboard-overview-table.component.html | 1 + .../dashboard-overview-table.component.ts | 7 + .../pipeline-overview.component.html | 1 + .../pipeline-overview.component.ts | 9 +- 11 files changed, 375 insertions(+), 2 deletions(-) diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html index 6ff7d89f3c..5252adb559 100644 --- a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html +++ b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html @@ -167,6 +167,63 @@ </ng-container> } + @if (assetContextConfig) { + <ng-container [matColumnDef]="assetContextColumnId"> + <th mat-header-cell *matHeaderCellDef> + {{ assetContextColumnLabel | translate }} + </th> + <td mat-cell *matCellDef="let element"> + @if (getAssetContext(element); as assetContext) { + <div + class="asset-context-cell" + fxLayout="row" + fxLayoutAlign="start center" + fxLayoutGap="6px" + > + @for (site of assetContext.sites; track site.id) { + <sp-label + size="small" + tone="info" + variant="soft" + [labelText]="site.label" + > + </sp-label> + } + @for ( + asset of assetContext.assets; + track asset.id + ) { + <sp-label + size="small" + tone="neutral" + variant="soft" + [labelText]="asset.label" + [matTooltip]="asset.tooltip" + > + </sp-label> + } + @for ( + label of assetContext.labels; + track label._id ?? label.label + ) { + <sp-label + size="small" + variant="soft" + [color]="label.color" + [labelText]="label.label" + > + </sp-label> + } + </div> + } @else { + <span class="asset-context-empty">{{ + 'No asset link' | translate + }}</span> + } + </td> + </ng-container> + } + @if (showActionsMenu) { <ng-container matColumnDef="actions"> <th mat-header-cell *matHeaderCellDef></th> diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss index e58390be1f..f3f8e52aee 100644 --- a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss +++ b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss @@ -45,3 +45,15 @@ .checkbox-multi-select { width: 100px; } + +.asset-context-cell { + display: flex; + flex-wrap: wrap; + min-width: 220px; + padding: 6px 0; +} + +.asset-context-empty { + color: var(--color-text-2); + font-size: 12px; +} diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts index bdae3abb34..87b4095f23 100644 --- a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts +++ b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts @@ -23,6 +23,7 @@ import { ContentChild, ContentChildren, EventEmitter, + HostListener, inject, Input, OnChanges, @@ -72,6 +73,10 @@ import { MatFormField } from '@angular/material/form-field'; import { Subscription } from 'rxjs'; import { MatOption, MatSelect } from '@angular/material/select'; import { FormFieldComponent } from '../form-field/form-field.component'; +import { SpAssetBrowserService } from '../asset-browser/asset-browser.service'; +import { AssetBrowserData } from '../asset-browser/asset-browser.model'; +import { SpLabelComponent } from '../sp-label/sp-label.component'; +import { SpAsset, SpLabel } from '@streampipes/platform-services'; export interface SpTableMultiActionOption { value: string; @@ -85,6 +90,27 @@ export interface SpTableMultiActionExecuteEvent<T> { action: string | null; } +export interface SpTableAssetContextConfig { + resourceLinkType: string; + resourceIdKey?: string; + columnId?: string; + columnLabel?: string; + hideBelowWidth?: number; +} + +interface SpTableAssetContextValue { + id: string; + label: string; + tooltip?: string; +} + +interface SpTableAssetContext { + assets: SpTableAssetContextValue[]; + sites: SpTableAssetContextValue[]; + labels: SpLabel[]; + sortValue: string; +} + @Component({ selector: 'sp-table', templateUrl: './sp-table.component.html', @@ -121,12 +147,14 @@ export interface SpTableMultiActionExecuteEvent<T> { TranslatePipe, LayoutGapDirective, FormFieldComponent, + SpLabelComponent, ], }) export class SpTableComponent<T> implements AfterViewInit, AfterContentInit, OnChanges, OnDestroy { readonly selectionColumnId = 'spSelection'; + readonly defaultAssetContextColumnId = 'assetContext'; @ContentChildren(MatHeaderRowDef) headerRowDefs: QueryList<MatHeaderRowDef>; @ContentChildren(MatRowDef) rowDefs: QueryList<MatRowDef<T>>; @@ -146,6 +174,7 @@ export class SpTableComponent<T> @Input() multiActionOptions: SpTableMultiActionOption[] = []; @Input() featureCardId: string; @Input() resourceIdKey = 'elementId'; + @Input() assetContextConfig?: SpTableAssetContextConfig; @Input() dataSource: MatTableDataSource<T>; @@ -171,8 +200,15 @@ export class SpTableComponent<T> private localStorageService = inject(LocalStorageService); private featureCardService = inject(FeatureCardService); + private assetBrowserService = inject(SpAssetBrowserService); private renderedDataSubscription?: Subscription; + private assetDataSubscription?: Subscription; private viewInitialized = false; + private assetContextIndex = new Map< + string, + Map<string, SpTableAssetContext> + >(); + private compactLayout = false; readonly pageSize: Signal<number>; @@ -181,6 +217,12 @@ export class SpTableComponent<T> 'paginator-page-size', 10, ); + this.assetDataSubscription = + this.assetBrowserService.assetData$.subscribe(assetData => { + this.assetContextIndex = this.buildAssetContextIndex(assetData); + this.applyAssetContextSortingAccessor(); + }); + this.updateCompactLayout(); } ngAfterViewInit() { @@ -221,10 +263,21 @@ export class SpTableComponent<T> if (changes['multiActionOptions']) { this.ensureValidSelectedMultiAction(); } + + if (changes['assetContextConfig'] || changes['dataSource']) { + this.updateCompactLayout(); + this.applyAssetContextSortingAccessor(); + } } ngOnDestroy() { this.renderedDataSubscription?.unsubscribe(); + this.assetDataSubscription?.unsubscribe(); + } + + @HostListener('window:resize') + onResize() { + this.updateCompactLayout(); } mouseEnter(trigger) { @@ -257,7 +310,9 @@ export class SpTableComponent<T> } get renderedColumns(): string[] { - const baseColumns = this.columns ?? []; + const baseColumns = (this.columns ?? []).filter( + column => !this.shouldHideColumn(column), + ); if ( !this.showSelectionCheckboxes || baseColumns.includes(this.selectionColumnId) @@ -268,6 +323,33 @@ export class SpTableComponent<T> return [this.selectionColumnId, ...baseColumns]; } + get assetContextColumnId(): string { + return ( + this.assetContextConfig?.columnId ?? + this.defaultAssetContextColumnId + ); + } + + get assetContextColumnLabel(): string { + return this.assetContextConfig?.columnLabel ?? 'Asset Context'; + } + + getAssetContext(row: T): SpTableAssetContext | undefined { + const config = this.assetContextConfig; + if (!config) { + return undefined; + } + + const resourceId = this.getAssetContextResourceId(row, config); + if (!resourceId) { + return undefined; + } + + return this.assetContextIndex + .get(config.resourceLinkType) + ?.get(resourceId); + } + get selectedRows(): T[] { return this.selection.selected; } @@ -439,4 +521,194 @@ export class SpTableComponent<T> this.selectedMultiAction = null; this.multiActionSelectionChanged.emit(null); } + + private shouldHideColumn(column: string): boolean { + return ( + !!this.assetContextConfig && + column === this.assetContextColumnId && + this.compactLayout + ); + } + + private updateCompactLayout(): void { + const hideBelowWidth = this.assetContextConfig?.hideBelowWidth ?? 1200; + this.compactLayout = window.innerWidth < hideBelowWidth; + } + + private applyAssetContextSortingAccessor(): void { + if (!this.dataSource) { + return; + } + + const currentAccessor = + this.dataSource.sortingDataAccessor?.bind(this.dataSource) ?? + ((data: T, sortHeaderId: string) => + (data as Record<string, unknown>)?.[sortHeaderId] as + | string + | number); + + this.dataSource.sortingDataAccessor = (data, sortHeaderId) => { + if ( + this.assetContextConfig && + sortHeaderId === this.assetContextColumnId + ) { + return this.getAssetContext(data)?.sortValue ?? ''; + } + + return currentAccessor(data, sortHeaderId); + }; + } + + private buildAssetContextIndex( + assetData?: AssetBrowserData, + ): Map<string, Map<string, SpTableAssetContext>> { + const index = new Map<string, Map<string, SpTableAssetContext>>(); + if (!assetData) { + return index; + } + + const sitesById = new Map( + assetData.sites.map(site => [site._id, site.label]), + ); + const labelsById = new Map( + assetData.labels + .filter( + (label): label is SpLabel & { _id: string } => !!label._id, + ) + .map(label => [label._id, label]), + ); + + assetData.assets.forEach(asset => + this.collectAssetContexts( + asset, + index, + sitesById, + labelsById, + [], + [], + null, + ), + ); + + return index; + } + + private collectAssetContexts( + asset: SpAsset, + index: Map<string, Map<string, SpTableAssetContext>>, + sitesById: Map<string, string>, + labelsById: Map<string, SpLabel>, + hierarchy: string[], + inheritedLabels: SpLabel[], + inheritedSiteLabel: string | null, + ): void { + const currentHierarchy = [...hierarchy, asset.assetName].filter( + Boolean, + ); + const topLevelAsset = currentHierarchy[0] ?? asset.assetName; + const currentLabels = this.mergeLabels( + inheritedLabels, + (asset.labelIds ?? []) + .map(labelId => labelsById.get(labelId)) + .filter((label): label is SpLabel => !!label), + ); + const siteLabel = + (asset.assetSite?.siteId && + sitesById.get(asset.assetSite.siteId)) ?? + asset.assetSite?.area ?? + inheritedSiteLabel; + + (asset.assetLinks ?? []).forEach(link => { + const contextsByResource = + index.get(link.linkType) ?? + new Map<string, SpTableAssetContext>(); + const currentContext = contextsByResource.get(link.resourceId) ?? { + assets: [], + sites: [], + labels: [], + sortValue: '', + }; + + currentContext.assets = this.uniqueBy( + [ + ...currentContext.assets, + { + id: asset.assetId, + label: topLevelAsset, + tooltip: currentHierarchy.join(' / '), + }, + ], + item => item.id, + ); + currentContext.sites = this.uniqueBy( + siteLabel + ? [ + ...currentContext.sites, + { + id: asset.assetSite?.siteId ?? siteLabel, + label: siteLabel, + }, + ] + : currentContext.sites, + item => item.id, + ); + currentContext.labels = this.uniqueBy( + [...currentContext.labels, ...currentLabels], + label => label._id ?? label.label, + ); + currentContext.sortValue = [ + currentContext.sites.map(site => site.label).join(' '), + currentContext.assets + .map(assetItem => assetItem.label) + .join(' '), + currentContext.labels.map(label => label.label).join(' '), + ].join(' '); + + contextsByResource.set(link.resourceId, currentContext); + index.set(link.linkType, contextsByResource); + }); + + (asset.assets ?? []).forEach(child => + this.collectAssetContexts( + child, + index, + sitesById, + labelsById, + currentHierarchy, + currentLabels, + siteLabel, + ), + ); + } + + private getAssetContextResourceId( + row: T, + config: SpTableAssetContextConfig, + ): string | undefined { + const key = config.resourceIdKey ?? this.resourceIdKey; + return (row as Record<string, string | undefined>)?.[key]; + } + + private mergeLabels(base: SpLabel[], additional: SpLabel[]): SpLabel[] { + return this.uniqueBy( + [...base, ...additional], + label => label._id ?? label.label, + ); + } + + private uniqueBy<T>( + items: T[], + getKey: (item: T) => string | undefined, + ): T[] { + const seen = new Set<string>(); + return items.filter(item => { + const key = getKey(item); + if (!key || seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); + } } diff --git a/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html b/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html index fee14dfa0a..d1834177a6 100644 --- a/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html +++ b/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html @@ -25,6 +25,7 @@ fxFlex="100" [columns]="displayedColumns" [dataSource]="dataSource" + [assetContextConfig]="assetContextConfig" featureCardId="chart" [showActionsMenu]="true" [rowsClickable]="true" diff --git a/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.ts b/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.ts index efc6c5168c..8d638844bc 100644 --- a/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.ts +++ b/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.ts @@ -33,6 +33,7 @@ import { ConfirmDialogComponent, DateFormatService, SpAssetBrowserService, + SpTableAssetContextConfig, SpBasicHeaderTitleComponent, SpTableActionsDirective, SpTableComponent, @@ -88,10 +89,16 @@ export class ChartOverviewTableComponent implements OnInit { dataSource = new MatTableDataSource<DataExplorerWidgetModel>(); displayedColumns: string[] = [ 'name', + 'assetContext', 'lastModified', 'createdAt', 'actions', ]; + readonly assetContextConfig: SpTableAssetContextConfig = { + resourceLinkType: 'chart', + resourceIdKey: 'elementId', + columnLabel: 'Asset Context', + }; charts: DataExplorerWidgetModel[] = []; filteredCharts: DataExplorerWidgetModel[] = []; diff --git a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html index 4606e92016..91ba5b3cf1 100644 --- a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html +++ b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html @@ -79,6 +79,7 @@ resourceIdKey="elementId" [columns]="displayedColumns" [dataSource]="dataSource" + [assetContextConfig]="assetContextConfig" [showSelectionCheckboxes]="true" [showMultiActionsExecuteButton]="true" [multiActionOptions]="bulkAdapterActionOptions" diff --git a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.ts b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.ts index ed68dc74f7..ad0e61a2a8 100644 --- a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.ts +++ b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.ts @@ -45,6 +45,7 @@ import { SpBreadcrumbService, SpExceptionDetailsDialogComponent, SpLabelComponent, + SpTableAssetContextConfig, SpTableMultiActionExecuteEvent, SpTableMultiActionOption, SpTableActionsDirective, @@ -122,12 +123,18 @@ export class ExistingAdaptersComponent implements OnInit, OnDestroy { 'status', 'start', 'name', + 'assetContext', 'adapterBase', 'lastModified', 'messagesSent', 'lastMessage', 'actions', ]; + readonly assetContextConfig: SpTableAssetContextConfig = { + resourceLinkType: 'adapter', + resourceIdKey: 'elementId', + columnLabel: 'Asset Context', + }; dataSource: MatTableDataSource<AdapterDescription> = new MatTableDataSource(); diff --git a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html index c28ee3bbfe..97435c4079 100644 --- a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html +++ b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html @@ -24,6 +24,7 @@ <sp-table fxFlex="100" [columns]="displayedColumns" + [assetContextConfig]="assetContextConfig" featureCardId="dashboard" [showActionsMenu]="true" [rowsClickable]="true" diff --git a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts index 2d1e0ead40..726d2097c3 100644 --- a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts +++ b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts @@ -39,6 +39,7 @@ import { DialogService, PanelType, SpAssetBrowserService, + SpTableAssetContextConfig, SpBasicHeaderTitleComponent, SpTableActionsDirective, SpTableComponent, @@ -98,10 +99,16 @@ export class DashboardOverviewTableComponent implements OnInit, OnDestroy { displayedColumns: string[] = [ 'name', + 'assetContext', 'lastModified', 'createdAt', 'actions', ]; + readonly assetContextConfig: SpTableAssetContextConfig = { + resourceLinkType: 'dashboard', + resourceIdKey: 'elementId', + columnLabel: 'Asset Context', + }; dashboards: Dashboard[] = []; filteredDashboards: Dashboard[] = []; diff --git a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html index b627f15ccf..cb78aaa817 100644 --- a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html +++ b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html @@ -26,6 +26,7 @@ " featureCardId="pipeline" resourceIdKey="_id" + [assetContextConfig]="assetContextConfig" [showActionsMenu]="true" [rowsClickable]="true" (multiActionsExecute)="executeSelectedPipelineAction($event)" diff --git a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts index e37e1a43ab..591248d91b 100644 --- a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts +++ b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts @@ -45,6 +45,7 @@ import { DialogRef, DialogService, PanelType, + SpTableAssetContextConfig, SpTableMultiActionExecuteEvent, SpTableMultiActionOption, SpTableActionsDirective, @@ -101,6 +102,7 @@ export class PipelineOverviewComponent implements OnInit, OnDestroy { 'status', 'start', 'name', + 'assetContext', 'lastModified', 'actions', ]; @@ -111,6 +113,11 @@ export class PipelineOverviewComponent implements OnInit, OnDestroy { starting = false; stopping = false; hasPipelineWritePrivileges = false; + readonly assetContextConfig: SpTableAssetContextConfig = { + resourceLinkType: 'pipeline', + resourceIdKey: 'elementId', + columnLabel: 'Asset Context', + }; readonly bulkPipelineActionOptions: SpTableMultiActionOption[] = [ { value: 'start', label: 'Start selected', icon: 'play_arrow' }, { value: 'stop', label: 'Stop selected', icon: 'stop' }, @@ -159,7 +166,7 @@ export class PipelineOverviewComponent implements OnInit, OnDestroy { } addPipelinesToTable() { - this.dataSource.data = this._pipelines; + this.dataSource.data = this._pipelines ?? []; this.dataSource.sortingDataAccessor = (pipeline, column) => { if (column === 'status') { return pipeline.running;
