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 <[email protected]> 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)), + ); + } }
