From 04b28873f06a929fb288bbe5e96dbfbc437de4b5 Mon Sep 17 00:00:00 2001 From: Nikita Nazarov Date: Thu, 29 May 2025 14:35:07 +0300 Subject: [PATCH 1/4] feat(datcat-ui): various fixed to alerts, notification channels and dashboards --- .../create-alert-button.component.html | 150 ++++++----- .../create-alert-button.component.scss | 18 ++ .../create-alert-button.component.ts | 31 +++ ...e-notification-group-button.component.html | 1 + ...ate-notification-group-button.component.ts | 10 +- .../delete-alert-button.component.html | 7 +- .../delete-alert-button.component.ts | 1 - .../edit-alert/edit-alert-form.component.html | 152 ++++++----- .../edit-alert/edit-alert-form.component.scss | 18 ++ .../edit-alert/edit-alert-form.component.ts | 33 ++- ...dit-notification-group-form.component.html | 117 ++++---- ...dit-notification-group-form.component.scss | 20 +- .../edit-notification-group-form.component.ts | 46 ++-- .../mute-alert-button.component.html | 48 +++- .../mute-alert-button.component.scss | 17 ++ .../mute-alert/mute-alert-button.component.ts | 12 +- .../src/pages/workspace.component.html | 134 +++++----- .../src/pages/workspace.component.ts | 250 +++++++++--------- .../manage-alert/manage-alert.component.html | 14 +- .../manage-alert/manage-alert.component.scss | 9 +- .../manage-notification-group.component.html | 8 +- 21 files changed, 649 insertions(+), 447 deletions(-) diff --git a/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.html b/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.html index 0dfd404..0341c9b 100644 --- a/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.html +++ b/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.html @@ -12,66 +12,72 @@ header="Create alert" >
-
-

Description

- -
-
-

Query

- -
-
-

Notification Template

- -
-
-

Execution interval

- -
-
-

Notification Trigger Period

- -
-
-

Data Source

- -
-
-

Notification Group

- +
+
+
+

Description

+ +
+
+

Query

+ +
+
+

Notification Template

+ +
+
+
+
+

Execution interval

+ +
+
+

Notification Trigger Period

+ +
+
+

Data Source

+ +
+
+

Notification Group

+ +
+

Tags

