Jelajahi Sumber

Rangechart in

Signed-off-by: Thomas Evans <tevans3@icloud.com>
jjarrett21 2 tahun lalu
induk
melakukan
e47c9a4f6a

+ 193 - 0
ui/src/CloudCost/CloudCost.js

@@ -0,0 +1,193 @@
+import React from "react";
+import { get, round } from "lodash";
+import { makeStyles } from "@material-ui/styles";
+import {
+  Typography,
+  TableContainer,
+  TableCell,
+  TableHead,
+  TablePagination,
+  TableRow,
+  TableSortLabel,
+  Table,
+  TableBody,
+} from "@material-ui/core";
+
+import { toCurrency } from "../util";
+import CloudCostChart from "./CloudCostChart";
+
+const CloudCost = ({ cumulativeData, totalData, graphData, currency }) => {
+  const useStyles = makeStyles({
+    noResults: {
+      padding: 24,
+    },
+  });
+
+  const classes = useStyles();
+
+  function descendingComparator(a, b, orderBy) {
+    if (get(b, orderBy) < get(a, orderBy)) {
+      return -1;
+    }
+    if (get(b, orderBy) > get(a, orderBy)) {
+      return 1;
+    }
+    return 0;
+  }
+
+  function getComparator(order, orderBy) {
+    return order === "desc"
+      ? (a, b) => descendingComparator(a, b, orderBy)
+      : (a, b) => -descendingComparator(a, b, orderBy);
+  }
+
+  function stableSort(array, comparator) {
+    const stabilizedThis = array.map((el, index) => [el, index]);
+    stabilizedThis.sort((a, b) => {
+      const order = comparator(a[0], b[0]);
+      if (order !== 0) return order;
+      return a[1] - b[1];
+    });
+    return stabilizedThis.map((el) => el[0]);
+  }
+
+  const headCells = [
+    { id: "name", numeric: false, label: "Name", width: "auto" },
+    {
+      id: "kubernetesPercent",
+      numeric: true,
+      label: "K8's Utilization",
+      width: 90,
+    },
+    {
+      id: "cost",
+      numeric: true,
+      label: "Sum of Sample Data",
+      width: 90,
+    },
+  ];
+
+  const [order, setOrder] = React.useState("desc");
+  const [orderBy, setOrderBy] = React.useState("totalCost");
+  const [page, setPage] = React.useState(0);
+  const [rowsPerPage, setRowsPerPage] = React.useState(25);
+  const numData = cumulativeData.length;
+
+  const lastPage = Math.floor(numData / rowsPerPage);
+
+  const handleChangePage = (event, newPage) => setPage(newPage);
+
+  const handleChangeRowsPerPage = (event) => {
+    setRowsPerPage(parseInt(event.target.value, 10));
+    setPage(0);
+  };
+
+  const createSortHandler = (property) => (event) =>
+    handleRequestSort(event, property);
+
+  const handleRequestSort = (event, property) => {
+    const isDesc = orderBy === property && order === "desc";
+    setOrder(isDesc ? "asc" : "desc");
+    setOrderBy(property);
+  };
+
+  const orderedRows = stableSort(cumulativeData, getComparator(order, orderBy));
+  const pageRows = orderedRows.slice(
+    page * rowsPerPage,
+    page * rowsPerPage + rowsPerPage
+  );
+
+  React.useEffect(() => {
+    setPage(0);
+  }, [numData]);
+
+  if (cumulativeData.length === 0) {
+    return (
+      <Typography variant="body2" className={classes.noResults}>
+        No results
+      </Typography>
+    );
+  }
+
+  return (
+    <div id="cloud-cost">
+      <div id="cloud-graph-">
+        <CloudCostChart
+          currency={currency}
+          graphData={graphData}
+          height={300}
+          n={10}
+        />
+      </div>
+      <div id="cloud-cost-table">
+        <TableContainer>
+          <Table>
+            <TableHead>
+              <TableRow>
+                {headCells.map((cell) => (
+                  <TableCell
+                    key={cell.id}
+                    colSpan={cell.colspan}
+                    align={cell.numeric ? "right" : "left"}
+                    sortDirection={orderBy === cell.id ? order : false}
+                    style={{ width: cell.width }}
+                  >
+                    <TableSortLabel
+                      active={orderBy === cell.id}
+                      direction={orderBy === cell.id ? order : "asc"}
+                      onClick={createSortHandler(cell.id)}
+                    >
+                      {cell.label}
+                    </TableSortLabel>
+                  </TableCell>
+                ))}
+              </TableRow>
+            </TableHead>
+            <TableBody>
+              <TableRow>
+                {headCells.map((cell) => {
+                  return (
+                    <TableCell
+                      key={cell.id}
+                      colSpan={cell.colspan}
+                      align={cell.numeric ? "right" : "left"}
+                      style={{ fontWeight: 500 }}
+                    >
+                      {cell.id === "kubernetesPercent"
+                        ? round(totalData[cell.id] * 100, 2)
+                        : totalData[cell.id]}
+                    </TableCell>
+                  );
+                })}
+              </TableRow>
+              {pageRows.map((row, key) => {
+                return (
+                  <TableRow key={key}>
+                    <TableCell align="left">{row.name}</TableCell>
+                    <TableCell align="right">
+                      {round(row.kubernetesPercent * 100)}
+                    </TableCell>
+                    <TableCell align="right">
+                      {toCurrency(row.cost, currency)}
+                    </TableCell>
+                  </TableRow>
+                );
+              })}
+            </TableBody>
+          </Table>
+        </TableContainer>
+        <TablePagination
+          component="div"
+          count={numData}
+          rowsPerPage={rowsPerPage}
+          rowsPerPageOptions={[10, 25, 50]}
+          page={Math.min(page, lastPage)}
+          onChangePage={handleChangePage}
+          onChangeRowsPerPage={handleChangeRowsPerPage}
+        />
+      </div>
+    </div>
+  );
+};
+
+export default React.memo(CloudCost);

