This is an automated email from the ASF dual-hosted git repository. dominikriemer pushed a commit to branch add-search-to-overview-tables in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 9d4cf9d37235f00eedf91b6c50a66b6ba1b34b9f Author: Dominik Riemer <[email protected]> AuthorDate: Sat Jun 13 11:09:54 2026 +0200 feat: Enable search in overview tables --- ui/deployment/i18n/de.json | 2 +- ui/deployment/i18n/en.json | 2 +- ui/deployment/i18n/pl.json | 2 +- .../components/sp-table/sp-table.component.html | 55 +++++++++++-- .../components/sp-table/sp-table.component.scss | 16 +++- .../lib/components/sp-table/sp-table.component.ts | 91 +++++++++++++++++++++- .../src/lib/components/sp-table/sp-table.model.ts | 6 ++ .../asset-overview/asset-overview.component.html | 5 ++ .../chart-overview-table.component.html | 1 + .../existing-adapters.component.html | 10 +-- .../existing-adapters.component.ts | 21 +---- .../dashboard-overview-table.component.html | 4 + .../datalake-configuration.component.html | 4 + .../pipeline-overview.component.html | 4 + 14 files changed, 186 insertions(+), 37 deletions(-) diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json index b861858a15..be13f060a4 100644 --- a/ui/deployment/i18n/de.json +++ b/ui/deployment/i18n/de.json @@ -190,6 +190,7 @@ "Clear": "Zurücksetzen", "Clear & use simple filters": "Löschen und einfache Filter verwenden", "Clear Assembly Area": "Pipeline-Bereich leeren", + "Clear search": "Suche löschen", "Clear selection & reload": "Auswahl löschen & neu laden", "Click to add label": "Klicken, um Label hinzuzufügen", "Click to load": "Klicken zum Laden", @@ -339,7 +340,6 @@ "Dimension": "Dimension", "Disable live preview": "Live-Vorschau deaktivieren", "Disable script": "Skript deaktivieren", - "Discard": "Verwerfen", "Discard changes": "Änderungen verwerfen", "Display Name": "Anzeigename", "Display an image": "Ein Bild anzeigen", diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json index 175bf2e9d3..81888abf2b 100644 --- a/ui/deployment/i18n/en.json +++ b/ui/deployment/i18n/en.json @@ -190,6 +190,7 @@ "Clear": null, "Clear & use simple filters": null, "Clear Assembly Area": null, + "Clear search": null, "Clear selection & reload": null, "Click to add label": null, "Click to load": null, @@ -339,7 +340,6 @@ "Dimension": null, "Disable live preview": null, "Disable script": null, - "Discard": null, "Discard changes": null, "Display Name": null, "Display an image": null, diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json index 7bbc0436e1..c888b0c757 100644 --- a/ui/deployment/i18n/pl.json +++ b/ui/deployment/i18n/pl.json @@ -190,6 +190,7 @@ "Clear": "Wyczyść", "Clear & use simple filters": "Wyczyść i użyj prostych filtrów", "Clear Assembly Area": "Wyczyść obszar montażu", + "Clear search": "Wyczyść wyszukiwanie", "Clear selection & reload": "Wyczyść wybór i przeładuj", "Click to add label": "Kliknij, aby dodać etykietę", "Click to load": "Kliknij, aby wczytać", @@ -339,7 +340,6 @@ "Dimension": "Wymiar", "Disable live preview": "Wyłącz podgląd na żywo", "Disable script": "Wyłącz skrypt", - "Discard": "Odrzuć", "Discard changes": "Odrzuć zmiany", "Display Name": "Nazwa wyświetlana", "Display an image": "Wyświetl obraz", 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 540a4be3d9..01dbff145d 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,7 +17,10 @@ --> <div fxLayout="column"> - @if (shouldShowGroupingControls && !showSelectionCheckboxes) { + @if ( + (shouldShowGroupingControls || shouldShowNameSearch) && + !showSelectionCheckboxes + ) { <div class="grouping-toolbar grouping-toolbar--standalone" fxLayout="row wrap" @@ -31,8 +34,14 @@ fxLayoutAlign="end center" fxLayoutGap="12px" > - <ng-container *ngTemplateOutlet="groupingControls"> - </ng-container> + @if (shouldShowNameSearch) { + <ng-container *ngTemplateOutlet="nameSearchField"> + </ng-container> + } + @if (shouldShowGroupingControls) { + <ng-container *ngTemplateOutlet="groupingControls"> + </ng-container> + } </div> </div> } @@ -148,20 +157,52 @@ } </div> - @if (shouldShowGroupingControls) { + @if (shouldShowGroupingControls || shouldShowNameSearch) { <div class="selection-toolbar__right" - fxLayout="row" + fxLayout="row wrap" fxLayoutAlign="end center" fxLayoutGap="8px" > - <ng-container *ngTemplateOutlet="groupingControls"> - </ng-container> + @if (shouldShowNameSearch) { + <ng-container *ngTemplateOutlet="nameSearchField"> + </ng-container> + } + @if (shouldShowGroupingControls) { + <ng-container *ngTemplateOutlet="groupingControls"> + </ng-container> + } </div> } </div> } + <ng-template #nameSearchField> + <mat-form-field class="form-field-small table-search-field"> + <mat-icon matPrefix>search</mat-icon> + <input + matInput + [placeholder]=" + nameSearchConfig?.placeholder ?? ('Search' | translate) + " + [value]="nameSearchTerm" + (input)="onNameSearchInput($any($event.target).value)" + /> + @if (nameSearchTerm) { + <button + mat-icon-button + matSuffix + type="button" + class="table-search-field__clear" + [attr.aria-label]="'Clear search' | translate" + (click)="clearNameSearch()" + > + <mat-icon>close</mat-icon> + </button> + } + </mat-form-field> + </ng-template> + <ng-template #groupingControls> @if (viewMode === 'grouped') { <mat-form-field class="form-field-small grouping-toolbar__select"> 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 28969295e4..3d122a76ca 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 @@ -46,6 +46,10 @@ margin-left: auto; } +.grouping-toolbar__left { + flex: 1 1 auto; +} + .selection-toolbar__actions-inline { margin-left: var(--space-xs); align-items: center; @@ -61,7 +65,9 @@ } .selection-toolbar__actions-inline .mat-mdc-form-field, -.grouping-toolbar__controls .mat-mdc-form-field { +.grouping-toolbar__controls .mat-mdc-form-field, +.selection-toolbar__left .mat-mdc-form-field, +.grouping-toolbar__left .mat-mdc-form-field { margin-bottom: 0; } @@ -74,6 +80,14 @@ margin-left: auto; } +.table-search-field { + min-width: 220px; +} + +.table-search-field__clear { + color: var(--color-text-2); +} + .mat-mdc-row:hover { background-color: var(--color-bg-1); } 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 379b0c8b9e..d2a5cae146 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 @@ -69,7 +69,12 @@ import { NgClass, NgTemplateOutlet } from '@angular/common'; import { ClassDirective } from '@ngbracket/ngx-layout/extended'; import { TranslatePipe } from '@ngx-translate/core'; import { MatCheckbox } from '@angular/material/checkbox'; -import { MatFormField } from '@angular/material/form-field'; +import { + MatFormField, + MatPrefix, + MatSuffix, +} from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; import { Subscription } from 'rxjs'; import { MatOption, MatSelect } from '@angular/material/select'; import { SpAssetBrowserService } from '../asset-browser/asset-browser.service'; @@ -82,6 +87,7 @@ import { SpTableAssetContextConfig, SpTableMultiActionExecuteEvent, SpTableMultiActionOption, + SpTableNameSearchConfig, SpTableResolvedAssetContext, } from './sp-table.model'; import { SpTableAssetContextService } from './sp-asset-context/sp-table-asset-context.service'; @@ -126,6 +132,9 @@ type SpTableRenderedRow<T> = T | SpTableGroupHeaderRow; MatIcon, MatCheckbox, MatFormField, + MatPrefix, + MatSuffix, + MatInput, MatMenuTrigger, MatMenu, MatSelect, @@ -175,6 +184,7 @@ export class SpTableComponent<T> @Input() featureCardId: string; @Input() resourceIdKey = 'elementId'; @Input() assetContextConfig?: SpTableAssetContextConfig; + @Input() nameSearchConfig?: SpTableNameSearchConfig<T>; @Input() dataSource: MatTableDataSource<T>; @@ -193,6 +203,7 @@ export class SpTableComponent<T> visiblePageRows: T[] = []; selectedMultiAction: string | null = null; + nameSearchTerm = ''; viewMode: SpTableGroupViewMode = 'list'; groupBy: SpTableGroupingMode = 'asset'; groupedSections: SpTableGroupedSection<T>[] = []; @@ -206,6 +217,10 @@ export class SpTableComponent<T> private renderedDataSubscription?: Subscription; private assetDataSubscription?: Subscription; private viewInitialized = false; + private defaultFilterPredicates = new WeakMap< + MatTableDataSource<T>, + (data: T, filter: string) => boolean + >(); private assetContextIndex = new Map< string, Map<string, SpTableResolvedAssetContext> @@ -252,6 +267,7 @@ export class SpTableComponent<T> this.selection.clear(); this.emitSelection(); this.visiblePageRows = []; + this.configureNameSearch(); if (this.viewInitialized) { this.bindDataSource(); } @@ -275,6 +291,10 @@ export class SpTableComponent<T> this.applyAssetContextSortingAccessor(); this.refreshRenderedRows(); } + + if (changes['nameSearchConfig'] && !changes['dataSource']) { + this.configureNameSearch(); + } } ngOnDestroy() { @@ -323,6 +343,18 @@ export class SpTableComponent<T> return !!this.assetContextConfig; } + get shouldShowNameSearch(): boolean { + return !!this.nameSearchConfig?.enabled; + } + + get nameSearchKeys(): Array<keyof T | 'name'> { + if (!this.nameSearchConfig?.searchKey?.length) { + return ['name']; + } + + return this.nameSearchConfig.searchKey; + } + get renderedDataSource(): MatTableDataSource<T> | SpTableRenderedRow<T>[] { return this.viewMode === 'grouped' ? this.groupedSections.flatMap(section => [ @@ -476,6 +508,21 @@ export class SpTableComponent<T> this.refreshRenderedRows(); } + onNameSearchInput(value: string) { + this.nameSearchTerm = value; + if (!this.dataSource) { + return; + } + + this.dataSource.filter = value.trim().toLocaleLowerCase(); + this.paginator?.firstPage(); + this.refreshRenderedRows(); + } + + clearNameSearch() { + this.onNameSearchInput(''); + } + isGroupHeaderRow = (_: number, row: SpTableRenderedRow<T>) => this.hasGroupHeaderMarker(row); @@ -487,6 +534,7 @@ export class SpTableComponent<T> return; } + this.configureNameSearch(); this.dataSource.paginator = this.paginator; this.renderedDataSubscription?.unsubscribe(); @@ -499,6 +547,47 @@ export class SpTableComponent<T> this.updateRenderedState(this.getCurrentPageRows(), false); } + private configureNameSearch() { + if (!this.dataSource) { + return; + } + + if (!this.defaultFilterPredicates.has(this.dataSource)) { + this.defaultFilterPredicates.set( + this.dataSource, + this.dataSource.filterPredicate, + ); + } + + const defaultFilterPredicate = + this.defaultFilterPredicates.get(this.dataSource) ?? + this.dataSource.filterPredicate; + + if (!this.shouldShowNameSearch) { + this.nameSearchTerm = ''; + this.dataSource.filterPredicate = defaultFilterPredicate; + this.dataSource.filter = ''; + return; + } + + this.dataSource.filterPredicate = (row, filter) => { + const normalizedFilter = filter.trim().toLocaleLowerCase(); + if (!normalizedFilter) { + return true; + } + + return this.nameSearchKeys.some(searchKey => { + const searchValue = (row as Record<string, unknown>)[ + searchKey as string + ]; + return String(searchValue ?? '') + .toLocaleLowerCase() + .includes(normalizedFilter); + }); + }; + this.dataSource.filter = this.nameSearchTerm.trim().toLocaleLowerCase(); + } + private getCurrentPageRows(): T[] { const rows = this.dataSource?.filteredData ?? this.dataSource?.data ?? []; 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 index 9ce38d0571..7a69a85f4c 100644 --- 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 @@ -36,6 +36,12 @@ export interface SpTableAssetContextConfig { hideBelowWidth?: number; } +export interface SpTableNameSearchConfig<T = Record<string, unknown>> { + enabled: boolean; + placeholder?: string; + searchKey?: Array<keyof T>; +} + export class SpTableAssetContextValue { id: string; label: string; diff --git a/ui/src/app/assets/components/asset-overview/asset-overview.component.html b/ui/src/app/assets/components/asset-overview/asset-overview.component.html index 3fd8bb3d83..80a6478999 100644 --- a/ui/src/app/assets/components/asset-overview/asset-overview.component.html +++ b/ui/src/app/assets/components/asset-overview/asset-overview.component.html @@ -58,6 +58,11 @@ fxFlex="100" [columns]="displayedColumns" [dataSource]="dataSource" + [nameSearchConfig]="{ + enabled: true, + placeholder: 'Search assets', + searchKey: ['assetName', 'assetDescription'], + }" [showActionsMenu]="true" [rowsClickable]="true" (rowClicked)="goToDetailsView($event)" 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 6c0803791f..1be768d6a4 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" + [nameSearchConfig]="{ enabled: true, placeholder: 'Search charts' }" [assetContextConfig]="assetContextConfig" featureCardId="chart" [showActionsMenu]="true" 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 91ba5b3cf1..ac68fa8b6f 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 @@ -35,11 +35,6 @@ }} </button> <div fxFlex fxLayout="row" fxLayoutAlign="end center"> - <sp-connect-filter-toolbar - class="filter-bar-margin" - (filterChangedEmitter)="applyFilter($event)" - > - </sp-connect-filter-toolbar> <div fxFlex="100" fxLayout="row" @@ -79,6 +74,11 @@ resourceIdKey="elementId" [columns]="displayedColumns" [dataSource]="dataSource" + [nameSearchConfig]="{ + enabled: true, + placeholder: 'Search', + searchKey: ['name', 'description'], + }" [assetContextConfig]="assetContextConfig" [showSelectionCheckboxes]="true" [showMultiActionsExecuteButton]="true" 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 01b82347ad..25c7340a41 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 @@ -55,8 +55,6 @@ import { DeleteAdapterDialogComponent } from '../../dialog/delete-adapter-dialog import { AllAdapterActionsComponent } from '../../dialog/start-all-adapters/all-adapter-actions-dialog.component'; import { MatSort, MatSortHeader } from '@angular/material/sort'; import { Router } from '@angular/router'; -import { AdapterFilterSettingsModel } from '../../model/adapter-filter-settings.model'; -import { AdapterFilterPipe } from '../../filter/adapter-filter.pipe'; import { SpConnectRoutes } from '../../connect.breadcrumb'; import { Subscription } from 'rxjs'; import { ShepherdService } from '../../../services/tour/shepherd.service'; @@ -69,7 +67,6 @@ import { } from '@ngbracket/ngx-layout/flex'; import { MatButton, MatIconButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; -import { SpConnectFilterToolbarComponent } from '../filter-toolbar/filter-toolbar.component'; import { MatTooltip } from '@angular/material/tooltip'; import { AdapterStatusLightComponent } from './adapter-status-light/adapter-status-light.component'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; @@ -88,7 +85,6 @@ import { DatePipe } from '@angular/common'; LayoutGapDirective, MatButton, MatIcon, - SpConnectFilterToolbarComponent, MatIconButton, MatTooltip, SpBasicHeaderTitleComponent, @@ -112,8 +108,6 @@ import { DatePipe } from '@angular/common'; export class ExistingAdaptersComponent implements OnInit, OnDestroy { existingAdapters: AdapterSummaryDto[] = []; filteredAdapters: AdapterSummaryDto[] = []; - - currentFilter: AdapterFilterSettingsModel; operationInProgressAdapterId: string | undefined; @ViewChild(MatSort) @@ -158,7 +152,6 @@ export class ExistingAdaptersComponent implements OnInit, OnDestroy { private currentUserService = inject(CurrentUserService); private router = inject(Router); private pipelineElementAssetService = inject(PipelineElementAssetService); - private adapterFilter = inject(AdapterFilterPipe); private breadcrumbService = inject(SpBreadcrumbService); private shepherdService = inject(ShepherdService); private translate = inject(TranslateService); @@ -379,12 +372,7 @@ export class ExistingAdaptersComponent implements OnInit, OnDestroy { applyAdapterFilters(elementIds: Set<string>): void { this.currentFilterIds = elementIds; - this.filteredAdapters = ( - this.adapterFilter.transform( - this.existingAdapters, - this.currentFilter, - ) as AdapterSummaryDto[] - ).filter(a => { + this.filteredAdapters = this.existingAdapters.filter(a => { if (elementIds === undefined) { return false; } else if (elementIds.size === 0) { @@ -406,13 +394,6 @@ export class ExistingAdaptersComponent implements OnInit, OnDestroy { }); } - applyFilter(filter: AdapterFilterSettingsModel): void { - this.currentFilter = filter; - if (this.dataSource) { - this.applyAdapterFilters(this.currentFilterIds); - } - } - navigateToDetailsOverviewPage(adapter: AdapterSummaryDto): void { this.router.navigate(['connect', 'details', adapter.elementId]); } 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 561f4657c3..f31201ea25 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 @@ -25,6 +25,10 @@ fxFlex="100" [columns]="displayedColumns" [assetContextConfig]="assetContextConfig" + [nameSearchConfig]="{ + enabled: true, + searchKey: ['name', 'description'], + }" featureCardId="dashboard" [showActionsMenu]="true" [rowsClickable]="true" diff --git a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html index 198b41c9f0..d8792a53a4 100644 --- a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html +++ b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html @@ -61,6 +61,10 @@ featureCardId="measurement" [dataSource]="dataSource" [columns]="displayedColumns" + [nameSearchConfig]="{ + enabled: true, + placeholder: 'Search datasets', + }" [assetContextConfig]="assetContextConfig" [showActionsMenu]="true" matSort 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 bc1d182188..4fb0cb6d3f 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 @@ -24,6 +24,10 @@ [multiActionOptions]=" hasPipelineWritePrivileges ? bulkPipelineActionOptions : [] " + [nameSearchConfig]="{ + enabled: true, + searchKey: ['name', 'description'], + }" featureCardId="pipeline" resourceIdKey="elementId" [assetContextConfig]="assetContextConfig"
