feat: implement metrics on the dashboard

This commit is contained in:
waterplea
2024-04-18 13:39:49 +07:00
parent 2693b9a42d
commit 8b89e03999
9 changed files with 528 additions and 17 deletions

View File

@@ -0,0 +1,114 @@
<svg viewBox="0 0 121 121" xmlns="http://www.w3.org/2000/svg">
<path
d="M111.835 90.6061C112.091 90.1731 112.341 89.7369 112.585 89.2976L105.865 85.5584C105.651 85.9415 105.433 86.3218 105.211 86.6994L111.835 90.6061Z"
/>
<path
d="M111.921 82.3126C111.976 82.1863 112.03 82.0598 112.084 81.9331L108.558 80.4304C108.507 80.5484 108.457 80.6663 108.405 80.7839L111.921 82.3126Z"
/>
<path
d="M114.196 76.1002C114.236 75.9683 114.275 75.8363 114.314 75.7041L110.635 74.6273C110.599 74.7505 110.563 74.8735 110.526 74.9963L114.196 76.1002Z"
/>
<path
d="M115.711 69.7413C115.735 69.6057 115.758 69.47 115.781 69.3342L112.002 68.6929C111.981 68.8194 111.959 68.9458 111.936 69.0722L115.711 69.7413Z"
/>
<path
d="M116.477 63.2023C116.485 63.0648 116.493 62.9273 116.5 62.7898L112.671 62.596C112.665 62.7241 112.658 62.8522 112.65 62.9803L116.477 63.2023Z"
/>
<path
d="M116.48 56.7558C116.472 56.6183 116.464 56.4808 116.455 56.3434L112.63 56.5904C112.638 56.7184 112.646 56.8465 112.653 56.9745L116.48 56.7558Z"
/>
<path
d="M114.205 43.838C114.165 43.7061 114.125 43.5743 114.084 43.4427L110.421 44.5718C110.459 44.6944 110.496 44.8171 110.533 44.94L114.205 43.838Z"
/>
<path
d="M111.978 37.7306C111.923 37.6042 111.868 37.4779 111.813 37.3519L108.305 38.8974C108.356 39.0148 108.408 39.1324 108.459 39.2502L111.978 37.7306Z"
/>
<path
d="M109.046 31.8987C108.977 31.7795 108.907 31.6605 108.837 31.5418L105.533 33.4846C105.598 33.5952 105.663 33.706 105.727 33.8171L109.046 31.8987Z"
/>
<path
d="M105.478 26.4774C105.396 26.367 105.313 26.2568 105.23 26.1469L102.172 28.4586C102.249 28.561 102.326 28.6636 102.403 28.7665L105.478 26.4774Z"
/>
<path
d="M101.301 21.4976C101.206 21.3974 101.111 21.2976 101.016 21.1981L98.2465 23.8481C98.3352 23.9408 98.4236 24.0338 98.5116 24.1272L101.301 21.4976Z"
/>
<path
d="M91.3072 13.1107C91.1921 13.035 91.0768 12.9598 90.9611 12.885L88.879 16.1035C88.9867 16.1731 89.0942 16.2432 89.2014 16.3137L91.3072 13.1107Z"
/>
<path
d="M85.6651 9.85432C85.5421 9.79252 85.4188 9.73118 85.2952 9.6703L83.6005 13.1086C83.7156 13.1653 83.8305 13.2224 83.9451 13.28L85.6651 9.85432Z"
/>
<path
d="M79.6855 7.27516C79.5561 7.22805 79.4265 7.18142 79.2967 7.13527L78.0121 10.7469C78.133 10.7899 78.2537 10.8333 78.3743 10.8772L79.6855 7.27516Z"
/>
<path
d="M73.4811 5.41491C73.3471 5.38304 73.213 5.35167 73.0788 5.32079L72.2193 9.05647C72.3444 9.08523 72.4693 9.11446 72.5941 9.14414L73.4811 5.41491Z"
/>
<path
d="M67.1241 4.28516C66.9874 4.26891 66.8505 4.25317 66.7137 4.23793L66.2894 8.04764C66.417 8.06184 66.5444 8.07651 66.6718 8.09164L67.1241 4.28516Z"
/>
<path
d="M53.9975 4.27302C53.8607 4.28902 53.724 4.30552 53.5873 4.32253L54.0606 8.12645C54.188 8.11061 54.3153 8.09524 54.4428 8.08034L53.9975 4.27302Z"
/>
<path
d="M47.715 5.37301C47.581 5.40444 47.447 5.43636 47.3131 5.46878L48.2154 9.19433C48.3401 9.16413 48.4649 9.13439 48.5899 9.10511L47.715 5.37301Z"
/>
<path
d="M41.4231 7.24251C41.2936 7.28939 41.1643 7.33676 41.0352 7.3846L42.3667 10.9792C42.487 10.9346 42.6075 10.8905 42.7282 10.8468L41.4231 7.24251Z"
/>
<path
d="M35.4375 9.81224C35.3143 9.87383 35.1914 9.93587 35.0687 9.99837L36.8082 13.4142C36.9225 13.356 37.037 13.2982 37.1518 13.2408L35.4375 9.81224Z"
/>
<path
d="M29.7994 13.053C29.6842 13.1284 29.5692 13.2043 29.4546 13.2806L31.578 16.472C31.6848 16.4009 31.7919 16.3303 31.8992 16.26L29.7994 13.053Z"
/>
<path
d="M19.8792 21.3273C19.7843 21.4271 19.6898 21.5273 19.5956 21.6278L22.3931 24.2485C22.4808 24.1548 22.5689 24.0615 22.6573 23.9685L19.8792 21.3273Z"
/>
<path
d="M15.6658 26.3102C15.5832 26.4204 15.5009 26.5308 15.4191 26.6416L18.5021 28.9195C18.5784 28.8163 18.655 28.7134 18.732 28.6107L15.6658 26.3102Z"
/>
<path
d="M12.0819 31.7114C12.0125 31.8303 11.9436 31.9496 11.8751 32.069L15.2004 33.9758C15.2643 33.8645 15.3285 33.7534 15.3931 33.6426L12.0819 31.7114Z"
/>
<path
d="M9.14535 37.4902C9.09017 37.6163 9.03545 37.7427 8.98119 37.8693L12.5044 39.3795C12.555 39.2615 12.606 39.1438 12.6574 39.0262L9.14535 37.4902Z"
/>
<path
d="M6.88003 43.62C6.8399 43.7517 6.80026 43.8836 6.7611 44.0156L10.4362 45.1055C10.4726 44.9825 10.5096 44.8597 10.547 44.7369L6.88003 43.62Z"
/>
<path
d="M4.56455 56.3311C4.55565 56.4685 4.54725 56.606 4.53936 56.7435L8.36632 56.9631C8.37368 56.835 8.3815 56.707 8.38979 56.5789L4.56455 56.3311Z"
/>
<path
d="M4.52264 62.8617C4.52978 62.9993 4.53742 63.1368 4.54558 63.2742L8.37211 63.0473C8.36452 62.9193 8.3574 62.7911 8.35074 62.663L4.52264 62.8617Z"
/>
<path
d="M5.24339 69.37C5.26652 69.5058 5.29014 69.6415 5.31427 69.7771L9.08825 69.1055C9.06577 68.9792 9.04376 68.8528 9.02222 68.7263L5.24339 69.37Z"
/>
<path
d="M6.71719 75.7468C6.75598 75.8789 6.79526 76.0109 6.83502 76.1428L10.505 75.036C10.468 74.9131 10.4314 74.7902 10.3952 74.6671L6.71719 75.7468Z"
/>
<path
d="M8.87695 81.7971C8.9306 81.924 8.98472 82.0506 9.03931 82.1771L12.5586 80.6577C12.5077 80.5399 12.4573 80.4219 12.4073 80.3037L8.87695 81.7971Z"
/>
<path
d="M119.372 50.3842C119.291 49.8881 119.204 49.393 119.11 48.8991L111.554 50.3324C111.636 50.7631 111.712 51.1948 111.783 51.6274L119.372 50.3842Z"
/>
<path
d="M99.4634 14.7267C99.0837 14.3972 98.6999 14.0725 98.312 13.7527L93.4189 19.6861C93.7571 19.965 94.0918 20.2481 94.4229 20.5354L99.4634 14.7267Z"
/>
<path
d="M61.2735 0.038891C60.7709 0.0290338 60.2682 0.0254873 59.7654 0.0282522L59.8077 7.71888C60.2461 7.71647 60.6845 7.71956 61.1227 7.72816L61.2735 0.038891Z"
/>
<path
d="M22.2535 13.4468C21.8631 13.7636 21.4766 14.0852 21.0943 14.4116L26.0879 20.2606C26.4213 19.976 26.7583 19.6956 27.0987 19.4194L22.2535 13.4468Z"
/>
<path
d="M1.07789 48.9193C0.984365 49.4132 0.897043 49.9083 0.815948 50.4045L8.40597 51.6451C8.47668 51.2125 8.55282 50.7807 8.63437 50.35L1.07789 48.9193Z"
/>
<path
d="M7.74081 89.5377C7.98724 89.9759 8.23916 90.4109 8.49652 90.8428L15.1031 86.9058C14.8787 86.5292 14.6591 86.1499 14.4442 85.7678L7.74081 89.5377Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,81 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
@Component({
standalone: true,
selector: 'app-cpu',
template: `
<div class="meter"></div>
<div class="arrow" [style.transform]="transform"></div>
<div class="percent">{{ percent }}%</div>
`,
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)`
}
}

View File

@@ -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: `
<time>{{ date() | date: 'medium' }}</time>
<app-metrics>
<app-metrics [metrics]="metrics$ | async">
<h2>
<tui-icon icon="tuiIconActivity" />
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: '' })
}