+ 279 - 0
ui/src/CloudCost/CloudCostChart/RangeChart.js

@@ -0,0 +1,279 @@
+import React from "react";
+import { makeStyles } from "@material-ui/styles";
+import {
+  BarChart,
+  Bar,
+  XAxis,
+  YAxis,
+  CartesianGrid,
+  Tooltip,
+  ResponsiveContainer,
+  Cell,
+} from "recharts";
+import { primary, greyscale, browns } from "../../constants/colors";
+import { toCurrency } from "../../util";
+
+const RangeChart = ({ data, currency, height }) => {
+  const useStyles = makeStyles({
+    tooltip: {
+      borderRadius: 2,
+      background: "rgba(255, 255, 255, 0.95)",
+      padding: 12,
+    },
+    tooltipLineItem: {
+      fontSize: "1rem",
+      margin: 0,
+      marginBottom: 4,
+      padding: 0,
+    },
+  });
+
+  const accents = [...primary, ...greyscale, ...browns];
+
+  const _IDLE_ = "__idle__";
+  const _OTHER_ = "others";
+
+  const getItemCost = (item) => {
+    return item.totalCost;
+  };
+
+  function toBar({ end, graph, start }) {
+    const points = graph.map((item) => ({
+      ...item,
+      window: { end, start },
+    }));
+
+    const dateFormatter = Intl.DateTimeFormat(navigator.language, {
+      year: "numeric",
+      month: "numeric",
+      day: "numeric",
+      timeZone: "UTC",
+    });
+
+    const timeFormatter = Intl.DateTimeFormat(navigator.language, {
+      hour: "numeric",
+      minute: "numeric",
+      timeZone: "UTC",
+    });
+
+    const s = new Date(start);
+    const e = new Date(end);
+    const interval = (e.valueOf() - s.valueOf()) / 1000 / 60 / 60;
+
+    const bar = {
+      end: new Date(end),
+      key: interval >= 24 ? dateFormatter.format(s) : timeFormatter.format(s),
+      items: {},
+      start: new Date(start),
+    };
+
+    points.forEach((item) => {
+      const windowStart = new Date(item.window.start);
+      const windowEnd = new Date(item.window.end);
+      const windowHours =
+        (windowEnd.valueOf() - windowStart.valueOf()) / 1000 / 60 / 60;
+
+      if (windowHours >= 24) {
+        bar.key = dateFormatter.format(bar.start);
+      } else {
+        bar.key = timeFormatter.format(bar.start);
+      }
+
+      bar.items[item.name] = getItemCost(item);
+    });
+
+    return bar;
+  }
+
+  const getDataForCloudDay = (dayData) => {
+    const { end, start } = dayData;
+    const copy = [...dayData.items];
+
+    // find items for idle and other
+    const idleIndex = copy.findIndex((item) => item.name === _IDLE_);
+    let idle = undefined;
+    if (idleIndex > -1) {
+      idle = copy[idleIndex];
+      copy.splice(idleIndex, 1);
+    }
+    const otherIndex = copy.findIndex(
+      (i) => i.name === _OTHER_ || i.name === "other"
+    );
+    let other = undefined;
+    if (otherIndex > -1) {
+      other = { ...copy[otherIndex], name: "other" };
+      copy.splice(otherIndex, 1);
+    }
+
+    // sort and remove any items < top 8
+    const sortedItems = copy.slice().sort((a, b) => {
+      return a.value > b.value ? -1 : 1;
+    });
+
+    const top8 = sortedItems.slice(0, 8);
+    // get items that didn't make the cut and shove into other
+    const lefovers = sortedItems.slice(8);
+    if (lefovers.length > 0) {
+      const othersTotal = lefovers.reduce((a, b) => a.value + b.value);
+      if (other) {
+        other.value += othersTotal;
+      } else if (othersTotal) {
+        other = {
+          name: "other",
+          value: othersTotal,
+        };
+      }
+    }
+    // add in idle and other
+    if (idle) {
+      top8.unshift(idle);
+    }
+    if (other) {
+      top8.unshift(other);
+    }
+
+    return { end, start, graph: top8 };
+  };
+
+  const getDataForGraph = (dataPoints) => {
+    // for each day, we want top 8 + Idle and Other
+    const orderedDataPoints = dataPoints.map(getDataForCloudDay);
+    const bars = orderedDataPoints.map(toBar);
+
+    const keyToFill = {};
+    // we want to keep track of the order of fill assignment
+    const assignmentOrder = [];
+    let p = 0;
+
+    orderedDataPoints.forEach(({ graph, start, end }) => {
+      graph.forEach(({ name }) => {
+        const key = name;
+        if (keyToFill[key] === undefined) {
+          assignmentOrder.push(key);
+          if (key === _IDLE_) {
+            keyToFill[key] = idle;
+          } else if (key === _OTHER_ || key === "other") {
+            keyToFill[key] = accents[0];
+          } else {
+            // non-idle/other allocations get the next available color
+            keyToFill[key] = accents;
+            p = (p + 1) % accents.length;
+          }
+        }
+      });
+    });
+    // list of dataKeys and fillColors in order of importance (price w/ 'others' last)
+    const labels = assignmentOrder.map((dataKey) => ({
+      dataKey,
+      fill: keyToFill[dataKey],
+    }));
+
+    return { bars, labels, keyToFill };
+  };
+
+  const { bars: barData, labels: barLabels, keyToFill } = getDataForGraph(data);
+
+  const classes = useStyles();
+
+  const CustomTooltip = (params) => {
+    const { active, payload } = params;
+
+    if (!payload || payload.length == 0) {
+      return null;
+    }
+
+    const total = payload.reduce((sum, item) => sum + item.value, 0.0);
+    if (active) {
+      return (
+        <div className={classes.tooltip}>
+          <p
+            className={classes.tooltipLineItem}
+            style={{ color: "#000000" }}
+          >{`Total: ${toCurrency(total, currency)}`}</p>
+
+          {payload
+            .slice()
+            .map((item, i) => (
+              <div
+                key={item.name}
+                style={{
+                  display: "grid",
+                  gridTemplateColumns: "20px 1fr",
+                  gap: ".5em",
+                  margin: ".25em",
+                }}
+              >
+                <div>
+                  <div
+                    style={{
+                      backgroundColor: keyToFill[item.payload.items[i][0]],
+                      width: 18,
+                      height: 18,
+                    }}
+                  />
+                </div>
+                <div>
+                  <p className={classes.tooltipLineItem}>{`${
+                    item.payload.items[i][0]
+                  }: ${toCurrency(item.value, modelConfig.currencyCode)}`}</p>
+                </div>
+              </div>
+            ))
+            .reverse()}
+        </div>
+      );
+    }
+
+    return null;
+  };
+
+  const orderedBars = barData.map((bar) => {
+    return {
+      ...bar,
+      items: Object.entries(bar.items).sort((a, b) => {
+        if (a[0] === "other") {
+          return -1;
+        }
+        if (b[0] === "other") {
+          return 1;
+        }
+        return a[1] > b[1] ? -1 : 1;
+      }),
+    };
+  });
+
+  console.log(barLabels);
+  console.log(orderedBars);
+  console.log(keyToFill);
+
+  return (
+    <ResponsiveContainer height={height} width={"100%"}>
+      <BarChart
+        data={orderedBars}
+        margin={{ top: 30, right: 35, left: 30, bottom: 45 }}
+      >
+        <CartesianGrid strokeDasharray={"3 3"} vertical={false} />
+        <XAxis dataKey={"key"} />
+        <YAxis tickFormatter={(val) => toCurrency(val, currency, 2, true)} />
+        <Tooltip content={<CustomTooltip />} wrapperStyle={{ zIndex: 1000 }} />
+
+        {new Array(10).fill(0).map((item, idx) => (
+          <Bar
+            dataKey={(entry) => (entry.items[idx] ? entry.items[idx][1] : null)}
+            stackId="x"
+          >
+            {orderedBars.map((bar) =>
+              bar.items[idx] ? (
+                <Cell fill={keyToFill[bar.items[idx][0]]} />
+              ) : (
+                <Cell />
+              )
+            )}
+          </Bar>
+        ))}
+      </BarChart>
+    </ResponsiveContainer>
+  );
+};
+
+export default RangeChart;

