From 3cac50b3bad96a767d66fd38dc6e743d4b285335 Mon Sep 17 00:00:00 2001 From: Nikita Nazarov Date: Wed, 28 May 2025 17:01:51 +0300 Subject: [PATCH 1/5] feat(datacat-ui): fixing dashboards --- .../edit-panel/edit-panel.component.html | 6 +- .../edit-panel/edit-panel.component.ts | 16 ++++-- .../panels-grid/panels-grid.consts.ts | 9 ++- .../src/shared/ui/charts/bar/bar.style.ts | 0 .../src/shared/ui/charts/chart.service.ts | 44 +++++++++++++++ .../src/shared/ui/charts/chart.types.ts | 11 ++++ .../shared/ui/charts/line/line.component.html | 3 + .../shared/ui/charts/line/line.component.scss | 0 .../shared/ui/charts/line/line.component.ts | 55 +++++++++++++++++++ .../src/shared/ui/charts/line/line.style.ts | 25 +++++++++ .../legend/legend-options.component.html | 13 +++++ .../legend/legend-options.component.ts | 25 +++++++++ .../ui/charts/options/options.style.scss | 12 ++++ .../panel-chart/panel-chart.component.html | 12 ++++ .../panel-chart/panel-chart.component.ts | 29 ++++++++++ .../src/shared/ui/charts/pie/pie.style.ts | 0 .../unsupported/unsupported.component.html | 1 + .../unsupported/unsupported.component.scss | 4 ++ .../unsupported/unsupported.component.ts | 9 +++ .../panel-visualization.component.ts | 18 ++++++ .../time-range-select.component.scss | 3 +- 21 files changed, 282 insertions(+), 13 deletions(-) create mode 100644 frontend/datacat-ui/src/shared/ui/charts/bar/bar.style.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/chart.service.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/chart.types.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/line/line.component.html create mode 100644 frontend/datacat-ui/src/shared/ui/charts/line/line.component.scss create mode 100644 frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/line/line.style.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.html create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/options.style.scss create mode 100644 frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.html create mode 100644 frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/pie/pie.style.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.html create mode 100644 frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.scss create mode 100644 frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.ts diff --git a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html index dfda1a3..a77e102 100644 --- a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html +++ b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html @@ -2,11 +2,7 @@
- +
diff --git a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts index da093b7..a5789e9 100644 --- a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts +++ b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts @@ -1,5 +1,4 @@ import { AfterViewInit, Component, Input, ViewChild } from '@angular/core'; -import { PanelVisualizationComponent } from '../../../shared/ui/panel-visualization'; import { PanelVisualizationOptionsComponent } from '../../../shared/ui/panel-visualization-options'; import { PanelModule } from 'primeng/panel'; import { @@ -30,6 +29,8 @@ import { TimeSeries } from '../../../entities/dashboards/data.types'; import { PanelDataService } from '../panels-grid/panel-data.service'; import { TimeRangeSelectComponent } from '../../../shared/ui/time-range-select/time-range-select.component'; import { TimeRange } from '../../../entities/dashboards/etc.types'; +import { PanelChartComponent } from '../../../shared/ui/charts/panel-chart/panel-chart.component'; +import { ChartService } from '../../../shared/ui/charts/chart.service'; @Component({ standalone: true, @@ -37,7 +38,6 @@ import { TimeRange } from '../../../entities/dashboards/etc.types'; templateUrl: './edit-panel.component.html', styleUrl: './edit-panel.component.scss', imports: [ - PanelVisualizationComponent, PanelVisualizationOptionsComponent, PanelModule, ReactiveFormsModule, @@ -46,8 +46,9 @@ import { TimeRange } from '../../../entities/dashboards/etc.types'; DataSourceSelectComponent, ButtonModule, TimeRangeSelectComponent, + PanelChartComponent, ], - providers: [PanelDataService], + providers: [ChartService], }) export class EditPanelComponent implements AfterViewInit { private _panelId?: string; @@ -70,10 +71,15 @@ export class EditPanelComponent implements AfterViewInit { step: '00:30:00', from: (() => { const date = new Date(); - date.setMinutes(date.getMinutes() - 360); + date.setTime(date.getTime() - 4 * 60 * 60 * 1000); + date.setMilliseconds(0); + return date; + })(), + to: (() => { + const date = new Date(); + date.setMilliseconds(0); return date; })(), - to: new Date(), }); protected editForm = new FormGroup({ diff --git a/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.consts.ts b/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.consts.ts index bffa8aa..d78ef85 100644 --- a/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.consts.ts +++ b/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.consts.ts @@ -14,8 +14,13 @@ export const DEFAULT_TIME_RANGE: TimeRange = { step: '00:30:00', from: (() => { const date = new Date(); - date.setMinutes(date.getMinutes() - 360); + date.setTime(date.getTime() - 4 * 60 * 60 * 1000); + date.setMilliseconds(0); + return date; + })(), + to: (() => { + const date = new Date(); + date.setMilliseconds(0); return date; })(), - to: new Date(), }; diff --git a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.style.ts b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.style.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/datacat-ui/src/shared/ui/charts/chart.service.ts b/frontend/datacat-ui/src/shared/ui/charts/chart.service.ts new file mode 100644 index 0000000..334dfcd --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/chart.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { TimeSeries } from './chart.types'; +import { ApiService } from '../../services/datacat-generated-client'; +import { VisualizationType } from '../../../entities'; + +@Injectable({ + providedIn: 'root', +}) +export class ChartService { + public readonly type$: Observable; + public readonly data$: Observable; + public readonly style$: Observable; + + public query?: string; + public dataSourceName?: string; + + private typeSubject = new BehaviorSubject( + VisualizationType.LINE, + ); + private dataSubject = new BehaviorSubject({}); + private styleSubject = new BehaviorSubject({}); + + constructor(private api: ApiService) { + this.type$ = this.typeSubject.asObservable(); + this.data$ = this.dataSubject.asObservable(); + this.style$ = this.styleSubject.asObservable(); + } + + public updateStyle(style: any): void { + this.styleSubject.next({ + ...this.styleSubject.value, + ...style, + }); + } + + public showTimeRange(from: Date, to: Date, step: string): void {} + + public addPointAt(date: Date): void {} + + private loadTimeRange() { + this.api; + } +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/chart.types.ts b/frontend/datacat-ui/src/shared/ui/charts/chart.types.ts new file mode 100644 index 0000000..8b8d4e7 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/chart.types.ts @@ -0,0 +1,11 @@ +export type TimeSeries = { + name: string; + labels: string[]; + points: DataPoint[]; +}; + +export type DataPoint = { + value: number; + timestamp: Date; + labels: string[]; +}; diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.html b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.html new file mode 100644 index 0000000..f7abb5f --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.scss b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts new file mode 100644 index 0000000..4cac847 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts @@ -0,0 +1,55 @@ +import { Component, Host, Input, OnInit, ViewChild } from '@angular/core'; +import { LineStyle, LineStyleScheme } from './line.style'; +import { TimeSeries } from '../chart.types'; +import { ChartModule, UIChart } from 'primeng/chart'; +import { ProgressSpinnerModule } from 'primeng/progressspinner'; +import { ChartService } from '../chart.service'; + +@Component({ + standalone: true, + selector: 'datacat-line-chart', + templateUrl: 'line.component.html', + styleUrl: 'line.component.scss', + imports: [ChartModule, ProgressSpinnerModule], +}) +export class LineChartComponent implements OnInit { + @ViewChild(UIChart) chart?: UIChart; + + protected chartjsOptions: any; + protected chartjsData: any; + + constructor(private chartService: ChartService) { + this.chartService.style$.subscribe((style) => { + this.updateStyle(style); + }); + this.chartService.data$.subscribe((data) => { + this.updateData(data); + }); + } + + ngOnInit() { + this.chartjsData = { + labels: [], + datasets: [], + }; + this.chartjsOptions = {}; + } + + private updateStyle(style: any) { + const parseResult = LineStyleScheme.safeParse(style); + if (parseResult.success) { + this.chartjsOptions = this.getChartjsOptionsFromLineStyle( + parseResult.data, + ); + } + } + + private updateData(data: TimeSeries[]) { + this.chartjsData = { + labels: [], + datasets: [], + }; + } + + private getChartjsOptionsFromLineStyle(lineStyle: LineStyle) {} +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.style.ts b/frontend/datacat-ui/src/shared/ui/charts/line/line.style.ts new file mode 100644 index 0000000..737b8df --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/line/line.style.ts @@ -0,0 +1,25 @@ +import { z } from 'zod/v4'; + +export const LineStyleScheme = z.object({ + title: z.object({ + enabled: z.boolean().default(false), + text: z.string().default(''), + }), + legend: z.object({ + enabled: z.boolean().default(true), + position: z.enum(['top', 'left', 'bottom', 'right']).default('top'), + }), + tooltip: z.object({ + enabled: z.boolean().default(true), + }), + axis: z.object({ + xAxisTitle: z.string().default('Date').optional(), + xAxisMin: z.date().optional(), + xAxisMax: z.date().optional(), + yAxisTitle: z.string().default('Value').optional(), + yAxisMin: z.number().optional(), + yAxisMax: z.number().optional(), + }), +}); + +export type LineStyle = z.infer; diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.html b/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.html new file mode 100644 index 0000000..420e589 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.html @@ -0,0 +1,13 @@ + +
+

Enabled

+ +
+
+

Position

+ +
+ diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.ts b/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.ts new file mode 100644 index 0000000..b8e3b2a --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { StyleService } from '../../style.service'; +import { CheckboxModule } from 'primeng/checkbox'; +import { SelectModule } from 'primeng/select'; + +@Component({ + standalone: true, + selector: 'datacat-legend-options', + templateUrl: 'legend-options.component.html', + styleUrl: '../options.style.scss', + imports: [ReactiveFormsModule, CheckboxModule, SelectModule], +}) +export class LegendOptionsComponent { + protected form = new FormGroup({ + enabled: new FormControl(true), + position: new FormControl<'top' | 'bottom' | 'left' | 'right'>('top'), + }); + + constructor(private styleService: StyleService) { + this.form.valueChanges.subscribe((options) => + this.styleService.updateStyle(options), + ); + } +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/options.style.scss b/frontend/datacat-ui/src/shared/ui/charts/options/options.style.scss new file mode 100644 index 0000000..4b5c3fa --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/options.style.scss @@ -0,0 +1,12 @@ +form { + display: flex; + flex-direction: column; + gap: var(--p-padding-md); + padding: var(--p-padding-md); +} + +div { + display: flex; + flex-direction: column; + gap: var(--p-padding-sm); +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.html b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.html new file mode 100644 index 0000000..d13a0d4 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.html @@ -0,0 +1,12 @@ +@if (loading) { + +} @else { + @switch (type) { + @case (VisualizationType.LINE) { + + } + @default { + + } + } +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts new file mode 100644 index 0000000..f4e545a --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts @@ -0,0 +1,29 @@ +import { Component, Host, Input } from '@angular/core'; +import { VisualizationType } from '../../../../entities'; +import { LineChartComponent } from '../line/line.component'; +import { UnsupportedChartComponent } from '../unsupported/unsupported.component'; +import { ChartService } from '../chart.service'; +import { ProgressSpinnerModule } from 'primeng/progressspinner'; + +@Component({ + standalone: true, + selector: 'datacat-panel-chart', + templateUrl: 'panel-chart.component.html', + imports: [ + ProgressSpinnerModule, + LineChartComponent, + UnsupportedChartComponent, + ], +}) +export class PanelChartComponent { + @Input() public loading: boolean = false; + + protected type?: VisualizationType; + protected VisualizationType = VisualizationType; + + constructor(private chartService: ChartService) { + this.chartService.type$.subscribe((type) => { + this.type = type; + }); + } +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.style.ts b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.style.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.html b/frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.html new file mode 100644 index 0000000..c8ee7ee --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.html @@ -0,0 +1 @@ +
Unsupported chart
diff --git a/frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.scss b/frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.scss new file mode 100644 index 0000000..f9cc014 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.scss @@ -0,0 +1,4 @@ +div { + text-align: center; + color: var(--p-surface-400); +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.ts b/frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.ts new file mode 100644 index 0000000..2321fc2 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/unsupported/unsupported.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'datacat-unsupported-chart', + templateUrl: 'unsupported.component.html', + styleUrl: 'unsupported.component.scss', +}) +export class UnsupportedChartComponent {} diff --git a/frontend/datacat-ui/src/shared/ui/panel-visualization/panel-visualization.component.ts b/frontend/datacat-ui/src/shared/ui/panel-visualization/panel-visualization.component.ts index afe7bc3..2077ac5 100644 --- a/frontend/datacat-ui/src/shared/ui/panel-visualization/panel-visualization.component.ts +++ b/frontend/datacat-ui/src/shared/ui/panel-visualization/panel-visualization.component.ts @@ -56,6 +56,24 @@ export class PanelVisualizationComponent { this.chartjsOptions.plugins.tooltip.enabled = settings.tooltip?.enabled; + switch (this.visualizationType) { + case VisualizationType.LINE: { + this.chartjsOptions = { + ...this.chartjsOptions, + x: { + type: 'time', + time: { + unit: 'day', + }, + title: { + display: true, + text: 'Date', + }, + }, + }; + } + } + this.chartRef?.chart?.update(); } diff --git a/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.component.scss b/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.component.scss index b87a400..d455a20 100644 --- a/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.component.scss +++ b/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.component.scss @@ -1,7 +1,8 @@ .container { display: flex; + flex-direction: column; gap: var(--p-padding-md); - align-items: center; + align-items: start; } .item { From 79ee3318a75edbf7a8b77e975c7a74a77616a462 Mon Sep 17 00:00:00 2001 From: Nikita Nazarov Date: Thu, 29 May 2025 01:03:53 +0300 Subject: [PATCH 2/5] feat(datacat-ui): keep fixing issues --- frontend/datacat-ui/package-lock.json | 21 +++++ frontend/datacat-ui/package.json | 2 + .../edit-panel/edit-panel.component.html | 8 +- .../edit-panel/edit-panel.component.ts | 20 ++++- .../shared/ui/charts/bar/bar.component.html | 7 ++ .../shared/ui/charts/bar/bar.component.scss | 4 + .../src/shared/ui/charts/bar/bar.component.ts | 81 +++++++++++++++++++ .../src/shared/ui/charts/bar/bar.style.ts | 41 ++++++++++ .../src/shared/ui/charts/chart.service.ts | 78 ++++++++++++++++-- .../src/shared/ui/charts/chart.types.ts | 4 +- .../shared/ui/charts/line/line.component.html | 6 +- .../shared/ui/charts/line/line.component.scss | 4 + .../shared/ui/charts/line/line.component.ts | 72 ++++++++++++++--- .../src/shared/ui/charts/line/line.style.ts | 54 ++++++++----- .../options/axis/axis-options.component.html | 14 ++++ .../options/axis/axis-options.component.ts | 46 +++++++++++ .../legend/legend-options.component.ts | 15 +++- .../ui/charts/options/options.component.html | 43 ++++++++++ .../ui/charts/options/options.component.scss | 31 +++++++ .../ui/charts/options/options.component.ts | 52 ++++++++++++ .../ui/charts/options/options.style.scss | 5 +- .../title/title-options.component.html | 10 +++ .../options/title/title-options.component.ts | 34 ++++++++ .../tooltip/tooltip-options.component.html | 6 ++ .../tooltip/tooltip-options.component.ts | 33 ++++++++ .../panel-chart/panel-chart.component.html | 10 ++- .../panel-chart/panel-chart.component.scss | 3 + .../panel-chart/panel-chart.component.ts | 7 +- .../shared/ui/charts/pie/pie.component.html | 7 ++ .../shared/ui/charts/pie/pie.component.scss | 4 + .../src/shared/ui/charts/pie/pie.component.ts | 73 +++++++++++++++++ .../src/shared/ui/charts/pie/pie.style.ts | 31 +++++++ 32 files changed, 769 insertions(+), 57 deletions(-) create mode 100644 frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.html create mode 100644 frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.scss create mode 100644 frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/axis/axis-options.component.html create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/axis/axis-options.component.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/options.component.html create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/options.component.scss create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/options.component.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/title/title-options.component.html create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/title/title-options.component.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/tooltip/tooltip-options.component.html create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/tooltip/tooltip-options.component.ts create mode 100644 frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.scss create mode 100644 frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.html create mode 100644 frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.scss create mode 100644 frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts diff --git a/frontend/datacat-ui/package-lock.json b/frontend/datacat-ui/package-lock.json index 00789f9..acac713 100644 --- a/frontend/datacat-ui/package-lock.json +++ b/frontend/datacat-ui/package-lock.json @@ -20,6 +20,8 @@ "@primeng/themes": "^19.0.3", "angular-gridster2": "^18.0.1", "chart.js": "^4.4.7", + "chartjs-adapter-luxon": "^1.3.1", + "luxon": "^3.6.1", "primeicons": "^7.0.0", "primeng": "^18.0.2", "rxjs": "~7.8.0", @@ -6625,6 +6627,16 @@ "pnpm": ">=8" } }, + "node_modules/chartjs-adapter-luxon": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.3.1.tgz", + "integrity": "sha512-yxHov3X8y+reIibl1o+j18xzrcdddCLqsXhriV2+aQ4hCR66IYFchlRXUvrJVoxglJ380pgytU7YWtoqdIgqhg==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=3.0.0", + "luxon": ">=1.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -10847,6 +10859,15 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", diff --git a/frontend/datacat-ui/package.json b/frontend/datacat-ui/package.json index 4415a22..a3e35c1 100644 --- a/frontend/datacat-ui/package.json +++ b/frontend/datacat-ui/package.json @@ -23,6 +23,8 @@ "@primeng/themes": "^19.0.3", "angular-gridster2": "^18.0.1", "chart.js": "^4.4.7", + "chartjs-adapter-luxon": "^1.3.1", + "luxon": "^3.6.1", "primeicons": "^7.0.0", "primeng": "^18.0.2", "rxjs": "~7.8.0", diff --git a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html index a77e102..90830ad 100644 --- a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html +++ b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html @@ -2,7 +2,7 @@
- +
@@ -36,9 +36,5 @@ />
- +
diff --git a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts index a5789e9..76016ea 100644 --- a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts +++ b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts @@ -24,13 +24,14 @@ import { import { ApiService } from '../../../shared/services/datacat-generated-client'; import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; import { ButtonModule } from 'primeng/button'; -import { finalize } from 'rxjs'; +import { finalize, timer } from 'rxjs'; import { TimeSeries } from '../../../entities/dashboards/data.types'; import { PanelDataService } from '../panels-grid/panel-data.service'; import { TimeRangeSelectComponent } from '../../../shared/ui/time-range-select/time-range-select.component'; import { TimeRange } from '../../../entities/dashboards/etc.types'; import { PanelChartComponent } from '../../../shared/ui/charts/panel-chart/panel-chart.component'; import { ChartService } from '../../../shared/ui/charts/chart.service'; +import { ChartOptionsComponent } from '../../../shared/ui/charts/options/options.component'; @Component({ standalone: true, @@ -38,7 +39,6 @@ import { ChartService } from '../../../shared/ui/charts/chart.service'; templateUrl: './edit-panel.component.html', styleUrl: './edit-panel.component.scss', imports: [ - PanelVisualizationOptionsComponent, PanelModule, ReactiveFormsModule, InputTextModule, @@ -47,6 +47,7 @@ import { ChartService } from '../../../shared/ui/charts/chart.service'; ButtonModule, TimeRangeSelectComponent, PanelChartComponent, + ChartOptionsComponent, ], providers: [ChartService], }) @@ -95,15 +96,17 @@ export class EditPanelComponent implements AfterViewInit { private api: ApiService, private logger: ToastLoggerService, private panelDataService: PanelDataService, + private chartService: ChartService, ) { this.panelDataService.data$.subscribe((v) => (this.data = v)); this.timeRangeControl.valueChanges.subscribe((tr) => { - if (tr) this.panelDataService.loadTimeRange(tr); + this.refreshPreview(); }); this.editForm.get('dataSourceId')?.valueChanges.subscribe((id) => { if (id && this.panel) { this.panel.dataSource!.id = id; this.panelDataService.panel = this.panel; + this.chartService.setDataSourceName(this.panel.dataSource!.name); this.refreshPreview(); } }); @@ -111,6 +114,7 @@ export class EditPanelComponent implements AfterViewInit { if (q && this.panel) { this.panel.query = q; this.panelDataService.panel = this.panel; + this.chartService.setQuery(this.panel.query); this.refreshPreview(); } }); @@ -147,6 +151,9 @@ export class EditPanelComponent implements AfterViewInit { ) as VisualizationSettings, }; this.panelDataService.panel = this.panel; + this.chartService.setQuery(this.panel.query); + this.chartService.setDataSourceName(this.panel.dataSource!.name); + this.chartService.updateStyle(this.panel.visualizationSettings); this.refreshPreview(); this.optionsComponent?.setVisualizationSettings( @@ -168,6 +175,13 @@ export class EditPanelComponent implements AfterViewInit { protected refreshPreview() { this.panelDataService.loadTimeRange(this.timeRangeControl.getRawValue()!); + + const timeRange = this.timeRangeControl.getRawValue()!; + this.chartService.loadTimeRange( + timeRange.from, + timeRange.to, + timeRange.step, + ); } protected saveChanges() { diff --git a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.html b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.html new file mode 100644 index 0000000..5bf9b2e --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.html @@ -0,0 +1,7 @@ +
+ @if (isError) { +

Error

+ } @else { + + } +
diff --git a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.scss b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.scss new file mode 100644 index 0000000..8d519de --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.scss @@ -0,0 +1,4 @@ +p { + text-align: center; + color: var(--p-surface-400); +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts new file mode 100644 index 0000000..eeb3185 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts @@ -0,0 +1,81 @@ +import { Component, ViewChild } from '@angular/core'; +import { BarStyle, BarStyleScheme } from './bar.style'; +import { TimeSeries } from '../chart.types'; +import { ChartModule, UIChart } from 'primeng/chart'; +import { ProgressSpinnerModule } from 'primeng/progressspinner'; +import { ChartService } from '../chart.service'; + +@Component({ + standalone: true, + selector: 'datacat-bar-chart', + templateUrl: 'bar.component.html', + styleUrl: 'bar.component.scss', + imports: [ChartModule, ProgressSpinnerModule], +}) +export class BarChartComponent { + @ViewChild(UIChart) chart?: UIChart; + + protected isError: boolean = false; + protected chartjsOptions: any; + protected chartjsData: any; + + constructor(private chartService: ChartService) { + this.chartService.style$.subscribe((style) => { + this.updateStyle(style); + }); + this.chartService.data$.subscribe((data) => { + this.updateData(data); + }); + this.chartService.error$.subscribe((error) => { + this.isError = error; + }); + } + + private updateStyle(style: any) { + const parseResult = BarStyleScheme.safeParse(style); + if (parseResult.success) { + this.chartjsOptions = this.getChartjsOptionsFromBarStyle( + parseResult.data, + ); + this.chart?.chart?.update(); + } + } + + private updateData(data: TimeSeries[]): void { + this.chartjsData = { + labels: [], + datasets: data.map((ts) => { + return { + label: ts.name + JSON.stringify(ts.labels), + data: ts.points.map((pt) => { + return { + x: pt.timestamp.toISOString(), + y: pt.value, + }; + }), + }; + }), + }; + this.chart?.chart?.update(); + } + + private getChartjsOptionsFromBarStyle(barStyle: BarStyle): any { + return { + animation: false, + maintainAspectRatio: false, + plugins: { + legend: { + display: barStyle.legend.enabled, + position: barStyle.legend.position, + }, + title: { + display: barStyle.title.enabled, + text: barStyle.title.text, + }, + tooltip: { + enabled: barStyle.tooltip.enabled, + }, + }, + }; + } +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.style.ts b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.style.ts index e69de29..1966e57 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.style.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.style.ts @@ -0,0 +1,41 @@ +import { z } from 'zod/v4'; + +export const BarStyleScheme = z.object({ + title: z + .object({ + enabled: z.boolean().default(false), + text: z.string().default(''), + }) + .default({ + enabled: false, + text: '', + }), + legend: z + .object({ + enabled: z.boolean().default(true), + position: z.enum(['top', 'left', 'bottom', 'right']).default('top'), + }) + .default({ + enabled: true, + position: 'top', + }), + tooltip: z + .object({ + enabled: z.boolean().default(true), + }) + .default({ + enabled: true, + }), + axis: z + .object({ + xAxisTitle: z.string().default('Date').optional(), + xAxisMin: z.date().optional(), + xAxisMax: z.date().optional(), + yAxisTitle: z.string().default('Value').optional(), + yAxisMin: z.number().optional(), + yAxisMax: z.number().optional(), + }) + .default({}), +}); + +export type BarStyle = z.infer; diff --git a/frontend/datacat-ui/src/shared/ui/charts/chart.service.ts b/frontend/datacat-ui/src/shared/ui/charts/chart.service.ts index 334dfcd..dc18cb3 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/chart.service.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/chart.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { TimeSeries } from './chart.types'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { DataPoint, TimeSeries } from './chart.types'; import { ApiService } from '../../services/datacat-generated-client'; import { VisualizationType } from '../../../entities'; +import { ThemeSelectionService } from '../../../features/appearence/select-theme/select-theme.service'; @Injectable({ providedIn: 'root', @@ -11,6 +12,7 @@ export class ChartService { public readonly type$: Observable; public readonly data$: Observable; public readonly style$: Observable; + public readonly error$: Observable; public query?: string; public dataSourceName?: string; @@ -18,13 +20,33 @@ export class ChartService { private typeSubject = new BehaviorSubject( VisualizationType.LINE, ); - private dataSubject = new BehaviorSubject({}); + private dataSubject = new BehaviorSubject([]); private styleSubject = new BehaviorSubject({}); + private errorSubject = new BehaviorSubject(false); + + public get type(): VisualizationType { + return this.typeSubject.value; + } + + public get data(): any { + return this.dataSubject.value; + } + + public get style(): any { + return this.styleSubject.value; + } + + private loadTimeRangeSubscription?: Subscription; constructor(private api: ApiService) { this.type$ = this.typeSubject.asObservable(); this.data$ = this.dataSubject.asObservable(); this.style$ = this.styleSubject.asObservable(); + this.error$ = this.errorSubject.asObservable(); + } + + public setType(type: VisualizationType): void { + this.typeSubject.next(type); } public updateStyle(style: any): void { @@ -34,11 +56,53 @@ export class ChartService { }); } - public showTimeRange(from: Date, to: Date, step: string): void {} + public setQuery(query: string): void { + this.query = query; + } - public addPointAt(date: Date): void {} + public setDataSourceName(name: string): void { + this.dataSourceName = name; + } - private loadTimeRange() { - this.api; + public loadTimeRange(from: Date, to: Date, step: string): void { + if (!this.dataSourceName || !this.query) return; + + this.loadTimeRangeSubscription?.unsubscribe(); + this.errorSubject.next(false); + this.loadTimeRangeSubscription = this.api + .getApiV1MetricsQueryRange( + this.dataSourceName, + this.query, + 'undefined' as any, + null, + from, + to, + step, + ) + .subscribe({ + next: (data) => { + const timeSeries = + data.map((ts) => { + return { + name: ts.metricName!, + labels: ts.labels!, + points: + ts.points?.map((p) => { + return { + value: p.value!, + timestamp: p.timestamp!, + labels: p.labels!, + }; + }) || [], + }; + }) || []; + this.dataSubject.next(timeSeries); + }, + error: () => { + this.errorSubject.next(true); + }, + }); } + + public addPointAt(date: Date): void {} } diff --git a/frontend/datacat-ui/src/shared/ui/charts/chart.types.ts b/frontend/datacat-ui/src/shared/ui/charts/chart.types.ts index 8b8d4e7..bf830d3 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/chart.types.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/chart.types.ts @@ -1,11 +1,11 @@ export type TimeSeries = { name: string; - labels: string[]; + labels: { [key: string]: string }; points: DataPoint[]; }; export type DataPoint = { value: number; timestamp: Date; - labels: string[]; + labels: { [key: string]: string }; }; diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.html b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.html index f7abb5f..933ba5f 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.html +++ b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.html @@ -1,3 +1,7 @@
- + @if (isError) { +

Error

+ } @else { + + }
diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.scss b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.scss index e69de29..8d519de 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.scss +++ b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.scss @@ -0,0 +1,4 @@ +p { + text-align: center; + color: var(--p-surface-400); +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts index 4cac847..310343b 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts @@ -1,4 +1,4 @@ -import { Component, Host, Input, OnInit, ViewChild } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; import { LineStyle, LineStyleScheme } from './line.style'; import { TimeSeries } from '../chart.types'; import { ChartModule, UIChart } from 'primeng/chart'; @@ -12,9 +12,10 @@ import { ChartService } from '../chart.service'; styleUrl: 'line.component.scss', imports: [ChartModule, ProgressSpinnerModule], }) -export class LineChartComponent implements OnInit { +export class LineChartComponent { @ViewChild(UIChart) chart?: UIChart; + protected isError: boolean = false; protected chartjsOptions: any; protected chartjsData: any; @@ -25,31 +26,76 @@ export class LineChartComponent implements OnInit { this.chartService.data$.subscribe((data) => { this.updateData(data); }); - } - - ngOnInit() { - this.chartjsData = { - labels: [], - datasets: [], - }; - this.chartjsOptions = {}; + this.chartService.error$.subscribe((error) => { + this.isError = error; + }); } private updateStyle(style: any) { const parseResult = LineStyleScheme.safeParse(style); + console.log(parseResult); if (parseResult.success) { this.chartjsOptions = this.getChartjsOptionsFromLineStyle( parseResult.data, ); + this.chart?.chart?.update(); } } - private updateData(data: TimeSeries[]) { + private updateData(data: TimeSeries[]): void { this.chartjsData = { labels: [], - datasets: [], + datasets: data.map((ts) => { + return { + label: ts.name + JSON.stringify(ts.labels), + data: ts.points.map((pt) => { + return { + x: pt.timestamp.toISOString(), + y: pt.value, + }; + }), + }; + }), }; + this.chart?.chart?.update(); } - private getChartjsOptionsFromLineStyle(lineStyle: LineStyle) {} + private getChartjsOptionsFromLineStyle(lineStyle: LineStyle): any { + return { + animation: false, + maintainAspectRatio: false, + plugins: { + legend: { + display: lineStyle.legend.enabled, + position: lineStyle.legend.position, + }, + title: { + display: lineStyle.title.enabled, + text: lineStyle.title.text, + }, + tooltip: { + enabled: lineStyle.tooltip.enabled, + }, + }, + scales: { + x: { + type: 'time', + min: lineStyle.axis.xAxisMin, + max: lineStyle.axis.xAxisMax, + title: { + display: lineStyle.axis.xAxisTitle != undefined, + text: lineStyle.axis.xAxisTitle, + }, + }, + y: { + min: lineStyle.axis.yAxisMin, + max: lineStyle.axis.yAxisMax, + title: { + display: lineStyle.axis.yAxisTitle != undefined, + text: lineStyle.axis.yAxisTitle, + }, + }, + }, + }; + } } diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.style.ts b/frontend/datacat-ui/src/shared/ui/charts/line/line.style.ts index 737b8df..fd92828 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/line/line.style.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/line/line.style.ts @@ -1,25 +1,41 @@ import { z } from 'zod/v4'; export const LineStyleScheme = z.object({ - title: z.object({ - enabled: z.boolean().default(false), - text: z.string().default(''), - }), - legend: z.object({ - enabled: z.boolean().default(true), - position: z.enum(['top', 'left', 'bottom', 'right']).default('top'), - }), - tooltip: z.object({ - enabled: z.boolean().default(true), - }), - axis: z.object({ - xAxisTitle: z.string().default('Date').optional(), - xAxisMin: z.date().optional(), - xAxisMax: z.date().optional(), - yAxisTitle: z.string().default('Value').optional(), - yAxisMin: z.number().optional(), - yAxisMax: z.number().optional(), - }), + title: z + .object({ + enabled: z.boolean().default(false), + text: z.string().default(''), + }) + .default({ + enabled: false, + text: '', + }), + legend: z + .object({ + enabled: z.boolean().default(true), + position: z.enum(['top', 'left', 'bottom', 'right']).default('top'), + }) + .default({ + enabled: true, + position: 'top', + }), + tooltip: z + .object({ + enabled: z.boolean().default(true), + }) + .default({ + enabled: true, + }), + axis: z + .object({ + xAxisTitle: z.string().default('Date').nullish(), + xAxisMin: z.date().nullish(), + xAxisMax: z.date().nullish(), + yAxisTitle: z.string().default('Value').nullish(), + yAxisMin: z.number().nullish(), + yAxisMax: z.number().nullish(), + }) + .default({}), }); export type LineStyle = z.infer; diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/axis/axis-options.component.html b/frontend/datacat-ui/src/shared/ui/charts/options/axis/axis-options.component.html new file mode 100644 index 0000000..544d1e4 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/axis/axis-options.component.html @@ -0,0 +1,14 @@ +
+
+

X axis

+ + + +
+
+

Y axis

+ + + +
+
diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/axis/axis-options.component.ts b/frontend/datacat-ui/src/shared/ui/charts/options/axis/axis-options.component.ts new file mode 100644 index 0000000..7b1ac57 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/axis/axis-options.component.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { CheckboxModule } from 'primeng/checkbox'; +import { ChartService } from '../../chart.service'; +import { InputTextModule } from 'primeng/inputtext'; +import { DatePickerModule } from 'primeng/datepicker'; +import { InputNumberModule } from 'primeng/inputnumber'; + +@Component({ + standalone: true, + selector: 'datacat-axis-options', + templateUrl: 'axis-options.component.html', + styleUrl: '../options.style.scss', + imports: [ + ReactiveFormsModule, + CheckboxModule, + InputTextModule, + InputNumberModule, + DatePickerModule, + ], +}) +export class AxisOptionsComponent { + protected form = new FormGroup({ + xAxisTitle: new FormControl(null), + xAxisMin: new FormControl(null), + xAxisMax: new FormControl(null), + yAxisTitle: new FormControl(null), + yAxisMin: new FormControl(null), + yAxisMax: new FormControl(null), + }); + + constructor(private chartService: ChartService) { + this.form.valueChanges.subscribe((options) => + this.chartService.updateStyle({ + axis: { + ...options, + }, + }), + ); + this.chartService.style$.subscribe((style) => { + if (style.axis) { + this.form.setValue(style.axis, { emitEvent: false }); + } + }); + } +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.ts b/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.ts index b8e3b2a..55770f5 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.ts @@ -1,8 +1,8 @@ import { Component } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { StyleService } from '../../style.service'; import { CheckboxModule } from 'primeng/checkbox'; import { SelectModule } from 'primeng/select'; +import { ChartService } from '../../chart.service'; @Component({ standalone: true, @@ -17,9 +17,18 @@ export class LegendOptionsComponent { position: new FormControl<'top' | 'bottom' | 'left' | 'right'>('top'), }); - constructor(private styleService: StyleService) { + constructor(private chartService: ChartService) { this.form.valueChanges.subscribe((options) => - this.styleService.updateStyle(options), + this.chartService.updateStyle({ + legend: { + ...options, + }, + }), ); + this.chartService.style$.subscribe((style) => { + if (style.legend) { + this.form.setValue(style.legend, { emitEvent: false }); + } + }); } } diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/options.component.html b/frontend/datacat-ui/src/shared/ui/charts/options/options.component.html new file mode 100644 index 0000000..ba6c0a4 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/options.component.html @@ -0,0 +1,43 @@ + +
+

Visualization

+ +
+
+ + + Title + + + + + + Legend + + + + + + Tooltip + + + + + + + @switch (type) { + @case (VisualizationType.LINE) { + + Axes + + + + + } + @case (VisualizationType.BAR) {} + @case (VisualizationType.PIE) {} + @default {} + } + +
+
diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/options.component.scss b/frontend/datacat-ui/src/shared/ui/charts/options/options.component.scss new file mode 100644 index 0000000..84d79da --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/options.component.scss @@ -0,0 +1,31 @@ +.sub { + display: flex; + flex-direction: column; + gap: var(--p-padding-md); +} + +.attr { + display: flex; + flex-direction: column; + gap: var(--p-padding-xs); +} + +.attr p { + color: var(--p-surface-400); +} + +.pad-lr { + padding-left: var(--p-padding-md); + padding-right: var(--p-padding-md); +} + +.pad-tb { + padding-top: var(--p-padding-md); + padding-bottom: var(--p-padding-md); +} + +.options { + margin-bottom: -1px; + overflow: hidden; + border-radius: var(--p-panel-border-radius); +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/options.component.ts b/frontend/datacat-ui/src/shared/ui/charts/options/options.component.ts new file mode 100644 index 0000000..453a163 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/options.component.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { ChartService } from '../chart.service'; +import { VisualizationType } from '../../../../entities'; +import { PanelModule } from 'primeng/panel'; +import { AccordionModule } from 'primeng/accordion'; +import { SelectModule } from 'primeng/select'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { LegendOptionsComponent } from './legend/legend-options.component'; +import { TitleOptionsComponent } from './title/title-options.component'; +import { TooltipOptionsComponent } from './tooltip/tooltip-options.component'; +import { AxisOptionsComponent } from './axis/axis-options.component'; + +@Component({ + standalone: true, + selector: 'datacat-chart-options', + templateUrl: 'options.component.html', + styleUrl: 'options.component.scss', + imports: [ + ReactiveFormsModule, + PanelModule, + AccordionModule, + SelectModule, + LegendOptionsComponent, + TitleOptionsComponent, + TooltipOptionsComponent, + AxisOptionsComponent, + ], +}) +export class ChartOptionsComponent { + protected VisualizationType = VisualizationType; + protected selectableTypes = Object.values(VisualizationType).filter( + (t) => t !== VisualizationType.UNKNOWN, + ); + protected typeControl = new FormControl( + VisualizationType.LINE, + ); + + protected get type(): VisualizationType { + return this.typeControl.value!; + } + + constructor(private chartService: ChartService) { + this.chartService.type$.subscribe((type) => { + this.typeControl.setValue(type, { emitEvent: false }); + }); + this.typeControl.valueChanges.subscribe((type) => { + if (type) { + this.chartService.setType(type); + } + }); + } +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/options.style.scss b/frontend/datacat-ui/src/shared/ui/charts/options/options.style.scss index 4b5c3fa..af74e90 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/options/options.style.scss +++ b/frontend/datacat-ui/src/shared/ui/charts/options/options.style.scss @@ -2,7 +2,6 @@ form { display: flex; flex-direction: column; gap: var(--p-padding-md); - padding: var(--p-padding-md); } div { @@ -10,3 +9,7 @@ div { flex-direction: column; gap: var(--p-padding-sm); } + +p { + color: var(--p-surface-400); +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/title/title-options.component.html b/frontend/datacat-ui/src/shared/ui/charts/options/title/title-options.component.html new file mode 100644 index 0000000..9ddd0ed --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/title/title-options.component.html @@ -0,0 +1,10 @@ +
+
+

Enabled

+ +
+
+

Text

+ +
+
diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/title/title-options.component.ts b/frontend/datacat-ui/src/shared/ui/charts/options/title/title-options.component.ts new file mode 100644 index 0000000..98d39ce --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/title/title-options.component.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { CheckboxModule } from 'primeng/checkbox'; +import { ChartService } from '../../chart.service'; +import { InputTextModule } from 'primeng/inputtext'; + +@Component({ + standalone: true, + selector: 'datacat-title-options', + templateUrl: 'title-options.component.html', + styleUrl: '../options.style.scss', + imports: [ReactiveFormsModule, CheckboxModule, InputTextModule], +}) +export class TitleOptionsComponent { + protected form = new FormGroup({ + enabled: new FormControl(true), + text: new FormControl(''), + }); + + constructor(private chartService: ChartService) { + this.form.valueChanges.subscribe((options) => + this.chartService.updateStyle({ + title: { + ...options, + }, + }), + ); + this.chartService.style$.subscribe((style) => { + if (style.title) { + this.form.setValue(style.title, { emitEvent: false }); + } + }); + } +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/tooltip/tooltip-options.component.html b/frontend/datacat-ui/src/shared/ui/charts/options/tooltip/tooltip-options.component.html new file mode 100644 index 0000000..d892279 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/tooltip/tooltip-options.component.html @@ -0,0 +1,6 @@ +
+
+

Enabled

+ +
+
diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/tooltip/tooltip-options.component.ts b/frontend/datacat-ui/src/shared/ui/charts/options/tooltip/tooltip-options.component.ts new file mode 100644 index 0000000..51be979 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/tooltip/tooltip-options.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { CheckboxModule } from 'primeng/checkbox'; +import { SelectModule } from 'primeng/select'; +import { ChartService } from '../../chart.service'; + +@Component({ + standalone: true, + selector: 'datacat-tooltip-options', + templateUrl: 'tooltip-options.component.html', + styleUrl: '../options.style.scss', + imports: [ReactiveFormsModule, CheckboxModule, SelectModule], +}) +export class TooltipOptionsComponent { + protected form = new FormGroup({ + enabled: new FormControl(true), + }); + + constructor(private chartService: ChartService) { + this.form.valueChanges.subscribe((options) => + this.chartService.updateStyle({ + tooltip: { + ...options, + }, + }), + ); + this.chartService.style$.subscribe((style) => { + if (style.tooltip) { + this.form.setValue(style.tooltip, { emitEvent: false }); + } + }); + } +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.html b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.html index d13a0d4..6fb4ec5 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.html +++ b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.html @@ -1,10 +1,18 @@ @if (loading) { - +
+ +
} @else { @switch (type) { @case (VisualizationType.LINE) { } + @case (VisualizationType.BAR) { + + } + @case (VisualizationType.PIE) { + + } @default { } diff --git a/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.scss b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.scss new file mode 100644 index 0000000..cc26754 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.scss @@ -0,0 +1,3 @@ +div { + text-align: center; +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts index f4e545a..f57a011 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts @@ -1,17 +1,22 @@ -import { Component, Host, Input } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { VisualizationType } from '../../../../entities'; import { LineChartComponent } from '../line/line.component'; import { UnsupportedChartComponent } from '../unsupported/unsupported.component'; import { ChartService } from '../chart.service'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; +import { BarChartComponent } from '../bar/bar.component'; +import { PieChartComponent } from '../pie/pie.component'; @Component({ standalone: true, selector: 'datacat-panel-chart', templateUrl: 'panel-chart.component.html', + styleUrl: 'panel-chart.component.scss', imports: [ ProgressSpinnerModule, LineChartComponent, + BarChartComponent, + PieChartComponent, UnsupportedChartComponent, ], }) diff --git a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.html b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.html new file mode 100644 index 0000000..260b8f9 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.html @@ -0,0 +1,7 @@ +
+ @if (isError) { +

Error

+ } @else { + + } +
diff --git a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.scss b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.scss new file mode 100644 index 0000000..8d519de --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.scss @@ -0,0 +1,4 @@ +p { + text-align: center; + color: var(--p-surface-400); +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts new file mode 100644 index 0000000..2c70691 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts @@ -0,0 +1,73 @@ +import { Component, ViewChild } from '@angular/core'; +import { PieStyle, PieStyleScheme } from './pie.style'; +import { TimeSeries } from '../chart.types'; +import { ChartModule, UIChart } from 'primeng/chart'; +import { ProgressSpinnerModule } from 'primeng/progressspinner'; +import { ChartService } from '../chart.service'; + +@Component({ + standalone: true, + selector: 'datacat-pie-chart', + templateUrl: 'pie.component.html', + styleUrl: 'pie.component.scss', + imports: [ChartModule, ProgressSpinnerModule], +}) +export class PieChartComponent { + @ViewChild(UIChart) chart?: UIChart; + + protected isError: boolean = false; + protected chartjsOptions: any; + protected chartjsData: any; + + constructor(private chartService: ChartService) { + this.chartService.style$.subscribe((style) => { + this.updateStyle(style); + }); + this.chartService.data$.subscribe((data) => { + this.updateData(data); + }); + this.chartService.error$.subscribe((error) => { + this.isError = error; + }); + } + + private updateStyle(style: any) { + const parseResult = PieStyleScheme.safeParse(style); + if (parseResult.success) { + this.chartjsOptions = this.getChartjsOptionsFromPieStyle( + parseResult.data, + ); + this.chart?.chart?.update(); + } + } + + private updateData(data: TimeSeries[]): void { + this.chartjsData = { + labels: data.map((ts) => JSON.stringify(ts.labels)) || [], + datasets: { + data: data.map((ts) => ts.points[0].value) || [], + }, + }; + this.chart?.chart?.update(); + } + + private getChartjsOptionsFromPieStyle(pieStyle: PieStyle): any { + return { + animation: false, + maintainAspectRatio: false, + plugins: { + legend: { + display: pieStyle.legend.enabled, + position: pieStyle.legend.position, + }, + title: { + display: pieStyle.title.enabled, + text: pieStyle.title.text, + }, + tooltip: { + enabled: pieStyle.tooltip.enabled, + }, + }, + }; + } +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.style.ts b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.style.ts index e69de29..3a8f76c 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.style.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.style.ts @@ -0,0 +1,31 @@ +import { z } from 'zod/v4'; + +export const PieStyleScheme = z.object({ + title: z + .object({ + enabled: z.boolean().default(false), + text: z.string().default(''), + }) + .default({ + enabled: false, + text: '', + }), + legend: z + .object({ + enabled: z.boolean().default(true), + position: z.enum(['top', 'left', 'bottom', 'right']).default('top'), + }) + .default({ + enabled: true, + position: 'top', + }), + tooltip: z + .object({ + enabled: z.boolean().default(true), + }) + .default({ + enabled: true, + }), +}); + +export type PieStyle = z.infer; From 75e4644b896e609949038b19cd5d1be3dacc06af Mon Sep 17 00:00:00 2001 From: Nikita Nazarov Date: Thu, 29 May 2025 01:46:12 +0300 Subject: [PATCH 3/5] feat(datacat-ui): finish fixing edit panel --- .../edit-panel/edit-panel.component.ts | 25 ++------------- .../shared/ui/charts/bar/bar.component.html | 2 ++ .../src/shared/ui/charts/bar/bar.component.ts | 23 +++++++++++++ .../src/shared/ui/charts/bar/bar.style.ts | 12 +++---- .../shared/ui/charts/line/line.component.html | 2 ++ .../shared/ui/charts/line/line.component.ts | 7 +++- .../aggregate-options.component.html | 6 ++++ .../aggregate/aggregate-options.component.ts | 32 +++++++++++++++++++ .../ui/charts/options/options.component.html | 18 +++++++++-- .../ui/charts/options/options.component.ts | 2 ++ .../shared/ui/charts/pie/pie.component.html | 2 ++ .../src/shared/ui/charts/pie/pie.component.ts | 24 ++++++++++++-- 12 files changed, 121 insertions(+), 34 deletions(-) create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.html create mode 100644 frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.ts diff --git a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts index 76016ea..e4deb07 100644 --- a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts +++ b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts @@ -51,7 +51,7 @@ import { ChartOptionsComponent } from '../../../shared/ui/charts/options/options ], providers: [ChartService], }) -export class EditPanelComponent implements AfterViewInit { +export class EditPanelComponent { private _panelId?: string; @Input() public set panelId(id: string | undefined) { @@ -59,9 +59,6 @@ export class EditPanelComponent implements AfterViewInit { this.refresh(); } - @ViewChild(PanelVisualizationOptionsComponent) - optionsComponent?: PanelVisualizationOptionsComponent; - protected panel?: Panel; protected data: TimeSeries[] | null = null; @@ -120,15 +117,6 @@ export class EditPanelComponent implements AfterViewInit { }); } - ngAfterViewInit() { - if (this.panel) { - this.optionsComponent?.setVisualizationSettings( - this.panel.visualizationType!, - this.panel.visualizationSettings!, - ); - } - } - protected refresh() { if (!this._panelId) return; @@ -156,11 +144,6 @@ export class EditPanelComponent implements AfterViewInit { this.chartService.updateStyle(this.panel.visualizationSettings); this.refreshPreview(); - this.optionsComponent?.setVisualizationSettings( - this.panel.visualizationType!, - this.panel.visualizationSettings!, - ); - this.editForm.setValue({ title: this.panel.title, dataSourceId: this.panel.dataSource?.id, @@ -189,13 +172,11 @@ export class EditPanelComponent implements AfterViewInit { const request: any = { title: this.editForm.get('title')?.value || '', - type: encodeVisualizationType(this.visualizationType), + type: encodeVisualizationType(this.chartService.type), rawQuery: this.editForm.get('query')?.value || '', dataSourceId: this.editForm.get('dataSourceId')?.value || '', layout: serializeLayout(this.panel.layout), - styleConfiguration: encodeVisualizationSettings( - this.visualizationSettings, - ), + styleConfiguration: encodeVisualizationSettings(this.chartService.style), }; this.editForm.disable(); diff --git a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.html b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.html index 5bf9b2e..5724b6e 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.html +++ b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.html @@ -1,6 +1,8 @@
@if (isError) {

Error

+ } @else if (!hasData()) { +

No data

} @else { } diff --git a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts index eeb3185..50fb40b 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts @@ -76,6 +76,29 @@ export class BarChartComponent { enabled: barStyle.tooltip.enabled, }, }, + scales: { + x: { + type: 'time', + min: barStyle.axis.xAxisMin, + max: barStyle.axis.xAxisMax, + title: { + display: barStyle.axis.xAxisTitle != undefined, + text: barStyle.axis.xAxisTitle, + }, + }, + y: { + min: barStyle.axis.yAxisMin, + max: barStyle.axis.yAxisMax, + title: { + display: barStyle.axis.yAxisTitle != undefined, + text: barStyle.axis.yAxisTitle, + }, + }, + }, }; } + + protected hasData(): boolean { + return this.chartjsData.datasets.length !== 0; + } } diff --git a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.style.ts b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.style.ts index 1966e57..831deef 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.style.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.style.ts @@ -28,12 +28,12 @@ export const BarStyleScheme = z.object({ }), axis: z .object({ - xAxisTitle: z.string().default('Date').optional(), - xAxisMin: z.date().optional(), - xAxisMax: z.date().optional(), - yAxisTitle: z.string().default('Value').optional(), - yAxisMin: z.number().optional(), - yAxisMax: z.number().optional(), + xAxisTitle: z.string().default('Date').nullish(), + xAxisMin: z.date().nullish(), + xAxisMax: z.date().nullish(), + yAxisTitle: z.string().default('Value').nullish(), + yAxisMin: z.number().nullish(), + yAxisMax: z.number().nullish(), }) .default({}), }); diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.html b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.html index 933ba5f..be9d999 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.html +++ b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.html @@ -1,6 +1,8 @@
@if (isError) {

Error

+ } @else if (!hasData()) { +

No data

} @else { } diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts index 310343b..31871e2 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts @@ -4,6 +4,8 @@ import { TimeSeries } from '../chart.types'; import { ChartModule, UIChart } from 'primeng/chart'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; import { ChartService } from '../chart.service'; +import 'luxon'; +import 'chartjs-adapter-luxon'; @Component({ standalone: true, @@ -33,7 +35,6 @@ export class LineChartComponent { private updateStyle(style: any) { const parseResult = LineStyleScheme.safeParse(style); - console.log(parseResult); if (parseResult.success) { this.chartjsOptions = this.getChartjsOptionsFromLineStyle( parseResult.data, @@ -98,4 +99,8 @@ export class LineChartComponent { }, }; } + + protected hasData(): boolean { + return this.chartjsData.datasets.length !== 0; + } } diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.html b/frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.html new file mode 100644 index 0000000..d424035 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.html @@ -0,0 +1,6 @@ +
+
+

Aggregate function

+ +
+
diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.ts b/frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.ts new file mode 100644 index 0000000..4bdf828 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { ChartService } from '../../chart.service'; +import { SelectModule } from 'primeng/select'; + +@Component({ + standalone: true, + selector: 'datacat-aggregate-options', + templateUrl: 'aggregate-options.component.html', + styleUrl: '../options.style.scss', + imports: [ReactiveFormsModule, SelectModule], +}) +export class AggregateOptionsComponent { + protected form = new FormGroup({ + function: new FormControl<'AVG' | 'MIN' | 'MAX'>('AVG'), + }); + + constructor(private chartService: ChartService) { + this.form.valueChanges.subscribe((options) => + this.chartService.updateStyle({ + aggregate: { + ...options, + }, + }), + ); + this.chartService.style$.subscribe((style) => { + if (style.aggregate) { + this.form.setValue(style.aggregate, { emitEvent: false }); + } + }); + } +} diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/options.component.html b/frontend/datacat-ui/src/shared/ui/charts/options/options.component.html index ba6c0a4..66eb621 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/options/options.component.html +++ b/frontend/datacat-ui/src/shared/ui/charts/options/options.component.html @@ -34,8 +34,22 @@ } - @case (VisualizationType.BAR) {} - @case (VisualizationType.PIE) {} + @case (VisualizationType.BAR) { + + Axes + + + + + } + @case (VisualizationType.PIE) { + + Aggregate + + + + + } @default {} } diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/options.component.ts b/frontend/datacat-ui/src/shared/ui/charts/options/options.component.ts index 453a163..0f070ca 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/options/options.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/options/options.component.ts @@ -9,6 +9,7 @@ import { LegendOptionsComponent } from './legend/legend-options.component'; import { TitleOptionsComponent } from './title/title-options.component'; import { TooltipOptionsComponent } from './tooltip/tooltip-options.component'; import { AxisOptionsComponent } from './axis/axis-options.component'; +import { AggregateOptionsComponent } from './aggregate/aggregate-options.component'; @Component({ standalone: true, @@ -24,6 +25,7 @@ import { AxisOptionsComponent } from './axis/axis-options.component'; TitleOptionsComponent, TooltipOptionsComponent, AxisOptionsComponent, + AggregateOptionsComponent, ], }) export class ChartOptionsComponent { diff --git a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.html b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.html index 260b8f9..813e3f8 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.html +++ b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.html @@ -1,6 +1,8 @@
@if (isError) {

Error

+ } @else if (!hasData()) { +

No data

} @else { } diff --git a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts index 2c70691..3fe1f3d 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts @@ -44,9 +44,23 @@ export class PieChartComponent { private updateData(data: TimeSeries[]): void { this.chartjsData = { labels: data.map((ts) => JSON.stringify(ts.labels)) || [], - datasets: { - data: data.map((ts) => ts.points[0].value) || [], - }, + datasets: [ + { + data: + data.map((ts) => { + return ( + ts.points.reduce( + (prev, curr) => { + return { + value: prev.value + curr.value, + }; + }, + { value: 0 }, + ).value / ts.points.length + ); + }) || [], + }, + ], }; this.chart?.chart?.update(); } @@ -70,4 +84,8 @@ export class PieChartComponent { }, }; } + + protected hasData(): boolean { + return this.chartjsData.datasets.length !== 0; + } } From 84aedbda4136dfcbb19022e33723448ce87492d4 Mon Sep 17 00:00:00 2001 From: Nikita Nazarov Date: Thu, 29 May 2025 07:50:46 +0000 Subject: [PATCH 4/5] feat(datacat-ui): finish with major fixes --- .../edit-panel/edit-panel.component.html | 2 +- .../edit-panel/edit-panel.component.ts | 7 ----- .../panel-in-grid.component.html | 12 ++------ .../panel-in-grid/panel-in-grid.component.ts | 28 +++++++++++-------- .../src/shared/ui/charts/bar/bar.component.ts | 2 +- .../src/shared/ui/charts/chart.service.ts | 7 ++++- .../shared/ui/charts/line/line.component.ts | 2 +- .../panel-chart/panel-chart.component.ts | 5 +++- 8 files changed, 32 insertions(+), 33 deletions(-) diff --git a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html index 90830ad..25e362b 100644 --- a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html +++ b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.html @@ -2,7 +2,7 @@
- +
diff --git a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts index e4deb07..9c2d5bb 100644 --- a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts +++ b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts @@ -92,17 +92,14 @@ export class EditPanelComponent { constructor( private api: ApiService, private logger: ToastLoggerService, - private panelDataService: PanelDataService, private chartService: ChartService, ) { - this.panelDataService.data$.subscribe((v) => (this.data = v)); this.timeRangeControl.valueChanges.subscribe((tr) => { this.refreshPreview(); }); this.editForm.get('dataSourceId')?.valueChanges.subscribe((id) => { if (id && this.panel) { this.panel.dataSource!.id = id; - this.panelDataService.panel = this.panel; this.chartService.setDataSourceName(this.panel.dataSource!.name); this.refreshPreview(); } @@ -110,7 +107,6 @@ export class EditPanelComponent { this.editForm.get('query')?.valueChanges.subscribe((q) => { if (q && this.panel) { this.panel.query = q; - this.panelDataService.panel = this.panel; this.chartService.setQuery(this.panel.query); this.refreshPreview(); } @@ -138,7 +134,6 @@ export class EditPanelComponent { data.styleConfiguration!, ) as VisualizationSettings, }; - this.panelDataService.panel = this.panel; this.chartService.setQuery(this.panel.query); this.chartService.setDataSourceName(this.panel.dataSource!.name); this.chartService.updateStyle(this.panel.visualizationSettings); @@ -157,8 +152,6 @@ export class EditPanelComponent { } protected refreshPreview() { - this.panelDataService.loadTimeRange(this.timeRangeControl.getRawValue()!); - const timeRange = this.timeRangeControl.getRawValue()!; this.chartService.loadTimeRange( timeRange.from, diff --git a/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.html b/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.html index 71aeae9..62a6268 100644 --- a/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.html +++ b/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.html @@ -24,11 +24,7 @@ @if (isError) {

Loading error

} @else { - + }
@@ -45,10 +41,6 @@
- +
diff --git a/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.ts b/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.ts index ed022cd..e6a2cbc 100644 --- a/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.ts +++ b/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.ts @@ -1,16 +1,16 @@ import { Component, Input } from '@angular/core'; import { PanelModule } from 'primeng/panel'; -import { PanelVisualizationComponent } from '../../../../shared/ui/panel-visualization/panel-visualization.component'; import { Panel } from '../../../../entities'; import { ButtonModule } from 'primeng/button'; import { Router } from '@angular/router'; import * as urls from '../../../../shared/common/urls'; -import { TimeSeries } from '../../../../entities/dashboards/data.types'; import { DialogModule } from 'primeng/dialog'; import { TextareaModule } from 'primeng/textarea'; import { DividerModule } from 'primeng/divider'; -import { PanelDataService } from '../panel-data.service'; import { DashboardService } from '../dashboard.service'; +import { PanelChartComponent } from '../../../../shared/ui/charts/panel-chart/panel-chart.component'; +import { ChartService } from '../../../../shared/ui/charts/chart.service'; +import { TimeSeries } from '../../../../shared/ui/charts/chart.types'; @Component({ standalone: true, @@ -19,20 +19,26 @@ import { DashboardService } from '../dashboard.service'; styleUrl: './panel-in-grid.component.scss', imports: [ PanelModule, - PanelVisualizationComponent, + PanelChartComponent, ButtonModule, DialogModule, TextareaModule, DividerModule, ], - providers: [PanelDataService], + providers: [ChartService], }) export class PanelInGridComponent { @Input() set panel(p: Panel | undefined) { this._panel = p; - this.panelDataService.panel = p; + if (p) { + this.chartService.setType(p.visualizationType!); + this.chartService.setQuery(p.query); + this.chartService.setDataSourceName(p.dataSource!.name); + this.chartService.updateStyle(p.visualizationSettings); + } if (this.dashboardService.timeRange) { - this.panelDataService.loadTimeRange(this.dashboardService.timeRange); + const tr = this.dashboardService.timeRange; + this.chartService.loadTimeRange(tr.from, tr.to, tr.step); } } @@ -43,13 +49,13 @@ export class PanelInGridComponent { constructor( private router: Router, - private panelDataService: PanelDataService, private dashboardService: DashboardService, + private chartService: ChartService, ) { - this.panelDataService.data$.subscribe((v) => (this.data = v)); - this.panelDataService.error$.subscribe((v) => (this.isError = v)); + this.chartService.data$.subscribe((v) => (this.data = v)); + this.chartService.error$.subscribe((v) => (this.isError = v)); this.dashboardService.timeRange$.subscribe((tr) => { - if (tr) this.panelDataService.loadTimeRange(tr); + if (tr) this.chartService.loadTimeRange(tr.from, tr.to, tr.step); }); } diff --git a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts index 50fb40b..1eacafa 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/bar/bar.component.ts @@ -46,7 +46,7 @@ export class BarChartComponent { labels: [], datasets: data.map((ts) => { return { - label: ts.name + JSON.stringify(ts.labels), + label: JSON.stringify(ts.labels), data: ts.points.map((pt) => { return { x: pt.timestamp.toISOString(), diff --git a/frontend/datacat-ui/src/shared/ui/charts/chart.service.ts b/frontend/datacat-ui/src/shared/ui/charts/chart.service.ts index dc18cb3..2a7934b 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/chart.service.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/chart.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { BehaviorSubject, finalize, Observable, Subscription } from 'rxjs'; import { DataPoint, TimeSeries } from './chart.types'; import { ApiService } from '../../services/datacat-generated-client'; import { VisualizationType } from '../../../entities'; @@ -13,6 +13,7 @@ export class ChartService { public readonly data$: Observable; public readonly style$: Observable; public readonly error$: Observable; + public readonly isLoading$: Observable; public query?: string; public dataSourceName?: string; @@ -23,6 +24,7 @@ export class ChartService { private dataSubject = new BehaviorSubject([]); private styleSubject = new BehaviorSubject({}); private errorSubject = new BehaviorSubject(false); + private isLoadingSubject = new BehaviorSubject(false); public get type(): VisualizationType { return this.typeSubject.value; @@ -43,6 +45,7 @@ export class ChartService { this.data$ = this.dataSubject.asObservable(); this.style$ = this.styleSubject.asObservable(); this.error$ = this.errorSubject.asObservable(); + this.isLoading$ = this.isLoadingSubject.asObservable(); } public setType(type: VisualizationType): void { @@ -69,6 +72,7 @@ export class ChartService { this.loadTimeRangeSubscription?.unsubscribe(); this.errorSubject.next(false); + this.isLoadingSubject.next(true); this.loadTimeRangeSubscription = this.api .getApiV1MetricsQueryRange( this.dataSourceName, @@ -79,6 +83,7 @@ export class ChartService { to, step, ) + .pipe(finalize(() => this.isLoadingSubject.next(false))) .subscribe({ next: (data) => { const timeSeries = diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts index 31871e2..9b4bec0 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts @@ -48,7 +48,7 @@ export class LineChartComponent { labels: [], datasets: data.map((ts) => { return { - label: ts.name + JSON.stringify(ts.labels), + label: JSON.stringify(ts.labels), data: ts.points.map((pt) => { return { x: pt.timestamp.toISOString(), diff --git a/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts index f57a011..5a1a1d8 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/panel-chart/panel-chart.component.ts @@ -21,7 +21,7 @@ import { PieChartComponent } from '../pie/pie.component'; ], }) export class PanelChartComponent { - @Input() public loading: boolean = false; + protected loading: boolean = false; protected type?: VisualizationType; protected VisualizationType = VisualizationType; @@ -30,5 +30,8 @@ export class PanelChartComponent { this.chartService.type$.subscribe((type) => { this.type = type; }); + this.chartService.isLoading$.subscribe((isLoading) => { + this.loading = isLoading; + }); } } From 2644aeec83209e6383ea38b14c33817d8f8448e7 Mon Sep 17 00:00:00 2001 From: Nikita Nazarov Date: Thu, 29 May 2025 12:23:34 +0300 Subject: [PATCH 5/5] feat(datacat-ui): small adjustments --- .../edit-panel/edit-panel.component.ts | 1 + .../panels-grid/panels-grid.component.ts | 1 + .../shared/ui/charts/line/line.component.ts | 5 +- .../aggregate-options.component.html | 6 ++- .../legend/legend-options.component.html | 1 + .../src/shared/ui/charts/pie/pie.component.ts | 51 ++++++++++++++----- .../src/shared/ui/charts/pie/pie.style.ts | 7 +++ 7 files changed, 58 insertions(+), 14 deletions(-) diff --git a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts index 9c2d5bb..0819034 100644 --- a/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts +++ b/frontend/datacat-ui/src/features/dashboards/edit-panel/edit-panel.component.ts @@ -134,6 +134,7 @@ export class EditPanelComponent { data.styleConfiguration!, ) as VisualizationSettings, }; + this.chartService.setType(this.panel.visualizationType!); this.chartService.setQuery(this.panel.query); this.chartService.setDataSourceName(this.panel.dataSource!.name); this.chartService.updateStyle(this.panel.visualizationSettings); diff --git a/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.ts b/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.ts index d6bdf8e..70170fe 100644 --- a/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.ts +++ b/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.ts @@ -82,6 +82,7 @@ export class PanelsGridComponent { resizable: { enabled: true, }, + outerMargin: false, enableBoundaryControl: true, itemChangeCallback: this.handleGridsterItemChange.bind(this), }; diff --git a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts index 9b4bec0..779dabd 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/line/line.component.ts @@ -48,7 +48,10 @@ export class LineChartComponent { labels: [], datasets: data.map((ts) => { return { - label: JSON.stringify(ts.labels), + label: (() => { + const { __name__, ...labels } = ts.labels; + return JSON.stringify(labels); + })(), data: ts.points.map((pt) => { return { x: pt.timestamp.toISOString(), diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.html b/frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.html index d424035..213c599 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.html +++ b/frontend/datacat-ui/src/shared/ui/charts/options/aggregate/aggregate-options.component.html @@ -1,6 +1,10 @@

Aggregate function

- +
diff --git a/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.html b/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.html index 420e589..ab59de2 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.html +++ b/frontend/datacat-ui/src/shared/ui/charts/options/legend/legend-options.component.html @@ -8,6 +8,7 @@
diff --git a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts index 3fe1f3d..dc8dcbf 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.component.ts @@ -4,6 +4,7 @@ import { TimeSeries } from '../chart.types'; import { ChartModule, UIChart } from 'primeng/chart'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; import { ChartService } from '../chart.service'; +import { parse } from 'zod/v4'; @Component({ standalone: true, @@ -19,6 +20,8 @@ export class PieChartComponent { protected chartjsOptions: any; protected chartjsData: any; + protected aggregationFunction: string = 'AVG'; + constructor(private chartService: ChartService) { this.chartService.style$.subscribe((style) => { this.updateStyle(style); @@ -37,6 +40,8 @@ export class PieChartComponent { this.chartjsOptions = this.getChartjsOptionsFromPieStyle( parseResult.data, ); + this.aggregationFunction = parseResult.data.aggregate.function; + this.updateData(this.chartService.data); this.chart?.chart?.update(); } } @@ -47,18 +52,9 @@ export class PieChartComponent { datasets: [ { data: - data.map((ts) => { - return ( - ts.points.reduce( - (prev, curr) => { - return { - value: prev.value + curr.value, - }; - }, - { value: 0 }, - ).value / ts.points.length - ); - }) || [], + data.map((ts) => + this.aggregateByType(this.aggregationFunction, ts), + ) || [], }, ], }; @@ -88,4 +84,35 @@ export class PieChartComponent { protected hasData(): boolean { return this.chartjsData.datasets.length !== 0; } + + protected aggregateByType(type: string, ts: TimeSeries): number { + if (ts.points.length !== 0) { + switch (type) { + case 'AVG': { + return ( + ts.points + .map((pt) => pt.value) + .reduce((sum, curr) => sum + curr, 0) / ts.points.length + ); + } + case 'MIN': { + return ts.points + .map((pt) => pt.value) + .reduce( + (min, curr) => (curr < min ? curr : min), + ts.points[0].value, + ); + } + case 'MAX': { + return ts.points + .map((pt) => pt.value) + .reduce( + (max, curr) => (curr < max ? max : curr), + ts.points[0].value, + ); + } + } + } + return 0; + } } diff --git a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.style.ts b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.style.ts index 3a8f76c..99d28d4 100644 --- a/frontend/datacat-ui/src/shared/ui/charts/pie/pie.style.ts +++ b/frontend/datacat-ui/src/shared/ui/charts/pie/pie.style.ts @@ -26,6 +26,13 @@ export const PieStyleScheme = z.object({ .default({ enabled: true, }), + aggregate: z + .object({ + function: z.string().default('AVG'), + }) + .default({ + function: 'AVG', + }), }); export type PieStyle = z.infer;