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

dominikriemer 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 fac5220632 fix: Improve performance of charts (#4583)
fac5220632 is described below

commit fac52206325b9d36a0ce823c86a54fc937edd1ce
Author: Dominik Riemer <[email protected]>
AuthorDate: Wed Jun 17 23:04:22 2026 +0200

    fix: Improve performance of charts (#4583)
---
 .../lib/components/sp-table/sp-table.component.ts  |  45 ++++++--
 .../registry/chart-registry.service.ts             |  21 ++++
 .../chart-overview-table.component.html            |  28 +++--
 .../chart-overview-table.component.ts              |  57 ++++++----
 .../chart-overview/chart-overview.component.html   |   2 +
 .../chart-overview/chart-overview.component.scss   |   8 --
 .../chart-selection-panel.component.ts             |   8 +-
 .../chart-preview/chart-preview.component.html     |  19 ++--
 .../chart-preview/chart-preview.component.ts       |  29 ++---
 .../chart-selection/chart-selection.component.html |  15 ++-
 .../chart-selection/chart-selection.component.ts   | 119 ++++++++++++++-------
 .../chart-selection/chart-selection.model.ts}      |  37 ++-----
 12 files changed, 229 insertions(+), 159 deletions(-)

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 d7f0b5c3ce..2b01a2b6cd 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
@@ -75,7 +75,12 @@ import {
     MatSuffix,
 } from '@angular/material/form-field';
 import { MatInput } from '@angular/material/input';
-import { Subscription } from 'rxjs';
+import {
+    debounceTime,
+    distinctUntilChanged,
+    Subject,
+    Subscription,
+} from 'rxjs';
 import { MatOption, MatSelect } from '@angular/material/select';
 import { SpAssetBrowserService } from '../asset-browser/asset-browser.service';
 import { SpLabelComponent } from '../sp-label/sp-label.component';
@@ -207,6 +212,7 @@ export class SpTableComponent<T>
     viewMode: SpTableGroupViewMode = 'list';
     groupBy: SpTableGroupingMode = 'asset';
     groupedSections: SpTableGroupedSection<T>[] = [];
+    renderedGroupedRows: SpTableRenderedRow<T>[] = [];
 
     readonly selection = new SelectionModel<T>(true, []);
 
@@ -216,6 +222,8 @@ export class SpTableComponent<T>
     private assetContextService = inject(SpTableAssetContextService);
     private renderedDataSubscription?: Subscription;
     private assetDataSubscription?: Subscription;
+    private nameSearchSubscription?: Subscription;
+    private nameSearchInput$ = new Subject<string>();
     private viewInitialized = false;
     private defaultFilterPredicates = new WeakMap<
         MatTableDataSource<T>,
@@ -241,6 +249,9 @@ export class SpTableComponent<T>
                 this.applyAssetContextSortingAccessor();
                 this.refreshRenderedRows();
             });
+        this.nameSearchSubscription = this.nameSearchInput$
+            .pipe(debounceTime(150), distinctUntilChanged())
+            .subscribe(value => this.applyNameSearchFilter(value));
         this.updateCompactLayout();
     }
 
@@ -300,6 +311,7 @@ export class SpTableComponent<T>
     ngOnDestroy() {
         this.renderedDataSubscription?.unsubscribe();
         this.assetDataSubscription?.unsubscribe();
+        this.nameSearchSubscription?.unsubscribe();
     }
 
     @HostListener('window:resize')
@@ -357,16 +369,7 @@ export class SpTableComponent<T>
 
     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.renderedGroupedRows
             : this.dataSource;
     }
 
@@ -510,10 +513,19 @@ export class SpTableComponent<T>
 
     onNameSearchInput(value: string) {
         this.nameSearchTerm = value;
+        this.nameSearchInput$.next(value);
+    }
+
+    private applyNameSearchFilter(value: string) {
         if (!this.dataSource) {
             return;
         }
 
+        if (!this.shouldShowNameSearch) {
+            this.dataSource.filter = '';
+            return;
+        }
+
         const normalizedFilter = value.trim().toLocaleLowerCase();
         if (this.dataSource.filter === normalizedFilter) {
             return;
@@ -730,6 +742,7 @@ export class SpTableComponent<T>
     private rebuildGroupedSections(rows: T[]) {
         if (!this.assetContextConfig || this.viewMode !== 'grouped') {
             this.groupedSections = [];
+            this.renderedGroupedRows = [];
             return;
         }
 
@@ -756,6 +769,16 @@ export class SpTableComponent<T>
                 ...group,
                 rows: [...group.rows],
             }));
