Reports.js 8.8 KB

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