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;

Reply via email to