Ming Wang commited on
Commit
1f7eb48
·
1 Parent(s): 602851d

Phase 2: chart integration

Browse files
frontend/package-lock.json CHANGED
@@ -16,6 +16,8 @@
16
  "@angular/material": "^21.2.3",
17
  "@angular/platform-browser": "^21.2.0",
18
  "@angular/router": "^21.2.0",
 
 
19
  "rxjs": "~7.8.0",
20
  "tslib": "^2.3.0"
21
  },
@@ -4479,6 +4481,22 @@
4479
  "node": ">= 0.4"
4480
  }
4481
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4482
  "node_modules/ee-first": {
4483
  "version": "1.1.1",
4484
  "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -5937,6 +5955,19 @@
5937
  "node": ">= 0.6"
5938
  }
5939
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
5940
  "node_modules/node-addon-api": {
5941
  "version": "6.1.0",
5942
  "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
@@ -7718,6 +7749,21 @@
7718
  "peerDependencies": {
7719
  "zod": "^3.25 || ^4"
7720
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7721
  }
7722
  }
7723
  }
 
16
  "@angular/material": "^21.2.3",
17
  "@angular/platform-browser": "^21.2.0",
18
  "@angular/router": "^21.2.0",
19
+ "echarts": "^6.0.0",
20
+ "ngx-echarts": "^21.0.0",
21
  "rxjs": "~7.8.0",
22
  "tslib": "^2.3.0"
23
  },
 
4481
  "node": ">= 0.4"
4482
  }
4483
  },
4484
+ "node_modules/echarts": {
4485
+ "version": "6.0.0",
4486
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
4487
+ "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
4488
+ "license": "Apache-2.0",
4489
+ "dependencies": {
4490
+ "tslib": "2.3.0",
4491
+ "zrender": "6.0.0"
4492
+ }
4493
+ },
4494
+ "node_modules/echarts/node_modules/tslib": {
4495
+ "version": "2.3.0",
4496
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
4497
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
4498
+ "license": "0BSD"
4499
+ },
4500
  "node_modules/ee-first": {
4501
  "version": "1.1.1",
4502
  "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
 
5955
  "node": ">= 0.6"
5956
  }
5957
  },
5958
+ "node_modules/ngx-echarts": {
5959
+ "version": "21.0.0",
5960
+ "resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-21.0.0.tgz",
5961
+ "integrity": "sha512-vivBRmGYMFlnPxK/uxliY+sexGwHqnFZ2pylGbIMF2wikt9RpZpGGSitLmuUXvzYwkVFGo6dQmiUQh1+6Pbfuw==",
5962
+ "license": "MIT",
5963
+ "dependencies": {
5964
+ "tslib": "^2.3.0"
5965
+ },
5966
+ "peerDependencies": {
5967
+ "@angular/core": ">=21.0.0",
5968
+ "echarts": ">=5.0.0"
5969
+ }
5970
+ },
5971
  "node_modules/node-addon-api": {
5972
  "version": "6.1.0",
5973
  "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
 
7749
  "peerDependencies": {
7750
  "zod": "^3.25 || ^4"
7751
  }
7752
+ },
7753
+ "node_modules/zrender": {
7754
+ "version": "6.0.0",
7755
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
7756
+ "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
7757
+ "license": "BSD-3-Clause",
7758
+ "dependencies": {
7759
+ "tslib": "2.3.0"
7760
+ }
7761
+ },
7762
+ "node_modules/zrender/node_modules/tslib": {
7763
+ "version": "2.3.0",
7764
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
7765
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
7766
+ "license": "0BSD"
7767
  }
7768
  }
7769
  }
frontend/package.json CHANGED
@@ -19,6 +19,8 @@
19
  "@angular/material": "^21.2.3",
20
  "@angular/platform-browser": "^21.2.0",
21
  "@angular/router": "^21.2.0",
 
 
22
  "rxjs": "~7.8.0",
23
  "tslib": "^2.3.0"
24
  },
 
19
  "@angular/material": "^21.2.3",
20
  "@angular/platform-browser": "^21.2.0",
21
  "@angular/router": "^21.2.0",
22
+ "echarts": "^6.0.0",
23
+ "ngx-echarts": "^21.0.0",
24
  "rxjs": "~7.8.0",
25
  "tslib": "^2.3.0"
26
  },
