@@ -168,49 +168,9 @@ const METRICS = [
168168 'total_high_prevented' ,
169169 'total_medium_prevented' ,
170170 'total_low_prevented'
171- ]
171+ ] as const
172172
173- async function fetchOrgAnalyticsData (
174- time : number ,
175- spinner : Spinner ,
176- apiToken : string ,
177- outputJson : boolean ,
178- filePath : string
179- ) : Promise < void > {
180- const socketSdk = await setupSdk ( apiToken )
181- const result = await handleApiCall (
182- socketSdk . getOrgAnalytics ( time . toString ( ) ) ,
183- 'fetching analytics data'
184- )
185-
186- if ( result . success === false ) {
187- return handleUnsuccessfulApiResponse ( 'getOrgAnalytics' , result , spinner )
188- }
189-
190- spinner . stop ( )
191-
192- if ( ! result . data . length ) {
193- return console . log (
194- 'No analytics data is available for this organization yet.'
195- )
196- }
197- const data = formatData ( result . data , 'org' )
198- if ( outputJson && ! filePath ) {
199- return console . log ( result . data )
200- }
201- if ( filePath ) {
202- try {
203- await fs . writeFile ( filePath , JSON . stringify ( result . data ) , 'utf8' )
204- console . log ( `Data successfully written to ${ filePath } ` )
205- } catch ( e : any ) {
206- console . error ( e )
207- }
208- return
209- }
210- return displayAnalyticsScreen ( data )
211- }
212-
213- const months = [
173+ const MONTHS = [
214174 'Jan' ,
215175 'Feb' ,
216176 'Mar' ,
@@ -223,159 +183,9 @@ const months = [
223183 'Oct' ,
224184 'Nov' ,
225185 'Dec'
226- ]
227-
228- const formatDate = ( date : string ) => {
229- return `${ months [ new Date ( date ) . getMonth ( ) ] } ${ new Date ( date ) . getDate ( ) } `
230- }
231-
232- const formatData = ( data : any , scope : string ) => {
233- let formattedData , sortedTopFiveAlerts
234-
235- if ( scope === 'org' ) {
236- const topFiveAlerts = data . map (
237- ( d : { [ k : string ] : any } ) => d [ 'top_five_alert_types' ]
238- )
239-
240- const totalTopAlerts : { [ key : string ] : number } = topFiveAlerts . reduce (
241- ( acc : { [ k : string ] : number } , current : { [ key : string ] : number } ) => {
242- const alertTypes = Object . keys ( current )
243- alertTypes . map ( ( type : string ) => {
244- if ( ! acc [ type ] ) {
245- acc [ type ] = current [ type ] !
246- } else {
247- acc [ type ] += current [ type ] !
248- }
249- return acc
250- } )
251- return acc
252- } ,
253- { } as { [ k : string ] : number }
254- )
255-
256- sortedTopFiveAlerts = Object . entries ( totalTopAlerts )
257- . sort ( ( { 1 : a } , { 1 : b } ) => b - a )
258- . slice ( 0 , 5 )
259- . reduce (
260- ( r , { 0 : k , 1 : v } ) => {
261- r [ k ] = v
262- return r
263- } ,
264- { } as typeof totalTopAlerts
265- )
266-
267- const formatData = ( label : string ) => {
268- return data . reduce (
269- ( acc : { [ k : string ] : number } , current : { [ key : string ] : any } ) => {
270- const date : string = formatDate ( current [ 'created_at' ] )
271- if ( ! acc [ date ] ) {
272- acc [ date ] = current [ label ] !
273- } else {
274- acc [ date ] += current [ label ] !
275- }
276- return acc
277- } ,
278- { }
279- )
280- }
281-
282- formattedData = METRICS . reduce (
283- ( acc , current : string ) => {
284- acc [ current ] = formatData ( current )
285- return acc
286- } ,
287- { } as { [ k : string ] : number }
288- )
289- } else if ( scope === 'repo' ) {
290- const topAlerts : { [ key : string ] : number } = data . reduce (
291- ( acc : { [ key : string ] : number } , current : { [ key : string ] : any } ) => {
292- const alertTypes = Object . keys ( current [ 'top_five_alert_types' ] )
293- alertTypes . map ( type => {
294- if ( ! acc [ type ] ) {
295- acc [ type ] = current [ 'top_five_alert_types' ] [ type ]
296- } else {
297- if ( current [ 'top_five_alert_types' ] [ type ] > ( acc [ type ] || 0 ) ) {
298- acc [ type ] = current [ 'top_five_alert_types' ] [ type ]
299- }
300- }
301- return acc
302- } )
303- return acc
304- } ,
305- { } as { [ key : string ] : number }
306- )
307-
308- sortedTopFiveAlerts = Object . entries ( topAlerts )
309- . sort ( ( { 1 : a } , { 1 : b } ) => b - a )
310- . slice ( 0 , 5 )
311- . reduce (
312- ( r , { 0 : k , 1 : v } ) => {
313- r [ k ] = v
314- return r
315- } ,
316- { } as typeof topAlerts
317- )
318-
319- formattedData = data . reduce (
320- ( acc : any , current : { [ key : string ] : any } ) => {
321- METRICS . forEach ( ( m : string ) => {
322- if ( ! acc [ m ] ) {
323- acc [ m ] = { }
324- }
325- acc [ m ] [ formatDate ( current [ 'created_at' ] ) ] = current [ m ]
326- return acc
327- } )
328- return acc
329- } ,
330- { } as { [ k : string ] : number }
331- )
332- }
333-
334- return { ...formattedData , top_five_alert_types : sortedTopFiveAlerts }
335- }
336-
337- async function fetchRepoAnalyticsData (
338- repo : string ,
339- time : number ,
340- spinner : Spinner ,
341- apiToken : string ,
342- outputJson : boolean ,
343- filePath : string
344- ) : Promise < void > {
345- const socketSdk = await setupSdk ( apiToken )
346- const result = await handleApiCall (
347- socketSdk . getRepoAnalytics ( repo , time . toString ( ) ) ,
348- 'fetching analytics data'
349- )
350-
351- if ( result . success === false ) {
352- return handleUnsuccessfulApiResponse ( 'getRepoAnalytics' , result , spinner )
353- }
354-
355- spinner . stop ( )
356-
357- if ( ! result . data . length ) {
358- return console . log (
359- 'No analytics data is available for this organization yet.'
360- )
361- }
362- const data = formatData ( result . data , 'repo' )
363- if ( outputJson && ! filePath ) {
364- return console . log ( result . data )
365- }
366- if ( filePath ) {
367- try {
368- await fs . writeFile ( filePath , JSON . stringify ( result . data ) , 'utf8' )
369- console . log ( `Data successfully written to ${ filePath } ` )
370- } catch ( e : any ) {
371- console . error ( e )
372- }
373- return
374- }
375- return displayAnalyticsScreen ( data )
376- }
186+ ] as const
377187
378- const displayAnalyticsScreen = ( data : any ) => {
188+ function displayAnalyticsScreen ( data : any ) {
379189 const screen = new ScreenWidget ( )
380190 const grid = new GridLayout ( { rows : 5 , cols : 4 , screen } )
381191
@@ -457,13 +267,181 @@ const displayAnalyticsScreen = (data: any) => {
457267 screen . key ( [ 'escape' , 'q' , 'C-c' ] , ( ) => process . exit ( 0 ) )
458268}
459269
460- const renderLineCharts = (
270+ async function fetchOrgAnalyticsData (
271+ time : number ,
272+ spinner : Spinner ,
273+ apiToken : string ,
274+ outputJson : boolean ,
275+ filePath : string
276+ ) : Promise < void > {
277+ const socketSdk = await setupSdk ( apiToken )
278+ const result = await handleApiCall (
279+ socketSdk . getOrgAnalytics ( time . toString ( ) ) ,
280+ 'fetching analytics data'
281+ )
282+
283+ if ( result . success === false ) {
284+ return handleUnsuccessfulApiResponse ( 'getOrgAnalytics' , result , spinner )
285+ }
286+
287+ spinner . stop ( )
288+
289+ if ( ! result . data . length ) {
290+ return console . log (
291+ 'No analytics data is available for this organization yet.'
292+ )
293+ }
294+ const data = formatData ( result . data , 'org' )
295+ if ( outputJson && ! filePath ) {
296+ console . log ( result . data )
297+ return
298+ }
299+ if ( filePath ) {
300+ try {
301+ await fs . writeFile ( filePath , JSON . stringify ( result . data ) , 'utf8' )
302+ console . log ( `Data successfully written to ${ filePath } ` )
303+ } catch ( e : any ) {
304+ console . error ( e )
305+ }
306+ return
307+ }
308+ return displayAnalyticsScreen ( data )
309+ }
310+
311+ async function fetchRepoAnalyticsData (
312+ repo : string ,
313+ time : number ,
314+ spinner : Spinner ,
315+ apiToken : string ,
316+ outputJson : boolean ,
317+ filePath : string
318+ ) : Promise < void > {
319+ const socketSdk = await setupSdk ( apiToken )
320+ const result = await handleApiCall (
321+ socketSdk . getRepoAnalytics ( repo , time . toString ( ) ) ,
322+ 'fetching analytics data'
323+ )
324+
325+ if ( result . success === false ) {
326+ return handleUnsuccessfulApiResponse ( 'getRepoAnalytics' , result , spinner )
327+ }
328+
329+ spinner . stop ( )
330+
331+ if ( ! result . data . length ) {
332+ return console . log (
333+ 'No analytics data is available for this organization yet.'
334+ )
335+ }
336+ const data = formatData ( result . data , 'repo' )
337+ if ( outputJson && ! filePath ) {
338+ return console . log ( result . data )
339+ }
340+ if ( filePath ) {
341+ try {
342+ await fs . writeFile ( filePath , JSON . stringify ( result . data ) , 'utf8' )
343+ console . log ( `Data successfully written to ${ filePath } ` )
344+ } catch ( e : any ) {
345+ console . error ( e )
346+ }
347+ return
348+ }
349+ return displayAnalyticsScreen ( data )
350+ }
351+
352+ type FormattedData = {
353+ top_five_alert_types : { [ key : string ] : number }
354+ total_critical_alerts : { [ key : string ] : number }
355+ total_high_alerts : { [ key : string ] : number }
356+ total_medium_alerts : { [ key : string ] : number }
357+ total_low_alerts : { [ key : string ] : number }
358+ total_critical_added : { [ key : string ] : number }
359+ total_medium_added : { [ key : string ] : number }
360+ total_low_added : { [ key : string ] : number }
361+ total_high_added : { [ key : string ] : number }
362+ total_critical_prevented : { [ key : string ] : number }
363+ total_high_prevented : { [ key : string ] : number }
364+ total_medium_prevented : { [ key : string ] : number }
365+ total_low_prevented : { [ key : string ] : number }
366+ }
367+
368+ function formatData (
369+ data : { [ key : string ] : any } [ ] ,
370+ scope : string
371+ ) : FormattedData {
372+ const formattedData = < Omit < FormattedData , 'top_five_alert_types' > > { }
373+ const sortedTopFiveAlerts : { [ key : string ] : number } = { }
374+ const totalTopAlerts : { [ key : string ] : number } = { }
375+
376+ for ( const metric of METRICS ) {
377+ formattedData [ metric ] = { }
378+ }
379+ if ( scope === 'org' ) {
380+ for ( const entry of data ) {
381+ const topFiveAlertTypes = entry [ 'top_five_alert_types' ]
382+ for ( const type of Object . keys ( topFiveAlertTypes ) ) {
383+ const count = topFiveAlertTypes [ type ] ?? 0
384+ if ( ! totalTopAlerts [ type ] ) {
385+ totalTopAlerts [ type ] = count
386+ } else {
387+ totalTopAlerts [ type ] += count
388+ }
389+ }
390+ }
391+ for ( const metric of METRICS ) {
392+ const formatted = formattedData [ metric ]
393+ for ( const entry of data ) {
394+ const date = formatDate ( entry [ 'created_at' ] )
395+ if ( ! formatted [ date ] ) {
396+ formatted [ date ] = entry [ metric ] !
397+ } else {
398+ formatted [ date ] += entry [ metric ] !
399+ }
400+ }
401+ }
402+ } else if ( scope === 'repo' ) {
403+ for ( const entry of data ) {
404+ const topFiveAlertTypes = entry [ 'top_five_alert_types' ]
405+ for ( const type of Object . keys ( topFiveAlertTypes ) ) {
406+ const count = topFiveAlertTypes [ type ] ?? 0
407+ if ( ! totalTopAlerts [ type ] ) {
408+ totalTopAlerts [ type ] = count
409+ } else if ( count > ( totalTopAlerts [ type ] ?? 0 ) ) {
410+ totalTopAlerts [ type ] = count
411+ }
412+ }
413+ }
414+ for ( const entry of data ) {
415+ for ( const metric of METRICS ) {
416+ formattedData [ metric ] ! [ formatDate ( entry [ 'created_at' ] ) ] = entry [ metric ]
417+ }
418+ }
419+ }
420+
421+ const topFiveAlertEntries = Object . entries ( totalTopAlerts )
422+ . sort ( ( { 1 : a } , { 1 : b } ) => b - a )
423+ . slice ( 0 , 5 )
424+ for ( const { 0 : key , 1 : value } of topFiveAlertEntries ) {
425+ sortedTopFiveAlerts [ key ] = value
426+ }
427+
428+ return {
429+ ...formattedData ,
430+ top_five_alert_types : sortedTopFiveAlerts
431+ }
432+ }
433+
434+ function formatDate ( date : string ) {
435+ return `${ MONTHS [ new Date ( date ) . getMonth ( ) ] } ${ new Date ( date ) . getDate ( ) } `
436+ }
437+
438+ function renderLineCharts (
461439 grid : any ,
462440 screen : any ,
463441 title : string ,
464442 coords : number [ ] ,
465443 data : { [ key : string ] : number }
466- ) => {
444+ ) {
467445 const line = grid . set ( ...coords , LineChart , {
468446 style : { line : 'cyan' , text : 'cyan' , baseline : 'black' } ,
469447 xLabelPadding : 0 ,
0 commit comments