This is an automated email from the ASF dual-hosted git repository.

zehnder pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new 6a5296f321 feat(#4247): Add asset infos to resource table (#4248)
6a5296f321 is described below

commit 6a5296f3213cd3d35f7de4fe3a354b772606badd
Author: Philipp Zehnder <[email protected]>
AuthorDate: Fri Mar 13 15:02:13 2026 +0100

    feat(#4247): Add asset infos to resource table (#4248)
---
 .../{ => sp-actions}/sp-table-actions.directive.ts |   0
 .../sp-table-multi-actions.directive.ts            |   0
 .../sp-table-asset-context.service.ts              | 178 +++++++++++
 .../components/sp-table/sp-table.component.html    | 340 ++++++++++++++++----
 .../components/sp-table/sp-table.component.scss    |  90 +++++-
 .../lib/components/sp-table/sp-table.component.ts  | 355 +++++++++++++++++++--
 .../src/lib/components/sp-table/sp-table.model.ts  |  63 ++++
 .../streampipes/shared-ui/src/public-api.ts        |   6 +-
 .../chart-overview-table.component.html            |   1 +
 .../chart-overview-table.component.ts              |   6 +
 .../existing-adapters.component.html               |   1 +
 .../existing-adapters.component.ts                 |   6 +
 .../dashboard-overview-table.component.html        |   1 +
 .../dashboard-overview-table.component.ts          |   6 +
 .../pipeline-overview.component.html               |   1 +
 .../pipeline-overview.component.ts                 |  10 +-
 16 files changed, 968 insertions(+), 96 deletions(-)

diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table-actions.directive.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-actions/sp-table-actions.directive.ts
similarity index 100%
rename from 
ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table-actions.directive.ts
rename to 
ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-actions/sp-table-actions.directive.ts
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table-multi-actions.directive.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-actions/sp-table-multi-actions.directive.ts
similarity index 100%
rename from 
ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table-multi-actions.directive.ts
rename to 
ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-actions/sp-table-multi-actions.directive.ts
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-asset-context/sp-table-asset-context.service.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-asset-context/sp-table-asset-context.service.ts
new file mode 100644
index 0000000000..368ba67faf
--- /dev/null
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-asset-context/sp-table-asset-context.service.ts
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { Injectable } from '@angular/core';
+import { SpAsset, SpLabel } from '@streampipes/platform-services';
+import { AssetBrowserData } from '../../asset-browser/asset-browser.model';
+import {
+    SpTableAssetContextValue,
+    SpTableResolvedAssetContext,
+} from '../sp-table.model';
+
+@Injectable({ providedIn: 'root' })
+export class SpTableAssetContextService {
+    buildAssetContextIndex(
+        assetData?: AssetBrowserData,
+    ): Map<string, Map<string, SpTableResolvedAssetContext>> {
+        const index = new Map<
+            string,
+            Map<string, SpTableResolvedAssetContext>
+        >();
+        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,
+                asset.assetId,
+                asset.assetName,
+                [],
+                [],
+                null,
+            ),
+        );
+
+        return index;
+    }
+
+    private collectAssetContexts(
+        asset: SpAsset,
+        index: Map<string, Map<string, SpTableResolvedAssetContext>>,
+        sitesById: Map<string, string>,
+        labelsById: Map<string, SpLabel>,
+        topLevelAssetId: string,
+        topLevelAssetLabel: string,
+        hierarchy: string[],
+        inheritedLabels: SpLabel[],
+        inheritedSiteLabel: string | null,
+    ): void {
+        const currentHierarchy = [...hierarchy, asset.assetName].filter(
+            Boolean,
+        );
+        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, SpTableResolvedAssetContext>();
+            const currentContext =
+                contextsByResource.get(link.resourceId) ??
+                new SpTableResolvedAssetContext();
+
+            currentContext.assets = this.uniqueBy(
+                [
+                    ...currentContext.assets,
+                    new SpTableAssetContextValue(
+                        topLevelAssetId,
+                        topLevelAssetLabel,
+                        currentHierarchy.join(' / '),
+                    ),
+                ],
+                item => item.id,
+            );
+            currentContext.sites = this.uniqueBy(
+                siteLabel
+                    ? [
+                          ...currentContext.sites,
+                          new SpTableAssetContextValue(
+                              asset.assetSite?.siteId ?? siteLabel,
+                              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,
+                topLevelAssetId,
+                topLevelAssetLabel,
+                currentHierarchy,
+                currentLabels,
+                siteLabel,
+            ),
+        );
+    }
+
+    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/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..7988ffbc0c 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
@@ -17,6 +17,26 @@
   -->
 
 <div fxLayout="column">
+    @if (shouldShowGroupingControls && !showSelectionCheckboxes) {
+        <div
+            class="grouping-toolbar grouping-toolbar--standalone"
+            fxLayout="row wrap"
+            fxLayoutAlign="space-between center"
+            fxLayoutGap="12px"
+        >
+            <div class="grouping-toolbar__spacer" fxFlex></div>
+            <div
+                class="grouping-toolbar__controls"
+                fxLayout="row wrap"
+                fxLayoutAlign="end center"
+                fxLayoutGap="12px"
+            >
+                <ng-container *ngTemplateOutlet="groupingControls">
+                </ng-container>
+            </div>
+        </div>
+    }
+
     @if (showSelectionCheckboxes) {
         <div
             class="selection-toolbar"
@@ -26,6 +46,7 @@
             fxLayoutGap="8px"
         >
             <div
+                class="selection-toolbar__left"
                 fxLayout="row wrap"
                 fxLayoutAlign="start center"
                 fxLayoutGap="8px"
@@ -56,16 +77,15 @@
                         {{ 'Select none' | translate }}
                     </button>
                 </div>
-            </div>
 
-            @if (hasMultiActionsToolbarControls()) {
-                <div fxLayout="row" fxLayoutGap="5px">
-                    @if (hasBuiltInMultiActionSelect()) {
-                        <sp-form-field
-                            [level]="3"
-                            [label]="multiActionsSelectLabel | translate"
-                            margin="0"
-                        >
+                @if (selectedRows.length && hasMultiActionsToolbarControls()) {
+                    <div
+                        class="selection-toolbar__actions-inline"
+                        fxLayout="row"
+                        fxLayoutAlign="start center"
+                        fxLayoutGap="5px"
+                    >
+                        @if (hasBuiltInMultiActionSelect()) {
                             <mat-form-field class="form-field-small">
                                 <mat-select
                                     data-cy="sp-table-multi-action-select"
@@ -100,21 +120,19 @@
                                     }
                                 </mat-select>
                             </mat-form-field>
-                        </sp-form-field>
-                    }
-
-                    @if (multiActionsTemplate) {
-                        <ng-container
-                            *ngTemplateOutlet="
-                                multiActionsTemplate;
-                                context: multiActionsContext
-                            "
-                        >
-                        </ng-container>
-                    }
+                        }
 
-                    @if (showMultiActionsExecuteButton) {
-                        <sp-form-field [level]="3" label="&nbsp;" margin="0">
+                        @if (multiActionsTemplate) {
+                            <ng-container
+                                *ngTemplateOutlet="
+                                    multiActionsTemplate;
+                                    context: multiActionsContext
+                                "
+                            >
+                            </ng-container>
+                        }
+
+                        @if (showMultiActionsExecuteButton) {
                             <button
                                 mat-flat-button
                                 data-cy="sp-table-multi-action-execute"
@@ -125,14 +143,65 @@
                             >
                                 {{ multiActionsExecuteLabel | translate }}
                             </button>
-                        </sp-form-field>
-                    }
+                        }
+                    </div>
+                }
+            </div>
+
+            @if (shouldShowGroupingControls) {
+                <div
+                    class="selection-toolbar__right"
+                    fxLayout="row"
+                    fxLayoutAlign="end center"
+                    fxLayoutGap="8px"
+                >
+                    <ng-container *ngTemplateOutlet="groupingControls">
+                    </ng-container>
                 </div>
             }
         </div>
     }
 
-    <table mat-table class="sp-table" [dataSource]="dataSource">
+    <ng-template #groupingControls>
+        @if (viewMode === 'grouped') {
+            <mat-form-field class="form-field-small grouping-toolbar__select">
+                <mat-select
+                    [value]="groupBy"
+                    (valueChange)="setGrouping($event)"
+                >
+                    <mat-option value="label">
+                        {{ 'Label' | translate }}
+                    </mat-option>
+                    <mat-option value="site">
+                        {{ 'Site' | translate }}
+                    </mat-option>
+                    <mat-option value="asset">
+                        {{ 'Asset' | translate }}
+                    </mat-option>
+                </mat-select>
+            </mat-form-field>
+        }
+
+        <mat-button-toggle-group
+            class="view-mode-toggle"
+            [value]="viewMode"
+            (change)="setViewMode($event.value)"
+        >
+            <mat-button-toggle value="grouped">
+                {{ 'Grouped' | translate }}
+            </mat-button-toggle>
+            <mat-button-toggle value="list">
+                {{ 'List' | translate }}
+            </mat-button-toggle>
+        </mat-button-toggle-group>
+    </ng-template>
+
+    <table
+        mat-table
+        class="sp-table"
+        [dataSource]="renderedDataSource"
+        [multiTemplateDataRows]="viewMode === 'grouped'"
+    >
         <ng-content></ng-content>
 
         @if (showSelectionCheckboxes) {
@@ -167,62 +236,98 @@
             </ng-container>
         }
 
+        @if (assetContextConfig) {
+            <ng-container [matColumnDef]="assetContextColumnId">
+                <th mat-header-cell *matHeaderCellDef>
+                    <span class="font-bold">{{
+                        'Asset Context' | translate
+                    }}</span>
+                </th>
+                <td mat-cell *matCellDef="let element">
+                    <ng-container
+                        *ngTemplateOutlet="
+                            assetContextCell;
+                            context: { $implicit: element }
+                        "
+                    >
+                    </ng-container>
+                </td>
+            </ng-container>
+        }
+
         @if (showActionsMenu) {
             <ng-container matColumnDef="actions">
                 <th mat-header-cell *matHeaderCellDef></th>
                 <td mat-cell *matCellDef="let element">
-                    <div fxLayoutAlign="end center">
-                        @if (featureCardId) {
-                            <button
-                                [matTooltip]="'Preview' | translate"
-                                mat-icon-button
-                                (click)="
-                                    openFeatureCard(element);
-                                    $event.stopPropagation()
-                                "
-                            >
-                                <mat-icon>preview</mat-icon>
-                            </button>
-                        }
+                    <ng-container
+                        *ngTemplateOutlet="
+                            actionsCell;
+                            context: { $implicit: element }
+                        "
+                    >
+                    </ng-container>
+                </td>
+            </ng-container>
+        }
+
+        <ng-container [matColumnDef]="groupHeaderColumnId">
+            <td
+                mat-cell
+                *matCellDef="let group"
+                class="grouped-table-cell"
+                [attr.colspan]="renderedColumns.length"
+            >
+                @if (viewMode === 'grouped') {
+                    <div
+                        class="grouped-table-header"
+                        fxLayout="row wrap"
+                        fxLayoutAlign="space-between center"
+                        fxLayoutGap="8px"
+                    >
                         <div
-                            [matMenuTriggerFor]="menu"
-                            #menuTrigger="matMenuTrigger"
-                            (mouseenter)="mouseEnter(menuTrigger)"
-                            (mouseleave)="mouseLeave(menuTrigger)"
+                            fxLayout="row wrap"
+                            fxLayoutAlign="start center"
+                            fxLayoutGap="8px"
                         >
-                            <button
-                                mat-icon-button
-                                [matMenuTriggerFor]="menu"
-                                #menuTrigger="matMenuTrigger"
-                                (click)="$event.stopPropagation()"
-                                [attr.data-cy]="'more-options'"
+                            @if (group.color) {
+                                <sp-label
+                                    size="small"
+                                    variant="soft"
+                                    [color]="group.color"
+                                    [labelText]="group.title"
+                                >
+                                </sp-label>
+                            } @else {
+                                <h4 class="grouped-table-title">
+                                    {{ group.title }}
+                                </h4>
+                            }
+                            <sp-label
+                                size="small"
+                                tone="neutral"
+                                variant="soft"
+                                [labelText]="group.count"
                             >
-                                <mat-icon>more_vert</mat-icon>
-                            </button>
+                            </sp-label>
                         </div>
-                        <mat-menu #menu="matMenu" [hasBackdrop]="false">
-                            <div
-                                (mouseenter)="mouseEnter(menuTrigger)"
-                                (mouseleave)="mouseLeave(menuTrigger)"
-                            >
-                                <ng-container
-                                    *ngTemplateOutlet="
-                                        actionsTemplate;
-                                        context: { $implicit: element }
-                                    "
-                                >
-                                </ng-container>
-                            </div>
-                        </mat-menu>
                     </div>
-                </td>
-            </ng-container>
-        }
+                }
+            </td>
+        </ng-container>
 
         <tr mat-header-row *matHeaderRowDef="renderedColumns"></tr>
         <tr
             mat-row
-            *matRowDef="let row; columns: renderedColumns"
+            *matRowDef="
+                let row;
+                columns: groupHeaderColumns;
+                when: isGroupHeaderRow
+            "
+            class="grouped-table-row grouped-table-row--header"
+        ></tr>
+        <tr
+            mat-row
+            *matRowDef="let row; columns: renderedColumns; when: isDataRow"
             (click)="rowClicked.emit(row)"
             [ngClass]="rowsClickable ? 'cursor-pointer' : ''"
         ></tr>
@@ -237,6 +342,7 @@
             </td>
         </tr>
     </table>
+
     <div fxFlex="100" fxLayoutAlign="end end" class="paginator-container">
         <mat-paginator
             #paginator
@@ -249,3 +355,99 @@
         </mat-paginator>
     </div>
 </div>
+
+<ng-template #actionsCell let-element>
+    <div fxLayoutAlign="end center">
+        @if (featureCardId) {
+            <button
+                [matTooltip]="'Preview' | translate"
+                mat-icon-button
+                (click)="openFeatureCard(element); $event.stopPropagation()"
+            >
+                <mat-icon>preview</mat-icon>
+            </button>
+        }
+        <div
+            [matMenuTriggerFor]="menu"
+            #menuTrigger="matMenuTrigger"
+            (mouseenter)="mouseEnter(menuTrigger)"
+            (mouseleave)="mouseLeave(menuTrigger)"
+        >
+            <button
+                mat-icon-button
+                [matMenuTriggerFor]="menu"
+                #menuTrigger="matMenuTrigger"
+                (click)="$event.stopPropagation()"
+                [attr.data-cy]="'more-options'"
+            >
+                <mat-icon>more_vert</mat-icon>
+            </button>
+        </div>
+        <mat-menu #menu="matMenu" [hasBackdrop]="false">
+            <div
+                (mouseenter)="mouseEnter(menuTrigger)"
+                (mouseleave)="mouseLeave(menuTrigger)"
+            >
+                <ng-container
+                    *ngTemplateOutlet="
+                        actionsTemplate;
+                        context: { $implicit: element }
+                    "
+                >
+                </ng-container>
+            </div>
+        </mat-menu>
+    </div>
+</ng-template>
+
+<ng-template #assetContextCell let-element>
+    @if (getAssetContext(element); as assetContext) {
+        <div class="asset-context-cell">
+            @if (
+                showGroupedSitesInAssetContext && assetContext.sites.length > 0
+            ) {
+                @for (site of assetContext.sites; track site.id) {
+                    <sp-label
+                        size="small"
+                        tone="info"
+                        variant="soft"
+                        [labelText]="site.label"
+                    >
+                    </sp-label>
+                }
+            }
+            @if (
+                showGroupedAssetsInAssetContext &&
+                assetContext.assets.length > 0
+            ) {
+                @for (asset of assetContext.assets; track asset.id) {
+                    <sp-label
+                        size="small"
+                        tone="neutral"
+                        variant="soft"
+                        [labelText]="asset.label"
+                        [matTooltip]="asset.tooltip"
+                    >
+                    </sp-label>
+                }
+            }
+            @if (
+                showGroupedLabelsInAssetContext &&
+                assetContext.labels.length > 0
+            ) {
+                @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>
+    }
+</ng-template>
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..28969295e4 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
@@ -22,6 +22,56 @@
 
 .selection-toolbar {
     border-bottom: 1px solid var(--color-bg-3);
+    margin-bottom: var(--space-md);
+    padding-bottom: var(--space-xs);
+}
+
+.selection-toolbar__left {
+    flex: 1 1 auto;
+}
+
+.grouping-toolbar__select {
+    width: 180px;
+}
+
+.grouping-toolbar {
+    padding: var(--space-sm) 0 var(--space-lg);
+}
+
+.grouping-toolbar--standalone {
+    margin-left: auto;
+}
+
+.grouping-toolbar__controls {
+    margin-left: auto;
+}
+
+.selection-toolbar__actions-inline {
+    margin-left: var(--space-xs);
+    align-items: center;
+}
+
+.selection-toolbar__right {
+    margin-left: auto;
+    align-self: center;
+}
+
+.view-mode-toggle {
+    align-self: center;
+}
+
+.selection-toolbar__actions-inline .mat-mdc-form-field,
+.grouping-toolbar__controls .mat-mdc-form-field {
+    margin-bottom: 0;
+}
+
+.selection-toolbar__actions-inline button[mat-flat-button],
+.grouping-toolbar__controls .view-mode-toggle {
+    align-self: center;
+}
+
+.selection-toolbar__actions {
+    margin-left: auto;
 }
 
 .mat-mdc-row:hover {
@@ -38,10 +88,46 @@
 }
 
 .right-column {
-    text-align: right; /* align contents inside cell */
-    margin-left: auto; /* push this column to the far right */
+    text-align: right;
+    margin-left: auto;
 }
 
 .checkbox-multi-select {
     width: 100px;
 }
+
+.asset-context-cell {
+    display: inline-flex;
+    flex-wrap: wrap;
+    align-items: center;
+    min-width: 220px;
+    padding: var(--space-2xs) 0;
+    gap: var(--space-sm);
+}
+
+.asset-context-empty {
+    color: var(--color-text-2);
+    font-size: var(--font-size-xs);
+}
+
+.grouped-table-row--header {
+    height: auto;
+}
+
+.grouped-table-cell {
+    padding: 0;
+    border-bottom: none;
+}
+
+.grouped-table-header {
+    padding: var(--space-md) var(--space-lg);
+    border-top: 1px solid var(--color-bg-3);
+    border-bottom: 1px solid var(--color-bg-3);
+    background: var(--color-bg-1);
+}
+
+.grouped-table-title {
+    margin: 0;
+    font-size: var(--font-size-md);
+    font-weight: var(--font-weight-bold);
+}
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..cb0d15a507 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,
@@ -50,9 +51,9 @@ import {
     MatTableDataSource,
 } from '@angular/material/table';
 import { MatPaginator, PageEvent } from '@angular/material/paginator';
-import { SpTableActionsDirective } from './sp-table-actions.directive';
+import { SpTableActionsDirective } from 
'./sp-actions/sp-table-actions.directive';
 import { MatMenu, MatMenuTrigger } from '@angular/material/menu';
-import { SpTableMultiActionsDirective } from 
'./sp-table-multi-actions.directive';
+import { SpTableMultiActionsDirective } from 
'./sp-actions/sp-table-multi-actions.directive';
 import { LocalStorageService } from 
'../../services/local-storage-settings.service';
 import { FeatureCardService } from '../feature-card-host/feature-card.service';
 import {
@@ -71,20 +72,41 @@ import { MatCheckbox } from '@angular/material/checkbox';
 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';
-
-export interface SpTableMultiActionOption {
-    value: string;
-    label: string;
-    icon?: string;
-    disabled?: boolean;
+import { SpAssetBrowserService } from '../asset-browser/asset-browser.service';
+import { SpLabelComponent } from '../sp-label/sp-label.component';
+import {
+    MatButtonToggle,
+    MatButtonToggleGroup,
+} from '@angular/material/button-toggle';
+import {
+    SpTableAssetContextConfig,
+    SpTableMultiActionExecuteEvent,
+    SpTableMultiActionOption,
+    SpTableResolvedAssetContext,
+} from './sp-table.model';
+import { SpTableAssetContextService } from 
'./sp-asset-context/sp-table-asset-context.service';
+
+type SpTableGroupViewMode = 'list' | 'grouped';
+type SpTableGroupingMode = 'label' | 'site' | 'asset';
+
+interface SpTableGroupedSection<T> {
+    id: string;
+    title: string;
+    color?: string;
+    count: number;
+    rows: T[];
 }
 
-export interface SpTableMultiActionExecuteEvent<T> {
-    selectedRows: T[];
-    action: string | null;
+interface SpTableGroupHeaderRow {
+    __spGroupHeader: true;
+    id: string;
+    title: string;
+    color?: string;
+    count: number;
 }
 
+type SpTableRenderedRow<T> = T | SpTableGroupHeaderRow;
+
 @Component({
     selector: 'sp-table',
     templateUrl: './sp-table.component.html',
@@ -108,6 +130,8 @@ export interface SpTableMultiActionExecuteEvent<T> {
         MatMenu,
         MatSelect,
         MatOption,
+        MatButtonToggleGroup,
+        MatButtonToggle,
         NgTemplateOutlet,
         MatHeaderRowDef,
         MatHeaderRow,
@@ -120,20 +144,24 @@ export interface SpTableMultiActionExecuteEvent<T> {
         MatPaginator,
         TranslatePipe,
         LayoutGapDirective,
-        FormFieldComponent,
+        SpLabelComponent,
     ],
 })
 export class SpTableComponent<T>
     implements AfterViewInit, AfterContentInit, OnChanges, OnDestroy
 {
     readonly selectionColumnId = 'spSelection';
+    readonly assetContextColumnId = 'assetContext';
+    readonly groupHeaderColumnId = 'spGroupHeader';
 
     @ContentChildren(MatHeaderRowDef) headerRowDefs: 
QueryList<MatHeaderRowDef>;
     @ContentChildren(MatRowDef) rowDefs: QueryList<MatRowDef<T>>;
     @ContentChildren(MatColumnDef) columnDefs: QueryList<MatColumnDef>;
     @ContentChild(MatNoDataRow) noDataRow: MatNoDataRow;
 
-    @ViewChild(MatTable, { static: true }) table: MatTable<T>;
+    @ViewChild(MatTable, { static: true }) table: MatTable<
+        SpTableRenderedRow<T>
+    >;
 
     @Input() columns: string[];
     @Input() rowsClickable = false;
@@ -146,6 +174,7 @@ export class SpTableComponent<T>
     @Input() multiActionOptions: SpTableMultiActionOption[] = [];
     @Input() featureCardId: string;
     @Input() resourceIdKey = 'elementId';
+    @Input() assetContextConfig?: SpTableAssetContextConfig;
 
     @Input() dataSource: MatTableDataSource<T>;
 
@@ -166,13 +195,24 @@ export class SpTableComponent<T>
     trigger: MatMenuTrigger | undefined = undefined;
     visiblePageRows: T[] = [];
     selectedMultiAction: string | null = null;
+    viewMode: SpTableGroupViewMode = 'list';
+    groupBy: SpTableGroupingMode = 'asset';
+    groupedSections: SpTableGroupedSection<T>[] = [];
 
     readonly selection = new SelectionModel<T>(true, []);
 
     private localStorageService = inject(LocalStorageService);
     private featureCardService = inject(FeatureCardService);
+    private assetBrowserService = inject(SpAssetBrowserService);
+    private assetContextService = inject(SpTableAssetContextService);
     private renderedDataSubscription?: Subscription;
+    private assetDataSubscription?: Subscription;
     private viewInitialized = false;
+    private assetContextIndex = new Map<
+        string,
+        Map<string, SpTableResolvedAssetContext>
+    >();
+    private compactLayout = false;
 
     readonly pageSize: Signal<number>;
 
@@ -181,6 +221,14 @@ export class SpTableComponent<T>
             'paginator-page-size',
             10,
         );
+        this.assetDataSubscription =
+            this.assetBrowserService.assetData$.subscribe(assetData => {
+                this.assetContextIndex =
+                    this.assetContextService.buildAssetContextIndex(assetData);
+                this.applyAssetContextSortingAccessor();
+                this.refreshRenderedRows();
+            });
+        this.updateCompactLayout();
     }
 
     ngAfterViewInit() {
@@ -196,7 +244,9 @@ export class SpTableComponent<T>
         this.headerRowDefs.forEach(headerRowDef =>
             this.table.addHeaderRowDef(headerRowDef),
         );
-        this.table.setNoDataRow(this.noDataRow);
+        if (this.noDataRow) {
+            this.table.setNoDataRow(this.noDataRow);
+        }
     }
 
     ngOnChanges(changes: SimpleChanges) {
@@ -221,10 +271,22 @@ export class SpTableComponent<T>
         if (changes['multiActionOptions']) {
             this.ensureValidSelectedMultiAction();
         }
+
+        if (changes['assetContextConfig']) {
+            this.updateCompactLayout();
+            this.applyAssetContextSortingAccessor();
+            this.refreshRenderedRows();
+        }
     }
 
     ngOnDestroy() {
         this.renderedDataSubscription?.unsubscribe();
+        this.assetDataSubscription?.unsubscribe();
+    }
+
+    @HostListener('window:resize')
+    onResize() {
+        this.updateCompactLayout();
     }
 
     mouseEnter(trigger) {
@@ -247,6 +309,9 @@ export class SpTableComponent<T>
 
     onPage(event: PageEvent) {
         this.localStorageService.set('paginator-page-size', event.pageSize);
+        if (this.viewMode === 'grouped') {
+            this.refreshRenderedRows();
+        }
     }
 
     openFeatureCard(element: T) {
@@ -257,7 +322,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 +335,57 @@ export class SpTableComponent<T>
         return [this.selectionColumnId, ...baseColumns];
     }
 
+    get groupHeaderColumns(): string[] {
+        return [this.groupHeaderColumnId];
+    }
+
+    get shouldShowGroupingControls(): boolean {
+        return !!this.assetContextConfig;
+    }
+
+    get renderedDataSource(): MatTableDataSource<T> | SpTableRenderedRow<T>[] {
+        return this.viewMode === 'grouped'
+            ? this.groupedSections.flatMap(section => [
+                  {
+                      __spGroupHeader: true as const,
+                      id: section.id,
+                      title: section.title,
+                      color: section.color,
+                      count: section.count,
+                  },
+                  ...section.rows,
+              ])
+            : this.dataSource;
+    }
+
+    get showGroupedLabelsInAssetContext(): boolean {
+        return this.viewMode !== 'grouped' || this.groupBy !== 'label';
+    }
+
+    get showGroupedSitesInAssetContext(): boolean {
+        return this.viewMode !== 'grouped' || this.groupBy !== 'site';
+    }
+
+    get showGroupedAssetsInAssetContext(): boolean {
+        return this.viewMode !== 'grouped' || this.groupBy !== 'asset';
+    }
+
+    getAssetContext(row: T): SpTableResolvedAssetContext | 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;
     }
@@ -354,8 +472,38 @@ export class SpTableComponent<T>
         );
     }
 
+    setViewMode(mode: SpTableGroupViewMode) {
+        if (mode === 'grouped') {
+            this.groupBy = 'asset';
+        }
+
+        if (this.viewMode === mode) {
+            this.refreshRenderedRows();
+            return;
+        }
+
+        this.viewMode = mode;
+        this.bindDataSource();
+        this.refreshRenderedRows();
+    }
+
+    setGrouping(mode: SpTableGroupingMode) {
+        if (this.groupBy === mode) {
+            return;
+        }
+
+        this.groupBy = mode;
+        this.refreshRenderedRows();
+    }
+
+    isGroupHeaderRow = (_: number, row: SpTableRenderedRow<T>) =>
+        this.hasGroupHeaderMarker(row);
+
+    isDataRow = (_: number, row: SpTableRenderedRow<T>) =>
+        !this.hasGroupHeaderMarker(row);
+
     private bindDataSource() {
-        if (!this.dataSource || !this.paginator) {
+        if (!this.dataSource) {
             return;
         }
 
@@ -363,13 +511,39 @@ export class SpTableComponent<T>
 
         this.renderedDataSubscription?.unsubscribe();
         this.renderedDataSubscription = this.dataSource.connect().subscribe({
-            next: rows => {
-                this.visiblePageRows = rows ?? [];
-                this.pruneSelection();
-            },
+            next: rows => this.updateRenderedState(rows ?? []),
         });
     }
 
+    private refreshRenderedRows() {
+        this.updateRenderedState(this.getCurrentPageRows(), false);
+    }
+
+    private getCurrentPageRows(): T[] {
+        const rows =
+            this.dataSource?.filteredData ?? this.dataSource?.data ?? [];
+        if (!this.paginator) {
+            return rows;
+        }
+
+        const pageSize = this.paginator.pageSize || this.pageSize();
+        const startIndex = this.paginator.pageIndex * pageSize;
+        return rows.slice(startIndex, startIndex + pageSize);
+    }
+
+    private updateRenderedState(rows: T[], pruneSelection = true) {
+        this.visiblePageRows = rows;
+        this.rebuildGroupedSections(rows);
+
+        if (pruneSelection) {
+            this.pruneSelection();
+        }
+
+        if (this.viewInitialized) {
+            this.table.renderRows();
+        }
+    }
+
     private pruneSelection() {
         if (!this.selection.hasValue() || !this.dataSource) {
             return;
@@ -439,4 +613,143 @@ 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 rebuildGroupedSections(rows: T[]) {
+        if (!this.assetContextConfig || this.viewMode !== 'grouped') {
+            this.groupedSections = [];
+            return;
+        }
+
+        const grouped = new Map<string, SpTableGroupedSection<T>>();
+
+        rows.forEach(row => {
+            this.resolveGroups(row).forEach(group => {
+                const current = grouped.get(group.id) ?? {
+                    id: group.id,
+                    title: group.title,
+                    color: group.color,
+                    count: 0,
+                    rows: [],
+                };
+                current.rows.push(row);
+                current.count += 1;
+                grouped.set(group.id, current);
+            });
+        });
+
+        this.groupedSections = Array.from(grouped.values())
+            .sort((left, right) => left.title.localeCompare(right.title))
+            .map(group => ({
+                ...group,
+                rows: [...group.rows],
+            }));
+    }
+
+    private resolveGroups(
+        row: T,
+    ): { id: string; title: string; color?: string }[] {
+        const assetContext = this.getAssetContext(row);
+
+        if (this.groupBy === 'label') {
+            return this.resolveLabelGroups(assetContext);
+        }
+
+        if (this.groupBy === 'site') {
+            return this.resolveSiteGroups(assetContext);
+        }
+
+        return this.resolveAssetGroups(assetContext);
+    }
+
+    private resolveLabelGroups(
+        assetContext?: SpTableResolvedAssetContext,
+    ): { id: string; title: string; color?: string }[] {
+        const labels = assetContext?.labels ?? [];
+        return labels.length
+            ? labels.map(label => ({
+                  id: `label:${label._id ?? label.label}`,
+                  title: label.label,
+                  color: label.color,
+              }))
+            : [this.createUnassignedGroup()];
+    }
+
+    private resolveSiteGroups(
+        assetContext?: SpTableResolvedAssetContext,
+    ): { id: string; title: string; color?: string }[] {
+        const sites = assetContext?.sites ?? [];
+        return sites.length
+            ? sites.map(site => ({
+                  id: `site:${site.id}`,
+                  title: site.label,
+              }))
+            : [this.createUnassignedGroup()];
+    }
+
+    private resolveAssetGroups(
+        assetContext?: SpTableResolvedAssetContext,
+    ): { id: string; title: string; color?: string }[] {
+        const assets = assetContext?.assets ?? [];
+        return assets.length
+            ? assets.map(asset => ({
+                  id: `asset:${asset.id}`,
+                  title: asset.label,
+              }))
+            : [this.createUnassignedGroup()];
+    }
+
+    private createUnassignedGroup(): { id: string; title: string } {
+        return { id: 'unassigned', title: 'Unassigned' };
+    }
+
+    private getAssetContextResourceId(
+        row: T,
+        config: SpTableAssetContextConfig,
+    ): string | undefined {
+        const key = config.resourceIdKey ?? this.resourceIdKey;
+        return (row as Record<string, string | undefined>)?.[key];
+    }
+
+    private hasGroupHeaderMarker(
+        row: SpTableRenderedRow<T>,
+    ): row is SpTableGroupHeaderRow {
+        return !!(row as SpTableGroupHeaderRow).__spGroupHeader;
+    }
 }
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.model.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.model.ts
new file mode 100644
index 0000000000..9ce38d0571
--- /dev/null
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.model.ts
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { SpLabel } from '@streampipes/platform-services';
+
+export interface SpTableMultiActionOption {
+    value: string;
+    label: string;
+    icon?: string;
+    disabled?: boolean;
+}
+
+export interface SpTableMultiActionExecuteEvent<T> {
+    selectedRows: T[];
+    action: string | null;
+}
+
+export interface SpTableAssetContextConfig {
+    resourceLinkType: string;
+    resourceIdKey?: string;
+    hideBelowWidth?: number;
+}
+
+export class SpTableAssetContextValue {
+    id: string;
+    label: string;
+    tooltip?: string;
+
+    constructor(id: string, label: string, tooltip?: string) {
+        this.id = id;
+        this.label = label;
+        this.tooltip = tooltip;
+    }
+}
+
+export class SpTableResolvedAssetContext {
+    assets: SpTableAssetContextValue[];
+    sites: SpTableAssetContextValue[];
+    labels: SpLabel[];
+    sortValue: string;
+
+    constructor() {
+        this.assets = [];
+        this.sites = [];
+        this.labels = [];
+        this.sortValue = '';
+    }
+}
diff --git a/ui/projects/streampipes/shared-ui/src/public-api.ts 
b/ui/projects/streampipes/shared-ui/src/public-api.ts
index 446a78381e..fb7d075f89 100644
--- a/ui/projects/streampipes/shared-ui/src/public-api.ts
+++ b/ui/projects/streampipes/shared-ui/src/public-api.ts
@@ -43,8 +43,10 @@ export * from 
'./lib/components/sp-exception-message/exception-details-dialog/ex
 export * from 
'./lib/components/sp-exception-message/exception-details/exception-details.component';
 export * from './lib/components/sp-label/sp-label.component';
 export * from './lib/components/sp-table/sp-table.component';
-export * from './lib/components/sp-table/sp-table-actions.directive';
-export * from './lib/components/sp-table/sp-table-multi-actions.directive';
+export * from 
'./lib/components/sp-table/sp-actions/sp-table-actions.directive';
+export * from 
'./lib/components/sp-table/sp-actions/sp-table-multi-actions.directive';
+export * from './lib/components/sp-table/sp-table.model';
+export * from 
'./lib/components/sp-table/sp-asset-context/sp-table-asset-context.service';
 export * from './lib/components/alert-banner/alert-banner.component';
 export * from './lib/components/time-selector/time-selector.model';
 export * from './lib/components/time-selector/time-range-selector.component';
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..77e36bacff 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,15 @@ export class ChartOverviewTableComponent implements OnInit {
     dataSource = new MatTableDataSource<DataExplorerWidgetModel>();
     displayedColumns: string[] = [
         'name',
+        'assetContext',
         'lastModified',
         'createdAt',
         'actions',
     ];
+    readonly assetContextConfig: SpTableAssetContextConfig = {
+        resourceLinkType: 'chart',
+        resourceIdKey: 'elementId',
+    };
     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..cba1c1f980 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,17 @@ export class ExistingAdaptersComponent implements OnInit, 
OnDestroy {
         'status',
         'start',
         'name',
+        'assetContext',
         'adapterBase',
         'lastModified',
         'messagesSent',
         'lastMessage',
         'actions',
     ];
+    readonly assetContextConfig: SpTableAssetContextConfig = {
+        resourceLinkType: 'adapter',
+        resourceIdKey: 'elementId',
+    };
 
     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..91cc6a56fd 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,15 @@ export class DashboardOverviewTableComponent implements 
OnInit, OnDestroy {
 
     displayedColumns: string[] = [
         'name',
+        'assetContext',
         'lastModified',
         'createdAt',
         'actions',
     ];
+    readonly assetContextConfig: SpTableAssetContextConfig = {
+        resourceLinkType: 'dashboard',
+        resourceIdKey: 'elementId',
+    };
     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 be16fc2814..2951379335 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,10 @@ export class PipelineOverviewComponent implements OnInit, 
OnDestroy {
     starting = false;
     stopping = false;
     hasPipelineWritePrivileges = false;
+    readonly assetContextConfig: SpTableAssetContextConfig = {
+        resourceLinkType: 'pipeline',
+        resourceIdKey: 'elementId',
+    };
     readonly bulkPipelineActionOptions: SpTableMultiActionOption[] = [
         { value: 'start', label: 'Start selected', icon: 'play_arrow' },
         { value: 'stop', label: 'Stop selected', icon: 'stop' },
@@ -125,7 +131,7 @@ export class PipelineOverviewComponent implements OnInit, 
OnDestroy {
     private dialogService = inject(DialogService);
 
     ngOnInit() {
-        this.userSub = this.currentUserService.user$.subscribe(user => {
+        this.userSub = this.currentUserService.user$.subscribe(() => {
             this.hasPipelineWritePrivileges = this.authService.hasRole(
                 UserPrivilege.PRIVILEGE_WRITE_PIPELINE,
             );
@@ -159,7 +165,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