frontend/src/app/core/mocks/charts.mock.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface TrafficDataPoint {
2
+ time: string;
3
+ inbound: number;
4
+ outbound: number;
5
+ latency: number;
6
+ }
7
+
8
+ export const TRAFFIC_MOCK: TrafficDataPoint[] = [
9
+ { time: '00:00', inbound: 120, outbound: 95, latency: 12 },
10
+ { time: '01:00', inbound: 85, outbound: 70, latency: 10 },
11
+ { time: '02:00', inbound: 60, outbound: 52, latency: 8 },
12
+ { time: '03:00', inbound: 45, outbound: 38, latency: 7 },
13
+ { time: '04:00', inbound: 40, outbound: 35, latency: 6 },
14
+ { time: '05:00', inbound: 55, outbound: 42, latency: 8 },
15
+ { time: '06:00', inbound: 90, outbound: 78, latency: 11 },
16
+ { time: '07:00', inbound: 145, outbound: 120, latency: 15 },
17
+ { time: '08:00', inbound: 210, outbound: 180, latency: 22 },
18
+ { time: '09:00', inbound: 280, outbound: 245, latency: 28 },
19
+ { time: '10:00', inbound: 310, outbound: 270, latency: 32 },
20
+ { time: '11:00', inbound: 295, outbound: 260, latency: 30 },
21
+ { time: '12:00', inbound: 270, outbound: 235, latency: 27 },
22
+ { time: '13:00', inbound: 285, outbound: 250, latency: 29 },
23
+ { time: '14:00', inbound: 320, outbound: 280, latency: 35 },
24
+ { time: '15:00', inbound: 300, outbound: 265, latency: 31 },
25
+ { time: '16:00', inbound: 275, outbound: 240, latency: 26 },
26
+ { time: '17:00', inbound: 250, outbound: 215, latency: 24 },
27
+ { time: '18:00', inbound: 220, outbound: 190, latency: 20 },
28
+ { time: '19:00', inbound: 185, outbound: 160, latency: 18 },
29
+ { time: '20:00', inbound: 160, outbound: 138, latency: 16 },
30
+ { time: '21:00', inbound: 140, outbound: 118, latency: 14 },
31
+ { time: '22:00', inbound: 125, outbound: 105, latency: 13 },
32
+ { time: '23:00', inbound: 110, outbound: 90, latency: 11 },
33
+ ];
34
+
35
+ export interface IncidentRegionData {
36
+ region: string;
37
+ critical: number;
38
+ warning: number;
39
+ info: number;
40
+ }
41
+
42
+ export const INCIDENT_REGION_MOCK: IncidentRegionData[] = [
43
+ { region: 'US-East', critical: 5, warning: 12, info: 8 },
44
+ { region: 'US-West', critical: 2, warning: 8, info: 15 },
45
+ { region: 'EU-West', critical: 7, warning: 10, info: 6 },
46
+ { region: 'AP-South', critical: 3, warning: 14, info: 11 },
47
+ { region: 'AP-East', critical: 1, warning: 6, info: 9 },
48
+ ];
frontend/src/app/dashboard/chart-config.factory.ts ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { EChartsOption } from 'echarts';
2
+ import { TrafficDataPoint, IncidentRegionData } from '../core/mocks/charts.mock';
3
+
4
+ /**
5
+ * Chart color palette aligned with Material 3 violet theme.
6
+ * Uses CSS variable fallbacks for runtime theme integration.
7
+ */
8
+ const PALETTE = {
9
+ primary: '#7b1fa2',
10
+ secondary: '#9c27b0',
11
+ tertiary: '#42a5f5',
12
+ surface: '#f5f0ff',
13
+ onSurface: '#1c1b1f',
14
+ outline: '#79747e',
15
+ critical: '#d32f2f',
16
+ warning: '#ed6c02',
17
+ info: '#0288d1',
18
+ } as const;
19
+
20
+ const BASE_TEXT_STYLE = {
21
+ fontFamily: 'Roboto, sans-serif',
22
+ color: PALETTE.onSurface,
23
+ } as const;
24
+
25
+ function baseGrid(): EChartsOption['grid'] {
26
+ return { left: 48, right: 24, top: 40, bottom: 32, containLabel: true };
27
+ }
28
+
29
+ export function buildTrafficLatencyOption(data: TrafficDataPoint[]): EChartsOption {
30
+ const times = data.map((d) => d.time);
31
+
32
+ return {
33
+ textStyle: BASE_TEXT_STYLE,
34
+ tooltip: { trigger: 'axis' },
35
+ legend: { top: 0, textStyle: { fontSize: 12 } },
36
+ grid: baseGrid(),
37
+ xAxis: {
38
+ type: 'category',
39
+ data: times,
40
+ axisLine: { lineStyle: { color: PALETTE.outline } },
41
+ axisLabel: { fontSize: 11 },
42
+ },
43
+ yAxis: [
44
+ {
45
+ type: 'value',
46
+ name: 'Mbps',
47
+ nameTextStyle: { fontSize: 11 },
48
+ axisLine: { show: false },
49
+ splitLine: { lineStyle: { color: PALETTE.surface } },
50
+ },
51
+ {
52
+ type: 'value',
53
+ name: 'ms',
54
+ nameTextStyle: { fontSize: 11 },
55
+ axisLine: { show: false },
56
+ splitLine: { show: false },
57
+ },
58
+ ],
59
+ series: [
60
+ {
61
+ name: 'Inbound',
62
+ type: 'line',
63
+ data: data.map((d) => d.inbound),
64
+ smooth: true,
65
+ symbol: 'none',
66
+ lineStyle: { width: 2 },
67
+ itemStyle: { color: PALETTE.primary },
68
+ areaStyle: { color: `${PALETTE.primary}18` },
69
+ },
70
+ {
71
+ name: 'Outbound',
72
+ type: 'line',
73
+ data: data.map((d) => d.outbound),
74
+ smooth: true,
75
+ symbol: 'none',
76
+ lineStyle: { width: 2 },
77
+ itemStyle: { color: PALETTE.secondary },
78
+ areaStyle: { color: `${PALETTE.secondary}18` },
79
+ },
80
+ {
81
+ name: 'Latency',
82
+ type: 'line',
83
+ yAxisIndex: 1,
84
+ data: data.map((d) => d.latency),
85
+ smooth: true,
86
+ symbol: 'none',
87
+ lineStyle: { width: 2, type: 'dashed' },
88
+ itemStyle: { color: PALETTE.tertiary },
89
+ },
90
+ ],
91
+ };
92
+ }
93
+
94
+ export function buildIncidentRegionOption(data: IncidentRegionData[]): EChartsOption {
95
+ const regions = data.map((d) => d.region);
96
+
97
+ return {
98
+ textStyle: BASE_TEXT_STYLE,
99
+ tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
100
+ legend: { top: 0, textStyle: { fontSize: 12 } },
101
+ grid: baseGrid(),
102
+ xAxis: {
103
+ type: 'category',
104
+ data: regions,
105
+ axisLine: { lineStyle: { color: PALETTE.outline } },
106
+ axisLabel: { fontSize: 11 },
107
+ },
108
+ yAxis: {
109
+ type: 'value',
110
+ name: 'Incidents',
111
+ nameTextStyle: { fontSize: 11 },
112
+ axisLine: { show: false },
113
+ splitLine: { lineStyle: { color: PALETTE.surface } },
114
+ },
115
+ series: [
116
+ {
117
+ name: 'Critical',
118
+ type: 'bar',
119
+ stack: 'severity',
120
+ data: data.map((d) => d.critical),
121
+ itemStyle: { color: PALETTE.critical, borderRadius: [0, 0, 0, 0] },
122
+ },
123
+ {
124
+ name: 'Warning',
125
+ type: 'bar',
126
+ stack: 'severity',
127
+ data: data.map((d) => d.warning),
128
+ itemStyle: { color: PALETTE.warning },
129
+ },
130
+ {
131
+ name: 'Info',
132
+ type: 'bar',
133
+ stack: 'severity',
134
+ data: data.map((d) => d.info),
135
+ itemStyle: { color: PALETTE.info, borderRadius: [4, 4, 0, 0] },
136
+ barWidth: '50%',
137
+ },
138
+ ],
139
+ };
140
+ }
frontend/src/app/dashboard/chart-widget/chart-widget.html ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <mat-card class="chart-card" appearance="outlined">
2
+ <mat-card-content>
3
+ <h3 class="chart-title">{{ title() }}</h3>
4
+ <div class="chart-container" echarts [options]="options()"></div>
5
+ </mat-card-content>
6
+ </mat-card>
frontend/src/app/dashboard/chart-widget/chart-widget.scss ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :host {
2
+ display: block;
3
+ }
4
+
5
+ .chart-card {
6
+ height: 100%;
7
+ border-radius: var(--pf-radius-md);
8
+ }
9
+
10
+ .chart-title {
11
+ font: var(--mat-sys-title-medium);
12
+ color: var(--mat-sys-on-surface);
13
+ margin: 0 0 var(--pf-spacing-md);
14
+ }
15
+
16
+ .chart-container {
17
+ width: 100%;
18
+ height: 300px;
19
+ }
frontend/src/app/dashboard/chart-widget/chart-widget.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, input } from '@angular/core';
2
+ import { MatCard, MatCardContent } from '@angular/material/card';
3
+ import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts';
4
+ import * as echarts from 'echarts/core';
5
+ import { LineChart, BarChart } from 'echarts/charts';
6
+ import {
7
+ TitleComponent,
8
+ TooltipComponent,
9
+ GridComponent,
10
+ LegendComponent,
11
+ } from 'echarts/components';
12
+ import { CanvasRenderer } from 'echarts/renderers';
13
+ import type { EChartsOption } from 'echarts';
14
+
15
+ echarts.use([
16
+ LineChart,
17
+ BarChart,
18
+ TitleComponent,
19
+ TooltipComponent,
20
+ GridComponent,
21
+ LegendComponent,
22
+ CanvasRenderer,
23
+ ]);
24
+
25
+ @Component({
26
+ selector: 'app-chart-widget',
27
+ imports: [MatCard, MatCardContent, NgxEchartsDirective],
28
+ providers: [provideEchartsCore({ echarts })],
29
+ templateUrl: './chart-widget.html',
30
+ styleUrl: './chart-widget.scss',
31
+ })
32
+ export class ChartWidget {
33
+ title = input.required<string>();
34
+ options = input.required<EChartsOption>();
35
+ }
frontend/src/app/dashboard/dashboard.html CHANGED
@@ -18,8 +18,8 @@
18
  </div>
