33import { Chart as ChartJS , BarElement , LinearScale , CategoryScale , TimeScale , Tooltip , Legend , ChartData } from "chart.js" ;
44import { useEffect , useMemo , useState , useRef , memo } from "react" ;
55import { Bar } from "react-chartjs-2" ;
6- import { NginxLog } from "@/lib/types" ;
76import 'chartjs-adapter-date-fns' ;
8- import { getDateRange , Period , periodStart } from "@/lib/period" ;
7+ import { Period } from "@/lib/period" ;
98
109ChartJS . register (
1110 BarElement ,
@@ -16,149 +15,6 @@ ChartJS.register(
1615 Legend
1716) ;
1817
19- function getDayId ( date : Date ) {
20- if ( ! ( date instanceof Date ) ) {
21- throw new Error ( "Invalid date object" ) ;
22- }
23-
24- return new Date ( date ) . setHours ( 0 , 0 , 0 , 0 ) ; // Set minutes, seconds, and milliseconds to zero
25- }
26-
27- function getHourId ( date : Date ) {
28- if ( ! ( date instanceof Date ) ) {
29- throw new Error ( "Invalid date object" ) ;
30- }
31-
32- return new Date ( date ) . setMinutes ( 0 , 0 , 0 ) ; // Set minutes, seconds, and milliseconds to zero
33- }
34-
35- function get6HourId ( date : Date ) {
36- if ( ! ( date instanceof Date ) ) {
37- throw new Error ( "Invalid date object" ) ;
38- }
39- const ms6h = 6 * 60 * 60 * 1000 ;
40- return Math . floor ( date . getTime ( ) / ms6h ) * ms6h ;
41- }
42-
43- function get5MinuteId ( date : Date ) {
44- if ( ! ( date instanceof Date ) ) {
45- throw new Error ( "Invalid date object" ) ;
46- }
47-
48- const msPer5Min = 5 * 60 * 1000 ; // 5 minutes in milliseconds
49- return ( new Date ( Math . round ( date . getTime ( ) / msPer5Min ) * msPer5Min ) ) . getTime ( ) ;
50- }
51-
52- function getMinuteId ( date : Date ) {
53- if ( ! ( date instanceof Date ) ) {
54- throw new Error ( "Invalid date object" ) ;
55- }
56-
57- return new Date ( date ) . setSeconds ( 0 , 0 ) ; // Changed to use setSeconds instead
58- }
59-
60- const getStepSize = ( period : Period , data : NginxLog [ ] ) => {
61- switch ( period ) {
62- case '24 hours' :
63- return 300000 ; // 5 minutes
64- case 'week' :
65- return 3600000 ; // 1 hour
66- case 'month' :
67- return 21600000 ; // 6 hours
68- case '6 months' :
69- return 86400000 ; // 1 day
70- case 'all time' :
71- const range = getDateRange ( data ) ;
72- if ( ! range ) {
73- return 8.64e+7 ; // day
74- }
75-
76- const diff = range . end - range . start ;
77- if ( diff <= 86400000 ) {
78- return 300000 ; // 5 minutes
79- } else if ( diff <= 604800000 ) {
80- return 3600000 ; // hour
81- } else {
82- return 8.64e+7 ; // day
83- }
84- default :
85- return 8.64e+7 ; // day
86- }
87- }
88-
89- const incrementDate = ( date : Date , period : Period ) => {
90- switch ( period ) {
91- case '24 hours' :
92- return new Date ( date . setMinutes ( date . getMinutes ( ) + 5 ) ) ;
93- case 'week' :
94- return new Date ( date . setHours ( date . getHours ( ) + 1 ) ) ;
95- case 'month' :
96- return new Date ( date . setHours ( date . getHours ( ) + 6 ) ) ;
97- case '6 months' :
98- case 'all time' :
99- default :
100- return new Date ( date . setDate ( date . getDate ( ) + 1 ) ) ;
101- }
102- }
103-
104- const getTimeIdGetter = ( period : Period , data : NginxLog [ ] ) => {
105- switch ( period ) {
106- case '24 hours' :
107- return get5MinuteId
108- case 'week' :
109- return getHourId
110- case 'month' :
111- return get6HourId
112- case '6 months' :
113- return getDayId
114- case 'all time' :
115- const range = getDateRange ( data ) ;
116- if ( ! range ) {
117- return getDayId ;
118- }
119-
120- const diff = range . end - range . start ;
121- if ( diff <= 86400000 ) {
122- return get5MinuteId ;
123- } else if ( diff <= 604800000 ) {
124- return getHourId ;
125- } else {
126- return getDayId ;
127- }
128- default :
129- return getDayId
130- }
131- }
132-
133- const getTimeUnit = ( period : Period , data : NginxLog [ ] ) => {
134- switch ( period ) {
135- case '24 hours' :
136- return 'minute'
137- case 'week' :
138- return 'hour'
139- case 'month' :
140- return 'hour'
141- case '6 months' :
142- return 'day'
143- case 'all time' :
144- const range = getDateRange ( data ) ;
145- if ( ! range ) {
146- return 'day' ;
147- }
148-
149- const diff = range . end - range . start ;
150- if ( diff <= 86400000 ) {
151- return 'minute' ;
152- } else if ( diff <= 604800000 ) {
153- return 'hour' ;
154- } else {
155- return 'day' ;
156- }
157- default :
158- return 'day'
159- }
160- }
161-
16218const getSuccessRateLevel = ( successRate : number | null ) => {
16319 if ( successRate === null ) {
16420 return null
@@ -204,7 +60,21 @@ function calculateDisplayRates(rates: { timestamp: number, value: number | null
20460 return sampled ;
20561}
20662
207- function Activity ( { data, period } : { data : NginxLog [ ] , period : Period } ) {
63+ function Activity ( {
64+ period,
65+ activityBuckets,
66+ activitySuccessRates,
67+ activityPeriodLabels,
68+ activityStepSize,
69+ activityTimeUnit,
70+ } : {
71+ period : Period ;
72+ activityBuckets : { timestamp : number ; requests : number ; users : number } [ ] ;
73+ activitySuccessRates : { timestamp : number ; successRate : number | null } [ ] ;
74+ activityPeriodLabels : { start : string ; end : string } ;
75+ activityStepSize : number ;
76+ activityTimeUnit : 'minute' | 'hour' | 'day' ;
77+ } ) {
20878 const [ containerWidth , setContainerWidth ] = useState < number > ( 0 ) ;
20979 const containerRef = useRef < HTMLDivElement > ( null ) ;
21080
@@ -239,64 +109,15 @@ function Activity({ data, period }: { data: NginxLog[], period: Period }) {
239109 } ;
240110 } , [ ] ) ; // Remove containerWidth from dependencies to prevent circular updates
241111
242- // Single pass over data to compute chart data, success rates, and period labels
243- const { plotData, plotOptions, successRates, periodLabels } = useMemo ( ( ) => {
244- const chartPoints : { [ id : string ] : { requests : number ; users : Set < string > } } = { } ;
245- const ratePoints : { [ id : string ] : { success : number , total : number } } = { } ;
246-
247- const start = periodStart ( period ) ;
248- const getTimeId = getTimeIdGetter ( period , data ) ;
249-
250- let currentDate : Date ;
251- let end : Date ;
252- if ( start === null ) {
253- const range = getDateRange ( data ) ;
254- if ( ! range ) {
255- return { plotData : null , plotOptions : null , successRates : [ ] , periodLabels : { start : '' , end : '' } } ;
256- }
257- currentDate = new Date ( range . start ) ;
258- end = new Date ( range . end ) ;
259- } else {
260- end = new Date ( ) ;
261- currentDate = new Date ( start ) ;
262- }
263-
264- while ( currentDate <= end ) {
265- const timeId = getTimeId ( currentDate ) ;
266- chartPoints [ timeId ] = { requests : 0 , users : new Set ( ) } ;
267- ratePoints [ timeId ] = { success : 0 , total : 0 } ;
268- currentDate = incrementDate ( currentDate , period ) ;
269- }
270-
271- // Single loop over data for both chart points and success rates
272- for ( const row of data ) {
273- if ( ! row . timestamp ) continue ;
274-
275- const timeId = getTimeId ( row . timestamp ) ;
276- const userId = `${ row . ipAddress } ::${ row . userAgent } ` ;
277-
278- if ( chartPoints [ timeId ] ) {
279- chartPoints [ timeId ] . requests ++ ;
280- chartPoints [ timeId ] . users . add ( userId ) ;
281- } else {
282- chartPoints [ timeId ] = { requests : 1 , users : new Set ( [ userId ] ) } ;
283- }
284-
285- if ( row . status ) {
286- if ( ! ratePoints [ timeId ] ) {
287- ratePoints [ timeId ] = { success : 0 , total : 0 } ;
288- }
289- if ( row . status >= 200 && row . status <= 399 ) {
290- ratePoints [ timeId ] . success ++ ;
291- }
292- ratePoints [ timeId ] . total ++ ;
293- }
112+ const { plotData, plotOptions, successRates } = useMemo ( ( ) => {
113+ if ( activityBuckets . length === 0 ) {
114+ return { plotData : null , plotOptions : null , successRates : [ ] } ;
294115 }
295116
296- const values = Object . entries ( chartPoints ) . map ( ( [ x , y ] ) => ( {
297- x : new Date ( parseInt ( x ) ) ,
298- requests : y . requests - y . users . size ,
299- users : y . users . size
117+ const values = activityBuckets . map ( b => ( {
118+ x : new Date ( b . timestamp ) ,
119+ requests : b . requests ,
120+ users : b . users ,
300121 } ) ) ;
301122
302123 const plotData : ChartData < "bar" > = {
@@ -320,31 +141,34 @@ function Activity({ data, period }: { data: NginxLog[], period: Period }) {
320141 ]
321142 } ;
322143
144+ // For the x-axis max, we need the current time bucketed to the same granularity
145+ let nowId : number ;
146+ switch ( activityTimeUnit ) {
147+ case 'minute' : nowId = Math . round ( Date . now ( ) / 300000 ) * 300000 ; break ;
148+ case 'hour' : nowId = new Date ( ) . setMinutes ( 0 , 0 , 0 ) ; break ;
149+ default : nowId = new Date ( ) . setHours ( 0 , 0 , 0 , 0 ) ; break ;
150+ }
151+
323152 const plotOptions : object = {
324153 scales : {
325154 x : {
326155 type : 'time' ,
327156 display : false ,
328- grid : {
329- display : false
330- } ,
331- max : period === 'all time' ? undefined : getTimeId ( new Date ( ) )
157+ grid : { display : false } ,
158+ time : { unit : activityTimeUnit , stepSize : activityStepSize } ,
159+ max : period === 'all time' ? undefined : nowId ,
332160 } ,
333161 y : {
334162 display : false ,
335- title : {
336- text : 'Requests'
337- } ,
163+ title : { text : 'Requests' } ,
338164 min : 0 ,
339- stacked : true
165+ stacked : true ,
340166 }
341167 } ,
342168 maintainAspectRatio : false ,
343169 responsive : true ,
344170 plugins : {
345- legend : {
346- display : false
347- } ,
171+ legend : { display : false } ,
348172 tooltip : {
349173 enabled : true ,
350174 callbacks : {
@@ -365,42 +189,13 @@ function Activity({ data, period }: { data: NginxLog[], period: Period }) {
365189 }
366190 } ;
367191
368- const successRates = Object . entries ( ratePoints )
369- . sort ( ( [ t1 ] , [ t2 ] ) => Number ( t2 ) - Number ( t1 ) )
370- . map ( ( [ timeId , value ] ) => ( {
371- timestamp : Number ( timeId ) ,
372- value : value . total ? value . success / value . total : null
373- } ) )
374- . reverse ( ) ;
375-
376- let periodLabels = { start : '' , end : '' } ;
377- switch ( period ) {
378- case '24 hours' :
379- periodLabels = { start : '24 hours ago' , end : 'Now' } ;
380- break ;
381- case 'week' :
382- periodLabels = { start : 'One week ago' , end : 'Now' } ;
383- break ;
384- case 'month' :
385- periodLabels = { start : 'One month ago' , end : 'Now' } ;
386- break ;
387- case '6 months' :
388- periodLabels = { start : 'Six months ago' , end : 'Now' } ;
389- break ;
390- default : {
391- const range = getDateRange ( data ) ;
392- if ( range ) {
393- periodLabels = {
394- start : new Date ( range . start ) . toLocaleDateString ( ) ,
395- end : new Date ( range . end ) . toLocaleDateString ( )
396- } ;
397- }
398- break ;
399- }
400- }
192+ const successRates = activitySuccessRates . map ( r => ( {
193+ timestamp : r . timestamp ,
194+ value : r . successRate ,
195+ } ) ) ;
401196
402- return { plotData, plotOptions, successRates, periodLabels } ;
403- } , [ data , period ] ) ;
197+ return { plotData, plotOptions, successRates } ;
198+ } , [ activityBuckets , activitySuccessRates , activityStepSize , activityTimeUnit , period ] ) ;
404199
405200 // Only recalculate display rates when successRates or container width changes
406201 const displayRates = useMemo (
@@ -446,8 +241,8 @@ function Activity({ data, period }: { data: NginxLog[], period: Period }) {
446241
447242 < div className = "pb-0" >
448243 < div className = "flex justify-between mt-2 mb-1 overflow-hidden text-xs text-[var(--text-muted3)] mx-1" >
449- < div > { periodLabels . start } </ div >
450- < div > { periodLabels . end } </ div >
244+ < div > { activityPeriodLabels . start } </ div >
245+ < div > { activityPeriodLabels . end } </ div >
451246 </ div >
452247 </ div >
453248 </ div >
0 commit comments