Reports.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import CircularProgress from '@material-ui/core/CircularProgress'
  2. import IconButton from '@material-ui/core/IconButton'
  3. import Paper from '@material-ui/core/Paper'
  4. import Typography from '@material-ui/core/Typography'
  5. import RefreshIcon from '@material-ui/icons/Refresh'
  6. import { makeStyles } from '@material-ui/styles'
  7. import { filter, find, forEach, get, isArray, sortBy, toArray, trim } from 'lodash'
  8. import React, { useEffect, useState } from 'react'
  9. import ReactDOM from 'react-dom'
  10. import { useLocation, useHistory } from 'react-router';
  11. import AllocationReport from './components/AllocationReport';
  12. import Controls from './components/Controls';
  13. import Header from './components/Header';
  14. import Page from './components/Page';
  15. import Subtitle from './components/Subtitle';
  16. import Warnings from './components/Warnings';
  17. import AllocationService from './services/allocation';
  18. import { checkCustomWindow, cumulativeToTotals, rangeToCumulative, toVerboseTimeRange } from './util';
  19. import { currencyCodes } from './constants/currencyCodes'
  20. const windowOptions = [
  21. { name: 'Today', value: 'today' },
  22. { name: 'Yesterday', value: 'yesterday' },
  23. { name: 'Week-to-date', value: 'week' },
  24. { name: 'Month-to-date', value: 'month' },
  25. { name: 'Last week', value: 'lastweek' },
  26. { name: 'Last month', value: 'lastmonth' },
  27. { name: 'Last 7 days', value: '6d' },
  28. { name: 'Last 30 days', value: '29d' },
  29. { name: 'Last 60 days', value: '59d' },
  30. { name: 'Last 90 days', value: '89d' },
  31. ]
  32. const aggregationOptions = [
  33. { name: 'Cluster', value: 'cluster' },
  34. { name: 'Node', value: 'node' },
  35. { name: 'Namespace', value: 'namespace' },
  36. { name: 'Controller kind', value: 'controllerKind' },
  37. { name: 'Controller', value: 'controller' },
  38. { name: 'Service', value: 'service' },
  39. { name: 'Pod', value: 'pod' },
  40. { name: 'Container', value: 'container' },
  41. ]
  42. const accumulateOptions = [
  43. { name: 'Entire window', value: true },
  44. { name: 'Daily', value: false },
  45. ]
  46. const useStyles = makeStyles({
  47. reportHeader: {
  48. display: 'flex',
  49. flexFlow: 'row',
  50. padding: 24,
  51. },
  52. titles: {
  53. flexGrow: 1,
  54. },
  55. })
  56. // generateTitle generates a string title from a report object
  57. function generateTitle({ window, aggregateBy, accumulate }) {
  58. let windowName = get(find(windowOptions, { value: window }), 'name', '')
  59. if (windowName === '') {
  60. if (checkCustomWindow(window)) {
  61. windowName = toVerboseTimeRange(window)
  62. } else {
  63. console.warn(`unknown window: ${window}`)
  64. }
  65. }
  66. let aggregationName = get(find(aggregationOptions, { value: aggregateBy }), 'name', '').toLowerCase()
  67. if (aggregationName === '') {
  68. console.warn(`unknown aggregation: ${aggregateBy}`)
  69. }
  70. let str = `${windowName} by ${aggregationName}`
  71. if (!accumulate) {
  72. str = `${str} daily`
  73. }
  74. return str
  75. }
  76. const ReportsPage = () => {
  77. const classes = useStyles()
  78. // Allocation data state
  79. const [allocationData, setAllocationData] = useState([])
  80. const [cumulativeData, setCumulativeData] = useState({})
  81. const [totalData, setTotalData] = useState({})
  82. // When allocation data changes, create a cumulative version of it
  83. useEffect(() => {
  84. const cumulative = rangeToCumulative(allocationData, aggregateBy)
  85. setCumulativeData(toArray(cumulative))
  86. setTotalData(cumulativeToTotals(cumulative))
  87. }, [allocationData])
  88. // Form state, which controls form elements, but not the report itself. On
  89. // certain actions, the form state may flow into the report state.
  90. const [window, setWindow] = useState(windowOptions[0].value)
  91. const [aggregateBy, setAggregateBy] = useState(aggregationOptions[0].value)
  92. const [accumulate, setAccumulate] = useState(accumulateOptions[0].value)
  93. const [currency, setCurrency] = useState('USD')
  94. // Report state, including current report and saved options
  95. const [title, setTitle] = useState('Last 7 days by namespace daily')
  96. // When parameters changes, fetch data. This should be the
  97. // only mechanism used to fetch data. Also generate a sensible title from the paramters.
  98. useEffect(() => {
  99. setFetch(true)
  100. setTitle(generateTitle({ window, aggregateBy, accumulate }))
  101. }, [window, aggregateBy, accumulate])
  102. // page and settings state
  103. const [init, setInit] = useState(false)
  104. const [fetch, setFetch] = useState(false)
  105. const [loading, setLoading] = useState(true)
  106. const [errors, setErrors] = useState([])
  107. // Initialize once, then fetch report each time setFetch(true) is called
  108. useEffect(() => {
  109. if (!init) {
  110. initialize()
  111. }
  112. if (init && fetch) {
  113. fetchData()
  114. }
  115. }, [init, fetch])
  116. // parse any context information from the URL
  117. const routerLocation = useLocation();
  118. const searchParams = new URLSearchParams(routerLocation.search);
  119. const routerHistory = useHistory();
  120. useEffect(() => {
  121. setWindow(searchParams.get('window') || '6d');
  122. setAggregateBy(searchParams.get('agg') || 'namespace');
  123. setAccumulate((searchParams.get('acc') === 'true') || false);
  124. setCurrency(searchParams.get('currency') || 'USD');
  125. }, [routerLocation]);
  126. async function initialize() {
  127. setInit(true)
  128. }
  129. async function fetchData() {
  130. setLoading(true)
  131. setErrors([])
  132. try {
  133. const resp = await AllocationService.fetchAllocation(window, aggregateBy, { accumulate })
  134. if (resp.data && resp.data.length > 0) {
  135. const allocationRange = resp.data
  136. for (const i in allocationRange) {
  137. // update cluster aggregations to use clusterName/clusterId names
  138. allocationRange[i] = sortBy(allocationRange[i], a => a.totalCost)
  139. }
  140. setAllocationData(allocationRange)
  141. } else {
  142. if (resp.message && resp.message.indexOf('boundary error') >= 0) {
  143. let match = resp.message.match(/(ETL is \d+\.\d+% complete)/)
  144. let secondary = 'Try again after ETL build is complete'
  145. if (match.length > 0) {
  146. secondary = `${match[1]}. ${secondary}`
  147. }
  148. setErrors([{
  149. primary: 'Data unavailable while ETL is building',
  150. secondary: secondary,
  151. }])
  152. }
  153. setAllocationData([])
  154. }
  155. } catch (err) {
  156. if (err.message.indexOf('404') === 0) {
  157. setErrors([{
  158. primary: 'Failed to load report data',
  159. secondary: 'Please update Kubecost to the latest version, then contact support if problems persist.'
  160. }])
  161. } else {
  162. let secondary = 'Please contact Kubecost support with a bug report if problems persist.'
  163. if (err.message.length > 0) {
  164. secondary = err.message
  165. }
  166. setErrors([{
  167. primary: 'Failed to load report data',
  168. secondary: secondary,
  169. }])
  170. }
  171. setAllocationData([])
  172. }
  173. setLoading(false)
  174. setFetch(false)
  175. }
  176. return (
  177. <Page active="reports.html">
  178. <Header>
  179. <IconButton aria-label="refresh" onClick={() => setFetch(true)}>
  180. <RefreshIcon />
  181. </IconButton>
  182. </Header>
  183. {!loading && errors.length > 0 && (
  184. <div style={{ marginBottom: 20 }}>
  185. <Warnings warnings={errors} />
  186. </div>
  187. )}
  188. {init && <Paper id="report">
  189. <div className={classes.reportHeader}>
  190. <div className={classes.titles}>
  191. <Typography variant="h5">{title}</Typography>
  192. <Subtitle
  193. report={{ window, aggregateBy, accumulate }}
  194. />
  195. </div>
  196. <Controls
  197. windowOptions={windowOptions}
  198. window={window}
  199. setWindow={(win) => {
  200. searchParams.set('window', win);
  201. routerHistory.push({
  202. search: `?${searchParams.toString()}`,
  203. });
  204. }}
  205. aggregationOptions={aggregationOptions}
  206. aggregateBy={aggregateBy}
  207. setAggregateBy={(agg) => {
  208. searchParams.set('agg', agg);
  209. routerHistory.push({
  210. search: `?${searchParams.toString()}`,
  211. });
  212. }}
  213. accumulateOptions={accumulateOptions}
  214. accumulate={accumulate}
  215. setAccumulate={(acc) => {
  216. searchParams.set('acc', acc);
  217. routerHistory.push({
  218. search: `?${searchParams.toString()}`
  219. });
  220. }}
  221. title={title}
  222. cumulativeData={cumulativeData}
  223. currency={currency}
  224. currencyOptions={currencyCodes}
  225. setCurrency={(curr) => {
  226. searchParams.set('currency', curr);
  227. routerHistory.push({
  228. search: `?${searchParams.toString()}`
  229. });
  230. }}
  231. />
  232. </div>
  233. {loading && (
  234. <div style={{ display: 'flex', justifyContent: 'center' }}>
  235. <div style={{ paddingTop: 100, paddingBottom: 100 }}>
  236. <CircularProgress />
  237. </div>
  238. </div>
  239. )}
  240. {!loading && (
  241. <AllocationReport
  242. allocationData={allocationData}
  243. cumulativeData={cumulativeData}
  244. totalData={totalData}
  245. currency={currency}
  246. />
  247. )}
  248. </Paper>}
  249. </Page>
  250. )
  251. }
  252. export default React.memo(ReportsPage);