Prechádzať zdrojové kódy

Cloud cost skeleton w/ controls

Signed-off-by: Thomas Evans <tevans3@icloud.com>
jjarrett21 2 rokov pred
rodič
commit
18032864ba

+ 91 - 0
ui/src/CloudCost/Controls/CloudCostEditControls.js

@@ -0,0 +1,91 @@
+import { makeStyles } from "@material-ui/styles";
+import FormControl from "@material-ui/core/FormControl";
+import InputLabel from "@material-ui/core/InputLabel";
+import MenuItem from "@material-ui/core/MenuItem";
+import Select from "@material-ui/core/Select";
+
+import React from "react";
+
+import SelectWindow from "../../components/SelectWindow";
+
+const useStyles = makeStyles({
+  wrapper: {
+    display: "inline-flex",
+  },
+  formControl: {
+    margin: 8,
+    minWidth: 120,
+  },
+});
+
+function EditCloudCostControls({
+  windowOptions,
+  window,
+  setWindow,
+  aggregationOptions,
+  aggregateBy,
+  setAggregateBy,
+  costMetricOptions,
+  costMetric,
+  setCostMetric,
+  currencyOptions,
+  currency,
+  setCurrency,
+}) {
+  const classes = useStyles();
+  return (
+    <div className={classes.wrapper}>
+      <SelectWindow
+        windowOptions={windowOptions}
+        window={window}
+        setWindow={setWindow}
+      />
+      <FormControl className={classes.formControl}>
+        <InputLabel id="aggregation-select-label">Breakdown</InputLabel>
+        <Select
+          id="aggregation-select"
+          value={aggregateBy}
+          onChange={(e) => {
+            setAggregateBy(e.target.value);
+          }}
+        >
+          {aggregationOptions.map((opt) => (
+            <MenuItem key={opt.value} value={opt.value}>
+              {opt.name}
+            </MenuItem>
+          ))}
+        </Select>
+      </FormControl>
+      <FormControl className={classes.formControl}>
+        <InputLabel id="costMetric-label">Cost Metric</InputLabel>
+        <Select
+          id="costMetric"
+          value={costMetric}
+          onChange={(e) => setCostMetric(e.target.value)}
+        >
+          {costMetricOptions.map((opt) => (
+            <MenuItem key={opt.value} value={opt.value}>
+              {opt.name}
+            </MenuItem>
+          ))}
+        </Select>
+      </FormControl>
+      <FormControl className={classes.formControl}>
+        <InputLabel id="currency-label">Currency</InputLabel>
+        <Select
+          id="currency"
+          value={currency}
+          onChange={(e) => setCurrency(e.target.value)}
+        >
+          {currencyOptions?.map((currency) => (
+            <MenuItem key={currency} value={currency}>
+              {currency}
+            </MenuItem>
+          ))}
+        </Select>
+      </FormControl>
+    </div>
+  );
+}
+
+export default React.memo(EditCloudCostControls);

+ 32 - 0
ui/src/CloudCost/tokens.js

@@ -0,0 +1,32 @@
+const windowOptions = [
+  { name: "Today", value: "today" },
+  { name: "Yesterday", value: "yesterday" },
+  { name: "Week-to-date", value: "week" },
+  { name: "Month-to-date", value: "month" },
+  { name: "Last week", value: "lastweek" },
+  { name: "Last month", value: "lastmonth" },
+  { name: "Last 24h", value: "24h" },
+  { name: "Last 48h", value: "48h" },
+  { name: "Last 7 days", value: "7d" },
+  { name: "Last 30 days", value: "30d" },
+  { name: "Last 60 days", value: "60d" },
+  { name: "Last 90 days", value: "90d" },
+];
+
+const aggregationOptions = [
+  { name: "Account", value: "accountID" },
+  { name: "Invoice Entity", value: "invoiceEntityID" },
+  { name: "Provider", value: "provider" },
+  { name: "service ", value: "service" },
+  { name: "category", value: "category" },
+  { name: "item", value: "item" },
+];
+
+const costMetricOptions = [
+  { name: "Amortized Net Cost", value: "AmortizedNetCost" },
+  { name: "List Cost", value: "ListCost" },
+  { name: "Invoiced Cost", value: "InvoicedCost" },
+  { name: "Amortized Cost", value: "AmortizedCost" },
+];
+
+export { windowOptions, aggregationOptions, costMetricOptions };

+ 177 - 15
ui/src/CloudCostReports.js