+ 15 - 0
ui/src/CloudCost/CloudCostChart/index.js

@@ -0,0 +1,15 @@
+import React from "react";
+import { isArray, filter, map, reduce, reverse, sortBy } from "lodash";
+
+import Typography from "@material-ui/core/Typography";
+
+import RangeChart from "./RangeChart";
+
+const CloudCostChart = ({ graphData, currency, n, height }) => {
+  if (graphData.length === 0) {
+    return <Typography variant="body2">No data</Typography>;
+  }
+  return <RangeChart data={graphData} currency={currency} height={height} />;
+};
+
+export default React.memo(CloudCostChart);

+ 60 - 5
ui/src/CloudCostReports.js

@@ -7,13 +7,13 @@ import { makeStyles } from "@material-ui/styles";
 import { Paper, Typography } from "@material-ui/core";
 import CircularProgress from "@material-ui/core/CircularProgress";
 import { get, find, sortBy, toArray } from "lodash";
-import { checkCustomWindow, toVerboseTimeRange } from "./util";
-
 import { useLocation, useHistory } from "react-router";
 
+import { checkCustomWindow, toVerboseTimeRange } from "./util";
 import CloudCostEditControls from "./CloudCost/Controls/CloudCostEditControls";
 import Subtitle from "./components/Subtitle";
 import Warnings from "./components/Warnings";
