Skip to content

Commit d7d7548

Browse files
feat[frontend](federation): fixed overview charts
1 parent 67c5c59 commit d7d7548

9 files changed

Lines changed: 138 additions & 112 deletions

frontend/src/app/federation/components/instance-overview-card/instance-overview-card.component.html

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,42 @@
11
<div class="overview-card">
22
<div class="overview-card__title">{{ entry.instanceName }}</div>
33

4-
<div class="overview-card__charts" *ngIf="hasData">
5-
<div class="overview-card__chart" *ngIf="statusOption" echarts [options]="statusOption"></div>
6-
<div class="overview-card__chart" *ngIf="severityOption" echarts [options]="severityOption"></div>
4+
<div class="overview-card__rows" *ngIf="hasData">
5+
<div class="overview-card__row">
6+
<div class="overview-card__row-label">Severity</div>
7+
<div class="overview-card__row-meters">
8+
<ngx-gauge *ngFor="let meter of severityMeters"
9+
[animate]="true"
10+
cap="round"
11+
[foregroundColor]="meter.color"
12+
[label]="meter.label"
13+
[max]="severityMax || 1"
14+
[min]="0"
15+
[thick]="6"
16+
type="full"
17+
[value]="meter.value"
18+
[size]="70"
19+
class="overview-card__gauge"></ngx-gauge>
20+
</div>
21+
</div>
22+
23+
<div class="overview-card__row">
24+
<div class="overview-card__row-label">Status</div>
25+
<div class="overview-card__row-meters">
26+
<ngx-gauge *ngFor="let meter of statusMeters"
27+
[animate]="true"
28+
[size]="70"
29+
cap="round"
30+
[foregroundColor]="meter.color"
31+
[label]="meter.label"
32+
[max]="statusMax || 1"
33+
[min]="0"
34+
[thick]="6"
35+
type="full"
36+
[value]="meter.value"
37+
class="overview-card__gauge"></ngx-gauge>
38+
</div>
39+
</div>
740
</div>
841

942
<div class="overview-card__empty" *ngIf="!hasData">

frontend/src/app/federation/components/instance-overview-card/instance-overview-card.component.scss

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,48 @@
1919
text-align: center;
2020
}
2121

