You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2026/06/13 08:15:38 UTC
(streampipes) 02/03: Improve chart selection, add chart search
This is an automated email from the ASF dual-hosted git repository.
dominikriemer pushed a commit to branch load-summary-charts-in-dashboard
in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 4380fb7288c71db12fac01f0634b4427ea96ad9d
Author: Dominik Riemer <do...@gmail.com>
AuthorDate: Sat Jun 13 10:07:38 2026 +0200
Improve chart selection, add chart search
---
.../chart-preview/chart-preview.component.html | 85 +++++++++++----
.../chart-preview/chart-preview.component.scss | 110 +++++++++++++++++++-
.../chart-preview/chart-preview.component.ts | 26 ++++-
.../chart-selection/chart-selection.component.html | 114 +++++++++++++++++----
.../chart-selection/chart-selection.component.scss | 56 +++++++++-
.../chart-selection/chart-selection.component.ts | 59 ++++++++++-
6 files changed, 398 insertions(+), 52 deletions(-)
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 52d80b569e..56576d9cf0 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,32 +18,75 @@
<div
fxLayout="row"
+ fxLayoutAlign="space-between center"
+ fxLayoutGap="12px"
class="data-view-preview-outer"
[attr.data-cy]="'add-data-view-btn-' + chart.name.replaceAll(' ', '')"
- (click)="addChartEmitter.emit(chart.elementId)"
+ (click)="addChart()"
>
- <div fxFlex fxLayout="column">
- <h5>{{ chart.name }}</h5>
- <div fxLayout="row" fxLayoutGap="15px" class="text-sm">
- <span fxLayoutAlign="start center"
- ><mat-icon color="primary">insert_chart</mat-icon></span
- >
- <span fxFlex fxLayoutAlign="start center">{{
- widgetTypeLabel
- }}</span>
+ <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"
+ >
+ <mat-icon>{{ widgetTypeIcon }}</mat-icon>
</div>
- @if (chart.datasetName) {
- <div fxLayout="row" fxLayoutGap="15px" class="text-sm">
- <span fxLayoutAlign="start center"
- ><mat-icon color="primary">folder</mat-icon></span
- >
- <span fxFlex fxLayoutAlign="start center">{{
- chart.datasetName
- }}</span>
+
+ <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>
- }
+
+ <div
+ fxLayout="row"
+ fxLayoutAlign="start center"
+ fxLayoutGap="12px"
+ class="chart-preview-meta"
+ >
+ @if (chart.datasetName) {
+ <span class="chart-preview-meta-item chart-preview-dataset">
+ {{ chart.datasetName }}
+ </span>
+ }
+ </div>
+ </div>
</div>
- <div fxLayoutAlign="end center">
- <i class="material-icons">add</i>
+
+ <div
+ fxLayout="row"
+ fxLayoutAlign="end center"
+ fxLayoutGap="4px"
+ class="chart-preview-actions"
+ >
+ <button
+ mat-icon-button
+ type="button"
+ [matTooltip]="'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"
+ >
+ <mat-icon>add</mat-icon>
+ </div>
</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 1a98fbb087..5282cd1fc1 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,16 +17,118 @@
*/
.data-view-preview-outer {
- border: 1px solid var(--color-bg-2);
- padding: 10px;
+ min-height: 72px;
+ border: 1px solid color-mix(in srgb, var(--color-bg-3) 68%, transparent);
+ border-radius: 14px;
+ padding: 10px 12px;
+ background: color-mix(
+ in srgb,
+ var(--color-bg-0) 90%,
+ var(--color-bg-1) 10%
+ );
+ transition:
+ border-color 120ms ease,
+ background-color 120ms ease,
+ transform 120ms ease,
+ box-shadow 120ms ease;
}
.data-view-preview-outer:hover {
- border-color: var(--color-bg-3);
- background: var(--color-bg-1);
+ border-color: color-mix(
+ in srgb,
+ var(--color-primary) 34%,
+ var(--color-bg-3)
+ );
+ 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;
}
mat-icon {
--mat-icon-color: var(--mat-sys-secondary);
}
+
+.chart-preview-icon-shell {
+ width: 40px;
+ min-width: 40px;
+ height: 40px;
+ border-radius: 12px;
+ background: linear-gradient(
+ 180deg,
+ color-mix(in srgb, var(--color-primary) 14%, white) 0%,
+ color-mix(in srgb, var(--color-primary) 8%, var(--color-bg-1)) 100%
+ );
+ color: var(--color-primary);
+}
+
+.chart-preview-main {
+ min-width: 0;
+}
+
+.chart-preview-copy {
+ min-width: 0;
+}
+
+.chart-preview-title-row {
+ min-width: 0;
+ width: 100%;
+}
+
+.chart-preview-title {
+ min-width: 0;
+ flex: 1 1 auto;
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: var(--font-size-sm);
+ line-height: 1.25;
+}
+
+.chart-preview-meta {
+ min-width: 0;
+ width: 100%;
+ margin-top: 2px;
+ color: var(--color-paragraph);
+ font-size: var(--font-size-sm);
+}
+
+.chart-preview-meta-item {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.chart-preview-dataset {
+ color: var(--color-text);
+}
+
+.chart-preview-meta-dot {
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background: var(--color-secondary);
+ flex-shrink: 0;
+}
+
+.chart-preview-actions {
+ flex-shrink: 0;
+}
+
+.chart-preview-add-indicator {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: color-mix(in srgb, var(--color-primary) 14%, transparent);
+ color: var(--color-primary);
+}
+
+.chart-preview-actions button {
+ opacity: 0.82;
+}
+
+.data-view-preview-outer:hover .chart-preview-actions button,
+.data-view-preview-outer:hover .chart-preview-add-indicator {
+ opacity: 1;
+}
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 15fefe9e9d..153a49c417 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
@@ -33,6 +33,10 @@ import {
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';
@Component({
selector: 'sp-chart-preview',
@@ -44,22 +48,38 @@ import { MatIcon } from '@angular/material/icon';
LayoutGapDirective,
LayoutAlignDirective,
MatIcon,
+ MatIconButton,
+ MatTooltip,
+ TranslatePipe,
],
})
export class ChartPreviewComponent implements OnInit {
private widgetRegistryService = inject(ChartRegistry);
+ private featureCardService = inject(FeatureCardService);
@Input()
chart: ChartSummaryDto;
- widgetTypeLabel: string;
+ widgetTypeLabel = '';
+ widgetTypeIcon = 'insert_chart';
@Output()
addChartEmitter: EventEmitter<string> = new EventEmitter<string>();
ngOnInit(): void {
- this.widgetTypeLabel = this.widgetRegistryService.getChartTemplate(
+ const template = this.widgetRegistryService.getChartTemplate(
this.chart.widgetType,
- ).label;
+ );
+ this.widgetTypeLabel = template?.label ?? this.chart.widgetType;
+ this.widgetTypeIcon = template?.icon ?? 'insert_chart';
+ }
+
+ addChart(): void {
+ this.addChartEmitter.emit(this.chart.elementId);
+ }
+
+ openPreview(event: MouseEvent): void {
+ event.stopPropagation();
+ this.featureCardService.openFeatureCard('chart', this.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 377df74878..150d24b364 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
@@ -16,35 +16,111 @@
~
-->
-<div fxFlex="100" class="m-10" fxLayoutGap="10px" fxLayout="column">
- @if (charts.length > 0) {
- @for (chart of charts; track chart) {
- <div>
- <sp-chart-preview
- [chart]="chart"
- (addChartEmitter)="addChartEmitter.emit($event)"
- >
- </sp-chart-preview>
+<div
+ fxFlex="100"
+ fxLayout="column"
+ fxLayoutGap="12px"
+ class="chart-selection-shell"
+>
+ <div fxLayout="column" fxLayoutGap="10px" class="chart-selection-toolbar">
+ <div
+ fxLayout="row"
+ fxLayoutAlign="space-between center"
+ fxLayoutGap="12px"
+ >
+ <div fxLayout="column" class="chart-selection-heading">
+ <span class="chart-selection-caption">
+ {{ filteredCharts.length }}
+ @if (filteredCharts.length === 1) {
+ {{ 'chart available' | translate }}
+ } @else {
+ {{ 'charts available' | translate }}
+ }
+ </span>
</div>
- }
- } @else {
- <div fxLayoutAlign="center center" fxLayout="column" fxFlex="100">
- <span class="no-widget-hint">{{
- 'No charts found - create a new chart first to add it to this dashboard.'
- | translate
- }}</span>
@if (hasChartWritePrivileges) {
<button
- mat-flat-button
- class="mt-10 mat-basic"
+ mat-icon-button
+ class="chart-selection-create"
data-cy="create-chart-button"
+ [matTooltip]="'Create chart' | translate"
(click)="navigateToDataViewCreation()"
>
<mat-icon>add</mat-icon>
- {{ 'Create chart' | translate }}
</button>
}
</div>
+
+ <mat-form-field
+ appearance="outline"
+ subscriptSizing="dynamic"
+ class="chart-selection-search"
+ >
+ <mat-icon matPrefix>search</mat-icon>
+ <input
+ [placeholder]="'Search charts' | translate"
+ matInput
+ [value]="searchTerm"
+ (input)="onSearchTermChanged($any($event.target).value)"
+ data-cy="chart-selection-search"
+ />
+ @if (hasActiveSearch()) {
+ <button
+ mat-icon-button
+ matSuffix
+ type="button"
+ (click)="clearSearch()"
+ [attr.aria-label]="'Clear search' | translate"
+ >
+ <mat-icon>close</mat-icon>
+ </button>
+ }
+ </mat-form-field>
+ </div>
+
+ @if (charts.length > 0 && filteredCharts.length > 0) {
+ <div
+ fxFlexFill
+ fxLayout="column"
+ fxLayoutGap="8px"
+ class="chart-selection-results"
+ >
+ @for (chart of filteredCharts; track chart.elementId) {
+ <sp-chart-preview
+ [chart]="chart"
+ (addChartEmitter)="addChartEmitter.emit($event)"
+ >
+ </sp-chart-preview>
+ }
+ </div>
+ } @else if (charts.length === 0) {
+ <div fxLayoutAlign="center center" fxLayout="column" fxFlex="100">
+ <span class="no-widget-hint">{{
+ 'No charts found - create a new chart first to add it to this dashboard.'
+ | translate
+ }}</span>
+ </div>
+ } @else {
+ <div
+ fxLayoutAlign="center center"
+ fxLayout="column"
+ fxLayoutGap="8px"
+ fxFlex="100"
+ class="chart-selection-empty"
+ >
+ <mat-icon class="chart-selection-empty-icon">search_off</mat-icon>
+ <span class="no-widget-hint">{{
+ 'No charts match your search.' | translate
+ }}</span>
+ <button
+ mat-button
+ type="button"
+ class="chart-selection-reset"
+ (click)="clearSearch()"
+ >
+ {{ 'Clear search' | translate }}
+ </button>
+ </div>
}
</div>
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 8cf72cbca9..548782df84 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
@@ -16,11 +16,63 @@
*
*/
-.m-10 {
- margin: 10px;
+.chart-selection-shell {
+ height: 100%;
+ padding: 6px 6px 8px;
+ background: linear-gradient(
+ 180deg,
+ var(--color-bg-0) 0%,
+ color-mix(in srgb, var(--color-bg-0) 96%, transparent) 100%
+ );
+}
+
+.chart-selection-toolbar {
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ padding: 2px 0 4px;
+}
+
+.chart-selection-heading {
+ min-width: 0;
+}
+
+.chart-selection-caption {
+ font-size: var(--font-size-sm);
+ color: var(--color-paragraph);
+}
+
+.chart-selection-create {
+ flex-shrink: 0;
+}
+
+.chart-selection-search {
+ width: 100%;
+}
+
+.chart-selection-results {
+ padding-top: 2px;
+}
+
+.chart-selection-empty {
+ min-height: 220px;
+ text-align: center;
+}
+
+.chart-selection-empty-icon {
+ width: 32px;
+ height: 32px;
+ font-size: 32px;
+ color: var(--color-secondary);
+}
+
+.chart-selection-reset {
+ min-width: 0;
}
.no-widget-hint {
font-size: var(--font-size-md);
text-align: center;
+ color: var(--color-paragraph);
+ max-width: 280px;
}
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 8d362f4c31..c253cc2e7d 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
@@ -17,20 +17,29 @@
*/
import { Component, EventEmitter, inject, OnInit, Output } from '@angular/core';
-import { ChartSummaryDto, ChartService } from '@streampipes/platform-services';
+import { ChartService, ChartSummaryDto } from '@streampipes/platform-services';
import { Router } from '@angular/router';
import { AuthService } from '../../../../../services/auth.service';
import { UserPrivilege } from '../../../../../core/auth/user-privilege.enum';
+import { ChartRegistry } from '../../../../../chart-shared/registry/chart-registry.service';
import {
FlexDirective,
+ FlexFillDirective,
LayoutAlignDirective,
LayoutDirective,
LayoutGapDirective,
} from '@ngbracket/ngx-layout/flex';
import { ChartPreviewComponent } from './chart-preview/chart-preview.component';
-import { MatButton } from '@angular/material/button';
+import { MatButton, MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { TranslatePipe } from '@ngx-translate/core';
+import {
+ MatFormField,
+ MatPrefix,
+ MatSuffix,
+} from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { MatTooltip } from '@angular/material/tooltip';
@Component({
selector: 'sp-chart-selection',
@@ -38,25 +47,34 @@ import { TranslatePipe } from '@ngx-translate/core';
styleUrls: ['./chart-selection.component.scss'],
imports: [
FlexDirective,
+ FlexFillDirective,
LayoutGapDirective,
LayoutDirective,
ChartPreviewComponent,
LayoutAlignDirective,
MatButton,
+ MatIconButton,
MatIcon,
+ MatFormField,
+ MatPrefix,
+ MatSuffix,
+ MatInput,
TranslatePipe,
+ MatTooltip,
],
})
export class ChartSelectionComponent implements OnInit {
private dataViewService = inject(ChartService);
private router = inject(Router);
-
private authService = inject(AuthService);
+ private chartRegistryService = inject(ChartRegistry);
@Output()
addChartEmitter: EventEmitter<string> = new EventEmitter();
charts: ChartSummaryDto[] = [];
+ filteredCharts: ChartSummaryDto[] = [];
+ searchTerm = '';
hasChartWritePrivileges: boolean = false;
@@ -65,6 +83,7 @@ export class ChartSelectionComponent implements OnInit {
this.charts = chartSummary.resources.sort((a, b) =>
a.name.localeCompare(b.name),
);
+ this.applySearch();
});
this.hasChartWritePrivileges = this.authService.hasRole(
@@ -78,4 +97,38 @@ export class ChartSelectionComponent implements OnInit {
state: { omitConfirm: true },
});
}
+
+ onSearchTermChanged(value: string): void {
+ this.searchTerm = value;
+ this.applySearch();
+ }
+
+ clearSearch(): void {
+ this.searchTerm = '';
+ this.applySearch();
+ }
+
+ hasActiveSearch(): boolean {
+ return this.searchTerm.trim().length > 0;
+ }
+
+ private applySearch(): void {
+ const query = this.searchTerm.trim().toLowerCase();
+ if (!query) {
+ this.filteredCharts = this.charts;
+ 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)),
+ );
+ }
}