+import CloudCostService from "./services/cloudCost";
 
 import {
   windowOptions,
@@ -21,6 +21,7 @@ import {
   aggregationOptions,
 } from "./CloudCost/tokens";
 import { currencyCodes } from "./constants/currencyCodes";
+import CloudCost from "./CloudCost/CloudCost";
 
 const CloudCostReports = () => {
   const useStyles = makeStyles({
@@ -54,6 +55,9 @@ const CloudCostReports = () => {
   const [loading, setLoading] = React.useState(true);
   const [errors, setErrors] = React.useState([]);
 
+  // data
+  const [cloudCostData, setCloudCostData] = React.useState([]);
+
   function generateTitle({ window, aggregateBy, costMetric }) {
     let windowName = get(find(windowOptions, { value: window }), "name", "");
     if (windowName === "") {
@@ -96,8 +100,52 @@ const CloudCostReports = () => {
     setErrors([]);
     try {
       console.log("look at me fetching data");
-    } catch (error) {
-      console.error(error);
+      const resp = await CloudCostService.fetchCloudCostData(
+        window,
+        aggregateBy,
+        costMetric
+      );
+      if (resp.data) {
+        setCloudCostData(resp.data);
+      } else {
+        if (resp.message && resp.message.indexOf("boundary error") >= 0) {
+          let match = resp.message.match(/(ETL is \d+\.\d+% complete)/);
+          let secondary = "Try again after ETL build is complete";
+          if (match.length > 0) {
+            secondary = `${match[1]}. ${secondary}`;
+          }
+          setErrors([
+            {
+              primary: "Data unavailable while ETL is building",
+              secondary: secondary,
+            },
+          ]);
+        }
+        setCloudCostData([]);
+      }
+    } catch (err) {
+      if (err.message.indexOf("404") === 0) {
+        setErrors([
+          {
+            primary: "Failed to load report data",
+            secondary:
+              "Please update Kubecost to the latest version, then contact support if problems persist.",
+          },
+        ]);
+      } else {
+        let secondary =
+          "Please contact Kubecost support with a bug report if problems persist.";
+        if (err.message.length > 0) {
+          secondary = err.message;
+        }
+        setErrors([
+          {
+            primary: "Failed to load report data",
+            secondary: secondary,
+          },
+        ]);
+      }
+      setCloudCostData([]);
     }
     setLoading(false);
   }
@@ -191,7 +239,14 @@ const CloudCostReports = () => {
             </div>
           )}
 
-          {!loading && <div>Cloud Cost Report goes here</div>}
+          {!loading && (
+            <CloudCost
+              cumulativeData={cloudCostData.tableRows}
+              currency={currency}
+              graphData={cloudCostData.graphData}
+              totalData={cloudCostData.tableTotal}
+            />
+          )}
         </Paper>
       )}
     </Page>

+ 136 - 95
ui/src/components/AllocationReport.js

@@ -1,99 +1,117 @@
-import React, { useEffect, useState } from 'react'
-import { get, round } from 'lodash'
-import { makeStyles } from '@material-ui/styles'
-import Table from '@material-ui/core/Table'
-import TableBody from '@material-ui/core/TableBody'
-import TableCell from '@material-ui/core/TableCell'
-import TableContainer from '@material-ui/core/TableContainer'
-import TableHead from '@material-ui/core/TableHead'
-import TablePagination from '@material-ui/core/TablePagination'
-import TableRow from '@material-ui/core/TableRow'
-import TableSortLabel from '@material-ui/core/TableSortLabel'
-import Typography from '@material-ui/core/Typography'
-import AllocationChart from './AllocationChart';
-import { toCurrency } from '../util';
+import React, { useEffect, useState } from "react";
+import { get, round } from "lodash";
+import { makeStyles } from "@material-ui/styles";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableContainer from "@material-ui/core/TableContainer";
+import TableHead from "@material-ui/core/TableHead";
+import TablePagination from "@material-ui/core/TablePagination";
+import TableRow from "@material-ui/core/TableRow";
+import TableSortLabel from "@material-ui/core/TableSortLabel";
+import Typography from "@material-ui/core/Typography";
+import AllocationChart from "./AllocationChart";
+import { toCurrency } from "../util";
 
 const useStyles = makeStyles({
   noResults: {
     padding: 24,
   },
-})
+});
 
 function descendingComparator(a, b, orderBy) {
   if (get(b, orderBy) < get(a, orderBy)) {
-    return -1
+    return -1;
   }
   if (get(b, orderBy) > get(a, orderBy)) {
-    return 1
+    return 1;
   }
-  return 0
+  return 0;
 }
 
 function getComparator(order, orderBy) {
-  return order === 'desc'
+  return order === "desc"
     ? (a, b) => descendingComparator(a, b, orderBy)
-    : (a, b) => -descendingComparator(a, b, orderBy)
+    : (a, b) => -descendingComparator(a, b, orderBy);
 }
 
 function stableSort(array, comparator) {
-  const stabilizedThis = array.map((el, index) => [el, index])
+  const stabilizedThis = array.map((el, index) => [el, index]);
   stabilizedThis.sort((a, b) => {
-    const order = comparator(a[0], b[0])
-    if (order !== 0) return order
-    return a[1] - b[1]
-  })
-  return stabilizedThis.map((el) => el[0])
+    const order = comparator(a[0], b[0]);
+    if (order !== 0) return order;
+    return a[1] - b[1];
+  });
+  return stabilizedThis.map((el) => el[0]);
 }
 
 const headCells = [
-  { id: 'name', numeric: false, label: 'Name', width: 'auto' },
-  { id: 'cpuCost', numeric: true, label: 'CPU', width: 90 },
-  { id: 'ramCost', numeric: true, label: "RAM", width: 90 },
-  { id: 'pvCost', numeric: true, label: 'PV', width: 90 },
-  { id: 'totalEfficiency', numeric: true, label: 'Efficiency', width: 90 },
-  { id: 'totalCost', numeric: true, label: 'Total cost', width: 90 },
-]
-
-const AllocationReport = ({ allocationData, cumulativeData, totalData, currency }) => {
-  const classes = useStyles()
+  { id: "name", numeric: false, label: "Name", width: "auto" },
+  { id: "cpuCost", numeric: true, label: "CPU", width: 90 },
+  { id: "ramCost", numeric: true, label: "RAM", width: 90 },
+  { id: "pvCost", numeric: true, label: "PV", width: 90 },
+  { id: "totalEfficiency", numeric: true, label: "Efficiency", width: 90 },
+  { id: "totalCost", numeric: true, label: "Total cost", width: 90 },
+];
+
+const AllocationReport = ({
+  allocationData,
+  cumulativeData,
+  totalData,
+  currency,
+}) => {
+  const classes = useStyles();
 
   if (allocationData.length === 0) {
-    return <Typography variant="body2" className={classes.noResults}>No results</Typography>
+    return (
+      <Typography variant="body2" className={classes.noResults}>
+        No results
+      </Typography>
+    );
   }
 
-  const [order, setOrder] = React.useState('desc')
-  const [orderBy, setOrderBy] = React.useState('totalCost')
-  const [page, setPage] = useState(0)
-  const [rowsPerPage, setRowsPerPage] = useState(25)
-  const numData = cumulativeData.length
+  const [order, setOrder] = React.useState("desc");
+  const [orderBy, setOrderBy] = React.useState("totalCost");
+  const [page, setPage] = useState(0);
+  const [rowsPerPage, setRowsPerPage] = useState(25);
+  const numData = cumulativeData.length;
 
   useEffect(() => {
-    setPage(0)
-  }, [numData])
+    setPage(0);
+  }, [numData]);
 
