From 8b89e03999d3cffdd626047469cf0edd3b4461d3 Mon Sep 17 00:00:00 2001 From: waterplea Date: Thu, 18 Apr 2024 13:39:49 +0700 Subject: [PATCH] feat: implement metrics on the dashboard --- web/projects/shared/assets/img/meter.svg | 114 +++++++++++ .../portal/routes/dashboard/cpu.component.ts | 81 ++++++++ .../routes/dashboard/dashboard.component.ts | 15 +- .../routes/dashboard/metric.component.ts | 48 +++++ .../routes/dashboard/metrics.component.ts | 187 +++++++++++++++++- .../routes/dashboard/temperature.component.ts | 89 +++++++++ .../routes/dashboard/utilities.component.ts | 4 +- .../src/app/services/breadcrumbs.service.ts | 3 - .../ui/src/app/utils/system-utilities.ts | 4 + 9 files changed, 528 insertions(+), 17 deletions(-) create mode 100644 web/projects/shared/assets/img/meter.svg create mode 100644 web/projects/ui/src/app/routes/portal/routes/dashboard/cpu.component.ts create mode 100644 web/projects/ui/src/app/routes/portal/routes/dashboard/metric.component.ts create mode 100644 web/projects/ui/src/app/routes/portal/routes/dashboard/temperature.component.ts diff --git a/web/projects/shared/assets/img/meter.svg b/web/projects/shared/assets/img/meter.svg new file mode 100644 index 000000000..1366ae2fb --- /dev/null +++ b/web/projects/shared/assets/img/meter.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/projects/ui/src/app/routes/portal/routes/dashboard/cpu.component.ts b/web/projects/ui/src/app/routes/portal/routes/dashboard/cpu.component.ts new file mode 100644 index 000000000..0b3ef27ae --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/routes/dashboard/cpu.component.ts @@ -0,0 +1,81 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' + +@Component({ + standalone: true, + selector: 'app-cpu', + template: ` +
+
+
{{ percent }}%
+ `, + styles: ` + @import '@taiga-ui/core/styles/taiga-ui-local'; + + :host { + position: relative; + margin: 1rem auto; + width: 80%; + aspect-ratio: 1; + background: var(--tui-clear); + border-radius: 100%; + } + + .meter { + position: absolute; + inset: 7%; + mask: url(/assets/img/meter.svg); + background: conic-gradient( + from 180deg, + var(--tui-success-fill) 30%, + var(--tui-warning-fill), + var(--tui-error-fill) 70% + ); + } + + .percent { + position: absolute; + bottom: 10%; + width: 100%; + text-align: center; + } + + .arrow { + @include transition(transform); + position: absolute; + top: 50%; + left: 50%; + + &:before { + content: ''; + position: absolute; + inset: -0.5rem; + background: var(--tui-base-02); + border-radius: 100%; + } + + &:after { + content: ''; + position: absolute; + width: 0.25rem; + height: 2rem; + border-radius: 1rem; + background: var(--tui-text-01); + transform: translate(-0.125rem, -0.125rem); + clip-path: polygon(0 0, 100% 0, 60% 100%, 40% 100%); + } + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CpuComponent { + @Input() + value = 0 + + get percent(): string { + return (100 * this.value).toFixed(1) + } + + get transform(): string { + return `rotate(${60 + 300 * this.value}deg)` + } +} diff --git a/web/projects/ui/src/app/routes/portal/routes/dashboard/dashboard.component.ts b/web/projects/ui/src/app/routes/portal/routes/dashboard/dashboard.component.ts index 2b63042c5..2778c970c 100644 --- a/web/projects/ui/src/app/routes/portal/routes/dashboard/dashboard.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/dashboard/dashboard.component.ts @@ -1,8 +1,10 @@ -import { DatePipe } from '@angular/common' -import { ChangeDetectionStrategy, Component } from '@angular/core' +import { AsyncPipe, DatePipe } from '@angular/common' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' +import { RouterLink } from '@angular/router' import { TuiIconModule } from '@taiga-ui/experimental' import { map, timer } from 'rxjs' +import { ApiService } from 'src/app/services/api/embassy-api.service' import { MetricsComponent } from './metrics.component' import { ServicesComponent } from './services.component' import { UtilitiesComponent } from './utilities.component' @@ -11,7 +13,7 @@ import { UtilitiesComponent } from './utilities.component' standalone: true, template: ` - +

Metrics @@ -60,6 +62,7 @@ import { UtilitiesComponent } from './utilities.component' left: 22%; font-weight: bold; line-height: 1.75rem; + text-shadow: 0 0 0.25rem #000; } h2 { @@ -78,10 +81,11 @@ import { UtilitiesComponent } from './utilities.component' } :host-context(tui-root._mobile) { - height: calc(100vh - 7rem); + height: calc(100vh - 7.375rem); display: block; margin: 0; border-top: 0; + border-bottom: 0; app-metrics, app-utilities, @@ -121,9 +125,12 @@ import { UtilitiesComponent } from './utilities.component' UtilitiesComponent, TuiIconModule, DatePipe, + RouterLink, + AsyncPipe, ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class DashboardComponent { readonly date = toSignal(timer(0, 1000).pipe(map(() => new Date()))) + readonly metrics$ = inject(ApiService).openMetricsWebsocket$({ url: '' }) } diff --git a/web/projects/ui/src/app/routes/portal/routes/dashboard/metric.component.ts b/web/projects/ui/src/app/routes/portal/routes/dashboard/metric.component.ts new file mode 100644 index 000000000..d8cb1e352 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/routes/dashboard/metric.component.ts @@ -0,0 +1,48 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' + +@Component({ + standalone: true, + selector: 'app-metric', + template: ` +
{{ label }}
+ + `, + changeDetection: ChangeDetectionStrategy.OnPush, + styles: ` + :host { + display: flex; + flex-direction: column; + flex: 1; + border: 1px solid var(--tui-clear); + border-radius: 0 1rem 1rem 1rem; + box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.25); + overflow: hidden; + } + + header { + position: relative; + width: fit-content; + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + background: var(--tui-clear); + + &::before { + position: absolute; + top: 0; + left: 100%; + content: ''; + border-left: 1rem solid var(--tui-clear); + border-bottom: 1.75rem solid transparent; + } + } + + :host-context(tui-root._mobile) { + min-height: 8rem; + min-width: 100%; + border-radius: 0; + } + `, +}) +export class MetricComponent { + @Input() label = '' +} diff --git a/web/projects/ui/src/app/routes/portal/routes/dashboard/metrics.component.ts b/web/projects/ui/src/app/routes/portal/routes/dashboard/metrics.component.ts index ce38e16b1..5ee1a78e7 100644 --- a/web/projects/ui/src/app/routes/portal/routes/dashboard/metrics.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/dashboard/metrics.component.ts @@ -1,11 +1,74 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { TuiProgressModule } from '@taiga-ui/kit' +import { CpuComponent } from 'src/app/routes/portal/routes/dashboard/cpu.component' +import { MetricComponent } from 'src/app/routes/portal/routes/dashboard/metric.component' +import { TemperatureComponent } from 'src/app/routes/portal/routes/dashboard/temperature.component' +import { Metrics } from 'src/app/services/api/api.types' @Component({ standalone: true, selector: 'app-metrics', template: ` -
TODO
+
+ + +
+
+ + {{ getValue(metrics?.disk?.used?.value) }} + + Used +
+
+
+ + {{ getValue(metrics?.disk?.available?.value) }} + + Available +
+
+
+ + + + + +
+
+ + {{ getValue(metrics?.memory?.used?.value) }} + + Used +
+
+
+ + {{ getValue(metrics?.memory?.available?.value) }} + + Available +
+
+
+ +
`, styles: ` :host { @@ -28,20 +91,126 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' 1.25rem calc(100% - 2rem), 0 calc(100% - 4rem) ); + } - section { - height: 80%; - display: flex; - align-items: center; - justify-content: center; + section { + height: 80%; + display: flex; + padding: 1rem 1.5rem 0.5rem; + gap: 1rem; + } + + aside { + display: flex; + flex: 1; + flex-direction: column; + gap: 1rem; + margin-top: -1.5rem; + } + + footer { + display: flex; + white-space: nowrap; + background: var(--tui-clear); + } + + label { + margin: auto; + text-align: center; + padding: 0.375rem 0; + } + + progress { + height: 1.5rem; + width: 80%; + margin: auto; + border-radius: 0; + clip-path: none; + mask: linear-gradient(to right, #000 80%, transparent 80%); + mask-size: 5% 100%; + } + + hr { + height: 100%; + width: 1px; + margin: 0; + background: rgba(0, 0, 0, 0.1); + } + + div { + display: flex; + flex-direction: column; + flex: 1; + text-align: center; + text-transform: uppercase; + color: var(--tui-text-02); + font-size: 0.5rem; + line-height: 1rem; + + span { + font-size: 0.75rem; + font-weight: bold; + color: var(--tui-text-01); + padding-top: 0.4rem; + + &:after { + content: attr(data-unit); + font-size: 0.5rem; + font-weight: normal; + color: var(--tui-text-02); + } } } :host-context(tui-root._mobile) { --clip-path: none !important; - height: 100%; + min-height: 100%; + + section { + flex-wrap: wrap; + padding: 1rem; + } + + aside { + order: -1; + flex-direction: row; + margin: 0; + } + + app-metric { + min-width: calc(50% - 0.5rem); + + &.wide { + min-width: 100%; + } + } } `, changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + TuiProgressModule, + MetricComponent, + TemperatureComponent, + CpuComponent, + ], }) -export class MetricsComponent {} +export class MetricsComponent { + @Input({ required: true }) + metrics: Metrics | null = null + + get cpu(): number { + return Number(this.metrics?.cpu.percentageUsed.value || 0) / 100 + } + + get temperature(): number { + return Number(this.metrics?.general.temperature?.value || 0) + } + + get memory(): number { + return Number(this.metrics?.memory?.percentageUsed?.value) || 0 + } + + getValue(value?: string | null): number | string | undefined { + return value == null ? '-' : Number.parseInt(value) + } +} diff --git a/web/projects/ui/src/app/routes/portal/routes/dashboard/temperature.component.ts b/web/projects/ui/src/app/routes/portal/routes/dashboard/temperature.component.ts new file mode 100644 index 000000000..4b0b130d7 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/routes/dashboard/temperature.component.ts @@ -0,0 +1,89 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' + +@Component({ + standalone: true, + selector: 'app-temperature', + template: ` + + + + + + + + + + + + + + + {{ value || '-' }} C° + `, + styles: ` + @import '@taiga-ui/core/styles/taiga-ui-local'; + + :host { + height: 100%; + display: flex; + align-items: center; + margin: auto; + } + + svg { + width: auto; + height: 75%; + } + + span { + width: 4rem; + flex-shrink: 0; + white-space: nowrap; + text-align: center; + } + + .bar { + @include transition(clip-path); + clip-path: inset(var(--fill) 0 0 0); + } + `, + host: { + '[style.--fill.%]': '100 - value', + }, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TemperatureComponent { + @Input() + value = 0 +} diff --git a/web/projects/ui/src/app/routes/portal/routes/dashboard/utilities.component.ts b/web/projects/ui/src/app/routes/portal/routes/dashboard/utilities.component.ts index 1e3c1871e..d18ffc89e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/dashboard/utilities.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/dashboard/utilities.component.ts @@ -100,10 +100,12 @@ import { BadgeService } from 'src/app/services/badge.service' export class UtilitiesComponent { private readonly badge = inject(BadgeService) readonly items = Object.keys(SYSTEM_UTILITIES) - .filter(key => key !== '/portal/system/notifications') + .filter(key => !SKIPPED.includes(key)) .map(key => ({ ...SYSTEM_UTILITIES[key], routerLink: key, notification$: this.badge.getCount(key), })) } + +const SKIPPED = ['/portal/system/notifications', '/portal/system/metrics'] diff --git a/web/projects/ui/src/app/services/breadcrumbs.service.ts b/web/projects/ui/src/app/services/breadcrumbs.service.ts index 7a7346263..193ebf0c0 100644 --- a/web/projects/ui/src/app/services/breadcrumbs.service.ts +++ b/web/projects/ui/src/app/services/breadcrumbs.service.ts @@ -41,9 +41,6 @@ function toBreadcrumbs( id: string, packages: Record = {}, ): Breadcrumb[] { - const item = SYSTEM_UTILITIES[id] - const routerLink = toRouterLink(id) - if (id.startsWith('/portal/system/')) { const [page, ...path] = id.replace('/portal/system/', '').split('/') const service = `/portal/system/${page}` diff --git a/web/projects/ui/src/app/utils/system-utilities.ts b/web/projects/ui/src/app/utils/system-utilities.ts index e29bc1059..07a988226 100644 --- a/web/projects/ui/src/app/utils/system-utilities.ts +++ b/web/projects/ui/src/app/utils/system-utilities.ts @@ -4,6 +4,10 @@ export const SYSTEM_UTILITIES: Record = icon: 'tuiIconSave', title: 'Backups', }, + '/portal/system/metrics': { + icon: 'tuiIconActivity', + title: 'Metrics', + }, '/portal/system/logs': { icon: 'tuiIconFileText', title: 'Logs',