View File

@@ -0,0 +1,48 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
@Component({
standalone: true,
selector: 'app-metric',
template: `
<header>{{ label }}</header>
<ng-content />
`,
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 = ''
}

View File

@@ -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: `
<ng-content />
<section>TODO</section>
<section>
<app-metric class="wide" label="Storage" [style.max-height.%]="85">
<progress
tuiProgressBar
[max]="100"
[attr.value]="metrics?.disk?.percentageUsed?.value"
></progress>
<footer>
<div>
<span [attr.data-unit]="metrics?.disk?.used?.unit">
{{ getValue(metrics?.disk?.used?.value) }}
</span>
Used
</div>
<hr />
<div>
<span [attr.data-unit]="metrics?.disk?.available?.unit">
{{ getValue(metrics?.disk?.available?.value) }}
</span>
Available
</div>
</footer>
</app-metric>
<app-metric label="CPU">
<app-cpu [value]="cpu" />
</app-metric>
<app-metric label="Memory">
<label tuiProgressLabel>
<tui-progress-circle size="l" [max]="100" [value]="memory" />
{{ metrics?.memory?.percentageUsed?.value || ' - ' }}%
</label>
<footer>
<div>
<span [attr.data-unit]="metrics?.memory?.used?.unit">
{{ getValue(metrics?.memory?.used?.value) }}
</span>
Used
</div>
<hr />
<div>
<span [attr.data-unit]="metrics?.memory?.available?.unit">
{{ getValue(metrics?.memory?.available?.value) }}
</span>
Available
</div>
</footer>
</app-metric>
<aside>
<app-metric label="Uptime" [style.flex]="'unset'">
<label>
-:-:-:-
<div>Days : Hrs : Mins : Secs</div>
</label>
</app-metric>
<app-metric label="Temperature">
<app-temperature [value]="temperature" />
</app-metric>
</aside>
</section>
`,
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)
}
}