-  const lastPage = Math.floor(numData / rowsPerPage)
+  const lastPage = Math.floor(numData / rowsPerPage);
 
-  const handleChangePage = (event, newPage) => setPage(newPage)
+  const handleChangePage = (event, newPage) => setPage(newPage);
 
-  const handleChangeRowsPerPage = event => {
-    setRowsPerPage(parseInt(event.target.value, 10))
-    setPage(0)
-  }
+  const handleChangeRowsPerPage = (event) => {
+    setRowsPerPage(parseInt(event.target.value, 10));
+    setPage(0);
+  };
 
-  const createSortHandler = (property) => (event) => handleRequestSort(event, property)
+  const createSortHandler = (property) => (event) =>
+    handleRequestSort(event, property);
 
   const handleRequestSort = (event, property) => {
-    const isDesc = orderBy === property && order === 'desc'
-    setOrder(isDesc ? 'asc' : 'desc')
-    setOrderBy(property)
-  }
+    const isDesc = orderBy === property && order === "desc";
+    setOrder(isDesc ? "asc" : "desc");
+    setOrderBy(property);
+  };
 
-  const orderedRows = stableSort(cumulativeData, getComparator(order, orderBy))
-  const pageRows = orderedRows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
+  const orderedRows = stableSort(cumulativeData, getComparator(order, orderBy));
+  const pageRows = orderedRows.slice(
+    page * rowsPerPage,
+    page * rowsPerPage + rowsPerPage
+  );
 
   return (
     <div id="report">
-      <AllocationChart allocationRange={allocationData} currency={currency} n={10} height={300} />
+      <AllocationChart
+        allocationRange={allocationData}
+        currency={currency}
+        n={10}
+        height={300}
+      />
       <TableContainer>
         <Table>
           <TableHead>
@@ -102,13 +120,13 @@ const AllocationReport = ({ allocationData, cumulativeData, totalData, currency
                 <TableCell
                   key={cell.id}
                   colSpan={cell.colspan}
-                  align={cell.numeric ? 'right' : 'left'}
+                  align={cell.numeric ? "right" : "left"}
                   sortDirection={orderBy === cell.id ? order : false}
                   style={{ width: cell.width }}
                 >
                   <TableSortLabel
                     active={orderBy === cell.id}
-                    direction={orderBy === cell.id ? order : 'asc'}
+                    direction={orderBy === cell.id ? order : "asc"}
                     onClick={createSortHandler(cell.id)}
                   >
                     {cell.label}
@@ -121,35 +139,42 @@ const AllocationReport = ({ allocationData, cumulativeData, totalData, currency
             <TableRow>
               {headCells.map((cell) => {
                 return (
-                <TableCell
-                  key={cell.id}
-                  colSpan={cell.colspan}
-                  align={cell.numeric ? 'right' : 'left'}
-                  style={{ fontWeight: 500 }}
-                >
-                  {cell.numeric
-                    ? (cell.label === 'Efficiency'
-                      ? (totalData.totalEfficiency == 1.0 && totalData.cpuReqCoreHrs == 0 && totalData.ramReqByteHrs == 0)
-                        ? "Inf%"
-                        : `${round(totalData.totalEfficiency*100, 1)}%`
-                      : toCurrency(totalData[cell.id], currency))
-                    : totalData[cell.id]}
-                </TableCell>
-              )})}
+                  <TableCell
+                    key={cell.id}
+                    colSpan={cell.colspan}
+                    align={cell.numeric ? "right" : "left"}
+                    style={{ fontWeight: 500 }}
+                  >
+                    {cell.numeric
+                      ? cell.label === "Efficiency"
+                        ? totalData.totalEfficiency == 1.0 &&
+                          totalData.cpuReqCoreHrs == 0 &&
+                          totalData.ramReqByteHrs == 0
+                          ? "Inf%"
+                          : `${round(totalData.totalEfficiency * 100, 1)}%`
+                        : toCurrency(totalData[cell.id], currency)
+                      : totalData[cell.id]}
+                  </TableCell>
+                );
+              })}
             </TableRow>
             {pageRows.map((row, key) => {
               if (row.name === "__unmounted__") {
-                row.name = "Unmounted PVs"
+                row.name = "Unmounted PVs";
               }
 
-              let isIdle = row.name.indexOf("__idle__") >= 0
-              let isUnallocated = row.name.indexOf("__unallocated__") >= 0
-              let isUnmounted = row.name.indexOf("Unmounted PVs") >= 0
+              let isIdle = row.name.indexOf("__idle__") >= 0;
+              let isUnallocated = row.name.indexOf("__unallocated__") >= 0;
+              let isUnmounted = row.name.indexOf("Unmounted PVs") >= 0;
 
               // Replace "efficiency" with Inf if there is usage w/o request
-              let efficiency = round(row.totalEfficiency*100, 1)
-              if (row.totalEfficiency == 1.0 && row.cpuReqCoreHrs == 0 && row.ramReqByteHrs == 0) {
-                efficiency = "Inf"
+              let efficiency = round(row.totalEfficiency * 100, 1);
+              if (
+                row.totalEfficiency == 1.0 &&
+                row.cpuReqCoreHrs == 0 &&
+                row.ramReqByteHrs == 0
+              ) {
+                efficiency = "Inf";
               }
 
               // Do not allow drill-down for idle and unallocated rows
@@ -157,29 +182,45 @@ const AllocationReport = ({ allocationData, cumulativeData, totalData, currency
                 return (
                   <TableRow key={key}>
                     <TableCell align="left">{row.name}</TableCell>
-                    <TableCell align="right">{toCurrency(row.cpuCost, currency)}</TableCell>
-                    <TableCell align="right">{toCurrency(row.ramCost, currency)}</TableCell>
-                    <TableCell align="right">{toCurrency(row.pvCost, currency)}</TableCell>
+                    <TableCell align="right">
+                      {toCurrency(row.cpuCost, currency)}
+                    </TableCell>
+                    <TableCell align="right">
+                      {toCurrency(row.ramCost, currency)}
+                    </TableCell>
+                    <TableCell align="right">
+                      {toCurrency(row.pvCost, currency)}
+                    </TableCell>
                     {isIdle ? (
                       <TableCell align="right">&mdash;</TableCell>
                     ) : (
                       <TableCell align="right">{efficiency}%</TableCell>
                     )}
-                    <TableCell align="right">{toCurrency(row.totalCost, currency)}</TableCell>
+                    <TableCell align="right">
+                      {toCurrency(row.totalCost, currency)}
+                    </TableCell>
                   </TableRow>
-                )
+                );
               }
 
               return (
                 <TableRow key={key}>
                   <TableCell align="left">{row.name}</TableCell>
-                  <TableCell align="right">{toCurrency(row.cpuCost, currency)}</TableCell>
-                  <TableCell align="right">{toCurrency(row.ramCost, currency)}</TableCell>
-                  <TableCell align="right">{toCurrency(row.pvCost, currency)}</TableCell>
+                  <TableCell align="right">
+                    {toCurrency(row.cpuCost, currency)}
+                  </TableCell>
+                  <TableCell align="right">
+                    {toCurrency(row.ramCost, currency)}
+                  </TableCell>
+                  <TableCell align="right">
+                    {toCurrency(row.pvCost, currency)}
+                  </TableCell>
                   <TableCell align="right">{efficiency}%</TableCell>
-                  <TableCell align="right">{toCurrency(row.totalCost, currency)}</TableCell>
+                  <TableCell align="right">
+                    {toCurrency(row.totalCost, currency)}
+                  </TableCell>
                 </TableRow>
-              )
+              );
             })}
           </TableBody>
         </Table>
@@ -194,7 +235,7 @@ const AllocationReport = ({ allocationData, cumulativeData, totalData, currency
         onChangeRowsPerPage={handleChangeRowsPerPage}
       />
     </div>
-  )
-}
+  );
+};
 
