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

dominikriemer pushed a commit to branch improve-chart-selection-loading
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to 
refs/heads/improve-chart-selection-loading by this push:
     new b5305ec847 Improve table performance
b5305ec847 is described below

commit b5305ec847b1fcba4452ffd2dd41fdffe346027f
Author: Dominik Riemer <[email protected]>
AuthorDate: Wed Jun 17 15:45:11 2026 +0200

    Improve table performance
---
 .../lib/components/sp-table/sp-table.component.ts  |  13 +-
 .../chart-overview-table.component.html            | 294 +++++++++++----------
 .../chart-overview-table.component.ts              | 105 +++++---
 .../chart-overview/chart-overview.component.scss   |  13 +
 .../chart-selection/chart-selection.component.html |  18 +-
 .../chart-selection/chart-selection.component.scss |   9 +
 .../chart-selection/chart-selection.component.ts   |   2 +
 7 files changed, 275 insertions(+), 179 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 d2a5cae146..d7f0b5c3ce 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
@@ -514,9 +514,16 @@ export class SpTableComponent<T>
             return;
         }
 
-        this.dataSource.filter = value.trim().toLocaleLowerCase();
-        this.paginator?.firstPage();
-        this.refreshRenderedRows();
+        const normalizedFilter = value.trim().toLocaleLowerCase();
+        if (this.dataSource.filter === normalizedFilter) {
+            return;
+        }
+
+        if (this.paginator && this.paginator.pageIndex !== 0) {
+            this.paginator.pageIndex = 0;
+        }
+
+        this.dataSource.filter = normalizedFilter;
     }
 
     clearNameSearch() {
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 1be768d6a4..9b55684743 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,161 +20,165 @@
     <sp-basic-header-title-component
         [title]="'Charts' | translate"
     ></sp-basic-header-title-component>
-    <div fxFlex="100" fxLayout="row" fxLayoutAlign="center start">
-        <sp-table
+    @if (isLoading) {
+        <div
             fxFlex="100"
-            [columns]="displayedColumns"
-            [dataSource]="dataSource"
-            [nameSearchConfig]="{ enabled: true, placeholder: 'Search charts' 
}"
-            [assetContextConfig]="assetContextConfig"
-            featureCardId="chart"
-            [showActionsMenu]="true"
-            [rowsClickable]="true"
-            (rowClicked)="openChart($event, true)"
-            matSort
+            fxLayout="row"
+            fxLayoutAlign="center center"
+            class="chart-overview-loading"
         >
-            <ng-container matColumnDef="name">
-                <th mat-header-cell mat-sort-header *matHeaderCellDef>
-                    {{ 'Chart' | translate }}
-                </th>
-                <td
-                    mat-cell
-                    data-cy="data-views-table-overview"
-                    *matCellDef="let element"
-                >
-                    <div
-                        fxLayout="row"
-                        fxLayoutAlign="start center"
-                        fxLayoutGap="5px"
+            <div
+                fxLayout="column"
+                fxLayoutAlign="center center"
+                fxLayoutGap="12px"
+            >
+                <mat-progress-spinner
+                    mode="indeterminate"
+                    diameter="40"
+                ></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"
+                [columns]="displayedColumns"
+                [dataSource]="dataSource"
+                [nameSearchConfig]="nameSearchConfig"
+                [assetContextConfig]="assetContextConfig"
+                featureCardId="chart"
+                [showActionsMenu]="true"
+                [rowsClickable]="true"
+                (rowClicked)="openChart($event, true)"
+                matSort
+            >
+                <ng-container matColumnDef="name">
+                    <th mat-header-cell mat-sort-header *matHeaderCellDef>
+                        {{ 'Chart' | translate }}
+                    </th>
+                    <td
+                        mat-cell
+                        data-cy="data-views-table-overview"
+                        *matCellDef="let element"
                     >
-                        @if (isLegacyMultiSourceChart(element)) {
-                            <mat-icon
-                                [matTooltip]="
-                                    'This chart is outdated and must be 
migrated.'
-                                        | translate
-                                "
-                                style="color: var(--color-warn)"
-                                >warning</mat-icon
-                            >
-                        }
-                        @if (requiresAttention(element)) {
-                            <mat-icon
-                                data-cy="chart-sync-problem-icon"
-                                [matTooltip]="
-                                    'This chart requires attention because the 
dataset schema changed.'
-                                        | translate
-                                "
-                                style="color: var(--color-warn)"
-                                >sync_problem</mat-icon
-                            >
-                        }
-                        <div fxLayout="column" fxLayoutAlign="start start">
-                            <span class="text-sm">{{ element.name }}</span>
+                        <div
+                            fxLayout="row"
+                            fxLayoutAlign="start center"
+                            fxLayoutGap="5px"
+                        >
+                            @if (element.showLegacyWarning) {
+                                <mat-icon
+                                    [matTooltip]="
+                                        'This chart is outdated and must be 
migrated.'
+                                            | translate
+                                    "
+                                    style="color: var(--color-warn)"
+                                    >warning</mat-icon
+                                >
+                            }
+                            @if (element.showRequiresAttentionWarning) {
+                                <mat-icon
+                                    data-cy="chart-sync-problem-icon"
+                                    [matTooltip]="
+                                        'This chart requires attention because 
the dataset schema changed.'
+                                            | translate
+                                    "
+                                    style="color: var(--color-warn)"
+                                    >sync_problem</mat-icon
+                                >
+                            }
+                            <div fxLayout="column" fxLayoutAlign="start start">
+                                <span class="text-sm">{{ element.name }}</span>
+                            </div>
                         </div>
-                    </div>
-                </td>
-            </ng-container>
+                    </td>
+                </ng-container>
 
-            <ng-container matColumnDef="chartType">
-                <th mat-header-cell mat-sort-header *matHeaderCellDef>
-                    {{ 'Type' | translate }}
-                </th>
-                <td mat-cell *matCellDef="let element">
-                    <div
-                        fxLayout="row"
-                        fxLayoutAlign="start center"
-                        class="text-sm"
-                    >
-                        <mat-icon [matTooltip]="getChartTypeName(element)">{{
-                            getChartTypeIcon(element)
-                        }}</mat-icon>
-                    </div>
-                </td>
-            </ng-container>
-            <ng-container matColumnDef="lastModified">
-                <th mat-header-cell mat-sort-header *matHeaderCellDef>
-                    {{ 'Last modified' | translate }}
-                </th>
-                <td mat-cell *matCellDef="let element">
-                    @if (element.lastModifiedEpochMs !== null) {
-                        <div>
-                            {{ formatDate(element.lastModifiedEpochMs) }}
+                <ng-container matColumnDef="chartType">
+                    <th mat-header-cell mat-sort-header *matHeaderCellDef>
+                        {{ 'Type' | translate }}
+                    </th>
+                    <td mat-cell *matCellDef="let element">
+                        <div
+                            fxLayout="row"
+                            fxLayoutAlign="start center"
+                            class="text-sm"
+                        >
+                            <mat-icon [matTooltip]="element.chartTypeName">{{
+                                element.chartTypeIcon
+                            }}</mat-icon>
                         </div>
-                    } @else {
-                        <div>–</div>
-                    }
-                </td>
-            </ng-container>
+                    </td>
+                </ng-container>
+                <ng-container matColumnDef="lastModified">
+                    <th mat-header-cell mat-sort-header *matHeaderCellDef>
+                        {{ 'Last modified' | translate }}
+                    </th>
+                    <td mat-cell *matCellDef="let element">
+                        <div>{{ element.lastModifiedLabel }}</div>
+                    </td>
+                </ng-container>
 
-            <ng-container matColumnDef="createdAt">
-                <th mat-header-cell mat-sort-header *matHeaderCellDef>
-                    {{ 'Created' | translate }}
-                </th>
-                <td mat-cell *matCellDef="let element">
-                    @if (element.createdAtEpochMs !== null) {
-                        <div>
-                            {{ formatDate(element.createdAtEpochMs) }}
-                        </div>
-                    } @else {
-                        <div>–</div>
-                    }
-                </td>
-            </ng-container>
+                <ng-container matColumnDef="createdAt">
+                    <th mat-header-cell mat-sort-header *matHeaderCellDef>
+                        {{ 'Created' | translate }}
+                    </th>
+                    <td mat-cell *matCellDef="let element">
+                        <div>{{ element.createdAtLabel }}</div>
+                    </td>
+                </ng-container>
 
-            <ng-template spTableActions let-element>
-                <button
-                    mat-menu-item
-                    [attr.data-cy]="
-                        'show-data-view-' + element.name.replaceAll(' ', '')
-                    "
-                    (click)="openChart(element, false)"
-                >
-                    <mat-icon>visibility</mat-icon>
-                    <span>{{ 'Show' | translate }}</span>
-                </button>
-                @if (hasDataExplorerWritePrivileges) {
+                <ng-template spTableActions let-element>
                     <button
                         mat-menu-item
-                        [attr.data-cy]="
-                            'edit-data-view-' + element.name.replaceAll(' ', 
'')
-                        "
-                        (click)="openChart(element, true)"
+                        [attr.data-cy]="element.showDataCyId"
+                        (click)="openChart(element, false)"
                     >
-                        <mat-icon>edit</mat-icon>
-                        <span>{{ 'Edit chart' | translate }}</span>
-                    </button>
-                }
-                @if (hasDataExplorerWritePrivileges) {
-                    <button
-                        [attr.data-cy]="
-                            'open-manage-permissions-' +
-                            element.name.replaceAll(' ', '')
-                        "
-                        mat-menu-item
-                        (click)="showManageDialog(element)"
-                    >
-                        <mat-icon>settings</mat-icon>
-                        <span>{{ 'Manage' | translate }}</span>
-                    </button>
-                }
-                @if (hasDataExplorerWritePrivileges) {
-                    <button mat-menu-item (click)="cloneChart(element)">
-                        <mat-icon>flip_to_front</mat-icon>
-                        <span>{{ 'Clone chart' | translate }}</span>
+                        <mat-icon>visibility</mat-icon>
+                        <span>{{ 'Show' | translate }}</span>
                     </button>
-                }
-                @if (hasDataExplorerWritePrivileges) {
-                    <button
-                        mat-menu-item
-                        [matTooltip]=""
-                        [attr.data-cy]="'delete-data-view-' + element.name"
-                        (click)="deleteChart(element)"
-                    >
-                        <mat-icon>delete</mat-icon>
-                        <span>{{ 'Delete chart' | translate }}</span>
-                    </button>
-                }
-            </ng-template>
-        </sp-table>
-    </div>
+                    @if (hasDataExplorerWritePrivileges) {
+                        <button
+                            mat-menu-item
+                            [attr.data-cy]="element.editDataCyId"
+                            (click)="openChart(element, true)"
+                        >
+                            <mat-icon>edit</mat-icon>
+                            <span>{{ 'Edit chart' | translate }}</span>
+                        </button>
+                    }
+                    @if (hasDataExplorerWritePrivileges) {
+                        <button
+                            [attr.data-cy]="element.manageDataCyId"
+                            mat-menu-item
+                            (click)="showManageDialog(element)"
+                        >
+                            <mat-icon>settings</mat-icon>
+                            <span>{{ 'Manage' | translate }}</span>
+                        </button>
+                    }
+                    @if (hasDataExplorerWritePrivileges) {
+                        <button mat-menu-item (click)="cloneChart(element)">
+                            <mat-icon>flip_to_front</mat-icon>
+                            <span>{{ 'Clone chart' | translate }}</span>
+                        </button>
+                    }
+                    @if (hasDataExplorerWritePrivileges) {
+                        <button
+                            mat-menu-item
+                            [matTooltip]=""
+                            [attr.data-cy]="element.deleteDataCyId"
+                            (click)="deleteChart(element)"
+                        >
+                            <mat-icon>delete</mat-icon>
+                            <span>{{ 'Delete chart' | translate }}</span>
+                        </button>
+                    }
+                </ng-template>
+            </sp-table>
+        </div>
+    }
 </div>
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 0968a4a169..4b4ff52f91 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
@@ -65,12 +65,26 @@ import { MatMenuItem } from '@angular/material/menu';
 import { MatIcon } from '@angular/material/icon';
 import { MatTooltip } from '@angular/material/tooltip';
 import { ChartRegistry } from 
