Skip to content

Commit 9e3ebba

Browse files
Merge pull request #116 from NessieCanCode/fix-kpi-tile-updates-and-data-aggregation
Fix fiscal year overview aggregation and charts
2 parents 5c50bc4 + 8597ec3 commit 9e3ebba

File tree

1 file changed

+95
-38
lines changed

1 file changed

+95
-38
lines changed

src/slurmcostmanager.js

Lines changed: 95 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
104142
function 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

322381
function 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

Comments
 (0)