import CircularProgress from '@material-ui/core/CircularProgress' import IconButton from '@material-ui/core/IconButton' import Paper from '@material-ui/core/Paper' import Typography from '@material-ui/core/Typography' import RefreshIcon from '@material-ui/icons/Refresh' import { makeStyles } from '@material-ui/styles' import { filter, find, forEach, get, isArray, sortBy, toArray, trim } from 'lodash' import React, { useEffect, useState } from 'react' import ReactDOM from 'react-dom' import { useLocation, useHistory } from 'react-router'; import AllocationReport from './components/AllocationReport'; import Controls from './components/Controls'; 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 { checkCustomWindow, cumulativeToTotals, rangeToCumulative, toVerboseTimeRange } from './util'; 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 7 days', value: '6d' }, { name: 'Last 30 days', value: '29d' }, { name: 'Last 60 days', value: '59d' }, { name: 'Last 90 days', value: '89d' }, ] const aggregationOptions = [ { name: 'Cluster', value: 'cluster' }, { name: 'Node', value: 'node' }, { name: 'Namespace', value: 'namespace' }, { name: 'Controller kind', value: 'controllerKind' }, { name: 'Controller', value: 'controller' }, { name: 'Service', value: 'service' }, { name: 'Pod', value: 'pod' }, ] const accumulateOptions = [ { name: 'Entire window', value: true }, { name: 'Daily', value: false }, ] const useStyles = makeStyles({ reportHeader: { display: 'flex', flexFlow: 'row', padding: 24, }, titles: { flexGrow: 1, }, }) // 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() // Allocation data state const [allocationData, setAllocationData] = useState([]) const [cumulativeData, setCumulativeData] = useState({}) const [totalData, setTotalData] = useState({}) // When allocation data changes, create a cumulative version of it useEffect(() => { const cumulative = rangeToCumulative(allocationData, aggregateBy) setCumulativeData(toArray(cumulative)) setTotalData(cumulativeToTotals(cumulative)) }, [allocationData]) // Form state, which controls form elements, but not the report itself. On // certain actions, the form state may flow into the report state. const [window, setWindow] = useState(windowOptions[0].value) const [aggregateBy, setAggregateBy] = useState(aggregationOptions[0].value) const [accumulate, setAccumulate] = useState(accumulateOptions[0].value) // Report state, including current report and saved options const [title, setTitle] = useState('Last 7 days by namespace daily') // 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) setTitle(generateTitle({ window, aggregateBy, accumulate })) }, [window, aggregateBy, accumulate]) // page and settings state const [init, setInit] = useState(false) const [fetch, setFetch] = useState(false) const [loading, setLoading] = useState(true) const [errors, setErrors] = useState([]) const [currency, setCurrency] = useState('USD') // Initialize once, then fetch report each time setFetch(true) is called useEffect(() => { if (!init) { initialize() } if (init && fetch) { fetchData() } }, [init, fetch]) // parse any context information from the URL const routerLocation = useLocation(); const searchParams = new URLSearchParams(routerLocation.search); const routerHistory = useHistory(); useEffect(() => { setWindow(searchParams.get('window') || '6d'); setAggregateBy(searchParams.get('agg') || 'namespace'); setAccumulate((searchParams.get('acc') === 'true') || false); }, [routerLocation]); async function initialize() { setInit(true) } async function fetchData() { setLoading(true) setErrors([]) try { 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') { for (const k in allocationRange[i]) { allocationRange[i][k].name = 'cluster-one'; } } allocationRange[i] = sortBy(allocationRange[i], a => a.totalCost) } setAllocationData(allocationRange) } 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, }]) } setAllocationData([]) } } 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, }]) } setAllocationData([]) } setLoading(false) setFetch(false) } return (
setFetch(true)}>
{!loading && errors.length > 0 && (
)} {init &&
{title}
{ searchParams.set('window', win); routerHistory.push({ search: `?${searchParams.toString()}`, }); }} aggregationOptions={aggregationOptions} aggregateBy={aggregateBy} setAggregateBy={(agg) => { searchParams.set('agg', agg); routerHistory.push({ search: `?${searchParams.toString()}`, }); }} accumulateOptions={accumulateOptions} accumulate={accumulate} setAccumulate={(acc) => { searchParams.set('acc', acc); routerHistory.push({ search: `?${searchParams.toString()}` }); }} title={title} cumulativeData={cumulativeData} currency={currency} />
{loading && (
)} {!loading && ( )}
}
) } export default React.memo(ReportsPage);