Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,319 changes: 1,289 additions & 30 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
],
"dependencies": {
"@babel/runtime": "^7.28.6",
"@carbon/charts": "^1.27.2",
"@carbon/charts-react": "^1.27.2",
"@carbon/react": "^1.100.0",
"@date-io/core": "^3.2.0",
"@date-io/date-fns": "^3.2.1",
"@emotion/react": "^11.14.0",
Expand All @@ -38,6 +41,7 @@
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-transform-runtime": "^7.28.5",
"@babel/preset-react": "^7.28.5",
"baseline-browser-mapping": "^2.9.19",
"buffer": "^6.0.3",
"parcel": "^2.16.3",
"prettier": "^3.8.0",
Expand Down
1 change: 1 addition & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '@carbon/styles/css/styles.css';
import { createRoot } from "react-dom/client";
import Routes from "./route";

Expand Down
123 changes: 123 additions & 0 deletions src/components/Assets/AssetsTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from "react";
import {
DataTable,
TableContainer,
Table,
TableHead,
TableRow,
TableHeader,
TableBody,
TableCell,
TableExpandRow,
TableExpandedRow,
TableExpandHeader,
TableToolbar,
TableToolbarContent,
TableToolbarSearch
} from "@carbon/react";
import { calculateIdlePercent } from "../../services/asset_utils";
import AssetDetailsPanel from "./DatailsPanel";

import { Tag } from "@carbon/react";

function AssetIdleTag({ percent }) {
let kind = "green";
if (percent >= 70) kind = "red";
else if (percent >= 40) kind = "orange";

return (
<Tag type={kind} size="sm">
{percent.toFixed(0)}%
</Tag>
);
}

export default function AssetsTable({ assets }) {
const headers = [
{ key: "type", header: "Type" },
{ key: "category", header: "Category" },
{ key: "provider", header: "Provider" },
{ key: "totalCost", header: "Total Cost" },
{ key: "idlePercent", header: "Idle %" },
{ key: "adjustment", header: "Adjustment" }
];

const rows = assets.map((asset, idx) => {
const idlePercent = calculateIdlePercent(asset);
const totalCost = Number(asset.totalCost ?? 0);
const adjustment = Number(asset.adjustment ?? 0);

return {
id: `${idx}`,
type: asset.type,
category: asset.properties?.category || "N/A",
provider: asset.properties?.provider || "N/A",
totalCost: `$${totalCost.toFixed(2)}`,
idlePercent: idlePercent !== null ? (
<AssetIdleTag percent={idlePercent} />
) : "N/A",
adjustment: `$${adjustment.toFixed(2)}`
};
});

return (
<DataTable rows={rows} headers={headers}>
{({
rows,
headers,
getTableProps,
getHeaderProps,
getRowProps,
getToolbarProps,
onInputChange
}) => (
<TableContainer title="All Assets">
<TableToolbar {...getToolbarProps()}>
<TableToolbarContent>
<TableToolbarSearch
onChange={onInputChange}
placeholder="Search assets..."
persistent
/>
</TableToolbarContent>
</TableToolbar>
<Table {...getTableProps()}>
<TableHead>
<TableRow>
<TableExpandHeader />
{headers.map(header => (
<TableHeader {...getHeaderProps({ header })} key={header.key}>
{header.header}
</TableHeader>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map(row => {
const { key, ...rowProps } = getRowProps({ row });
const assetIndex = parseInt(row.id);
const originalAsset = assets[assetIndex];

return (
<React.Fragment key={row.id}>
<TableExpandRow key={key} {...rowProps}>
{row.cells.map(cell => (
<TableCell key={cell.id}>{cell.value}</TableCell>
))}
</TableExpandRow>

{row.isExpanded && (
<TableExpandedRow colSpan={headers.length + 1}>
<AssetDetailsPanel asset={originalAsset} />
</TableExpandedRow>
)}
</React.Fragment>
);
})}
</TableBody>
</Table>
</TableContainer>
)}
</DataTable>
);
}
61 changes: 61 additions & 0 deletions src/components/Assets/CostDistribution.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Tile, Grid, Column, ProgressBar } from "@carbon/react";
import { DonutChart } from "@carbon/charts-react";
import "@carbon/charts/styles.css";

export default function AssetCostDistribution({ assets, categorized }) {
const totalCost = assets.reduce((sum, a) => sum + a.totalCost, 0);

const donutData = assets
.sort((a, b) => Number(b.totalCost ?? 0) - Number(a.totalCost ?? 0))
.map(asset => ({
group: asset.type,
value: Number(asset.totalCost ?? 0)
}));

const donutOptions = {
title: "Cost by Asset Type",
resizable: true,
donut: {
center: {
label: "Total Cost"
}
},
height: "400px",
theme: "g10",
legend: {
alignment: "center"
}
};

return (
<Tile style={{ padding: 24 }}>
<h4 style={{ marginBottom: 24 }}>Cost Distribution</h4>

<Grid narrow>
<Column sm={4} md={8} lg={8}>
<DonutChart data={donutData} options={donutOptions} />
</Column>

<Column sm={4} md={8} lg={8}>
<h5 style={{ marginBottom: 20 }}>By Category</h5>
{Object.entries(categorized)
.sort(([, a], [, b]) => b - a)
.map(([category, cost]) => {
const percent = (cost / totalCost) * 100;
return (
<div key={category} style={{ marginBottom: 20 }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
<span style={{ fontWeight: 500 }}>{category}</span>
<span style={{ fontWeight: 600 }}>
${cost.toFixed(2)} ({percent.toFixed(0)}%)
</span>
</div>
<ProgressBar value={percent} max={100} size="big" />
</div>
);
})}
</Column>
</Grid>
</Tile>
);
}
158 changes: 158 additions & 0 deletions src/components/Assets/DatailsPanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Grid, Column, Tag } from "@carbon/react";
import { ProgressBar } from "@carbon/react";

function AssetBreakdownBar({ label, value }) {
const percent = value > 1 ? value : value * 100;

return (
<div style={{ marginBottom: 12 }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 4 }}>
<span style={{ textTransform: "capitalize", fontSize: 14 }}>
{label}
</span>
<span style={{ fontSize: 14, fontWeight: 500 }}>
{percent.toFixed(1)}%
</span>
</div>
<ProgressBar value={percent} max={100} size="small" />
</div>
);
}
export default function AssetDetailsPanel({ asset }) {
if (!asset) return null;

const totalCost = Number(asset.totalCost ?? 0);
const adjustment = Number(asset.adjustment ?? 0);

return (
<div style={{ padding: 24 }}>
<h5 style={{ marginBottom: 16 }}>
{asset.type} Details - {asset.properties?.cluster || 'Unknown Cluster'}
</h5>

{/* CPU and RAM Utilization Side by Side */}
{(asset.cpuBreakdown || asset.ramBreakdown) && (
<Grid narrow style={{ marginBottom: 24 }}>
{asset.cpuBreakdown && (
<Column sm={4} md={8} lg={8}>
<div style={{
padding: 16,
background: "#f4f4f4",
borderRadius: 4,
height: "100%"
}}>
<h6 style={{ marginBottom: 12 }}>CPU Utilization</h6>
{Object.entries(asset.cpuBreakdown).map(([key, value]) => (
<AssetBreakdownBar key={key} label={key} value={value} />
))}
</div>
</Column>
)}

{asset.ramBreakdown && (
<Column sm={4} md={8} lg={8}>
<div style={{
padding: 16,
background: "#f4f4f4",
borderRadius: 4,
height: "100%"
}}>
<h6 style={{ marginBottom: 12 }}>RAM Utilization</h6>
{Object.entries(asset.ramBreakdown).map(([key, value]) => (
<AssetBreakdownBar key={key} label={key} value={value} />
))}
</div>
</Column>
)}
</Grid>
)}

{/* Generic Breakdown (Disk, etc.) */}
{asset.breakdown && !asset.cpuBreakdown && (
<div style={{ marginBottom: 24 }}>
<h6 style={{ marginBottom: 12 }}>Usage Breakdown</h6>
{Object.entries(asset.breakdown).map(([key, value]) => (
<AssetBreakdownBar key={key} label={key} value={value} />
))}
</div>
)}

{/* Cost Breakdown */}
<div style={{ padding: 16, background: "#e0e0e0", borderRadius: 4, marginBottom: 16 }}>
<h6 style={{ marginBottom: 12 }}>Cost Breakdown</h6>
<div style={{ fontSize: 14 }}>
{asset.cpuCost > 0 && (
<div style={{ marginBottom: 4 }}>
<span style={{ color: "#525252" }}>CPU Cost:</span>{" "}
<span style={{ fontWeight: 600 }}>${asset.cpuCost.toFixed(2)}</span>
</div>
)}
{asset.ramCost > 0 && (
<div style={{ marginBottom: 4 }}>
<span style={{ color: "#525252" }}>RAM Cost:</span>{" "}
<span style={{ fontWeight: 600 }}>${asset.ramCost.toFixed(2)}</span>
</div>
)}
{asset.gpuCost > 0 && (
<div style={{ marginBottom: 4 }}>
<span style={{ color: "#525252" }}>GPU Cost:</span>{" "}
<span style={{ fontWeight: 600 }}>${asset.gpuCost.toFixed(2)}</span>
</div>
)}
{adjustment !== 0 && (
<div style={{ marginBottom: 4 }}>
<span style={{ color: "#525252" }}>Adjustment:</span>{" "}
<span style={{ fontWeight: 600, color: adjustment < 0 ? "#24a148" : "#da1e28" }}>
${adjustment.toFixed(2)}
</span>
</div>
)}
<div style={{
marginTop: 12,
paddingTop: 12,
borderTop: "1px solid #8d8d8d",
fontWeight: 600,
fontSize: 16
}}>
Total: ${totalCost.toFixed(2)}
</div>
</div>
</div>

{/* Resource Specs */}
{(asset.cpuCores || asset.ramBytes) && (
<div style={{ padding: 16, background: "#f4f4f4", borderRadius: 4, marginBottom: 16 }}>
<h6 style={{ marginBottom: 8 }}>Resource Specifications</h6>
<div style={{ fontSize: 14, color: "#525252" }}>
{asset.cpuCores && <div>CPU Cores: {asset.cpuCores}</div>}
{asset.ramBytes && (
<div>RAM: {(asset.ramBytes / (1024 ** 3)).toFixed(2)} GB</div>
)}
{asset.gpuCount > 0 && <div>GPU Count: {asset.gpuCount}</div>}
</div>
</div>
)}

{/* Labels */}
{asset.labels && Object.keys(asset.labels).length > 0 && (
<div>
<h6 style={{ marginBottom: 8 }}>Labels</h6>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
{Object.entries(asset.labels)
.slice(0, 10)
.map(([key, value]) => (
<Tag key={key} type="gray" size="sm">
{key}: {value}
</Tag>
))}
{Object.keys(asset.labels).length > 10 && (
<Tag type="outline" size="sm">
+{Object.keys(asset.labels).length - 10} more
</Tag>
)}
</div>
</div>
)}
</div>
);
}
Loading