'../../../../chart-shared/registry/chart-registry.service';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
 
 type ManageableChart = DataExplorerWidgetModel & {
     name: string;
     description: string;
 };
 
+type ChartOverviewRow = ChartSummaryDto & {
+    chartTypeIcon: string;
+    chartTypeName: string;
+    createdAtLabel: string;
+    lastModifiedLabel: string;
+    showLegacyWarning: boolean;
+    showRequiresAttentionWarning: boolean;
+    showDataCyId: string;
+    editDataCyId: string;
+    manageDataCyId: string;
+    deleteDataCyId: string;
+};
+
 @Component({
     selector: 'sp-data-explorer-overview-table',
     templateUrl: './chart-overview-table.component.html',
@@ -93,6 +107,7 @@ type ManageableChart = DataExplorerWidgetModel & {
         MatMenuItem,
         MatIcon,
         MatTooltip,
+        MatProgressSpinner,
         TranslatePipe,
     ],
 })
@@ -101,9 +116,14 @@ export class ChartOverviewTableComponent implements 
OnInit, OnDestroy {
     hasDataExplorerWritePrivileges: boolean;
 
     @ViewChild(MatSort)
-    sort: MatSort;
+    set sort(sort: MatSort | undefined) {
+        this._sort = sort;
+        if (sort) {
+            this.dataSource.sort = sort;
+        }
+    }
 
-    dataSource = new MatTableDataSource<ChartSummaryDto>();
+    dataSource = new MatTableDataSource<ChartOverviewRow>();
     displayedColumns: string[] = [
         'name',
         'chartType',
@@ -116,8 +136,13 @@ export class ChartOverviewTableComponent implements 
OnInit, OnDestroy {
         resourceLinkType: 'chart',
         resourceIdKey: 'elementId',
     };
-    charts: ChartSummaryDto[] = [];
-    filteredCharts: ChartSummaryDto[] = [];
+    readonly nameSearchConfig = {
+        enabled: true,
+        placeholder: 'Search charts',
+    };
+    isLoading = false;
+    charts: ChartOverviewRow[] = [];
+    filteredCharts: ChartOverviewRow[] = [];
 
     private dataViewService = inject(ChartService);
     private dialog = inject(MatDialog);
@@ -130,6 +155,7 @@ export class ChartOverviewTableComponent implements OnInit, 
OnDestroy {
 
     assetFilter$: Subscription;
     currentFilterIds = new Set<string>();
+    private _sort?: MatSort;
     private chartTypeMetadata = new Map<
         string,
         { icon: string; label: string }
@@ -159,11 +185,20 @@ export class ChartOverviewTableComponent implements 
OnInit, OnDestroy {
     }
 
     getCharts(): void {
-        this.dataViewService.getChartSummary().subscribe(chartSummary => {
-            this.charts = chartSummary.resources.sort((a, b) =>
-                a.name.localeCompare(b.name),
-            );
-            this.applyChartFilters(this.currentFilterIds);
+        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;
+            },
         });
     }
 
@@ -281,30 +316,12 @@ export class ChartOverviewTableComponent implements 
OnInit, OnDestroy {
                 elementIds.has(a.elementId),
             );
         }
-        this.dataSource.sort = this.sort;
+        if (this._sort) {
+            this.dataSource.sort = this._sort;
+        }
         this.dataSource.data = this.filteredCharts;
     }
 
