DashboardExecutions.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. import { DateTime, Duration } from "luxon";
  15. import { observer } from "mobx-react";
  16. import * as React from "react";
  17. import styled from "styled-components";
  18. import { MigrationItem, ReplicaItem, TransferItem } from "@src/@types/MainItem";
  19. import DashboardBarChart from "@src/components/modules/DashboardModule/DashboardBarChart";
  20. import { ThemePalette, ThemeProps } from "@src/components/Theme";
  21. import DropdownLink from "@src/components/ui/Dropdowns/DropdownLink";
  22. import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
  23. import DateUtils from "@src/utils/DateUtils";
  24. import emptyBackgroundImage from "./images/empty-background.svg";
  25. const INTERVALS = [
  26. { label: "Last {x} days", value: "30-days" },
  27. { label: "Last 12 months", value: "1-years" },
  28. ];
  29. const Wrapper = styled.div<any>``;
  30. const Title = styled.div<any>`
  31. font-size: 24px;
  32. font-weight: ${ThemeProps.fontWeights.light};
  33. margin-bottom: 12px;
  34. `;
  35. const Module = styled.div<any>`
  36. position: relative;
  37. display: flex;
  38. background: ${ThemePalette.grayscale[0]};
  39. border-radius: ${ThemeProps.borderRadius};
  40. height: 240px;
  41. `;
  42. const ChartWrapper = styled.div<any>`
  43. display: flex;
  44. flex-direction: column;
  45. height: 100%;
  46. width: 100%;
  47. `;
  48. const BarChartWrapper = styled.div<any>`
  49. display: flex;
  50. height: 100%;
  51. width: 100%;
  52. `;
  53. const LoadingWrapper = styled.div<any>`
  54. display: flex;
  55. width: 100%;
  56. height: 100%;
  57. overflow: hidden;
  58. justify-content: center;
  59. align-items: center;
  60. `;
  61. const DropdownWrapper = styled.div<any>`
  62. display: flex;
  63. justify-content: flex-end;
  64. margin: 16px;
  65. `;
  66. const Tooltip = styled.div<any>`
  67. position: absolute;
  68. bottom: ${props => props.position.y}px;
  69. left: ${props => props.position.x}px;
  70. background: ${ThemePalette.black};
  71. padding: 8px 16px 16px 16px;
  72. border-radius: ${ThemeProps.borderRadius};
  73. color: white;
  74. ${ThemeProps.exactWidth("174px")}
  75. box-shadow: rgba(0,0,0,0.1) 0 0 6px 1px;
  76. `;
  77. const TooltipHeader = styled.div<any>`
  78. font-size: 24px;
  79. font-weight: ${ThemeProps.fontWeights.light};
  80. text-align: center;
  81. border-bottom: 1px solid;
  82. padding-bottom: 4px;
  83. `;
  84. const TooltipBody = styled.div<any>`
  85. font-size: 12px;
  86. `;
  87. const TooltipRow = styled.div<any>`
  88. display: flex;
  89. justify-content: space-between;
  90. margin-top: 16px;
  91. `;
  92. const TooltipRowLabel = styled.div<any>``;
  93. const TooltipTip = styled.div<any>`
  94. position: absolute;
  95. width: 16px;
  96. height: 16px;
  97. bottom: -8px;
  98. background: ${ThemePalette.black};
  99. left: calc(50% - 16px);
  100. transform: rotate(45deg);
  101. `;
  102. const NoData = styled.div<any>`
  103. padding: 0 16px;
  104. position: relative;
  105. `;
  106. const NoDataMessage = styled.div<any>`
  107. position: absolute;
  108. font-size: 17px;
  109. color: ${ThemePalette.grayscale[4]};
  110. display: flex;
  111. top: 0;
  112. bottom: 0;
  113. right: 0;
  114. left: 0;
  115. justify-content: center;
  116. align-items: center;
  117. text-shadow: rgba(255, 255, 255, 1) 0px 0px 20px;
  118. `;
  119. const EmptyBackgroundImage = styled.div<any>`
  120. width: 100%;
  121. height: 146px;
  122. background: url("${emptyBackgroundImage}");
  123. `;
  124. type Props = {
  125. // eslint-disable-next-line react/no-unused-prop-types
  126. replicas: ReplicaItem[];
  127. migrations: MigrationItem[];
  128. loading: boolean;
  129. };
  130. type GroupedData = {
  131. label: string;
  132. values: number[];
  133. data?: string;
  134. };
  135. type TooltipData = {
  136. title: string;
  137. migrations: number;
  138. replicas: number;
  139. };
  140. type State = {
  141. selectedPeriod: string;
  142. groupedData: GroupedData[];
  143. tooltipPosition: { x: number; y: number };
  144. tooltipData: TooltipData | null;
  145. };
  146. const COLORS = [ThemePalette.alert, ThemePalette.primary];
  147. @observer
  148. class DashboardExecutions extends React.Component<Props, State> {
  149. state: State = {
  150. selectedPeriod: INTERVALS[0].value,
  151. groupedData: [],
  152. tooltipData: null,
  153. tooltipPosition: { x: 0, y: 0 },
  154. };
  155. componentDidMount() {
  156. this.groupCreations(this.props);
  157. }
  158. UNSAFE_componentWillReceiveProps(props: Props) {
  159. this.groupCreations(props);
  160. }
  161. groupCreations(props: Props) {
  162. let creations: TransferItem[] = [...props.replicas, ...props.migrations];
  163. const periodUnit: any = this.state.selectedPeriod.split("-")[1];
  164. const periodValue: any = Number(this.state.selectedPeriod.split("-")[0]);
  165. const oldestDate: Date = DateTime.local()
  166. .minus(Duration.fromObject({ [periodUnit]: periodValue }))
  167. .toJSDate();
  168. creations = creations.filter(
  169. e => new Date(e.created_at).getTime() >= oldestDate.getTime()
  170. );
  171. creations.sort(
  172. (a, b) =>
  173. new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
  174. );
  175. this.groupByPeriod(creations, periodUnit);
  176. }
  177. groupByPeriod(transferItems: TransferItem[], periodUnit: string) {
  178. const groupedData: GroupedData[] = [];
  179. const periods: {
  180. [period: string]: { replicas: number; migrations: number };
  181. } = {};
  182. transferItems.forEach(item => {
  183. const date = DateUtils.getUtcDate(item.created_at);
  184. const period: string =
  185. periodUnit === "days"
  186. ? date.toFormat("dd-LLL-yyyy_dd LLLL")
  187. : date.toFormat("LLL-yyyy_LLLL yyyy");
  188. if (!periods[period]) {
  189. periods[period] = { replicas: 0, migrations: 0 };
  190. }
  191. if (item.type === "replica") {
  192. periods[period].replicas += 1;
  193. } else if (item.type === "migration") {
  194. periods[period].migrations += 1;
  195. }
  196. });
  197. Object.keys(periods).forEach(period => {
  198. if (!periods[period].replicas && !periods[period].migrations) {
  199. return;
  200. }
  201. const label = period.split("_")[0];
  202. const title = period.split("_")[1];
  203. groupedData.push({
  204. label:
  205. periodUnit === "days"
  206. ? `${label.split("-")[0]} ${label.split("-")[1]}`
  207. : label.split("-")[0],
  208. values: [periods[period].migrations, periods[period].replicas],
  209. data: title,
  210. });
  211. });
  212. this.setState({ groupedData });
  213. }
  214. handleDropdownChange(selectedPeriod: string) {
  215. this.setState({ selectedPeriod }, () => {
  216. this.groupCreations(this.props);
  217. });
  218. }
  219. handleBarMouseEnter(position: { x: number; y: number }, item: GroupedData) {
  220. this.setState({
  221. tooltipPosition: { x: position.x - 86, y: position.y },
  222. tooltipData: {
  223. replicas: item.values[1],
  224. migrations: item.values[0],
  225. title: item.data || "-",
  226. },
  227. });
  228. }
  229. handleBarMouseLeave() {
  230. this.setState({ tooltipData: null });
  231. }
  232. renderDropdown() {
  233. const items = INTERVALS.map(interval => ({
  234. value: interval.value,
  235. label: interval.label.replace("{x}", interval.value.split("-")[0]),
  236. }));
  237. const selectedItem = INTERVALS.find(
  238. i => i.value === this.state.selectedPeriod
  239. );
  240. return (
  241. <DropdownWrapper>
  242. <DropdownLink
  243. items={items}
  244. selectedItem={selectedItem && selectedItem.value}
  245. onChange={item => {
  246. this.handleDropdownChange(item.value);
  247. }}
  248. />
  249. </DropdownWrapper>
  250. );
  251. }
  252. renderTooltip() {
  253. const data = this.state.tooltipData;
  254. if (!data) {
  255. return null;
  256. }
  257. return (
  258. <Tooltip position={this.state.tooltipPosition}>
  259. <TooltipHeader>{data.title}</TooltipHeader>
  260. <TooltipBody>
  261. <TooltipRow>
  262. <TooltipRowLabel>Created</TooltipRowLabel>
  263. <TooltipRowLabel>{data.replicas + data.migrations}</TooltipRowLabel>
  264. </TooltipRow>
  265. <TooltipRow>
  266. <TooltipRowLabel>Replicas</TooltipRowLabel>
  267. <TooltipRowLabel>{data.replicas}</TooltipRowLabel>
  268. </TooltipRow>
  269. <TooltipRow>
  270. <TooltipRowLabel>Migrations</TooltipRowLabel>
  271. <TooltipRowLabel>{data.migrations}</TooltipRowLabel>
  272. </TooltipRow>
  273. </TooltipBody>
  274. <TooltipTip />
  275. </Tooltip>
  276. );
  277. }
  278. renderBarChart() {
  279. return (
  280. <BarChartWrapper>
  281. <DashboardBarChart
  282. style={{ height: "164px" }}
  283. yNumTicks={3}
  284. data={this.state.groupedData}
  285. colors={COLORS}
  286. onBarMouseEnter={(position, item) => {
  287. this.handleBarMouseEnter(position, item);
  288. }}
  289. onBarMouseLeave={() => {
  290. this.handleBarMouseLeave();
  291. }}
  292. />
  293. {this.renderTooltip()}
  294. </BarChartWrapper>
  295. );
  296. }
  297. renderChart() {
  298. return (
  299. <ChartWrapper>
  300. {this.renderDropdown()}
  301. {this.state.groupedData.length
  302. ? this.renderBarChart()
  303. : this.renderNoData()}
  304. </ChartWrapper>
  305. );
  306. }
  307. renderLoading() {
  308. return (
  309. <LoadingWrapper>
  310. <StatusImage status="RUNNING" />
  311. </LoadingWrapper>
  312. );
  313. }
  314. renderNoData() {
  315. return (
  316. <NoData>
  317. <EmptyBackgroundImage />
  318. <NoDataMessage>No recent activity in this project</NoDataMessage>
  319. </NoData>
  320. );
  321. }
  322. render() {
  323. return (
  324. <Wrapper>
  325. <Title>Items Created</Title>
  326. <Module>
  327. {this.props.replicas.length === 0 && this.props.loading
  328. ? this.renderLoading()
  329. : this.renderChart()}
  330. </Module>
  331. </Wrapper>
  332. );
  333. }
  334. }
  335. export default DashboardExecutions;