-export default React.memo(AllocationReport)
+export default React.memo(AllocationReport);

+ 11 - 9
ui/src/services/allocation.js

@@ -1,23 +1,25 @@
-import axios from 'axios';
+import axios from "axios";
 
 class AllocationService {
-  BASE_URL = process.env.BASE_URL || '{PLACEHOLDER_BASE_URL}';
+  BASE_URL = process.env.BASE_URL || "{PLACEHOLDER_BASE_URL}";
 
   async fetchAllocation(win, aggregate, options) {
-    if (this.BASE_URL.includes('PLACEHOLDER_BASE_URL')) {
-      this.BASE_URL = `http://localhost:9090/model`
+    if (this.BASE_URL.includes("PLACEHOLDER_BASE_URL")) {
+      this.BASE_URL = `http://localhost:9090/model`;
     }
-    
-    const { accumulate, filters, } = options;
+
+    const { accumulate, filters } = options;
     const params = {
       window: win,
       aggregate: aggregate,
-      step: '1d',
+      step: "1d",
     };
-    if (typeof accumulate === 'boolean') {
+    if (typeof accumulate === "boolean") {
       params.accumulate = accumulate;
     }
-    const result = await axios.get(`${this.BASE_URL}/allocation/compute`, { params });
+    const result = await axios.get(`${this.BASE_URL}/allocation/compute`, {
+      params,
+    });
     return result.data;
   }
 }

+ 25 - 0
ui/src/services/cloudCost.js

@@ -0,0 +1,25 @@
+import axios from "axios";
+
+class CloudCostService {
+  BASE_URL = process.env.BASE_URL || "{PLACEHOLDER_BASE_URL}";
+
+  async fetchCloudCostData(window, aggregate, costMetric) {
+    if (this.BASE_URL.includes("PLACEHOLDER_BASE_URL")) {
+      this.BASE_URL = `http://localhost:9090/model`;
+    }
+
+    const params = {
+      window,
+      aggregate,
+      costMetric,
+      accumulate: false,
+    };
+
+    const result = await axios.get(`${this.BASE_URL}/model/cloudCost/view`, {
+      params,
+    });
+    return result.data;
+  }
+}
+
+export default new CloudCostService();