mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
feat: refactor metrics (#2861)
This commit is contained in:
@@ -1,23 +1,39 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { ServerMetrics } from 'src/app/services/api/api.types'
|
||||
import { DataComponent } from './data.component'
|
||||
|
||||
const LABELS = {
|
||||
percentageUsed: 'Percentage Used',
|
||||
userSpace: 'User Space',
|
||||
kernelSpace: 'Kernel Space',
|
||||
idle: 'Idle',
|
||||
wait: 'I/O Wait',
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-cpu',
|
||||
selector: 'metrics-cpu',
|
||||
template: `
|
||||
<div class="meter"></div>
|
||||
<div class="arrow" [style.transform]="transform"></div>
|
||||
<div class="percent">{{ percent }}%</div>
|
||||
<div class="cpu">
|
||||
<div class="meter"></div>
|
||||
<div class="arrow" [style.transform]="transform()"></div>
|
||||
<div class="percent">{{ value()?.percentageUsed?.value }}%</div>
|
||||
</div>
|
||||
<metrics-data [labels]="labels" [value]="value()" />
|
||||
`,
|
||||
styles: `
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
:host {
|
||||
.cpu {
|
||||
position: relative;
|
||||
margin: 1rem auto;
|
||||
width: 80%;
|
||||
width: 7rem;
|
||||
aspect-ratio: 1;
|
||||
background: var(--tui-background-neutral-1);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.meter {
|
||||
@@ -37,6 +53,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
bottom: 10%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font: var(--tui-font-text-l);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
@@ -66,16 +83,15 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [DataComponent],
|
||||
})
|
||||
export class CpuComponent {
|
||||
@Input()
|
||||
value = 0
|
||||
readonly value = input<ServerMetrics['cpu']>()
|
||||
|
||||
get percent(): string {
|
||||
return (100 * this.value).toFixed(1)
|
||||
}
|
||||
readonly transform = computed(
|
||||
(value = this.value()?.percentageUsed?.value || '0') =>
|
||||
`rotate(${60 + (300 * Number.parseFloat(value)) / 100}deg)`,
|
||||
)
|
||||
|
||||
get transform(): string {
|
||||
return `rotate(${60 + 300 * this.value}deg)`
|
||||
}
|
||||
readonly labels = LABELS
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { ServerMetrics } from 'src/app/services/api/api.types'
|
||||
import { ValuePipe } from './value.pipe'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'metrics-data',
|
||||
template: `
|
||||
@for (key of keys(); track $index) {
|
||||
<div tuiCell="m">
|
||||
<span tuiTitle>{{ labels()[key] }}</span>
|
||||
<span tuiTitle [attr.data-unit]="$any(value()?.[key])?.unit">
|
||||
{{ $any(value()?.[key])?.value | value }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
[tuiTitle] {
|
||||
&:first-child {
|
||||
color: var(--tui-text-tertiary);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
flex-direction: row;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: attr(data-unit);
|
||||
font: var(--tui-font-text-ui-xs);
|
||||
color: var(--tui-text-secondary);
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiCell, TuiTitle, ValuePipe],
|
||||
})
|
||||
export class DataComponent<T extends ServerMetrics[keyof ServerMetrics]> {
|
||||
readonly labels = input.required<Record<keyof T, string>>()
|
||||
readonly value = input<T>()
|
||||
readonly keys = computed(() => Object.keys(this.labels()) as Array<keyof T>)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { TuiProgress } from '@taiga-ui/kit'
|
||||
import { ServerMetrics } from 'src/app/services/api/api.types'
|
||||
import { DataComponent } from './data.component'
|
||||
import { ValuePipe } from './value.pipe'
|
||||
|
||||
const LABELS = {
|
||||
percentageUsed: 'Percentage Used',
|
||||
total: 'Total',
|
||||
used: 'Used',
|
||||
available: 'Available',
|
||||
zramUsed: 'zram Used',
|
||||
zramTotal: 'zram Total',
|
||||
zramAvailable: 'zram Available',
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'metrics-memory',
|
||||
template: `
|
||||
<label tuiProgressLabel>
|
||||
<tui-progress-circle size="l" [max]="100" [value]="used()" />
|
||||
{{ value()?.percentageUsed?.value | value }}%
|
||||
</label>
|
||||
<metrics-data [labels]="labels" [value]="value()" />
|
||||
`,
|
||||
styles: `
|
||||
label {
|
||||
margin: 2rem auto;
|
||||
display: block;
|
||||
width: fit-content;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [DataComponent, TuiProgress, ValuePipe],
|
||||
})
|
||||
export class MemoryComponent {
|
||||
readonly value = input<ServerMetrics['memory']>()
|
||||
|
||||
readonly used = computed(
|
||||
(value = this.value()?.percentageUsed.value || '0') =>
|
||||
Number.parseFloat(value),
|
||||
)
|
||||
|
||||
readonly labels = LABELS
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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-background-neutral-1);
|
||||
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-background-neutral-1);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
content: '';
|
||||
border-left: 1rem solid var(--tui-background-neutral-1);
|
||||
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 = ''
|
||||
}
|
||||
@@ -1,164 +1,84 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { TuiProgress } from '@taiga-ui/kit'
|
||||
import { CpuComponent } from 'src/app/routes/portal/routes/metrics/cpu.component'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { TemperatureComponent } from 'src/app/routes/portal/routes/metrics/temperature.component'
|
||||
import { MetricComponent } from 'src/app/routes/portal/routes/metrics/metric.component'
|
||||
import { MetricsService } from 'src/app/routes/portal/routes/metrics/metrics.service'
|
||||
import { TimeService } from 'src/app/services/time.service'
|
||||
import { CpuComponent } from './cpu.component'
|
||||
import { MemoryComponent } from './memory.component'
|
||||
import { MetricsService } from './metrics.service'
|
||||
import { StorageComponent } from './storage.component'
|
||||
import { TemperatureComponent } from './temperature.component'
|
||||
import { TimeComponent } from './time.component'
|
||||
import { UptimeComponent } from './uptime.component'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-metrics',
|
||||
template: `
|
||||
<ng-container *title>Metrics</ng-container>
|
||||
<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>
|
||||
{{ uptime() }}
|
||||
<div>Days : Hrs : Mins : Secs</div>
|
||||
</label>
|
||||
</app-metric>
|
||||
<app-metric label="Temperature">
|
||||
<app-temperature [value]="temperature" />
|
||||
</app-metric>
|
||||
</aside>
|
||||
</section>
|
||||
<div>
|
||||
<section class="g-card">
|
||||
<header>System Time</header>
|
||||
<metrics-time />
|
||||
</section>
|
||||
<section class="g-card">
|
||||
<header>Uptime</header>
|
||||
<metrics-uptime />
|
||||
</section>
|
||||
<section class="g-card">
|
||||
<header>Temperature</header>
|
||||
<metrics-temperature [value]="temperature()" />
|
||||
</section>
|
||||
<section class="g-card">
|
||||
<header>CPU</header>
|
||||
<metrics-cpu [value]="metrics()?.cpu" />
|
||||
</section>
|
||||
<section class="g-card">
|
||||
<header>Memory</header>
|
||||
<metrics-memory [value]="metrics()?.memory" />
|
||||
</section>
|
||||
<section class="g-card">
|
||||
<header>Storage</header>
|
||||
<metrics-storage [value]="metrics()?.disk" />
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
section {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1rem 0;
|
||||
}
|
||||
:host {
|
||||
padding: 1rem;
|
||||
|
||||
aside {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
div {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-auto-flow: dense;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
background: var(--tui-background-neutral-1);
|
||||
}
|
||||
|
||||
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-secondary);
|
||||
font-size: 0.5rem;
|
||||
line-height: 1rem;
|
||||
|
||||
span {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
color: var(--tui-text-primary);
|
||||
padding-top: 0.4rem;
|
||||
|
||||
&::after {
|
||||
content: attr(data-unit);
|
||||
font-size: 0.5rem;
|
||||
font-weight: normal;
|
||||
color: var(--tui-text-secondary);
|
||||
}
|
||||
header {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
section {
|
||||
min-height: 100%;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -2rem -2rem;
|
||||
}
|
||||
.g-card {
|
||||
grid-column: span 3;
|
||||
|
||||
aside {
|
||||
order: -1;
|
||||
flex-direction: row;
|
||||
}
|
||||
&:nth-child(1),
|
||||
&:nth-child(2) {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
app-metric {
|
||||
min-width: calc(50% - 0.5rem);
|
||||
&:nth-child(3) {
|
||||
grid-column: span 1;
|
||||
grid-row: span 2;
|
||||
padding-top: 0;
|
||||
|
||||
&.wide {
|
||||
min-width: 100%;
|
||||
header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,30 +86,19 @@ import { TimeService } from 'src/app/services/time.service'
|
||||
host: { class: 'g-page' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiProgress,
|
||||
MetricComponent,
|
||||
TemperatureComponent,
|
||||
StorageComponent,
|
||||
CpuComponent,
|
||||
TitleDirective,
|
||||
MemoryComponent,
|
||||
UptimeComponent,
|
||||
TimeComponent,
|
||||
],
|
||||
})
|
||||
export default class SystemMetricsComponent {
|
||||
readonly metrics = toSignal(inject(MetricsService))
|
||||
readonly uptime = toSignal(inject(TimeService).uptime$)
|
||||
|
||||
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)
|
||||
}
|
||||
readonly temperature = computed(() =>
|
||||
Number(this.metrics()?.general.temperature?.value || 0),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { TuiProgress } from '@taiga-ui/kit'
|
||||
import { ServerMetrics } from 'src/app/services/api/api.types'
|
||||
import { DataComponent } from './data.component'
|
||||
|
||||
const LABELS = {
|
||||
percentageUsed: 'Percentage Used',
|
||||
capacity: 'Capacity',
|
||||
used: 'Used',
|
||||
available: 'Available',
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'metrics-storage',
|
||||
template: `
|
||||
<progress
|
||||
tuiProgressBar
|
||||
[max]="100"
|
||||
[attr.value]="value()?.percentageUsed?.value"
|
||||
></progress>
|
||||
<metrics-data [labels]="labels" [value]="value()" />
|
||||
`,
|
||||
styles: `
|
||||
progress {
|
||||
height: 1.5rem;
|
||||
width: 80%;
|
||||
margin: 3.75rem auto;
|
||||
border-radius: 0;
|
||||
clip-path: none;
|
||||
mask: linear-gradient(to right, #000 80%, transparent 80%);
|
||||
mask-size: 5% 100%;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiProgress, DataComponent],
|
||||
})
|
||||
export class StorageComponent {
|
||||
readonly value = input<ServerMetrics['disk']>()
|
||||
|
||||
readonly used = computed(
|
||||
(
|
||||
capacity = this.value()?.capacity?.value,
|
||||
used = this.value()?.used?.value,
|
||||
) =>
|
||||
capacity && used
|
||||
? (
|
||||
(Number.parseFloat(used) / Number.parseFloat(capacity)) *
|
||||
100
|
||||
).toFixed(1)
|
||||
: '-',
|
||||
)
|
||||
|
||||
readonly labels = LABELS
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-temperature',
|
||||
selector: 'metrics-temperature',
|
||||
template: `
|
||||
<svg viewBox="0 0 43 95" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@@ -12,10 +12,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
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"
|
||||
@@ -33,6 +29,10 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
class="bar"
|
||||
fill="url(#temperature)"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="temperature"
|
||||
@@ -49,7 +49,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>{{ value || '-' }} C°</span>
|
||||
<b>{{ value() || '-' }} C°</b>
|
||||
`,
|
||||
styles: `
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
@@ -57,13 +57,16 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: auto;
|
||||
height: 75%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
span {
|
||||
@@ -79,11 +82,10 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
}
|
||||
`,
|
||||
host: {
|
||||
'[style.--fill.%]': '100 - value',
|
||||
'[style.--fill.%]': '100 - value()',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TemperatureComponent {
|
||||
@Input()
|
||||
value = 0
|
||||
readonly value = input.required<number>()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { TuiNotification, TuiTitle } from '@taiga-ui/core'
|
||||
import { TimeService } from 'src/app/services/time.service'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'metrics-time',
|
||||
template: `
|
||||
@if (now(); as time) {
|
||||
@if (!time.synced) {
|
||||
<tui-notification appearance="warning">
|
||||
NTP not synced, time could be wrong
|
||||
</tui-notification>
|
||||
}
|
||||
<div tuiTitle>
|
||||
<div tuiSubtitle class="g-secondary">
|
||||
{{ time.now | date: 'h:mm a z' : 'UTC' }}
|
||||
</div>
|
||||
<b>{{ time.now | date: 'MMMM d, y' : 'UTC' }}</b>
|
||||
</div>
|
||||
} @else {
|
||||
Loading...
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
[tuiTitle] {
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiNotification, DatePipe, TuiTitle],
|
||||
})
|
||||
export class TimeComponent {
|
||||
readonly now = toSignal(inject(TimeService).now$)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { TimeService } from 'src/app/services/time.service'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'metrics-uptime',
|
||||
template: `
|
||||
@if (uptime(); as time) {
|
||||
<div>
|
||||
<b>{{ time.days }}</b>
|
||||
Days
|
||||
</div>
|
||||
<div>
|
||||
<b>{{ time.hours }}</b>
|
||||
Hours
|
||||
</div>
|
||||
<div>
|
||||
<b>{{ time.minutes }}</b>
|
||||
Minutes
|
||||
</div>
|
||||
<div>
|
||||
<b>{{ time.seconds }}</b>
|
||||
Seconds
|
||||
</div>
|
||||
} @else {
|
||||
Loading...
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
b {
|
||||
display: block;
|
||||
font: var(--tui-font-heading-5);
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
font: var(--tui-font-text-ui-xs);
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UptimeComponent {
|
||||
readonly uptime = toSignal(inject(TimeService).uptime$)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
|
||||
@Pipe({
|
||||
standalone: true,
|
||||
name: 'value',
|
||||
})
|
||||
export class ValuePipe implements PipeTransform {
|
||||
readonly transform = getValue
|
||||
}
|
||||
|
||||
export function getValue(value?: string | null): number | string {
|
||||
return value == null ? '-' : Number.parseFloat(value)
|
||||
}
|
||||
@@ -123,6 +123,10 @@ export class MockApiService extends ApiService {
|
||||
return from(this.initProgress()).pipe(
|
||||
startWith(PROGRESS),
|
||||
) as WebSocketSubject<T>
|
||||
} else if (guid === 'metrics-guid') {
|
||||
return interval(1000).pipe(
|
||||
map(() => Mock.getMetrics()),
|
||||
) as WebSocketSubject<T>
|
||||
} else {
|
||||
throw new Error('invalid guid type')
|
||||
}
|
||||
@@ -358,7 +362,7 @@ export class MockApiService extends ApiService {
|
||||
): Promise<RR.FollowServerMetricsRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
guid: 'iqudh37um-i38u3-34-a51b-jkhd783ein',
|
||||
guid: 'metrics-guid',
|
||||
metrics: Mock.getMetrics(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
combineLatest,
|
||||
defer,
|
||||
map,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import { combineLatest, defer, map, shareReplay, switchMap, timer } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@@ -44,9 +36,7 @@ export class TimeService {
|
||||
const hoursSec = uptime % (60 * 60)
|
||||
const minutes = Math.floor(hoursSec / 60)
|
||||
const seconds = uptime % 60
|
||||
|
||||
return `${days}:${hours}:${minutes}:${seconds}`
|
||||
return { days, hours, minutes, seconds }
|
||||
}),
|
||||
startWith('-:-:-:-'),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user