diff --git a/frontend/datacat-ui/package-lock.json b/frontend/datacat-ui/package-lock.json index fe8a12d..00789f9 100644 --- a/frontend/datacat-ui/package-lock.json +++ b/frontend/datacat-ui/package-lock.json @@ -24,6 +24,7 @@ "primeng": "^18.0.2", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "zod": "^3.25.28", "zone.js": "~0.14.3" }, "devDependencies": { @@ -16726,6 +16727,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.28", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.28.tgz", + "integrity": "sha512-/nt/67WYKnr5by3YS7LroZJbtcCBurDKKPBPWWzaxvVCGuG/NOsiKkrjoOhI8mJ+SQUXEbUzeB3S+6XDUEEj7Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zone.js": { "version": "0.14.10", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz", diff --git a/frontend/datacat-ui/package.json b/frontend/datacat-ui/package.json index b4bbb2c..4415a22 100644 --- a/frontend/datacat-ui/package.json +++ b/frontend/datacat-ui/package.json @@ -27,6 +27,7 @@ "primeng": "^18.0.2", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "zod": "^3.25.28", "zone.js": "~0.14.3" }, "devDependencies": { diff --git a/frontend/datacat-ui/public/assets/icons/auth/keycloak.png b/frontend/datacat-ui/public/assets/icons/auth/keycloak.png new file mode 100644 index 0000000..4af3e33 Binary files /dev/null and b/frontend/datacat-ui/public/assets/icons/auth/keycloak.png differ diff --git a/frontend/datacat-ui/public/assets/icons/datasource/elasticsearch.png b/frontend/datacat-ui/public/assets/icons/datasource/elasticsearch.png new file mode 100644 index 0000000..afcf635 Binary files /dev/null and b/frontend/datacat-ui/public/assets/icons/datasource/elasticsearch.png differ diff --git a/frontend/datacat-ui/public/assets/icons/datasource/jaeger.png b/frontend/datacat-ui/public/assets/icons/datasource/jaeger.png new file mode 100644 index 0000000..e08be66 Binary files /dev/null and b/frontend/datacat-ui/public/assets/icons/datasource/jaeger.png differ diff --git a/frontend/datacat-ui/public/assets/icons/datasource/prometheus.png b/frontend/datacat-ui/public/assets/icons/datasource/prometheus.png new file mode 100644 index 0000000..2375468 Binary files /dev/null and b/frontend/datacat-ui/public/assets/icons/datasource/prometheus.png differ diff --git a/frontend/datacat-ui/src/app/app.config.ts b/frontend/datacat-ui/src/app/app.config.ts index bf37361..7443c4e 100644 --- a/frontend/datacat-ui/src/app/app.config.ts +++ b/frontend/datacat-ui/src/app/app.config.ts @@ -1,40 +1,46 @@ -import {APP_INITIALIZER, ApplicationConfig, provideZoneChangeDetection,} from '@angular/core'; -import {provideRouter, withComponentInputBinding} from '@angular/router'; -import {ROUTES} from '../pages/workspace.routes'; -import {provideAnimationsAsync} from '@angular/platform-browser/animations/async'; -import {providePrimeNG} from 'primeng/config'; -import {PRIMENG_CONFIG} from '../shared/primeng/primeng.config'; -import {provideHttpClient, withInterceptors} from '@angular/common/http'; -import {apiInterceptor} from '../shared/interceptors/api.interceptor'; -import {authInterceptor} from '../shared/interceptors/auth.interceptor'; -import {DialogService} from 'primeng/dynamicdialog'; -import {MessageService} from 'primeng/api'; -import {ApiService} from '../shared/services/datacat-generated-client'; -import {ThemeSelectionService} from '../features/appearence/select-theme/select-theme.service'; -import {namespaceInterceptor} from "../shared/interceptors/namespace.interceptor"; -import {NamespaceService} from "../shared/services/namespace.service"; -import {UserService} from "../shared/services/user.service"; +import { + APP_INITIALIZER, + ApplicationConfig, + provideZoneChangeDetection, +} from '@angular/core'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { ROUTES } from '../pages/workspace.routes'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { providePrimeNG } from 'primeng/config'; +import { PRIMENG_CONFIG } from '../shared/primeng/primeng.config'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { apiInterceptor } from '../shared/interceptors/api.interceptor'; +import { authInterceptor } from '../shared/interceptors/auth.interceptor'; +import { DialogService } from 'primeng/dynamicdialog'; +import { MessageService } from 'primeng/api'; +import { ApiService } from '../shared/services/datacat-generated-client'; +import { ThemeSelectionService } from '../features/appearence/select-theme/select-theme.service'; +import { namespaceInterceptor } from '../shared/interceptors/namespace.interceptor'; +import { NamespaceService } from '../shared/services/namespace.service'; +import { UserService } from '../shared/services/user.service'; export const APP_CONFIG: ApplicationConfig = { - providers: [ - provideZoneChangeDetection({eventCoalescing: true}), - provideRouter(ROUTES, withComponentInputBinding()), - provideAnimationsAsync(), - provideHttpClient(withInterceptors([apiInterceptor, namespaceInterceptor, authInterceptor])), - providePrimeNG(PRIMENG_CONFIG), - DialogService, - MessageService, - ApiService, - ThemeSelectionService, - NamespaceService, - UserService, - { - provide: APP_INITIALIZER, - useFactory: (themeService: ThemeSelectionService) => { - return () => themeService.loadSavedTheme(); - }, - deps: [ThemeSelectionService], - multi: true, - }, - ], + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(ROUTES, withComponentInputBinding()), + provideAnimationsAsync(), + provideHttpClient( + withInterceptors([apiInterceptor, namespaceInterceptor, authInterceptor]), + ), + providePrimeNG(PRIMENG_CONFIG), + DialogService, + MessageService, + ApiService, + ThemeSelectionService, + NamespaceService, + UserService, + { + provide: APP_INITIALIZER, + useFactory: (themeService: ThemeSelectionService) => { + return () => themeService.loadSavedTheme(); + }, + deps: [ThemeSelectionService], + multi: true, + }, + ], }; diff --git a/frontend/datacat-ui/src/app/app.style.scss b/frontend/datacat-ui/src/app/app.style.scss index fea0bc1..d220f0e 100644 --- a/frontend/datacat-ui/src/app/app.style.scss +++ b/frontend/datacat-ui/src/app/app.style.scss @@ -73,4 +73,4 @@ .p-panel-content-container { flex-grow: 1; -} \ No newline at end of file +} diff --git a/frontend/datacat-ui/src/entities/dashboards/data.types.ts b/frontend/datacat-ui/src/entities/dashboards/data.types.ts index 4819862..f050441 100644 --- a/frontend/datacat-ui/src/entities/dashboards/data.types.ts +++ b/frontend/datacat-ui/src/entities/dashboards/data.types.ts @@ -1,6 +1,10 @@ export type DataPoint = { value: number; - timestamp: string; + timestamp: Date; }; -export type DataPoints = DataPoint[]; +export type TimeSeries = { + metric?: string; + labels?: { [key: string]: string }; + dataPoints: DataPoint[]; +}; diff --git a/frontend/datacat-ui/src/entities/dashboards/etc.types.ts b/frontend/datacat-ui/src/entities/dashboards/etc.types.ts new file mode 100644 index 0000000..fcf9f27 --- /dev/null +++ b/frontend/datacat-ui/src/entities/dashboards/etc.types.ts @@ -0,0 +1,5 @@ +export type TimeRange = { + from: Date; + to: Date; + step: string; +}; diff --git a/frontend/datacat-ui/src/entities/dashboards/index.ts b/frontend/datacat-ui/src/entities/dashboards/index.ts index d02c72d..5275871 100644 --- a/frontend/datacat-ui/src/entities/dashboards/index.ts +++ b/frontend/datacat-ui/src/entities/dashboards/index.ts @@ -1,3 +1,4 @@ export * from './dashboard.types'; export * from './panel.types'; export * from './variables.types'; +export * from './mappings'; diff --git a/frontend/datacat-ui/src/entities/dashboards/mappings.ts b/frontend/datacat-ui/src/entities/dashboards/mappings.ts new file mode 100644 index 0000000..dedcf3e --- /dev/null +++ b/frontend/datacat-ui/src/entities/dashboards/mappings.ts @@ -0,0 +1,111 @@ +import { + DataSourceResponse, + GetPanelResponse, + VariableResponse, +} from '../../shared/services/datacat-generated-client'; +import { DataSource } from '../alerting'; +import { + Layout, + Panel, + VisualizationSettings, + VisualizationType, +} from './panel.types'; +import { DashboardVariable } from './variables.types'; +import { z } from 'zod/v4'; + +const LayoutShema = z.object({ + x: z.number(), + y: z.number(), + cols: z.number(), + rows: z.number(), +}); + +const parseLayout = (s: string): Layout => { + try { + return LayoutShema.parse(JSON.parse(s)); + } catch { + return { + x: 0, + y: 0, + rows: 3, + cols: 5, + }; + } +}; + +const parseVisualizationSettings = (s: string): VisualizationSettings => { + try { + return JSON.parse(s); + } catch { + return {}; + } +}; + +const parseVisualizationType = (s: string): VisualizationType => { + switch (s) { + case 'LineChart': + return VisualizationType.LINE; + case 'BarChart': + return VisualizationType.BAR; + case 'PieChart': + return VisualizationType.PIE; + default: + return VisualizationType.UNKNOWN; + } +}; + +export const mapDataSourceResponseToDataSource = ( + r: DataSourceResponse, +): DataSource => { + return { + id: r.id!, + name: r.name!, + driver: r.type! as any, + connectionUrl: r.connectionString!, + }; +}; + +export const mapGetPanelResponeToPanel = (r: GetPanelResponse): Panel => { + return { + id: r.id!, + title: r.title!, + query: r.query!.query!, + dataSource: mapDataSourceResponseToDataSource(r.query!.dataSource!), + layout: parseLayout(r.layout!), + visualizationType: parseVisualizationType(r.typeName!), + visualizationSettings: parseVisualizationSettings(r.styleConfiguration!), + }; +}; + +export const mapVariableResponseToDashboardVariable = ( + r: VariableResponse, +): DashboardVariable => { + return { + id: r.id!, + placeholder: r.placeholder!, + value: r.value!, + }; +}; + +export const serializeVisualizationSettings = ( + vs: VisualizationSettings, +): string => { + return JSON.stringify(vs); +}; + +export const serializeLayout = (layout: Layout): string => { + return JSON.stringify(layout); +}; + +export const serializeVisualizationType = (type: VisualizationType): number => { + switch (type) { + case VisualizationType.LINE: + return 1; + case VisualizationType.BAR: + return 3; + case VisualizationType.PIE: + return 2; + default: + return 4; + } +}; diff --git a/frontend/datacat-ui/src/entities/dashboards/panel.types.ts b/frontend/datacat-ui/src/entities/dashboards/panel.types.ts index f4edfc9..c1c6acc 100644 --- a/frontend/datacat-ui/src/entities/dashboards/panel.types.ts +++ b/frontend/datacat-ui/src/entities/dashboards/panel.types.ts @@ -1,11 +1,14 @@ import { DataSource } from '../alerting'; +export type PanelType = { + id: number; + type: VisualizationType; +}; + export enum VisualizationType { LINE = 'line', BAR = 'bar', PIE = 'pie', - GAUGE = 'gauge', - TABLE = 'table', UNKNOWN = 'unknown', } @@ -51,10 +54,6 @@ export type Panel = { visualizationSettings?: VisualizationSettings; }; -export type LineStyle = { - lineWidth: number; -}; - export const decodeLayout = (encoded: string | undefined): Layout => { if (encoded) { try { @@ -101,9 +100,9 @@ export const encodeVisualizationType = ( case VisualizationType.LINE: return 1; case VisualizationType.BAR: - return 2; - case VisualizationType.PIE: return 3; + case VisualizationType.PIE: + return 2; default: return 4; } @@ -113,11 +112,11 @@ export const decodeVisualizationType = ( type: string | undefined, ): VisualizationType => { switch (type) { - case 'Graph': + case 'LineChart': return VisualizationType.LINE; - case 'Table': + case 'BarChart': return VisualizationType.BAR; - case 'Pie Chart': + case 'PieChart': return VisualizationType.PIE; default: return VisualizationType.UNKNOWN; diff --git a/frontend/datacat-ui/src/features/alerting/alerts-counts-by-status/alerts-counts-by-status.component.ts b/frontend/datacat-ui/src/features/alerting/alerts-counts-by-status/alerts-counts-by-status.component.ts index c741b7f..e35b772 100644 --- a/frontend/datacat-ui/src/features/alerting/alerts-counts-by-status/alerts-counts-by-status.component.ts +++ b/frontend/datacat-ui/src/features/alerting/alerts-counts-by-status/alerts-counts-by-status.component.ts @@ -5,6 +5,7 @@ import { AlertStatus } from '../../../entities'; import { TooltipModule } from 'primeng/tooltip'; import { from } from 'rxjs'; import { FAKE_ALERTS_COUNTS_BY_STATUS } from '../../../shared/mock/fakes'; +import { ApiService } from '../../../shared/services/datacat-generated-client'; @Component({ standalone: true, @@ -16,18 +17,19 @@ import { FAKE_ALERTS_COUNTS_BY_STATUS } from '../../../shared/mock/fakes'; export class AlertsCountsByStatusComponent implements OnInit { protected alertsCountsByStatus?: AlertsCountsByStatus; + constructor(private apiService: ApiService) {} + ngOnInit() { this.loadAlertsCountsByStatus(); } protected loadAlertsCountsByStatus() { - // TODO: add API call - from([FAKE_ALERTS_COUNTS_BY_STATUS]).subscribe({ - next: (alertsCountsByStatus) => { - this.alertsCountsByStatus = alertsCountsByStatus; - }, - error: () => { - // TODO + this.apiService.getApiV1AlertGetCounters().subscribe({ + next: (data) => { + this.alertsCountsByStatus = new Map(); + data.forEach((d) => { + this.alertsCountsByStatus?.set(d.status as AlertStatus, d.count || 0); + }); }, }); } diff --git a/frontend/datacat-ui/src/features/dashboards/dashboards-list/dashboards-list.component.html b/frontend/datacat-ui/src/features/dashboards/dashboards-list/dashboards-list.component.html index a69c232..529cb0e 100644 --- a/frontend/datacat-ui/src/features/dashboards/dashboards-list/dashboards-list.component.html +++ b/frontend/datacat-ui/src/features/dashboards/dashboards-list/dashboards-list.component.html @@ -54,7 +54,7 @@ - + diff --git a/frontend/datacat-ui/src/features/dashboards/delete-dashboard/delete-dashboard-button.component.html b/frontend/datacat-ui/src/features/dashboards/delete-dashboard/delete-dashboard-button.component.html new file mode 100644 index 0000000..9e2a050 --- /dev/null +++ b/frontend/datacat-ui/src/features/dashboards/delete-dashboard/delete-dashboard-button.component.html @@ -0,0 +1,31 @@ + + +

Are you sure you want to delete this dashboard?

+ @if (isDeletionError) { +

Unable to delete dashboard

+ } +
+ + +
+
diff --git a/frontend/datacat-ui/src/features/dashboards/delete-dashboard/delete-dashboard-button.component.scss b/frontend/datacat-ui/src/features/dashboards/delete-dashboard/delete-dashboard-button.component.scss new file mode 100644 index 0000000..340d0cc --- /dev/null +++ b/frontend/datacat-ui/src/features/dashboards/delete-dashboard/delete-dashboard-button.component.scss @@ -0,0 +1,9 @@ +.actions-container { + display: flex; + justify-content: space-between; + margin-top: var(--p-padding-md); +} + +.error { + margin-top: var(--p-padding-sm); +} diff --git a/frontend/datacat-ui/src/features/dashboards/delete-dashboard/delete-dashboard-button.component.ts b/frontend/datacat-ui/src/features/dashboards/delete-dashboard/delete-dashboard-button.component.ts new file mode 100644 index 0000000..424ad27 --- /dev/null +++ b/frontend/datacat-ui/src/features/dashboards/delete-dashboard/delete-dashboard-button.component.ts @@ -0,0 +1,53 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; +import { ApiService } from '../../../shared/services/datacat-generated-client'; +import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; + +@Component({ + standalone: true, + selector: 'datacat-delete-dashboard-button', + templateUrl: './delete-dashboard-button.component.html', + styleUrl: './delete-dashboard-button.component.scss', + imports: [ButtonModule, DialogModule], +}) +export class DeleteDashboardButtonComponent { + @Output() onDelete = new EventEmitter(); + + @Input() dashboardId?: string; + protected isDeletionInitiated = false; + protected isDeletionDialogVisible = false; + protected isDeletionError = false; + + constructor( + private apiService: ApiService, + private loggerService: ToastLoggerService, + ) {} + + protected showDeletionDialog() { + this.isDeletionError = false; + this.isDeletionDialogVisible = true; + } + + protected hideDeletionDialog() { + this.isDeletionDialogVisible = false; + } + + protected deleteDashboard() { + this.isDeletionError = false; + this.isDeletionInitiated = true; + if (this.dashboardId) { + this.apiService.deleteApiV1DashboardRemove(this.dashboardId).subscribe({ + next: () => { + this.loggerService.success('Deleted dashboard'); + this.onDelete.emit(); + }, + error: (e) => { + this.loggerService.error(e); + this.isDeletionInitiated = false; + this.isDeletionError = true; + }, + }); + } + } +} diff --git a/frontend/datacat-ui/src/features/dashboards/delete-dashboard/index.ts b/frontend/datacat-ui/src/features/dashboards/delete-dashboard/index.ts new file mode 100644 index 0000000..b977585 --- /dev/null +++ b/frontend/datacat-ui/src/features/dashboards/delete-dashboard/index.ts @@ -0,0 +1 @@ +export { DeleteDashboardButtonComponent } from './delete-dashboard-button.component'; diff --git a/frontend/datacat-ui/src/features/dashboards/delete-variable/delete-variable-button.component.ts b/frontend/datacat-ui/src/features/dashboards/delete-variable/delete-variable-button.component.ts index b77be16..32f01d2 100644 --- a/frontend/datacat-ui/src/features/dashboards/delete-variable/delete-variable-button.component.ts +++ b/frontend/datacat-ui/src/features/dashboards/delete-variable/delete-variable-button.component.ts @@ -47,7 +47,6 @@ export class DeleteVariableButtonComponent { ) .subscribe({ next: () => { - this.loggerService.success('Deleted variable'); this.onDelete.emit(); this.hideDeletionDialog(); }, diff --git a/frontend/datacat-ui/src/features/dashboards/edit-dashboard/edit-dashboard-button.component.html b/frontend/datacat-ui/src/features/dashboards/edit-dashboard/edit-dashboard-button.component.html new file mode 100644 index 0000000..4969b29 --- /dev/null +++ b/frontend/datacat-ui/src/features/dashboards/edit-dashboard/edit-dashboard-button.component.html @@ -0,0 +1,48 @@ + + + +
+
+
+

Name

+ +
+
+

Description

+ +
+
+ @if (nameControl.invalid) { +

* Name is required

+ } + @if (descriptionControl.invalid) { +

* Description is required

+ } +
+
+
+ + + +
diff --git a/frontend/datacat-ui/src/features/dashboards/edit-dashboard/edit-dashboard-button.component.scss b/frontend/datacat-ui/src/features/dashboards/edit-dashboard/edit-dashboard-button.component.scss new file mode 100644 index 0000000..308f334 --- /dev/null +++ b/frontend/datacat-ui/src/features/dashboards/edit-dashboard/edit-dashboard-button.component.scss @@ -0,0 +1,32 @@ +.creation-form { + display: flex; + flex-direction: column; + gap: var(--p-padding-md); + + &__item { + display: flex; + flex-direction: column; + gap: var(--p-padding-xs); + } + + &__item p { + color: var(--p-surface-400); + } + + &__footer { + display: flex; + justify-content: end; + } +} + +.creation-dialog { + display: flex; + gap: var(--p-padding-md); +} + +.validation-errors { + display: flex; + flex-direction: column; + gap: var(--p-padding-xs); + color: var(--p-surface-400); +} diff --git a/frontend/datacat-ui/src/features/dashboards/edit-dashboard/edit-dashboard-button.component.ts b/frontend/datacat-ui/src/features/dashboards/edit-dashboard/edit-dashboard-button.component.ts new file mode 100644 index 0000000..2b31770 --- /dev/null +++ b/frontend/datacat-ui/src/features/dashboards/edit-dashboard/edit-dashboard-button.component.ts @@ -0,0 +1,92 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { finalize } from 'rxjs'; +import { InputTextModule } from 'primeng/inputtext'; +import { TextareaModule } from 'primeng/textarea'; +import { PanelModule } from 'primeng/panel'; +import { ApiService } from '../../../shared/services/datacat-generated-client'; +import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; + +@Component({ + standalone: true, + selector: './datacat-edit-dashboard-button', + templateUrl: './edit-dashboard-button.component.html', + styleUrl: './edit-dashboard-button.component.scss', + imports: [ + ButtonModule, + DialogModule, + ReactiveFormsModule, + InputTextModule, + TextareaModule, + PanelModule, + ], +}) +export class EditDashboardButtonComponent { + @Output() onEdit = new EventEmitter(); + + @Input() public dashboardId?: string; + + protected isEditDialogVisible = false; + protected isEditInitiated = false; + + protected editForm = new FormGroup({ + name: new FormControl('', Validators.required), + description: new FormControl('', Validators.required), + }); + + protected get nameControl() { + return this.editForm.get('name')!; + } + + protected get descriptionControl() { + return this.editForm.get('description')!; + } + + constructor( + private apiService: ApiService, + private loggerService: ToastLoggerService, + ) {} + + protected editDashboard() { + this.editForm.markAllAsTouched(); + this.editForm.updateValueAndValidity(); + + if (this.editForm.invalid || !this.dashboardId) return; + + this.isEditInitiated = true; + + const request: any = this.editForm.getRawValue(); + + this.apiService + .putApiV1DashboardUpdate(this.dashboardId, request) + .pipe( + finalize(() => { + this.isEditInitiated = false; + }), + ) + .subscribe({ + next: () => { + this.isEditDialogVisible = false; + this.editForm.reset(); + this.onEdit.emit(); + }, + error: (e) => { + this.loggerService.error(e); + }, + }); + } + + public fillForm(name: string, description: string) { + this.editForm.setValue({ + name, + description, + }); + } +} diff --git a/frontend/datacat-ui/src/features/dashboards/edit-dashboard/index.ts b/frontend/datacat-ui/src/features/dashboards/edit-dashboard/index.ts new file mode 100644 index 0000000..214c365 --- /dev/null +++ b/frontend/datacat-ui/src/features/dashboards/edit-dashboard/index.ts @@ -0,0 +1 @@ +export { EditDashboardButtonComponent } from './edit-dashboard-button.component'; 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 4e99266..dfda1a3 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 @@ -10,6 +10,7 @@
+

Title

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 fa28606..da093b7 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,4 +1,4 @@ -import { afterNextRender, Component, Input } from '@angular/core'; +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'; @@ -18,6 +18,7 @@ import { encodeVisualizationSettings, encodeVisualizationType, Panel, + serializeLayout, VisualizationSettings, VisualizationType, } from '../../../entities'; @@ -25,6 +26,10 @@ 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 { 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'; @Component({ standalone: true, @@ -40,9 +45,11 @@ import { finalize } from 'rxjs'; TextareaModule, DataSourceSelectComponent, ButtonModule, + TimeRangeSelectComponent, ], + providers: [PanelDataService], }) -export class EditPanelComponent { +export class EditPanelComponent implements AfterViewInit { private _panelId?: string; @Input() public set panelId(id: string | undefined) { @@ -50,20 +57,25 @@ export class EditPanelComponent { this.refresh(); } + @ViewChild(PanelVisualizationOptionsComponent) + optionsComponent?: PanelVisualizationOptionsComponent; + protected panel?: Panel; - protected data: any = { - labels: ['1', '2', '3', '4', '5', '6', '7'], - datasets: [ - { - label: 'First Dataset', - data: [65, 59, 80, 81, 56, 55, 40], - }, - ], - }; + protected data: TimeSeries[] | null = null; protected visualizationType?: VisualizationType; protected visualizationSettings?: VisualizationSettings; + protected timeRangeControl = new FormControl({ + step: '00:30:00', + from: (() => { + const date = new Date(); + date.setMinutes(date.getMinutes() - 360); + return date; + })(), + to: new Date(), + }); + protected editForm = new FormGroup({ title: new FormControl('', Validators.required), dataSourceId: new FormControl( @@ -74,14 +86,43 @@ export class EditPanelComponent { }); constructor( - private apiService: ApiService, - private loggerService: ToastLoggerService, - ) {} + private api: ApiService, + private logger: ToastLoggerService, + private panelDataService: PanelDataService, + ) { + this.panelDataService.data$.subscribe((v) => (this.data = v)); + this.timeRangeControl.valueChanges.subscribe((tr) => { + if (tr) this.panelDataService.loadTimeRange(tr); + }); + this.editForm.get('dataSourceId')?.valueChanges.subscribe((id) => { + if (id && this.panel) { + this.panel.dataSource!.id = id; + this.panelDataService.panel = this.panel; + this.refreshPreview(); + } + }); + this.editForm.get('query')?.valueChanges.subscribe((q) => { + if (q && this.panel) { + this.panel.query = q; + this.panelDataService.panel = this.panel; + this.refreshPreview(); + } + }); + } + + ngAfterViewInit() { + if (this.panel) { + this.optionsComponent?.setVisualizationSettings( + this.panel.visualizationType!, + this.panel.visualizationSettings!, + ); + } + } protected refresh() { if (!this._panelId) return; - this.apiService.getApiV1Panel(this._panelId).subscribe({ + this.api.getApiV1Panel(this._panelId).subscribe({ next: (data) => { this.panel = { id: data.id || '', @@ -95,9 +136,17 @@ export class EditPanelComponent { }, layout: decodeLayout(data.layout), visualizationType: decodeVisualizationType(data.typeName), - visualizationSettings: - data.styleConfiguration as VisualizationSettings, + visualizationSettings: JSON.parse( + data.styleConfiguration!, + ) as VisualizationSettings, }; + this.panelDataService.panel = this.panel; + this.refreshPreview(); + + this.optionsComponent?.setVisualizationSettings( + this.panel.visualizationType!, + this.panel.visualizationSettings!, + ); this.editForm.setValue({ title: this.panel.title, @@ -105,36 +154,40 @@ export class EditPanelComponent { query: this.panel.query, }); }, - error: (e) => { - this.loggerService.error(e); + error: () => { + this.logger.error('Cannot load panel data'); }, }); } + protected refreshPreview() { + this.panelDataService.loadTimeRange(this.timeRangeControl.getRawValue()!); + } + protected saveChanges() { - if (!this._panelId) return; + if (!this.panel) return; const request: any = { title: this.editForm.get('title')?.value || '', type: encodeVisualizationType(this.visualizationType), rawQuery: this.editForm.get('query')?.value || '', dataSourceId: this.editForm.get('dataSourceId')?.value || '', - // layout: '', + layout: serializeLayout(this.panel.layout), styleConfiguration: encodeVisualizationSettings( this.visualizationSettings, ), }; this.editForm.disable(); - this.apiService - .putApiV1PanelUpdate(this._panelId, request) + this.api + .putApiV1PanelUpdate(this.panel.id, request) .pipe(finalize(() => this.editForm.enable())) .subscribe({ next: () => { - this.loggerService.success('Saved'); + this.logger.success('Saved'); }, - error: (e) => { - this.loggerService.error(e); + error: () => { + this.logger.error('Cannot save'); }, }); } diff --git a/frontend/datacat-ui/src/features/dashboards/panels-grid/dashboard.service.ts b/frontend/datacat-ui/src/features/dashboards/panels-grid/dashboard.service.ts new file mode 100644 index 0000000..b6c4082 --- /dev/null +++ b/frontend/datacat-ui/src/features/dashboards/panels-grid/dashboard.service.ts @@ -0,0 +1,211 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, filter, finalize, forkJoin, Observable } from 'rxjs'; +import { + Dashboard, + DashboardVariable, + decodeLayout, + Layout, + mapGetPanelResponeToPanel, + mapVariableResponseToDashboardVariable, + Panel, + serializeLayout, + serializeVisualizationSettings, + serializeVisualizationType, +} from '../../../entities'; +import { TimeRange } from '../../../entities/dashboards/etc.types'; +import { ApiService } from '../../../shared/services/datacat-generated-client'; +import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; + +@Injectable({ + providedIn: 'root', +}) +export class DashboardService { + public dashboard$: Observable; + public timeRange$: Observable; + // public refresh$: Observable; + public panels$: Observable; + public isBusy$: Observable; + public variables$: Observable; + + private dashboardSubject = new BehaviorSubject(null); + private timeRangeSubject = new BehaviorSubject(null); + private panelsSubject = new BehaviorSubject(null); + private isBusySubject = new BehaviorSubject(false); + private variablesSubject = new BehaviorSubject( + null, + ); + private refreshSubject = new BehaviorSubject(null); + + private _timeRange: TimeRange | null = null; + private panels?: Panel[]; + private dashboard?: Dashboard; + + constructor( + private api: ApiService, + private logger: ToastLoggerService, + ) { + this.dashboard$ = this.dashboardSubject.asObservable(); + this.timeRange$ = this.timeRangeSubject.asObservable(); + // this.refresh$ = this.refreshSubject + // .asObservable() + // .pipe(filter((v) => v is Date)); + this.panels$ = this.panelsSubject.asObservable(); + this.isBusy$ = this.isBusySubject.asObservable(); + this.variables$ = this.variablesSubject.asObservable(); + } + + public set dashboardId(id: string) { + if (this.dashboard?.id === id) return; + this.refreshDashboardPanelsById(id); + this.refreshDashboardVariablesById(id); + } + + public set timeRange(tr: TimeRange) { + this._timeRange = tr; + this.timeRangeSubject.next(tr); + } + + public get timeRange(): TimeRange | null { + return this._timeRange; + } + + public savePanelsLayout() { + this.isBusySubject.next(true); + const requests = this.panels!.map((p) => { + const request = { + title: p.title, + type: serializeVisualizationType(p.visualizationType!), + rawQuery: p.query, + dataSourceId: p.dataSource!.id, + layout: serializeLayout(p.layout!), + styleConfiguration: serializeVisualizationSettings( + p.visualizationSettings!, + ), + } as any; + return this.api.putApiV1PanelUpdate(p.id, request); + }); + + forkJoin(requests) + .pipe(finalize(() => this.isBusySubject.next(false))) + .subscribe({ + error: () => { + this.logger.error('Cannot save full layout'); + }, + }); + } + + public refreshDashboardVariables() { + if (!this.dashboard) return; + this.refreshDashboardVariablesById(this.dashboard.id); + } + + public refreshDashboardVariablesById(id: string): void { + this.api.getApiV1VariablesDashboard(id).subscribe({ + next: (data) => { + const variables = data.map(mapVariableResponseToDashboardVariable); + this.variablesSubject.next(variables); + }, + error: () => { + this.logger.error('Unable to update variables'); + }, + }); + } + + public refreshDashboardOnly() { + if (!this.dashboard) return; + this.api + .getApiV1DashboardFull(this.dashboard.id) + .pipe(finalize(() => this.isBusySubject.next(false))) + .subscribe({ + next: (data) => { + const panels = data.panels!.map((p) => { + return { + id: p.id!, + title: p.title!, + query: p.query!, + layout: decodeLayout(p.layout!), + }; + }); + this.dashboard = { + id: data.id!, + name: data.name!, + description: data.description!, + panels, + createdAt: data.createdAt!, + lastUpdatedAt: data.updatedAt!, + }; + this.dashboardSubject.next(this.dashboard!); + }, + error: () => {}, + }); + } + + public refreshDashboardPanels() { + if (!this.dashboard) return; + this.refreshDashboardPanelsById(this.dashboard.id); + } + + private refreshDashboardPanelsById(id: string) { + this.isBusySubject.next(true); + this.api + .getApiV1DashboardFull(id) + .pipe(finalize(() => this.isBusySubject.next(false))) + .subscribe({ + next: (data) => { + const panels = data.panels!.map((p) => { + return { + id: p.id!, + title: p.title!, + query: p.query!, + layout: decodeLayout(p.layout!), + }; + }); + this.dashboard = { + id: data.id!, + name: data.name!, + description: data.description!, + panels, + createdAt: data.createdAt!, + lastUpdatedAt: data.updatedAt!, + }; + this.dashboardSubject.next(this.dashboard!); + + const requests = panels.map((p) => this.api.getApiV1Panel(p.id)); + + if (requests.length == 0) { + this.panels = []; + this.panelsSubject.next([]); + } else { + forkJoin(requests).subscribe({ + next: (data) => { + this.panels = data.map(mapGetPanelResponeToPanel); + this.panelsSubject.next(this.panels); + }, + error: () => { + this.logger.error('Cannot load panel data'); + }, + }); + } + }, + error: () => { + this.logger.error('Cannot load dashboard'); + }, + }); + } + + public updatePanelLayout(panelId: string, layout: Layout) { + this.panels = this.panels?.map((p) => { + if (p.id == panelId) { + return { + ...p, + layout, + }; + } + return p; + }); + } + + public requestRefresh(date: Date) { + this.refreshSubject.next(date); + } +} diff --git a/frontend/datacat-ui/src/features/dashboards/panels-grid/index.ts b/frontend/datacat-ui/src/features/dashboards/panels-grid/index.ts index eaa1573..7094a4f 100644 --- a/frontend/datacat-ui/src/features/dashboards/panels-grid/index.ts +++ b/frontend/datacat-ui/src/features/dashboards/panels-grid/index.ts @@ -1,2 +1,3 @@ +export { REFRESH_RATE_OPTIONS } from './panels-grid.consts'; export { PanelsGridComponent } from './panels-grid.component'; export { RefreshRateOption } from './panels-grid.types'; diff --git a/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-data.service.ts b/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-data.service.ts new file mode 100644 index 0000000..12cb4cb --- /dev/null +++ b/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-data.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { DataPoint, TimeSeries } from '../../../entities/dashboards/data.types'; +import { Panel } from '../../../entities'; +import { TimeRange } from '../../../entities/dashboards/etc.types'; +import { ApiService } from '../../../shared/services/datacat-generated-client'; + +@Injectable({ + providedIn: 'root', +}) +export class PanelDataService { + public data$: Observable; + public error$: Observable; + + private dataSubject = new BehaviorSubject(null); + private errorSubject = new BehaviorSubject(false); + + private data?: TimeSeries[]; + private _panel?: Panel; + + public set panel(p: Panel | undefined) { + this._panel = p; + } + + constructor(private api: ApiService) { + this.data$ = this.dataSubject.asObservable(); + this.error$ = this.errorSubject.asObservable(); + } + + public loadTimeRange(tr: TimeRange): void { + if (!this._panel) return; + + this.errorSubject.next(false); + this.api + .getApiV1MetricsQueryRange( + this._panel.dataSource!.name, + this._panel.query, + 'undefined' as any, + null, + tr.from, + tr.to, + tr.step, + ) + .subscribe({ + next: (data) => { + if (data.length !== 0) { + this.data = + data.map((ts) => { + return { + metric: ts.metricName, + labels: ts.labels, + dataPoints: + ts.points?.map((p) => { + return { + value: p.value!, + timestamp: p.timestamp!, + }; + }) || [], + }; + }) || []; + } else { + this.data = []; + } + this.dataSubject.next(this.data); + }, + error: () => { + this.errorSubject.next(true); + }, + }); + } +} 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 eb7c515..71aeae9 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 @@ -1,25 +1,54 @@
-

{{ panel?.title || 'Unnamed panel' }}

- +

{{ _panel?.title || 'Unnamed panel' }}

+
+ + +
- @if (isRefreshError) { -

Loading error

+ @if (isError) { +

Loading error

} @else { }
+ + +
+
+

Query

+ +
+ + +
+
diff --git a/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.scss b/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.scss index 0b95477..28a083a 100644 --- a/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.scss +++ b/frontend/datacat-ui/src/features/dashboards/panels-grid/panel-in-grid/panel-in-grid.component.scss @@ -11,8 +11,24 @@ margin-left: var(--p-padding-md); margin-right: var(--p-padding-md); margin-bottom: var(--p-padding-md); + text-align: center; } .p-panel-content { flex-grow: 1; } + +.dialog { + width: 800px; + height: 400px; +} + +.attr { + display: flex; + flex-direction: column; + gap: var(--p-padding-sm); + + p { + color: var(--p-surface-400); + } +} 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 90a1d03..ed022cd 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,93 +1,65 @@ import { Component, Input } from '@angular/core'; import { PanelModule } from 'primeng/panel'; import { PanelVisualizationComponent } from '../../../../shared/ui/panel-visualization/panel-visualization.component'; -import { - DataSourceDriver, - decodeLayout, - decodeVisualizationSettings, - Panel, - VisualizationType, -} from '../../../../entities'; -import { ApiService } from '../../../../shared/services/datacat-generated-client'; -import { ToastLoggerService } from '../../../../shared/services/toast-logger.service'; +import { Panel } from '../../../../entities'; import { ButtonModule } from 'primeng/button'; import { Router } from '@angular/router'; import * as urls from '../../../../shared/common/urls'; -import { DataPoints } from '../../../../entities/dashboards/data.types'; -import { DatePipe } from '@angular/common'; +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'; @Component({ standalone: true, selector: 'datacat-panel-in-grid', templateUrl: './panel-in-grid.component.html', styleUrl: './panel-in-grid.component.scss', - imports: [PanelModule, PanelVisualizationComponent, ButtonModule], + imports: [ + PanelModule, + PanelVisualizationComponent, + ButtonModule, + DialogModule, + TextareaModule, + DividerModule, + ], + providers: [PanelDataService], }) export class PanelInGridComponent { - private _panelId?: string; - - @Input() set panelId(id: string) { - this._panelId = id; - this.refresh(); + @Input() set panel(p: Panel | undefined) { + this._panel = p; + this.panelDataService.panel = p; + if (this.dashboardService.timeRange) { + this.panelDataService.loadTimeRange(this.dashboardService.timeRange); + } } - protected data: DataPoints = []; - - protected panel?: Panel; - - protected isRefreshError = false; + protected _panel?: Panel; + protected isError: boolean = false; + protected data: TimeSeries[] | null = null; + protected isDialogShown = false; constructor( private router: Router, - private apiService: ApiService, - private loggerService: ToastLoggerService, - ) {} - - protected refresh() { - if (!this._panelId) return; - - this.apiService.getApiV1Panel(this._panelId).subscribe({ - next: (data) => { - this.isRefreshError = false; - this.panel = { - id: data.id || '', - title: data.title || '', - query: data.query?.query || '', - dataSource: { - id: data.query?.dataSource?.id || '', - name: data.query?.dataSource?.name || '', - driver: data.query?.dataSource?.type as DataSourceDriver, - connectionUrl: data.query?.dataSource?.connectionString || '', - }, - layout: decodeLayout(data.layout), - visualizationType: VisualizationType.LINE, - visualizationSettings: decodeVisualizationSettings( - data.styleConfiguration, - ), - }; - }, - error: (e) => { - this.loggerService.error(e); - this.isRefreshError = true; - }, + private panelDataService: PanelDataService, + private dashboardService: DashboardService, + ) { + this.panelDataService.data$.subscribe((v) => (this.data = v)); + this.panelDataService.error$.subscribe((v) => (this.isError = v)); + this.dashboardService.timeRange$.subscribe((tr) => { + if (tr) this.panelDataService.loadTimeRange(tr); }); } protected editPanel() { - if (this._panelId) { - this.router.navigateByUrl(urls.panelEditUrl(this._panelId)); + if (this._panel?.id) { + this.router.navigateByUrl(urls.panelEditUrl(this._panel?.id)); } } - public refreshData() { - const datepipe = new DatePipe('en-US'); - - this.data = [ - ...this.data, - { - value: Math.random() * 10, - timestamp: datepipe.transform(Date.now(), 'dd.MM HH:mm:ss') || '', - }, - ]; + public showDialog() { + this.isDialogShown = true; } } diff --git a/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.html b/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.html index 34cd325..a0ce7a6 100644 --- a/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.html +++ b/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.html @@ -15,29 +15,22 @@ }
- -
- @if (panels.length === 0) { + @if (!panels) { +

...

+ } @else if (panels.length === 0) {

You haven't created any panels yet!

} @else { @for (item of gridsterItems; track $index) { - + } }
+ diff --git a/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.scss b/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.scss index a7f14e0..269ac2c 100644 --- a/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.scss +++ b/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.component.scss @@ -8,7 +8,7 @@ .panels-grid { margin-top: var(--p-padding-md); display: block; - height: 600px; + height: 570px; } .container-header { @@ -22,6 +22,12 @@ } } +.container-footer { + display: flex; + justify-content: start; + gap: var(--p-padding-md); +} + gridster-item { background: transparent; } 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 80dd4bb..d6bdf8e 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 @@ -1,23 +1,21 @@ -import { Component, Input, QueryList, ViewChildren } from '@angular/core'; +import { Component } from '@angular/core'; import { + Dashboard, DashboardVariable, - decodeLayout, - encodeLayout, - encodeVisualizationSettings, Panel, + PanelType, } from '../../../entities'; -import { ApiService } from '../../../shared/services/datacat-generated-client'; -import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; import { ButtonModule } from 'primeng/button'; import { SelectModule } from 'primeng/select'; import { InputGroupModule } from 'primeng/inputgroup'; import { InputGroupAddonModule } from 'primeng/inputgroupaddon'; import { ToggleButtonModule } from 'primeng/togglebutton'; -import { finalize, forkJoin, interval, Subscription, timer } from 'rxjs'; +import { Subscription } from 'rxjs'; import { DisplayGrid, GridsterConfig, GridsterItem, + GridsterItemComponentInterface, GridsterModule, GridType, } from 'angular-gridster2'; @@ -25,11 +23,15 @@ import { CardModule } from 'primeng/card'; import { PanelModule } from 'primeng/panel'; import { PanelInGridComponent } from './panel-in-grid'; import { CreatePanelButtonComponent } from './create-panel-button'; -import { RefreshRateOption } from '.'; +import { REFRESH_RATE_OPTIONS, RefreshRateOption } from '.'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { AddVariableButtonComponent } from '../add-variable'; import { DeleteVariableButtonComponent } from '../delete-variable'; import { DatePickerModule } from 'primeng/datepicker'; +import { TimeRangeSelectComponent } from '../../../shared/ui/time-range-select'; +import { DashboardService } from './dashboard.service'; +import { DEFAULT_TIME_RANGE } from './panels-grid.consts'; +import { TimeRange } from '../../../entities/dashboards/etc.types'; @Component({ standalone: true, @@ -51,49 +53,22 @@ import { DatePickerModule } from 'primeng/datepicker'; AddVariableButtonComponent, DeleteVariableButtonComponent, DatePickerModule, + TimeRangeSelectComponent, ], }) export class PanelsGridComponent { - @ViewChildren(PanelInGridComponent) - public panelsComponents!: QueryList; - - constructor( - private apiService: ApiService, - private loggerService: ToastLoggerService, - ) { - this.freezeGrid(); - this.refreshRateControl.valueChanges.subscribe((seconds) => - this.setRefreshRate(seconds), - ); - } - - protected _dashboardId?: string; - - @Input() public set dashboardId(id: string) { - this._dashboardId = id; - this.refreshDashboard(); - this.refreshDashboardVariables(); - } - - protected isSaving = false; - protected isBusy = false; - - protected refreshRateOptions: RefreshRateOption[] = [ - { title: 'off', seconds: null }, - { title: '10s', seconds: 10 }, - { title: '30s', seconds: 30 }, - { title: '1m', seconds: 60 }, - { title: '10m', seconds: 600 }, - { title: '1h', seconds: 3600 }, - ]; + protected refreshRateOptions: RefreshRateOption[] = REFRESH_RATE_OPTIONS; protected refreshRateControl = new FormControl(null); protected refreshRateSubscription?: Subscription; + protected timeRangeControl = new FormControl(DEFAULT_TIME_RANGE); - protected variables: DashboardVariable[] = []; - - protected panels: Panel[] = []; + protected dashboard: Dashboard | null = null; + protected variables: DashboardVariable[] | null = null; + protected panels: Panel[] | null = null; + protected panelTypes: PanelType[] = []; + protected isBusy = false; - protected gridsterItems: GridsterItem[] = []; + protected gridsterItems: GridsterItem[] | null = null; protected gridsterOptions: GridsterConfig = { gridType: GridType.Fixed, displayGrid: DisplayGrid.None, @@ -108,67 +83,25 @@ export class PanelsGridComponent { enabled: true, }, enableBoundaryControl: true, + itemChangeCallback: this.handleGridsterItemChange.bind(this), }; - protected refreshDashboard() { - if (!this._dashboardId) return; - - this.isBusy = true; - this.refreshRateControl.disable(); - this.apiService - .getApiV1DashboardFull(this._dashboardId) - .pipe( - finalize(() => { - this.isBusy = false; - this.refreshRateControl.enable(); - }), - ) - .subscribe({ - next: (data) => { - this.panels = - data.panels?.map((item) => { - return { - id: item.id || '', - title: item.title || '', - query: item.query || '', - layout: decodeLayout(item.layout), - }; - }) || []; - this.refreshGridsterItems(); - }, - error: (e) => { - this.loggerService.error(e); - }, - }); - } - - protected refreshDashboardVariables() { - if (!this._dashboardId) return; - - this.apiService.getApiV1VariablesDashboard(this._dashboardId).subscribe({ - next: (data) => { - this.variables = data.map((item) => { - return { - id: item.id || '', - placeholder: item.placeholder || '', - value: item.value || '', - }; - }); - }, - error: (e) => { - this.loggerService.error(e); - }, + constructor(private dashboardService: DashboardService) { + this.freezeGrid(); + // this.refreshRateControl.valueChanges.subscribe((seconds) => + // this.setRefreshRate(seconds), + // ); + this.timeRangeControl.valueChanges.subscribe((timeRange) => { + this.dashboardService.timeRange = timeRange!; }); - } - - protected refreshDashboardsData() { - this.isBusy = true; - - this.panelsComponents.forEach((component) => { - component.refreshData(); + this.dashboardService.panels$.subscribe((v) => { + this.panels = v; + this.refreshGridsterItems(); }); - - timer(500).subscribe(() => (this.isBusy = false)); + this.dashboardService.isBusy$.subscribe((v) => (this.isBusy = v)); + this.dashboardService.dashboard$.subscribe((v) => (this.dashboard = v)); + this.dashboardService.variables$.subscribe((v) => (this.variables = v)); + this.dashboardService.timeRange = this.timeRangeControl.getRawValue()!; } protected freezeGrid() { @@ -183,10 +116,16 @@ export class PanelsGridComponent { this.gridsterOptions.api?.optionsChanged!(); } - protected refreshGridsterItems() { - this.gridsterItems = this.panels.map((panel) => { - return { ...panel.layout, panelId: panel.id }; - }); + protected refreshDashboardVariables() { + this.dashboardService.refreshDashboardVariables(); + } + + protected refreshDashboardPanels() { + this.dashboardService.refreshDashboardPanels(); + } + + protected savePanelsLayout() { + this.dashboardService.savePanelsLayout(); } protected toggleMode(event: any) { @@ -197,51 +136,27 @@ export class PanelsGridComponent { } } - protected setRefreshRate(seconds: number | null) { - this.refreshRateSubscription?.unsubscribe(); - if (seconds) { - this.refreshRateSubscription = interval(seconds * 1000).subscribe(() => { - this.refreshDashboardsData(); - }); - } + protected refreshGridsterItems() { + this.gridsterItems = + this.panels?.map((panel) => { + return { ...panel.layout, panelId: panel.id }; + }) || null; } - protected saveLayout() { - this.isSaving = true; - - const observables = this.gridsterItems.map((item) => { - const panel = this.panels.filter((p) => p.id == item['panelId']).at(0); - const request: any = { - title: panel?.title, - type: panel?.visualizationType, - rawQuery: panel?.query, - dataSourceId: panel?.dataSource?.id, - styleConfiguration: encodeVisualizationSettings( - panel?.visualizationSettings, - ), - layout: encodeLayout({ - x: item.x, - y: item.y, - cols: item.cols, - rows: item.rows, - }), - }; - return this.apiService.putApiV1PanelUpdate(item['panelId'], request); + protected handleGridsterItemChange( + item: GridsterItem, + component: GridsterItemComponentInterface, + ) { + const panelId = item['panelId']; + this.dashboardService.updatePanelLayout(panelId, { + x: item.x, + y: item.y, + cols: item.cols, + rows: item.rows, }); + } - forkJoin(observables) - .pipe( - finalize(() => { - this.isSaving = false; - }), - ) - .subscribe({ - next: () => { - this.loggerService.success('Saved layout'); - }, - error: () => { - this.loggerService.error('Unable to save layout'); - }, - }); + protected getPanelById(id: string): Panel | undefined { + return this.panels?.find((p) => p.id == id); } } 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 new file mode 100644 index 0000000..bffa8aa --- /dev/null +++ b/frontend/datacat-ui/src/features/dashboards/panels-grid/panels-grid.consts.ts @@ -0,0 +1,21 @@ +import { TimeRange } from '../../../entities/dashboards/etc.types'; +import { RefreshRateOption } from './panels-grid.types'; + +export const REFRESH_RATE_OPTIONS: RefreshRateOption[] = [ + { title: 'off', seconds: null }, + { title: '10s', seconds: 10 }, + { title: '30s', seconds: 30 }, + { title: '1m', seconds: 60 }, + { title: '10m', seconds: 600 }, + { title: '1h', seconds: 3600 }, +]; + +export const DEFAULT_TIME_RANGE: TimeRange = { + step: '00:30:00', + from: (() => { + const date = new Date(); + date.setMinutes(date.getMinutes() - 360); + return date; + })(), + to: new Date(), +}; diff --git a/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.html b/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.html index 9fa9b0d..9825b23 100644 --- a/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.html +++ b/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.html @@ -1,8 +1,24 @@
-

Dashboard {{ dashboardId }}

+
+

{{ dashboard?.name }}

+ +
+
+ + +
- +
diff --git a/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.scss b/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.scss index fbd24f1..574a96e 100644 --- a/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.scss +++ b/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.scss @@ -1,5 +1,12 @@ .header { display: flex; - gap: var(--p-padding-md); align-items: center; + justify-content: space-between; + width: 100%; + + &__name { + display: flex; + align-items: center; + gap: var(--p-padding-md); + } } diff --git a/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.ts b/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.ts index 335e3cd..da03d9b 100644 --- a/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.ts +++ b/frontend/datacat-ui/src/processes/view-dashboard/view-dashboard.component.ts @@ -1,15 +1,56 @@ -import { Component, Input } from '@angular/core'; +import { AfterContentInit, Component, Input, ViewChild } from '@angular/core'; import { PanelModule } from 'primeng/panel'; import { ButtonModule } from 'primeng/button'; import { PanelsGridComponent } from '../../features/dashboards/panels-grid/panels-grid.component'; +import { DeleteDashboardButtonComponent } from '../../features/dashboards/delete-dashboard'; +import { Router } from '@angular/router'; +import * as urls from '../../shared/common/urls'; +import { EditDashboardButtonComponent } from '../../features/dashboards/edit-dashboard'; +import { Dashboard } from '../../entities'; +import { ApiService } from '../../shared/services/datacat-generated-client'; +import { ToastLoggerService } from '../../shared/services/toast-logger.service'; +import { TooltipModule } from 'primeng/tooltip'; +import { DashboardService } from '../../features/dashboards/panels-grid/dashboard.service'; @Component({ standalone: true, selector: 'datacat-view-dashboard', templateUrl: './view-dashboard.component.html', styleUrl: './view-dashboard.component.scss', - imports: [PanelModule, ButtonModule, PanelsGridComponent], + imports: [ + PanelModule, + ButtonModule, + PanelsGridComponent, + DeleteDashboardButtonComponent, + EditDashboardButtonComponent, + TooltipModule, + ], + providers: [DashboardService], }) -export class ViewDashboardComponent { +export class ViewDashboardComponent implements AfterContentInit { @Input() protected dashboardId: string = ''; + + @ViewChild(EditDashboardButtonComponent) + editDashboardButtonComponent?: EditDashboardButtonComponent; + + protected dashboard: Dashboard | null = null; + + constructor( + private dashboardService: DashboardService, + private router: Router, + ) { + this.dashboardService.dashboard$.subscribe((v) => (this.dashboard = v)); + } + + ngAfterContentInit() { + this.dashboardService.dashboardId = this.dashboardId; + } + + protected refreshDashboardOnly() { + this.dashboardService.refreshDashboardOnly(); + } + + protected showDashboardsList() { + this.router.navigateByUrl(urls.DASHBOARDS_EXPLORER_URL); + } } diff --git a/frontend/datacat-ui/src/shared/ui/data-source-select/data-source-select.component.html b/frontend/datacat-ui/src/shared/ui/data-source-select/data-source-select.component.html index 630eedd..5a4e6d7 100644 --- a/frontend/datacat-ui/src/shared/ui/data-source-select/data-source-select.component.html +++ b/frontend/datacat-ui/src/shared/ui/data-source-select/data-source-select.component.html @@ -9,19 +9,35 @@ [loading]="isLoading" (onChange)="onSelect($event.value)" [(ngModel)]="selectedDataSource" + appendTo="body" > -
+
@switch (dataSource.driver) { @case (DataSourceDriver.PROMETHEUS) { - P +
+ +
} @case (DataSourceDriver.ELASTIC_SEARCH) { - E +
+ +
} @case (DataSourceDriver.JAEGER) { - J +
+ +
} }
diff --git a/frontend/datacat-ui/src/shared/ui/data-source-select/data-source-select.component.scss b/frontend/datacat-ui/src/shared/ui/data-source-select/data-source-select.component.scss new file mode 100644 index 0000000..93e1784 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/data-source-select/data-source-select.component.scss @@ -0,0 +1,4 @@ +.option { + display: flex; + gap: var(--p-padding-sm); +} diff --git a/frontend/datacat-ui/src/shared/ui/data-source-select/data-source-select.component.ts b/frontend/datacat-ui/src/shared/ui/data-source-select/data-source-select.component.ts index 303281f..737de65 100644 --- a/frontend/datacat-ui/src/shared/ui/data-source-select/data-source-select.component.ts +++ b/frontend/datacat-ui/src/shared/ui/data-source-select/data-source-select.component.ts @@ -17,6 +17,7 @@ import { convertToApiFilters } from '../../../entities/data-sources'; standalone: true, selector: 'datacat-data-source-select', templateUrl: './data-source-select.component.html', + styleUrl: './data-source-select.component.scss', imports: [SelectModule, FormsModule], providers: [ { diff --git a/frontend/datacat-ui/src/shared/ui/panel-visualization-options/consts.ts b/frontend/datacat-ui/src/shared/ui/panel-visualization-options/consts.ts new file mode 100644 index 0000000..eca8ee2 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/panel-visualization-options/consts.ts @@ -0,0 +1,15 @@ +import { LineVisualizationOptions } from '../../../entities'; + +export const DEFAULT_OPTIONS: LineVisualizationOptions = { + title: { + enabled: false, + text: '', + }, + legend: { + enabled: false, + position: 'top', + }, + tooltip: { + enabled: true, + }, +}; diff --git a/frontend/datacat-ui/src/shared/ui/panel-visualization-options/forms.ts b/frontend/datacat-ui/src/shared/ui/panel-visualization-options/forms.ts index 4fc880c..fecfc19 100644 --- a/frontend/datacat-ui/src/shared/ui/panel-visualization-options/forms.ts +++ b/frontend/datacat-ui/src/shared/ui/panel-visualization-options/forms.ts @@ -6,12 +6,15 @@ export const createOptionsForm = ( ): FormGroup => { return new FormGroup({ legend: new FormGroup({ - enabled: new FormControl(false), + enabled: new FormControl(true), position: new FormControl('top'), }), title: new FormGroup({ + enabled: new FormControl(false), + text: new FormControl(''), + }), + tooltip: new FormGroup({ enabled: new FormControl(true), - text: new FormControl('a'), }), }); diff --git a/frontend/datacat-ui/src/shared/ui/panel-visualization-options/option-groups/tooltip-options/tooltip-options.component.html b/frontend/datacat-ui/src/shared/ui/panel-visualization-options/option-groups/tooltip-options/tooltip-options.component.html index 2a9ff85..5301dbe 100644 --- a/frontend/datacat-ui/src/shared/ui/panel-visualization-options/option-groups/tooltip-options/tooltip-options.component.html +++ b/frontend/datacat-ui/src/shared/ui/panel-visualization-options/option-groups/tooltip-options/tooltip-options.component.html @@ -2,7 +2,7 @@

Enabled

- +
diff --git a/frontend/datacat-ui/src/shared/ui/panel-visualization-options/panel-visualization-options.component.html b/frontend/datacat-ui/src/shared/ui/panel-visualization-options/panel-visualization-options.component.html index c27c536..685b310 100644 --- a/frontend/datacat-ui/src/shared/ui/panel-visualization-options/panel-visualization-options.component.html +++ b/frontend/datacat-ui/src/shared/ui/panel-visualization-options/panel-visualization-options.component.html @@ -24,7 +24,7 @@ Tooltip - + diff --git a/frontend/datacat-ui/src/shared/ui/panel-visualization-options/panel-visualization-options.component.ts b/frontend/datacat-ui/src/shared/ui/panel-visualization-options/panel-visualization-options.component.ts index e5f778a..10253f9 100644 --- a/frontend/datacat-ui/src/shared/ui/panel-visualization-options/panel-visualization-options.component.ts +++ b/frontend/datacat-ui/src/shared/ui/panel-visualization-options/panel-visualization-options.component.ts @@ -12,6 +12,7 @@ import { TooltipOptionsComponent, } from './option-groups'; import { TitleOptionsComponent } from './option-groups/title-options/title-options.component'; +import { DEFAULT_OPTIONS } from './consts'; @Component({ standalone: true, @@ -66,9 +67,6 @@ export class PanelVisualizationOptionsComponent implements OnInit { this.updateOptionsForm(); this.emit(); }); - this.optionsForm?.valueChanges.subscribe(() => { - this.emit(); - }); } ngOnInit() { @@ -84,5 +82,16 @@ export class PanelVisualizationOptionsComponent implements OnInit { private updateOptionsForm() { this.optionsForm = createOptionsForm(this.visualizationType); + this.optionsForm.valueChanges.subscribe(() => { + this.emit(); + }); + } + + public setVisualizationSettings( + type: VisualizationType, + settings: VisualizationSettings, + ) { + this.visualizationTypeControl.setValue(type); + this.optionsForm.setValue({ ...DEFAULT_OPTIONS, ...settings }); } } diff --git a/frontend/datacat-ui/src/shared/ui/panel-visualization/panel-visualization.component.html b/frontend/datacat-ui/src/shared/ui/panel-visualization/panel-visualization.component.html index 11816bd..b4c828f 100644 --- a/frontend/datacat-ui/src/shared/ui/panel-visualization/panel-visualization.component.html +++ b/frontend/datacat-ui/src/shared/ui/panel-visualization/panel-visualization.component.html @@ -1,34 +1,38 @@
- @switch (visualizationType) { - @case (VisualizationType.LINE) { - - - } - @case (VisualizationType.BAR) { - - - } - @case (VisualizationType.PIE) { - - - } - @default { -

Unsupported visualization

+ @if (hasData()) { + @switch (visualizationType) { + @case (VisualizationType.LINE) { + + + } + @case (VisualizationType.BAR) { + + + } + @case (VisualizationType.PIE) { + + + } + @default { +

Unsupported visualization

+ } } + } @else { +

No data

}
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 27a300e..afe7bc3 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 @@ -2,15 +2,15 @@ import { Component, Input, ViewChild } from '@angular/core'; import { VisualizationSettings, VisualizationType } from '../../../entities'; import { ChartModule } from 'primeng/chart'; import { BASIC_OPTIONS } from './consts'; -import { DataPoints } from '../../../entities/dashboards/data.types'; -import { ThemeProvider } from 'primeng/config'; +import { TimeSeries } from '../../../entities/dashboards/data.types'; +import { CommonModule, DatePipe } from '@angular/common'; @Component({ standalone: true, selector: 'datacat-panel-vizualization', templateUrl: './panel-visualization.component.html', styleUrl: './panel-visualization.component.scss', - imports: [ChartModule], + imports: [ChartModule, CommonModule], }) export class PanelVisualizationComponent { protected VisualizationType = VisualizationType; @@ -19,14 +19,21 @@ export class PanelVisualizationComponent { protected chartRef: any; + protected chartjsData: any = { + labels: [], + datasets: [], + }; + @ViewChild('chart') protected set chart(ref: any) { - this.chartRef = ref; - this.chartRef?.chart?.update(); + if (ref) { + this.chartRef = ref; + this.chartRef?.chart?.update(); + } } @Input() public visualizationType?: VisualizationType; - @Input() public set data(data: DataPoints) { + @Input() public set data(data: TimeSeries[] | null) { if (data) { this.parseDataIntoChartjsData(data); } @@ -41,41 +48,60 @@ export class PanelVisualizationComponent { } protected parseSettingsIntoChartjsOptions(settings: VisualizationSettings) { - const chart: any = this.chartRef?.chart; - this.chartjsOptions.plugins.legend.display = settings.legend?.enabled; this.chartjsOptions.plugins.legend.position = settings.legend?.position; this.chartjsOptions.plugins.title.display = settings.title?.enabled; this.chartjsOptions.plugins.title.text = settings.title?.text; - chart?.update(); + this.chartjsOptions.plugins.tooltip.enabled = settings.tooltip?.enabled; + + this.chartRef?.chart?.update(); } - protected parseDataIntoChartjsData(data: DataPoints) { - this.chartjsData = { - labels: data.map((d) => d.timestamp), - datasets: [ - { - label: 'Label', - data: data.map((d) => d.value), - }, - ], - }; - - this.chart?.update(); + protected parseDataIntoChartjsData(data: TimeSeries[]) { + const datePipe = new DatePipe('en-US', undefined, { + dateFormat: 'M/d/yy, h:mm a', + }); + + switch (this.visualizationType) { + case VisualizationType.PIE: { + this.chartjsData = { + labels: data?.map((ts) => JSON.stringify(ts.labels)) || [], + datasets: [ + { + label: null, + data: data?.map((ts) => ts.dataPoints[0].value) || [], + }, + ], + }; + break; + } + default: { + this.chartjsData = { + labels: + data[0]?.dataPoints.map((d) => datePipe.transform(d.timestamp)) || + [], + datasets: data.map((ts) => { + return { + label: ts.metric + JSON.stringify(ts.labels), + data: ts.dataPoints.map((d) => d.value), + }; + }), + }; + } + } + + this.chartRef?.chart?.update(); } - protected chartjsData: any = { - labels: ['1', '2', '3', '4', '5', '6'], - datasets: [ - { - order: 0, - label: 'Label 1', - data: [1, 8, 3, 2, 5, 10], - // borderColor: 'red', - // backgroundColor: 'blue', - }, - ], - }; + protected hasData(): boolean { + if (this.chartjsData.datasets.length === 0) return false; + + for (const ds of this.chartjsData.datasets) { + if (ds.data.length !== 0) return true; + } + + return false; + } } diff --git a/frontend/datacat-ui/src/shared/ui/time-range-select/index.ts b/frontend/datacat-ui/src/shared/ui/time-range-select/index.ts new file mode 100644 index 0000000..8209ca4 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/time-range-select/index.ts @@ -0,0 +1,2 @@ +export { TimeRangeSelectComponent } from './time-range-select.component'; +export { TimeRange } from './time-range-select.types'; diff --git a/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.component.html b/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.component.html new file mode 100644 index 0000000..c5230e6 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.component.html @@ -0,0 +1,41 @@ + + + {{ from | date: 'short' }} to + {{ to | date: 'short' }} with step + + {{ step }} + + + +
+
+

Step

+ +
+
+

From

+ +
+
+

To

+ +
+
+
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 new file mode 100644 index 0000000..b87a400 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.component.scss @@ -0,0 +1,19 @@ +.container { + display: flex; + gap: var(--p-padding-md); + align-items: center; +} + +.item { + display: flex; + flex-direction: column; + gap: var(--p-padding-xs); + + p { + color: var(--p-surface-400); + } +} + +.date { + color: var(--p-primary-100); +} diff --git a/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.component.ts b/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.component.ts new file mode 100644 index 0000000..f2c3962 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.component.ts @@ -0,0 +1,84 @@ +import { Component, forwardRef } from '@angular/core'; +import { + ControlValueAccessor, + FormControl, + FormGroup, + NG_VALUE_ACCESSOR, + ReactiveFormsModule, +} from '@angular/forms'; +import { DatePickerModule } from 'primeng/datepicker'; +import { TimeRange } from './time-range-select.types'; +import { SelectModule } from 'primeng/select'; +import { STEP_OPTIONS } from './time-range-select.consts'; +import { PopoverModule } from 'primeng/popover'; +import { ButtonModule } from 'primeng/button'; +import { CommonModule } from '@angular/common'; + +@Component({ + standalone: true, + selector: 'datacat-time-range-select', + templateUrl: './time-range-select.component.html', + styleUrl: './time-range-select.component.scss', + imports: [ + DatePickerModule, + SelectModule, + ReactiveFormsModule, + PopoverModule, + ButtonModule, + CommonModule, + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeRangeSelectComponent), + multi: true, + }, + ], +}) +export class TimeRangeSelectComponent implements ControlValueAccessor { + protected stepOptions = STEP_OPTIONS; + + private onChange = (_: any) => {}; + private onTouched = () => {}; + + protected formGroup = new FormGroup({ + step: new FormControl(null), + from: new FormControl(null), + to: new FormControl(null), + }); + + public get step(): string { + return this.formGroup.get('step')?.value!; + } + + public get from(): Date { + return this.formGroup.get('from')?.value!; + } + + public get to(): Date { + return this.formGroup.get('to')?.value!; + } + + constructor() { + this.formGroup.valueChanges.subscribe(() => this.notifyTouchedAndChanged()); + } + + writeValue(value: TimeRange | undefined): void { + if (value) { + this.formGroup.setValue(value); + } + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + notifyTouchedAndChanged() { + this.onChange(this.formGroup.getRawValue()); + this.onTouched(); + } +} diff --git a/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.consts.ts b/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.consts.ts new file mode 100644 index 0000000..bdaad57 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.consts.ts @@ -0,0 +1,26 @@ +export const STEP_OPTIONS = [ + { + label: '1s', + value: '00:00:01', + }, + { + label: '10s', + value: '00:00:10', + }, + { + label: '30s', + value: '00:00:30', + }, + { + label: '1m', + value: '00:01:00', + }, + { + label: '10m', + value: '00:10:00', + }, + { + label: '30m', + value: '00:30:00', + }, +]; diff --git a/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.types.ts b/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.types.ts new file mode 100644 index 0000000..fcf9f27 --- /dev/null +++ b/frontend/datacat-ui/src/shared/ui/time-range-select/time-range-select.types.ts @@ -0,0 +1,5 @@ +export type TimeRange = { + from: Date; + to: Date; + step: string; +};