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 081769c71b fix: Improve chart selection loading (#4582)
081769c71b is described below
commit 081769c71b6daad6b1fa9c70b977681db377874a
Author: Dominik Riemer <[email protected]>
AuthorDate: Wed Jun 17 18:17:08 2026 +0200
fix: Improve chart selection loading (#4582)
---
ui/deployment/i18n/de.json | 1 +
ui/deployment/i18n/en.json | 1 +
ui/deployment/i18n/pl.json | 1 +
.../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-panel.component.html | 2 +-
.../chart-selection-panel.component.scss | 5 +
.../chart-preview/chart-preview.component.html | 61 ++---
.../chart-preview/chart-preview.component.scss | 32 ++-
.../chart-preview/chart-preview.component.ts | 22 +-
.../chart-selection/chart-selection.component.html | 37 ++-
.../chart-selection/chart-selection.component.scss | 38 ++-
.../chart-selection/chart-selection.component.ts | 33 ++-
.../panel/dashboard-panel.component.html | 12 +-
.../panel/dashboard-panel.component.scss | 21 ++
17 files changed, 438 insertions(+), 253 deletions(-)
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 56d3b9f2c0..da3470b010 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -616,6 +616,7 @@
"Live preview": "Live-Vorschau",
"Load anyway": "Trotzdem laden",
"Loading": "Laden",
+ "Loading charts...": "Lade Diagramme...",
"Location": "Lage",
"Location configuration updated": "Standortkonfiguration aktualisiert",
"Locked": "Gesperrt",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index e58a1a807e..2600d4fae4 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -616,6 +616,7 @@
"Live preview": null,
"Load anyway": null,
"Loading": null,
+ "Loading charts...": null,
"Location": null,
"Location configuration updated": null,
"Locked": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index b9062bcd9f..e2f55e67af 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -616,6 +616,7 @@
"Live preview": "Podgląd na żywo",
"Load anyway": "Załaduj mimo to",
"Loading": "Ładowanie",
+ "Loading charts...": "ładowanie wykresów",
"Location": "Położenie",
"Location configuration updated": "Zaktualizowano konfigurację lokalizacji",
"Locked": "Zablokowane",
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-panel.component.html
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection-panel.component.html
index 37166e0a2e..e1f0fd1508 100644
---
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection-panel.component.html
+++
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection-panel.component.html
@@ -27,7 +27,7 @@
data-cy="designer-panel-data-config"
[label]="'Charts' | translate"
>
- <div class="scroll-tab-content">
+ <div class="scroll-tab-content chart-selection-tab-content">
<sp-chart-selection
(addChartEmitter)="addChartEmitter.emit($event)"
fxFlex="100"
diff --git
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection-panel.component.scss
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection-panel.component.scss
index e280dd74d5..1ef662e462 100644
---
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection-panel.component.scss
+++
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection-panel.component.scss
@@ -15,3 +15,8 @@
* limitations under the License.
*
*/
+
+.chart-selection-tab-content {
+ height: 100%;
+ overflow-y: hidden;
+}
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 56576d9cf0..29eddbab1d 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
@@ -17,48 +17,27 @@
-->
<div
- fxLayout="row"
- fxLayoutAlign="space-between center"
- fxLayoutGap="12px"
class="data-view-preview-outer"
- [attr.data-cy]="'add-data-view-btn-' + chart.name.replaceAll(' ', '')"
+ [attr.data-cy]="dataCyId"
+ [attr.title]="chart.name"
(click)="addChart()"
>
- <div
- fxLayout="row"
- fxLayoutAlign="start center"
- fxLayoutGap="12px"
- fxFlex
- class="chart-preview-main"
- >
- <div
- class="chart-preview-icon-shell"
- fxLayout="row"
- fxLayoutAlign="center center"
- [matTooltip]="widgetTypeLabel"
- >
+ <div class="chart-preview-header">
+ <div class="chart-preview-icon-shell" [attr.title]="widgetTypeLabel">
<mat-icon>{{ widgetTypeIcon }}</mat-icon>
</div>
- <div fxFlex fxLayout="column" class="chart-preview-copy">
- <div
- fxLayout="row"
- fxLayoutAlign="start center"
- fxLayoutGap="8px"
- class="chart-preview-title-row"
- [matTooltip]="chart.name"
- >
- <h5 fxFlex class="chart-preview-title">{{ chart.name }}</h5>
+ <div class="chart-preview-copy">
+ <div class="chart-preview-title-row">
+ <h5 class="chart-preview-title">{{ chart.name }}</h5>
</div>
- <div
- fxLayout="row"
- fxLayoutAlign="start center"
- fxLayoutGap="12px"
- class="chart-preview-meta"
- >
+ <div class="chart-preview-meta">
@if (chart.datasetName) {
- <span class="chart-preview-meta-item
chart-preview-dataset">
+ <span
+ class="chart-preview-meta-item chart-preview-dataset"
+ [attr.title]="chart.datasetName"
+ >
{{ chart.datasetName }}
</span>
}
@@ -66,26 +45,18 @@
</div>
</div>
- <div
- fxLayout="row"
- fxLayoutAlign="end center"
- fxLayoutGap="4px"
- class="chart-preview-actions"
- >
+ <div class="chart-preview-actions">
<button
mat-icon-button
type="button"
- [matTooltip]="'Preview chart' | translate"
+ [attr.aria-label]="'Preview chart' | translate"
+ [attr.title]="'Preview chart' | translate"
(click)="openPreview($event)"
data-cy="chart-preview-feature-card"
>
<mat-icon>visibility</mat-icon>
</button>
- <div
- class="chart-preview-add-indicator"
- fxLayout="row"
- fxLayoutAlign="center center"
- >
+ <div class="chart-preview-add-indicator">
<mat-icon>add</mat-icon>
</div>
</div>
diff --git
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.scss
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.scss
index 5282cd1fc1..3d7a82ba85 100644
---
a/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.scss
+++
b/ui/src/app/dashboard/components/panel/chart-selection-panel/chart-selection/chart-preview/chart-preview.component.scss
@@ -17,6 +17,10 @@
*/
.data-view-preview-outer {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 8px;
min-height: 72px;
border: 1px solid color-mix(in srgb, var(--color-bg-3) 68%, transparent);
border-radius: 14px;
@@ -41,7 +45,6 @@
);
background: color-mix(in srgb, var(--color-bg-1) 86%, white 14%);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
- transform: translateY(-1px);
cursor: pointer;
}
@@ -50,6 +53,9 @@ mat-icon {
}
.chart-preview-icon-shell {
+ display: flex;
+ align-items: center;
+ justify-content: center;
width: 40px;
min-width: 40px;
height: 40px;
@@ -63,11 +69,23 @@ mat-icon {
}
.chart-preview-main {
+ display: contents;
+}
+
+.chart-preview-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
min-width: 0;
+ overflow: hidden;
}
.chart-preview-copy {
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
min-width: 0;
+ overflow: hidden;
}
.chart-preview-title-row {
@@ -88,13 +106,16 @@ mat-icon {
.chart-preview-meta {
min-width: 0;
+ display: block;
width: 100%;
+ overflow: hidden;
margin-top: 2px;
color: var(--color-paragraph);
font-size: var(--font-size-sm);
}
.chart-preview-meta-item {
+ display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -113,10 +134,19 @@ mat-icon {
}
.chart-preview-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 4px;
flex-shrink: 0;
+ width: 100%;
+ margin-left: 0;
}
.chart-preview-add-indicator {
+ display: flex;
+ align-items: center;
+ justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
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 153a49c417..4ca3306b26 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
@@ -17,6 +17,7 @@
*/
import {
+ ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
@@ -26,15 +27,8 @@ import {
} from '@angular/core';
import { ChartSummaryDto } from '@streampipes/platform-services';
import { ChartRegistry } from
'../../../../../../chart-shared/registry/chart-registry.service';
-import {
- FlexDirective,
- LayoutAlignDirective,
- LayoutDirective,
- LayoutGapDirective,
-} from '@ngbracket/ngx-layout/flex';
import { MatIcon } from '@angular/material/icon';
import { MatIconButton } from '@angular/material/button';
-import { MatTooltip } from '@angular/material/tooltip';
import { FeatureCardService } from '@streampipes/shared-ui';
import { TranslatePipe } from '@ngx-translate/core';
@@ -42,16 +36,8 @@ import { TranslatePipe } from '@ngx-translate/core';
selector: 'sp-chart-preview',
templateUrl: './chart-preview.component.html',
styleUrls: ['./chart-preview.component.scss'],
- imports: [
- LayoutDirective,
- FlexDirective,
- LayoutGapDirective,
- LayoutAlignDirective,
- MatIcon,
- MatIconButton,
- MatTooltip,
- TranslatePipe,
- ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [MatIcon, MatIconButton, TranslatePipe],
})
export class ChartPreviewComponent implements OnInit {
private widgetRegistryService = inject(ChartRegistry);
@@ -62,6 +48,7 @@ export class ChartPreviewComponent implements OnInit {
widgetTypeLabel = '';
widgetTypeIcon = 'insert_chart';
+ dataCyId = '';
@Output()
addChartEmitter: EventEmitter<string> = new EventEmitter<string>();
@@ -72,6 +59,7 @@ export class ChartPreviewComponent implements OnInit {
);
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 {
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 d77aeb90f0..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,21 +99,44 @@
</mat-form-field>
</div>
- @if (charts.length > 0 && filteredCharts.length > 0) {
+ @if (isRefreshing) {
<div
- fxFlexFill
+ fxFlex="100"
fxLayout="column"
- fxLayoutGap="8px"
- class="chart-selection-results"
+ 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"
+ [itemSize]="chartItemSize"
+ [minBufferPx]="chartItemSize * 4"
+ [maxBufferPx]="chartItemSize * 8"
>
- @for (chart of filteredCharts; track chart.elementId) {
+ <div
+ *cdkVirtualFor="
+ let chart of filteredCharts;
+ trackBy: trackByChartId
+ "
+ class="chart-selection-item"
+ >
<sp-chart-preview
[chart]="chart"
(addChartEmitter)="addChartEmitter.emit($event)"
>
</sp-chart-preview>
- }
- </div>
+ </div>
+ </cdk-virtual-scroll-viewport>
} @else if (charts.length === 0) {
<div fxLayoutAlign="center center" fxLayout="column" fxFlex="100">
<span class="no-widget-hint">{{
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 445d919e70..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
@@ -17,7 +17,12 @@
*/
.chart-selection-shell {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
height: 100%;
+ min-height: 0;
+ overflow: hidden;
padding: 6px 6px 8px;
background: linear-gradient(
180deg,
@@ -27,6 +32,7 @@
}
.chart-selection-toolbar {
+ flex: 0 0 auto;
position: sticky;
top: 0;
z-index: 2;
@@ -64,7 +70,37 @@
}
.chart-selection-results {
- padding-top: 2px;
+ flex: 1 1 0;
+ min-height: 0;
+ padding-top: 0;
+}
+
+.chart-selection-viewport {
+ box-sizing: border-box;
+ width: 100%;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.chart-selection-item {
+ box-sizing: border-box;
+ width: 100%;
+ height: 132px;
+ padding: 0 0 6px;
+}
+
+.chart-selection-viewport ::ng-deep .cdk-virtual-scroll-content-wrapper {
+ width: 100%;
+ 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 {
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 9059f2b978..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
@@ -16,7 +16,15 @@
*
*/
-import { Component, EventEmitter, inject, OnInit, Output } from
'@angular/core';
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ inject,
+ OnInit,
+ Output,
+} from '@angular/core';
import { ChartService, ChartSummaryDto } from '@streampipes/platform-services';
import { AuthService } from '../../../../../services/auth.service';
import { UserPrivilege } from '../../../../../core/auth/user-privilege.enum';
@@ -39,12 +47,19 @@ 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,
+ CdkVirtualForOf,
+ CdkVirtualScrollViewport,
+} from '@angular/cdk/scrolling';
@Component({
selector: 'sp-chart-selection',
templateUrl: './chart-selection.component.html',
styleUrls: ['./chart-selection.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
FlexDirective,
FlexFillDirective,
@@ -59,8 +74,12 @@ import { MatTooltip } from '@angular/material/tooltip';
MatPrefix,
MatSuffix,
MatInput,
+ MatProgressSpinner,
TranslatePipe,
MatTooltip,
+ CdkVirtualScrollViewport,
+ CdkFixedSizeVirtualScroll,
+ CdkVirtualForOf,
],
})
export class ChartSelectionComponent implements OnInit {
@@ -68,6 +87,7 @@ export class ChartSelectionComponent implements OnInit {
private authService = inject(AuthService);
private chartRegistryService = inject(ChartRegistry);
private chartRoutingService = inject(ChartRoutingService);
+ private cdr = inject(ChangeDetectorRef);
@Output()
addChartEmitter: EventEmitter<string> = new EventEmitter();
@@ -76,6 +96,7 @@ export class ChartSelectionComponent implements OnInit {
filteredCharts: ChartSummaryDto[] = [];
searchTerm = '';
isRefreshing = false;
+ readonly chartItemSize = 132;
hasChartWritePrivileges: boolean = false;
@@ -93,18 +114,22 @@ 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();
},
});
}
@@ -112,17 +137,23 @@ export class ChartSelectionComponent implements OnInit {
onSearchTermChanged(value: string): void {
this.searchTerm = value;
this.applySearch();
+ this.cdr.markForCheck();
}
clearSearch(): void {
this.searchTerm = '';
this.applySearch();
+ this.cdr.markForCheck();
}
hasActiveSearch(): boolean {
return this.searchTerm.trim().length > 0;
}
+ trackByChartId(index: number, chart: ChartSummaryDto): string {
+ return chart.elementId;
+ }
+
private applySearch(): void {
const query = this.searchTerm.trim().toLowerCase();
if (!query) {
diff --git
a/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
b/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
index 86eda57b85..d60106f42e 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
@@ -75,17 +75,23 @@
position="end"
class="designer-panel"
>
- <div fxLayout="column" fxFlex="100">
+ <div
+ fxLayout="column"
+ fxFlex="100"
+ style="min-height: 0; overflow: hidden"
+ >
@if (editMode) {
<div
fxFlex="100"
class="designer-panel-content"
fxLayout="column"
+ style="min-height: 0"
>
<div
fxFlex="100"
fxLayout="column"
class="no-overflow"
+ style="min-height: 0"
>
<mat-tab-group
color="accent"
@@ -99,7 +105,9 @@
data-cy="designer-panel-data-config"
[label]="'Charts' | translate"
>
- <div
class="scroll-tab-content">
+ <div
+ class="scroll-tab-content
chart-selection-tab-content"
+ >
<sp-chart-selection
(addChartEmitter)="
addChartToDashboard(
diff --git
a/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss
b/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss
index 3708772e94..2655e51d21 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss
@@ -45,6 +45,27 @@
.designer-panel {
width: 350px;
+ overflow: hidden;
+}
+
+.chart-selection-tab-content {
+ height: 100%;
+ overflow-y: hidden;
+}
+
+.designer-panel ::ng-deep .mat-drawer-inner-container {
+ overflow: hidden;
+}
+
+.designer-panel ::ng-deep .mat-mdc-tab-body-wrapper,
+.designer-panel ::ng-deep .mat-mdc-tab-body,
+.designer-panel ::ng-deep .mat-mdc-tab-body-content {
+ height: 100%;
+ min-height: 0;
+}
+
+.designer-panel ::ng-deep .mat-mdc-tab-body-content {
+ overflow: hidden;
}
.panel-toggle-button {