19
 
20
  <div class="chart-grid">
21
- <app-chart-placeholder title="Network Traffic (24h)" />
22
- <app-chart-placeholder title="Incident Trend (7d)" />
23
  </div>
24
 
25
  <app-alert-list [alerts]="alerts" />
 
18
  </div>
19
 
20
  <div class="chart-grid">
21
+ <app-chart-widget title="24h Traffic / Latency Trend" [options]="trafficChartOptions" />
22
+ <app-chart-widget title="Incident Severity by Region" [options]="incidentChartOptions" />
23
  </div>
24
 
25
  <app-alert-list [alerts]="alerts" />
frontend/src/app/dashboard/dashboard.ts CHANGED
@@ -1,17 +1,21 @@
1
  import { Component } from '@angular/core';
2
  import { KpiCard } from './kpi-card/kpi-card';
3
- import { ChartPlaceholder } from './chart-placeholder/chart-placeholder';
4
  import { AlertList } from './alert-list/alert-list';
5
  import { KPI_MOCKS } from '../core/mocks/kpi.mock';
6
  import { ALERT_MOCKS } from '../core/mocks/alerts.mock';
 
 
7
 
8
  @Component({
9
  selector: 'app-dashboard',
10
- imports: [KpiCard, ChartPlaceholder, AlertList],
11
  templateUrl: './dashboard.html',
12
  styleUrl: './dashboard.scss',
13
  })