-    getChartTypeIcon(chart: ChartSummaryDto): string {
-        return this.getChartTypeMetadata(chart.widgetType).icon;
-    }
-
-    getChartTypeName(chart: ChartSummaryDto): string {
-        return this.getChartTypeMetadata(chart.widgetType).label;
-    }
-
-    formatDate(timestamp?: number): string {
-        return this.dateFormatService.formatDate(timestamp);
-    }
-
-    isLegacyMultiSourceChart(chart: ChartSummaryDto): boolean {
-        return chart.multiSourceChart;
-    }
-
-    requiresAttention(chart: ChartSummaryDto): boolean {
-        return chart?.healthStatus === 'REQUIRES_ATTENTION';
-    }
-
     private withChart(
         chartSummary: ChartSummaryDto,
         callback: (chart: DataExplorerWidgetModel) => void,
@@ -333,4 +350,32 @@ export class ChartOverviewTableComponent implements 
OnInit, OnDestroy {
         this.chartTypeMetadata.set(widgetType, metadata);
         return metadata;
     }
+
+    private toChartOverviewRow(chart: ChartSummaryDto): ChartOverviewRow {
+        const typeMetadata = this.getChartTypeMetadata(chart.widgetType);
+        const sanitizedName = chart.name.replaceAll(' ', '');
+
+        return {
+            ...chart,
+            chartTypeIcon: typeMetadata.icon,
+            chartTypeName: typeMetadata.label,
+            createdAtLabel:
+                chart.createdAtEpochMs !== null
+                    ? this.dateFormatService.formatDate(chart.createdAtEpochMs)
+                    : '–',
+            lastModifiedLabel:
+                chart.lastModifiedEpochMs !== null
+                    ? this.dateFormatService.formatDate(
+                          chart.lastModifiedEpochMs,
+                      )
+                    : '–',
+            showLegacyWarning: !!chart.multiSourceChart,
+            showRequiresAttentionWarning:
+                chart.healthStatus === 'REQUIRES_ATTENTION',
+            showDataCyId: `show-data-view-${sanitizedName}`,
+            editDataCyId: `edit-data-view-${sanitizedName}`,
+            manageDataCyId: `open-manage-permissions-${sanitizedName}`,
+            deleteDataCyId: `delete-data-view-${chart.name}`,
+        };
+    }
 }
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 9103beb12d..06c5911910 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
@@ -35,3 +35,16 @@
 .mr-10 {
     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/chart-selection.component.html
 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.html
index 8fd0b96a9e..535eed1f08 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
@@ -99,7 +99,23 @@
         </mat-form-field>
     </div>
 
-    @if (charts.length > 0 && filteredCharts.length > 0) {
+    @if (isRefreshing) {
+        <div
+            fxFlex="100"
+            fxLayout="column"
+            fxLayoutAlign="center center"
+            fxLayoutGap="12px"
+            class="chart-selection-loading"
+        >
+            <mat-progress-spinner
+                mode="indeterminate"
+                diameter="36"
+            ></mat-progress-spinner>
+            <span class="chart-selection-loading-text">{{
+                'Loading charts...' | translate
+            }}</span>
+        </div>
+    } @else if (charts.length > 0 && filteredCharts.length > 0) {
         <cdk-virtual-scroll-viewport
             fxFlexFill
             class="chart-selection-results chart-selection-viewport"
diff --git 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.scss
 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.scss
index 50eee74146..6af04c100e 100644
--- 
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.scss
+++ 
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-selection.component.scss
@@ -94,6 +94,15 @@
     max-width: 100%;
 }
 
+.chart-selection-loading {
+    text-align: center;
+}
+
+.chart-selection-loading-text {
+    font-size: var(--font-size-sm);
+    color: var(--color-paragraph);
+}
+
 .chart-selection-empty {
     min-height: 220px;
     text-align: center;
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 e9a346177f..ebba633d40 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
@@ -47,6 +47,7 @@ import {
     MatSuffix,
 } from '@angular/material/form-field';
 import { MatInput } from '@angular/material/input';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
 import { MatTooltip } from '@angular/material/tooltip';
 import {
     CdkFixedSizeVirtualScroll,
@@ -73,6 +74,7 @@ import {
         MatPrefix,
         MatSuffix,
         MatInput,
+        MatProgressSpinner,
         TranslatePipe,
         MatTooltip,
         CdkVirtualScrollViewport,

Reply via email to