@@ -101,24 +101,70 @@ function useBillingData(period) {
101101 return { data, error, reload : load } ;
102102}
103103
104+ function aggregateAccountDetails ( details = [ ] ) {
105+ const map = { } ;
106+ details . forEach ( d => {
107+ const acct = map [ d . account ] || {
108+ account : d . account ,
109+ core_hours : 0 ,
110+ gpu_hours : 0 ,
111+ cost : 0 ,
112+ users : { }
113+ } ;
114+ acct . core_hours += d . core_hours || 0 ;
115+ acct . gpu_hours += d . gpu_hours || 0 ;
116+ acct . cost += d . cost || 0 ;
117+ ( d . users || [ ] ) . forEach ( u => {
118+ const user = acct . users [ u . user ] || {
119+ user : u . user ,
120+ core_hours : 0 ,
121+ cost : 0
122+ } ;
123+ user . core_hours += u . core_hours || 0 ;
124+ user . cost += u . cost || 0 ;
125+ acct . users [ u . user ] = user ;
126+ } ) ;
127+ map [ d . account ] = acct ;
128+ } ) ;
129+ return Object . values ( map ) . map ( a => ( {
130+ account : a . account ,
131+ core_hours : Math . round ( a . core_hours * 100 ) / 100 ,
132+ gpu_hours : Math . round ( a . gpu_hours * 100 ) / 100 ,
133+ cost : Math . round ( a . cost * 100 ) / 100 ,
134+ users : Object . values ( a . users ) . map ( u => ( {
135+ user : u . user ,
136+ core_hours : Math . round ( u . core_hours * 100 ) / 100 ,
137+ cost : Math . round ( u . cost * 100 ) / 100
138+ } ) )
139+ } ) ) ;
140+ }
141+
104142function AccountsChart ( { details } ) {
105143 const canvasRef = useRef ( null ) ;
106144 useEffect ( ( ) => {
107145 if ( ! canvasRef . current ) return ;
108146 const ctx = canvasRef . current . getContext ( '2d' ) ;
109- const top = details
147+ const aggregated = aggregateAccountDetails ( details ) ;
148+ const top = aggregated
110149 . slice ( )
111- . sort ( ( a , b ) => b . core_hours - a . core_hours )
150+ . sort (
151+ ( a , b ) => b . core_hours + b . gpu_hours - ( a . core_hours + a . gpu_hours )
152+ )
112153 . slice ( 0 , 10 ) ;
113154 const chart = new Chart ( ctx , {
114155 type : 'bar' ,
115156 data : {
116157 labels : top . map ( d => d . account ) ,
117158 datasets : [
118159 {
119- label : 'Core Hours ' ,
160+ label : 'CPU hrs ' ,
120161 data : top . map ( d => d . core_hours ) ,
121162 backgroundColor : '#4e79a7'
163+ } ,
164+ {
165+ label : 'GPU hrs' ,
166+ data : top . map ( d => d . gpu_hours ) ,
167+ backgroundColor : '#f28e2b'
122168 }
123169 ]
124170 } ,
@@ -260,23 +306,32 @@ function BulletChart({ actual, target }) {
260306 return React . createElement ( 'canvas' , { ref : canvasRef , className : 'kpi-chart' , width : 180 , height : 60 } ) ;
261307}
262308
263- function HistoricalUsageChart ( { monthly = [ ] } ) {
309+ function HistoricalUsageChart ( { data = [ ] } ) {
264310 const canvasRef = useRef ( null ) ;
265311 useEffect ( ( ) => {
266- if ( ! canvasRef . current || monthly . length === 0 ) return ;
267- const labels = monthly . map ( m => m . month ) ;
268- const cpu = monthly . map ( m => m . core_hours ) ;
269- const gpu = monthly . map ( m => m . gpu_hours || 0 ) ;
312+ if ( ! canvasRef . current || data . length === 0 ) return ;
313+ const labels = data . map ( m => m . month || m . year ) ;
314+ const cpu = data . map ( m => m . core_hours ) ;
315+ const gpu = data . map ( m => m . gpu_hours || 0 ) ;
316+ const isMonthly = ! ! data [ 0 ] . month ;
270317 const lastLabel = labels [ labels . length - 1 ] ;
271- let [ year , month ] = lastLabel . split ( '-' ) . map ( Number ) ;
272318 const forecastLabels = [ ] ;
273- for ( let i = 0 ; i < 3 ; i ++ ) {
274- month ++ ;
275- if ( month > 12 ) {
276- month = 1 ;
319+ if ( isMonthly ) {
320+ let [ year , month ] = lastLabel . split ( '-' ) . map ( Number ) ;
321+ for ( let i = 0 ; i < 3 ; i ++ ) {
322+ month ++ ;
323+ if ( month > 12 ) {
324+ month = 1 ;
325+ year ++ ;
326+ }
327+ forecastLabels . push ( `${ year } -${ String ( month ) . padStart ( 2 , '0' ) } ` ) ;
328+ }
329+ } else {
330+ let year = Number ( lastLabel ) ;
331+ for ( let i = 0 ; i < 3 ; i ++ ) {
277332 year ++ ;
333+ forecastLabels . push ( String ( year ) ) ;
278334 }
279- forecastLabels . push ( `${ year } -${ String ( month ) . padStart ( 2 , '0' ) } ` ) ;
280335 }
281336 const avg =
282337 cpu . slice ( - 3 ) . reduce ( ( a , b ) => a + b , 0 ) /
@@ -315,8 +370,12 @@ function HistoricalUsageChart({ monthly = [] }) {
315370 options : { responsive : false , maintainAspectRatio : false }
316371 } ) ;
317372 return ( ) => chart . destroy ( ) ;
318- } , [ monthly ] ) ;
319- return React . createElement ( 'div' , { className : 'chart-container' } , React . createElement ( 'canvas' , { ref : canvasRef , width : 600 , height : 300 } ) ) ;
373+ } , [ data ] ) ;
374+ return React . createElement (
375+ 'div' ,
376+ { className : 'chart-container' } ,
377+ React . createElement ( 'canvas' , { ref : canvasRef , width : 600 , height : 300 } )
378+ ) ;
320379}
321380
322381function PiConsumptionChart ( { details, width = 300 , height = 300 , legend = true } ) {
@@ -541,13 +600,17 @@ function SuccessFailChart({ data }) {
541600 return React . createElement ( 'div' , { className : 'chart-container' } , React . createElement ( 'canvas' , { ref : canvasRef , width : 600 , height : 300 } ) ) ;
542601}
543602
544- function Summary ( { summary, details = [ ] , daily = [ ] , monthly = [ ] } ) {
603+ function Summary ( { summary, details = [ ] , daily = [ ] , monthly = [ ] , yearly = [ ] } ) {
545604 const sparklineData = daily . map ( d => d . core_hours ) ;
546605 const gpuSparklineData = daily . map ( d => d . gpu_hours || 0 ) ;
547606 const ratio = summary . projected_revenue
548607 ? summary . total / summary . projected_revenue
549608 : 1 ;
550609 const targetRevenue = summary . projected_revenue || summary . total ;
610+ const historical = yearly . length ? yearly : monthly ;
611+ const historicalLabel = yearly . length
612+ ? 'Historical CPU/GPU-hrs (yearly)'
613+ : 'Historical CPU/GPU-hrs (monthly)' ;
551614
552615 return React . createElement (
553616 'div' ,
@@ -631,8 +694,8 @@ function Summary({ summary, details = [], daily = [], monthly = [] }) {
631694 ) ,
632695 React . createElement ( 'h3' , null , 'CPU/GPU-hrs per Slurm account' ) ,
633696 React . createElement ( AccountsChart , { details } ) ,
634- React . createElement ( 'h3' , null , 'Historical CPU/GPU-hrs (monthly)' ) ,
635- React . createElement ( HistoricalUsageChart , { monthly } )
697+ React . createElement ( 'h3' , null , historicalLabel ) ,
698+ React . createElement ( HistoricalUsageChart , { data : historical } )
636699 ) ;
637700}
638701
@@ -1153,6 +1216,12 @@ function App() {
11531216 const yearPeriod = useMemo ( ( ) => getYearPeriod ( currentYear ) , [ currentYear ] ) ;
11541217 const period = view === 'year' ? yearPeriod : month ;
11551218 const { data, error, reload } = useBillingData ( period ) ;
1219+ const details = useMemo ( ( ) => {
1220+ if ( ! data ) return [ ] ;
1221+ return view === 'year'
1222+ ? aggregateAccountDetails ( data . details || [ ] )
1223+ : data . details || [ ] ;
1224+ } , [ data , view ] ) ;
11561225 const [ showErrorDetails , setShowErrorDetails ] = useState ( false ) ;
11571226 const monthOptions = Array . from (
11581227 { length : now . getMonth ( ) + 1 } ,
@@ -1230,29 +1299,17 @@ function App() {
12301299 ) ,
12311300 data &&
12321301 view === 'year' &&
1233- React . createElement (
1234- React . Fragment ,
1235- null ,
1236- React . createElement ( Summary , {
1237- summary : data . summary ,
1238- details : data . details ,
1239- daily : data . daily ,
1240- monthly : data . monthly
1241- } ) ,
1242- React . createElement ( Details , {
1243- details : data . details ,
1244- daily : data . daily ,
1245- partitions : data . partitions ,
1246- accounts : data . accounts ,
1247- users : data . users ,
1248- monthOptions : [ ]
1249- } )
1250- ) ,
1302+ React . createElement ( Summary , {
1303+ summary : data . summary ,
1304+ details,
1305+ daily : data . daily ,
1306+ yearly : data . yearly
1307+ } ) ,
12511308 data &&
12521309 view === 'summary' &&
12531310 React . createElement ( Summary , {
12541311 summary : data . summary ,
1255- details : data . details ,
1312+ details,
12561313 daily : data . daily ,
12571314 monthly : data . monthly
12581315 } ) ,
0 commit comments