14
  export class Dashboard {
15
  protected readonly kpis = KPI_MOCKS;
16
  protected readonly alerts = ALERT_MOCKS;
 
 
17
  }
 
1
  import { Component } from '@angular/core';
2
  import { KpiCard } from './kpi-card/kpi-card';
3
+ import { ChartWidget } from './chart-widget/chart-widget';
4
  import { AlertList } from './alert-list/alert-list';
5
  import { KPI_MOCKS } from '../core/mocks/kpi.mock';
6
  import { ALERT_MOCKS } from '../core/mocks/alerts.mock';
7
+ import { TRAFFIC_MOCK, INCIDENT_REGION_MOCK } from '../core/mocks/charts.mock';
8
+ import { buildTrafficLatencyOption, buildIncidentRegionOption } from './chart-config.factory';
9
 
10
  @Component({
11
  selector: 'app-dashboard',
12
+ imports: [KpiCard, ChartWidget, AlertList],
13
  templateUrl: './dashboard.html',
14
  styleUrl: './dashboard.scss',
15
  })
16
  export class Dashboard {
17
  protected readonly kpis = KPI_MOCKS;
18
  protected readonly alerts = ALERT_MOCKS;
19
+ protected readonly trafficChartOptions = buildTrafficLatencyOption(TRAFFIC_MOCK);
20
+ protected readonly incidentChartOptions = buildIncidentRegionOption(INCIDENT_REGION_MOCK);
21
  }