|
1 | 1 | import React, { useEffect, useMemo, useRef, useState } from 'react' |
2 | | -import { useCycleTimes, useStocks, useStocksWeek, useThroughputWeek, usePRChangeRequestsWeek, useCloudSpendingMonthly, useCloudSpendingServices } from './api' |
| 2 | +import { useCycleTimes, useStocks, useStocksWeek, useThroughputWeek, usePRChangeRequestsWeek, useCloudSpendingMonthly, useCloudSpendingServices, useCloudSpendingCompared } from './api' |
3 | 3 | import { Card, CardContent, CardHeader, CardTitle } from './components/ui/card' |
4 | 4 | import { Sparkline } from './components/Sparkline' |
5 | 5 | import { LineChart, Point } from './components/LineChart' |
@@ -405,9 +405,11 @@ function CloudSpendingBlock() { |
405 | 405 | const { t } = useTranslation() |
406 | 406 | const monthlyQuery = useCloudSpendingMonthly() |
407 | 407 | const servicesQuery = useCloudSpendingServices() |
| 408 | + const comparedQuery = useCloudSpendingCompared() |
408 | 409 |
|
409 | 410 | const monthlyData = monthlyQuery.data ?? [] |
410 | 411 | const servicesData = servicesQuery.data ?? [] |
| 412 | + const comparedData = comparedQuery.data ?? [] |
411 | 413 |
|
412 | 414 | // Process overall monthly data (Azure & GCP per month) |
413 | 415 | const monthlyLabels = useMemo(() => { |
@@ -506,6 +508,57 @@ function CloudSpendingBlock() { |
506 | 508 | return map |
507 | 509 | }, [servicesData, servicesLabels]) |
508 | 510 |
|
| 511 | + // Process compared services data |
| 512 | + const comparedLabels = useMemo(() => { |
| 513 | + const months = new Set<string>() |
| 514 | + comparedData.forEach(r => { |
| 515 | + const month = r['month'] |
| 516 | + if (month) months.add(month) |
| 517 | + }) |
| 518 | + return Array.from(months).sort() |
| 519 | + }, [comparedData]) |
| 520 | + |
| 521 | + const comparisons = useMemo(() => { |
| 522 | + const map = new Map<string, { stacks: StackSeries[]; currency: string }>() |
| 523 | + // Colors for comparison groups - can be more varied |
| 524 | + const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'] |
| 525 | + |
| 526 | + const tmp: Record<string, Record<string, number[]>> = {} |
| 527 | + const cur: Record<string, Set<string>> = {} |
| 528 | + |
| 529 | + comparedData.forEach(r => { |
| 530 | + const comp = r['comparison'] |
| 531 | + const group = r['group'] |
| 532 | + const month = r['month'] |
| 533 | + if (!comp || !group || !month) return |
| 534 | + const idx = comparedLabels.indexOf(month) |
| 535 | + if (idx < 0) return |
| 536 | + |
| 537 | + if (!tmp[comp]) tmp[comp] = {} |
| 538 | + if (!tmp[comp][group]) tmp[comp][group] = new Array(comparedLabels.length).fill(0) |
| 539 | + tmp[comp][group][idx] = parseNumber(r['cost']) ?? 0 |
| 540 | + |
| 541 | + const c = (r['currency'] || '').toString().trim() |
| 542 | + if (c) { |
| 543 | + if (!cur[comp]) cur[comp] = new Set<string>() |
| 544 | + cur[comp].add(c) |
| 545 | + } |
| 546 | + }) |
| 547 | + |
| 548 | + Object.keys(tmp).forEach(comp => { |
| 549 | + const groups = Object.keys(tmp[comp]).sort() |
| 550 | + const stacks = groups.map((group, i) => ({ |
| 551 | + name: group, |
| 552 | + values: tmp[comp][group], |
| 553 | + color: colors[i % colors.length] |
| 554 | + })) |
| 555 | + const set = cur[comp] |
| 556 | + const currency = set && set.size === 1 ? Array.from(set)[0] : '' |
| 557 | + map.set(comp, { stacks, currency }) |
| 558 | + }) |
| 559 | + return map |
| 560 | + }, [comparedData, comparedLabels]) |
| 561 | + |
509 | 562 | return ( |
510 | 563 | <section> |
511 | 564 | <h2 className="text-xl font-semibold mb-3">{t('cloudSpending.sectionTitle')}</h2> |
@@ -533,6 +586,27 @@ function CloudSpendingBlock() { |
533 | 586 | </CardContent> |
534 | 587 | </Card> |
535 | 588 |
|
| 589 | + {/* Compared services: side-by-side comparison */} |
| 590 | + {comparisons.size > 0 && Array.from(comparisons.entries()).map(([comp, info]) => ( |
| 591 | + <Card key={comp}> |
| 592 | + <CardHeader> |
| 593 | + <CardTitle>{comp}</CardTitle> |
| 594 | + </CardHeader> |
| 595 | + <CardContent> |
| 596 | + <div className="w-full overflow-x-auto"> |
| 597 | + <StackedBarChart |
| 598 | + labels={comparedLabels} |
| 599 | + stacks={info.stacks} |
| 600 | + width={Math.max(900, comparedLabels.length * 60)} |
| 601 | + height={300} |
| 602 | + yAxisLabel={info.currency ? `${t('cloudSpending.amount')} (${info.currency})` : t('cloudSpending.amount')} |
| 603 | + showLegend |
| 604 | + /> |
| 605 | + </div> |
| 606 | + </CardContent> |
| 607 | + </Card> |
| 608 | + ))} |
| 609 | + |
536 | 610 | {/* Service-specific: one chart per service */} |
537 | 611 | {servicesByService.size > 0 ? ( |
538 | 612 | Array.from(servicesByService.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([service, info]) => ( |
|
0 commit comments