/* Copyright (C) 2017 Cloudbase Solutions SRL This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { DateTime, Duration } from "luxon"; import { observer } from "mobx-react"; import * as React from "react"; import styled from "styled-components"; import { MigrationItem, ReplicaItem, TransferItem } from "@src/@types/MainItem"; import DashboardBarChart from "@src/components/modules/DashboardModule/DashboardBarChart"; import { ThemePalette, ThemeProps } from "@src/components/Theme"; import DropdownLink from "@src/components/ui/Dropdowns/DropdownLink"; import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; import DateUtils from "@src/utils/DateUtils"; import emptyBackgroundImage from "./images/empty-background.svg"; const INTERVALS = [ { label: "Last {x} days", value: "30-days" }, { label: "Last 12 months", value: "1-years" }, ]; const Wrapper = styled.div``; const Title = styled.div` font-size: 24px; font-weight: ${ThemeProps.fontWeights.light}; margin-bottom: 12px; `; const Module = styled.div` position: relative; display: flex; background: ${ThemePalette.grayscale[0]}; border-radius: ${ThemeProps.borderRadius}; height: 240px; `; const ChartWrapper = styled.div` display: flex; flex-direction: column; height: 100%; width: 100%; `; const BarChartWrapper = styled.div` display: flex; height: 100%; width: 100%; `; const LoadingWrapper = styled.div` display: flex; width: 100%; height: 100%; overflow: hidden; justify-content: center; align-items: center; `; const DropdownWrapper = styled.div` display: flex; justify-content: flex-end; margin: 16px; `; const Tooltip = styled.div` position: absolute; bottom: ${props => props.position.y}px; left: ${props => props.position.x}px; background: ${ThemePalette.black}; padding: 8px 16px 16px 16px; border-radius: ${ThemeProps.borderRadius}; color: white; ${ThemeProps.exactWidth("174px")} box-shadow: rgba(0,0,0,0.1) 0 0 6px 1px; `; const TooltipHeader = styled.div` font-size: 24px; font-weight: ${ThemeProps.fontWeights.light}; text-align: center; border-bottom: 1px solid; padding-bottom: 4px; `; const TooltipBody = styled.div` font-size: 12px; `; const TooltipRow = styled.div` display: flex; justify-content: space-between; margin-top: 16px; `; const TooltipRowLabel = styled.div``; const TooltipTip = styled.div` position: absolute; width: 16px; height: 16px; bottom: -8px; background: ${ThemePalette.black}; left: calc(50% - 16px); transform: rotate(45deg); `; const NoData = styled.div` padding: 0 16px; position: relative; `; const NoDataMessage = styled.div` position: absolute; font-size: 17px; color: ${ThemePalette.grayscale[4]}; display: flex; top: 0; bottom: 0; right: 0; left: 0; justify-content: center; align-items: center; text-shadow: rgba(255, 255, 255, 1) 0px 0px 20px; `; const EmptyBackgroundImage = styled.div` width: 100%; height: 146px; background: url("${emptyBackgroundImage}"); `; type Props = { // eslint-disable-next-line react/no-unused-prop-types replicas: ReplicaItem[]; migrations: MigrationItem[]; loading: boolean; }; type GroupedData = { label: string; values: number[]; data?: string; }; type TooltipData = { title: string; migrations: number; replicas: number; }; type State = { selectedPeriod: string; groupedData: GroupedData[]; tooltipPosition: { x: number; y: number }; tooltipData: TooltipData | null; }; const COLORS = [ThemePalette.alert, ThemePalette.primary]; @observer class DashboardExecutions extends React.Component { state: State = { selectedPeriod: INTERVALS[0].value, groupedData: [], tooltipData: null, tooltipPosition: { x: 0, y: 0 }, }; componentDidMount() { this.groupCreations(this.props); } UNSAFE_componentWillReceiveProps(props: Props) { this.groupCreations(props); } groupCreations(props: Props) { let creations: TransferItem[] = [...props.replicas, ...props.migrations]; const periodUnit: any = this.state.selectedPeriod.split("-")[1]; const periodValue: any = Number(this.state.selectedPeriod.split("-")[0]); const oldestDate: Date = DateTime.local() .minus(Duration.fromObject({ [periodUnit]: periodValue })) .toJSDate(); creations = creations.filter( e => new Date(e.created_at).getTime() >= oldestDate.getTime() ); creations.sort( (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() ); this.groupByPeriod(creations, periodUnit); } groupByPeriod(transferItems: TransferItem[], periodUnit: string) { const groupedData: GroupedData[] = []; const periods: { [period: string]: { replicas: number; migrations: number }; } = {}; transferItems.forEach(item => { const date = DateUtils.getUtcDate(item.created_at); const period: string = periodUnit === "days" ? date.toFormat("dd-LLL-yyyy_dd LLLL") : date.toFormat("LLL-yyyy_LLLL yyyy"); if (!periods[period]) { periods[period] = { replicas: 0, migrations: 0 }; } if (item.type === "replica") { periods[period].replicas += 1; } else if (item.type === "migration") { periods[period].migrations += 1; } }); Object.keys(periods).forEach(period => { if (!periods[period].replicas && !periods[period].migrations) { return; } const label = period.split("_")[0]; const title = period.split("_")[1]; groupedData.push({ label: periodUnit === "days" ? `${label.split("-")[0]} ${label.split("-")[1]}` : label.split("-")[0], values: [periods[period].migrations, periods[period].replicas], data: title, }); }); this.setState({ groupedData }); } handleDropdownChange(selectedPeriod: string) { this.setState({ selectedPeriod }, () => { this.groupCreations(this.props); }); } handleBarMouseEnter(position: { x: number; y: number }, item: GroupedData) { this.setState({ tooltipPosition: { x: position.x - 86, y: position.y }, tooltipData: { replicas: item.values[1], migrations: item.values[0], title: item.data || "-", }, }); } handleBarMouseLeave() { this.setState({ tooltipData: null }); } renderDropdown() { const items = INTERVALS.map(interval => ({ value: interval.value, label: interval.label.replace("{x}", interval.value.split("-")[0]), })); const selectedItem = INTERVALS.find( i => i.value === this.state.selectedPeriod ); return ( { this.handleDropdownChange(item.value); }} /> ); } renderTooltip() { const data = this.state.tooltipData; if (!data) { return null; } return ( {data.title} Created {data.replicas + data.migrations} Replicas {data.replicas} Migrations {data.migrations} ); } renderBarChart() { return ( { this.handleBarMouseEnter(position, item); }} onBarMouseLeave={() => { this.handleBarMouseLeave(); }} /> {this.renderTooltip()} ); } renderChart() { return ( {this.renderDropdown()} {this.state.groupedData.length ? this.renderBarChart() : this.renderNoData()} ); } renderLoading() { return ( ); } renderNoData() { return ( No recent activity in this project ); } render() { return ( Items Created {this.props.replicas.length === 0 && this.props.loading ? this.renderLoading() : this.renderChart()} ); } } export default DashboardExecutions;