From 30c330edc0b3541cd3c52535609070831df21b07 Mon Sep 17 00:00:00 2001 From: Faisal Mehmood Date: Wed, 11 Feb 2026 17:12:56 +0500 Subject: [PATCH 1/5] feat: implement asset discovery dashboard for LFX challenge - Integrated Carbon Design System components for asset visualization. - Fixed MUI Select out-of-range warnings with value fallbacks. - Optimized provider injection to prevent MetaMask connection race conditions. --- src/components/Controls/Edit.js | 50 +++-- src/components/Nav/SidebarNav.js | 5 +- src/css/index.css | 10 + src/pages/Assets.js | 356 +++++++++++++++++++++++++++++++ src/route.js | 7 +- src/services/assets.js | 39 ++++ src/services/assets.mock.js | 13 ++ 7 files changed, 455 insertions(+), 25 deletions(-) create mode 100644 src/pages/Assets.js create mode 100644 src/services/assets.js create mode 100644 src/services/assets.mock.js diff --git a/src/components/Controls/Edit.js b/src/components/Controls/Edit.js index 9e3abf47..3736168b 100644 --- a/src/components/Controls/Edit.js +++ b/src/components/Controls/Edit.js @@ -2,72 +2,78 @@ import FormControl from "@mui/material/FormControl"; import InputLabel from "@mui/material/InputLabel"; import MenuItem from "@mui/material/MenuItem"; import Select from "@mui/material/Select"; - import React from "react"; - import SelectWindow from "../SelectWindow"; function EditControl({ - windowOptions, - window, + windowOptions = [], + window = "", setWindow, - aggregationOptions, - aggregateBy, + aggregationOptions = [], + aggregateBy = "", setAggregateBy, - accumulateOptions, - accumulate, + accumulateOptions = [], + accumulate = "", setAccumulate, - currencyOptions, - currency, + currencyOptions = [], + currency = "", setCurrency, }) { return (
+ {/* Time Window Selector */} + + {/* Breakdown / Aggregation Selector */} Breakdown + + {/* Resolution / Accumulate Selector */} Resolution + + {/* Currency Selector */} Currency @@ -76,4 +82,4 @@ function EditControl({ ); } -export default React.memo(EditControl); +export default React.memo(EditControl); \ No newline at end of file diff --git a/src/components/Nav/SidebarNav.js b/src/components/Nav/SidebarNav.js index 781184d7..ecd38c32 100644 --- a/src/components/Nav/SidebarNav.js +++ b/src/components/Nav/SidebarNav.js @@ -2,7 +2,7 @@ import * as React from "react"; import { Drawer, List } from "@mui/material"; import { NavItem } from "./NavItem"; -import { BarChart, Cloud } from "@mui/icons-material"; +import { BarChart, Cloud, Storage } from "@mui/icons-material"; const logo = new URL("../../images/logo.png", import.meta.url).href; @@ -25,6 +25,7 @@ const SidebarNav = ({ active }) => { }, { name: "Cloud Costs", href: "/cloud", icon: }, { name: "External Costs", href: "/external-costs", icon: }, + {name: "Assets", href: "/assets", icon: }, ]; return ( @@ -57,4 +58,4 @@ const SidebarNav = ({ active }) => { ); }; -export { SidebarNav }; +export { SidebarNav }; \ No newline at end of file diff --git a/src/css/index.css b/src/css/index.css index 8ace1e31..af365e96 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -18,3 +18,13 @@ body .page-container { .recharts-tooltip-wrapper { z-index: 1000; } +/* Increase padding and make click target feel bigger */ +.cds--overflow-menu-options__option-content { + padding-block: 0.25rem; + padding-inline: 0.75rem; +} + +/* Ensure the option row has a visible hover background */ +.cds--overflow-menu-options__btn:hover { + background-color: var(--cds-layer-hover, #e5e5e5); +} \ No newline at end of file diff --git a/src/pages/Assets.js b/src/pages/Assets.js new file mode 100644 index 00000000..3e05254c --- /dev/null +++ b/src/pages/Assets.js @@ -0,0 +1,356 @@ +import React, { useEffect, useState, useMemo } from "react"; +import { + DataTable, + Table, + TableHead, + TableRow, + TableHeader, + TableBody, + TableCell, + TableContainer, + TableToolbar, + TableToolbarContent, + TableToolbarSearch, + Pagination, + Tag, + TableExpandHeader, + TableExpandRow, + TableExpandedRow, + Tile, + Layer, + Section, + Heading, + Button, + OverflowMenu, + OverflowMenuItem, + Grid, + Column, + DataTableSkeleton, +} from "@carbon/react"; +import { Renew, Download, Launch } from "@carbon/icons-react"; + +import Page from "../components/Page"; +import Header from "../components/Header"; +import Footer from "../components/Footer"; +import Controls from "../components/Controls"; +import AssetsService from "../services/assets"; +import { toCurrency } from "../util"; + +// Constants for the Select controls to prevent "out-of-range" errors +const WINDOW_OPTIONS = ["1d", "2d", "7d", "30d", "all"]; +const CURRENCY_OPTIONS = ["USD", "EUR", "GBP", "INR", "JPY"]; + +const Assets = () => { + const [assets, setAssets] = useState([]); + const [filteredAssets, setFilteredAssets] = useState([]); + const [loading, setLoading] = useState(true); + const [window, setWindow] = useState("7d"); + const [currency, setCurrency] = useState("USD"); + const [firstRowIndex, setFirstRowIndex] = useState(0); + const [currentPageSize, setCurrentPageSize] = useState(10); + + const headers = [ + { header: "Asset Name", key: "name" }, + { header: "Category", key: "category" }, + { header: "Type", key: "type" }, + { header: "Cluster", key: "cluster" }, + { header: "Total Cost", key: "totalCost" }, + ]; + + const fetchData = async () => { + setLoading(true); + try { + const data = await AssetsService.fetchAssets(window); + const safeData = Array.isArray(data) ? data : []; + setAssets(safeData); + setFilteredAssets(safeData); + setFirstRowIndex(0); + } catch (e) { + console.error("Fetch error:", e); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [window]); + + const assetById = useMemo(() => { + const map = new Map(); + assets.forEach((asset, index) => { + const id = asset.id || asset.providerId || asset.name || `asset-${index}`; + map.set(id, asset); + }); + return map; + }, [assets]); + + const tableRows = useMemo(() => { + return filteredAssets.map((asset, index) => { + const id = asset.id || asset.providerId || asset.name || `asset-${index}`; + return { + id, + name: asset.name || "(unnamed asset)", + category: asset.category || "Unknown", + type: asset.type || "Unknown", + cluster: asset.cluster || "Unknown", + totalCost: asset.totalCost || 0, + ...asset, + }; + }); + }, [filteredAssets]); + + const handleSearch = (e) => { + const rawValue = e.target.value || ""; + const value = rawValue.toLowerCase().trim(); + + if (!value) { + setFilteredAssets(assets); + setFirstRowIndex(0); + return; + } + + const filtered = assets.filter((asset) => { + const name = (asset.name || "").toLowerCase(); + const type = (asset.type || "").toLowerCase(); + const cluster = (asset.cluster || "").toLowerCase(); + return ( + name.includes(value) || + type.includes(value) || + cluster.includes(value) + ); + }); + + setFilteredAssets(filtered); + setFirstRowIndex(0); + }; + + const stats = useMemo(() => { + const total = assets.reduce((sum, a) => sum + (a.totalCost || 0), 0); + const compute = assets.filter( + (a) => (a.category || "").toLowerCase() === "compute" + ).length; + const storage = assets.filter( + (a) => (a.category || "").toLowerCase() === "storage" + ).length; + return { total, compute, storage, count: assets.length }; + }, [assets]); + + const pagedRows = useMemo(() => { + return tableRows.slice(firstRowIndex, firstRowIndex + currentPageSize); + }, [tableRows, firstRowIndex, currentPageSize]); + + return ( + +
+
+ + + +
+ + + + Total Spend ({window}) +

+ {toCurrency(stats.total, currency)} +

+
+
+ + + Active Assets +

{stats.count}

+
+
+ + + Compute / Storage Ratio +

+ {stats.compute} : {stats.storage} +

+
+
+
+ +
+ + + + {loading ? ( + + ) : ( + + {({ + rows, + headers, + getHeaderProps, + getTableProps, + getRowProps, + }) => { + const tableProps = getTableProps(); + const { key: _tableKey, ...tableRest } = tableProps || {}; + return ( + + + + + + + + + + + + {headers.map((header) => { + const hp = getHeaderProps({ header }); + const { key: _h, ...hpRest } = hp || {}; + return ( + + {header.header} + + ); + })} + + + + + {rows.map((row) => { + const fullAsset = assetById.get(row.id); + const rp = getRowProps({ row }); + const { key: _r, ...rpRest } = rp || {}; + return ( + + + {row.cells.map((cell) => ( + + {cell.info.header === "totalCost" ? ( + + {toCurrency(cell.value, currency)} + + ) : cell.info.header === "category" ? ( + + {cell.value} + + ) : ( + cell.value + )} + + ))} + + + + + + + + +
+ + + Identity +

+ Provider ID:{" "} + {fullAsset?.providerId || "—"} +

+

+ Cluster:{" "} + {fullAsset?.cluster || "Unknown"} +

+
+ + Resource Breakdown +
+
+ CPU +

+ {toCurrency(fullAsset?.cpuCost || 0, currency)} +

+
+
+ RAM +

+ {toCurrency(fullAsset?.ramCost || 0, currency)} +

+
+
+
+
+
+
+
+ ); + })} +
+
+
+ ); + }} +
+ )} + { + setCurrentPageSize(pageSize); + setFirstRowIndex((page - 1) * pageSize); + }} + /> +
+
+
+
+
+