22-
&__charts {
22+
&__rows {
2323
flex: 1 1 auto;
24+
display: flex;
25+
flex-direction: column;
26+
gap: 10px;
27+
}
28+
29+
&__row {
30+
display: flex;
31+
flex-direction: column;
32+
gap: 4px;
33+
}
34+
35+
&__row-label {
36+
font-size: 11px;
37+
font-weight: 600;
38+
color: #6b7280;
39+
text-transform: uppercase;
40+
letter-spacing: 0.04em;
41+
}
42+
43+
&__row-meters {
2444
display: grid;
25-
grid-template-columns: 1fr 1fr;
45+
grid-template-columns: repeat(3, 1fr);
2646
gap: 8px;
27-
min-height: 200px;
47+
min-width: 0;
48+
justify-items: center;
2849
}
2950

30-
&__chart {
51+
&__gauge {
52+
display: block;
3153
min-width: 0;
32-
min-height: 200px;
33-
height: 200px;
54+
55+
::ng-deep .reading-block {
56+
font-size: 14px !important;
57+
font-weight: 600;
58+
}
59+
60+
::ng-deep .reading-label {
61+
font-size: 10px !important;
62+
color: #475569;
63+
}
3464
}
3565

3666
&__empty {
Lines changed: 30 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,15 @@
11
import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from '@angular/core';
22
import {
33
AlertSeveritySerie,
4-
CountAlertsBySeverityBucket,
54
CountAlertsBySeverityEntry
65
} from '../../domain/count-alerts-by-severity.model';
76
import {AlertStatusSerie, CountAlertsByStatusEntry} from '../../domain/count-alerts-by-status.model';
87
import {FEDERATION_NAV_ACTIONS, NavAction} from '../../domain/federation-nav-actions';
98

10-
interface PieDatum {
11-
name: string;
9+
interface MeterDatum {
10+
label: string;
1211
value: number;
13-
itemStyle: { color: string };
14-
}
15-
16-
interface PieChartOption {
17-
tooltip: { trigger: string; formatter: string };
18-
legend: {
19-
orient: 'horizontal';
20-
bottom: number;
21-
data: string[];
22-
textStyle: { fontSize: number };
23-
};
24-
title: {
25-
text: string;
26-
left: string;
27-
top: number;
28-
textStyle: { fontSize: number; fontWeight: string };
29-
};
30-
series: Array<{
31-
name: string;
32-
type: 'pie';
33-
radius: [string, string];
34-
center: [string, string];
35-
data: PieDatum[];
36-
label: { show: boolean };
37-
labelLine: { show: boolean };
38-
}>;
12+
color: string;
3913
}
4014

4115
const STATUS_COLORS: Record<AlertStatusSerie, string> = {
@@ -51,6 +25,7 @@ const SEVERITY_COLORS: Record<AlertSeveritySerie, string> = {
5125
};
5226

5327
const SEVERITY_ORDER: ReadonlyArray<AlertSeveritySerie> = ['High', 'Medium', 'Low'];
28+
const STATUS_ORDER: ReadonlyArray<AlertStatusSerie> = ['Open', 'In review', 'Completed'];
5429

5530
@Component({
5631
selector: 'app-instance-overview-card',
@@ -63,23 +38,40 @@ export class InstanceOverviewCardComponent implements OnChanges {
6338
@Output() select = new EventEmitter<string>();
6439

6540
readonly actions: ReadonlyArray<NavAction> = FEDERATION_NAV_ACTIONS;
66-
statusOption: PieChartOption | null = null;
67-
severityOption: PieChartOption | null = null;
41+
severityMeters: MeterDatum[] = [];
42+
statusMeters: MeterDatum[] = [];
43+
severityMax = 0;
44+
statusMax = 0;
6845
totalAlerts = 0;
6946
totalSeverity = 0;
7047

7148
ngOnChanges(changes: SimpleChanges): void {
7249
if (changes.entry && this.entry) {
73-
console.log(this.entry)
74-
this.totalAlerts = this.entry.data.reduce((sum, bucket) => sum + bucket.value, 0);
75-
this.statusOption = this.totalAlerts > 0 ? this.buildStatusOption(this.entry) : null;
50+
const buckets = this.entry.data || [];
51+
this.totalAlerts = buckets.reduce((sum, bucket) => sum + bucket.value, 0);
52+
this.statusMeters = STATUS_ORDER.map(serie => {
53+
const bucket = buckets.find(b => b.serie === serie);
54+
return {
55+
label: serie,
56+
value: bucket ? bucket.value : 0,
57+
color: STATUS_COLORS[serie]
58+
};
59+
});
60+
this.statusMax = this.statusMeters.reduce((m, d) => m+d.value, 0);
7661
}
7762
if (changes.severityEntry) {
78-
const buckets = this.severityEntry ? this.severityEntry.data : [];
63+
const buckets = this.severityEntry ? this.severityEntry.data.value : [];
7964
this.totalSeverity = buckets.reduce((sum, bucket) => sum + bucket.value, 0);
80-
this.severityOption = this.totalSeverity > 0
81-
? this.buildSeverityOption(buckets)
82-
: null;
65+
this.severityMeters = SEVERITY_ORDER.map(serie => {
66+
const bucket = buckets.find(b => b.name === serie);
67+
console.log(bucket,name)
68+
return {
69+
label: serie,
70+
value: bucket ? bucket.value : 0,
71+
color: SEVERITY_COLORS[serie]
72+
};
73+
});
74+
this.severityMax = this.severityMeters.reduce((m, d) => Math.max(m, d.value), 0);
8375
}
8476
}
8577

@@ -90,52 +82,4 @@ export class InstanceOverviewCardComponent implements OnChanges {
9082
onSelect(route: string): void {
9183
this.select.emit(route);
9284
}
93-
94-
private buildStatusOption(entry: CountAlertsByStatusEntry): PieChartOption {
95-
const data: PieDatum[] = entry.data.map(bucket => ({
96-
name: bucket.serie,
97-
value: bucket.value,
98-
itemStyle: {color: STATUS_COLORS[bucket.serie] || '#6c757d'}
99-
}));
100-
return this.buildPieOption('Status', data, entry.data.map(bucket => bucket.serie));
101-
}
102-
103-
private buildSeverityOption(buckets: CountAlertsBySeverityBucket[]): PieChartOption {
104-
const ordered = SEVERITY_ORDER
105-
.map(serie => buckets.find(bucket => bucket.serie === serie))
106-
.filter((bucket): bucket is CountAlertsBySeverityBucket => !!bucket && bucket.value > 0);
107-
const data: PieDatum[] = ordered.map(bucket => ({
108-
name: bucket.serie,
109-
value: bucket.value,
110-
itemStyle: {color: SEVERITY_COLORS[bucket.serie] || '#6c757d'}
111-
}));
112-
return this.buildPieOption('Severity', data, ordered.map(bucket => bucket.serie));
113-
}
114-
115-
private buildPieOption(title: string, data: PieDatum[], legend: string[]): PieChartOption {
116-
return {
117-
tooltip: {trigger: 'item', formatter: '{b}: {c} ({d}%)'},
118-
legend: {
119-
orient: 'horizontal',
120-
bottom: 0,
121-
data: legend,
122-
textStyle: {fontSize: 11}
123-
},
124-
title: {
125-
text: title,
126-
left: 'center',
127-
top: 4,
128-
textStyle: {fontSize: 12, fontWeight: '600'}
129-
},
130-
series: [{
131-
name: title,
132-
type: 'pie',
133-
radius: ['45%', '70%'],
134-
center: ['50%', '55%'],
135-
data,
136-
label: {show: false},
137-
labelLine: {show: false}
138-
}]
139-
};
140-
}
14185
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
export type AlertSeveritySerie = 'Low' | 'Medium' | 'High';
22

33
export interface CountAlertsBySeverityBucket {
4-
serie: AlertSeveritySerie;
4+
name: AlertSeveritySerie;
55
value: number;
66
}
77

88
export interface CountAlertsBySeverityEntry {
99
instanceId: number;
1010
instanceName: string;
11-
data: CountAlertsBySeverityBucket[];
11+
data: {value:CountAlertsBySeverityBucket[]};
1212
}

frontend/src/app/federation/federation.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {RouterModule} from '@angular/router';
55
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
66
import {InlineSVGModule} from 'ng-inline-svg';
77
import {NgxEchartsModule} from 'ngx-echarts';
8+
import {NgxGaugeModule} from 'ngx-gauge';
89
import {FederationOverviewGridComponent} from './components/federation-overview-grid/federation-overview-grid.component';
910
import {FederationSidebarComponent} from './components/federation-sidebar/federation-sidebar.component';
1011
import {
@@ -28,7 +29,8 @@ import {WelcomeComponent} from './pages/welcome/welcome.component';
2829
RouterModule,
2930
NgbModule,
3031
InlineSVGModule,
31-
NgxEchartsModule
32+
NgxEchartsModule,
33+
NgxGaugeModule
3234
],
3335
declarations: [
3436
FederationSidebarComponent,
Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
<div class="container-fluid py-3">
2-
<div class="d-flex align-items-center justify-content-between mb-3">
3-
<div>
4-
<h4 class="label-header text-uppercase mb-1">Federation Overview</h4>
5-
<p class="text-muted mb-0">Active instances and alert distribution.</p>
6-
</div>
7-
<button type="button"
8-
class="btn utm-button utm-button-primary"
9-
(click)="openCreateModal()">
10-
<i class="icon-plus3 mr-2"></i>Connect instance
11-
</button>
1+
<ng-container *ngIf="(hasInstances$ | async); else emptyState">
2+
<div class="welcome-overview">
3+
<app-federation-overview-grid></app-federation-overview-grid>
124
</div>
5+
</ng-container>
136

14-
<app-federation-overview-grid></app-federation-overview-grid>
15-
</div>
7+
<ng-template #emptyState>
8+
<div class="container-fluid d-flex align-items-center justify-content-center" style="min-height: calc(100vh - 95px);">
9+
<div class="text-center" style="max-width: 480px;">
10+
<div class="mb-4">
11+
<i class="icon-cloud-plus" style="font-size: 72px; color: #232f3e; opacity: 0.85;"></i>
12+
</div>
13+
<h4 class="label-header text-uppercase mb-2">Welcome to UTMStack Federation</h4>
14+
<p class="text-muted mb-4">
15+
No instances are connected yet. Connect your first instance to start managing it from this console.
16+
</p>
17+
<button type="button"
18+
class="btn utm-button utm-button-primary"
19+
(click)="openCreateModal()">
20+
<i class="icon-plus3 mr-2"></i>Connect instance
21+
</button>
22+
</div>
23+
</div>
24+
</ng-template>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
:host { display: block; }
2+
3+
.welcome-overview {
4+
padding: 16px;
5+
}

frontend/src/app/federation/pages/welcome/welcome.component.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {Component} from '@angular/core';
22
import {Router} from '@angular/router';
33
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
4+
import {Observable} from 'rxjs';
5+
import {map} from 'rxjs/operators';
46
import {InstanceFormModalComponent} from '../../components/instance-form-modal/instance-form-modal.component';
57
import {FederationInstance} from '../../domain/federation-instance.model';
68
import {FederationInstanceStateService} from '../../services/federation-instance-state.service';
@@ -12,6 +14,8 @@ import {FederationInstancesService} from '../../services/federation-instances.se
1214
styleUrls: ['./welcome.component.scss']
1315
})
1416
export class WelcomeComponent {
17+
readonly hasInstances$: Observable<boolean> = this.instanceState.instances$
18+
.pipe(map(instances => instances.length > 0));
1519

1620
constructor(
1721
private modalService: NgbModal,

frontend/src/app/federation/services/federation-overview.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class FederationOverviewService {
1818
}
1919

2020
countAlertsBySeverity(from: string, to: string): Observable<CountAlertsBySeverityEntry[]> {
21-
const params = new HttpParams().set('from', from).set('to', to);
21+
const params = new HttpParams().set('from', from).set('to', to).set('top','100000');
2222
return this.http.get<CountAlertsBySeverityEntry[]>(this.severityEndpoint, {params});
2323
}
2424
}

0 commit comments

Comments
 (0)