+        this.renderedGroupedRows = this.groupedSections.flatMap(section => [
+            {
+                __spGroupHeader: true as const,
+                id: section.id,
+                title: section.title,
+                color: section.color,
+                count: section.count,
+            },
+            ...section.rows,
+        ]);
     }
 
     private resolveGroups(
diff --git a/ui/src/app/chart-shared/registry/chart-registry.service.ts 
b/ui/src/app/chart-shared/registry/chart-registry.service.ts
index d1d32dd65d..e7108a6257 100644
--- a/ui/src/app/chart-shared/registry/chart-registry.service.ts
+++ b/ui/src/app/chart-shared/registry/chart-registry.service.ts
@@ -82,6 +82,8 @@ export class ChartRegistry {
     private translateService = inject(TranslateService);
 
     chartTypes: IWidget<any>[] = [];
+    registeredChartSummary: Record<string, { icon: string; label: string }> =
+        {};
 
     constructor() {
         this.chartTypes = [
@@ -303,10 +305,22 @@ export class ChartRegistry {
                 ),
             },
         ];
+        this.makeChartSummary();
+    }
+
+    private makeChartSummary() {
+        this.registeredChartSummary = {};
+        this.chartTypes.forEach(c => {
+            this.registeredChartSummary[c.id] = {
+                label: c.label,
+                icon: c.icon,
+            };
+        });
     }
 
     registerChart(chart: IWidget<any>): void {
         this.chartTypes.push(chart);
+        this.makeChartSummary();
     }
 
     getAvailableChartTemplates(): IWidget<any>[] {
@@ -325,6 +339,13 @@ export class ChartRegistry {
         return this.getChartTemplate(chartId).id;
     }
 
+    getRegisteredChartSummary(chartId: string): {
+        icon: string;
+        label: string;
+    } {
+        return this.registeredChartSummary[chartId];
+    }
+
     private findBackwardsCompatibleChart(chartId: string): IWidget<any> {
         return this.chartTypes.find(
             chart => chart.alias !== undefined && chart.alias === chartId,
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 9b55684743..48adec8070 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
@@ -20,28 +20,38 @@
     <sp-basic-header-title-component
         [title]="'Charts' | translate"
     ></sp-basic-header-title-component>
-    @if (isLoading) {
+    @if (isLoading && !hasLoadedCharts) {
         <div
+            fxLayout="column"
             fxFlex="100"
-            fxLayout="row"
             fxLayoutAlign="center center"
-            class="chart-overview-loading"
+            fxLayoutGap="12px"
         >
+            <mat-progress-spinner
+                mode="indeterminate"
+                diameter="40"
+            ></mat-progress-spinner>
+            <span class="chart-overview-loading-text">{{
+                'Loading' | translate
+            }}</span>
+        </div>
+    } @else {
+        @if (isLoading) {
             <div
-                fxLayout="column"
-                fxLayoutAlign="center center"
-                fxLayoutGap="12px"
+                fxLayout="row"
+                fxLayoutAlign="end center"
+                fxLayoutGap="8px"
+                class="p-2"
             >
                 <mat-progress-spinner
                     mode="indeterminate"
-                    diameter="40"
+                    diameter="18"
                 ></mat-progress-spinner>
                 <span class="chart-overview-loading-text">{{
                     'Loading' | translate
                 }}</span>
             </div>
-        </div>
-    } @else {
+        }
         <div fxFlex="100" fxLayout="row" fxLayoutAlign="center start">
             <sp-table
                 fxFlex="100"
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 4b4ff52f91..28d2659fda 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
@@ -17,6 +17,8 @@
  */
 
 import {
+    ChangeDetectionStrategy,
+    ChangeDetectorRef,
     Component,
     inject,
     Input,
@@ -53,7 +55,7 @@ import {
 import { MatDialog } from '@angular/material/dialog';
 import { TranslatePipe, TranslateService } from '@ngx-translate/core';
 import { ChartRoutingService } from 
'../../../../chart-shared/services/chart-routing.service';
-import { Subscription } from 'rxjs';
+import { finalize, Subscription } from 'rxjs';
 import { MatSort, MatSortHeader } from '@angular/material/sort';
 import {
     FlexDirective,
@@ -110,6 +112,7 @@ type ChartOverviewRow = ChartSummaryDto & {
         MatProgressSpinner,
         TranslatePipe,
     ],
+    changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class ChartOverviewTableComponent implements OnInit, OnDestroy {
     @Input()
@@ -117,7 +120,6 @@ export class ChartOverviewTableComponent implements OnInit, 
OnDestroy {
 
     @ViewChild(MatSort)
     set sort(sort: MatSort | undefined) {
-        this._sort = sort;
         if (sort) {
             this.dataSource.sort = sort;
         }
@@ -141,6 +143,7 @@ export class ChartOverviewTableComponent implements OnInit, 
OnDestroy {
         placeholder: 'Search charts',
     };
     isLoading = false;
+    hasLoadedCharts = false;
     charts: ChartOverviewRow[] = [];
     filteredCharts: ChartOverviewRow[] = [];
 
@@ -152,10 +155,10 @@ export class ChartOverviewTableComponent implements 
OnInit, OnDestroy {
     private routingService = inject(ChartRoutingService);
     private assetFilterService = inject(SpAssetBrowserService);
     private chartRegistryService = inject(ChartRegistry);
+    private cdr = inject(ChangeDetectorRef);
 
     assetFilter$: Subscription;
-    currentFilterIds = new Set<string>();
-    private _sort?: MatSort;
+    currentFilterIds?: Set<string>;
     private chartTypeMetadata = new Map<
         string,
         { icon: string; label: string }
@@ -167,6 +170,7 @@ export class ChartOverviewTableComponent implements OnInit, 
OnDestroy {
             this.assetFilterService.currentAssetFilter$.subscribe(filter => {
                 this.currentFilterIds = filter?.activeElementIds;
                 this.applyChartFilters(this.currentFilterIds);
+                this.cdr.markForCheck();
             });
 
         this.dataSource.sortingDataAccessor = (chart, column) => {
@@ -186,20 +190,29 @@ export class ChartOverviewTableComponent implements 
OnInit, OnDestroy {
 
     getCharts(): void {
         this.isLoading = true;
-        this.dataViewService.getChartSummary().subscribe({
-            next: chartSummary => {
-                this.charts = chartSummary.resources
-                    .map(chart => this.toChartOverviewRow(chart))
-                    .sort((a, b) => a.name.localeCompare(b.name));
-                this.applyChartFilters(this.currentFilterIds);
-            },
-            complete: () => {
-                this.isLoading = false;
-            },
-            error: () => {
-                this.isLoading = false;
-            },
-        });
+        this.cdr.markForCheck();
+        this.dataViewService
+            .getChartSummary()
+            .pipe(
+                finalize(() => {
+                    this.isLoading = false;
+                    this.cdr.markForCheck();
+                }),
+            )
+            .subscribe({
+                next: chartSummary => {
+                    this.charts = chartSummary.resources
+                        .map(chart => this.toChartOverviewRow(chart))
+                        .sort((a, b) => a.name.localeCompare(b.name));
+                    this.hasLoadedCharts = true;
+                    this.applyChartFilters(this.currentFilterIds);
+                    this.cdr.markForCheck();
+                },
+                error: () => {
+                    this.hasLoadedCharts = true;
+                    this.cdr.markForCheck();
+                },
+            });
     }
 
     ngOnDestroy(): void {
@@ -306,7 +319,7 @@ export class ChartOverviewTableComponent implements OnInit, 
OnDestroy {
         });
     }
 
-    applyChartFilters(elementIds: Set<string>): void {
+    applyChartFilters(elementIds?: Set<string>): void {
         if (elementIds === undefined) {
             this.filteredCharts = [];
         } else if (elementIds.size === 0) {
@@ -316,9 +329,6 @@ export class ChartOverviewTableComponent implements OnInit, 
OnDestroy {
                 elementIds.has(a.elementId),
             );
         }
-        if (this._sort) {
-            this.dataSource.sort = this._sort;
-        }
         this.dataSource.data = this.filteredCharts;
     }
 
@@ -342,7 +352,8 @@ export class ChartOverviewTableComponent implements OnInit, 
OnDestroy {
             return cached;
         }
 
-        const template = 
this.chartRegistryService.getChartTemplate(widgetType);
+        const template =
+            this.chartRegistryService.getRegisteredChartSummary(widgetType);
         const metadata = {
             icon: template?.icon ?? 'insert_chart',
             label: template?.label ?? widgetType,
diff --git 
a/ui/src/app/chart/components/chart-overview/chart-overview.component.html 
b/ui/src/app/chart/components/chart-overview/chart-overview.component.html
index e4ee9d1afb..299e3ce368 100644
--- a/ui/src/app/chart/components/chart-overview/chart-overview.component.html
+++ b/ui/src/app/chart/components/chart-overview/chart-overview.component.html
@@ -40,6 +40,8 @@
     </div>
     <div fxFlex="100" fxLayout="column">
         <sp-data-explorer-overview-table
+            fxLayout="column"
+            fxFlex="100"
             [hasDataExplorerWritePrivileges]="hasDataExplorerWritePrivileges"
         ></sp-data-explorer-overview-table>
     </div>
diff --git 
a/ui/src/app/chart/components/chart-overview/chart-overview.component.scss 
b/ui/src/app/chart/components/chart-overview/chart-overview.component.scss
index 06c5911910..c10f9025ea 100644
--- a/ui/src/app/chart/components/chart-overview/chart-overview.component.scss
+++ b/ui/src/app/chart/components/chart-overview/chart-overview.component.scss
@@ -36,14 +36,6 @@
     margin-right: 10px;
 }
 
-.chart-overview-loading {
-    display: flex;
-    flex: 1 1 auto;
-    align-items: center;
-    justify-content: center;
-    text-align: center;
-}
-
 .chart-overview-loading-text {
     font-size: var(--font-size-sm);
     color: var(--color-paragraph);
diff --git 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection-panel.component.ts
 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection-panel.component.ts
index a6eef3e077..1f1204530e 100644
--- 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection-panel.component.ts
+++ 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection-panel.component.ts
@@ -16,7 +16,12 @@
  *
  */
 
-import { Component, EventEmitter, Output } from '@angular/core';
+import {
+    ChangeDetectionStrategy,
+    Component,
+    EventEmitter,
+    Output,
+} from '@angular/core';
 import { MatTab, MatTabGroup } from '@angular/material/tabs';
 import {
     FlexDirective,
@@ -42,6 +47,7 @@ import { TranslatePipe } from '@ngx-translate/core';
         ChartSelectionComponent,
         TranslatePipe,
     ],
+    changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class ChartSelectionPanelComponent {
     @Output()
diff --git 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.html
 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.html
index 29eddbab1d..d046ac5236 100644
--- 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.html
+++ 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.html
@@ -18,27 +18,30 @@
 
 <div
     class="data-view-preview-outer"
-    [attr.data-cy]="dataCyId"
-    [attr.title]="chart.name"
+    [attr.data-cy]="chartItem.dataCyId"
+    [attr.title]="chartItem.chart.name"
     (click)="addChart()"
 >
     <div class="chart-preview-header">
-        <div class="chart-preview-icon-shell" [attr.title]="widgetTypeLabel">
-            <mat-icon>{{ widgetTypeIcon }}</mat-icon>
+        <div
+            class="chart-preview-icon-shell"
+            [attr.title]="chartItem.widgetTypeLabel"
+        >
+            <mat-icon>{{ chartItem.widgetTypeIcon }}</mat-icon>
         </div>
 
         <div class="chart-preview-copy">
             <div class="chart-preview-title-row">
-                <h5 class="chart-preview-title">{{ chart.name }}</h5>
+                <h5 class="chart-preview-title">{{ chartItem.chart.name }}</h5>
             </div>
 
             <div class="chart-preview-meta">
-                @if (chart.datasetName) {
+                @if (chartItem.chart.datasetName) {
                     <span
                         class="chart-preview-meta-item chart-preview-dataset"
-                        [attr.title]="chart.datasetName"
+                        [attr.title]="chartItem.chart.datasetName"
                     >
-                        {{ chart.datasetName }}
+                        {{ chartItem.chart.datasetName }}
                     </span>
                 }
             </div>
diff --git 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.ts
 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.ts
index 4ca3306b26..327a252307 100644
--- 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.ts
+++ 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.ts
@@ -21,16 +21,14 @@ import {
     Component,
     EventEmitter,
     Input,
-    OnInit,
     Output,
     inject,
 } from '@angular/core';
-import { ChartSummaryDto } from '@streampipes/platform-services';
-import { ChartRegistry } from 
'../../../../../../chart-shared/registry/chart-registry.service';
 import { MatIcon } from '@angular/material/icon';
 import { MatIconButton } from '@angular/material/button';
 import { FeatureCardService } from '@streampipes/shared-ui';
 import { TranslatePipe } from '@ngx-translate/core';
+import { ChartSelectionItem } from '../chart-selection.model';
 
 @Component({
     selector: 'sp-chart-preview',
@@ -39,35 +37,24 @@ import { TranslatePipe } from '@ngx-translate/core';
     changeDetection: ChangeDetectionStrategy.OnPush,
     imports: [MatIcon, MatIconButton, TranslatePipe],
 })
-export class ChartPreviewComponent implements OnInit {
-    private widgetRegistryService = inject(ChartRegistry);
+export class ChartPreviewComponent {
     private featureCardService = inject(FeatureCardService);
 
     @Input()
-    chart: ChartSummaryDto;
-
-    widgetTypeLabel = '';
-    widgetTypeIcon = 'insert_chart';
-    dataCyId = '';
+    chartItem!: ChartSelectionItem;
 
     @Output()
     addChartEmitter: EventEmitter<string> = new EventEmitter<string>();
 
-    ngOnInit(): void {
-        const template = this.widgetRegistryService.getChartTemplate(
-            this.chart.widgetType,
-        );
-        this.widgetTypeLabel = template?.label ?? this.chart.widgetType;
-        this.widgetTypeIcon = template?.icon ?? 'insert_chart';
-        this.dataCyId = `add-data-view-btn-${this.chart.name.replaceAll(' ', 
'')}`;
-    }
-
     addChart(): void {
-        this.addChartEmitter.emit(this.chart.elementId);
+        this.addChartEmitter.emit(this.chartItem.chart.elementId);
     }
 
     openPreview(event: MouseEvent): void {
         event.stopPropagation();
-        this.featureCardService.openFeatureCard('chart', this.chart.elementId);
+        this.featureCardService.openFeatureCard(
+            'chart',
+            this.chartItem.chart.elementId,
+        );
     }
 }
diff --git 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.html
 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.html
index 535eed1f08..0554e8ffdd 100644
--- 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.html
+++ 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.html
@@ -30,8 +30,8 @@
         >
             <div fxLayout="column" class="chart-selection-heading">
                 <span class="chart-selection-caption">
-                    {{ filteredCharts.length }}
-                    @if (filteredCharts.length === 1) {
+                    {{ filteredChartItems.length }}
+                    @if (filteredChartItems.length === 1) {
                         {{ 'chart available' | translate }}
                     } @else {
                         {{ 'charts available' | translate }}
@@ -81,11 +81,10 @@
             <input
                 [placeholder]="'Search charts' | translate"
                 matInput
-                [value]="searchTerm"
-                (input)="onSearchTermChanged($any($event.target).value)"
+                [formControl]="searchControl"
                 data-cy="chart-selection-search"
             />
-            @if (hasActiveSearch()) {
+            @if (hasActiveSearch) {
                 <button
                     mat-icon-button
                     matSuffix
@@ -115,7 +114,7 @@
                 'Loading charts...' | translate
             }}</span>
         </div>
-    } @else if (charts.length > 0 && filteredCharts.length > 0) {
+    } @else if (charts.length > 0 && filteredChartItems.length > 0) {
         <cdk-virtual-scroll-viewport
             fxFlexFill
             class="chart-selection-results chart-selection-viewport"
@@ -125,13 +124,13 @@
         >
             <div
                 *cdkVirtualFor="
-                    let chart of filteredCharts;
+                    let chartItem of filteredChartItems;
                     trackBy: trackByChartId
                 "
                 class="chart-selection-item"
             >
                 <sp-chart-preview
-                    [chart]="chart"
+                    [chartItem]="chartItem"
                     (addChartEmitter)="addChartEmitter.emit($event)"
                 >
                 </sp-chart-preview>
diff --git 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.ts
 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.ts
index ebba633d40..dc3bd91733 100644
--- 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.ts
+++ 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.ts
@@ -20,12 +20,15 @@ import {
     ChangeDetectionStrategy,
     ChangeDetectorRef,
     Component,
+    DestroyRef,
     EventEmitter,
     inject,
     OnInit,
     Output,
 } from '@angular/core';
 import { ChartService, ChartSummaryDto } from '@streampipes/platform-services';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
 import { AuthService } from '../../../../../services/auth.service';
 import { UserPrivilege } from '../../../../../core/auth/user-privilege.enum';
 import { ChartRegistry } from 
'../../../../../chart-shared/registry/chart-registry.service';
@@ -54,6 +57,8 @@ import {
     CdkVirtualForOf,
     CdkVirtualScrollViewport,
 } from '@angular/cdk/scrolling';
+import { debounceTime, distinctUntilChanged, finalize } from 'rxjs';
+import { ChartSelectionItem } from './chart-selection.model';
 
 @Component({
     selector: 'sp-chart-selection',
@@ -80,6 +85,7 @@ import {
         CdkVirtualScrollViewport,
         CdkFixedSizeVirtualScroll,
         CdkVirtualForOf,
+        ReactiveFormsModule,
     ],
 })
 export class ChartSelectionComponent implements OnInit {
@@ -88,14 +94,18 @@ export class ChartSelectionComponent implements OnInit {
     private chartRegistryService = inject(ChartRegistry);
     private chartRoutingService = inject(ChartRoutingService);
     private cdr = inject(ChangeDetectorRef);
+    private destroyRef = inject(DestroyRef);
 
     @Output()
     addChartEmitter: EventEmitter<string> = new EventEmitter();
 
     charts: ChartSummaryDto[] = [];
-    filteredCharts: ChartSummaryDto[] = [];
+    chartItems: ChartSelectionItem[] = [];
+    filteredChartItems: ChartSelectionItem[] = [];
     searchTerm = '';
+    searchControl = new FormControl('', { nonNullable: true });
     isRefreshing = false;
+    hasActiveSearch = false;
     readonly chartItemSize = 132;
 
     hasChartWritePrivileges: boolean = false;
@@ -106,6 +116,16 @@ export class ChartSelectionComponent implements OnInit {
         );
 
         this.refreshCharts();
+
+        this.searchControl.valueChanges
+            .pipe(
+                debounceTime(150),
+                distinctUntilChanged(),
+                takeUntilDestroyed(this.destroyRef),
+            )
+            .subscribe(value => {
+                this.setSearchTerm(value);
+            });
     }
 
     navigateToDataViewCreation(): void {
@@ -115,62 +135,81 @@ export class ChartSelectionComponent implements OnInit {
     refreshCharts(): void {
         this.isRefreshing = true;
         this.cdr.markForCheck();
-        this.dataViewService.getChartSummary().subscribe({
-            next: chartSummary => {
-                this.charts = chartSummary.resources.sort((a, b) =>
-                    a.name.localeCompare(b.name),
-                );
-                this.applySearch();
-                this.cdr.markForCheck();
-            },
-            complete: () => {
-                this.isRefreshing = false;
-                this.cdr.markForCheck();
-            },
-            error: () => {
-                this.isRefreshing = false;
-                this.cdr.markForCheck();
-            },
-        });
+        this.dataViewService
+            .getChartSummary()
+            .pipe(
+                finalize(() => {
+                    this.isRefreshing = false;
+                    this.cdr.markForCheck();
+                }),
+            )
+            .subscribe({
+                next: chartSummary => {
+                    this.charts = [...chartSummary.resources].sort((a, b) =>
+                        a.name.localeCompare(b.name),
+                    );
+                    this.chartItems = this.charts.map(chart =>
+                        this.toChartSelectionItem(chart),
+                    );
+                    this.applySearch();
+                    this.cdr.markForCheck();
+                },
+                error: () => {
+                    this.charts = [];
+                    this.chartItems = [];
+                    this.filteredChartItems = [];
+                },
+            });
     }
 
-    onSearchTermChanged(value: string): void {
-        this.searchTerm = value;
-        this.applySearch();
-        this.cdr.markForCheck();
+    clearSearch(): void {
+        this.searchControl.setValue('', { emitEvent: false });
+        this.setSearchTerm('');
     }
 
-    clearSearch(): void {
-        this.searchTerm = '';
+    trackByChartId(index: number, item: ChartSelectionItem): string {
+        return item.chart.elementId;
+    }
+
+    private setSearchTerm(value: string): void {
+        this.searchTerm = value;
+        this.hasActiveSearch = this.searchTerm.trim().length > 0;
         this.applySearch();
         this.cdr.markForCheck();
     }
 
-    hasActiveSearch(): boolean {
-        return this.searchTerm.trim().length > 0;
-    }
+    private toChartSelectionItem(chart: ChartSummaryDto): ChartSelectionItem {
+        const template = this.chartRegistryService.getRegisteredChartSummary(
+            chart.widgetType,
+        );
+        const widgetTypeLabel = template?.label ?? chart.widgetType;
 
-    trackByChartId(index: number, chart: ChartSummaryDto): string {
-        return chart.elementId;
+        return {
+            chart,
+            widgetTypeLabel,
+            widgetTypeIcon: template?.icon ?? 'insert_chart',
+            dataCyId: `add-data-view-btn-${chart.name.replaceAll(' ', '')}`,
+            searchText: [
+                chart.name,
+                chart.datasetName,
+                chart.widgetType,
+                widgetTypeLabel,
+            ]
+                .filter((value): value is string => !!value)
+                .join(' ')
+                .toLowerCase(),
+        };
     }
 
     private applySearch(): void {
         const query = this.searchTerm.trim().toLowerCase();
         if (!query) {
-            this.filteredCharts = this.charts;
+            this.filteredChartItems = this.chartItems;
             return;
         }
 
-        this.filteredCharts = this.charts.filter(chart =>
-            [
-                chart.name,
-                chart.datasetName,
-                chart.widgetType,
-                this.chartRegistryService.getChartTemplate(chart.widgetType)
-                    ?.label,
-            ]
-                .filter((value): value is string => !!value)
-                .some(value => value.toLowerCase().includes(query)),
+        this.filteredChartItems = this.chartItems.filter(item =>
+            item.searchText.includes(query),
         );
     }
 }
diff --git 
a/ui/src/app/chart/components/chart-overview/chart-overview.component.scss 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.model.ts
similarity index 65%
copy from 
ui/src/app/chart/components/chart-overview/chart-overview.component.scss
copy to 
ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.model.ts
index 06c5911910..78e4f80aa5 100644
--- a/ui/src/app/chart/components/chart-overview/chart-overview.component.scss
+++ 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.model.ts
@@ -16,35 +16,12 @@
  *
  */
 
-.mat-mdc-header-cell {
-    font-weight: bold;
-}
-
-.w-100 {
-    width: 100%;
-}
-
-.p-2 {
-    padding: 2px;
-}
-
-.m-20 {
-    margin: 20px;
-}
-
-.mr-10 {
-    margin-right: 10px;
-}
-
-.chart-overview-loading {
-    display: flex;
-    flex: 1 1 auto;
-    align-items: center;
-    justify-content: center;
-    text-align: center;
-}
+import { ChartSummaryDto } from '@streampipes/platform-services';
 
-.chart-overview-loading-text {
-    font-size: var(--font-size-sm);
-    color: var(--color-paragraph);
+export interface ChartSelectionItem {
+    chart: ChartSummaryDto;
+    widgetTypeLabel: string;
+    widgetTypeIcon: string;
+    dataCyId: string;
+    searchText: string;
 }

Reply via email to