@@ -13,6 +13,8 @@ import Link from 'next/link';
13
13
import { useRouter } from 'next/router' ;
14
14
import cx from 'classnames' ;
15
15
import { clamp , sub } from 'date-fns' ;
16
+ import { useAtom } from 'jotai' ;
17
+ import { atomWithStorage } from 'jotai/utils' ;
16
18
import { Button } from 'react-bootstrap' ;
17
19
import { useHotkeys } from 'react-hotkeys-hook' ;
18
20
import {
@@ -30,13 +32,16 @@ import {
30
32
useQueryParams ,
31
33
withDefault ,
32
34
} from 'use-query-params' ;
33
- import { ActionIcon , Indicator } from '@mantine/core' ;
35
+ import { ActionIcon , Indicator , Tooltip as MTooltip } from '@mantine/core' ;
34
36
import { notifications } from '@mantine/notifications' ;
35
37
36
38
import { TimePicker } from '@/components/TimePicker' ;
37
39
38
40
import { ErrorBoundary } from './components/ErrorBoundary' ;
39
- import api from './api' ;
41
+ import { Heatmap } from './components/Heatmap/Heatmap' ;
42
+ import { Icon } from './components/Icon' ;
43
+ import api , { useMultiSeriesChartV2 } from './api' ;
44
+ import { convertDateRangeToGranularityString } from './ChartUtils' ;
40
45
import CreateLogAlertModal from './CreateLogAlertModal' ;
41
46
import { withAppNav } from './layout' ;
42
47
import LogSidePanel from './LogSidePanel' ;
@@ -48,12 +53,18 @@ import { SearchPageFilters, ToggleFilterButton } from './SearchPage.components';
48
53
import SearchPageActionBar from './SearchPageActionBar' ;
49
54
import { Tags } from './Tags' ;
50
55
import { useTimeQuery } from './timeQuery' ;
56
+ import type { TimeChartSeries } from './types' ;
51
57
import { useDisplayedColumns } from './useDisplayedColumns' ;
52
58
import { FormatTime , useFormatTime } from './useFormatTime' ;
53
59
54
60
import 'react-modern-drawer/dist/index.css' ;
55
61
import styles from '../styles/SearchPage.module.scss' ;
56
62
63
+ const chartModeAtom = atomWithStorage < 'heatmap' | 'histogram' > (
64
+ 'hdx-search-page-chart-mode' ,
65
+ 'histogram' ,
66
+ ) ;
67
+
57
68
const HistogramBarChartTooltip = ( props : any ) => {
58
69
const { active, payload, label } = props ;
59
70
if ( active && payload && payload . length ) {
@@ -119,7 +130,7 @@ const HDXHistogram = memo(
119
130
const formatTime = useFormatTime ( ) ;
120
131
121
132
return isHistogramResultsLoading ? (
122
- < div className = "w-100 h-100 d-flex align-items-center justify-content-center" >
133
+ < div className = "w-100 h-100 fs-8 text-slate-300 d-flex align-items-center justify-content-center" >
123
134
Loading Graph...
124
135
</ div >
125
136
) : (
@@ -219,6 +230,79 @@ const HDXHistogram = memo(
219
230
} ,
220
231
) ;
221
232
233
+ function genLogScale ( total_intervals : number , start : number , end : number ) {
234
+ const x = ( Math . log ( end ) - Math . log ( start ) ) / total_intervals ;
235
+ const factor = Math . exp ( x ) ;
236
+ const result = [ start ] ;
237
+ let i ;
238
+
239
+ for ( i = 1 ; i < total_intervals ; i ++ ) {
240
+ result . push ( result [ result . length - 1 ] * factor ) ;
241
+ }
242
+ result . push ( end ) ;
243
+ return result ;
244
+ }
245
+
246
+ const HDXHeatmap = ( {
247
+ config,
248
+ isLive,
249
+ } : {
250
+ config : {
251
+ dateRange : [ Date , Date ] ;
252
+ where : string ;
253
+ } ;
254
+ isLive : boolean ;
255
+ } ) => {
256
+ const formatTime = useFormatTime ( ) ;
257
+
258
+ const input = useMemo ( ( ) => {
259
+ const scale = genLogScale ( 14 , 1 , 30 * 60 * 1000 ) ; // ms
260
+ return {
261
+ startDate : config . dateRange [ 0 ] ,
262
+ endDate : config . dateRange [ 1 ] ,
263
+ where : config . where ,
264
+ series : scale . map ( ( v , i ) => ( {
265
+ type : 'time' as const ,
266
+ table : 'logs' as const ,
267
+ aggFn : 'count' as const ,
268
+ where : `duration:>${ scale [ i - 1 ] || 0 } AND duration:<${ v } AND ${
269
+ config . where
270
+ } `,
271
+ groupBy : [ ] ,
272
+ } ) ) ,
273
+ seriesReturnType : 'column' as const ,
274
+ granularity : convertDateRangeToGranularityString ( config . dateRange , 200 ) ,
275
+ } ;
276
+ } , [ config ] ) ;
277
+
278
+ const { isFetching, isLoading, data } = api . useMultiSeriesChart ( input , {
279
+ keepPreviousData : true ,
280
+ } ) ;
281
+
282
+ const xLabels = useMemo ( ( ) => {
283
+ return [ formatTime ( config . dateRange [ 0 ] ) , formatTime ( config . dateRange [ 1 ] ) ] ;
284
+ } , [ config . dateRange , formatTime ] ) ;
285
+
286
+ const yLabels = useMemo ( ( ) => [ '0ms' , '30m' ] , [ ] ) ;
287
+
288
+ if ( isLoading ) {
289
+ return (
290
+ < div className = "w-100 fs-8 text-slate-300 h-100 d-flex align-items-center justify-content-center" >
291
+ Loading Graph...
292
+ </ div >
293
+ ) ;
294
+ }
295
+
296
+ return (
297
+ < Heatmap
298
+ xLabels = { xLabels }
299
+ yLabels = { yLabels }
300
+ data = { data }
301
+ isFetching = { isFetching }
302
+ />
303
+ ) ;
304
+ } ;
305
+
222
306
const HistogramResultCounter = ( {
223
307
config : { dateRange, where } ,
224
308
} : {
@@ -681,6 +765,8 @@ function SearchPage() {
681
765
[ displayedSearchQuery , displayedTimeInputValue , doSearch ] ,
682
766
) ;
683
767
768
+ const [ chartMode , setChartMode ] = useAtom ( chartModeAtom ) ;
769
+
684
770
return (
685
771
< div style = { { height : '100vh' } } >
686
772
< Head >
@@ -847,7 +933,31 @@ function SearchPage() {
847
933
</ ErrorBoundary >
848
934
< div className = "d-flex flex-column flex-grow-1" >
849
935
< div className = "d-flex mx-4 mt-2 justify-content-between" >
850
- < div className = "fs-8 text-muted" >
936
+ < div className = "fs-8 text-muted d-flex align-items-center gap-1" >
937
+ < MTooltip label = "Histogram" color = "gray" >
938
+ < ActionIcon
939
+ color = "gray"
940
+ variant = { chartMode === 'histogram' ? 'filled' : 'subtle' }
941
+ size = "sm"
942
+ onClick = { ( ) => setChartMode ( 'histogram' ) }
943
+ >
944
+ < Icon
945
+ name = "bar-chart-line-fill"
946
+ className = "fs-8 text-success"
947
+ />
948
+ </ ActionIcon >
949
+ </ MTooltip >
950
+ < MTooltip color = "gray" label = "Heat map" >
951
+ < ActionIcon
952
+ color = "gray"
953
+ size = "sm"
954
+ mr = { 4 }
955
+ variant = { chartMode === 'heatmap' ? 'filled' : 'subtle' }
956
+ onClick = { ( ) => setChartMode ( 'heatmap' ) }
957
+ >
958
+ < Icon name = "grid-fill" className = "fs-8 text-success" />
959
+ </ ActionIcon >
960
+ </ MTooltip >
851
961
{ isReady ? (
852
962
< HistogramResultCounter
853
963
config = { {
@@ -860,37 +970,41 @@ function SearchPage() {
860
970
/>
861
971
) : null }
862
972
</ div >
863
- < div className = "d-flex" >
864
- < Link
865
- href = { generateSearchUrl ( searchedQuery , [
866
- zoomOutFrom ,
867
- zoomOutTo ,
868
- ] ) }
869
- className = "text-muted-hover text-decoration-none fs-8 me-3"
870
- >
871
- < i className = "bi bi-zoom-out me-1" > </ i > Zoom Out
872
- </ Link >
873
- < Link
874
- href = { generateSearchUrl ( searchedQuery , [
875
- zoomInFrom ,
876
- zoomInTo ,
877
- ] ) }
878
- className = "text-muted-hover text-decoration-none fs-8 me-3"
879
- >
880
- < i className = "bi bi-zoom-in me-1" > </ i > Zoom In
881
- </ Link >
882
- < Link
883
- href = { generateChartUrl ( {
884
- table : 'logs' ,
885
- aggFn : 'count' ,
886
- field : undefined ,
887
- groupBy : [ 'level' ] ,
888
- } ) }
889
- className = "text-muted-hover text-decoration-none fs-8"
890
- >
891
- < i className = "bi bi-plus-circle me-1" > </ i > Create Chart
892
- </ Link >
893
- </ div >
973
+ { chartMode === 'histogram' ? (
974
+ < div className = "d-flex" >
975
+ < Link
976
+ href = { generateSearchUrl ( searchedQuery , [
977
+ zoomOutFrom ,
978
+ zoomOutTo ,
979
+ ] ) }
980
+ className = "text-muted-hover text-decoration-none fs-8 me-3"
981
+ >
982
+ < i className = "bi bi-zoom-out me-1" > </ i > Zoom Out
983
+ </ Link >
984
+ < Link
985
+ href = { generateSearchUrl ( searchedQuery , [
986
+ zoomInFrom ,
987
+ zoomInTo ,
988
+ ] ) }
989
+ className = "text-muted-hover text-decoration-none fs-8 me-3"
990
+ >
991
+ < i className = "bi bi-zoom-in me-1" > </ i > Zoom In
992
+ </ Link >
993
+ < Link
994
+ href = { generateChartUrl ( {
995
+ table : 'logs' ,
996
+ aggFn : 'count' ,
997
+ field : undefined ,
998
+ groupBy : [ 'level' ] ,
999
+ } ) }
1000
+ className = "text-muted-hover text-decoration-none fs-8"
1001
+ >
1002
+ < i className = "bi bi-plus-circle me-1" > </ i > Create Chart
1003
+ </ Link >
1004
+ </ div >
1005
+ ) : (
1006
+ < div className = "fs-8 text-slate-600" > H Y P E R D X</ div >
1007
+ ) }
894
1008
</ div >
895
1009
< div style = { { height : 110 } } className = "my-2 px-3 w-100" >
896
1010
{ /* Hack, recharts will release real fix soon https://github.com/recharts/recharts/issues/172 */ }
@@ -911,11 +1025,15 @@ function SearchPage() {
911
1025
} }
912
1026
>
913
1027
{ isReady ? (
914
- < HDXHistogram
915
- config = { chartsConfig }
916
- onTimeRangeSelect = { onTimeRangeSelect }
917
- isLive = { isLive }
918
- />
1028
+ chartMode === 'histogram' ? (
1029
+ < HDXHistogram
1030
+ config = { chartsConfig }
1031
+ onTimeRangeSelect = { onTimeRangeSelect }
1032
+ isLive = { isLive }
1033
+ />
1034
+ ) : (
1035
+ < HDXHeatmap isLive = { isLive } config = { chartsConfig } />
1036
+ )
919
1037
) : null }
920
1038
</ div >
921
1039
</ div >
0 commit comments