@@ -97,6 +103,29 @@ /> }
+
+ @if (descriptionControl.invalid) { +

* Description is required

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

* Query is required

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

* Notification temlplate is required

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

* Execution interval is required

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

* Notification trigger period is required

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

* Data source is required

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

* Notification group is required

+ } +
diff --git a/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.scss b/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.scss index 8fdb685..20d7b3a 100644 --- a/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.scss +++ b/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.scss @@ -25,3 +25,21 @@ max-width: 25rem; } } + +.validations { + display: flex; + flex-direction: column; + gap: var(--p-padding-xs); + color: var(--p-surface-400); +} + +.h { + display: flex; + gap: var(--p-padding-lg); +} + +.v { + display: flex; + flex-direction: column; + gap: var(--p-padding-md); +} diff --git a/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.ts b/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.ts index 547f355..e58e18a 100644 --- a/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.ts +++ b/frontend/datacat-ui/src/features/alerting/create-alert/create-alert-button.component.ts @@ -4,6 +4,7 @@ import { ApiService } from '../../../shared/services/datacat-generated-client'; import { DialogModule } from 'primeng/dialog'; import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; import { + AbstractControl, FormControl, FormGroup, ReactiveFormsModule, @@ -76,6 +77,34 @@ export class CreateAlertButtonComponent { tags: new FormControl([]), }); + protected get descriptionControl(): AbstractControl { + return this.creationForm.get('description')!; + } + + protected get templateControl(): AbstractControl { + return this.creationForm.get('template')!; + } + + protected get queryControl(): AbstractControl { + return this.creationForm.get('query')!; + } + + protected get dataSourceIdControl(): AbstractControl { + return this.creationForm.get('dataSourceId')!; + } + + protected get notificationGroupNameControl(): AbstractControl { + return this.creationForm.get('notificationGroupName')!; + } + + protected get notificationTriggerPeriodControl(): AbstractControl { + return this.creationForm.get('notificationTriggerPeriod')!; + } + + protected get executionIntervalControl(): AbstractControl { + return this.creationForm.get('executionInterval')!; + } + protected get creationFormTags(): string[] { return this.creationForm.get('tags')?.value || []; } @@ -122,10 +151,12 @@ export class CreateAlertButtonComponent { tags: rawForm.tags, }; + this.creationForm.disable(); this.apiService .postApiV1AlertAdd(request) .pipe( finalize(() => { + this.creationForm.enable(); this.isCreationInitiated = false; }), ) diff --git a/frontend/datacat-ui/src/features/alerting/create-notification-group/create-notification-group-button.component.html b/frontend/datacat-ui/src/features/alerting/create-notification-group/create-notification-group-button.component.html index 94c1c3c..8fbc041 100644 --- a/frontend/datacat-ui/src/features/alerting/create-notification-group/create-notification-group-button.component.html +++ b/frontend/datacat-ui/src/features/alerting/create-notification-group/create-notification-group-button.component.html @@ -26,6 +26,7 @@ [loading]="isCreationInitiated" label="Create" (onClick)="createNotificationGroup()" + [disabled]="groupName.invalid" />
diff --git a/frontend/datacat-ui/src/features/alerting/create-notification-group/create-notification-group-button.component.ts b/frontend/datacat-ui/src/features/alerting/create-notification-group/create-notification-group-button.component.ts index 8124d2e..571a38a 100644 --- a/frontend/datacat-ui/src/features/alerting/create-notification-group/create-notification-group-button.component.ts +++ b/frontend/datacat-ui/src/features/alerting/create-notification-group/create-notification-group-button.component.ts @@ -5,7 +5,12 @@ import * as urls from '../../../shared/common/urls'; import { finalize, timer } from 'rxjs'; import { DialogModule } from 'primeng/dialog'; import { InputTextModule } from 'primeng/inputtext'; -import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + FormControl, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; import { ApiService, IAddNotificationChannelGroupRequest, @@ -29,7 +34,7 @@ export class CreateNotificationGroupButtonComponent { protected isCreationInitiated = false; protected isDialogVisible = false; - protected groupName = new FormControl(''); + protected groupName = new FormControl('', Validators.required); constructor( private router: Router, @@ -54,7 +59,6 @@ export class CreateNotificationGroupButtonComponent { .subscribe({ next: (groupId: string) => { this.router.navigateByUrl(urls.notificationGroupEditUrl(groupId)); - this.loggerService.success('Successfully created notification group'); }, error: (e) => { this.loggerService.error(e); diff --git a/frontend/datacat-ui/src/features/alerting/delete-alert/delete-alert-button.component.html b/frontend/datacat-ui/src/features/alerting/delete-alert/delete-alert-button.component.html index 22eee79..927054a 100644 --- a/frontend/datacat-ui/src/features/alerting/delete-alert/delete-alert-button.component.html +++ b/frontend/datacat-ui/src/features/alerting/delete-alert/delete-alert-button.component.html @@ -1,4 +1,9 @@ - + { - this.loggerService.success('Deleted alert'); this.router.navigateByUrl(urls.ALERTS_EXPLORER_URL); }, error: (e) => { diff --git a/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.html b/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.html index ff29e4b..ba93cbe 100644 --- a/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.html +++ b/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.html @@ -1,69 +1,72 @@
-
-

Description

- -
-
-

Query

- -
-
-

Notification Template

- -
-
-
-

Execution interval

- +
+
+
+

Description

+ +
+
+

Query

+ +
+
+

Notification Template

+ +
-
-

Notification Trigger Period

- -
-
-
-
-

Data Source

- -
-
-

Notification Group

- +
+
+

Execution interval

+ +
+
+

Notification Trigger Period

+ +
+
+

Data Source

+ +
+
+

Notification Group

+ +
+

Tags

@@ -88,12 +91,35 @@ /> }
+
+ @if (descriptionControl.invalid) { +

* Description is required

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

* Query is required

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

* Notification temlplate is required

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

* Execution interval is required

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

* Notification trigger period is required

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

* Data source is required

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

* Notification group is required

+ } +
diff --git a/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.scss b/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.scss index c3acf74..4191ea4 100644 --- a/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.scss +++ b/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.scss @@ -32,3 +32,21 @@ max-width: 25rem; } } + +.h { + display: flex; + gap: var(--p-padding-lg); +} + +.v { + display: flex; + flex-direction: column; + gap: var(--p-padding-md); +} + +.validations { + display: flex; + flex-direction: column; + gap: var(--p-padding-xs); + color: var(--p-surface-400); +} diff --git a/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.ts b/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.ts index e33c5bd..1645714 100644 --- a/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.ts +++ b/frontend/datacat-ui/src/features/alerting/edit-alert/edit-alert-form.component.ts @@ -4,6 +4,7 @@ import { ApiService } from '../../../shared/services/datacat-generated-client'; import { DialogModule } from 'primeng/dialog'; import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; import { + AbstractControl, FormControl, FormGroup, ReactiveFormsModule, @@ -82,6 +83,34 @@ export class EditAlertFormComponent { tags: new FormControl([]), }); + protected get descriptionControl(): AbstractControl { + return this.editForm.get('description')!; + } + + protected get templateControl(): AbstractControl { + return this.editForm.get('template')!; + } + + protected get queryControl(): AbstractControl { + return this.editForm.get('query')!; + } + + protected get dataSourceIdControl(): AbstractControl { + return this.editForm.get('dataSourceId')!; + } + + protected get notificationGroupNameControl(): AbstractControl { + return this.editForm.get('notificationGroupName')!; + } + + protected get notificationTriggerPeriodControl(): AbstractControl { + return this.editForm.get('notificationTriggerPeriod')!; + } + + protected get executionIntervalControl(): AbstractControl { + return this.editForm.get('executionInterval')!; + } + protected get editFormTags(): string[] { return this.editForm.get('tags')?.value || []; } @@ -143,7 +172,7 @@ export class EditAlertFormComponent { this.editForm.markAllAsTouched(); this.editForm.updateValueAndValidity(); - if (this.editForm.invalid) { + if (this.editForm.invalid || !this._alertId) { return; } @@ -162,7 +191,7 @@ export class EditAlertFormComponent { }; this.apiService - .postApiV1AlertAdd(request) + .putApiV1AlertUpdate(this._alertId, request) .pipe( finalize(() => { this.isSavingInitiated = false; diff --git a/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.html b/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.html index 1b5211e..5356f5e 100644 --- a/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.html +++ b/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.html @@ -25,11 +25,7 @@

Channels

- + @if (notificationChannels.length === 0) { } @else { @@ -37,27 +33,25 @@ @for (channel of notificationChannels; track $index) { @switch (channel.driver) { @case (NotificationChannelDriver.EMAIL) { - +

Email

-
+
@@ -65,65 +59,53 @@

Destination Address

- + }} +

Password Path

- + + {{ asEmailSettings(channel.settings).PasswordPath }} +

Port

- + {{ + asEmailSettings(channel.settings).Port + }}

SMTP Server

- + {{ + asEmailSettings(channel.settings).SmtpServer + }}
- + } @case (NotificationChannelDriver.TELEGRAM) { - +

Telegram

-
+
@@ -131,48 +113,39 @@

Chat ID

- + + {{ asTelegramSettings(channel.settings).ChatId }} +

Telegram Token Path

- + {{ + asTelegramSettings(channel.settings).TelegramTokenPath + }}
- + } @case (NotificationChannelDriver.WEBHOOK) { - +

Webhook

-
+
@@ -180,14 +153,12 @@

URL

- + {{ + asWebhookSettings(channel.settings).Url + }}
- + } } } @@ -251,7 +222,11 @@ label="Cancel" (onClick)="hideChannelCreationDialog()" /> - +
@@ -293,6 +268,10 @@ } } - + diff --git a/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.scss b/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.scss index 142fdfd..3a1e3e2 100644 --- a/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.scss +++ b/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.scss @@ -47,8 +47,16 @@ .channel-attributes { display: flex; flex-wrap: wrap; - gap: var(--p-padding-md); + gap: var(--p-padding-lg); align-items: center; + padding: var(--p-padding-md); + padding-top: 0; + + div { + p { + color: var(--p-surface-400); + } + } } p-card p { @@ -58,10 +66,8 @@ p-card p { .card-header { display: flex; gap: var(--p-padding-md); - margin-left: var(--p-padding-md); - margin-right: var(--p-padding-md); - margin-top: var(--p-padding-md); align-items: center; + width: 100%; justify-content: space-between; &__driver { @@ -70,10 +76,4 @@ p-card p { gap: var(--p-padding-sm); align-items: center; } - - &__actions { - display: flex; - gap: var(--p-padding-md); - align-items: center; - } } diff --git a/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.ts b/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.ts index 46da932..b13a91d 100644 --- a/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.ts +++ b/frontend/datacat-ui/src/features/alerting/edit-notification-group/edit-notification-group-form.component.ts @@ -1,5 +1,10 @@ import { Component, Input } from '@angular/core'; -import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; import { InputTextModule } from 'primeng/inputtext'; import { ButtonModule } from 'primeng/button'; import { @@ -25,6 +30,7 @@ import { DialogModule } from 'primeng/dialog'; import { finalize } from 'rxjs'; import { InputNumberModule } from 'primeng/inputnumber'; import { CardModule } from 'primeng/card'; +import { PanelModule } from 'primeng/panel'; @Component({ standalone: true, @@ -42,6 +48,7 @@ import { CardModule } from 'primeng/card'; DialogModule, InputNumberModule, CardModule, + PanelModule, ], }) export class EditNotificationGroupFormComponent { @@ -118,10 +125,10 @@ export class EditNotificationGroupFormComponent { this.addChannelForm.setControl( 'settings', new FormGroup({ - DestinationEmail: new FormControl(''), - SmtpServer: new FormControl(''), - Port: new FormControl(80), - PasswordPath: new FormControl(''), + DestinationEmail: new FormControl('', Validators.required), + SmtpServer: new FormControl('', Validators.required), + Port: new FormControl(80, Validators.required), + PasswordPath: new FormControl('', Validators.required), }), ); break; @@ -130,8 +137,8 @@ export class EditNotificationGroupFormComponent { this.addChannelForm.setControl( 'settings', new FormGroup({ - TelegramTokenPath: new FormControl(''), - PasswordPath: new FormControl(''), + TelegramTokenPath: new FormControl('', Validators.required), + ChatId: new FormControl('', Validators.required), }), ); break; @@ -140,7 +147,7 @@ export class EditNotificationGroupFormComponent { this.addChannelForm.setControl( 'settings', new FormGroup({ - Url: new FormControl(''), + Url: new FormControl('', Validators.required), }), ); break; @@ -187,7 +194,6 @@ export class EditNotificationGroupFormComponent { ) .subscribe({ next: () => { - this.loggerService.success('Added channel'); this.isChannelCreationDialogVisible = false; this.refresh(); }, @@ -202,10 +208,19 @@ export class EditNotificationGroupFormComponent { case NotificationChannelDriver.EMAIL: { const settings = channel.settings as EmailSettings; this.editChannelForm = new FormGroup({ - DestinationEmail: new FormControl(settings.DestinationEmail), - SmtpServer: new FormControl(settings.SmtpServer), - Port: new FormControl(settings.Port), - PasswordPath: new FormControl(settings.PasswordPath), + DestinationEmail: new FormControl( + settings.DestinationEmail, + Validators.required, + ), + SmtpServer: new FormControl( + settings.SmtpServer, + Validators.required, + ), + Port: new FormControl(settings.Port, Validators.required), + PasswordPath: new FormControl( + settings.PasswordPath, + Validators.required, + ), }); break; } @@ -214,15 +229,16 @@ export class EditNotificationGroupFormComponent { this.editChannelForm = new FormGroup({ TelegramTokenPath: new FormControl( settings.TelegramTokenPath, + Validators.required, ), - ChatId: new FormControl(settings.ChatId), + ChatId: new FormControl(settings.ChatId, Validators.required), }); break; } case NotificationChannelDriver.WEBHOOK: { const settings = channel.settings as WebhookSettings; this.editChannelForm = new FormGroup({ - Url: new FormControl(settings.Url), + Url: new FormControl(settings.Url, Validators.required), }); break; } diff --git a/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.html b/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.html index 744ea51..ac2b6c2 100644 --- a/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.html +++ b/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.html @@ -1,14 +1,34 @@ -
- - - + + + +
+
+

Mute for

+ +
+ +
+
diff --git a/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.scss b/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.scss index d416689..c65b8a8 100644 --- a/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.scss +++ b/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.scss @@ -1,5 +1,22 @@ .form { display: flex; + flex-direction: column; gap: var(--p-padding-md); align-items: center; } + +.item { + display: flex; + flex-direction: column; + gap: var(--p-padding-sm); + + p { + color: var(--p-surface-400); + } +} + +.footer { + display: flex; + width: 100%; + justify-content: end; +} diff --git a/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.ts b/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.ts index 55eb044..b6aaa8b 100644 --- a/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.ts +++ b/frontend/datacat-ui/src/features/alerting/mute-alert/mute-alert-button.component.ts @@ -5,19 +5,29 @@ import { ApiService } from '../../../shared/services/datacat-generated-client'; import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; import { InputMaskModule } from 'primeng/inputmask'; +import { DialogModule } from 'primeng/dialog'; +import { TooltipModule } from 'primeng/tooltip'; @Component({ standalone: true, selector: './datacat-mute-alert-button', templateUrl: './mute-alert-button.component.html', styleUrl: './mute-alert-button.component.scss', - imports: [ButtonModule, ReactiveFormsModule, InputMaskModule], + imports: [ + ButtonModule, + ReactiveFormsModule, + InputMaskModule, + DialogModule, + TooltipModule, + ], }) export class MuteAlertButtonComponent { @Input({ required: true }) alertId?: string; protected isMuteInitiated = false; + protected isDialogVisible = false; + protected nextExecutionTimeControl = new FormControl('', [ Validators.required, ]); diff --git a/frontend/datacat-ui/src/pages/workspace.component.html b/frontend/datacat-ui/src/pages/workspace.component.html index 0271503..fbc17a6 100644 --- a/frontend/datacat-ui/src/pages/workspace.component.html +++ b/frontend/datacat-ui/src/pages/workspace.component.html @@ -1,7 +1,7 @@ - - - - - + + + + + - - -
-
- - -
- -
-
-
- -
-
-
-
+ + +
+
+ + +
+ +
+
+
+ +
+
+
+
diff --git a/frontend/datacat-ui/src/pages/workspace.component.ts b/frontend/datacat-ui/src/pages/workspace.component.ts index a2a9d74..be5022c 100644 --- a/frontend/datacat-ui/src/pages/workspace.component.ts +++ b/frontend/datacat-ui/src/pages/workspace.component.ts @@ -1,145 +1,143 @@ -import {Component, computed} from '@angular/core'; -import {NavigationEnd, Router, RouterModule} from '@angular/router'; +import { Component, computed } from '@angular/core'; +import { NavigationEnd, Router, RouterModule } from '@angular/router'; -import {SplitterModule} from 'primeng/splitter'; -import {BreadcrumbModule} from 'primeng/breadcrumb'; -import {TreeModule, TreeNodeSelectEvent} from 'primeng/tree'; -import {SelectModule} from 'primeng/select'; -import {ButtonModule} from 'primeng/button'; -import {MenuItem, TreeNode} from 'primeng/api'; -import {Location} from '@angular/common'; -import {ActivityBreadcrumbComponent} from '../features/workspace/activity-breadcrumb'; +import { SplitterModule } from 'primeng/splitter'; +import { BreadcrumbModule } from 'primeng/breadcrumb'; +import { TreeModule, TreeNodeSelectEvent } from 'primeng/tree'; +import { SelectModule } from 'primeng/select'; +import { ButtonModule } from 'primeng/button'; +import { MenuItem, TreeNode } from 'primeng/api'; +import { Location } from '@angular/common'; +import { ActivityBreadcrumbComponent } from '../features/workspace/activity-breadcrumb'; import * as urls from '../shared/common/urls'; -import {filter} from 'rxjs'; -import {getAccessTokenFromCookie} from '../shared/interceptors/auth.interceptor'; -import {Toast} from 'primeng/toast'; -import {NamespaceService} from "../shared/services/namespace.service"; -import {FormsModule} from "@angular/forms"; +import { filter } from 'rxjs'; +import { getAccessTokenFromCookie } from '../shared/interceptors/auth.interceptor'; +import { Toast } from 'primeng/toast'; +import { NamespaceService } from '../shared/services/namespace.service'; +import { FormsModule } from '@angular/forms'; interface TreeMenuNodeData { - url?: string; + url?: string; } @Component({ - standalone: true, - selector: 'datacat-main-page', - templateUrl: './workspace.component.html', - styleUrl: './workspace.component.scss', - imports: [ - RouterModule, - SplitterModule, - BreadcrumbModule, - TreeModule, - SelectModule, - ButtonModule, - ActivityBreadcrumbComponent, - Toast, - FormsModule, - - - ], + standalone: true, + selector: 'datacat-main-page', + templateUrl: './workspace.component.html', + styleUrl: './workspace.component.scss', + imports: [ + RouterModule, + SplitterModule, + BreadcrumbModule, + TreeModule, + SelectModule, + ButtonModule, + ActivityBreadcrumbComponent, + Toast, + FormsModule, + ], }) export class WorkspaceComponent { - namespaces = computed(() => this.namespaceService.namespaces()); - selectedNamespace = computed(() => this.namespaceService.currentNamespace()); + namespaces = computed(() => this.namespaceService.namespaces()); + selectedNamespace = computed(() => this.namespaceService.currentNamespace()); - protected activityPathItems: MenuItem[] = []; - protected readonly treeMenuRootNode: TreeNode[] = [ - { - type: 'url', - icon: 'pi pi-megaphone', - label: 'Alerts', - data: { - url: urls.ALERTS_EXPLORER_URL, - }, - }, - { - type: 'url', - icon: 'pi pi-envelope', - label: 'Notifications', - data: { - url: urls.NOTIFICATIONS_EXPLORER_URL, - }, - }, - { - type: 'url', - icon: 'pi pi-table', - label: 'Dashboards', - data: { - url: urls.DASHBOARDS_EXPLORER_URL, - }, - }, - { - type: 'url', - icon: 'pi pi-crown', - label: 'Admin', - data: { - url: urls.ADMIN_URL, - }, - }, - { - type: 'url', - icon: 'pi pi-database', - label: 'Data Sources', - data: { - url: urls.DATA_SOURCES_EXPLORER_URL, - }, - }, - { - type: 'url', - icon: 'pi pi-file', - label: 'Logs', - data: { - url: urls.LOGS_EXPLORER_URL, - }, - }, - { - type: 'url', - icon: 'pi pi-sliders-h', - label: 'Traces', - data: { - url: urls.TRACES_EXPLORER_URL, - }, - }, - ]; + protected activityPathItems: MenuItem[] = []; + protected readonly treeMenuRootNode: TreeNode[] = [ + { + type: 'url', + icon: 'pi pi-megaphone', + label: 'Alerts', + data: { + url: urls.ALERTS_EXPLORER_URL, + }, + }, + { + type: 'url', + icon: 'pi pi-envelope', + label: 'Notifications', + data: { + url: urls.NOTIFICATIONS_EXPLORER_URL, + }, + }, + { + type: 'url', + icon: 'pi pi-table', + label: 'Dashboards', + data: { + url: urls.DASHBOARDS_EXPLORER_URL, + }, + }, + // { + // type: 'url', + // icon: 'pi pi-crown', + // label: 'Admin', + // data: { + // url: urls.ADMIN_URL, + // }, + // }, + { + type: 'url', + icon: 'pi pi-database', + label: 'Data Sources', + data: { + url: urls.DATA_SOURCES_EXPLORER_URL, + }, + }, + { + type: 'url', + icon: 'pi pi-file', + label: 'Logs', + data: { + url: urls.LOGS_EXPLORER_URL, + }, + }, + { + type: 'url', + icon: 'pi pi-sliders-h', + label: 'Traces', + data: { + url: urls.TRACES_EXPLORER_URL, + }, + }, + ]; - constructor( - private router: Router, - private location: Location, - private namespaceService: NamespaceService - ) { - this.router.events - .pipe(filter((event) => event instanceof NavigationEnd)) - .subscribe(() => { - const token = getAccessTokenFromCookie(); - const loginAttempted = localStorage.getItem('login_attempted'); + constructor( + private router: Router, + private location: Location, + private namespaceService: NamespaceService, + ) { + this.router.events + .pipe(filter((event) => event instanceof NavigationEnd)) + .subscribe(() => { + const token = getAccessTokenFromCookie(); + const loginAttempted = localStorage.getItem('login_attempted'); - if (token && loginAttempted) { - localStorage.removeItem('login_attempted'); - console.log('Login attempt reset after successful navigation'); - } - }); - } + if (token && loginAttempted) { + localStorage.removeItem('login_attempted'); + console.log('Login attempt reset after successful navigation'); + } + }); + } - onNamespaceChange(ns: any): void { - this.namespaceService.setCurrentNamespace(ns.value); - } + onNamespaceChange(ns: any): void { + this.namespaceService.setCurrentNamespace(ns.value); + } - protected onTreeMenuNodeSelect(event: TreeNodeSelectEvent) { - if (event.node.data && event.node.data.url) { - this.router.navigate([event.node.data.url]); - } + protected onTreeMenuNodeSelect(event: TreeNodeSelectEvent) { + if (event.node.data && event.node.data.url) { + this.router.navigate([event.node.data.url]); } + } - protected goToPreviousActivity() { - this.location.back(); - } + protected goToPreviousActivity() { + this.location.back(); + } - protected goToNextActivity() { - this.location.forward(); - } + protected goToNextActivity() { + this.location.forward(); + } - protected openSettings() { - this.router.navigate(['settings']); - } + protected openSettings() { + this.router.navigate(['settings']); + } } diff --git a/frontend/datacat-ui/src/processes/manage-alert/manage-alert.component.html b/frontend/datacat-ui/src/processes/manage-alert/manage-alert.component.html index c9d1831..1fd3185 100644 --- a/frontend/datacat-ui/src/processes/manage-alert/manage-alert.component.html +++ b/frontend/datacat-ui/src/processes/manage-alert/manage-alert.component.html @@ -1,9 +1,13 @@ - Edit alert -
- - -
+ +
+

Edit alert

+
+ + +
+
+
diff --git a/frontend/datacat-ui/src/processes/manage-alert/manage-alert.component.scss b/frontend/datacat-ui/src/processes/manage-alert/manage-alert.component.scss index d438089..59ffea8 100644 --- a/frontend/datacat-ui/src/processes/manage-alert/manage-alert.component.scss +++ b/frontend/datacat-ui/src/processes/manage-alert/manage-alert.component.scss @@ -2,10 +2,9 @@ margin: var(--p-padding-md); } -.actions { +.header { + width: 100%; display: flex; - flex-direction: row; - gap: var(--p-padding-md); - margin-left: var(--p-padding-md); - margin-right: var(--p-padding-md); + justify-content: space-between; + align-items: center; } diff --git a/frontend/datacat-ui/src/processes/manage-notification-group/manage-notification-group.component.html b/frontend/datacat-ui/src/processes/manage-notification-group/manage-notification-group.component.html index 6f60df1..f401fc4 100644 --- a/frontend/datacat-ui/src/processes/manage-notification-group/manage-notification-group.component.html +++ b/frontend/datacat-ui/src/processes/manage-notification-group/manage-notification-group.component.html @@ -1,15 +1,9 @@ @if (groupId) { - - Edit notification group {{ groupId }} - + Edit notification group
-
} @else { Loading From add60efcbc2404faaf773740ec0aa877d1a51d31 Mon Sep 17 00:00:00 2001 From: Nikita Nazarov Date: Thu, 29 May 2025 15:33:53 +0300 Subject: [PATCH 2/4] feat(datacat-ui): fix data sources styles --- .../alerts-list/alerts-list.component.ts | 3 +- .../create-data-source-button.component.html | 10 +- .../create-data-source-button.component.ts | 47 +-- .../create-data-source-form.component.html | 97 ++--- .../create-data-source-form.component.scss | 33 +- .../create-data-source-form.component.ts | 358 ++++++++++-------- ...elasticsearch-settings-form.component.html | 38 +- ...elasticsearch-settings-form.component.scss | 17 +- .../elasticsearch-settings-form.component.ts | 30 +- .../jaeger-settings-form.component.html | 53 +-- .../jaeger-settings-form.component.scss | 13 +- .../jaeger-settings-form.component.ts | 46 +-- .../prometheus-settings-form.component.html | 57 +-- .../prometheus-settings-form.component.scss | 17 +- .../prometheus-settings-form.component.ts | 46 +-- .../data-sources-filtering.component.html | 75 ++-- .../data-sources-filtering.component.scss | 22 +- .../data-sources-filtering.component.ts | 121 +++--- .../data-sources-list.component.html | 112 +++--- .../data-sources-list.component.scss | 5 + .../data-sources-list.component.ts | 246 ++++++------ .../explore-data-sources.component.html | 22 +- .../explore-data-sources.component.ts | 43 +-- .../src/shared/services/app-dialog.service.ts | 44 +-- .../src/shared/services/namespace.service.ts | 61 +-- .../services/realtime-metrics.service.ts | 38 +- .../shared/services/toast-logger.service.ts | 58 +-- .../src/shared/services/user.service.ts | 46 +-- 28 files changed, 914 insertions(+), 844 deletions(-) diff --git a/frontend/datacat-ui/src/features/alerting/alerts-list/alerts-list.component.ts b/frontend/datacat-ui/src/features/alerting/alerts-list/alerts-list.component.ts index d5fe5a6..c3d0cc2 100644 --- a/frontend/datacat-ui/src/features/alerting/alerts-list/alerts-list.component.ts +++ b/frontend/datacat-ui/src/features/alerting/alerts-list/alerts-list.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { TableLazyLoadEvent, TableModule } from 'primeng/table'; -import { Alert, AlertStatus, DataSource } from '../../../entities'; +import { Alert, AlertStatus } from '../../../entities'; import { TagModule } from 'primeng/tag'; import { CommonModule } from '@angular/common'; import { ButtonModule } from 'primeng/button'; @@ -26,7 +26,6 @@ import { TooltipModule } from 'primeng/tooltip'; import { DialogModule } from 'primeng/dialog'; import { DataSourceSelectComponent } from '../../../shared/ui/data-source-select/data-source-select.component'; import * as urls from '../../../shared/common/urls'; -import { LazyLoadEvent } from 'primeng/api'; @Component({ standalone: true, diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-button/create-data-source-button.component.html b/frontend/datacat-ui/src/features/data-sources/create-data-source-button/create-data-source-button.component.html index a813242..0ed8356 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-button/create-data-source-button.component.html +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-button/create-data-source-button.component.html @@ -1,7 +1,7 @@ diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-button/create-data-source-button.component.ts b/frontend/datacat-ui/src/features/data-sources/create-data-source-button/create-data-source-button.component.ts index 94ffeaf..a80990e 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-button/create-data-source-button.component.ts +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-button/create-data-source-button.component.ts @@ -1,30 +1,31 @@ -import {Component} from '@angular/core'; -import {Router} from "@angular/router"; -import {ButtonModule} from "primeng/button"; -import {AppDialogService} from "../../../shared/services/app-dialog.service"; -import {CreateDataSourceFormComponent} from "../create-data-source-form/create-data-source-form.component"; -import {ApiService} from "../../../shared/services/datacat-generated-client"; +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { ButtonModule } from 'primeng/button'; +import { AppDialogService } from '../../../shared/services/app-dialog.service'; +import { CreateDataSourceFormComponent } from '../create-data-source-form/create-data-source-form.component'; +import { ApiService } from '../../../shared/services/datacat-generated-client'; @Component({ - selector: 'app-create-data-source-button', - standalone: true, - imports: [ - ButtonModule - ], - templateUrl: './create-data-source-button.component.html', - styleUrl: './create-data-source-button.component.scss' + selector: 'app-create-data-source-button', + standalone: true, + imports: [ButtonModule], + templateUrl: './create-data-source-button.component.html', + styleUrl: './create-data-source-button.component.scss', }) export class CreateDataSourceButtonComponent { - protected isBusy = false; + protected isBusy = false; - constructor( - private dialogService: AppDialogService, - private router: Router, - private apiService: ApiService, - ) { - } + constructor( + private dialogService: AppDialogService, + private router: Router, + private apiService: ApiService, + ) {} - protected createDataSource() { - this.dialogService.showDialog(CreateDataSourceFormComponent, 'Create Data Source', {}); - } + protected createDataSource() { + this.dialogService.showDialog( + CreateDataSourceFormComponent, + 'Create Data Source', + {}, + ); + } } diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.html b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.html index 4bf018f..b8b5ef1 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.html +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.html @@ -1,60 +1,69 @@
-
+
+
+

Name

- -
- -
-

Data Source Type

- -
- -
+
+

Purpose

+
+
+

Data Source Type

+ +
- -
- @switch (form.value.dataSourceType) { +
+ +
+ @switch (form.value.dataSourceType) { @case ('prometheus') { - + } @case ('jaeger') { - + } @case ('elasticsearch') { - + } - } + } +
+
+
-
- -
+
+ +
diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.scss b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.scss index 1aac85f..3e8b354 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.scss +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.scss @@ -1,22 +1,17 @@ .form { - padding: 24px; - border-radius: 12px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; - gap: 24px; + gap: var(--p-padding-md); + align-items: start; &__item { display: flex; flex-direction: column; - gap: 8px; + gap: var(--p-padding-sm); } &__label { - font-size: 14px; - color: #6b7280; - font-weight: 500; - margin: 0; + color: var(--p-surface-400); } &__input { @@ -24,14 +19,30 @@ } &__settings { - padding-top: 16px; + padding: var(--p-padding-md); + padding-top: 0; } &__actions { - padding-top: 24px; + width: 100%; + display: flex; + justify-content: end; } &__submit { width: 100%; + display: flex; + justify-content: end; } } + +.h { + display: flex; + gap: var(--p-padding-lg); +} + +.v { + display: flex; + flex-direction: column; + gap: var(--p-padding-md); +} diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.ts b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.ts index aba6081..5e5cd17 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.ts +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.ts @@ -1,169 +1,215 @@ -import {Component, OnInit} from '@angular/core'; -import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; -import {ButtonModule} from "primeng/button"; -import {InputTextModule} from "primeng/inputtext"; -import {SelectModule} from "primeng/select"; - -import {PrometheusSettingsFormComponent} from "./prometheus-settings-form/prometheus-settings-form.component"; -import {catchError, finalize, tap} from "rxjs"; -import {ToastLoggerService} from "../../../shared/services/toast-logger.service"; -import {JaegerSettingsFormComponent} from "./jaeger-settings-form/jaeger-settings-form.component"; -import {ElasticsearchSettingsFormComponent} from "./elasticsearch-settings-form/elasticsearch-settings-form.component"; +import { Component, OnInit } from '@angular/core'; import { - ApiService, - DataSourceKind, - IAddDataSourceRequest, - IGetDataSourceTypeResponse -} from "../../../shared/services/datacat-generated-client"; + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; +import { SelectModule } from 'primeng/select'; + +import { PrometheusSettingsFormComponent } from './prometheus-settings-form/prometheus-settings-form.component'; +import { catchError, finalize, tap } from 'rxjs'; +import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; +import { JaegerSettingsFormComponent } from './jaeger-settings-form/jaeger-settings-form.component'; +import { ElasticsearchSettingsFormComponent } from './elasticsearch-settings-form/elasticsearch-settings-form.component'; +import { + ApiService, + DataSourceKind, + IAddDataSourceRequest, + IGetDataSourceTypeResponse, +} from '../../../shared/services/datacat-generated-client'; +import { PanelModule } from 'primeng/panel'; @Component({ - selector: 'app-create-data-source-form', - standalone: true, - imports: [ - ReactiveFormsModule, - ButtonModule, - InputTextModule, - SelectModule, - PrometheusSettingsFormComponent, - JaegerSettingsFormComponent, - ElasticsearchSettingsFormComponent - ], - templateUrl: './create-data-source-form.component.html', - styleUrl: './create-data-source-form.component.scss' + selector: 'app-create-data-source-form', + standalone: true, + imports: [ + ReactiveFormsModule, + ButtonModule, + InputTextModule, + SelectModule, + PrometheusSettingsFormComponent, + JaegerSettingsFormComponent, + ElasticsearchSettingsFormComponent, + PanelModule, + ], + templateUrl: './create-data-source-form.component.html', + styleUrl: './create-data-source-form.component.scss', }) export class CreateDataSourceFormComponent implements OnInit { - isBusy = false; + isBusy = false; - dataSourceTypes: IGetDataSourceTypeResponse[] = []; - dataSourcePurposes = Object.values(DataSourceKind); + dataSourceTypes: IGetDataSourceTypeResponse[] = []; + dataSourcePurposes = Object.values(DataSourceKind); - form = this.fb.group({ - name: ['', Validators.required], - dataSourceType: ['', Validators.required], - purpose: [null as DataSourceKind | null, Validators.required], - settings: this.fb.group({}) + form = this.fb.group({ + name: ['', Validators.required], + dataSourceType: ['prometheus', Validators.required], + purpose: [ + DataSourceKind.Metrics as DataSourceKind | null, + Validators.required, + ], + settings: this.fb.group({}), + }); + + constructor( + private fb: FormBuilder, + private apiService: ApiService, + private toastLoggerService: ToastLoggerService, + ) {} + + get settingsForm(): FormGroup { + return this.form.get('settings') as FormGroup; + } + + ngOnInit() { + this.isBusy = true; + + this.apiService + .getApiV1DataSourceTypeGetAll() + .pipe( + finalize(() => (this.isBusy = false)), + tap((res) => { + this.dataSourceTypes = res; + }), + catchError((err) => { + this.toastLoggerService.error(err); + return err; + }), + ) + .subscribe(); + + this.form.get('dataSourceType')?.valueChanges.subscribe((type) => { + this.updateSettingsForm(type!); }); - - constructor( - private fb: FormBuilder, - private apiService: ApiService, - private toastLoggerService: ToastLoggerService - ) { - } - - get settingsForm(): FormGroup { - return this.form.get('settings') as FormGroup; + } + + saveDataSource() { + if (this.form.invalid) return; + + const formValue = this.form.value; + const connectionSettings = this.getConnectionSettings(formValue); + + const request: any = { + uniqueName: formValue.name!, + dataSourceType: formValue.dataSourceType!, + purpose: formValue.purpose!, + connectionString: JSON.stringify(connectionSettings), + } as IAddDataSourceRequest; + + this.apiService + .postApiV1DataSourceAdd(request) + .pipe( + finalize(() => (this.isBusy = false)), + tap((res) => { + this.toastLoggerService.info(res); + }), + catchError((err) => { + this.toastLoggerService.error(err); + return err; + }), + ) + .subscribe(); + } + + private updateSettingsForm(type: string) { + while (Object.keys(this.settingsForm.controls).length) { + this.settingsForm.removeControl( + Object.keys(this.settingsForm.controls)[0], + ); } - ngOnInit() { - this.isBusy = true; - - this.apiService.getApiV1DataSourceTypeGetAll().pipe( - finalize(() => this.isBusy = false), - tap(res => { - this.dataSourceTypes = res; - }), - catchError((err) => { - this.toastLoggerService.error(err); - return err; - }) - ).subscribe(); - - this.form.get('dataSourceType')?.valueChanges.subscribe(type => { - this.updateSettingsForm(type!); - }); + const regexp_no_domain_validation = + /^(https?:\/\/)?((([a-z0-9-]+\.)+([a-z]{2,}))|((\d{1,3}\.){3}\d{1,3}))(:\d{1,5})?$/i; + const regexp_with_domain_validation = + /^(https?:\/\/)?((((?!-)([a-z0-9-]{1,63})(? this.isBusy = false), - tap(res => { - this.toastLoggerService.info(res); - }), - catchError((err) => { - this.toastLoggerService.error(err); - return err; - }) - ).subscribe() - } - - private updateSettingsForm(type: string) { - while (Object.keys(this.settingsForm.controls).length) { - this.settingsForm.removeControl(Object.keys(this.settingsForm.controls)[0]); - } - - const regexp_no_domain_validation = /^(https?:\/\/)?((([a-z0-9-]+\.)+([a-z]{2,}))|((\d{1,3}\.){3}\d{1,3}))(:\d{1,5})?$/i; - const regexp_with_domain_validation = /^(https?:\/\/)?((((?!-)([a-z0-9-]{1,63})(? +
+

Cluster URL

+ +
-
-

Cluster URL

- -
+
+

Index Pattern

+ +
-
-

Index Pattern

- -
+
+

Username

+ +
-
-

Username

- -
- -
-

Password

- -
+
+

Password

+ +
diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/elasticsearch-settings-form/elasticsearch-settings-form.component.scss b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/elasticsearch-settings-form/elasticsearch-settings-form.component.scss index 7944b63..2eb0e79 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/elasticsearch-settings-form/elasticsearch-settings-form.component.scss +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/elasticsearch-settings-form/elasticsearch-settings-form.component.scss @@ -1,27 +1,16 @@ .settings-group { - margin-bottom: 32px; - padding: 24px; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); display: flex; flex-direction: column; - gap: 20px; - - &:last-child { - margin-bottom: 0; - } + gap: var(--p-padding-md); .form__item { display: flex; flex-direction: column; - gap: 6px; + gap: var(--p-padding-sm); } .text-secondary { - font-size: 14px; - color: #6b7280; - font-weight: 500; - margin: 0; + color: var(--p-surface-400); } input[pInputText], diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/elasticsearch-settings-form/elasticsearch-settings-form.component.ts b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/elasticsearch-settings-form/elasticsearch-settings-form.component.ts index c14e2cf..3973eae 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/elasticsearch-settings-form/elasticsearch-settings-form.component.ts +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/elasticsearch-settings-form/elasticsearch-settings-form.component.ts @@ -1,24 +1,14 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {FormGroup, ReactiveFormsModule} from "@angular/forms"; -import {InputNumber} from "primeng/inputnumber"; -import {InputText} from "primeng/inputtext"; -import {Checkbox} from "primeng/checkbox"; +import { Component, Input, OnInit } from '@angular/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { InputText } from 'primeng/inputtext'; @Component({ - selector: 'app-elasticsearch-settings-form', - standalone: true, - imports: [ - InputNumber, - InputText, - ReactiveFormsModule, - Checkbox - ], - templateUrl: './elasticsearch-settings-form.component.html', - styleUrl: './elasticsearch-settings-form.component.scss' + selector: 'app-elasticsearch-settings-form', + standalone: true, + imports: [InputText, ReactiveFormsModule], + templateUrl: './elasticsearch-settings-form.component.html', + styleUrl: './elasticsearch-settings-form.component.scss', }) -export class ElasticsearchSettingsFormComponent implements OnInit { - @Input() form!: FormGroup; - - ngOnInit() { - } +export class ElasticsearchSettingsFormComponent { + @Input() form!: FormGroup; } diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.html b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.html index e765af2..78ec265 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.html +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.html @@ -1,33 +1,34 @@
+
+

Server URL

+ +
+ +
+

Authentication Type

+ +
+ + @if (authType === 'Basic') {
-

Server URL

- +

Username

+
-
-

Authentication Type

- +

Password

+
+ } - @if (authType === 'Basic') { -
-

Username

- -
-
-

Password

- -
- } - - @if (authType === 'Bearer') { -
-

Auth Token

- -
- } + @if (authType === 'Bearer') { +
+

Auth Token

+ +
+ }
diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.scss b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.scss index 7944b63..80f3770 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.scss +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.scss @@ -1,11 +1,7 @@ .settings-group { - margin-bottom: 32px; - padding: 24px; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); display: flex; flex-direction: column; - gap: 20px; + gap: var(--p-padding-md); &:last-child { margin-bottom: 0; @@ -14,14 +10,11 @@ .form__item { display: flex; flex-direction: column; - gap: 6px; + gap: var(--p-padding-sm); } .text-secondary { - font-size: 14px; - color: #6b7280; - font-weight: 500; - margin: 0; + color: var(--p-surface-400); } input[pInputText], diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.ts b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.ts index 4eb04dd..0bc3928 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.ts +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/jaeger-settings-form/jaeger-settings-form.component.ts @@ -1,34 +1,30 @@ -import {Component, Input} from '@angular/core'; -import {Select} from "primeng/select"; -import {FormGroup, ReactiveFormsModule} from "@angular/forms"; -import {InputText} from "primeng/inputtext"; +import { Component, Input } from '@angular/core'; +import { Select } from 'primeng/select'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { InputText } from 'primeng/inputtext'; @Component({ - selector: 'app-jaeger-settings-form', - standalone: true, - imports: [ - Select, - ReactiveFormsModule, - InputText - ], - templateUrl: './jaeger-settings-form.component.html', - styleUrl: './jaeger-settings-form.component.scss' + selector: 'app-jaeger-settings-form', + standalone: true, + imports: [Select, ReactiveFormsModule, InputText], + templateUrl: './jaeger-settings-form.component.html', + styleUrl: './jaeger-settings-form.component.scss', }) export class JaegerSettingsFormComponent { - @Input() form!: FormGroup; - @Input() authType!: string | undefined | null; + @Input() form!: FormGroup; + @Input() authType!: string | undefined | null; - authTypes = ['None', 'Basic', 'Bearer']; + authTypes = ['None', 'Basic', 'Bearer']; - onAuthTypeChange() { - const authType = this.form.value.authType; + onAuthTypeChange() { + const authType = this.form.value.authType; - if (authType === 'None') { - this.form.patchValue({ - username: null, - password: null, - authToken: null - }); - } + if (authType === 'None') { + this.form.patchValue({ + username: null, + password: null, + authToken: null, + }); } + } } diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.html b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.html index e765af2..ff8ba70 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.html +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.html @@ -1,33 +1,38 @@
+
+

Server URL

+ +
+ +
+

Authentication Type

+ +
+ + @if (authType === 'Basic') {
-

Server URL

- +

Username

+
-
-

Authentication Type

- +

Password

+
+ } - @if (authType === 'Basic') { -
-

Username

- -
-
-

Password

- -
- } - - @if (authType === 'Bearer') { -
-

Auth Token

- -
- } + @if (authType === 'Bearer') { +
+

Auth Token

+ +
+ }
diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.scss b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.scss index 7944b63..2eb0e79 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.scss +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.scss @@ -1,27 +1,16 @@ .settings-group { - margin-bottom: 32px; - padding: 24px; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); display: flex; flex-direction: column; - gap: 20px; - - &:last-child { - margin-bottom: 0; - } + gap: var(--p-padding-md); .form__item { display: flex; flex-direction: column; - gap: 6px; + gap: var(--p-padding-sm); } .text-secondary { - font-size: 14px; - color: #6b7280; - font-weight: 500; - margin: 0; + color: var(--p-surface-400); } input[pInputText], diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.ts b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.ts index 88a1b31..94ba437 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.ts +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/prometheus-settings-form/prometheus-settings-form.component.ts @@ -1,34 +1,30 @@ -import {Component, Input} from '@angular/core'; -import {FormGroup, ReactiveFormsModule} from "@angular/forms"; -import {Select} from "primeng/select"; -import {InputText} from "primeng/inputtext"; +import { Component, Input } from '@angular/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { Select } from 'primeng/select'; +import { InputText } from 'primeng/inputtext'; @Component({ - selector: 'app-prometheus-settings-form', - standalone: true, - imports: [ - Select, - ReactiveFormsModule, - InputText - ], - templateUrl: './prometheus-settings-form.component.html', - styleUrl: './prometheus-settings-form.component.scss' + selector: 'app-prometheus-settings-form', + standalone: true, + imports: [Select, ReactiveFormsModule, InputText], + templateUrl: './prometheus-settings-form.component.html', + styleUrl: './prometheus-settings-form.component.scss', }) export class PrometheusSettingsFormComponent { - @Input() form!: FormGroup; - @Input() authType!: string | undefined | null; + @Input() form!: FormGroup; + @Input() authType!: string | undefined | null; - authTypes = ['None', 'Basic', 'Bearer']; + authTypes = ['None', 'Basic', 'Bearer']; - onAuthTypeChange() { - const authType = this.form.value.authType; + onAuthTypeChange() { + const authType = this.form.value.authType; - if (authType === 'None') { - this.form.patchValue({ - username: null, - password: null, - authToken: null - }); - } + if (authType === 'None') { + this.form.patchValue({ + username: null, + password: null, + authToken: null, + }); } + } } diff --git a/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.html b/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.html index 3e639d2..b392fa7 100644 --- a/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.html +++ b/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.html @@ -1,38 +1,45 @@ - @if (isBusy) { - - } @else { -
-
-
-

Name

- -
+ @if (isBusy) { + + } @else { + +
+
+

Name

+ +
-
-

Type

- -
+
+

Type

+ +
-
-

Purpose

- -
-
- - } +
+

Purpose

+ +
+
+ + }
diff --git a/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.scss b/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.scss index 66eabd2..29d0b0c 100644 --- a/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.scss +++ b/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.scss @@ -1,13 +1,13 @@ .box { - padding: 1rem; display: flex; flex-direction: column; - gap: 1rem; + padding: var(--p-padding-md); + padding-top: 0; &__filters { display: flex; flex-wrap: wrap; - gap: 1.5rem; + gap: var(--p-padding-md); } } @@ -16,11 +16,10 @@ flex-direction: column; flex: 1 1 200px; min-width: 200px; + gap: var(--p-padding-sm); - p.text-secondary { - margin-bottom: 0.5rem; - font-weight: 500; - color: #6c757d; + p { + color: var(--p-surface-400); } input, @@ -46,15 +45,6 @@ .box__filters { .p-inputtext { - padding-top: 0.75rem; - padding-bottom: 0.75rem; height: calc(2.25rem + 2px); } - - .p-dropdown { - .p-dropdown-label { - padding-top: 0.75rem; - padding-bottom: 0.75rem; - } - } } diff --git a/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.ts b/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.ts index 658cf19..dcdb930 100644 --- a/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.ts +++ b/frontend/datacat-ui/src/features/data-sources/data-sources-filtering/data-sources-filtering.component.ts @@ -1,76 +1,71 @@ -import {Component, EventEmitter, OnInit, Output} from '@angular/core'; -import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; -import {DataSourcesFilter} from "../../../entities/data-sources/data-sources-filter"; -import {catchError, debounceTime, finalize, tap} from "rxjs"; -import {ToastLoggerService} from "../../../shared/services/toast-logger.service"; -import {Select} from "primeng/select"; -import {Panel} from "primeng/panel"; -import {InputText} from "primeng/inputtext"; -import {ProgressBar} from "primeng/progressbar"; +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { DataSourcesFilter } from '../../../entities/data-sources/data-sources-filter'; +import { catchError, debounceTime, finalize, tap } from 'rxjs'; +import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; +import { Select } from 'primeng/select'; +import { Panel } from 'primeng/panel'; +import { InputText } from 'primeng/inputtext'; +import { ProgressBar } from 'primeng/progressbar'; import { - ApiService, - DataSourceKind, - IGetDataSourceTypeResponse -} from "../../../shared/services/datacat-generated-client"; + ApiService, + DataSourceKind, + IGetDataSourceTypeResponse, +} from '../../../shared/services/datacat-generated-client'; @Component({ - selector: 'app-data-sources-filtering', - standalone: true, - imports: [ - Select, - Panel, - InputText, - ReactiveFormsModule, - ProgressBar - ], - templateUrl: './data-sources-filtering.component.html', - styleUrl: './data-sources-filtering.component.scss' + selector: 'app-data-sources-filtering', + standalone: true, + imports: [Select, Panel, InputText, ReactiveFormsModule, ProgressBar], + templateUrl: './data-sources-filtering.component.html', + styleUrl: './data-sources-filtering.component.scss', }) export class DataSourcesFilteringComponent implements OnInit { - isBusy = false; + isBusy = false; - @Output('filter') filterEmitter = new EventEmitter(); + @Output('filter') filterEmitter = new EventEmitter(); - protected form = new FormGroup({ - name: new FormControl(null), - type: new FormControl(null), - typeId: new FormControl(null), - purpose: new FormControl(null) - }); + protected form = new FormGroup({ + name: new FormControl(null), + type: new FormControl(null), + typeId: new FormControl(null), + purpose: new FormControl(null), + }); - protected types: IGetDataSourceTypeResponse[] = []; - protected purposes = Object.values(DataSourceKind); + protected types: IGetDataSourceTypeResponse[] = []; + protected purposes = Object.values(DataSourceKind); - constructor( - private apiService: ApiService, - private toastLoggerService: ToastLoggerService, - ) { - this.form.valueChanges.pipe( - debounceTime(300) - ).subscribe((value) => { - const selectedType = this.types.find(type => type.name === value.type); + constructor( + private apiService: ApiService, + private toastLoggerService: ToastLoggerService, + ) { + this.form.valueChanges.pipe(debounceTime(300)).subscribe((value) => { + const selectedType = this.types.find((type) => type.name === value.type); - this.filterEmitter.emit({ - name: value.name || undefined, - type: value.type || undefined, - typeId: selectedType?.id || undefined, - purpose: value.purpose || undefined - }); - }); - } + this.filterEmitter.emit({ + name: value.name || undefined, + type: value.type || undefined, + typeId: selectedType?.id || undefined, + purpose: value.purpose || undefined, + }); + }); + } - ngOnInit() { - this.isBusy = true; + ngOnInit() { + this.isBusy = true; - this.apiService.getApiV1DataSourceTypeGetAll().pipe( - finalize(() => this.isBusy = false), - tap(res => { - this.types = res; - }), - catchError((err) => { - this.toastLoggerService.error(err); - return err; - }) - ).subscribe(); - } + this.apiService + .getApiV1DataSourceTypeGetAll() + .pipe( + finalize(() => (this.isBusy = false)), + tap((res) => { + this.types = res; + }), + catchError((err) => { + this.toastLoggerService.error(err); + return err; + }), + ) + .subscribe(); + } } diff --git a/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.html b/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.html index b9da9d7..891956b 100644 --- a/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.html +++ b/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.html @@ -1,47 +1,71 @@ - - - Name - Type - Purpose - Actions - - - - - {{ dataSource.name }} - {{ dataSource.type }} - - - - -
- -
- - -
- - - - No data sources found - - - + + + Name + Type + Purpose + Actions + + + + + {{ dataSource.name }} + +
+ @switch (dataSource.type) { + @case ('prometheus') { + + } + @case ('jaeger') { + + } + @case ('elasticsearch') { + + } + } + {{ dataSource.type }} +
+ + + + + +
+ +
+ + +
+ + + No data sources found + +
diff --git a/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.scss b/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.scss index ee2013b..a2d9281 100644 --- a/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.scss +++ b/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.scss @@ -15,3 +15,8 @@ .text-center { text-align: center; } + +img { + width: 1rem; + height: 1rem; +} diff --git a/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.ts b/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.ts index dfc768f..6ea2a29 100644 --- a/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.ts +++ b/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.ts @@ -1,145 +1,149 @@ -import {Component, Input, OnInit, SimpleChanges} from '@angular/core'; -import {DataSourcesFilter} from "../../../entities/data-sources/data-sources-filter"; -import {Router} from "@angular/router"; +import { Component, Input, OnInit, SimpleChanges } from '@angular/core'; +import { DataSourcesFilter } from '../../../entities/data-sources/data-sources-filter'; +import { Router } from '@angular/router'; import { - ApiService, - ISearchDataSourcesResponse, - ISearchFilters, - MatchMode, - SearchFieldType, - SearchFilter, - SearchFilters -} from "../../../shared/services/datacat-generated-client"; -import {LazyLoadEvent} from "primeng/api"; -import {TableModule} from "primeng/table"; -import {Button} from "primeng/button"; -import {Tooltip} from "primeng/tooltip"; -import {ToastLoggerService} from "../../../shared/services/toast-logger.service"; -import {Tag} from "primeng/tag"; -import {capitalizeFirstLetter} from "../../../shared/utils/capitalizeFirstLetter"; -import * as urls from "../../../shared/common/urls"; + ApiService, + ISearchDataSourcesResponse, + ISearchFilters, + MatchMode, + SearchFieldType, + SearchFilter, + SearchFilters, +} from '../../../shared/services/datacat-generated-client'; +import { LazyLoadEvent } from 'primeng/api'; +import { TableModule } from 'primeng/table'; +import { Button } from 'primeng/button'; +import { Tooltip } from 'primeng/tooltip'; +import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; +import { Tag } from 'primeng/tag'; +import { capitalizeFirstLetter } from '../../../shared/utils/capitalizeFirstLetter'; +import * as urls from '../../../shared/common/urls'; @Component({ - selector: 'app-data-sources-list', - standalone: true, - imports: [ - TableModule, - Button, - Tooltip, - Tag, - ], - templateUrl: './data-sources-list.component.html', - styleUrl: './data-sources-list.component.scss' + selector: 'app-data-sources-list', + standalone: true, + imports: [TableModule, Button, Tooltip, Tag], + templateUrl: './data-sources-list.component.html', + styleUrl: './data-sources-list.component.scss', }) export class DataSourcesListComponent implements OnInit { - @Input() protected totalRecords = 0; - @Input() protected rows = 10; - @Input() protected dataSources: ISearchDataSourcesResponse[] = []; - @Input() protected loading = false; - protected currentPage = 1; // Текущая страница + @Input() protected totalRecords = 0; + @Input() protected rows = 10; + @Input() protected dataSources: ISearchDataSourcesResponse[] = []; + @Input() protected loading = false; + protected currentPage = 1; // Текущая страница - constructor( - private router: Router, - private apiService: ApiService, - private toastLoggerService: ToastLoggerService, - ) { - } + constructor( + private router: Router, + private apiService: ApiService, + private toastLoggerService: ToastLoggerService, + ) {} - @Input() public set filter(filter: DataSourcesFilter | undefined) { - if (filter) { - this.currentPage = 1; - this.refreshDataSources(filter, this.currentPage, this.rows); - } + @Input() public set filter(filter: DataSourcesFilter | undefined) { + if (filter) { + this.currentPage = 1; + this.refreshDataSources(filter, this.currentPage, this.rows); } + } - ngOnInit() { - this.currentPage = 1; - this.refreshDataSources(this.filter, this.currentPage, this.rows); - } + ngOnInit() { + this.currentPage = 1; + this.refreshDataSources(this.filter, this.currentPage, this.rows); + } - ngOnChanges(changes: SimpleChanges) { - if (changes['filter'] && !changes['filter'].firstChange) { - this.currentPage = 1; - } + ngOnChanges(changes: SimpleChanges) { + if (changes['filter'] && !changes['filter'].firstChange) { + this.currentPage = 1; } + } - protected editDataSource(dataSourceId: string) { - this.router.navigateByUrl(urls.dataSourceEditUrl(dataSourceId)); - } + protected editDataSource(dataSourceId: string) { + this.router.navigateByUrl(urls.dataSourceEditUrl(dataSourceId)); + } - protected loadData(event: LazyLoadEvent) { - if (event.first !== undefined && event.rows !== undefined) { - this.currentPage = Math.floor(event.first / event.rows) + 1; - this.rows = event.rows; - this.refreshDataSources(this.filter, this.currentPage, this.rows); - } + protected loadData(event: LazyLoadEvent) { + if (event.first !== undefined && event.rows !== undefined) { + this.currentPage = Math.floor(event.first / event.rows) + 1; + this.rows = event.rows; + this.refreshDataSources(this.filter, this.currentPage, this.rows); } + } - protected getSeverity(purpose: string): 'info' | 'warn' | 'success' | 'secondary' { - switch (purpose?.toLowerCase()) { - case 'metrics': - return 'info'; - case 'logs': - return 'warn'; - case 'traces': - return 'success'; - default: - return 'secondary'; - } + protected getSeverity( + purpose: string, + ): 'info' | 'warn' | 'success' | 'secondary' { + switch (purpose?.toLowerCase()) { + case 'metrics': + return 'info'; + case 'logs': + return 'warn'; + case 'traces': + return 'success'; + default: + return 'secondary'; } + } - private refreshDataSources(filter: DataSourcesFilter | undefined, page: number, pageSize: number) { - this.loading = true; - const request = this.convertToApiFilters(filter); - console.log(request); - - this.apiService.postApiV1DataSourceSearch(request, page, pageSize).subscribe({ - next: (result) => { - this.dataSources = result.items ?? []; - this.totalRecords = result.totalCount ?? 0; - this.loading = false; - }, - error: (err) => { - this.toastLoggerService.error(err.message); - console.error('Failed to load data sources', err); - this.loading = false; - } - }); - } + private refreshDataSources( + filter: DataSourcesFilter | undefined, + page: number, + pageSize: number, + ) { + this.loading = true; + const request = this.convertToApiFilters(filter); + console.log(request); - private convertToApiFilters(filter: DataSourcesFilter | undefined): SearchFilters { - let request = { - filters: [], - sort: undefined - } as ISearchFilters; + this.apiService + .postApiV1DataSourceSearch(request, page, pageSize) + .subscribe({ + next: (result) => { + this.dataSources = result.items ?? []; + this.totalRecords = result.totalCount ?? 0; + this.loading = false; + }, + error: (err) => { + this.toastLoggerService.error(err.message); + console.error('Failed to load data sources', err); + this.loading = false; + }, + }); + } - if (filter?.name) { - request.filters!.push({ - key: 'name', - value: filter.name, - matchMode: MatchMode.Equals, - fieldType: SearchFieldType.String - } as SearchFilter); - } + private convertToApiFilters( + filter: DataSourcesFilter | undefined, + ): SearchFilters { + let request = { + filters: [], + sort: undefined, + } as ISearchFilters; - if (filter?.typeId) { - request.filters!.push({ - key: 'typeId', - value: filter.typeId, - matchMode: MatchMode.Equals, - fieldType: SearchFieldType.Number - } as SearchFilter); - } + if (filter?.name) { + request.filters!.push({ + key: 'name', + value: filter.name, + matchMode: MatchMode.Equals, + fieldType: SearchFieldType.String, + } as SearchFilter); + } - if (filter?.purpose) { - request.filters!.push({ - key: 'purpose', - value: capitalizeFirstLetter(filter.purpose), - matchMode: MatchMode.Equals, - fieldType: SearchFieldType.String - } as SearchFilter); - } + if (filter?.typeId) { + request.filters!.push({ + key: 'typeId', + value: filter.typeId, + matchMode: MatchMode.Equals, + fieldType: SearchFieldType.Number, + } as SearchFilter); + } - return request as SearchFilters; + if (filter?.purpose) { + request.filters!.push({ + key: 'purpose', + value: capitalizeFirstLetter(filter.purpose), + matchMode: MatchMode.Equals, + fieldType: SearchFieldType.String, + } as SearchFilter); } + + return request as SearchFilters; + } } diff --git a/frontend/datacat-ui/src/processes/explore-data-sources/explore-data-sources.component.html b/frontend/datacat-ui/src/processes/explore-data-sources/explore-data-sources.component.html index f81bff8..4ecddae 100644 --- a/frontend/datacat-ui/src/processes/explore-data-sources/explore-data-sources.component.html +++ b/frontend/datacat-ui/src/processes/explore-data-sources/explore-data-sources.component.html @@ -1,14 +1,12 @@ -
-

Explore, create and edit data sources

-
-
- -
-
- -
-
- -
+
+

Explore, create and edit data sources

+
+
+ +
+
+ +
+
diff --git a/frontend/datacat-ui/src/processes/explore-data-sources/explore-data-sources.component.ts b/frontend/datacat-ui/src/processes/explore-data-sources/explore-data-sources.component.ts index 757d55d..12cc0f7 100644 --- a/frontend/datacat-ui/src/processes/explore-data-sources/explore-data-sources.component.ts +++ b/frontend/datacat-ui/src/processes/explore-data-sources/explore-data-sources.component.ts @@ -1,29 +1,26 @@ -import {Component} from '@angular/core'; -import {Panel} from "primeng/panel"; -import {CreateDataSourceButtonComponent} from "../../features/data-sources/create-data-source-button"; -import { - DataSourcesFilteringComponent -} from "../../features/data-sources/data-sources-filtering/data-sources-filtering.component"; -import {DataSourcesFilter} from "../../entities/data-sources"; -import {DataSourcesListComponent} from "../../features/data-sources/data-sources-list/data-sources-list.component"; +import { Component } from '@angular/core'; +import { Panel } from 'primeng/panel'; +import { CreateDataSourceButtonComponent } from '../../features/data-sources/create-data-source-button'; +import { DataSourcesFilteringComponent } from '../../features/data-sources/data-sources-filtering/data-sources-filtering.component'; +import { DataSourcesFilter } from '../../entities/data-sources'; +import { DataSourcesListComponent } from '../../features/data-sources/data-sources-list/data-sources-list.component'; @Component({ - selector: 'app-explore-data-sources', - standalone: true, - imports: [ - Panel, - CreateDataSourceButtonComponent, - DataSourcesFilteringComponent, - DataSourcesListComponent - ], - templateUrl: './explore-data-sources.component.html', - styleUrl: './explore-data-sources.component.scss' + selector: 'app-explore-data-sources', + standalone: true, + imports: [ + Panel, + CreateDataSourceButtonComponent, + DataSourcesFilteringComponent, + DataSourcesListComponent, + ], + templateUrl: './explore-data-sources.component.html', + styleUrl: './explore-data-sources.component.scss', }) export class ExploreDataSourcesComponent { - filter: DataSourcesFilter | undefined = undefined; + filter: DataSourcesFilter | undefined = undefined; - onFilterChange(filter: DataSourcesFilter) { - this.filter = filter; - console.log('New filter:', filter); - } + onFilterChange(filter: DataSourcesFilter) { + this.filter = filter; + } } diff --git a/frontend/datacat-ui/src/shared/services/app-dialog.service.ts b/frontend/datacat-ui/src/shared/services/app-dialog.service.ts index 6ed29bd..0e47d8b 100644 --- a/frontend/datacat-ui/src/shared/services/app-dialog.service.ts +++ b/frontend/datacat-ui/src/shared/services/app-dialog.service.ts @@ -1,30 +1,28 @@ -import {Injectable, Type} from '@angular/core'; -import {DialogService, DynamicDialogRef} from "primeng/dynamicdialog"; +import { Injectable, Type } from '@angular/core'; +import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AppDialogService { + ref: DynamicDialogRef | undefined; - ref: DynamicDialogRef | undefined; + constructor(public dialogService: DialogService) {} - constructor(public dialogService: DialogService) { - } - - showDialog( - component: Type, - header: string, - data: any, - height: string = '1200px', - width: string = '800px', - ): void { - this.ref = this.dialogService.open(component, { - header: `${header}`, - data: data, - modal: true, - width: width!, - height: height!, - focusOnShow: false, - }); - } + showDialog( + component: Type, + header: string, + data: any, + height: string = '1200px', + width: string = '800px', + ): void { + this.ref = this.dialogService.open(component, { + header: `${header}`, + data: data, + modal: true, + focusOnShow: false, + closable: true, + draggable: true, + }); + } } diff --git a/frontend/datacat-ui/src/shared/services/namespace.service.ts b/frontend/datacat-ui/src/shared/services/namespace.service.ts index 0858e31..6aa7933 100644 --- a/frontend/datacat-ui/src/shared/services/namespace.service.ts +++ b/frontend/datacat-ui/src/shared/services/namespace.service.ts @@ -1,33 +1,42 @@ -import {Injectable, signal} from "@angular/core"; -import {ApiService, GetAvailableNamespaceResponse} from "./datacat-generated-client"; -import {catchError, tap} from "rxjs"; -import {CURRENT_NAMESPACE_KEY, getCurrentNamespaceFromLocalStorage} from "../utils/getCurrentNamespaceFromLocalStorage"; +import { Injectable, signal } from '@angular/core'; +import { + ApiService, + GetAvailableNamespaceResponse, +} from './datacat-generated-client'; +import { catchError, tap } from 'rxjs'; +import { + CURRENT_NAMESPACE_KEY, + getCurrentNamespaceFromLocalStorage, +} from '../utils/getCurrentNamespaceFromLocalStorage'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class NamespaceService { - readonly currentNamespace = signal(getCurrentNamespaceFromLocalStorage()); - readonly namespaces = signal([]); - private readonly STORAGE_KEY = 'CURRENT_NAMESPACE'; + readonly currentNamespace = signal( + getCurrentNamespaceFromLocalStorage(), + ); + readonly namespaces = signal([]); + private readonly STORAGE_KEY = 'CURRENT_NAMESPACE'; - constructor( - private apiService: ApiService, - ) { - this.apiService.getApiV1NamespaceGetAvailable().pipe( - tap(data => { - if (data) { - this.namespaces.set(data); - } - }), - catchError(err => { - return err; - }) - ).subscribe(); - } + constructor(private apiService: ApiService) { + this.apiService + .getApiV1NamespaceGetAvailable() + .pipe( + tap((data) => { + if (data) { + this.namespaces.set(data); + } + }), + catchError((err) => { + return err; + }), + ) + .subscribe(); + } - setCurrentNamespace(ns: GetAvailableNamespaceResponse): void { - localStorage.setItem(CURRENT_NAMESPACE_KEY, JSON.stringify(ns)); - this.currentNamespace.set(ns); - } + setCurrentNamespace(ns: GetAvailableNamespaceResponse): void { + localStorage.setItem(CURRENT_NAMESPACE_KEY, JSON.stringify(ns)); + this.currentNamespace.set(ns); + } } diff --git a/frontend/datacat-ui/src/shared/services/realtime-metrics.service.ts b/frontend/datacat-ui/src/shared/services/realtime-metrics.service.ts index c1a3713..8e7c9de 100644 --- a/frontend/datacat-ui/src/shared/services/realtime-metrics.service.ts +++ b/frontend/datacat-ui/src/shared/services/realtime-metrics.service.ts @@ -1,28 +1,28 @@ -import {Injectable} from '@angular/core'; +import { Injectable } from '@angular/core'; import * as signalR from '@microsoft/signalr'; -import {environment} from "../env/environment"; +import { environment } from '../env/environment'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class RealtimeMetricsService { - private hubConnection: signalR.HubConnection; - private readonly baseUrl = environment.apiUrl; + private hubConnection: signalR.HubConnection; + private readonly baseUrl = environment.apiUrl; - constructor() { - this.hubConnection = new signalR.HubConnectionBuilder() + constructor() { + this.hubConnection = new signalR.HubConnectionBuilder() - .withUrl(`${this.baseUrl}/datacat-metrics`) - .build(); - this.hubConnection.on('ReceiveMessage', (user, message) => { - console.log(`User: ${user}, Message: ${message}`); - }); - this.hubConnection.start() - .catch(err => console.error(err)); - } + .withUrl(`${this.baseUrl}/datacat-metrics`) + .build(); + this.hubConnection.on('ReceiveMessage', (user, message) => { + console.log(`User: ${user}, Message: ${message}`); + }); + this.hubConnection.start().catch((err) => console.error(err)); + } - sendMessage(user: string, message: string): void { - this.hubConnection.invoke('Send', user, message) - .catch(err => console.error(err)); - } + sendMessage(user: string, message: string): void { + this.hubConnection + .invoke('Send', user, message) + .catch((err) => console.error(err)); + } } diff --git a/frontend/datacat-ui/src/shared/services/toast-logger.service.ts b/frontend/datacat-ui/src/shared/services/toast-logger.service.ts index a571715..c0d0916 100644 --- a/frontend/datacat-ui/src/shared/services/toast-logger.service.ts +++ b/frontend/datacat-ui/src/shared/services/toast-logger.service.ts @@ -1,31 +1,45 @@ -import {Injectable} from '@angular/core'; -import {MessageService} from "primeng/api"; +import { Injectable } from '@angular/core'; +import { MessageService } from 'primeng/api'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ToastLoggerService { + constructor(private messageService: MessageService) {} - constructor(private messageService: MessageService) { - } + success(message: any): void { + console.log(message); + this.messageService.add({ + severity: 'success', + summary: 'Success', + detail: message, + }); + } - success(message: any): void { - console.log(message); - this.messageService.add({severity: 'success', summary: 'Success', detail: message}); - } + info(message: any): void { + console.log(message); + this.messageService.add({ + severity: 'info', + summary: 'Info', + detail: message, + }); + } - info(message: any): void { - console.log(message); - this.messageService.add({severity: 'info', summary: 'Info', detail: message}); - } + warn(message: any): void { + console.warn(message); + this.messageService.add({ + severity: 'warn', + summary: 'Warning', + detail: message, + }); + } - warn(message: any): void { - console.warn(message); - this.messageService.add({severity: 'warn', summary: 'Warning', detail: message}); - } - - error(message: any): void { - console.error(message); - this.messageService.add({severity: 'error', summary: 'Error', detail: message}); - } + error(message: any): void { + console.error(message); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: message, + }); + } } diff --git a/frontend/datacat-ui/src/shared/services/user.service.ts b/frontend/datacat-ui/src/shared/services/user.service.ts index 045f0bf..6703b1c 100644 --- a/frontend/datacat-ui/src/shared/services/user.service.ts +++ b/frontend/datacat-ui/src/shared/services/user.service.ts @@ -1,30 +1,30 @@ -import {computed, Injectable, signal} from "@angular/core"; -import {ApiService, GetMeResponse} from "./datacat-generated-client"; -import {NamespaceService} from "./namespace.service"; +import { computed, Injectable, signal } from '@angular/core'; +import { ApiService, GetMeResponse } from './datacat-generated-client'; +import { NamespaceService } from './namespace.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class UserService { - private me = signal(null); + private me = signal(null); - hasPermission = computed(() => { - const current = this.namespaceService.currentNamespace(); - const claims = this.me()?.claims ?? []; - return (permission: string) => { - return claims.some(c => - c.namespaceId === current.id && c.role === permission - ); - }; - }); + hasPermission = computed(() => { + const current = this.namespaceService.currentNamespace(); + const claims = this.me()?.claims ?? []; + return (permission: string) => { + return claims.some( + (c) => c.namespaceId === current.id && c.role === permission, + ); + }; + }); - constructor( - private apiService: ApiService, - private namespaceService: NamespaceService - ) { - this.apiService.getApiV1UserGetMe().subscribe({ - next: user => this.me.set(user), - error: err => console.error(err) - }); - } + constructor( + private apiService: ApiService, + private namespaceService: NamespaceService, + ) { + this.apiService.getApiV1UserGetMe().subscribe({ + next: (user) => this.me.set(user), + error: (err) => console.error(err), + }); + } } From 4aec140274e9ab3cf54708f94816ff3c31b21a92 Mon Sep 17 00:00:00 2001 From: Nikita Nazarov Date: Thu, 29 May 2025 16:55:31 +0300 Subject: [PATCH 3/4] feat(datacat-ui): make fixes to data sources and logs --- .../create-data-source-form.component.html | 2 +- .../data-sources-list.component.ts | 3 +- .../delete-data-source-button.component.html | 51 ++- .../delete-data-source-button.component.ts | 97 +++-- .../edit-data-source-form.component.html | 65 ++- .../edit-data-source-form.component.scss | 29 +- .../edit-data-source-form.component.ts | 392 ++++++++++-------- .../logs-data-source-selector.component.html | 39 +- .../logs-data-source-selector.component.scss | 9 +- .../logs-data-source-selector.component.ts | 174 ++++---- .../logs-details/logs-details.component.html | 21 +- .../logs-details/logs-details.component.scss | 21 +- .../logs-details/logs-details.component.ts | 41 +- .../logs-filter/logs-filter.component.html | 174 +++++--- .../logs-filter/logs-filter.component.scss | 22 +- .../logs/logs-filter/logs-filter.component.ts | 113 ++--- .../logs/logs-list/logs-list.component.html | 137 +++--- .../logs/logs-list/logs-list.component.scss | 29 +- .../logs/logs-list/logs-list.component.ts | 130 +++--- .../explore-logs/explore-logs.component.html | 52 +-- .../explore-logs/explore-logs.component.scss | 1 - .../explore-logs/explore-logs.component.ts | 193 +++++---- .../manage-data-sources.component.html | 57 +-- .../manage-data-sources.component.scss | 6 + .../manage-data-sources.component.ts | 131 +++--- 25 files changed, 1046 insertions(+), 943 deletions(-) diff --git a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.html b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.html index b8b5ef1..c482a8e 100644 --- a/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.html +++ b/frontend/datacat-ui/src/features/data-sources/create-data-source-form/create-data-source-form.component.html @@ -63,7 +63,7 @@ (onClick)="saveDataSource()" [disabled]="form.invalid" class="form__submit" - label="Save" + label="Create" />
diff --git a/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.ts b/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.ts index 6ea2a29..89acd3f 100644 --- a/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.ts +++ b/frontend/datacat-ui/src/features/data-sources/data-sources-list/data-sources-list.component.ts @@ -13,7 +13,6 @@ import { import { LazyLoadEvent } from 'primeng/api'; import { TableModule } from 'primeng/table'; import { Button } from 'primeng/button'; -import { Tooltip } from 'primeng/tooltip'; import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; import { Tag } from 'primeng/tag'; import { capitalizeFirstLetter } from '../../../shared/utils/capitalizeFirstLetter'; @@ -22,7 +21,7 @@ import * as urls from '../../../shared/common/urls'; @Component({ selector: 'app-data-sources-list', standalone: true, - imports: [TableModule, Button, Tooltip, Tag], + imports: [TableModule, Button, Tag], templateUrl: './data-sources-list.component.html', styleUrl: './data-sources-list.component.scss', }) diff --git a/frontend/datacat-ui/src/features/data-sources/delete-data-source-button/delete-data-source-button.component.html b/frontend/datacat-ui/src/features/data-sources/delete-data-source-button/delete-data-source-button.component.html index 5687d48..69b5e70 100644 --- a/frontend/datacat-ui/src/features/data-sources/delete-data-source-button/delete-data-source-button.component.html +++ b/frontend/datacat-ui/src/features/data-sources/delete-data-source-button/delete-data-source-button.component.html @@ -1,25 +1,32 @@ - + + -

Are you sure you want to delete this data source?

- @if (isDeletionError) { -

Unable to delete data source

- } -
- - -
+

Are you sure you want to delete this data source?

+ @if (isDeletionError) { +

Unable to delete data source

+ } +
+ + +
diff --git a/frontend/datacat-ui/src/features/data-sources/delete-data-source-button/delete-data-source-button.component.ts b/frontend/datacat-ui/src/features/data-sources/delete-data-source-button/delete-data-source-button.component.ts index 9f308ea..b132267 100644 --- a/frontend/datacat-ui/src/features/data-sources/delete-data-source-button/delete-data-source-button.component.ts +++ b/frontend/datacat-ui/src/features/data-sources/delete-data-source-button/delete-data-source-button.component.ts @@ -1,60 +1,59 @@ -import {Component, Input} from '@angular/core'; -import {Button} from "primeng/button"; -import {Dialog} from "primeng/dialog"; -import {ApiService} from "../../../shared/services/datacat-generated-client"; -import {Router} from "@angular/router"; -import {catchError, tap} from "rxjs"; -import * as urls from "../../../shared/common/urls"; -import {ToastLoggerService} from "../../../shared/services/toast-logger.service"; +import { Component, Input } from '@angular/core'; +import { Button } from 'primeng/button'; +import { Dialog } from 'primeng/dialog'; +import { ApiService } from '../../../shared/services/datacat-generated-client'; +import { Router } from '@angular/router'; +import { catchError, tap } from 'rxjs'; +import * as urls from '../../../shared/common/urls'; +import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; @Component({ - selector: 'app-delete-data-source-button', - standalone: true, - imports: [ - Button, - Dialog - ], - templateUrl: './delete-data-source-button.component.html', - styleUrl: './delete-data-source-button.component.scss' + selector: 'app-delete-data-source-button', + standalone: true, + imports: [Button, Dialog], + templateUrl: './delete-data-source-button.component.html', + styleUrl: './delete-data-source-button.component.scss', }) export class DeleteDataSourceButtonComponent { - @Input({required: true}) dataSourceId?: string; + @Input({ required: true }) dataSourceId?: string; - protected isDeletionInitiated = false; - protected isDeletionDialogVisible = false; - protected isDeletionError = false; + protected isDeletionInitiated = false; + protected isDeletionDialogVisible = false; + protected isDeletionError = false; - constructor( - private apiService: ApiService, - private router: Router, - private toastLoggerService: ToastLoggerService, - ) { - } + constructor( + private apiService: ApiService, + private router: Router, + private toastLoggerService: ToastLoggerService, + ) {} - protected showDeletionDialog() { - this.isDeletionError = false; - this.isDeletionDialogVisible = true; - } + protected showDeletionDialog() { + this.isDeletionError = false; + this.isDeletionDialogVisible = true; + } - protected hideDeletionDialog() { - this.isDeletionDialogVisible = false; - } + protected hideDeletionDialog() { + this.isDeletionDialogVisible = false; + } - protected deleteNotificationGroup() { - this.isDeletionError = false; - this.isDeletionInitiated = true; - if (this.dataSourceId) { - this.apiService.deleteApiV1DataSourceRemove(this.dataSourceId).pipe( - tap(_ => { - this.toastLoggerService.success('Data Source successfully deleted'); - this.router.navigateByUrl(urls.DATA_SOURCES_EXPLORER_URL); - }), - catchError(err => { - this.isDeletionInitiated = false; - this.isDeletionError = true; - return err; - }) - ).subscribe() - } + protected deleteNotificationGroup() { + this.isDeletionError = false; + this.isDeletionInitiated = true; + if (this.dataSourceId) { + this.apiService + .deleteApiV1DataSourceRemove(this.dataSourceId) + .pipe( + tap((_) => { + this.toastLoggerService.success('Data Source successfully deleted'); + this.router.navigateByUrl(urls.DATA_SOURCES_EXPLORER_URL); + }), + catchError((err) => { + this.isDeletionInitiated = false; + this.isDeletionError = true; + return err; + }), + ) + .subscribe(); } + } } diff --git a/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.html b/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.html index 8e646aa..e378fa6 100644 --- a/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.html +++ b/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.html @@ -1,38 +1,35 @@
-
-

Name

- -
+
+

Name

+ +
-
- @switch (editDataSource.type) { - @case ('prometheus') { - - } - @case ('jaeger') { - - } - @case ('elasticsearch') { - - } - } -
- -
- + @switch (editDataSource.type) { + @case ('prometheus') { + + } + @case ('jaeger') { + -
+ } + @case ('elasticsearch') { + + } + } +
+ +
+ +
diff --git a/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.scss b/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.scss index 1aac85f..e575d71 100644 --- a/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.scss +++ b/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.scss @@ -1,37 +1,16 @@ .form { - padding: 24px; - border-radius: 12px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; - gap: 24px; + gap: var(--p-padding-md); + align-items: start; &__item { display: flex; flex-direction: column; - gap: 8px; + gap: var(--p-padding-sm); } &__label { - font-size: 14px; - color: #6b7280; - font-weight: 500; - margin: 0; - } - - &__input { - width: 100%; - } - - &__settings { - padding-top: 16px; - } - - &__actions { - padding-top: 24px; - } - - &__submit { - width: 100%; + color: var(--p-surface-400); } } diff --git a/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.ts b/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.ts index d1bb534..93fc8cd 100644 --- a/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.ts +++ b/frontend/datacat-ui/src/features/data-sources/edit-data-source-form/edit-data-source-form.component.ts @@ -1,177 +1,237 @@ -import {Component, Input} from '@angular/core'; -import {Button} from "primeng/button"; +import { Component, Input } from '@angular/core'; +import { Button } from 'primeng/button'; +import { ElasticsearchSettingsFormComponent } from '../create-data-source-form/elasticsearch-settings-form/elasticsearch-settings-form.component'; +import { InputText } from 'primeng/inputtext'; +import { JaegerSettingsFormComponent } from '../create-data-source-form/jaeger-settings-form/jaeger-settings-form.component'; +import { PrometheusSettingsFormComponent } from '../create-data-source-form/prometheus-settings-form/prometheus-settings-form.component'; import { - ElasticsearchSettingsFormComponent -} from "../create-data-source-form/elasticsearch-settings-form/elasticsearch-settings-form.component"; -import {InputText} from "primeng/inputtext"; + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; import { - JaegerSettingsFormComponent -} from "../create-data-source-form/jaeger-settings-form/jaeger-settings-form.component"; -import { - PrometheusSettingsFormComponent -} from "../create-data-source-form/prometheus-settings-form/prometheus-settings-form.component"; -import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; -import {ApiService, IUpdateDataSourceRequest} from "../../../shared/services/datacat-generated-client"; -import {ToastLoggerService} from "../../../shared/services/toast-logger.service"; -import {catchError, finalize, tap} from "rxjs"; -import {EditDataSource} from "../../../entities/data-sources/edit-data-source"; + ApiService, + IUpdateDataSourceRequest, +} from '../../../shared/services/datacat-generated-client'; +import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; +import { catchError, finalize, tap } from 'rxjs'; +import { EditDataSource } from '../../../entities/data-sources/edit-data-source'; @Component({ - selector: 'app-edit-data-source-form', - standalone: true, - imports: [ - Button, - ElasticsearchSettingsFormComponent, - InputText, - JaegerSettingsFormComponent, - PrometheusSettingsFormComponent, - ReactiveFormsModule - ], - templateUrl: './edit-data-source-form.component.html', - styleUrl: './edit-data-source-form.component.scss' + selector: 'app-edit-data-source-form', + standalone: true, + imports: [ + Button, + ElasticsearchSettingsFormComponent, + InputText, + JaegerSettingsFormComponent, + PrometheusSettingsFormComponent, + ReactiveFormsModule, + ], + templateUrl: './edit-data-source-form.component.html', + styleUrl: './edit-data-source-form.component.scss', }) export class EditDataSourceFormComponent { - @Input({required: true}) editDataSource: EditDataSource = null!; - - isBusy = false; - - form!: FormGroup; - protected readonly FormGroup = FormGroup; - - constructor( - private fb: FormBuilder, - private apiService: ApiService, - private toastLoggerService: ToastLoggerService - ) { - } - - get settingsForm(): FormGroup { - return this.form.get('settings') as FormGroup; - } - - ngOnInit() { - this.isBusy = true; - console.log(this.editDataSource); - - this.form = this.fb.group({ - name: [this.editDataSource?.name, Validators.required], - settings: this.fb.group({}) - }); - - this.form.get('name')?.disable(); - - this.updateSettingsForm(this.editDataSource.type!); + @Input({ required: true }) editDataSource: EditDataSource = null!; + + isBusy = false; + + form!: FormGroup; + protected readonly FormGroup = FormGroup; + + constructor( + private fb: FormBuilder, + private apiService: ApiService, + private toastLoggerService: ToastLoggerService, + ) {} + + get settingsForm(): FormGroup { + return this.form.get('settings') as FormGroup; + } + + ngOnInit() { + this.isBusy = true; + console.log(this.editDataSource); + + this.form = this.fb.group({ + name: [this.editDataSource?.name, Validators.required], + settings: this.fb.group({}), + }); + + this.form.get('name')?.disable(); + + this.updateSettingsForm(this.editDataSource.type!); + } + + saveDataSource() { + if (this.form.invalid) return; + + const formValue = this.form.value; + const connectionSettings = this.getConnectionSettings(formValue); + + const request: any = { + connectionString: JSON.stringify(connectionSettings), + } as IUpdateDataSourceRequest; + + this.apiService + .putApiV1DataSourceUpdateConnectionString(this.editDataSource.id, request) + .pipe( + finalize(() => (this.isBusy = false)), + tap((_) => { + this.toastLoggerService.success('Data Source updated successfully'); + }), + catchError((err) => { + this.toastLoggerService.error(err); + return err; + }), + ) + .subscribe(); + } + + private updateSettingsForm(type: string) { + Object.keys(this.settingsForm.controls).forEach((key) => { + this.settingsForm.removeControl(key); + }); + + let parsedSettings: Record = {}; + try { + parsedSettings = this.editDataSource?.connectionString + ? JSON.parse(this.editDataSource.connectionString) + : {}; + } catch (e) { + console.warn('Failed to parse connection string:', e); } - saveDataSource() { - if (this.form.invalid) return; - - const formValue = this.form.value; - const connectionSettings = this.getConnectionSettings(formValue); - - const request: any = { - connectionString: JSON.stringify(connectionSettings) - } as IUpdateDataSourceRequest; - - this.apiService.putApiV1DataSourceUpdateConnectionString(this.editDataSource.id, request).pipe( - finalize(() => this.isBusy = false), - tap(_ => { - this.toastLoggerService.info('Data Source updated successfully'); - }), - catchError((err) => { - this.toastLoggerService.error(err); - return err; - }) - ).subscribe() + const regexp_no_domain_validation = + /^(https?:\/\/)?((([a-z0-9-]+\.)+([a-z]{2,}))|((\d{1,3}\.){3}\d{1,3}))(:\d{1,5})?$/i; + const regexp_with_domain_validation = + /^(https?:\/\/)?((((?!-)([a-z0-9-]{1,63})(? { - this.settingsForm.removeControl(key); - }); - - let parsedSettings: Record = {}; - try { - parsedSettings = this.editDataSource?.connectionString - ? JSON.parse(this.editDataSource.connectionString) - : {}; - } catch (e) { - console.warn('Failed to parse connection string:', e); - } - - const regexp_no_domain_validation = /^(https?:\/\/)?((([a-z0-9-]+\.)+([a-z]{2,}))|((\d{1,3}\.){3}\d{1,3}))(:\d{1,5})?$/i; - const regexp_with_domain_validation = /^(https?:\/\/)?((((?!-)([a-z0-9-]{1,63})(? - - - @if (loading) { - -
- -
-
- } -
+ + + @if (loading) { + +
+ +
+
+ } +
diff --git a/frontend/datacat-ui/src/features/logs/logs-data-source-selector/logs-data-source-selector.component.scss b/frontend/datacat-ui/src/features/logs/logs-data-source-selector/logs-data-source-selector.component.scss index 79a9d66..19f4b27 100644 --- a/frontend/datacat-ui/src/features/logs/logs-data-source-selector/logs-data-source-selector.component.scss +++ b/frontend/datacat-ui/src/features/logs/logs-data-source-selector/logs-data-source-selector.component.scss @@ -1,11 +1,12 @@ .data-source-selector { - margin-bottom: 1.5rem; + display: flex; + flex-direction: column; + gap: var(--p-padding-sm); + justify-content: start; label { display: block; - margin-bottom: 0.5rem; - font-weight: 600; - color: var(--text-color); + color: var(--p-surface-400); } p-dropdown { diff --git a/frontend/datacat-ui/src/features/logs/logs-data-source-selector/logs-data-source-selector.component.ts b/frontend/datacat-ui/src/features/logs/logs-data-source-selector/logs-data-source-selector.component.ts index e41713a..831510b 100644 --- a/frontend/datacat-ui/src/features/logs/logs-data-source-selector/logs-data-source-selector.component.ts +++ b/frontend/datacat-ui/src/features/logs/logs-data-source-selector/logs-data-source-selector.component.ts @@ -1,102 +1,108 @@ -import {Component, EventEmitter, Input, Output} from '@angular/core'; -import {debounceTime, distinctUntilChanged, Subject, switchMap, tap} from "rxjs"; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { - ApiService, - MatchMode, - SearchDataSourcesResponse, - SearchFieldType, - SearchFilter, - SearchFilters -} from "../../../shared/services/datacat-generated-client"; -import {DropdownModule} from "primeng/dropdown"; -import {FormsModule} from "@angular/forms"; -import {ProgressSpinner} from "primeng/progressspinner"; + debounceTime, + distinctUntilChanged, + Subject, + switchMap, + tap, +} from 'rxjs'; +import { + ApiService, + MatchMode, + SearchDataSourcesResponse, + SearchFieldType, + SearchFilter, + SearchFilters, +} from '../../../shared/services/datacat-generated-client'; +import { DropdownModule } from 'primeng/dropdown'; +import { FormsModule } from '@angular/forms'; +import { ProgressSpinner } from 'primeng/progressspinner'; @Component({ - selector: 'app-logs-data-source-selector', - standalone: true, - imports: [ - DropdownModule, - FormsModule, - ProgressSpinner - ], - templateUrl: './logs-data-source-selector.component.html', - styleUrl: './logs-data-source-selector.component.scss' + selector: 'app-logs-data-source-selector', + standalone: true, + imports: [DropdownModule, FormsModule, ProgressSpinner], + templateUrl: './logs-data-source-selector.component.html', + styleUrl: './logs-data-source-selector.component.scss', }) export class LogsDataSourceSelectorComponent { - @Input() selectedDataSource: string | undefined | null = null; - @Output() selectedDataSourceChange = new EventEmitter(); - - dataSources: SearchDataSourcesResponse[] = []; - searchTerm$ = new Subject(); - loading = false; - firstLoad = true; + @Input() selectedDataSource: string | undefined | null = null; + @Output() selectedDataSourceChange = new EventEmitter(); - constructor( - private apiService: ApiService - ) { - } + dataSources: SearchDataSourcesResponse[] = []; + searchTerm$ = new Subject(); + loading = false; + firstLoad = true; - ngOnInit() { - this.searchTerm$.pipe( - debounceTime(300), - distinctUntilChanged(), - switchMap(term => this.searchDataSources(term)) - ).subscribe(data => { - this.dataSources = data.items!; - this.loading = false; - }); + constructor(private apiService: ApiService) {} - this.searchDataSources('').pipe( - tap(data => { - this.firstLoad = false; + ngOnInit() { + this.searchTerm$ + .pipe( + debounceTime(300), + distinctUntilChanged(), + switchMap((term) => this.searchDataSources(term)), + ) + .subscribe((data) => { + this.dataSources = data.items!; + this.loading = false; + }); - if (!data.items) { - return; - } + this.searchDataSources('') + .pipe( + tap((data) => { + this.firstLoad = false; - this.dataSources = data.items; - if (this.firstLoad && data.items.length > 0 && !this.selectedDataSource) { - this.selectedDataSource = data.items[0].name!; - this.selectedDataSourceChange.emit(this.selectedDataSource); - } + if (!data.items) { + return; + } - }) - ).subscribe(); - } + this.dataSources = data.items; + if ( + this.firstLoad && + data.items.length > 0 && + !this.selectedDataSource + ) { + this.selectedDataSource = data.items[0].name!; + this.selectedDataSourceChange.emit(this.selectedDataSource); + } + }), + ) + .subscribe(); + } - onSearch(event: { filter: string }) { - this.loading = true; - this.searchTerm$.next(event.filter); - } + onSearch(event: { filter: string }) { + this.loading = true; + this.searchTerm$.next(event.filter); + } - onDataSourceChange() { - this.selectedDataSourceChange.emit(this.selectedDataSource ?? ''); - } + onDataSourceChange() { + this.selectedDataSourceChange.emit(this.selectedDataSource ?? ''); + } - private searchDataSources(searchTerm: string) { - const filters: SearchFilter[] = [ - ({ - key: 'purpose', - value: 'Logs', - matchMode: MatchMode.Equals, - fieldType: SearchFieldType.String - } as SearchFilter) - ]; + private searchDataSources(searchTerm: string) { + const filters: SearchFilter[] = [ + { + key: 'purpose', + value: 'Logs', + matchMode: MatchMode.Equals, + fieldType: SearchFieldType.String, + } as SearchFilter, + ]; - if (searchTerm?.trim()) { - filters.unshift({ - key: 'name', - value: searchTerm, - matchMode: MatchMode.Contains, - fieldType: SearchFieldType.String - } as SearchFilter); - } + if (searchTerm?.trim()) { + filters.unshift({ + key: 'name', + value: searchTerm, + matchMode: MatchMode.Contains, + fieldType: SearchFieldType.String, + } as SearchFilter); + } - const request = { - filters: filters, - } as SearchFilters + const request = { + filters: filters, + } as SearchFilters; - return this.apiService.postApiV1DataSourceSearch(request, 1, 10); - } + return this.apiService.postApiV1DataSourceSearch(request, 1, 10); + } } diff --git a/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.html b/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.html index d34493d..ce80798 100644 --- a/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.html +++ b/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.html @@ -1,15 +1,14 @@
-
- No additional fields available -
+
+ No additional fields available +
-
-
- - {{ item }} -
-
- {{ data[item] | json }} -
+
+
+ {{ item }} +
+
+ {{ data[item] | json }}
+
diff --git a/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.scss b/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.scss index 401bf30..a6dbc6b 100644 --- a/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.scss +++ b/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.scss @@ -1,5 +1,4 @@ .details-content { - padding: 1.5rem; background-color: var(--surface-ground); .no-data { @@ -12,9 +11,8 @@ .detail-row { display: grid; grid-template-columns: 250px 1fr; - align-items: start; + align-items: center; gap: 1rem; - padding: 0.75rem 1rem; border-bottom: 1px solid var(--surface-border); position: relative; overflow: hidden; @@ -28,27 +26,20 @@ width: 150%; height: 100%; background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.05), - transparent + 90deg, + transparent, + rgba(255, 255, 255, 0.05), + transparent ); animation: shine 1s forwards; } .field-name { font-weight: 600; - color: var(--text-color); + color: var(--p-surface-400); display: flex; align-items: center; gap: 0.5rem; - - .dot { - width: 8px; - height: 8px; - background-color: var(--primary-color); - border-radius: 50%; - } } .field-value { diff --git a/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.ts b/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.ts index 05201bc..5e16a87 100644 --- a/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.ts +++ b/frontend/datacat-ui/src/features/logs/logs-details/logs-details.component.ts @@ -1,32 +1,23 @@ -import {Component} from '@angular/core'; -import {DynamicDialogConfig} from "primeng/dynamicdialog"; -import {Dialog} from "primeng/dialog"; -import {JsonPipe, NgForOf, NgIf} from "@angular/common"; +import { Component } from '@angular/core'; +import { DynamicDialogConfig } from 'primeng/dynamicdialog'; +import { JsonPipe, NgForOf, NgIf } from '@angular/common'; @Component({ - selector: 'app-logs-details', - standalone: true, - imports: [ - Dialog, - JsonPipe, - NgForOf, - NgIf - ], - templateUrl: './logs-details.component.html', - styleUrl: './logs-details.component.scss' + selector: 'app-logs-details', + standalone: true, + imports: [JsonPipe, NgForOf, NgIf], + templateUrl: './logs-details.component.html', + styleUrl: './logs-details.component.scss', }) export class LogsDetailsComponent { - data: any; - protected readonly Object = Object; + data: any; + protected readonly Object = Object; - constructor( - public config: DynamicDialogConfig - ) { - this.data = config.data; - console.log(this.data); - } + constructor(public config: DynamicDialogConfig) { + this.data = config.data; + } - get objectKeys() { - return Object.keys; - } + get objectKeys() { + return Object.keys; + } } diff --git a/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.html b/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.html index aa7f8e1..807ded0 100644 --- a/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.html +++ b/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.html @@ -1,74 +1,122 @@
-
-
- - -
- -
- - -
+
+
+ + +
-
- - -
+
+ +
-
- +
+ +
+
- @if (showCustomFilters) { -
-
- - - -
+
+ +
-
- - - -
-
+ @if (showCustomFilters) { +
+
+ + + +
-
-

Custom Filters

-
- - - -
- -
- } +
+ + + +
+
-
- +
+

Custom Filters

+
+ + + +
+
+ } + +
diff --git a/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.scss b/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.scss index b62a1c2..2d2ffd4 100644 --- a/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.scss +++ b/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.scss @@ -1,21 +1,18 @@ .filter-container { - padding: 1rem; - border-radius: 8px; - margin-bottom: 1rem; - .filter-row { display: flex; flex-wrap: wrap; - gap: 1rem; - margin-bottom: 1rem; + gap: var(--p-padding-md); .p-field { flex: 1 1 200px; + display: flex; + flex-direction: column; + gap: var(--p-padding-sm); label { display: block; - margin-bottom: 0.25rem; - font-weight: 600; + color: var(--p-surface-400); } } } @@ -60,11 +57,6 @@ } } - .filter-actions { - margin-top: 1rem; - text-align: right; - } - .uniform-height { height: 2.5rem; @@ -86,7 +78,9 @@ } } - input, p-dropdown, p-calendar { + input, + p-dropdown, + p-calendar { width: 100%; } } diff --git a/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.ts b/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.ts index 5cbe012..a649ec2 100644 --- a/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.ts +++ b/frontend/datacat-ui/src/features/logs/logs-filter/logs-filter.component.ts @@ -1,66 +1,73 @@ -import {Component, EventEmitter, Input, Output} from '@angular/core'; -import {ISearchLogsRequest} from "../../../shared/services/datacat-generated-client"; -import {InputText} from "primeng/inputtext"; -import {DropdownModule} from "primeng/dropdown"; -import {Calendar} from "primeng/calendar"; -import {FormsModule} from "@angular/forms"; -import {NgForOf} from "@angular/common"; -import {ButtonDirective} from "primeng/button"; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ISearchLogsRequest } from '../../../shared/services/datacat-generated-client'; +import { InputText } from 'primeng/inputtext'; +import { DropdownModule } from 'primeng/dropdown'; +import { Calendar } from 'primeng/calendar'; +import { FormsModule } from '@angular/forms'; +import { NgForOf } from '@angular/common'; +import { ButtonDirective, ButtonModule } from 'primeng/button'; @Component({ - selector: 'app-logs-filter', - standalone: true, - imports: [ - InputText, - DropdownModule, - Calendar, - FormsModule, - NgForOf, - ButtonDirective - ], - templateUrl: './logs-filter.component.html', - styleUrl: './logs-filter.component.scss' + selector: 'app-logs-filter', + standalone: true, + imports: [ + InputText, + DropdownModule, + Calendar, + FormsModule, + NgForOf, + ButtonDirective, + ButtonModule, + ], + templateUrl: './logs-filter.component.html', + styleUrl: './logs-filter.component.scss', }) export class LogsFilterComponent { - @Input() initialFilter!: ISearchLogsRequest; - @Output() filterChange = new EventEmitter(); + @Input() initialFilter!: ISearchLogsRequest; + @Output() filterChange = new EventEmitter(); - filter: ISearchLogsRequest; - customFilters: { key: string; value: string }[] = []; - severityLevels = [ - {label: 'Error', value: 'Error'}, - {label: 'Warning', value: 'Warning'}, - {label: 'Info', value: 'Information'}, - {label: 'Debug', value: 'Debug'} - ]; + filter: ISearchLogsRequest; + customFilters: { key: string; value: string }[] = []; + severityLevels = [ + { label: 'Error', value: 'Error' }, + { label: 'Warning', value: 'Warning' }, + { label: 'Info', value: 'Information' }, + { label: 'Debug', value: 'Debug' }, + ]; - showCustomFilters = false; + showCustomFilters = false; - constructor() { - this.filter = {...this.initialFilter}; - if (this.filter.customFilters) { - this.customFilters = Object.entries(this.filter.customFilters) - .map(([key, value]) => ({key, value})); - } + constructor() { + this.filter = { ...this.initialFilter }; + if (this.filter.customFilters) { + this.customFilters = Object.entries(this.filter.customFilters).map( + ([key, value]) => ({ key, value }), + ); } + } - addCustomFilter() { - this.customFilters.push({key: '', value: ''}); - } + addCustomFilter() { + this.customFilters.push({ key: '', value: '' }); + } - removeCustomFilter(index: number) { - this.customFilters.splice(index, 1); - } + removeCustomFilter(index: number) { + this.customFilters.splice(index, 1); + } - applyFilters() { - const customFilters = this.customFilters.reduce((acc, curr) => { - if (curr.key && curr.value) acc[curr.key] = curr.value; - return acc; - }, {} as Record); + applyFilters() { + const customFilters = this.customFilters.reduce( + (acc, curr) => { + if (curr.key && curr.value) acc[curr.key] = curr.value; + return acc; + }, + {} as Record, + ); - this.filterChange.emit({ - ...this.filter, - customFilters: Object.keys(customFilters).length ? customFilters : undefined - }); - } + this.filterChange.emit({ + ...this.filter, + customFilters: Object.keys(customFilters).length + ? customFilters + : undefined, + }); + } } diff --git a/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.html b/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.html index 0aeada0..42d1d30 100644 --- a/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.html +++ b/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.html @@ -1,56 +1,85 @@
-
- - - - - Timestamp - - - - Severity - - - - Service - - - Message - Trace ID - Details - - - - - {{ log.timestamp | date : 'medium' }} - - - - {{ log.serviceName }} - {{ sanitizeMessage(log.message) }} - {{ log.traceId }} - - - - - - -
+
+ + + + + Timestamp + + + + Severity + + + + Service + + + Message + Trace ID + Details + + + + + + {{ log.timestamp | date: 'short' }} + + + + + {{ log.serviceName }} + + {{ sanitizeMessage(log.message) | slice: 0 : 30 + }}... + + + {{ log.traceId | slice: 0 : 6 + }}... + + + + + + + +
diff --git a/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.scss b/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.scss index 625aff6..fcb9dd7 100644 --- a/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.scss +++ b/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.scss @@ -1,9 +1,6 @@ .logs-table-container { - max-height: 600px; // ограничение по высоте - overflow: auto; // включение скролла при переполнении - padding: 1rem; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + max-height: 600px; + overflow: auto; .table-scroll { min-width: 100%; @@ -11,9 +8,6 @@ } .p-datatable { - min-width: 1000px; // ширина для активации горизонтального скролла - width: max-content; - th { cursor: pointer; user-select: none; @@ -29,21 +23,8 @@ max-width: 400px; } } +} - .pagination-controls { - display: flex; - justify-content: center; - align-items: center; - margin-top: 1rem; - gap: 1rem; - - .page-info { - font-weight: 500; - } - - button { - min-width: 2rem; - height: 2rem; - } - } +.atomic { + white-space: nowrap; } diff --git a/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.ts b/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.ts index 43d1825..c2bbfc8 100644 --- a/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.ts +++ b/frontend/datacat-ui/src/features/logs/logs-list/logs-list.component.ts @@ -1,74 +1,82 @@ -import {Component, EventEmitter, Input, Output} from '@angular/core'; -import {LogEntry} from "../../../shared/services/datacat-generated-client"; -import {TableModule} from "primeng/table"; -import {DatePipe, NgIf} from "@angular/common"; -import {Tag} from "primeng/tag"; -import {Button} from "primeng/button"; -import {AppDialogService} from "../../../shared/services/app-dialog.service"; -import {LogsDetailsComponent} from "../logs-details/logs-details.component"; -import {sanitizeMessage} from "../../../shared/utils/sanitizeMessage"; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { LogEntry } from '../../../shared/services/datacat-generated-client'; +import { TableModule } from 'primeng/table'; +import { CommonModule, DatePipe, NgIf } from '@angular/common'; +import { Tag } from 'primeng/tag'; +import { Button } from 'primeng/button'; +import { AppDialogService } from '../../../shared/services/app-dialog.service'; +import { LogsDetailsComponent } from '../logs-details/logs-details.component'; +import { sanitizeMessage } from '../../../shared/utils/sanitizeMessage'; +import { TooltipModule } from 'primeng/tooltip'; @Component({ - selector: 'app-logs-list', - standalone: true, - imports: [ - TableModule, - DatePipe, - Tag, - NgIf, - Button - ], - templateUrl: './logs-list.component.html', - styleUrl: './logs-list.component.scss' + selector: 'app-logs-list', + standalone: true, + imports: [ + TableModule, + DatePipe, + Tag, + NgIf, + Button, + CommonModule, + TooltipModule, + ], + templateUrl: './logs-list.component.html', + styleUrl: './logs-list.component.scss', }) export class LogsListComponent { - @Input() logs: LogEntry[] = []; - @Input() pagination: any; - @Output() pageChange = new EventEmitter(); - @Output() sortChange = new EventEmitter<{ field: string; ascending: boolean }>(); + @Input() loading: boolean = false; + @Input() logs: LogEntry[] = []; + @Input() pagination: any; + @Output() pageChange = new EventEmitter(); + @Output() sortChange = new EventEmitter<{ + field: string; + ascending: boolean; + }>(); - currentSortField = ''; - sortAscending = false; - protected readonly sanitizeMessage = sanitizeMessage; + currentSortField = ''; + sortAscending = false; + protected readonly sanitizeMessage = sanitizeMessage; - constructor( - private appDialogService: AppDialogService, - ) { - } + constructor(private appDialogService: AppDialogService) {} - onPageChange(event: any) { - const newPage = (event.first / event.rows) + 1; - this.pageChange.emit(newPage); - } + onPageChange(event: any) { + const newPage = event.first / event.rows + 1; + this.pageChange.emit(newPage); + } - // Остальные методы без изменений - onSort(field: string) { - if (this.currentSortField === field) { - this.sortAscending = !this.sortAscending; - } else { - this.currentSortField = field; - this.sortAscending = true; - } - this.sortChange.emit({field: this.currentSortField, ascending: this.sortAscending}); + // Остальные методы без изменений + onSort(field: string) { + if (this.currentSortField === field) { + this.sortAscending = !this.sortAscending; + } else { + this.currentSortField = field; + this.sortAscending = true; } + this.sortChange.emit({ + field: this.currentSortField, + ascending: this.sortAscending, + }); + } - getSeverityClass(severity: string): 'success' | 'info' | 'warn' | 'danger' { - switch (severity?.toLowerCase()) { - case 'error': - return 'danger'; - case 'warning': - return 'warn'; - case 'info': - return 'info'; - default: - return 'success'; - } + getSeverityClass(severity: string): 'success' | 'info' | 'warn' | 'danger' { + switch (severity?.toLowerCase()) { + case 'error': + return 'danger'; + case 'warning': + return 'warn'; + case 'info': + return 'info'; + default: + return 'success'; } + } - showDetails(additionalFields: any) { - this.appDialogService.showDialog(LogsDetailsComponent, - 'Additional Fields', - additionalFields || {} - ) - } + showDetails(additionalFields: any) { + this.appDialogService.showDialog( + LogsDetailsComponent, + 'Additional Fields', + additionalFields || {}, + ); + } } diff --git a/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.html b/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.html index 48d562b..3b3fa96 100644 --- a/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.html +++ b/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.html @@ -1,34 +1,26 @@ -
-

Explore and analyze system logs in real-time

-
+
+

Explore and analyze system logs in real-time

+
-
- - -
+
+ +
-
- - -
-
- @if (isLoading) { -
- -

Loading logs...

-
- } @else { - - - } -
+
+ +
+
diff --git a/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.scss b/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.scss index 0e96cdc..c57fa80 100644 --- a/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.scss +++ b/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.scss @@ -5,7 +5,6 @@ .slot { margin: var(--p-padding-md); - position: relative; } .description-container { diff --git a/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.ts b/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.ts index 312942b..90b998c 100644 --- a/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.ts +++ b/frontend/datacat-ui/src/processes/explore-logs/explore-logs.component.ts @@ -1,114 +1,113 @@ -import {Component} from '@angular/core'; -import {Panel} from "primeng/panel"; +import { Component } from '@angular/core'; +import { Panel } from 'primeng/panel'; import { - ApiService, - ISearchLogsRequest, - LogEntry, - SearchLogsRequest -} from "../../shared/services/datacat-generated-client"; -import {LogsFilterComponent} from "../../features/logs/logs-filter/logs-filter.component"; -import {LogsListComponent} from "../../features/logs/logs-list/logs-list.component"; -import {DropdownModule} from "primeng/dropdown"; -import {FormsModule} from "@angular/forms"; -import {ToastLoggerService} from "../../shared/services/toast-logger.service"; -import { - LogsDataSourceSelectorComponent -} from "../../features/logs/logs-data-source-selector/logs-data-source-selector.component"; -import {catchError, finalize, tap} from "rxjs"; -import {ProgressSpinner} from "primeng/progressspinner"; -import {formatCustomProblemDetails} from "../../shared/utils/formatCustomProblemDetails"; + ApiService, + ISearchLogsRequest, + LogEntry, + SearchLogsRequest, +} from '../../shared/services/datacat-generated-client'; +import { LogsFilterComponent } from '../../features/logs/logs-filter/logs-filter.component'; +import { LogsListComponent } from '../../features/logs/logs-list/logs-list.component'; +import { DropdownModule } from 'primeng/dropdown'; +import { FormsModule } from '@angular/forms'; +import { ToastLoggerService } from '../../shared/services/toast-logger.service'; +import { LogsDataSourceSelectorComponent } from '../../features/logs/logs-data-source-selector/logs-data-source-selector.component'; +import { catchError, finalize, tap } from 'rxjs'; +import { formatCustomProblemDetails } from '../../shared/utils/formatCustomProblemDetails'; @Component({ - selector: 'app-explore-logs', - standalone: true, - imports: [ - Panel, - LogsFilterComponent, - LogsListComponent, - DropdownModule, - FormsModule, - LogsDataSourceSelectorComponent, - ProgressSpinner - ], - templateUrl: './explore-logs.component.html', - styleUrl: './explore-logs.component.scss' + selector: 'app-explore-logs', + standalone: true, + imports: [ + Panel, + LogsFilterComponent, + LogsListComponent, + DropdownModule, + FormsModule, + LogsDataSourceSelectorComponent, + ], + templateUrl: './explore-logs.component.html', + styleUrl: './explore-logs.component.scss', }) export class ExploreLogsComponent { - isLoading = false; + isLoading = false; - currentFilter: ISearchLogsRequest = { - dataSourceName: '', - pageSize: 100, - page: 1, - sortAscending: false - }; + currentFilter: ISearchLogsRequest = { + dataSourceName: '', + pageSize: 100, + page: 1, + sortAscending: false, + }; - logs: LogEntry[] = []; - pagination = { - page: 1, - pageSize: 100, - totalCount: 0, - totalPages: 0 - }; + logs: LogEntry[] = []; + pagination = { + page: 1, + pageSize: 100, + totalCount: 0, + totalPages: 0, + }; - constructor( - private apiService: ApiService, - private toastLoggerService: ToastLoggerService) { - } + constructor( + private apiService: ApiService, + private toastLoggerService: ToastLoggerService, + ) {} - ngOnInit() { - this.loadLogs(); - } + ngOnInit() { + this.loadLogs(); + } - onFilterChange(filter: ISearchLogsRequest) { - const currentDataSource = this.currentFilter.dataSourceName; - this.currentFilter = { - ...filter, - dataSourceName: currentDataSource, - page: 1 - }; - this.loadLogs(); - } + onFilterChange(filter: ISearchLogsRequest) { + const currentDataSource = this.currentFilter.dataSourceName; + this.currentFilter = { + ...filter, + dataSourceName: currentDataSource, + page: 1, + }; + this.loadLogs(); + } - onPageChange(page: number) { - this.currentFilter.page = page; - this.loadLogs(); - } + onPageChange(page: number) { + this.currentFilter.page = page; + this.loadLogs(); + } - onSortChange(sort: { field: string; ascending: boolean }) { - this.currentFilter.sortField = sort.field; - this.currentFilter.sortAscending = sort.ascending; - this.loadLogs(); - } + onSortChange(sort: { field: string; ascending: boolean }) { + this.currentFilter.sortField = sort.field; + this.currentFilter.sortAscending = sort.ascending; + this.loadLogs(); + } - onDataSourceChange(dataSourceName: string) { - this.currentFilter.dataSourceName = dataSourceName; - this.currentFilter.page = 1; - this.loadLogs(); - } + onDataSourceChange(dataSourceName: string) { + this.currentFilter.dataSourceName = dataSourceName; + this.currentFilter.page = 1; + this.loadLogs(); + } - private loadLogs() { - if (!this.currentFilter.dataSourceName) { - return; - } + private loadLogs() { + if (!this.currentFilter.dataSourceName) { + return; + } - this.isLoading = true; + this.isLoading = true; - this.apiService.postApiV1LogsSearch(this.currentFilter as SearchLogsRequest).pipe( - finalize(() => this.isLoading = false), - tap(page => { - this.logs = page.items!; - this.pagination = { - page: page.pageNumber!, - pageSize: page.pageSize!, - totalCount: page.totalCount!, - totalPages: page.totalPages! - }; - }), - catchError(error => { - this.toastLoggerService.error(formatCustomProblemDetails(error)); - return error; - }) - ).subscribe() - } + this.apiService + .postApiV1LogsSearch(this.currentFilter as SearchLogsRequest) + .pipe( + finalize(() => (this.isLoading = false)), + tap((page) => { + this.logs = page.items!; + this.pagination = { + page: page.pageNumber!, + pageSize: page.pageSize!, + totalCount: page.totalCount!, + totalPages: page.totalPages!, + }; + }), + catchError((error) => { + this.toastLoggerService.error(formatCustomProblemDetails(error)); + return error; + }), + ) + .subscribe(); + } } diff --git a/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.html b/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.html index dd21e26..18921c7 100644 --- a/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.html +++ b/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.html @@ -1,32 +1,33 @@ - - @if (isLoading) { - Loading Data Source... - } @else { - Edit Data Source {{ dataSourceId }}
- Type {{ dataSource.type | uppercase }} - } -
- - @if (isLoading) { -
- -

- - Fetching data source details... -

-
- } @else { -
- + +
+ @if (isLoading) { + Loading Data Source... + } @else { +
+ Edit data source + ({{ dataSource.type | uppercase }})
+ + } +
+
- - Danger Zone - - -
- -
- } + @if (isLoading) { +
+ +

+ + Fetching data source details... +

+
+ } @else { +
+ +
+ } diff --git a/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.scss b/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.scss index 9036948..2fe20c5 100644 --- a/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.scss +++ b/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.scss @@ -64,3 +64,9 @@ font-weight: 500; } } + +.header { + display: flex; + width: 100%; + justify-content: space-between; +} diff --git a/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.ts b/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.ts index 1af3ef0..b962fd1 100644 --- a/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.ts +++ b/frontend/datacat-ui/src/processes/manage-data-sources/manage-data-sources.component.ts @@ -1,76 +1,77 @@ -import {Component, OnInit} from '@angular/core'; -import {Panel} from "primeng/panel"; -import {ActivatedRoute} from "@angular/router"; -import {ApiService, GetDataSourceResponse} from "../../shared/services/datacat-generated-client"; -import {ToastLoggerService} from "../../shared/services/toast-logger.service"; -import {catchError, finalize, tap} from "rxjs"; -import {Divider} from "primeng/divider"; -import {PrimeTemplate} from "primeng/api"; -import {ProgressBar} from "primeng/progressbar"; +import { Component, OnInit } from '@angular/core'; +import { Panel } from 'primeng/panel'; +import { ActivatedRoute } from '@angular/router'; import { - DeleteDataSourceButtonComponent -} from "../../features/data-sources/delete-data-source-button/delete-data-source-button.component"; -import { - EditDataSourceFormComponent -} from "../../features/data-sources/edit-data-source-form/edit-data-source-form.component"; -import {EditDataSource} from "../../entities/data-sources/edit-data-source"; -import {UpperCasePipe} from "@angular/common"; + ApiService, + GetDataSourceResponse, +} from '../../shared/services/datacat-generated-client'; +import { ToastLoggerService } from '../../shared/services/toast-logger.service'; +import { catchError, finalize, tap } from 'rxjs'; +import { Divider } from 'primeng/divider'; +import { PrimeTemplate } from 'primeng/api'; +import { ProgressBar } from 'primeng/progressbar'; +import { DeleteDataSourceButtonComponent } from '../../features/data-sources/delete-data-source-button/delete-data-source-button.component'; +import { EditDataSourceFormComponent } from '../../features/data-sources/edit-data-source-form/edit-data-source-form.component'; +import { EditDataSource } from '../../entities/data-sources/edit-data-source'; +import { UpperCasePipe } from '@angular/common'; @Component({ - selector: 'app-manage-data-sources', - standalone: true, - imports: [ - Panel, - Divider, - PrimeTemplate, - ProgressBar, - DeleteDataSourceButtonComponent, - EditDataSourceFormComponent, - UpperCasePipe - ], - templateUrl: './manage-data-sources.component.html', - styleUrl: './manage-data-sources.component.scss' + selector: 'app-manage-data-sources', + standalone: true, + imports: [ + Panel, + Divider, + PrimeTemplate, + ProgressBar, + DeleteDataSourceButtonComponent, + EditDataSourceFormComponent, + UpperCasePipe, + ], + templateUrl: './manage-data-sources.component.html', + styleUrl: './manage-data-sources.component.scss', }) export class ManageDataSourcesComponent implements OnInit { - isLoading = false; - dataSourceId!: string; - dataSource!: GetDataSourceResponse; + isLoading = false; + dataSourceId!: string; + dataSource!: GetDataSourceResponse; - constructor( - private route: ActivatedRoute, - private apiService: ApiService, - private toastLogger: ToastLoggerService - ) { - } + constructor( + private route: ActivatedRoute, + private apiService: ApiService, + private toastLogger: ToastLoggerService, + ) {} - get editDataSource() { - return { - id: this.dataSource.id, - name: this.dataSource.name, - type: this.dataSource.type, - connectionString: this.dataSource.connectionString, - } as EditDataSource; - } + get editDataSource() { + return { + id: this.dataSource.id, + name: this.dataSource.name, + type: this.dataSource.type, + connectionString: this.dataSource.connectionString, + } as EditDataSource; + } - ngOnInit(): void { - this.loadDataSource(); - } + ngOnInit(): void { + this.loadDataSource(); + } - private loadDataSource(): void { - this.isLoading = true; - this.dataSourceId = this.route.snapshot.params['data-source-id']; + private loadDataSource(): void { + this.isLoading = true; + this.dataSourceId = this.route.snapshot.params['data-source-id']; - this.apiService.getApiV1DataSource(this.dataSourceId).pipe( - tap((data: GetDataSourceResponse) => { - this.dataSource = data; - }), - catchError((err) => { - this.toastLogger.error(err.message); - throw err; - }), - finalize(() => { - this.isLoading = false; - }) - ).subscribe(); - } + this.apiService + .getApiV1DataSource(this.dataSourceId) + .pipe( + tap((data: GetDataSourceResponse) => { + this.dataSource = data; + }), + catchError((err) => { + this.toastLogger.error(err.message); + throw err; + }), + finalize(() => { + this.isLoading = false; + }), + ) + .subscribe(); + } } From 4e031b786eace683b30d9cba79f8589e54ca6ddf Mon Sep 17 00:00:00 2001 From: Nikita Nazarov Date: Thu, 29 May 2025 18:22:17 +0300 Subject: [PATCH 4/4] feat(datacat-ui): fix traces --- .../alerts-counts-by-status.component.ts | 4 +- .../span-details-dialog.component.html | 96 ++++--- .../span-details-dialog.component.scss | 25 +- .../span-details-dialog.component.ts | 215 +++++++-------- ...traces-data-source-selector.component.html | 39 +-- ...traces-data-source-selector.component.scss | 8 +- .../traces-data-source-selector.component.ts | 192 +++++++------- .../traces-filter.component.html | 137 +++++----- .../traces-filter.component.scss | 22 +- .../traces-filter/traces-filter.component.ts | 139 +++++----- .../traces-list/traces-list.component.html | 98 ++++--- .../traces-list/traces-list.component.ts | 212 ++++++++------- .../explore-trace-spans.component.html | 120 +++++---- .../explore-trace-spans.component.scss | 35 ++- .../explore-trace-spans.component.ts | 246 +++++++++--------- .../explore-traces.component.html | 63 ++--- .../explore-traces.component.scss | 27 +- .../explore-traces.component.ts | 142 +++++----- 18 files changed, 925 insertions(+), 895 deletions(-) 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 e35b772..ffba5aa 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 @@ -3,8 +3,7 @@ import { AlertsCountsByStatus } from './alerts-counts-by-status.types'; import { TagModule } from 'primeng/tag'; import { AlertStatus } from '../../../entities'; import { TooltipModule } from 'primeng/tooltip'; -import { from } from 'rxjs'; -import { FAKE_ALERTS_COUNTS_BY_STATUS } from '../../../shared/mock/fakes'; +import { interval } from 'rxjs'; import { ApiService } from '../../../shared/services/datacat-generated-client'; @Component({ @@ -21,6 +20,7 @@ export class AlertsCountsByStatusComponent implements OnInit { ngOnInit() { this.loadAlertsCountsByStatus(); + interval(10000).subscribe(() => this.loadAlertsCountsByStatus()); } protected loadAlertsCountsByStatus() { diff --git a/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.html b/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.html index e5ec1fd..70558ad 100644 --- a/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.html +++ b/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.html @@ -1,55 +1,53 @@
-
-
- @if (!span || flattenedData.length === 0) { -
- No span data available -
- } @else { - @for (item of flattenedData; track item) { -
-
- - {{ item.key }} -
-
- {{ item.value }} -
-
- } - } -
+
+
+ @if (!span || flattenedData.length === 0) { +
No span data available
+ } @else { + @for (item of flattenedData; track item) { +
+
+ {{ item.key }} +
+
+ {{ item.value }} +
+
+ } + }
+
-
-

Related Logs

+
+

Related Logs

- - + + - - - - Message - Details - - - - - {{ sanitizeMessage(log.message) }} - - - - - - -
+ + + + Message + Details + + + + + + {{ sanitizeMessage(log.message) }} + + + + + + + +
diff --git a/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.scss b/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.scss index b1092a8..03f7822 100644 --- a/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.scss +++ b/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.scss @@ -8,7 +8,6 @@ .details-content { overflow-y: auto; max-height: 80vh; - padding: 1.5rem; background-color: var(--surface-ground); .no-data { @@ -21,9 +20,8 @@ .detail-row { display: grid; grid-template-columns: 250px 1fr; - align-items: start; + align-items: center; gap: 1rem; - padding: 0.75rem 1rem; border-bottom: 1px solid var(--surface-border); position: relative; overflow: hidden; @@ -37,27 +35,16 @@ width: 150%; height: 100%; background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.05), - transparent + 90deg, + transparent, + rgba(255, 255, 255, 0.05), + transparent ); animation: shine 1s forwards; } .field-name { - font-weight: 600; - color: var(--text-color); - display: flex; - align-items: center; - gap: 0.5rem; - - .dot { - width: 8px; - height: 8px; - background-color: var(--primary-color); - border-radius: 50%; - } + color: var(--p-surface-400); } .field-value { diff --git a/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.ts b/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.ts index 5743e11..6c80ba3 100644 --- a/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.ts +++ b/frontend/datacat-ui/src/features/traces/span-details-dialog/span-details-dialog.component.ts @@ -1,125 +1,132 @@ -import {Component, OnInit} from '@angular/core'; -import {DynamicDialogConfig} from "primeng/dynamicdialog"; +import { Component, OnInit } from '@angular/core'; +import { DynamicDialogConfig } from 'primeng/dynamicdialog'; import { - ApiService, - ISearchLogsRequest, - SearchLogsRequest, - SpanEntry -} from "../../../shared/services/datacat-generated-client"; -import {Button} from "primeng/button"; -import {PrimeTemplate} from "primeng/api"; -import {TableModule} from "primeng/table"; -import {sanitizeMessage} from "../../../shared/utils/sanitizeMessage"; -import {LogsDetailsComponent} from "../../logs/logs-details/logs-details.component"; -import {AppDialogService} from "../../../shared/services/app-dialog.service"; -import { - LogsDataSourceSelectorComponent -} from "../../logs/logs-data-source-selector/logs-data-source-selector.component"; + ApiService, + ISearchLogsRequest, + SearchLogsRequest, + SpanEntry, +} from '../../../shared/services/datacat-generated-client'; +import { Button } from 'primeng/button'; +import { PrimeTemplate } from 'primeng/api'; +import { TableModule } from 'primeng/table'; +import { sanitizeMessage } from '../../../shared/utils/sanitizeMessage'; +import { LogsDetailsComponent } from '../../logs/logs-details/logs-details.component'; +import { AppDialogService } from '../../../shared/services/app-dialog.service'; +import { LogsDataSourceSelectorComponent } from '../../logs/logs-data-source-selector/logs-data-source-selector.component'; @Component({ - selector: 'app-span-details-dialog', - standalone: true, - imports: [ - Button, - PrimeTemplate, - TableModule, - LogsDataSourceSelectorComponent - ], - templateUrl: './span-details-dialog.component.html', - styleUrl: './span-details-dialog.component.scss' + selector: 'app-span-details-dialog', + standalone: true, + imports: [ + Button, + PrimeTemplate, + TableModule, + LogsDataSourceSelectorComponent, + ], + templateUrl: './span-details-dialog.component.html', + styleUrl: './span-details-dialog.component.scss', }) export class SpanDetailsDialogComponent implements OnInit { - span: SpanEntry; - logs: any[] = []; - pagination = {page: 1, pageSize: 10, totalCount: 0}; - flattenedData: Array<{ key: string; value: any }> = []; - dataSourceName = ''; + span: SpanEntry; + logs: any[] = []; + pagination = { page: 1, pageSize: 10, totalCount: 0 }; + flattenedData: Array<{ key: string; value: any }> = []; + dataSourceName = ''; - protected readonly sanitizeMessage = sanitizeMessage; + protected readonly sanitizeMessage = sanitizeMessage; - constructor( - public config: DynamicDialogConfig, - private apiService: ApiService, - private appDialogService: AppDialogService, - ) { - this.span = config.data; - this.flattenObject(this.span, ''); - } + constructor( + public config: DynamicDialogConfig, + private apiService: ApiService, + private appDialogService: AppDialogService, + ) { + this.span = config.data; + this.flattenObject(this.span, ''); + } - get objectKeys() { - return Object.keys; - } + get objectKeys() { + return Object.keys; + } - ngOnInit(): void { - this.loadLogs(); - } + ngOnInit(): void { + this.loadLogs(); + } - loadLogs() { - if (!this.dataSourceName) return; + loadLogs() { + if (!this.dataSourceName) return; - const request: ISearchLogsRequest = { - dataSourceName: this.dataSourceName, - pageSize: 100, - page: 1, - sortAscending: false, - customFilters: { - 'fields.SpanId.keyword': this.span.spanId! - } - }; + const request: ISearchLogsRequest = { + dataSourceName: this.dataSourceName, + pageSize: 100, + page: 1, + sortAscending: false, + customFilters: { + 'fields.SpanId.keyword': this.span.spanId!, + }, + }; - this.apiService.postApiV1LogsSearch(request as SearchLogsRequest) - .subscribe(response => { - this.logs = response.items!; - this.pagination.totalCount = response.totalCount!; - }); - } + this.apiService + .postApiV1LogsSearch(request as SearchLogsRequest) + .subscribe((response) => { + this.logs = response.items!; + this.pagination.totalCount = response.totalCount!; + }); + } - showLogsDetails(additionalFields: any) { - this.appDialogService.showDialog(LogsDetailsComponent, - 'Additional Fields', - additionalFields || {} - ) - } + showLogsDetails(additionalFields: any) { + this.appDialogService.showDialog( + LogsDetailsComponent, + 'Additional Fields', + additionalFields || {}, + ); + } - onDataSourceChange(dataSourceName: string) { - this.dataSourceName = dataSourceName; - this.loadLogs(); - } + onDataSourceChange(dataSourceName: string) { + this.dataSourceName = dataSourceName; + this.loadLogs(); + } - private flattenObject(obj: any, prefix: string) { - for (const key of Object.keys(obj)) { - const fullKey = prefix ? `${prefix}.${key}` : key; - if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { - this.flattenObject(obj[key], fullKey); - } else if (Array.isArray(obj[key])) { - this.flattenArray(obj[key], fullKey); - } else { - this.flattenedData.push({ - key: fullKey, - value: this.formatValue(key, obj[key]) - }); - } - } + private flattenObject(obj: any, prefix: string) { + for (const key of Object.keys(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + if ( + typeof obj[key] === 'object' && + obj[key] !== null && + !Array.isArray(obj[key]) + ) { + this.flattenObject(obj[key], fullKey); + } else if (Array.isArray(obj[key])) { + this.flattenArray(obj[key], fullKey); + } else { + this.flattenedData.push({ + key: fullKey, + value: this.formatValue(key, obj[key]), + }); + } } + } - private flattenArray(arr: any[], prefix: string) { - arr.forEach((item, index) => { - const fullKey = `${prefix}[${index}]`; - if (typeof item === 'object' && item !== null) { - this.flattenObject(item, fullKey); - } else { - this.flattenedData.push({ - key: fullKey, - value: this.formatValue(fullKey, item) - }); - } + private flattenArray(arr: any[], prefix: string) { + arr.forEach((item, index) => { + const fullKey = `${prefix}[${index}]`; + if (typeof item === 'object' && item !== null) { + this.flattenObject(item, fullKey); + } else { + this.flattenedData.push({ + key: fullKey, + value: this.formatValue(fullKey, item), }); - } + } + }); + } - private formatValue(key: string, value: any): any { - if (key.toLowerCase().includes('time') || key.toLowerCase().includes('timestamp')) { - return new Date(value).toLocaleString(); - } - return value; + private formatValue(key: string, value: any): any { + if ( + key.toLowerCase().includes('time') || + key.toLowerCase().includes('timestamp') + ) { + return new Date(value).toLocaleString(); } + return value; + } } diff --git a/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.html b/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.html index 867c495..a096dd9 100644 --- a/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.html +++ b/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.html @@ -1,17 +1,26 @@
- - - @if (loading) { - -
- -
-
- } -
+ + + @if (loading) { + +
+ +
+
+ } +
diff --git a/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.scss b/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.scss index f21b4ae..29663e1 100644 --- a/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.scss +++ b/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.scss @@ -1,10 +1,10 @@ .data-source-selector { + display: flex; + flex-direction: column; + gap: var(--p-padding-sm); label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - color: var(--text-color); + color: var(--p-surface-400); } p-dropdown { diff --git a/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.ts b/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.ts index 82d66ef..6a61dd4 100644 --- a/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.ts +++ b/frontend/datacat-ui/src/features/traces/traces-data-source-selector/traces-data-source-selector.component.ts @@ -1,110 +1,118 @@ -import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { - ApiService, - MatchMode, - SearchDataSourcesResponse, - SearchFieldType, - SearchFilter, - SearchFilters -} from "../../../shared/services/datacat-generated-client"; -import {catchError, debounceTime, distinctUntilChanged, Subject, switchMap, tap} from "rxjs"; -import {DropdownModule} from "primeng/dropdown"; -import {PrimeTemplate} from "primeng/api"; -import {ProgressSpinner} from "primeng/progressspinner"; -import {FormsModule} from "@angular/forms"; -import {ToastLoggerService} from "../../../shared/services/toast-logger.service"; + ApiService, + MatchMode, + SearchDataSourcesResponse, + SearchFieldType, + SearchFilter, + SearchFilters, +} from '../../../shared/services/datacat-generated-client'; +import { + catchError, + debounceTime, + distinctUntilChanged, + Subject, + switchMap, + tap, +} from 'rxjs'; +import { DropdownModule } from 'primeng/dropdown'; +import { PrimeTemplate } from 'primeng/api'; +import { ProgressSpinner } from 'primeng/progressspinner'; +import { FormsModule } from '@angular/forms'; +import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; @Component({ - selector: 'app-traces-data-source-selector', - standalone: true, - imports: [ - DropdownModule, - PrimeTemplate, - ProgressSpinner, - FormsModule - ], - templateUrl: './traces-data-source-selector.component.html', - styleUrl: './traces-data-source-selector.component.scss' + selector: 'app-traces-data-source-selector', + standalone: true, + imports: [DropdownModule, PrimeTemplate, ProgressSpinner, FormsModule], + templateUrl: './traces-data-source-selector.component.html', + styleUrl: './traces-data-source-selector.component.scss', }) export class TracesDataSourceSelectorComponent implements OnInit { - @Input() selectedDataSource: string | undefined | null = null; - @Output() selectedDataSourceChange = new EventEmitter(); - - dataSources: SearchDataSourcesResponse[] = []; - searchTerm$ = new Subject(); - loading = false; - firstLoad = true; + @Input() selectedDataSource: string | undefined | null = null; + @Output() selectedDataSourceChange = new EventEmitter(); - constructor( - private apiService: ApiService, - private toastLoggerService: ToastLoggerService - ) { - } + dataSources: SearchDataSourcesResponse[] = []; + searchTerm$ = new Subject(); + loading = false; + firstLoad = true; - ngOnInit() { - this.searchTerm$.pipe( - debounceTime(300), - distinctUntilChanged(), - switchMap(term => this.searchDataSources(term)) - ).subscribe(data => { - this.dataSources = data.items!; - this.loading = false; - }); + constructor( + private apiService: ApiService, + private toastLoggerService: ToastLoggerService, + ) {} - this.searchDataSources('').pipe( - tap(data => { - this.firstLoad = false; + ngOnInit() { + this.searchTerm$ + .pipe( + debounceTime(300), + distinctUntilChanged(), + switchMap((term) => this.searchDataSources(term)), + ) + .subscribe((data) => { + this.dataSources = data.items!; + this.loading = false; + }); - if (!data.items) { - return; - } + this.searchDataSources('') + .pipe( + tap((data) => { + this.firstLoad = false; - this.dataSources = data.items; - if (this.firstLoad && data.items.length > 0 && !this.selectedDataSource) { - this.selectedDataSource = data.items[0].name!; - this.selectedDataSourceChange.emit(this.selectedDataSource); - } + if (!data.items) { + return; + } - }), - catchError(err => { - this.toastLoggerService.error(err.message); - return err; - }), - ).subscribe(); - } + this.dataSources = data.items; + if ( + this.firstLoad && + data.items.length > 0 && + !this.selectedDataSource + ) { + this.selectedDataSource = data.items[0].name!; + this.selectedDataSourceChange.emit(this.selectedDataSource); + } + }), + catchError((err) => { + this.toastLoggerService.error(err.message); + return err; + }), + ) + .subscribe(); + } - onSearch(event: { filter: string }) { - this.loading = true; - this.searchTerm$.next(event.filter); - } + onSearch(event: { filter: string }) { + this.loading = true; + this.searchTerm$.next(event.filter); + } - onDataSourceChange() { - this.selectedDataSourceChange.emit(this.selectedDataSource ?? ''); - } + onDataSourceChange() { + this.selectedDataSourceChange.emit(this.selectedDataSource ?? ''); + } - private searchDataSources(searchTerm: string) { - const filters: SearchFilter[] = [ - ({ - key: 'purpose', - value: 'Traces', - matchMode: MatchMode.Equals, - fieldType: SearchFieldType.String - } as SearchFilter) - ]; + private searchDataSources(searchTerm: string) { + const filters: SearchFilter[] = [ + { + key: 'purpose', + value: 'Traces', + matchMode: MatchMode.Equals, + fieldType: SearchFieldType.String, + } as SearchFilter, + ]; - if (searchTerm?.trim()) { - filters.unshift({ - key: 'name', - value: searchTerm, - matchMode: MatchMode.Contains, - fieldType: SearchFieldType.String - } as SearchFilter); - } + if (searchTerm?.trim()) { + filters.unshift({ + key: 'name', + value: searchTerm, + matchMode: MatchMode.Contains, + fieldType: SearchFieldType.String, + } as SearchFilter); + } - const request = { - filters: filters, - } as SearchFilters + const request = { + filters: filters, + } as SearchFilters; - return this.apiService.postApiV1DataSourceSearch(request, 1, 10); - } + return this.apiService.postApiV1DataSourceSearch(request, 1, 10); + } } diff --git a/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.html b/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.html index 9a324b1..7a364d6 100644 --- a/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.html +++ b/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.html @@ -1,73 +1,84 @@
-
-
- - - -
-
- - - -
+
+
+ + +
- -
-
- - - -
-
- - - -
+
+ + +
+
-
-
- - -
+
+
+ + + +
+
+ + +
+
-
-
- - -
-
- - -
+
+
+ +
+
-
- Apply Filters +
+
+ + +
+
+ +
+
+
+ +
diff --git a/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.scss b/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.scss index 2e29ca6..c5bd0c9 100644 --- a/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.scss +++ b/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.scss @@ -1,14 +1,12 @@ .filter-container { - padding-left: 1rem; - padding-top: 1rem; - border-radius: 0.5rem; - box-shadow: 0 0 6px rgba(0, 0, 0, 0.05); + display: flex; + flex-direction: column; + gap: var(--p-padding-md); .filter-row { display: flex; flex-direction: column; - gap: 0.75rem; - margin-bottom: 1.5rem; + gap: var(--p-padding-md); &.row-2col { flex-direction: row; @@ -17,7 +15,7 @@ .p-field { flex: 1 1 0; - min-width: 0; // for proper shrinking + min-width: 0; } } @@ -40,10 +38,10 @@ .p-field { display: flex; flex-direction: column; + gap: var(--p-padding-sm); label { - font-weight: 600; - margin-bottom: 0.25rem; + color: var(--p-surface-400); } input, @@ -57,11 +55,7 @@ .filter-actions { display: flex; + width: 100%; justify-content: flex-end; - margin-top: 1rem; - - p-button { - min-width: 150px; - } } } diff --git a/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.ts b/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.ts index 20120a2..5051855 100644 --- a/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.ts +++ b/frontend/datacat-ui/src/features/traces/traces-filter/traces-filter.component.ts @@ -1,84 +1,87 @@ -import {Component, EventEmitter, Input, Output} from '@angular/core'; -import {ApiService, ISearchTracesRequest} from "../../../shared/services/datacat-generated-client"; -import {DropdownModule} from "primeng/dropdown"; -import {FormsModule} from "@angular/forms"; -import {Calendar} from "primeng/calendar"; -import {InputText} from "primeng/inputtext"; -import {Button} from "primeng/button"; -import {catchError, tap} from "rxjs"; -import {ToastLoggerService} from "../../../shared/services/toast-logger.service"; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { + ApiService, + ISearchTracesRequest, +} from '../../../shared/services/datacat-generated-client'; +import { DropdownModule } from 'primeng/dropdown'; +import { FormsModule } from '@angular/forms'; +import { Calendar } from 'primeng/calendar'; +import { InputText } from 'primeng/inputtext'; +import { Button } from 'primeng/button'; +import { catchError, tap } from 'rxjs'; +import { ToastLoggerService } from '../../../shared/services/toast-logger.service'; @Component({ - selector: 'app-traces-filter', - standalone: true, - imports: [ - DropdownModule, - FormsModule, - Calendar, - InputText, - Button - ], - templateUrl: './traces-filter.component.html', - styleUrl: './traces-filter.component.scss' + selector: 'app-traces-filter', + standalone: true, + imports: [DropdownModule, FormsModule, Calendar, InputText, Button], + templateUrl: './traces-filter.component.html', + styleUrl: './traces-filter.component.scss', }) export class TracesFilterComponent { - @Output() filterChange = new EventEmitter(); - services: string[] = []; - operations: string[] = []; + @Output() filterChange = new EventEmitter(); + services: string[] = []; + operations: string[] = []; - constructor( - private apiService: ApiService, - private toastLoggerService: ToastLoggerService) { - } + constructor( + private apiService: ApiService, + private toastLoggerService: ToastLoggerService, + ) {} - private _currentFilter!: ISearchTracesRequest; + private _currentFilter!: ISearchTracesRequest; - get currentFilter(): ISearchTracesRequest { - return this._currentFilter; - } + get currentFilter(): ISearchTracesRequest { + return this._currentFilter; + } - @Input() - set currentFilter(value: ISearchTracesRequest) { - this._currentFilter = {...value}; - if (value.dataSourceName) { - this.loadServices(); - } + @Input() + set currentFilter(value: ISearchTracesRequest) { + this._currentFilter = { ...value }; + if (value.dataSourceName) { + this.loadServices(); } + } - loadServices() { - if (!this.currentFilter.dataSourceName) return; + loadServices() { + if (!this.currentFilter.dataSourceName) return; - this.apiService.getApiV1TracesServices(this.currentFilter.dataSourceName) - .pipe( - tap(services => this.services = services), - catchError(err => { - this.toastLoggerService.error(err.message); - return err; - }) - ).subscribe(); - } + this.apiService + .getApiV1TracesServices(this.currentFilter.dataSourceName) + .pipe( + tap((services) => (this.services = services)), + catchError((err) => { + this.toastLoggerService.error(err.message); + return err; + }), + ) + .subscribe(); + } - loadOperations() { - if (!this.currentFilter.dataSourceName || !this.currentFilter.service) return; + loadOperations() { + if (!this.currentFilter.dataSourceName || !this.currentFilter.service) + return; - this.apiService.getApiV1TracesOperations( - this.currentFilter.dataSourceName, - this.currentFilter.service) - .pipe( - tap(operations => this.operations = operations), - catchError(err => { - this.toastLoggerService.error(err.message); - return err; - }) - ).subscribe(); - } + this.apiService + .getApiV1TracesOperations( + this.currentFilter.dataSourceName, + this.currentFilter.service, + ) + .pipe( + tap((operations) => (this.operations = operations)), + catchError((err) => { + this.toastLoggerService.error(err.message); + return err; + }), + ) + .subscribe(); + } - onServiceChange() { - this.currentFilter.operation = undefined; - this.loadOperations(); - } + onServiceChange() { + this.currentFilter.operation = undefined; + this.loadOperations(); + } - applyFilters() { - this.filterChange.emit(this.currentFilter); - } + applyFilters() { + this.filterChange.emit(this.currentFilter); + } } diff --git a/frontend/datacat-ui/src/features/traces/traces-list/traces-list.component.html b/frontend/datacat-ui/src/features/traces/traces-list/traces-list.component.html index ba88043..e39baff 100644 --- a/frontend/datacat-ui/src/features/traces/traces-list/traces-list.component.html +++ b/frontend/datacat-ui/src/features/traces/traces-list/traces-list.component.html @@ -1,53 +1,49 @@
-
- - - - Duration - Trace - Spans - Services - Time - - - - - - -
- {{ getFormattedDuration(getCurrentDuration(trace)) }} - - - - {{ trace.traceId | slice:0:8 }} - - - - - - - @for (svc of getTraceServices(trace); track svc) { - - - } - - - {{ getTracesStartTime(trace) | date:'h:mm:ss a' }}
- {{ getTracesTimeAgo(trace) }} - - - - - - -
-
-
+
+ + + + Duration + Trace + Spans + Services + Time + + + + + +
+ {{ getFormattedDuration(getCurrentDuration(trace)) }} + + + + {{ trace.traceId | slice: 0 : 8 }} + + + + {{ getSpanCount(trace) }} + + + @for (svc of getTraceServices(trace); track svc) { + + + } + + + {{ getTracesStartTime(trace) | date: 'h:mm:ss a' }}
+ {{ getTracesTimeAgo(trace) }} + + +
+
+
diff --git a/frontend/datacat-ui/src/features/traces/traces-list/traces-list.component.ts b/frontend/datacat-ui/src/features/traces/traces-list/traces-list.component.ts index 64edae3..ddc9343 100644 --- a/frontend/datacat-ui/src/features/traces/traces-list/traces-list.component.ts +++ b/frontend/datacat-ui/src/features/traces/traces-list/traces-list.component.ts @@ -1,132 +1,130 @@ -import {Component, Input} from '@angular/core'; -import {TraceEntry} from "../../../shared/services/datacat-generated-client"; -import {TableModule} from "primeng/table"; -import {Router} from "@angular/router"; -import {Tag} from "primeng/tag"; -import {DatePipe, SlicePipe} from "@angular/common"; -import {FormsModule} from "@angular/forms"; -import * as urls from "../../../shared/common/urls"; +import { Component, Input } from '@angular/core'; +import { TraceEntry } from '../../../shared/services/datacat-generated-client'; +import { TableModule } from 'primeng/table'; +import { Router } from '@angular/router'; +import { Tag } from 'primeng/tag'; +import { DatePipe, SlicePipe } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import * as urls from '../../../shared/common/urls'; @Component({ - selector: 'app-traces-list', - standalone: true, - imports: [ - TableModule, - Tag, - SlicePipe, - FormsModule, - DatePipe - ], - templateUrl: './traces-list.component.html', - styleUrl: './traces-list.component.scss' + selector: 'app-traces-list', + standalone: true, + imports: [TableModule, Tag, SlicePipe, FormsModule, DatePipe], + templateUrl: './traces-list.component.html', + styleUrl: './traces-list.component.scss', }) export class TracesListComponent { - @Input() traces: TraceEntry[] = []; - @Input({required: true}) dataSourceName: string = ''; + @Input() traces: TraceEntry[] = []; + @Input({ required: true }) dataSourceName: string = ''; - constructor( - private router: Router) { - } - - getMaxDuration(): number { - if (!this.traces || this.traces.length === 0) { - return 0; - } - - const durations = this.traces.flatMap(log => - log.spans?.map(span => this.parseDuration(span.duration || "00:00:00.0000000")) || [] - ); + constructor(private router: Router) {} - return Math.max(...durations, 0); + getMaxDuration(): number { + if (!this.traces || this.traces.length === 0) { + return 0; } - getCurrentDuration(log: TraceEntry): number { - const durations = log.spans?.map(span => this.parseDuration(span.duration || "00:00:00.0000000")) || []; - return Math.max(...durations, 0); + const durations = this.traces.flatMap( + (log) => + log.spans?.map((span) => + this.parseDuration(span.duration || '00:00:00.0000000'), + ) || [], + ); + + return Math.max(...durations, 0); + } + + getCurrentDuration(log: TraceEntry): number { + const durations = + log.spans?.map((span) => + this.parseDuration(span.duration || '00:00:00.0000000'), + ) || []; + return Math.max(...durations, 0); + } + + getSpanCount(trace: TraceEntry): number { + if (trace?.spans && Array.isArray(trace.spans)) { + return trace.spans.length; } - - getSpanCount(trace: TraceEntry): number { - if (trace?.spans && Array.isArray(trace.spans)) { - return trace.spans.length; - } - return 0; + return 0; + } + + getTraceServices(trace: TraceEntry): string[] { + if (!trace?.processes) return []; + + const services = new Set(); + for (const processId in trace.processes) { + const process = trace.processes[processId]; + if (process?.serviceName) { + services.add(process.serviceName); + } } - getTraceServices(trace: TraceEntry): string[] { - if (!trace?.processes) return []; - - const services = new Set(); - for (const processId in trace.processes) { - const process = trace.processes[processId]; - if (process?.serviceName) { - services.add(process.serviceName); - } - } + return Array.from(services); + } - return Array.from(services); - } + getTracesStartTime(trace: TraceEntry): string { + const minStartTime = trace.spans?.reduce((minTime, span) => { + const spanStartTime = new Date(span.startTime!); + return spanStartTime < minTime ? spanStartTime : minTime; + }, new Date()); - getTracesStartTime(trace: TraceEntry): string { - const minStartTime = trace.spans?.reduce((minTime, span) => { - const spanStartTime = new Date(span.startTime!); - return spanStartTime < minTime ? spanStartTime : minTime; - }, new Date()); + return minStartTime?.toISOString() || ''; + } - return minStartTime?.toISOString() || ""; + getTracesTimeAgo(trace: TraceEntry): string { + if (!trace.spans || trace.spans.length === 0) { + return 'No traces available'; } - getTracesTimeAgo(trace: TraceEntry): string { - if (!trace.spans || trace.spans.length === 0) { - return 'No traces available'; - } - - const traceStartTime = this.getTracesStartTime(trace); - if (!traceStartTime) { - return ""; - } - - const now = new Date(); - const diffMs = now.getTime() - new Date(traceStartTime).getTime(); - const diffSec = Math.floor(diffMs / 1000); - const diffMin = Math.floor(diffSec / 60); - const diffHour = Math.floor(diffMin / 60); - - if (diffHour > 0) { - return `${diffHour} hour${diffHour > 1 ? 's' : ''} ago`; - } else if (diffMin > 0) { - return `${diffMin} minute${diffMin > 1 ? 's' : ''} ago`; - } else { - return `${diffSec} second${diffSec !== 1 ? 's' : ''} ago`; - } + const traceStartTime = this.getTracesStartTime(trace); + if (!traceStartTime) { + return ''; } - getFormattedDuration(durationMs: number): string { - if (durationMs >= 3600000) { - return `${(durationMs / 3600000).toFixed(2)} hour${durationMs / 3600000 > 1 ? 's' : ''}`; - } else if (durationMs >= 1000) { - return `${(durationMs / 1000).toFixed(2)} second${durationMs / 1000 > 1 ? 's' : ''}`; - } else { - return `${durationMs}ms`; - } + const now = new Date(); + const diffMs = now.getTime() - new Date(traceStartTime).getTime(); + const diffSec = Math.floor(diffMs / 1000); + const diffMin = Math.floor(diffSec / 60); + const diffHour = Math.floor(diffMin / 60); + + if (diffHour > 0) { + return `${diffHour} hour${diffHour > 1 ? 's' : ''} ago`; + } else if (diffMin > 0) { + return `${diffMin} minute${diffMin > 1 ? 's' : ''} ago`; + } else { + return `${diffSec} second${diffSec !== 1 ? 's' : ''} ago`; } + } + + getFormattedDuration(durationMs: number): string { + if (durationMs >= 3600000) { + return `${(durationMs / 3600000).toFixed(2)} hour${durationMs / 3600000 > 1 ? 's' : ''}`; + } else if (durationMs >= 1000) { + return `${(durationMs / 1000).toFixed(2)} second${durationMs / 1000 > 1 ? 's' : ''}`; + } else { + return `${durationMs}ms`; + } + } - parseDuration(duration: string): number { - const regex = /(\d{2}):(\d{2}):(\d{2})\.(\d{7})/; - const match = duration.match(regex); - - if (match) { - const hours = parseInt(match[1], 10); - const minutes = parseInt(match[2], 10); - const seconds = parseInt(match[3], 10); - const milliseconds = parseInt(match[4], 10) / 10000; + parseDuration(duration: string): number { + const regex = /(\d{2}):(\d{2}):(\d{2})\.(\d{7})/; + const match = duration.match(regex); - return hours * 3600000 + minutes * 60000 + seconds * 1000 + milliseconds; - } + if (match) { + const hours = parseInt(match[1], 10); + const minutes = parseInt(match[2], 10); + const seconds = parseInt(match[3], 10); + const milliseconds = parseInt(match[4], 10) / 10000; - return 0; + return hours * 3600000 + minutes * 60000 + seconds * 1000 + milliseconds; } - redirectToTraceDetails(traceId: string) { - this.router.navigateByUrl(urls.traceUrl(traceId, this.dataSourceName)) - } + return 0; + } + + redirectToTraceDetails(traceId: string) { + this.router.navigateByUrl(urls.traceUrl(traceId, this.dataSourceName)); + } } diff --git a/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.html b/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.html index b132500..ca4284d 100644 --- a/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.html +++ b/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.html @@ -1,56 +1,76 @@ - -
- -
-
Service & Operation
-
- @for (marker of timeMarkers; track marker; let i = $index; let last = $last; let first = $first) { -
- {{ formatTime(marker - startTime) }} -
- } -
-
Actions
-
+ +
+ +
+
Service & Operation
+
+ @for ( + marker of timeMarkers; + track marker; + let i = $index; + let last = $last; + let first = $first + ) { +
+ {{ formatTime(marker - startTime) }} +
+ } +
+
Actions
+
- - @if (trace) { -
- @for (span of trace.spans; track span) { -
-
- {{ getServiceName(span.processId!) }}
- {{ span.operationName }} -
+ + @if (trace) { +
+ @for (span of trace.spans; track span) { +
+
+ {{ + getServiceName(span.processId!) + }} + {{ span.operationName }} +
-
-
-
-
- - {{ formatTime(parseDuration(span.duration!)) }} - -
-
-
-
+
+
+
+
+ + {{ formatTime(parseDuration(span.duration!)) }} + +
+
+
+
-
- -
-
- } +
+
+
} -
+
+ } +
diff --git a/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.scss b/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.scss index eccd1ff..7e4ffa2 100644 --- a/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.scss +++ b/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.scss @@ -2,8 +2,8 @@ display: grid; grid-template-columns: 300px 1fr 120px; width: 100%; - font-family: sans-serif; overflow-x: auto; + border-bottom: 1px solid var(--p-surface-600); } .header-row { @@ -14,15 +14,14 @@ top: 0; z-index: 2; padding: 1rem 0.75rem; - font-weight: 600; - border-bottom: 2px solid #ccc; - border-top: 2px solid #ccc; + border-bottom: 1px solid var(--p-surface-600); + border-top: 1px solid var(--p-surface-600); } .service-header, .timeline-header, .actions-header { - border-right: 1px solid #ccc; + border-right: 1px solid var(--p-surface-600); } .service-header, @@ -30,7 +29,6 @@ display: flex; align-items: center; justify-content: center; - font-weight: bold; } .actions-header { @@ -48,8 +46,8 @@ > div { display: flex; align-items: center; - padding: 0.75rem; - border-bottom: 1px solid #e5e5e5; + padding-left: var(--p-padding-md); + padding-right: var(--p-padding-md); } &:hover { @@ -58,7 +56,16 @@ .service-cell, .timeline-cell { - border-right: 1px solid #e5e5e5; + border-right: 1px solid var(--p-surface-600); + } + + .service-cell { + display: flex; + flex-direction: column; + gap: var(--p-padding-xs); + align-items: start; + padding-top: var(--p-padding-sm); + padding-bottom: var(--p-padding-sm); } .actions-cell { @@ -105,19 +112,11 @@ .time-marker { position: absolute; - font-size: 0.75em; white-space: nowrap; } -.service-name { - font-weight: bold; - font-size: 1em; -} - .operation-name { - font-size: 0.5em; - margin-left: 5px; - color: gray; + color: var(--p-surface-400); } .actions-cell { diff --git a/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.ts b/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.ts index ec85541..b943142 100644 --- a/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.ts +++ b/frontend/datacat-ui/src/processes/explore-trace-spans/explore-trace-spans.component.ts @@ -1,144 +1,148 @@ -import {Component, OnInit} from '@angular/core'; -import {ActivatedRoute} from "@angular/router"; -import {ApiService, SpanEntry, TraceEntry} from "../../shared/services/datacat-generated-client"; -import {catchError, finalize, tap, throwError} from "rxjs"; -import {ToastLoggerService} from "../../shared/services/toast-logger.service"; -import {Button} from "primeng/button"; -import {Panel} from "primeng/panel"; -import {NgStyle} from "@angular/common"; -import {AppDialogService} from "../../shared/services/app-dialog.service"; -import {SpanDetailsDialogComponent} from "../../features/traces/span-details-dialog/span-details-dialog.component"; -import {formatCustomProblemDetails} from "../../shared/utils/formatCustomProblemDetails"; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { + ApiService, + SpanEntry, + TraceEntry, +} from '../../shared/services/datacat-generated-client'; +import { catchError, finalize, tap, throwError } from 'rxjs'; +import { ToastLoggerService } from '../../shared/services/toast-logger.service'; +import { Button } from 'primeng/button'; +import { Panel } from 'primeng/panel'; +import { NgStyle } from '@angular/common'; +import { AppDialogService } from '../../shared/services/app-dialog.service'; +import { SpanDetailsDialogComponent } from '../../features/traces/span-details-dialog/span-details-dialog.component'; +import { formatCustomProblemDetails } from '../../shared/utils/formatCustomProblemDetails'; @Component({ - selector: 'app-explore-trace-spans', - standalone: true, - imports: [ - Button, - Panel, - NgStyle - ], - templateUrl: './explore-trace-spans.component.html', - styleUrl: './explore-trace-spans.component.scss' + selector: 'app-explore-trace-spans', + standalone: true, + imports: [Button, Panel, NgStyle], + templateUrl: './explore-trace-spans.component.html', + styleUrl: './explore-trace-spans.component.scss', }) export class ExploreTraceSpansComponent implements OnInit { - isLoading = false; + isLoading = false; - trace: TraceEntry | undefined; + trace: TraceEntry | undefined; - traceId: string = ''; - dataSourceName: string = ''; + traceId: string = ''; + dataSourceName: string = ''; - // Добавляем новые свойства - totalDurationMs = 0; - startTime = 0; - timeMarkers: number[] = []; + // Добавляем новые свойства + totalDurationMs = 0; + startTime = 0; + timeMarkers: number[] = []; - constructor( - private route: ActivatedRoute, - private apiService: ApiService, - private toastLoggerService: ToastLoggerService, - private appDialogService: AppDialogService) { - } - - ngOnInit(): void { - this.traceId = this.route.snapshot.paramMap.get('traceId') || ''; - this.dataSourceName = this.route.snapshot.queryParamMap.get('dataSourceName') || ''; - - if (!this.dataSourceName) { - return; - } - - this.isLoading = true; - - this.apiService.getApiV1Traces(this.traceId, this.dataSourceName).pipe( - finalize(() => this.isLoading = false), - tap(trace => { - const processedSpans = trace.spans! - .filter(s => s.timestamp !== undefined && s.duration !== undefined) - .sort((a, b) => { - return a.timestamp! - b.timestamp! - } - ); - - this.trace = { - ...trace, - spans: processedSpans - } as TraceEntry; - - this.calculateTimeMetrics(); - }), - catchError(error => { - this.toastLoggerService.error(formatCustomProblemDetails(error)); - return throwError(() => error); - }) - ).subscribe(); - } - - openSpanDetails(span: SpanEntry) { - this.appDialogService.showDialog(SpanDetailsDialogComponent, - "Span Details", - span, - '1200px', - '1500px'); - } + constructor( + private route: ActivatedRoute, + private apiService: ApiService, + private toastLoggerService: ToastLoggerService, + private appDialogService: AppDialogService, + ) {} - getSpanWidth(span: SpanEntry): number { - const duration = this.parseDuration(span.duration!); - return (duration / this.totalDurationMs) * 100; - } - - getSpanOffset(span: SpanEntry): number { - if (!span.startTime) return 0; + ngOnInit(): void { + this.traceId = this.route.snapshot.paramMap.get('traceId') || ''; + this.dataSourceName = + this.route.snapshot.queryParamMap.get('dataSourceName') || ''; - const spanStart = new Date(span.startTime).getTime(); - const offset = spanStart - this.startTime; - return Math.max(0, (offset / this.totalDurationMs) * 100); + if (!this.dataSourceName) { + return; } - formatTime(ms: number): string { - if (this.totalDurationMs >= 1000) { - const totalSeconds = ms / 1000; - return `${totalSeconds.toFixed(2)}s`; - } - return `${ms}ms`; + this.isLoading = true; + + this.apiService + .getApiV1Traces(this.traceId, this.dataSourceName) + .pipe( + finalize(() => (this.isLoading = false)), + tap((trace) => { + const processedSpans = trace + .spans!.filter( + (s) => s.timestamp !== undefined && s.duration !== undefined, + ) + .sort((a, b) => { + return a.timestamp! - b.timestamp!; + }); + + this.trace = { + ...trace, + spans: processedSpans, + } as TraceEntry; + + this.calculateTimeMetrics(); + }), + catchError((error) => { + this.toastLoggerService.error(formatCustomProblemDetails(error)); + return throwError(() => error); + }), + ) + .subscribe(); + } + + openSpanDetails(span: SpanEntry) { + this.appDialogService.showDialog( + SpanDetailsDialogComponent, + 'Span Details', + span, + '1200px', + '1500px', + ); + } + + getSpanWidth(span: SpanEntry): number { + const duration = this.parseDuration(span.duration!); + return (duration / this.totalDurationMs) * 100; + } + + getSpanOffset(span: SpanEntry): number { + if (!span.startTime) return 0; + + const spanStart = new Date(span.startTime).getTime(); + const offset = spanStart - this.startTime; + return Math.max(0, (offset / this.totalDurationMs) * 100); + } + + formatTime(ms: number): string { + if (this.totalDurationMs >= 1000) { + const totalSeconds = ms / 1000; + return `${totalSeconds.toFixed(2)}s`; } + return `${ms}ms`; + } - parseDuration(duration: string): number { - // example of input: "00:00:00.3090000" - const parts = duration.split(':'); - const secondsPart = parts[2].split('.'); + parseDuration(duration: string): number { + // example of input: "00:00:00.3090000" + const parts = duration.split(':'); + const secondsPart = parts[2].split('.'); - const hours = parseInt(parts[0]) || 0; - const minutes = parseInt(parts[1]) || 0; - const seconds = parseInt(secondsPart[0]) || 0; - const milliseconds = parseInt(secondsPart[1]?.substring(0, 3)) || 0; // Берем только первые 3 цифры + const hours = parseInt(parts[0]) || 0; + const minutes = parseInt(parts[1]) || 0; + const seconds = parseInt(secondsPart[0]) || 0; + const milliseconds = parseInt(secondsPart[1]?.substring(0, 3)) || 0; // Берем только первые 3 цифры - return hours * 3600000 + - minutes * 60000 + - seconds * 1000 + - milliseconds; - } + return hours * 3600000 + minutes * 60000 + seconds * 1000 + milliseconds; + } - getServiceName(processId: string): string { - return this.trace?.processes?.[processId]?.serviceName || 'Unknown'; - } + getServiceName(processId: string): string { + return this.trace?.processes?.[processId]?.serviceName || 'Unknown'; + } - private calculateTimeMetrics() { - if (!this.trace?.spans?.length) return; + private calculateTimeMetrics() { + if (!this.trace?.spans?.length) return; - const spans = this.trace.spans; + const spans = this.trace.spans; - const startTimestamps = spans.map(s => new Date(s.startTime!).getTime()); - this.startTime = Math.min(...startTimestamps); + const startTimestamps = spans.map((s) => new Date(s.startTime!).getTime()); + this.startTime = Math.min(...startTimestamps); - const endTimestamps = spans.map(s => - new Date(s.startTime!).getTime() + this.parseDuration(s.duration!) - ); - this.totalDurationMs = Math.max(...endTimestamps) - this.startTime; + const endTimestamps = spans.map( + (s) => new Date(s.startTime!).getTime() + this.parseDuration(s.duration!), + ); + this.totalDurationMs = Math.max(...endTimestamps) - this.startTime; - this.timeMarkers = Array.from({length: 5}, (_, i) => - Math.round(this.startTime + (this.totalDurationMs * i) / 4) - ); - } + this.timeMarkers = Array.from({ length: 5 }, (_, i) => + Math.round(this.startTime + (this.totalDurationMs * i) / 4), + ); + } } diff --git a/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.html b/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.html index 783d0c6..cccdd07 100644 --- a/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.html +++ b/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.html @@ -1,32 +1,35 @@ - -
-
-
-

Explore and analyze system traces in real-time

-
- -
- - - - - -
+ +

+ Explore and analyze system traces in real-time +

+
+ +
+ + +
+
+ + @if (isLoading) { +
+ +

Loading traces...

- -
- @if (isLoading) { -
- -

Loading traces...

-
- } @else { - - } -
-
+ } @else { + + } +
+
diff --git a/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.scss b/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.scss index 07d5c9d..e74592c 100644 --- a/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.scss +++ b/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.scss @@ -4,7 +4,9 @@ } .slot { - margin: var(--p-padding-md); + margin-left: var(--p-padding-md); + margin-right: var(--p-padding-md); + margin-bottom: var(--p-padding-md); position: relative; } @@ -15,22 +17,13 @@ .traces-explorer-layout { display: flex; - gap: 1rem; + justify-content: space-between; height: 100%; - - .left-panel { - flex: 1; - max-width: 33.3333%; - display: flex; - flex-direction: column; - gap: 1rem; - } - - .right-panel { - flex: 2; - max-width: 66.6666%; - display: flex; - flex-direction: column; - } + margin: var(--p-padding-md); } +.h { + display: flex; + flex-direction: column; + gap: var(--p-padding-md); +} diff --git a/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.ts b/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.ts index c8e8e87..ba376ae 100644 --- a/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.ts +++ b/frontend/datacat-ui/src/processes/explore-traces/explore-traces.component.ts @@ -1,84 +1,84 @@ -import {Component} from '@angular/core'; -import {Panel} from "primeng/panel"; +import { Component } from '@angular/core'; +import { Panel } from 'primeng/panel'; +import { TracesDataSourceSelectorComponent } from '../../features/traces/traces-data-source-selector/traces-data-source-selector.component'; import { - TracesDataSourceSelectorComponent -} from "../../features/traces/traces-data-source-selector/traces-data-source-selector.component"; -import { - ApiService, - ISearchTracesRequest, - SearchTracesRequest, - TraceEntry -} from "../../shared/services/datacat-generated-client"; -import {catchError, finalize, tap} from "rxjs"; -import {ToastLoggerService} from "../../shared/services/toast-logger.service"; -import {ProgressSpinner} from "primeng/progressspinner"; -import {TracesListComponent} from "../../features/traces/traces-list/traces-list.component"; -import {TracesFilterComponent} from "../../features/traces/traces-filter/traces-filter.component"; -import {formatCustomProblemDetails} from "../../shared/utils/formatCustomProblemDetails"; + ApiService, + ISearchTracesRequest, + SearchTracesRequest, + TraceEntry, +} from '../../shared/services/datacat-generated-client'; +import { catchError, finalize, tap } from 'rxjs'; +import { ToastLoggerService } from '../../shared/services/toast-logger.service'; +import { ProgressSpinner } from 'primeng/progressspinner'; +import { TracesListComponent } from '../../features/traces/traces-list/traces-list.component'; +import { TracesFilterComponent } from '../../features/traces/traces-filter/traces-filter.component'; +import { formatCustomProblemDetails } from '../../shared/utils/formatCustomProblemDetails'; @Component({ - selector: 'app-explore-traces', - standalone: true, - imports: [ - Panel, - TracesDataSourceSelectorComponent, - ProgressSpinner, - TracesListComponent, - TracesFilterComponent - ], - templateUrl: './explore-traces.component.html', - styleUrl: './explore-traces.component.scss' + selector: 'app-explore-traces', + standalone: true, + imports: [ + Panel, + TracesDataSourceSelectorComponent, + ProgressSpinner, + TracesListComponent, + TracesFilterComponent, + ], + templateUrl: './explore-traces.component.html', + styleUrl: './explore-traces.component.scss', }) export class ExploreTracesComponent { - isLoading = false; + isLoading = false; - traces: TraceEntry[] = []; - currentFilter: ISearchTracesRequest = { - dataSourceName: '', - service: '', - operation: '', - tags: undefined, - start: new Date(Date.now() - 3600 * 1000), - end: new Date(), - limit: 20, - minDuration: undefined, - maxDuration: undefined - } as ISearchTracesRequest; + traces: TraceEntry[] = []; + currentFilter: ISearchTracesRequest = { + dataSourceName: '', + service: '', + operation: '', + tags: undefined, + start: new Date(Date.now() - 3600 * 1000), + end: new Date(), + limit: 20, + minDuration: undefined, + maxDuration: undefined, + } as ISearchTracesRequest; - constructor( - private apiService: ApiService, - private toastLoggerService: ToastLoggerService, - ) { - } + constructor( + private apiService: ApiService, + private toastLoggerService: ToastLoggerService, + ) {} - onDataSourceChange(dataSourceName: string) { - this.currentFilter = { - ...this.currentFilter, - dataSourceName: dataSourceName - }; - } + onDataSourceChange(dataSourceName: string) { + this.currentFilter = { + ...this.currentFilter, + dataSourceName: dataSourceName, + }; + } - onFilterChange(filter: any) { - this.currentFilter = filter; - this.loadTraces(); - } + onFilterChange(filter: any) { + this.currentFilter = filter; + this.loadTraces(); + } - private loadTraces() { - if (!this.currentFilter.dataSourceName) { - return; - } + private loadTraces() { + if (!this.currentFilter.dataSourceName) { + return; + } - this.isLoading = true; + this.isLoading = true; - this.apiService.postApiV1TracesSearch(this.currentFilter as SearchTracesRequest).pipe( - finalize(() => this.isLoading = false), - tap(traces => { - this.traces = traces!; - }), - catchError(error => { - this.toastLoggerService.error(formatCustomProblemDetails(error)); - return error; - }) - ).subscribe() - } + this.apiService + .postApiV1TracesSearch(this.currentFilter as SearchTracesRequest) + .pipe( + finalize(() => (this.isLoading = false)), + tap((traces) => { + this.traces = traces!; + }), + catchError((error) => { + this.toastLoggerService.error(formatCustomProblemDetails(error)); + return error; + }), + ) + .subscribe(); + } }