1- import { useEffect , useMemo } from 'react'
2- import {
3- Paper ,
4- Group ,
5- Title ,
6- Stack ,
7- SimpleGrid ,
8- Text ,
9- Badge ,
10- ThemeIcon ,
11- Box ,
12- LoadingOverlay ,
13- } from '@mantine/core'
14- import { useQuery } from '@tanstack/react-query'
15- import ReactEChartsCore from 'echarts-for-react/lib/core'
16- import * as echarts from 'echarts/core'
17- import { LineChart , BarChart , HeatmapChart , ScatterChart , PieChart } from 'echarts/charts'
18- import {
19- GridComponent ,
20- TooltipComponent ,
21- LegendComponent ,
22- VisualMapComponent ,
23- TitleComponent ,
24- } from 'echarts/components'
25- import { CanvasRenderer } from 'echarts/renderers'
26- import { Activity , AlertTriangle , Clock , Layers , Zap , BarChart3 , TrendingUp } from 'lucide-react'
27- import type { TrafficPoint , DashboardStats , LatencyHeatmapPoint } from '../../types'
28- import { useFilterParam } from '../../hooks/useFilterParams'
1+ import { Group , Title , Stack , Badge , Box } from '@mantine/core'
292import { useLiveMode } from '../../contexts/LiveModeContext'
303import { GlobalControls } from '../../components/GlobalControls'
31- import { useTimeRange } from '../../components/TimeRangeSelector'
32-
33- echarts . use ( [
34- LineChart , BarChart , HeatmapChart , ScatterChart , PieChart ,
35- GridComponent , TooltipComponent , LegendComponent , VisualMapComponent , TitleComponent ,
36- CanvasRenderer ,
37- ] )
4+ import { DashboardStatsGrid } from './components/DashboardStatsGrid'
5+ import { TrafficVolumeChart } from './components/TrafficVolumeChart'
6+ import { LatencyScatterChart } from './components/LatencyScatterChart'
7+ import { ThroughputPieChart } from './components/ThroughputPieChart'
8+ import { SimpleGrid } from '@mantine/core'
389
3910export function Dashboard ( ) {
40- const tr = useTimeRange ( '15m' )
41- const [ selectedService ] = useFilterParam ( 'service' , null )
42- const { isLive, isConnected, setServiceFilter } = useLiveMode ( )
43-
44- // Sync local filter param to global live mode filter
45- useEffect ( ( ) => {
46- if ( isLive ) {
47- setServiceFilter ( selectedService || '' )
48- }
49- } , [ isLive , selectedService , setServiceFilter ] )
50-
51- const serviceParams = selectedService ? `&service_name=${ encodeURIComponent ( selectedService ) } ` : ''
52-
53- // Traffic data
54- const trafficQueryKey = [ 'traffic' , tr . start , tr . end , selectedService , isLive ]
55- const { data : traffic , isFetching : isFetchingTraffic } = useQuery < TrafficPoint [ ] > ( {
56- queryKey : trafficQueryKey ,
57- queryFn : ( ) => fetch ( `/api/metrics/traffic?start=${ tr . start } &end=${ tr . end } ${ serviceParams } ` ) . then ( r => r . json ( ) ) ,
58- refetchInterval : isLive ? 10000 : false ,
59- staleTime : 30000 ,
60- refetchOnWindowFocus : false ,
61- } )
62-
63- // Dashboard Stats
64- const statsQueryKey = [ 'dashboardStats' , tr . start , tr . end , selectedService , isLive ]
65- const { data : stats , isFetching : isFetchingStats } = useQuery < DashboardStats > ( {
66- queryKey : statsQueryKey ,
67- queryFn : ( ) => fetch ( `/api/metrics/dashboard?start=${ tr . start } &end=${ tr . end } ${ serviceParams } ` ) . then ( r => r . json ( ) ) ,
68- refetchInterval : isLive ? 10000 : false ,
69- staleTime : 30000 ,
70- refetchOnWindowFocus : false ,
71- } )
72-
73- // Heatmap data
74- const heatmapQueryKey = [ 'heatmap' , tr . start , tr . end , selectedService , isLive ]
75- const { data : heatmap , isFetching : isFetchingHeatmap } = useQuery < LatencyHeatmapPoint [ ] > ( {
76- queryKey : heatmapQueryKey ,
77- queryFn : ( ) => fetch ( `/api/metrics/latency_heatmap?start=${ tr . start } &end=${ tr . end } ${ serviceParams } ` ) . then ( r => r . json ( ) ) ,
78- refetchInterval : isLive ? 10000 : false ,
79- } )
80-
81- const topFailing = stats ?. top_failing_services || [ ]
82-
83- const trafficChartOption = useMemo ( ( ) => ( {
84- tooltip : { trigger : 'axis' } ,
85- grid : { left : 50 , right : 20 , top : 20 , bottom : 30 } ,
86- xAxis : {
87- type : 'time' ,
88- axisLabel : { formatter : ( val : number ) => new Date ( val ) . toLocaleTimeString ( [ ] , { hour : '2-digit' , minute : '2-digit' } ) } ,
89- } ,
90- yAxis : { type : 'value' , name : 'Requests' } ,
91- series : [
92- {
93- name : 'Total' ,
94- type : 'line' ,
95- smooth : true ,
96- data : ( traffic || [ ] ) . map ( ( p : TrafficPoint ) => [ new Date ( p . timestamp ) . getTime ( ) , p . count ] ) ,
97- areaStyle : { opacity : 0.1 , color : '#4c6ef5' } ,
98- lineStyle : { color : '#4c6ef5' , width : 2 } ,
99- itemStyle : { color : '#4c6ef5' } ,
100- } ,
101- {
102- name : 'Errors' ,
103- type : 'line' ,
104- smooth : true ,
105- data : ( traffic || [ ] ) . map ( ( p : TrafficPoint ) => [ new Date ( p . timestamp ) . getTime ( ) , p . error_count ] ) ,
106- areaStyle : { opacity : 0.1 , color : '#fa5252' } ,
107- lineStyle : { color : '#fa5252' , width : 2 } ,
108- itemStyle : { color : '#fa5252' } ,
109- } ,
110- ] ,
111- } ) , [ traffic ] )
112-
113- const heatmapChartOption = useMemo ( ( ) => ( {
114- tooltip : {
115- trigger : 'item' ,
116- formatter : ( p : any ) => {
117- const durMs = p . value [ 1 ] / 1000 ;
118- return `Time: ${ new Date ( p . value [ 0 ] ) . toLocaleTimeString ( ) } <br/>Latency: <b>${ durMs . toFixed ( 2 ) } ms</b>` ;
119- }
120- } ,
121- grid : { left : 60 , right : 20 , top : 20 , bottom : 30 } ,
122- xAxis : { type : 'time' , axisLabel : { color : '#909296' } } ,
123- yAxis : {
124- type : 'value' ,
125- name : 'µs' ,
126- axisLabel : { color : '#909296' } ,
127- splitLine : { lineStyle : { color : 'rgba(255,255,255,0.05)' } }
128- } ,
129- series : [ {
130- type : 'scatter' ,
131- symbolSize : 8 ,
132- data : ( heatmap || [ ] ) . map ( p => [ new Date ( p . timestamp ) . getTime ( ) , p . duration ] ) ,
133- itemStyle : { color : 'rgba(76, 110, 245, 0.6)' } ,
134- large : true ,
135- largeThreshold : 500
136- } ]
137- } ) , [ heatmap ] )
138-
139- const throughputPieOption = useMemo ( ( ) => ( {
140- tooltip : { trigger : 'item' } ,
141- legend : { bottom : '5%' , left : 'center' , textStyle : { color : '#909296' } } ,
142- series : [ {
143- name : 'Throughput' ,
144- type : 'pie' ,
145- radius : [ '40%' , '70%' ] ,
146- avoidLabelOverlap : false ,
147- itemStyle : { borderRadius : 10 , borderColor : '#ffffffff' , borderWidth : 4 } ,
148- label : { show : false , position : 'center' } ,
149- emphasis : { label : { show : true , fontSize : 16 , fontWeight : 'bold' } } ,
150- labelLine : { show : false } ,
151- data : ( topFailing || [ ] ) . map ( s => ( { value : s . total_count , name : s . service_name } ) )
152- } ]
153- } ) , [ topFailing ] )
154-
155- const statCards = [
156- { label : 'Total Traces' , value : stats ?. total_traces ?? 0 , icon : Layers , color : 'indigo' } ,
157- { label : 'Total Logs' , value : stats ?. total_logs ?? 0 , icon : Activity , color : 'cyan' } ,
158- { label : 'Total Errors' , value : stats ?. total_errors ?? 0 , icon : AlertTriangle , color : 'red' } ,
159- { label : 'Avg Latency' , value : `${ ( stats ?. avg_latency_ms ?? 0 ) . toFixed ( 1 ) } ms` , icon : Clock , color : 'orange' } ,
160- ]
11+ const { isLive, isConnected } = useLiveMode ( )
16112
16213 return (
16314 < Stack gap = "md" >
@@ -176,59 +27,14 @@ export function Dashboard() {
17627 </ Group >
17728
17829 < Box style = { { position : 'relative' } } >
179- < LoadingOverlay visible = { ( isFetchingTraffic || isFetchingStats || isFetchingHeatmap ) && ! isLive } zIndex = { 1000 } overlayProps = { { radius : 'sm' , blur : 2 } } />
180-
181- < SimpleGrid cols = { { base : 2 , md : 4 } } mb = "md" >
182- { statCards . map ( ( s ) => (
183- < Paper key = { s . label } shadow = "xs" p = "md" radius = "md" withBorder >
184- < Group justify = "space-between" align = "flex-start" >
185- < Box >
186- < Text size = "xs" c = "dimmed" tt = "uppercase" fw = { 600 } > { s . label } </ Text >
187- < Title order = { 3 } mt = { 4 } > { typeof s . value === 'number' ? s . value . toLocaleString ( ) : s . value } </ Title >
188- </ Box >
189- < ThemeIcon variant = "light" color = { s . color } size = "lg" radius = "md" >
190- < s . icon size = { 18 } />
191- </ ThemeIcon >
192- </ Group >
193- </ Paper >
194- ) ) }
195- </ SimpleGrid >
30+ < DashboardStatsGrid />
19631
19732 < SimpleGrid cols = { { base : 1 , md : 2 } } spacing = "md" mb = "md" >
198- < Paper shadow = "xs" p = "md" radius = "md" withBorder >
199- < Group gap = "xs" mb = "sm" >
200- < BarChart3 size = { 16 } color = "var(--mantine-color-cyan-4)" />
201- < Text fw = { 600 } > Throughput Distribution</ Text >
202- </ Group >
203- < Box style = { { height : 350 } } >
204- < ReactEChartsCore echarts = { echarts } option = { throughputPieOption } style = { { height : '100%' } } />
205- </ Box >
206- </ Paper >
207-
208- < Paper shadow = "xs" p = "md" radius = "md" withBorder >
209- < Group gap = "xs" mb = "sm" >
210- < Zap size = { 16 } color = "var(--mantine-color-yellow-4)" />
211- < Text fw = { 600 } > Latency Distribution (Scatter)</ Text >
212- </ Group >
213- < Box style = { { height : 260 } } >
214- < ReactEChartsCore echarts = { echarts } option = { heatmapChartOption } style = { { height : '100%' } } />
215- </ Box >
216- </ Paper >
33+ < ThroughputPieChart />
34+ < LatencyScatterChart />
21735 </ SimpleGrid >
21836
219- < Paper shadow = "xs" p = "md" radius = "md" withBorder >
220- < Group gap = "xs" mb = "sm" >
221- < TrendingUp size = { 16 } color = "var(--mantine-color-indigo-4)" />
222- < Text fw = { 600 } > Traffic Request Volume</ Text >
223- </ Group >
224- < Box style = { { height : 260 } } >
225- < ReactEChartsCore echarts = { echarts } option = { trafficChartOption } style = { { height : '100%' } } />
226- </ Box >
227-
228-
229-
230-
231- </ Paper >
37+ < TrafficVolumeChart />
23238 </ Box >
23339 </ Stack >
23440 )
0 commit comments