This is an automated email from the ASF dual-hosted git repository.
tenthe 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 66dc262b02 feat: refactored Asset Link Table (#4646)
66dc262b02 is described below
commit 66dc262b020e497aeaedc9157708c2e8f3b400df
Author: Jacqueline Höllig <[email protected]>
AuthorDate: Tue Jul 7 08:06:36 2026 +0200
feat: refactored Asset Link Table (#4646)
Co-authored-by: Philipp Zehnder <[email protected]>
---
ui/cypress/support/utils/asset/AssetBtns.ts | 2 +-
.../assets/dialog/base-asset-links.directive.ts | 49 ++-
ui/src/app/assets/dialog/base-asset-links.model.ts | 33 ++
.../asset-link-table.component.html | 226 ++++++++++++
.../asset-link-table.component.scss | 64 ++++
.../asset-link-table/asset-link-table.component.ts | 325 +++++++++++++++++
.../asset-link-table/asset-link-table.model.ts | 29 ++
.../manage-asset-links-dialog.component.html | 402 +--------------------
.../manage-asset-links-dialog.component.ts | 162 ++++++---
.../components/shortcuts/shortcuts.component.ts | 3 +-
10 files changed, 839 insertions(+), 456 deletions(-)
diff --git a/ui/cypress/support/utils/asset/AssetBtns.ts
b/ui/cypress/support/utils/asset/AssetBtns.ts
index ff867b497b..64b33e90e9 100644
--- a/ui/cypress/support/utils/asset/AssetBtns.ts
+++ b/ui/cypress/support/utils/asset/AssetBtns.ts
@@ -87,7 +87,7 @@ export class AssetBtns {
}
public static adapterCheckbox(adapterName: string) {
- return cy.dataCy('select-adapters-checkbox-' + adapterName, {
+ return cy.dataCy('select-adapter-checkbox-' + adapterName, {
timeout: 10000,
});
}
diff --git a/ui/src/app/assets/dialog/base-asset-links.directive.ts
b/ui/src/app/assets/dialog/base-asset-links.directive.ts
index e11781ba01..ebaab2e9f8 100644
--- a/ui/src/app/assets/dialog/base-asset-links.directive.ts
+++ b/ui/src/app/assets/dialog/base-asset-links.directive.ts
@@ -18,23 +18,24 @@
import { Directive, inject } from '@angular/core';
import {
- AdapterDescription,
AdapterService,
ChartService,
- Dashboard,
DashboardService,
- DataExplorerWidgetModel,
- DataLakeMeasure,
DatalakeRestService,
FileMetadata,
FilesService,
GenericStorageService,
- Pipeline,
PipelineElementService,
PipelineService,
SpDataStream,
} from '@streampipes/platform-services';
import { zip } from 'rxjs';
+import { map } from 'rxjs/operators';
+import {
+ AssetLinkChartResource,
+ AssetLinkMeasurementResource,
+ AssetLinkNamedResource,
+} from './base-asset-links.model';
@Directive()
export abstract class BaseAssetLinksDirective {
@@ -48,12 +49,12 @@ export abstract class BaseAssetLinksDirective {
protected filesService = inject(FilesService);
// Resources
- pipelines: Pipeline[];
- charts: DataExplorerWidgetModel[];
- dashboards: Dashboard[];
- dataLakeMeasures: DataLakeMeasure[];
+ pipelines: AssetLinkNamedResource[];
+ charts: AssetLinkChartResource[];
+ dashboards: AssetLinkNamedResource[];
+ dataLakeMeasures: AssetLinkMeasurementResource[];
dataSources: SpDataStream[];
- adapters: AdapterDescription[];
+ adapters: AssetLinkNamedResource[];
files: FileMetadata[];
allResources: any[] = [];
@@ -64,13 +65,31 @@ export abstract class BaseAssetLinksDirective {
getAllResources() {
zip(
- this.pipelineService.getPipelines(),
- this.chartService.getAllCharts(),
- this.dashboardService.getDashboards(),
+ this.pipelineService
+ .getPipelineSummary()
+ .pipe(map(summary => summary.resources)),
+ this.chartService.getChartSummary().pipe(
+ map(summary =>
+ summary.resources.map(chart => ({
+ elementId: chart.elementId,
+ name: chart.name,
+ baseAppearanceConfig: {
+ widgetTitle: chart.name,
+ },
+ })),
+ ),
+ ),
+ this.dashboardService
+ .getDashboardSummary()
+ .pipe(map(summary => summary.resources)),
this.pipelineElementService.getDataStreams(),
- this.dataLakeService.getAllMeasurementSeries(),
+ this.dataLakeService
+ .getMeasurementSummary()
+ .pipe(map(summary => summary.resources)),
this.filesService.getFileMetadata(),
- this.adapterService.getAdapters(),
+ this.adapterService
+ .getAdapterSummary()
+ .pipe(map(summary => summary.resources)),
).subscribe(
([
pipelines,
diff --git a/ui/src/app/assets/dialog/base-asset-links.model.ts
b/ui/src/app/assets/dialog/base-asset-links.model.ts
new file mode 100644
index 0000000000..e5db03c894
--- /dev/null
+++ b/ui/src/app/assets/dialog/base-asset-links.model.ts
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ *
+ */
+
+export interface AssetLinkNamedResource {
+ elementId: string;
+ name: string;
+}
+
+export interface AssetLinkChartResource extends AssetLinkNamedResource {
+ baseAppearanceConfig: {
+ widgetTitle: string;
+ };
+}
+
+export interface AssetLinkMeasurementResource {
+ elementId: string;
+ measureName: string;
+}
diff --git
a/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.component.html
b/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.component.html
new file mode 100644
index 0000000000..5b92b8924c
--- /dev/null
+++
b/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.component.html
@@ -0,0 +1,226 @@
+<!--
+~ 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.
+~
+-->
+
+<div fxLayout="column" fxLayoutGap="8px">
+ <div
+ fxLayout="row wrap"
+ fxLayoutAlign="end center"
+ fxLayoutGap="8px"
+ class="asset-link-table-toolbar"
+ >
+ <button
+ mat-flat-button
+ class="mat-basic"
+ data-cy="asset-link-table-select-all"
+ [disabled]="!hasUnselectedFilteredResources()"
+ (click)="selectAll()"
+ >
+ <mat-icon>queue</mat-icon>
+ {{ 'Select all' | translate }}
+ </button>
+ <button
+ mat-flat-button
+ class="mat-basic"
+ data-cy="asset-link-table-deselect-all"
+ [disabled]="!hasSelectedFilteredResources()"
+ (click)="deselectAll()"
+ >
+ <mat-icon>filter_none</mat-icon>
+ {{ 'Deselect all' | translate }}
+ </button>
+
+ <mat-form-field class="form-field-small asset-link-table-search">
+ <mat-icon matPrefix>search</mat-icon>
+ <input
+ #searchInput
+ matInput
+ [placeholder]="'Search' | translate"
+ [value]="searchTerm"
+ (input)="updateSearch($any($event.target).value)"
+ />
+ @if (searchTerm) {
+ <button
+ mat-icon-button
+ matSuffix
+ type="button"
+ [attr.aria-label]="'Clear search' | translate"
+ (click)="clearSearch()"
+ >
+ <mat-icon>close</mat-icon>
+ </button>
+ }
+ </mat-form-field>
+
+ <mat-button-toggle-group
+ [value]="viewMode"
+ (change)="updateViewMode($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>
+ </div>
+
+ <table
+ mat-table
+ matSort
+ [matSortActive]="sort.active"
+ [matSortDirection]="sort.direction"
+ class="sp-table"
+ [dataSource]="renderedRows"
+ [multiTemplateDataRows]="viewMode === 'grouped'"
+ (matSortChange)="updateSort($event)"
+ >
+ <ng-container matColumnDef="selected">
+ <th mat-header-cell *matHeaderCellDef></th>
+ <td mat-cell *matCellDef="let resource">
+ <mat-checkbox
+ color="accent"
+ [checked]="isResourceSelected(resource.resourceId)"
+ [attr.data-cy]="
+ 'select-' +
+ resource.resourceType
+ .toLowerCase()
+ .replace(/\s+/g, '-') +
+ '-checkbox-' +
+ resource.resourceName
+ "
+ (change)="updateSelection($event.checked, resource)"
+ (click)="$event.stopPropagation()"
+ >
+ </mat-checkbox>
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="type">
+ <th mat-header-cell mat-sort-header *matHeaderCellDef>
+ {{ 'Type' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let resource">
+ <div
+ fxLayout="column"
+ fxLayoutAlign="start start"
+ class="truncate"
+ >
+ <div
+ class="chip mt-sm mb-sm"
+ [ngStyle]="{ color: getLinkTypeColor(resource) }"
+ >
+ <mat-icon class="link-type-icon">
+ {{ getLinkType(resource)?.linkIcon || 'link' }}
+ </mat-icon>
+ <span class="text-sm">
+ {{
+ getLinkType(resource)?.linkLabel ||
+ resource.resourceType
+ }}
+ </span>
+ </div>
+ </div>
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="linkLabel">
+ <th mat-header-cell mat-sort-header *matHeaderCellDef>
+ {{ 'Name' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let resource">
+ <div
+ fxLayout="column"
+ fxLayoutAlign="start start"
+ class="truncate"
+ >
+ <span>{{ resource.resourceName }}</span>
+ </div>
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="groupHeader">
+ <td
+ mat-cell
+ *matCellDef="let group"
+ class="asset-link-table-group-header-cell"
+ [attr.colspan]="displayedColumns.length"
+ >
+ <div
+ fxLayout="row"
+ fxLayoutAlign="start center"
+ fxLayoutGap="8px"
+ class="asset-link-table-group-header"
+ >
+ <button
+ mat-icon-button
+ type="button"
+ class="asset-link-table-group-toggle"
+ (click)="toggleGroup(group.id)"
+ >
+ <mat-icon>
+ {{
+ group.collapsed
+ ? 'chevron_right'
+ : 'expand_more'
+ }}
+ </mat-icon>
+ </button>
+ <div
+ class="chip asset-link-table-group-chip"
+ [ngStyle]="{ color: group.color }"
+ >
+ <mat-icon class="link-type-icon">
+ {{ group.icon }}
+ </mat-icon>
+ <span class="text-sm">
+ {{ group.title | translate }}
+ </span>
+ </div>
+ <span class="asset-link-table-group-count text-sm">
+ {{ group.count }}
+ </span>
+ </div>
+ </td>
+ </ng-container>
+
+ <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+ <tr
+ mat-row
+ *matRowDef="
+ let row;
+ columns: groupHeaderColumns;
+ when: isGroupHeaderRow
+ "
+ class="asset-link-table-group-row"
+ ></tr>
+ <tr
+ mat-row
+ *matRowDef="let row; columns: displayedColumns; when: isDataRow"
+ ></tr>
+
+ <tr class="mat-row" *matNoDataRow>
+ <td
+ class="mat-cell"
+ data-cy="no-table-entries"
+ [colSpan]="displayedColumns.length"
+ >
+ {{ 'No entries available.' | translate }}
+ </td>
+ </tr>
+ </table>
+</div>
diff --git
a/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.component.scss
b/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.component.scss
new file mode 100644
index 0000000000..32b3cdb973
--- /dev/null
+++
b/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.component.scss
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ *
+ */
+
+.asset-link-table-toolbar {
+ width: 100%;
+}
+
+.asset-link-table-search {
+ min-width: 16.25rem;
+}
+
+.asset-link-table-group-header-cell {
+ padding: 0;
+}
+
+.asset-link-table-group-header {
+ background: var(--color-bg-1);
+ border-top: 1px solid var(--color-bg-2);
+ padding: var(--space-xs) var(--space-sm);
+}
+
+.asset-link-table-group-toggle {
+ flex: 0 0 auto;
+}
+
+.chip {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+ padding: var(--space-2xs) var(--space-sm);
+ border-radius: var(--radius-sm);
+ font-weight: 600;
+}
+
+.asset-link-table-group-chip {
+ padding-left: 0;
+}
+
+.asset-link-table-group-count {
+ border-radius: var(--radius-sm);
+ background: var(--color-bg-2);
+ padding: var(--space-2xs) var(--space-sm);
+}
+
+.link-type-icon {
+ font-size: var(--font-size-md);
+ height: var(--font-size-md);
+ width: var(--font-size-md);
+}
diff --git
a/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.component.ts
b/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.component.ts
new file mode 100644
index 0000000000..16a15e71f0
--- /dev/null
+++
b/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.component.ts
@@ -0,0 +1,325 @@
+/*
+ * 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 {
+ Component,
+ ElementRef,
+ EventEmitter,
+ HostListener,
+ Input,
+ Output,
+ ViewChild,
+} from '@angular/core';
+import {
+ AssetLinkResourceRow,
+ AssetLinkSelectionChange,
+} from './asset-link-table.model';
+import { AssetLinkType } from '@streampipes/platform-services';
+import {
+ MatCell,
+ MatCellDef,
+ MatColumnDef,
+ MatHeaderCell,
+ MatHeaderCellDef,
+ MatHeaderRow,
+ MatHeaderRowDef,
+ MatNoDataRow,
+ MatRow,
+ MatRowDef,
+ MatTable,
+} from '@angular/material/table';
+import { MatCheckbox } from '@angular/material/checkbox';
+import { TranslatePipe } from '@ngx-translate/core';
+import { MatSort, MatSortHeader, Sort } from '@angular/material/sort';
+import {
+ MatFormField,
+ MatPrefix,
+ MatSuffix,
+} from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { MatIcon } from '@angular/material/icon';
+import { MatButton, MatIconButton } from '@angular/material/button';
+import { NgStyle } from '@angular/common';
+import {
+ MatButtonToggle,
+ MatButtonToggleGroup,
+} from '@angular/material/button-toggle';
+import {
+ FlexDirective,
+ LayoutAlignDirective,
+ LayoutDirective,
+} from '@ngbracket/ngx-layout/flex';
+import { LayoutGapDirective } from '@ngbracket/ngx-layout';
+
+interface AssetLinkGroupHeaderRow {
+ groupHeader: true;
+ id: string;
+ title: string;
+ icon: string;
+ color: string;
+ count: number;
+ collapsed: boolean;
+}
+
+type AssetLinkTableRow = AssetLinkResourceRow | AssetLinkGroupHeaderRow;
+type AssetLinkViewMode = 'grouped' | 'list';
+
+@Component({
+ selector: 'sp-manage-asset-link-table',
+ templateUrl: './asset-link-table.component.html',
+ styleUrls: ['./asset-link-table.component.scss'],
+ imports: [
+ FlexDirective,
+ LayoutDirective,
+ LayoutAlignDirective,
+ LayoutGapDirective,
+ MatButtonToggle,
+ MatButtonToggleGroup,
+ MatButton,
+ MatCell,
+ MatCellDef,
+ MatCheckbox,
+ MatColumnDef,
+ MatFormField,
+ MatHeaderCell,
+ MatHeaderCellDef,
+ MatHeaderRow,
+ MatHeaderRowDef,
+ MatIcon,
+ MatIconButton,
+ MatInput,
+ MatNoDataRow,
+ MatPrefix,
+ MatRow,
+ MatRowDef,
+ MatSort,
+ MatSortHeader,
+ MatSuffix,
+ MatTable,
+ NgStyle,
+ TranslatePipe,
+ ],
+})
+export class AssetLinkTableComponent {
+ @ViewChild('searchInput')
+ searchInput?: ElementRef<HTMLInputElement>;
+
+ @Input()
+ resources: AssetLinkResourceRow[] = [];
+
+ @Input()
+ selectedResourceIds: string[] = [];
+
+ @Input()
+ assetLinkTypes: AssetLinkType[] = [];
+
+ @Output()
+ selectionChange = new EventEmitter<AssetLinkSelectionChange>();
+
+ displayedColumns = ['selected', 'type', 'linkLabel'];
+ groupHeaderColumns = ['groupHeader'];
+ searchTerm = '';
+ viewMode: AssetLinkViewMode = 'grouped';
+ sort: Sort = { active: 'resourceName', direction: 'asc' };
+ collapsedGroupIds: Set<string> = new Set<string>();
+
+ get renderedRows(): AssetLinkTableRow[] {
+ const rows = this.filteredAndSortedResources;
+
+ if (this.viewMode === 'list') {
+ return rows;
+ }
+
+ const groupedRows = new Map<string, AssetLinkResourceRow[]>();
+ rows.forEach(row => {
+ const groupRows = groupedRows.get(row.assetLinkType) ?? [];
+ groupRows.push(row);
+ groupedRows.set(row.assetLinkType, groupRows);
+ });
+
+ return Array.from(groupedRows.entries())
+ .sort((left, right) =>
+
left[1][0].resourceType.localeCompare(right[1][0].resourceType),
+ )
+ .flatMap(([id, groupRows]) => [
+ {
+ groupHeader: true as const,
+ id,
+ title:
+ this.getLinkType(groupRows[0])?.linkLabel ??
+ groupRows[0].resourceType,
+ icon: this.getLinkType(groupRows[0])?.linkIcon ?? 'link',
+ color: this.getLinkTypeColor(groupRows[0]),
+ count: groupRows.length,
+ collapsed: this.isGroupCollapsed(id),
+ },
+ ...(this.isGroupCollapsed(id) ? [] : groupRows),
+ ]);
+ }
+
+ isResourceSelected(resourceId: string): boolean {
+ return this.selectedResourceIds.includes(resourceId);
+ }
+
+ updateSelection(checked: boolean, resource: AssetLinkResourceRow): void {
+ this.selectionChange.emit({ checked, resource });
+ }
+
+ selectAll(): void {
+ this.filteredAndSortedResources
+ .filter(resource => !this.isResourceSelected(resource.resourceId))
+ .forEach(resource =>
+ this.selectionChange.emit({ checked: true, resource }),
+ );
+ }
+
+ deselectAll(): void {
+ this.filteredAndSortedResources
+ .filter(resource => this.isResourceSelected(resource.resourceId))
+ .forEach(resource =>
+ this.selectionChange.emit({ checked: false, resource }),
+ );
+ }
+
+ hasUnselectedFilteredResources(): boolean {
+ return this.filteredAndSortedResources.some(
+ resource => !this.isResourceSelected(resource.resourceId),
+ );
+ }
+
+ hasSelectedFilteredResources(): boolean {
+ return this.filteredAndSortedResources.some(resource =>
+ this.isResourceSelected(resource.resourceId),
+ );
+ }
+
+ updateSearch(searchTerm: string): void {
+ this.searchTerm = searchTerm;
+ }
+
+ clearSearch(): void {
+ this.searchTerm = '';
+ }
+
+ @HostListener('document:keydown', ['$event'])
+ handleGlobalKeydown(event: KeyboardEvent): void {
+ const key = event.key.toLowerCase();
+ const ctrl = event.ctrlKey || event.metaKey;
+
+ if (ctrl && key === 'f') {
+ this.focusSearchInput();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ updateViewMode(viewMode: AssetLinkViewMode): void {
+ this.viewMode = viewMode;
+ }
+
+ toggleGroup(groupId: string): void {
+ if (this.collapsedGroupIds.has(groupId)) {
+ this.collapsedGroupIds.delete(groupId);
+ } else {
+ this.collapsedGroupIds.add(groupId);
+ }
+ this.collapsedGroupIds = new Set(this.collapsedGroupIds);
+ }
+
+ isGroupCollapsed(groupId: string): boolean {
+ return this.collapsedGroupIds.has(groupId);
+ }
+
+ updateSort(sort: Sort): void {
+ this.sort = sort;
+ }
+
+ getLinkType(resource: AssetLinkResourceRow): AssetLinkType | undefined {
+ return this.assetLinkTypes.find(
+ linkType => linkType.linkType === resource.assetLinkType,
+ );
+ }
+
+ getLinkTypeColor(resource: AssetLinkResourceRow): string {
+ return this.getLinkType(resource)?.linkColor ?? 'var(--color-primary)';
+ }
+
+ isGroupHeaderRow = (_: number, row: AssetLinkTableRow) =>
+ this.hasGroupHeaderMarker(row);
+
+ isDataRow = (_: number, row: AssetLinkTableRow) =>
+ !this.hasGroupHeaderMarker(row);
+
+ private get filteredAndSortedResources(): AssetLinkResourceRow[] {
+ const normalizedSearchTerm =
this.searchTerm.trim().toLocaleLowerCase();
+ const rows = normalizedSearchTerm
+ ? this.resources.filter(resource =>
+ [resource.resourceName, resource.resourceType].some(value =>
+ value.toLocaleLowerCase().includes(normalizedSearchTerm),
+ ),
+ )
+ : [...this.resources];
+
+ return rows.sort((left, right) => this.compareRows(left, right));
+ }
+
+ private compareRows(
+ left: AssetLinkResourceRow,
+ right: AssetLinkResourceRow,
+ ): number {
+ const direction = this.sort.direction === 'desc' ? -1 : 1;
+
+ if (!this.sort.active || !this.sort.direction) {
+ return left.resourceName.localeCompare(right.resourceName);
+ }
+
+ const leftValue = this.getSortValue(left, this.sort.active);
+ const rightValue = this.getSortValue(right, this.sort.active);
+
+ return (
+ String(leftValue ?? '').localeCompare(String(rightValue ?? '')) *
+ direction
+ );
+ }
+
+ private getSortValue(
+ row: AssetLinkResourceRow,
+ column: string,
+ ): string | undefined {
+ if (column === 'type') {
+ return row.resourceType;
+ }
+
+ if (column === 'linkLabel') {
+ return row.resourceName;
+ }
+
+ return row[column as keyof AssetLinkResourceRow];
+ }
+
+ private hasGroupHeaderMarker(
+ row: AssetLinkTableRow,
+ ): row is AssetLinkGroupHeaderRow {
+ return !!(row as AssetLinkGroupHeaderRow).groupHeader;
+ }
+
+ private focusSearchInput(): void {
+ this.searchInput?.nativeElement.focus();
+ this.searchInput?.nativeElement.select();
+ }
+}
diff --git
a/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.model.ts
b/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.model.ts
new file mode 100644
index 0000000000..e0abbf19a6
--- /dev/null
+++
b/ui/src/app/assets/dialog/manage-asset-links/asset-link-table/asset-link-table.model.ts
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ *
+ */
+
+export interface AssetLinkResourceRow {
+ resourceId: string;
+ resourceName: string;
+ resourceType: string;
+ assetLinkType: string;
+}
+
+export interface AssetLinkSelectionChange {
+ checked: boolean;
+ resource: AssetLinkResourceRow;
+}
diff --git
a/ui/src/app/assets/dialog/manage-asset-links/manage-asset-links-dialog.component.html
b/ui/src/app/assets/dialog/manage-asset-links/manage-asset-links-dialog.component.html
index 44353b24ac..7bcd39cc0c 100644
---
a/ui/src/app/assets/dialog/manage-asset-links/manage-asset-links-dialog.component.html
+++
b/ui/src/app/assets/dialog/manage-asset-links/manage-asset-links-dialog.component.html
@@ -19,399 +19,15 @@
<div class="sp-dialog-container">
<div class="sp-dialog-content p-15">
@if (clonedAssetLinks) {
- <div fxFlex="100" fxLayout="column">
- <sp-split-section [level]="3" [title]="'Adapters' | translate">
- <div
- section-actions
- fxLayout="row"
- fxLayoutAlign="end center"
- fxFlex
- >
- <button
- mat-button
- mat-flat-button
- color="accent"
- class="small-button"
- (click)="
- selectAll(
- adapters,
- elementIdFunction,
- nameFunction,
- 'adapter'
- )
- "
- style="margin-right: 5px; margin-left: 15px"
- >
- <span>{{ 'Select All' | translate }}</span>
- </button>
- <button
- mat-button
- mat-flat-button
- class="mat-basic small-button"
- (click)="deselectAll(adapters, elementIdFunction)"
- style="margin-right: 10px; margin-left: 5px"
- >
- <span>{{ 'Deselect All' | translate }}</span>
- </button>
- </div>
- @for (element of adapters; track element) {
- <div fxLayout="row">
- <mat-checkbox
- color="accent"
- [checked]="linkSelected(element.elementId)"
- [attr.data-cy]="
- 'select-adapters-checkbox-' + element.name
- "
- (change)="
- selectLink(
- $event.checked,
- element.elementId,
- element.name,
- 'adapter'
- )
- "
- >
- {{ element.name }}
- </mat-checkbox>
- </div>
- }
- </sp-split-section>
- <sp-split-section [level]="3" [title]="'Charts' | translate">
- <div
- section-actions
- fxLayout="row"
- fxLayoutAlign="end center"
- fxFlex
- >
- <button
- mat-button
- mat-flat-button
- color="accent"
- class="small-button"
- (click)="
- selectAll(
- charts,
- elementIdFunction,
- widgetNameFunction,
- 'chart'
- )
- "
- style="margin-right: 5px; margin-left: 15px"
- >
- <span>{{ 'Select All' | translate }}</span>
- </button>
- <button
- mat-button
- mat-flat-button
- class="mat-basic small-button"
- (click)="deselectAll(charts, elementIdFunction)"
- style="margin-right: 10px; margin-left: 5px"
- >
- <span>{{ 'Deselect All' | translate }}</span>
- </button>
- </div>
- @for (element of charts; track element) {
- <div fxLayout="row">
- <mat-checkbox
- color="accent"
- [checked]="linkSelected(element.elementId)"
- (change)="
- selectLink(
- $event.checked,
- element.elementId,
- element.baseAppearanceConfig
- .widgetTitle,
- 'chart'
- )
- "
- >
- {{ element.baseAppearanceConfig.widgetTitle }}
- </mat-checkbox>
- </div>
- }
- </sp-split-section>
- <sp-split-section
- [level]="3"
- [title]="'Dashboards' | translate"
- >
- <div
- section-actions
- fxLayout="row"
- fxLayoutAlign="end center"
- fxFlex
- >
- <button
- mat-button
- mat-flat-button
- color="accent"
- class="small-button"
- (click)="
- selectAll(
- dashboards,
- elementIdFunction,
- nameFunction,
- 'dashboard'
- )
- "
- style="margin-right: 5px; margin-left: 15px"
- >
- <span>{{ 'Select All' | translate }}</span>
- </button>
- <button
- mat-button
- mat-flat-button
- class="mat-basic small-button"
- (click)="deselectAll(dashboards,
elementIdFunction)"
- style="margin-right: 10px; margin-left: 5px"
- >
- <span>{{ 'Deselect All' | translate }}</span>
- </button>
- </div>
-
- @for (element of dashboards; track element) {
- <div fxLayout="row">
- <mat-checkbox
- color="accent"
- [checked]="linkSelected(element.elementId)"
- (change)="
- selectLink(
- $event.checked,
- element.elementId,
- element.name,
- 'dashboard'
- )
- "
- >
- {{ element.name }}
- </mat-checkbox>
- </div>
- }
- </sp-split-section>
- <sp-split-section
- [level]="3"
- [title]="'Data Lake Storage' | translate"
- >
- <div
- section-actions
- fxLayout="row"
- fxLayoutAlign="end center"
- fxFlex
- >
- <button
- mat-button
- mat-flat-button
- color="accent"
- class="small-button"
- (click)="
- selectAll(
- dataLakeMeasures,
- elementIdFunction,
- measureNameFunction,
- 'measurement'
- )
- "
- style="margin-right: 5px; margin-left: 15px"
- >
- <span>{{ 'Select All' | translate }}</span>
- </button>
- <button
- mat-button
- mat-flat-button
- class="mat-basic small-button"
- (click)="
- deselectAll(dataLakeMeasures,
elementIdFunction)
- "
- style="margin-right: 10px; margin-left: 5px"
- >
- <span>{{ 'Deselect All' | translate }}</span>
- </button>
- </div>
- @for (element of dataLakeMeasures; track element) {
- <div fxLayout="row">
- <mat-checkbox
- color="accent"
- [checked]="linkSelected(element.elementId)"
- (change)="
- selectLink(
- $event.checked,
- element.elementId,
- element.measureName,
- 'measurement'
- )
- "
- >
- {{ element.measureName }}
- </mat-checkbox>
- </div>
- }
- </sp-split-section>
- <sp-split-section
- [level]="3"
- [title]="'Data Streams' | translate"
- >
- <div
- section-actions
- fxLayout="row"
- fxLayoutAlign="end center"
- fxFlex
- >
- <button
- mat-button
- mat-flat-button
- color="accent"
- class="small-button"
- (click)="
- selectAll(
- dataSources,
- elementIdFunction,
- nameFunction,
- 'data-source'
- )
- "
- style="margin-right: 5px; margin-left: 15px"
- >
- <span>{{ 'Select All' | translate }}</span>
- </button>
- <button
- mat-button
- mat-flat-button
- class="mat-basic small-button"
- (click)="
- deselectAll(dataSources, elementIdFunction)
- "
- style="margin-right: 10px; margin-left: 5px"
- >
- <span>{{ 'Deselect All' | translate }}</span>
- </button>
- </div>
- @for (source of dataSources; track source) {
- <div fxLayout="row">
- <mat-checkbox
- color="accent"
- [checked]="linkSelected(source.elementId)"
- [attr.data-cy]="
- 'select-data-stream-checkbox-' +
source.name
- "
- (change)="
- selectLink(
- $event.checked,
- source.elementId,
- source.name,
- 'data-source'
- )
- "
- >
- {{ source.name }}
- </mat-checkbox>
- </div>
- }
- </sp-split-section>
- <sp-split-section [level]="3" [title]="'Files' | translate">
- <div
- section-actions
- fxLayout="row"
- fxLayoutAlign="end center"
- fxFlex
- >
- <button
- mat-button
- mat-flat-button
- color="accent"
- class="small-button"
- (click)="
- selectAll(
- files,
- fileIdFunction,
- filenameFunction,
- 'file'
- )
- "
- style="margin-right: 5px; margin-left: 15px"
- >
- <span>{{ 'Select All' | translate }}</span>
- </button>
- <button
- mat-button
- mat-flat-button
- class="mat-basic small-button"
- (click)="deselectAll(files, fileIdFunction)"
- style="margin-right: 10px; margin-left: 5px"
- >
- <span>{{ 'Deselect All' | translate }}</span>
- </button>
- </div>
- @for (element of files; track element) {
- <div fxLayout="row">
- <mat-checkbox
- color="accent"
- [checked]="linkSelected(element.fileId)"
- (change)="
- selectLink(
- $event.checked,
- element.fileId,
- element.filename,
- 'file'
- )
- "
- >
- {{ element.filename }}
- </mat-checkbox>
- </div>
- }
- </sp-split-section>
- <sp-split-section [level]="3" [title]="'Pipelines' |
translate">
- <div
- section-actions
- fxLayout="row"
- fxLayoutAlign="end center"
- fxFlex
- >
- <button
- mat-button
- mat-flat-button
- color="accent"
- class="small-button"
- (click)="
- selectAll(
- pipelines,
- elementIdFunction,
- nameFunction,
- 'pipeline'
- )
- "
- style="margin-right: 5px; margin-left: 15px"
- >
- <span>{{ 'Select All' | translate }}</span>
- </button>
- <button
- mat-button
- mat-flat-button
- class="mat-basic small-button"
- (click)="deselectAll(pipelines, elementIdFunction)"
- style="margin-right: 10px; margin-left: 5px"
- >
- <span>{{ 'Deselect All' | translate }}</span>
- </button>
- </div>
- @for (pipeline of pipelines; track pipeline) {
- <div fxLayout="row">
- <mat-checkbox
- color="accent"
- [checked]="linkSelected(pipeline.elementId)"
- (change)="
- selectLink(
- $event.checked,
- pipeline.elementId,
- pipeline.name,
- 'pipeline'
- )
- "
- >{{ pipeline.name }}
- </mat-checkbox>
- </div>
- }
- </sp-split-section>
- </div>
+ <sp-manage-asset-link-table
+ fxFlex="100"
+ [resources]="resourceRows"
+ [selectedResourceIds]="selectedResourceIds"
+ [assetLinkTypes]="assetLinkTypes"
+ (selectionChange)="selectLink($event)"
+ data-cy="manage-asset-links-table"
+ >
+ </sp-manage-asset-link-table>
}
</div>
<mat-divider></mat-divider>
diff --git
a/ui/src/app/assets/dialog/manage-asset-links/manage-asset-links-dialog.component.ts
b/ui/src/app/assets/dialog/manage-asset-links/manage-asset-links-dialog.component.ts
index 57d181302a..ddf8d37c14 100644
---
a/ui/src/app/assets/dialog/manage-asset-links/manage-asset-links-dialog.component.ts
+++
b/ui/src/app/assets/dialog/manage-asset-links/manage-asset-links-dialog.component.ts
@@ -16,8 +16,8 @@
*
*/
-import { Component, Input, OnInit, inject } from '@angular/core';
-import { DialogRef, SplitSectionComponent } from '@streampipes/shared-ui';
+import { Component, HostListener, Input, OnInit, inject } from '@angular/core';
+import { DialogRef } from '@streampipes/shared-ui';
import { AssetLink, AssetLinkType } from '@streampipes/platform-services';
import { BaseAssetLinksDirective } from '../base-asset-links.directive';
import {
@@ -26,9 +26,13 @@ import {
LayoutDirective,
} from '@ngbracket/ngx-layout/flex';
import { MatButton } from '@angular/material/button';
-import { MatCheckbox } from '@angular/material/checkbox';
import { MatDivider } from '@angular/material/divider';
import { TranslatePipe } from '@ngx-translate/core';
+import {
+ AssetLinkResourceRow,
+ AssetLinkSelectionChange,
+} from './asset-link-table/asset-link-table.model';
+import { AssetLinkTableComponent } from
'./asset-link-table/asset-link-table.component';
@Component({
selector: 'sp-manage-asset-links-dialog-component',
@@ -36,11 +40,10 @@ import { TranslatePipe } from '@ngx-translate/core';
imports: [
FlexDirective,
LayoutDirective,
- SplitSectionComponent,
LayoutAlignDirective,
MatButton,
- MatCheckbox,
MatDivider,
+ AssetLinkTableComponent,
TranslatePipe,
],
})
@@ -59,12 +62,7 @@ export class SpManageAssetLinksDialogComponent
clonedAssetLinks: AssetLink[] = [];
- elementIdFunction = el => el.elementId;
- fileIdFunction = el => el.fileId;
- nameFunction = el => el.name;
- filenameFunction = el => el.filename;
- measureNameFunction = el => el.measureName;
- widgetNameFunction = el => el.baseAppearanceConfig.widgetTitle;
+ resourceRows: AssetLinkResourceRow[] = [];
ngOnInit(): void {
super.onInit();
@@ -75,6 +73,10 @@ export class SpManageAssetLinksDialogComponent
];
}
+ get selectedResourceIds(): string[] {
+ return this.clonedAssetLinks.map(assetLink => assetLink.resourceId);
+ }
+
cancel(): void {
this.dialogRef.close();
}
@@ -84,7 +86,82 @@ export class SpManageAssetLinksDialogComponent
this.dialogRef.close(this.assetLinks);
}
- afterResourcesLoaded(): void {}
+ @HostListener('document:keydown', ['$event'])
+ handleGlobalKeydown(event: KeyboardEvent): void {
+ const key = event.key.toLowerCase();
+ const ctrl = event.ctrlKey || event.metaKey;
+
+ if (key === 'escape') {
+ this.cancel();
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (ctrl && key === 's') {
+ this.store();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ afterResourcesLoaded(): void {
+ this.resourceRows = [
+ ...this.adapters.map(adapter =>
+ this.makeResourceRow(
+ adapter.elementId,
+ adapter.name,
+ 'Adapter',
+ 'adapter',
+ ),
+ ),
+ ...this.charts.map(chart =>
+ this.makeResourceRow(
+ chart.elementId,
+ chart.baseAppearanceConfig.widgetTitle,
+ 'Chart',
+ 'chart',
+ ),
+ ),
+ ...this.dashboards.map(dashboard =>
+ this.makeResourceRow(
+ dashboard.elementId,
+ dashboard.name,
+ 'Dashboard',
+ 'dashboard',
+ ),
+ ),
+ ...this.dataLakeMeasures.map(measure =>
+ this.makeResourceRow(
+ measure.elementId,
+ measure.measureName,
+ 'Data Lake Storage',
+ 'measurement',
+ ),
+ ),
+ ...this.dataSources.map(source =>
+ this.makeResourceRow(
+ source.elementId,
+ source.name,
+ 'Data Stream',
+ 'data-source',
+ ),
+ ),
+ ...this.files.map(file =>
+ this.makeResourceRow(
+ file.fileId,
+ file.filename,
+ 'File',
+ 'file',
+ ),
+ ),
+ ...this.pipelines.map(pipeline =>
+ this.makeResourceRow(
+ pipeline.elementId,
+ pipeline.name,
+ 'Pipeline',
+ 'pipeline',
+ ),
+ ),
+ ];
+ }
linkSelected(resourceId: string): boolean {
return (
@@ -93,21 +170,27 @@ export class SpManageAssetLinksDialogComponent
);
}
- selectLink(
- checked: boolean,
- resourceId: string,
- label: string,
- assetLinkType: string,
- ): void {
- if (checked) {
+ selectLink(event: AssetLinkSelectionChange): void {
+ const resource = event.resource;
+ if (event.checked) {
+ if (this.linkSelected(resource.resourceId)) {
+ return;
+ }
+
this.clonedAssetLinks.push(
- this.makeLink(resourceId, label, assetLinkType),
+ this.makeLink(
+ resource.resourceId,
+ resource.resourceName,
+ resource.assetLinkType,
+ ),
);
} else {
const index = this.clonedAssetLinks.findIndex(
- al => al.resourceId === resourceId,
+ al => al.resourceId === resource.resourceId,
);
- this.clonedAssetLinks.splice(index, 1);
+ if (index > -1) {
+ this.clonedAssetLinks.splice(index, 1);
+ }
}
}
@@ -129,30 +212,17 @@ export class SpManageAssetLinksDialogComponent
};
}
- selectAll(
- elements: any[],
- idFunction: any,
- nameFunction: any,
+ private makeResourceRow(
+ resourceId: string,
+ resourceName: string,
+ resourceType: string,
assetLinkType: string,
- ): void {
- elements.forEach(el => {
- const id = idFunction(el);
- const elementName = nameFunction(el);
- if (!this.linkSelected(id)) {
- this.selectLink(true, id, elementName, assetLinkType);
- }
- });
- }
-
- deselectAll(elements: any[], idFunction: any): void {
- elements.forEach(el => {
- const id = idFunction(el);
- const index = this.clonedAssetLinks.findIndex(
- al => al.resourceId === id,
- );
- if (index > -1) {
- this.clonedAssetLinks.splice(index, 1);
- }
- });
+ ): AssetLinkResourceRow {
+ return {
+ resourceId,
+ resourceName,
+ resourceType,
+ assetLinkType,
+ };
}
}
diff --git a/ui/src/app/help/components/shortcuts/shortcuts.component.ts
b/ui/src/app/help/components/shortcuts/shortcuts.component.ts
index d48e04f8be..cdd21513f2 100644
--- a/ui/src/app/help/components/shortcuts/shortcuts.component.ts
+++ b/ui/src/app/help/components/shortcuts/shortcuts.component.ts
@@ -30,7 +30,8 @@ interface ShortcutDefinition {
const SHORTCUT_TRANSLATION_KEYS = {
title: 'Shortcuts',
- saveContext: "'Ctrl/Cmd + S' in chart/dashboard/pipeline edit view",
+ saveContext:
+ "'Ctrl/Cmd + S' in chart/dashboard/pipeline edit view or asset link
dialog",
saveDescription: 'Saves the current state',
editContext: "'E' in dashboard/pipeline panel",
editDescription: 'Enters edit mode.',