View File

@@ -0,0 +1,89 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
@Component({
standalone: true,
selector: 'app-temperature',
template: `
<svg viewBox="0 0 43 95" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M31.7543 56.2935C31.5285 56.1505 31.3425 55.9529 31.2135 55.7188C31.0846 55.4848 31.0168 55.2219 31.0165 54.9547V12.2071C31.0165 9.68211 30.0134 7.26051 28.228 5.47505C26.4425 3.68959 24.0209 2.68652 21.4959 2.68652C18.9708 2.68652 16.5492 3.68959 14.7638 5.47505C12.9783 7.26051 11.9753 9.68211 11.9753 12.2071V54.9547C11.9748 55.2214 11.9072 55.4837 11.7786 55.7174C11.65 55.951 11.4645 56.1485 11.2394 56.2915C8.41934 58.1285 6.12747 60.6696 4.59028 63.6636C3.05309 66.6576 2.32379 70.001 2.47448 73.3632C2.70143 78.3319 4.86361 83.0146 8.49859 86.4098C12.1336 89.8049 16.9527 91.6429 21.9254 91.5307C26.8981 91.4185 31.6294 89.365 35.1076 85.8094C38.5857 82.2537 40.5345 77.4783 40.5371 72.5043C40.5385 69.2847 39.7359 66.1157 38.2022 63.2848C36.6684 60.4539 34.4521 58.0508 31.7543 56.2935Z"
stroke="var(--tui-clear)"
stroke-width="4"
stroke-miterlimit="10"
stroke-linecap="round"
/>
<path
d="M21.5011 82.0256C26.7592 82.0256 31.0217 77.7631 31.0217 72.505C31.0217 67.2469 26.7592 62.9844 21.5011 62.9844C16.243 62.9844 11.9805 67.2469 11.9805 72.505C11.9805 77.7631 16.243 82.0256 21.5011 82.0256Z"
fill="#3853E3"
/>
<rect
x="18"
y="10"
width="7"
height="60"
rx="3"
fill="var(--tui-clear)"
/>
<rect
x="18"
y="10"
width="7"
height="60"
rx="3"
class="bar"
fill="url(#temperature)"
/>
<defs>
<linearGradient
id="temperature"
x1="21"
y1="13"
x2="21"
y2="66"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#EC2E34" />
<stop offset="0.35" stop-color="#C48510" />
<stop offset="0.65" stop-color="#00A030" />
<stop offset="1" stop-color="#325CE3" />
</linearGradient>
</defs>
</svg>
<span>{{ value || '-' }} C°</span>
`,
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
}

View File

@@ -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']

View File

@@ -41,9 +41,6 @@ function toBreadcrumbs(
id: string,
packages: Record<string, PackageDataEntry> = {},
): 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}`

View File

@@ -4,6 +4,10 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
icon: 'tuiIconSave',
title: 'Backups',
},
'/portal/system/metrics': {
icon: 'tuiIconActivity',
title: 'Metrics',
},
'/portal/system/logs': {
icon: 'tuiIconFileText',
title: 'Logs',