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=" " 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;