Просмотр исходного кода

details modal wip

Signed-off-by: Thomas Evans <tevans3@icloud.com>
jjarrett21 2 лет назад
Родитель
Сommit
1f588d2499

+ 173 - 0
ui/src/CloudCost/CloudCostDetails.js

@@ -0,0 +1,173 @@
+import * as React from "react";
+import { Modal, Typography } from "@material-ui/core";
+import Warnings from "../components/Warnings";
+import CircularProgress from "@material-ui/core/CircularProgress";
+
+import {
+  ResponsiveContainer,
+  CartesianGrid,
+  Legend,
+  XAxis,
+  YAxis,
+  Tooltip,
+  BarChart,
+  Bar,
+} from "recharts";
+import { toCurrency } from "../util";
+import cloudCostDayTotals from "../services/cloudCostDayTotals";
+
+const CloudCostDetails = ({
+  onClose,
+  selectedProviderId,
+  selectedItem,
+  agg,
+  filters,
+  costMetric,
+  window,
+  currency,
+}) => {
+  const [data, setData] = React.useState([]);
+  const [loading, setLoading] = React.useState(false);
+  const [errors, setErrors] = React.useState([]);
+  const [fetch, setFetch] = React.useState(true);
+
+  async function fetchData() {
+    setLoading(true);
+    setErrors([]);
+
+    try {
+      const resp = await cloudCostDayTotals.fetchCloudCostData(
+        window,
+        agg,
+        costMetric,
+        [
+          ...(filters ?? []),
+
+          { property: "providerIds", value: selectedProviderId },
+        ]
+      );
+      if (resp.data) {
+        setData(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,
+            },
+          ]);
+        }
+        setData([]);
+      }
+    } catch (error) {
+      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,
+          },
+        ]);
+      }
+      setData([]);
+    }
+    setLoading(false);
+    setFetch(false);
+  }
+
+  useEffect(() => {
+    if (fetch) {
+      fetchData();
+    }
+  }, [fetch]);
+
+  const drilldownData = (data ?? []).sort(
+    (a, b) =>
+      new Date(a.date ?? "").getTime() - new Date(b.date ?? "").getTime()
+  );
+
+  const itemData = drilldownData.map((items) => {
+    const dataPoint = {
+      time: new Date(items.date),
+      cost: items.cost,
+    };
+    return dataPoint;
+  });
+
+  return (
+    <Modal
+      open={true}
+      onClose={onClose}
+      title={`Costs over the last ${window}`}
+    >
+      <Typography style={{ marginTop: "1rem" }} variant="p">
+        {selectedItem}
+      </Typography>
+
+      {loading && (
+        <div style={{ display: "flex", justifyContent: "center" }}>
+          <div style={{ paddingTop: 100, paddingBottom: 100 }}>
+            <CircularProgress />
+          </div>
+        </div>
+      )}
+      {!loading && errors.length > 0 && (
+        <div style={{ marginBottom: 20 }}>
+          <Warnings warnings={errors} />
+        </div>
+      )}
+      {data && (
+        <div style={{ display: "flex", marginTop: "2.5rem" }}>
+          <ResponsiveContainer
+            height={250}
+            id={"cloud-cost-drilldown"}
+            width={"100%"}
+          >
+            <BarChart
+              data={itemData}
+              margin={{
+                top: 0,
+                bottom: 10,
+                left: 20,
+                right: 0,
+              }}
+            >
+              <CartesianGrid vertical={false} />
+              <Legend verticalAlign={"bottom"} />
+              <XAxis
+                dataKey={"time"}
+                tickFormatter={(date) => format(date, "MM/dd/yyy")}
+              />
+              <YAxis tickFormatter={(tick) => `${toCurrency(tick)}`} />
+              <Bar dataKey={"cost"} fill={"#2196f3"} name={"Item Cost"} />
+              <Tooltip
+                formatter={(value) =>
+                  `${toCurrency(value ?? 0, currency, 4, true)}`
+                }
+              />
+            </BarChart>
+          </ResponsiveContainer>
+        </div>
+      )}
+    </Modal>
+  );
+};
+
+export { CloudCostDetails };

+ 28 - 0
ui/src/CloudCostReports.js

@@ -23,6 +23,7 @@ import {
 } from "./CloudCost/tokens";
 import { currencyCodes } from "./constants/currencyCodes";
 import CloudCost from "./CloudCost/CloudCost";
+import { CloudCostDetails } from "./CloudCost/CloudCostDetails";
 
 const CloudCostReports = () => {
   const useStyles = makeStyles({
@@ -51,6 +52,8 @@ const CloudCostReports = () => {
   );
   const [filters, setFilters] = React.useState([]);
   const [currency, setCurrency] = React.useState("USD");
+  const [selectedProviderId, setSelectedProviderId] = React.useState("");
+  const [selectedItemName, setselectedItemName] = React.useState("");
   // page and settings state
   const [init, setInit] = React.useState(false);
   const [fetch, setFetch] = React.useState(false);
@@ -153,6 +156,16 @@ const CloudCostReports = () => {
   }
 
   function drilldown(row) {
+    if (aggregationState.includes("item")) {
+      try {
+        setSelectedProviderId(row.providerID);
+        setselectedItemName(row.labelName ?? row.name);
+      } catch (e) {
+        logger.error(e);
+      }
+
+      return;
+    }
     const nameParts = row.name.split("/");
     const nextAgg = aggregateBy.includes("service") ? "item" : "service";
     const aggToString = [aggregateBy];
@@ -268,6 +281,21 @@ const CloudCostReports = () => {
           )}
         </Paper>
       )}
+      {selectedProviderId && selectedItemName && (
+        <CloudCostDetails
+          onClose={() => {
+            setSelectedProviderId("");
+            setselectedItemName("");
+          }}
+          selectedProviderId={selectedProviderId}
+          selectedItem={selectedItemName}
+          agg={aggregateBy}
+          filters={filters}
+          costMetric={costMetric}
+          window={window}
+          currency={currency}
+        />
+      )}
     </Page>
   );
 };

+ 1 - 1
ui/src/components/Nav/SidebarNav.js

@@ -43,7 +43,7 @@ const SidebarNav = ({ active }) => {
       href: "allocation",
       icon: <BarChart />,
     },
-    { name: "Cloud Cost", href: "cloud", icon: <Cloud /> },
+    { name: "Cloud Costs", href: "cloud", icon: <Cloud /> },
   ];
 
   return (

+ 41 - 0
ui/src/services/cloudCostDayTotals.js

@@ -0,0 +1,41 @@
+import axios from "axios";
+import { getCloudFilters } from "../util";
+
+export function formatItemsForCost({ data }, costType) {
+  return data.sets.map(({ cloudCosts, window }) => {
+    return {
+      date: window.start,
+      cost: Object.values(cloudCosts).reduce(
+        (acc, costs) => acc + costs[costType].cost,
+        0
+      ),
+    };
+  });
+}
+
+class CloudCostDayTotalsService {
+  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`;
+    }
+
+    if (aggregate.includes("item")) {
+      const resp = await axios.get(
+        `${
+          this.BASE_URL
+        }/model/cloudCost/top?window=${window}&costMetric=${costMetric}${getCloudFilters(
+          filters
+        )}`
+      );
+      const result_2 = await resp.data;
+
+      return { data: formatItemsForCost(result_2) };
+    }
+
+    return [];
+  }
+}
+
+export default new CloudCostDayTotalsService();