@@ -4,22 +4,125 @@ import Header from "./components/Header";
 import IconButton from "@material-ui/core/IconButton";
 import RefreshIcon from "@material-ui/icons/Refresh";
 import { makeStyles } from "@material-ui/styles";
-import { Paper } from "@material-ui/core";
-
-const useStyles = makeStyles({
-  reportHeader: {
-    display: "flex",
-    flexFlow: "row",
-    padding: 24,
-  },
-  titles: {
-    flexGrow: 1,
-  },
-});
+import { Paper, Typography } from "@material-ui/core";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import { get, find, sortBy, toArray } from "lodash";
+
+import { useLocation, useHistory } from "react-router";
+
+import CloudCostEditControls from "./CloudCost/Controls/CloudCostEditControls";
+import Subtitle from "./components/Subtitle";
+import Warnings from "./components/Warnings";
+
+import {
+  windowOptions,
+  costMetricOptions,
+  aggregationOptions,
+} from "./CloudCost/tokens";
+import { currencyCodes } from "./constants/currencyCodes";
 
 const CloudCostReports = () => {
+  const useStyles = makeStyles({
+    reportHeader: {
+      display: "flex",
+      flexFlow: "row",
+      padding: 24,
+    },
+    titles: {
+      flexGrow: 1,
+    },
+  });
   const classes = useStyles();
 
+  // Form state, which controls form elements, but not the report itself. On
+  // certain actions, the form state may flow into the report state.
+  const [title, setTitle] = React.useState(
+    "Cumulative cost for last 7 days by account"
+  );
+  const [window, setWindow] = React.useState(windowOptions[0].value);
+  const [aggregateBy, setAggregateBy] = React.useState(
+    aggregationOptions[0].value
+  );
+  const [costMetric, setCostMetric] = React.useState(
+    costMetricOptions[0].value
+  );
+  const [currency, setCurrency] = React.useState("USD");
+  // page and settings state
+  const [init, setInit] = React.useState(false);
+  const [fetch, setFetch] = React.useState(false);
+  const [loading, setLoading] = React.useState(true);
+  const [errors, setErrors] = React.useState([]);
+
+  function generateTitle({ window, aggregateBy, costMetric }) {
+    let windowName = get(find(windowOptions, { value: window }), "name", "");
+    if (windowName === "") {
+      if (checkCustomWindow(window)) {
+        windowName = toVerboseTimeRange(window);
+      } else {
+        console.warn(`unknown window: ${window}`);
+      }
+    }
+
+    let aggregationName = get(
+      find(aggregationOptions, { value: aggregateBy }),
+      "name",
+      ""
+    ).toLowerCase();
+    if (aggregationName === "") {
+      console.warn(`unknown aggregation: ${aggregateBy}`);
+    }
+
+    let str = `${windowName} by ${aggregationName}`;
+
+    if (!costMetric) {
+      str = `${str} amoritizedNetCost`;
+    }
+
+    return str;
+  }
+
+  // parse any context information from the URL
+  const routerLocation = useLocation();
+  const searchParams = new URLSearchParams(routerLocation.search);
+  const routerHistory = useHistory();
+
+  async function initialize() {
+    setInit(true);
+  }
+
+  async function fetchData() {
+    setLoading(true);
+    setErrors([]);
+    try {
+      console.log("look at me fetching data");
+    } catch (error) {
+      console.error(error);
+    }
+    setLoading(false);
+  }
+
+  React.useEffect(() => {
+    setWindow(searchParams.get("window") || "7d");
+    setAggregateBy(searchParams.get("agg") || "service");
+    setCostMetric(searchParams.get("costMetric") || "AmortizedNetCost");
+    setCurrency(searchParams.get("currency") || "USD");
+  }, [routerLocation]);
+
+  // Initialize once, then fetch report each time setFetch(true) is called
+  React.useEffect(() => {
+    if (!init) {
+      initialize();
+    }
+    if (init && fetch) {
+      fetchData();
+    }
+  }, [init, fetch]);
+
+  React.useEffect(() => {
+    setFetch(true);
+    setTitle(generateTitle({ window, aggregateBy, costMetric }));
+  }, [window, aggregateBy, costMetric]);
+
   return (
     <Page active="cloud.html">
       <Header>
@@ -28,9 +131,68 @@ const CloudCostReports = () => {
         </IconButton>
       </Header>
 
-      <Paper id="cloud-cost">
-        <div>Cloud Cost </div>
-      </Paper>
+      {!loading && errors.length > 0 && (
+        <div style={{ marginBottom: 20 }}>
+          <Warnings warnings={errors} />
+        </div>
+      )}
+
+      {init && (
+        <Paper id="cloud-cost">
+          <div className={classes.reportHeader}>
+            <div className={classes.titles}>
+              <Typography variant="h5">{title}</Typography>
+              <Subtitle report={{ window, aggregateBy }} />
+            </div>
+            <CloudCostEditControls
+              windowOptions={windowOptions}
+              window={window}
+              setWindow={(win) => {
+                searchParams.set("window", win);
+                routerHistory.push({
+                  search: `?${searchParams.toString()}`,
+                });
+              }}
+              aggregationOptions={aggregationOptions}
+              aggregateBy={aggregateBy}
+              setAggregateBy={(agg) => {
+                searchParams.set("agg", agg);
+                routerHistory.push({
+                  search: `?${searchParams.toString()}`,
+                });
+              }}
+              costMetricOptions={costMetricOptions}
+              costMetric={costMetric}
+              setCostMetric={(c) => {
+                searchParams.set("costMetric", c);
+                routerHistory.push({
+                  search: `?${searchParams.toString()}`,
+                });
+              }}
+              title={title}
+              // cumulativeData={cumulativeData}
+              currency={currency}
+              currencyOptions={currencyCodes}
+              setCurrency={(curr) => {
+                searchParams.set("currency", curr);
+                routerHistory.push({
+                  search: `?${searchParams.toString()}`,
+                });
+              }}
+            />
+          </div>
+
+          {loading && (
+            <div style={{ display: "flex", justifyContent: "center" }}>
+              <div style={{ paddingTop: 100, paddingBottom: 100 }}>
+                <CircularProgress />
+              </div>
+            </div>
+          )}
+
+          {!loading && <div>Cloud Cost Report goes here</div>}
+        </Paper>
+      )}
     </Page>
   );
 };

+ 30 - 24
ui/src/components/Header.js

@@ -1,48 +1,54 @@
-import React from 'react'
-import { makeStyles } from '@material-ui/styles'
-import Breadcrumbs from '@material-ui/core/Breadcrumbs';
-import Link from '@material-ui/core/Link';
-import Typography from '@material-ui/core/Typography';
+import React from "react";
+import { makeStyles } from "@material-ui/styles";
+import Breadcrumbs from "@material-ui/core/Breadcrumbs";
+import Link from "@material-ui/core/Link";
+import Typography from "@material-ui/core/Typography";
 
 const useStyles = makeStyles({
   root: {
-    alignItems: 'center',
-    display: 'flex',
-    flexFlow: 'row',
+    alignItems: "center",
+    display: "flex",
+    flexFlow: "row",
     marginBottom: 20,
-    width: '100%',
+    width: "100%",
   },
   context: {
-    flex: '1 0 auto',
+    flex: "1 0 auto",
   },
   actions: {
-    flex: '0 0 auto',
+    flex: "0 0 auto",
   },
 });
 
 const Header = (props) => {
-  const classes = useStyles()
-  const { title, breadcrumbs } = props
+  const classes = useStyles();
+  const { title, breadcrumbs } = props;
 
   return (
     <div className={classes.root}>
-      <img src={ require('../images/logo.png') } alt="OpenCost" />
+      <img src={require("../images/logo.png")} alt="OpenCost" />
       <div className={classes.context}>
-        {title && <Typography variant="h4" className={classes.title}>{props.title}</Typography>}
+        {title && (
+          <Typography variant="h4" className={classes.title}>
+            {props.title}
+          </Typography>
+        )}
         {breadcrumbs && breadcrumbs.length > 0 && (
           <Breadcrumbs aria-label="breadcrumb">
-            {breadcrumbs.slice(0, breadcrumbs.length-1).map(b => (
-              <Link color="inherit" href={b.href} key={b.name}>{b.name}</Link>
+            {breadcrumbs.slice(0, breadcrumbs.length - 1).map((b) => (
+              <Link color="inherit" href={b.href} key={b.name}>
+                {b.name}
+              </Link>
             ))}
-            <Typography color="textPrimary">{breadcrumbs[breadcrumbs.length-1].name}</Typography>
+            <Typography color="textPrimary">
+              {breadcrumbs[breadcrumbs.length - 1].name}
+            </Typography>
           </Breadcrumbs>
         )}
       </div>
-      <div className={classes.actions}>
-        {props.children}
-      </div>
+      <div className={classes.actions}>{props.children}</div>
     </div>
-  )
-}
+  );
+};
 
-export default Header
+export default Header;

+ 67 - 0
ui/src/components/Sidebar.js

@@ -0,0 +1,67 @@
+import * as React from "react";
+import {
+  Box,
+  CssBaseline,
+  Drawer,
+  Typography,
+  Toolbar,
+  List,
+  Divider,
+  ListItem,
+  ListItemIcon,
+  ListItemText,
+  Button,
+  AppBar,
+} from "@material-ui/core";
+
+import { MoveToInbox as InboxIcon, Mail as MailIcon } from "@material-ui/icons";
+
+const drawerWidth = 240;
+
+const Sidebar = () => {
+  return (
+    <Box sx={{ display: "flex" }}>
+      <CssBaseline />
+      <AppBar
+        position="fixed"
+        sx={{ width: `calc(100% - ${drawerWidth}px)`, ml: `${drawerWidth}px` }}
+      >
+        {/* <Toolbar>
+          <Typography variant="h6" noWrap component="div">
+            Permanent drawer
+          </Typography>
+        </Toolbar> */}
+      </AppBar>
+      <Drawer
+        sx={{
+          width: drawerWidth,
+          flexShrink: 0,
+          "& .MuiDrawer-paper": {
+            width: drawerWidth,
+            boxSizing: "border-box",
+          },
+        }}
+        variant="permanent"
+        anchor="left"
+      >
+        <Toolbar />
+        <Divider />
+        <List>
+          {["Allocations", "Cloud Cost"].map((text, index) => (
+            <ListItem key={text} disablePadding>
+              <Button>
+                <ListItemIcon>
+                  {index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
+                </ListItemIcon>
+                <ListItemText primary={text} />
+              </Button>
+            </ListItem>
+          ))}
+        </List>
+        <Divider />
+      </Drawer>
+    </Box>
+  );
+};
+
+export default Sidebar;

+ 17 - 17
ui/src/components/Subtitle.js

@@ -1,28 +1,26 @@
-import React from 'react'
-import { makeStyles } from '@material-ui/styles'
-import { isArray, upperFirst } from 'lodash'
-import Breadcrumbs from '@material-ui/core/Breadcrumbs'
-import Link from '@material-ui/core/Link'
-import NavigateNextIcon from '@material-ui/icons/NavigateNext'
-import Tooltip from '@material-ui/core/Tooltip'
-import Typography from '@material-ui/core/Typography'
-import { toVerboseTimeRange } from '../util';
+import React from "react";
+import { makeStyles } from "@material-ui/styles";
+import { upperFirst } from "lodash";
+import Breadcrumbs from "@material-ui/core/Breadcrumbs";
+import NavigateNextIcon from "@material-ui/icons/NavigateNext";
+import Typography from "@material-ui/core/Typography";
+import { toVerboseTimeRange } from "../util";
 
 const useStyles = makeStyles({
   root: {
-    '& > * + *': {
+    "& > * + *": {
       marginTop: 2,
     },
   },
   link: {
     cursor: "pointer",
   },
-})
+});
 
 const Subtitle = ({ report }) => {
-  const classes = useStyles()
+  const classes = useStyles();
 
-  const { aggregateBy, window } = report
+  const { aggregateBy, window } = report;
 
   return (
     <div className={classes.root}>
@@ -31,13 +29,15 @@ const Subtitle = ({ report }) => {
         aria-label="breadcrumb"
       >
         {aggregateBy && aggregateBy.length > 0 ? (
-          <Typography>{toVerboseTimeRange(window)} by {upperFirst(aggregateBy)}</Typography>
+          <Typography>
+            {toVerboseTimeRange(window)} by {upperFirst(aggregateBy)}
+          </Typography>
         ) : (
           <Typography>{toVerboseTimeRange(window)}</Typography>
         )}
       </Breadcrumbs>
     </div>
-  )
-}
+  );
+};
 
-export default React.memo(Subtitle)
+export default React.memo(Subtitle);

+ 1 - 1
ui/src/route.js

@@ -1,9 +1,9 @@
 import React from "react";
-import ReactDOM from "react-dom";
 import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
 
 import Reports from "./Reports.js";
 import CloudCostReports from "./CloudCostReports.js";
+import Sidebar from "./components/Sidebar.js";
 
 const Routes = () => {
   return (

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