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

Narrowed down codebase given that we don't have to support drilldowns / complex filters and context

Neal Ormsbee 5 лет назад
Родитель
Сommit
ddfd6279f1

+ 43 - 273
ui/src/Reports.js

@@ -1,10 +1,8 @@
 import CircularProgress from '@material-ui/core/CircularProgress'
 import IconButton from '@material-ui/core/IconButton'
-import Link from '@material-ui/core/Link'
 import Paper from '@material-ui/core/Paper'
 import Typography from '@material-ui/core/Typography'
 import RefreshIcon from '@material-ui/icons/Refresh'
-import SettingsIcon from '@material-ui/icons/Settings'
 import { makeStyles } from '@material-ui/styles'
 import { filter, find, forEach, get, isArray, sortBy, toArray, trim } from 'lodash'
 import React, { useEffect, useState } from 'react'
@@ -13,13 +11,12 @@ import { useLocation, useHistory } from 'react-router';
 
 import AllocationReport from './components/AllocationReport';
 import Controls from './components/Controls';
-import DetailsDialog from './components/DetailsDialog';
 import Header from './components/Header';
 import Page from './components/Page';
 import Subtitle from './components/Subtitle';
 import Warnings from './components/Warnings';
 import AllocationService from './services/allocation';
-import { cumulativeToTotals, rangeToCumulative } from './util';
+import { checkCustomWindow, cumulativeToTotals, rangeToCumulative, toVerboseTimeRange } from './util';
 
 const windowOptions = [
   { name: 'Today', value: 'today' },
@@ -44,22 +41,11 @@ const aggregationOptions = [
   { name: 'Pod', value: 'pod' },
 ]
 
-const idleOptions = [
-  { name: 'Hide', value: "hide" },
-  { name: 'Share', value: "share" },
-  { name: 'Separate', value: "separate" },
-]
-
 const accumulateOptions = [
   { name: 'Entire window', value: true },
   { name: 'Daily', value: false },
 ]
 
-const shareSplitOptions = [
-  { name: 'Share evenly', value: 'even' },
-  { name: 'Share weighted by cost', value: 'weighted' },
-]
-
 const useStyles = makeStyles({
   reportHeader: {
     display: 'flex',
@@ -71,6 +57,32 @@ const useStyles = makeStyles({
   },
 })
 
+// generateTitle generates a string title from a report object
+function generateTitle({ window, aggregateBy, accumulate }) {
+  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 (!accumulate) {
+    str = `${str} daily`
+  }
+
+  return str
+}
+
+
 const ReportsPage = () => {
   const classes = useStyles()
 
@@ -91,187 +103,16 @@ const ReportsPage = () => {
   const [window, setWindow] = useState(windowOptions[0].value)
   const [aggregateBy, setAggregateBy] = useState(aggregationOptions[0].value)
   const [accumulate, setAccumulate] = useState(accumulateOptions[0].value)
-  const [idle, setIdle] = useState(idleOptions[0].value)
-  const [filters, setFilters] = useState([])
-  const [shareCost, setShareCost] = useState(0.0)
-  const [shareNamespaces, setShareNamespaces] = useState([])
-  const [shareLabels, setShareLabels] = useState(0.0)
-  const [shareSplit, setShareSplit] = useState('weighted')
-
-  // Context is used for drill-down; each drill-down gets pushed onto the
-  // context stack. Clearing resets to an empty stack. Using a breadcrumb
-  // should pop everything above that on the stack.
-  const [context, setContext] = useState([])
-
-  const clearContext = () => {
-    if (context.length > 0) {
-      searchParams.set('agg', context[0].property.toLowerCase());
-    }
-    searchParams.set('context', btoa(JSON.stringify([])));
-  }
-
-  const goToContext = (i) => {
-    if (!isArray(context)) {
-      console.warn(`context is not an array: ${context}`)
-      return
-    }
-
-    if (i > context.length-1) {
-      console.warn(`selected context out of range: ${i} with context length ${context.length}`)
-      return
-    }
-
-    if (i === context.length-1) {
-      console.warn(`selected current context: ${i} with context length ${context.length}`)
-    }
-
-    searchParams.set('agg', context[i+1].property.toLowerCase());
-    searchParams.set('context', btoa(JSON.stringify(context.slice(0, i+1))));
-    routerHistory.push({ search: `?${searchParams.toString()}` });
-  }
-
-  const drillDown = (row) => {
-    if (aggregateBy === "pod") {
-
-      let pod = row.name
-      let cluster = ""
-      let namespace = ""
-      let controllerKind = ""
-      let controller = ""
-
-      forEach(context, ctx => {
-        if (ctx.property.toLowerCase() == "cluster") {
-          cluster = ctx.value
-        } else if (ctx.property.toLowerCase() == "namespace") {
-          namespace = ctx.value
-        } else if (ctx.property.toLowerCase() == "controller") {
-          const tokens = ctx.value.split("/")
-          if (tokens.length == 2) {
-            controllerKind = tokens[0]
-            controller = tokens[1]
-          } else {
-            controller = ctx.value
-          }
-        }
-      })
-
-      openDetails(cluster, namespace, controllerKind, controller, pod)
-    }
-
-    if (aggregateBy === "controller") {
-      const ctx = [ ...context, {
-        property: "Controller",
-        value: row.name,
-        name: row.name,
-      }];
-
-      searchParams.set('agg', 'pod');
-      searchParams.set('context', btoa(JSON.stringify(ctx)));
-      routerHistory.push({
-        search: `?${searchParams.toString()}`,
-      });
-    }
-
-    if (aggregateBy === "cluster") {
-      let cluster = get(row, "cluster", "")
-      let clusterTokens = get(row, "cluster", "").split("/")
-      if (clusterTokens.length > 0) {
-        cluster = clusterTokens[0]
-      }
-
-      const ctx = [ ...context, {
-        property: "Cluster",
-        value: cluster,
-        name: cluster,
-      }];
-
-      searchParams.set('agg', 'namespace');
-      searchParams.set('context', btoa(JSON.stringify(ctx)));
-      routerHistory.push({
-        search: `?${searchParams.toString()}`,
-      });
-    }
-
-    if (aggregateBy === "namespace") {
-      const ctx = [ ...context, {
-        property: 'Namespace',
-        value: row.namespace,
-        name: row.namespace,
-      }];
-
-      searchParams.set('agg', 'controller');
-      searchParams.set('context', btoa(JSON.stringify(ctx)));
-      routerHistory.push({
-        search: `?${searchParams.toString()}`,
-      });
-    }
-
-    if (aggregateBy === "controllerKind") {
-      const ctx = [ ...context, {
-        property: "Controller Kind",
-        value: row.controllerKind,
-        name: row.controllerKind,
-      }];
-
-      searchParams.set('agg', 'controller');
-      searchParams.set('context', btoa(JSON.stringify(ctx)));
-      routerHistory.push({
-        search: `?${searchParams.toString()}`,
-      });
-    }
-
-    if (aggregateBy === "node") {
-      const ctx = [ ...context, {
-        property: "Node",
-        value: row.node,
-        name: row.node,
-      }];
-      searchParams.set('agg', 'controller');
-      searchParams.set('context', btoa(JSON.stringify(ctx)));
-      routerHistory.push({
-        search: `?${searchParams.toString()}`,
-      });
-    }
-
-    // TODO labels?
-  }
-
-  // Setting details to null closes the details dialog. Setting it to an
-  // object describing a controller opens it with that state.
-  const [details, setDetails] = useState(null)
-
-  const closeDetails = () => {
-    searchParams.set('details', btoa(JSON.stringify(null)));
-    routerHistory.push({ search: `?${searchParams.toString()}` });
-  }
-
-  const openDetails = (cluster, namespace, controllerKind, controller, pod) => {
-    searchParams.set('details', btoa(JSON.stringify({ cluster, namespace, controllerKind, controller, pod })));
-    routerHistory.push({ search: `?${searchParams.toString()}` });
-  }
 
   // Report state, including current report and saved options
-  const [title, setTitle] = useState("")
-  const [titleField, setTitleField] = useState("")
+  const [title, setTitle] = useState('Last 7 days by namespace daily')
 
-  // When parameters changes, clear context and fetch data. This should be the
-  // only mechanism used to fetch data. Also update title to saved title, if
-  // possible, or generate a sensible title from the paramters
+  // When parameters changes, fetch data. This should be the
+  // only mechanism used to fetch data. Also generate a sensible title from the paramters.
   useEffect(() => {
     setFetch(true)
-
-    // Use "aggregateBy" by default, but if we're within a context, then
-    // only use the top-level context; e.g. if we started by namespace, but
-    // drilled down, we'll have (aggregateBy == "controller"), but the
-    // report should keep the title of "by namespace".
-    let aggBy = aggregateBy
-    if (context.length > 0) {
-      aggBy = context[0].property.toLowerCase()
-    }
-
-    const curr = { window, aggregateBy: aggBy, accumulate, idle, filters }
-
-  }, [window, aggregateBy, accumulate, idle, filters, shareSplit])
+    setTitle(generateTitle({ window, aggregateBy, accumulate }))
+  }, [window, aggregateBy, accumulate])
 
   // page and settings state
   const [init, setInit] = useState(false)
@@ -295,36 +136,9 @@ const ReportsPage = () => {
   const searchParams = new URLSearchParams(routerLocation.search);
   const routerHistory = useHistory();
   useEffect(() => {
-    let ctx = searchParams.get('context');
-    let deets = searchParams.get('details');
-    let fltr = searchParams.get('filters');
-
-    try {
-      ctx = JSON.parse(atob(ctx)) || [];
-    } catch (err) {
-      ctx = [];
-    }
-
-    try {
-      deets = JSON.parse(atob(deets)) || null;
-    } catch (err) {
-      deets = null;
-    }
-
-    try {
-      fltr = JSON.parse(atob(fltr)) || [];
-    } catch (err) {
-      fltr = [];
-    }
     setWindow(searchParams.get('window') || '6d');
     setAggregateBy(searchParams.get('agg') || 'namespace');
     setAccumulate((searchParams.get('acc') === 'true') || false);
-    setIdle(searchParams.get('idle') || 'separate');
-    setTitle(searchParams.get('title') || 'Last 7 days by namespace daily');
-    setShareSplit(searchParams.get('split') || 'weighted');
-    setContext(ctx);
-    setDetails(deets);
-    setFilters(fltr);
   }, [routerLocation]);
 
   async function initialize() {
@@ -336,25 +150,12 @@ const ReportsPage = () => {
     setErrors([])
 
     try {
-      let queryFilters = []
-      if (context.length > 0) {
-        forEach(context, (contextFilter) => {
-          queryFilters.push(contextFilter)
-        })
-      }
-      forEach(filters, (filter) => {
-        queryFilters.push(filter)
-      })
-
-      const resp = await AllocationService.fetchAllocation(window, aggregateBy, {
-        accumulate: accumulate,
-        filters: queryFilters,
-      })
+      const resp = await AllocationService.fetchAllocation(window, aggregateBy, { accumulate })
       if (resp.data && resp.data.length > 0) {
         const allocationRange = resp.data
         for (const i in allocationRange) {
           // update cluster aggregations to use clusterName/clusterId names
-          if (aggregateBy == "cluster") {
+          if (aggregateBy == 'cluster') {
             for (const k in allocationRange[i]) {
               allocationRange[i][k].name = 'cluster-one';
             }
@@ -370,25 +171,25 @@ const ReportsPage = () => {
             secondary = `${match[1]}. ${secondary}`
           }
           setErrors([{
-            primary: "Data unavailable while ETL is building",
+            primary: 'Data unavailable while ETL is building',
             secondary: secondary,
           }])
         }
         setAllocationData([])
       }
     } catch (err) {
-      if (err.message.indexOf("404") === 0) {
+      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."
+          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."
+        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",
+          primary: 'Failed to load report data',
           secondary: secondary,
         }])
       }
@@ -401,8 +202,8 @@ const ReportsPage = () => {
   return (
     <Page active="reports.html">
       <Header breadcrumbs={[
-        { 'name': 'Reports', 'href': 'new_index.html#/reports' },
-        { 'name': title, 'href': `new_index.html#/reports` },
+        { 'name': 'Reports', 'href': '/' },
+        { 'name': title, 'href': '/' },
       ]}>
         <IconButton aria-label="refresh" onClick={() => setFetch(true)}>
           <RefreshIcon />
@@ -420,10 +221,7 @@ const ReportsPage = () => {
           <div className={classes.titles}>
             <Typography variant="h5">{title}</Typography>
             <Subtitle
-              report={{ window, aggregateBy, accumulate, idle, filters }}
-              context={context}
-              clearContext={() => { clearContext(); routerHistory.push({ search: `?${searchParams.toString()}`}); }}
-              goToContext={goToContext}
+              report={{ window, aggregateBy, accumulate }}
             />
           </div>
 
@@ -452,28 +250,9 @@ const ReportsPage = () => {
                 search: `?${searchParams.toString()}`
               });
             }}
-            idleOptions={idleOptions}
-            idle={idle}
-            setIdle={(idle) => {
-              searchParams.set('idle', idle);
-              routerHistory.push({
-                search: `?${searchParams.toString()}`,
-              });
-            }}
             title={title}
             cumulativeData={cumulativeData}
             currency={currency}
-            titleField={titleField}
-            filters={filters}
-            setFilters={(filters) => {
-              const fltr = btoa(JSON.stringify(filters));
-              searchParams.set('filters', fltr);
-              routerHistory.push({
-                search: `?${searchParams.toString()}`,
-              });
-            }}
-            clearContext={clearContext}
-            context={context}
           />
         </div>
 
@@ -490,18 +269,9 @@ const ReportsPage = () => {
             cumulativeData={cumulativeData}
             totalData={totalData}
             currency={currency}
-            drillDown={drillDown}
           />
         )}
       </Paper>}
-
-      <DetailsDialog
-        open={details != null}
-        close={() => closeDetails()}
-        details={details}
-        window={window}
-        currency={currency}
-      />
     </Page>
   )
 }

+ 2 - 2
ui/src/components/AllocationReport.js

@@ -54,7 +54,7 @@ const headCells = [
   { id: 'totalCost', numeric: true, label: 'Total cost', width: 90 },
 ]
 
-const AllocationReport = ({ allocationData, cumulativeData, totalData, currency, drillDown }) => {
+const AllocationReport = ({ allocationData, cumulativeData, totalData, currency }) => {
   const classes = useStyles()
 
   if (allocationData.length === 0) {
@@ -171,7 +171,7 @@ const AllocationReport = ({ allocationData, cumulativeData, totalData, currency,
               }
 
               return (
-                <TableRow key={key} hover style={{ cursor: 'pointer' }} onClick={() => drillDown(row)}>
+                <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>

+ 0 - 33
ui/src/components/Controls/Edit.js

@@ -48,43 +48,11 @@ const EditControl = ({
   accumulateOptions,
   accumulate,
   setAccumulate,
-  idleOptions,
-  idle,
-  setIdle,
-  filters,
-  setFilters,
   currency,
-  clearContext,
 }) => {
   const classes = useStyles()
   const [anchorEl, setAnchorEl] = React.useState(null)
 
-  const [filterProperty, setFilterProperty] = useState("namespace")
-  const [filterValue, setFilterValue] = useState("")
-
-  const filterPropertyOptions = ["cluster", "node", "namespace", "label", "service"]
-
-  const handleAddFilter = (newFilter) => {
-    // remove existing filters using the newFilter's property (overwrite it)
-    const oldFilters = isArray(filters) ? filters : []
-    const fs = filter(oldFilters, f => f.property !== newFilter.property)
-
-    // sanitize comma-separated values
-    const vals = sortBy(newFilter.value.split(',').map(v => trim(v).replace('=', ':')), str => str)
-    newFilter.value = vals.join(', ')
-
-    if (newFilter.value.length > 0) {
-      setFilters(sortBy([ ...fs, newFilter ], 'property'))
-    }
-
-    setFilterValue("")
-  }
-
-  const handleDeleteFilter = (delFilter) => {
-    const oldFilters = isArray(filters) ? filters : []
-    setFilters(filter(oldFilters, f => !(f.property === delFilter.property && f.value === delFilter.value)))
-  }
-
   const handleClick = (event) => {
     setAnchorEl(event.currentTarget)
   }
@@ -130,7 +98,6 @@ const EditControl = ({
                 id="aggregation-select"
                 value={aggregateBy}
                 onChange={e => {
-                  clearContext()
                   setAggregateBy(e.target.value)
                 }}
               >

+ 0 - 19
ui/src/components/Controls/index.js

@@ -20,25 +20,12 @@ const Controls = ({
   accumulateOptions,
   accumulate,
   setAccumulate,
-  idleOptions,
-  idle,
-  setIdle,
   title,
   cumulativeData,
   currency,
-  titleField,
-  filters,
-  setFilters,
-  clearContext,
-  context,
 }) => {
   const classes = useStyles()
 
-  let reportAggregateBy = aggregateBy
-  if (context.length > 0) {
-    reportAggregateBy = context[0].property.toLowerCase()
-  }
-
   return (
     <div className={classes.controls}>
       <EditControl
@@ -51,13 +38,7 @@ const Controls = ({
         accumulateOptions={accumulateOptions}
         accumulate={accumulate}
         setAccumulate={setAccumulate}
-        idleOptions={idleOptions}
-        idle={idle}
-        setIdle={setIdle}
-        filters={filters}
-        setFilters={setFilters}
         currency={currency}
-        clearContext={clearContext}
       />
 
       <DownloadControl

+ 0 - 59
ui/src/components/DetailsDialog.js

@@ -1,59 +0,0 @@
-import React from 'react'
-import { get } from 'lodash'
-import Button from '@material-ui/core/Button'
-import Dialog from '@material-ui/core/Dialog'
-import DialogActions from '@material-ui/core/DialogActions'
-import DialogContent from '@material-ui/core/DialogContent'
-import DialogTitle from '@material-ui/core/DialogTitle'
-import Details from './Details'
-
-const DetailsDialog = ({
-  open,
-  close,
-  details,
-  window,
-  currency,
-}) => {
-  const namespace = get(details, 'namespace', '')
-  const controllerKind = get(details, 'controllerKind', '')
-  const controller = get(details, 'controller', '')
-  const pod = get(details, 'pod', '')
-
-  let title = controller
-  if (namespace) {
-    title = `${namespace} / ${title}`
-  }
-  if (pod) {
-    title = `${title} : ${pod}`
-  }
-
-  return (
-    <Dialog
-      open={open}
-      onClose={close}
-      maxWidth="lg"
-      fullWidth
-    >
-      <DialogTitle>{title}</DialogTitle>
-      <DialogContent>
-        {(controllerKind && controller) &&
-          <Details
-            window={window}
-            currency={currency}
-            namespace={namespace}
-            controllerKind={controllerKind}
-            controller={controller}
-            pod={pod}
-          />
-        }
-      </DialogContent>
-      <DialogActions>
-        <Button onClick={close} color="primary">
-          Close
-        </Button>
-      </DialogActions>
-    </Dialog>
-  )
-}
-
-export default DetailsDialog

+ 6 - 39
ui/src/components/Subtitle.js

@@ -19,55 +19,22 @@ const useStyles = makeStyles({
   },
 })
 
-const Subtitle = ({ report, context, clearContext, goToContext }) => {
+const Subtitle = ({ report }) => {
   const classes = useStyles()
 
   const { aggregateBy, window } = report
 
-  if (!isArray(context) || context.length === 0) {
-    return (
-      <div className={classes.root}>
-        <Breadcrumbs
-          separator={<NavigateNextIcon fontSize="small" />}
-          aria-label="breadcrumb"
-        >
-          {aggregateBy && aggregateBy.length > 0 ? (
-            <Typography>{toVerboseTimeRange(window)} by {upperFirst(aggregateBy)}</Typography>
-          ) : (
-            <Typography>{toVerboseTimeRange(window)}</Typography>
-          )}
-        </Breadcrumbs>
-      </div>
-    )
-  }
-
-  const handleBreadcrumbClick = (i, cb) => (e) => {
-    e.preventDefault()
-    cb(i)
-  }
-
   return (
     <div className={classes.root}>
       <Breadcrumbs
         separator={<NavigateNextIcon fontSize="small" />}
         aria-label="breadcrumb"
       >
-        <Link className={classes.link} color="inherit" onClick={() => clearContext()}>
-          <Typography>{toVerboseTimeRange(window)} by {context[0].property}</Typography>
-        </Link>
-        {context.map((ctx, c) => {
-          return c === context.length-1 ? (
-            <Tooltip key={c} title={ctx.property} arrow>
-              <Typography style={{ cursor: "default" }}>{ctx.name}</Typography>
-            </Tooltip>
-          ) : (
-            <Link key={c} className={classes.link} color="inherit" onClick={handleBreadcrumbClick(c, goToContext)}>
-              <Tooltip title={ctx.property} arrow>
-                <Typography>{ctx.name}</Typography>
-              </Tooltip>
-            </Link>
-          )
-        })}
+        {aggregateBy && aggregateBy.length > 0 ? (
+          <Typography>{toVerboseTimeRange(window)} by {upperFirst(aggregateBy)}</Typography>
+        ) : (
+          <Typography>{toVerboseTimeRange(window)}</Typography>
+        )}
       </Breadcrumbs>
     </div>
   )