UNPKG

@liatrio/backstage-dora-plugin

Version:
549 lines (546 loc) 18.4 kB
import React, { useState, useEffect } from 'react'; import { useTheme } from '@mui/material/styles'; import { InfoCard } from '@backstage/core-components'; import { Grid, Box } from '@material-ui/core'; import { Tooltip } from 'react-tooltip'; import { getDateDaysInPast, Theme, TrendGraph, Board, DeploymentFrequencyGraph, ChangeLeadTimeGraph, ChangeFailureRateGraph, RecoverTimeGraph, fetchData, getDateDaysInPastUtc, buildDoraStateForPeriod } from '@liatrio/react-dora-charts'; import { useEntity } from '@backstage/plugin-catalog-react'; import { useApi, configApiRef } from '@backstage/core-plugin-api'; import { useAuthHeaderValueLookup, getRepositoryName, COLOR_LIGHT, COLOR_DARK } from '../helper.esm.js'; import { makeStyles } from '@material-ui/core/styles'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import { ChartTitle } from './ChartTitle.esm.js'; import Dropdown from 'react-dropdown'; import 'react-dropdown/style.css'; const useStyles = makeStyles((theme) => ({ doraCalendar: { "& .react-datepicker__header": { backgroundColor: theme.palette.background.default }, "& .react-datepicker__month-container": { backgroundColor: theme.palette.background.default }, "& .react-datepicker__current-month": { color: theme.palette.text.primary }, "& .react-datepicker__day": { backgroundColor: theme.palette.background.default, color: theme.palette.text.primary, "&:hover": { backgroundColor: "rgb(92, 92, 92)" } }, "& .react-datepicker__day-name": { color: theme.palette.text.primary }, "& .react-datepicker__day--in-range": { backgroundColor: "green", "&:hover": { backgroundColor: "rgb(0, 161, 0)" } }, "& .react-datepicker__input-container input": { backgroundColor: theme.palette.background.default, color: theme.palette.text.primary, padding: "10px" }, "& .react-datepicker": { borderWidth: "2px" } }, doraContainer: { "& .doraCard > :first-child": { padding: "6px 16px 6px 20px" }, "& .doraGrid": { paddingBottom: "0px" }, "& .Dropdown-root": { width: "50%" }, "& .Dropdown-control": { backgroundColor: theme.palette.background.default, color: theme.palette.text.primary }, "& .Dropdown-option is-selected": { backgroundColor: "green", color: theme.palette.text.primary }, "& .Dropdown-option": { backgroundColor: theme.palette.background.default, color: theme.palette.text.primary }, "& .Dropdown-option:hover": { backgroundColor: "green", color: theme.palette.text.primary }, "& .Dropdown-menu": { backgroundColor: theme.palette.background.default }, "& .doraOptions": { overflow: "visible" } }, pageView: { padding: "10px" } })); const defaultMetric = { average: 0, display: "", color: "", trend: 0, rank: 0 }; const defaultMetrics = { deploymentFrequency: defaultMetric, changeLeadTime: defaultMetric, changeFailureRate: defaultMetric, recoverTime: defaultMetric }; const Charts = (props) => { const entityContext = useEntity(); const entity = props.showServiceSelection ? null : entityContext; const configApi = useApi(configApiRef); const backendUrl = configApi.getString("backend.baseUrl"); const dataEndpoint = configApi.getString("dora.dataEndpoint"); const serviceListEndpoint = configApi.getString("dora.serviceListEndpoint"); const includeWeekends = configApi.getOptionalBoolean("dora.includeWeekends"); const showDetails = configApi.getOptionalBoolean("dora.showDetails"); const servicesList = configApi.getOptional("dora.services"); const showTrendGraph = configApi.getOptionalBoolean("dora.showTrendGraph"); const showIndividualTrends = configApi.getOptionalBoolean( "dora.showIndividualTrends" ); const daysToFetch = configApi.getNumber("dora.daysToFetch"); const rankThresholds = configApi.getOptional( "dora.rankThresholds" ); const getAuthHeaderValue = useAuthHeaderValueLookup(); const apiUrl = `${backendUrl}/api/proxy/dora/api/${dataEndpoint}`; const serviceListUrl = `${backendUrl}/api/proxy/dora/api/${serviceListEndpoint}`; const [serviceIndex, setServiceIndex] = useState(0); const [services, setServices] = useState([ { value: "", label: "Please Select" } ]); const [repository, setRepository] = useState(""); const [data, setData] = useState([]); const [startDate, setStartDate] = useState(getDateDaysInPast(30)); const [endDate, setEndDate] = useState(getDateDaysInPast(0)); const [calendarStartDate, setCalendarStartDate] = useState( getDateDaysInPast(30) ); const [calendarEndDate, setCalendarEndDate] = useState( getDateDaysInPast(0) ); const [loading, setLoading] = useState(true); const [metrics, setMetrics] = useState({ ...defaultMetrics }); const [message, setMessage] = useState(""); const classes = useStyles(); const backstageTheme = useTheme(); const theme = backstageTheme.palette.mode === "dark" ? Theme.Dark : Theme.Light; const getMetrics = (respData) => { if (!respData || respData.length === 0) { setMetrics({ ...defaultMetrics }); return; } const metricsData = buildDoraStateForPeriod( { data: [], metricThresholdSet: rankThresholds, holidays: [], includeWeekendsInCalculations: includeWeekends, graphEnd: endDate, graphStart: startDate }, respData, startDate, endDate ); setMetrics(metricsData); }; const updateData = (respData, start, end, msg) => { if (!respData || respData.length < 1) { setData([]); setMetrics({ ...defaultMetrics }); setMessage(""); } else { setData(respData); } getMetrics(respData); if (msg !== void 0) { setMessage(msg); } }; const makeFetchOptions = (service, repositories) => { const fetchOptions = { api: apiUrl, getAuthHeaderValue, start: getDateDaysInPast(daysToFetch), end: getDateDaysInPastUtc(0) }; if (!props.showServiceSelection) { fetchOptions.repositories = repositories; } else { fetchOptions.service = service; } return fetchOptions; }; const fetchServicesData = async (url, getAuthHeader, onSuccess, onError) => { try { const authHeader = await Promise.resolve(getAuthHeader()); const response = await fetch(url, { headers: { Authorization: authHeader || "" } }); if (!response.ok) { throw new Error(`Error fetching services: ${response.statusText}`); } const responseData = await response.json(); onSuccess(responseData); } catch (error) { onError(error); } }; const callFetchData = async (idx, repo) => { const fetchOptions = makeFetchOptions(services[idx]?.value, [repo]); setLoading(true); await fetchData( fetchOptions, (respData) => { updateData(respData, void 0, void 0, ""); setLoading(false); }, (_) => { setLoading(false); } ); }; const updateService = async (value) => { const newIndex = services.findIndex( (range) => range.label === value.label ); setServiceIndex(newIndex); if (!startDate || !endDate) { return; } if (newIndex === 0) { updateData(null, void 0, void 0, "Please select a Service"); return; } setMessage(""); await callFetchData(newIndex, repository); }; const updateDateRange = async (dates) => { const [newStartDate, newEndDate] = dates; setCalendarStartDate(newStartDate); setCalendarEndDate(newEndDate); if (!newStartDate || !newEndDate || props.showServiceSelection && serviceIndex === 0) { return; } setStartDate(newStartDate); setCalendarEndDate(newEndDate); }; useEffect(() => { setLoading(true); let repositoryName = ""; if (!props.showServiceSelection) { repositoryName = getRepositoryName(entity); setRepository(repositoryName); if (!repositoryName) { setLoading(false); return; } } const fetch2 = props.showServiceSelection ? async () => { if (servicesList && servicesList.length > 0) { const serviceEntries = [ { value: "", label: "Please Select" } ]; for (const service of servicesList) { serviceEntries.push({ value: service, label: service }); } setMessage("Please select a Service"); setLoading(false); setServices(serviceEntries); } else { fetchServicesData( serviceListUrl, getAuthHeaderValue, (services_data) => { const newList = [{ label: "Please Select", value: "" }]; for (const entry of services_data.services) { const newEntry = { label: entry, value: entry }; newList.push(newEntry); } setServices(newList); setLoading(false); }, (_) => { setLoading(false); } ); } } : async () => { callFetchData(serviceIndex, repositoryName); }; fetch2(); }, []); if (repository === "" && !props.showServiceSelection) { return /* @__PURE__ */ React.createElement("div", null, "DORA Metrics are not available for Non-GitHub repos currently"); } const tTitle = /* @__PURE__ */ React.createElement( ChartTitle, { title: "DORA: At a Glance", info: "Your DORA Trend, week over week, for the period selected", theme } ); const bTitle = /* @__PURE__ */ React.createElement( ChartTitle, { title: "DORA: At a Glance", info: "How well you are doing in each of the DORA Metrics", theme } ); const dfTitle = /* @__PURE__ */ React.createElement( ChartTitle, { scoreDisplay: metrics.deploymentFrequency.display, color: metrics.deploymentFrequency.color, title: "Deployment Frequency", info: "How often an organization successfully releases to production", theme } ); const cfrTitle = /* @__PURE__ */ React.createElement( ChartTitle, { scoreDisplay: metrics.changeFailureRate.display, color: metrics.changeFailureRate.color, title: "Change Failure Rate", info: "The percentage of deployments causing a failure in production", theme } ); const cltTitle = /* @__PURE__ */ React.createElement( ChartTitle, { scoreDisplay: metrics.changeLeadTime.display, color: metrics.changeLeadTime.color, title: "Change Lead Time", info: "The amount of time it takes a commit to get into production", theme } ); const rtTitle = /* @__PURE__ */ React.createElement( ChartTitle, { scoreDisplay: metrics.recoverTime.display, color: metrics.recoverTime.color, title: "Recovery Time", info: "How long it takes an organization to recover from a failure in production", theme } ); const containerClass = props.showServiceSelection ? `${classes.doraContainer} ${classes.pageView}` : classes.doraContainer; return /* @__PURE__ */ React.createElement("div", { className: containerClass }, /* @__PURE__ */ React.createElement( Tooltip, { id: "metric_tooltip", place: "bottom", border: `1px solid ${theme === Theme.Dark ? COLOR_LIGHT : COLOR_DARK}`, opacity: "1", style: { borderRadius: "10px", maxWidth: "300px", padding: "10px", zIndex: "100", backgroundColor: backstageTheme.palette.background.default } } ), /* @__PURE__ */ React.createElement( Grid, { container: true, style: { marginBottom: "12px", width: "calc(100% + 22px)" }, spacing: 3, alignItems: "stretch" }, /* @__PURE__ */ React.createElement( Grid, { item: true, md: 6, style: { paddingBottom: "25px", overflow: "visible" } }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Options", className: "doraOptions doraCard" }, /* @__PURE__ */ React.createElement(Box, { overflow: "visible", position: "relative" }, /* @__PURE__ */ React.createElement( Box, { overflow: "visible", display: "flex", justifyContent: "center", alignItems: "center" }, /* @__PURE__ */ React.createElement( "label", { htmlFor: "select-date-range", style: { paddingRight: "10px" } }, "Select Date Range:" ), /* @__PURE__ */ React.createElement("div", { className: classes.doraCalendar }, /* @__PURE__ */ React.createElement( DatePicker, { id: "select-date-range", selected: calendarStartDate, onChange: updateDateRange, startDate: calendarStartDate, endDate: calendarEndDate, selectsRange: true, popperPlacement: "bottom" } )), props.showServiceSelection && /* @__PURE__ */ React.createElement( "div", { style: { width: "50%", display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement("span", { style: { paddingRight: "10px" } }, "Select Service:"), /* @__PURE__ */ React.createElement( Dropdown, { options: services, onChange: updateService, value: services[serviceIndex] } ) ) ))) ), /* @__PURE__ */ React.createElement(Grid, { item: true, md: 6, className: "doraGrid" }, /* @__PURE__ */ React.createElement( InfoCard, { title: showTrendGraph ? tTitle : bTitle, className: "doraCard", noPadding: true }, /* @__PURE__ */ React.createElement(Box, { position: "relative" }, /* @__PURE__ */ React.createElement(Box, { display: "flex", justifyContent: "flex-end" }, /* @__PURE__ */ React.createElement( "div", { style: { width: "800px", height: "220px", paddingBottom: showIndividualTrends ? "10px" : "" } }, showTrendGraph ? /* @__PURE__ */ React.createElement( TrendGraph, { showIndividualTrends, data, loading, graphStart: startDate, graphEnd: endDate, metricThresholdSet: rankThresholds, message, theme } ) : /* @__PURE__ */ React.createElement( Board, { data, loading, alwaysShowDetails: showDetails, includeWeekendsInCalculations: includeWeekends, graphStart: startDate, graphEnd: endDate, metricThresholdSet: rankThresholds, message, theme } ) ))) )) ), /* @__PURE__ */ React.createElement( Grid, { container: true, spacing: 3, alignItems: "stretch", style: { width: "calc(100% + 22px)" } }, /* @__PURE__ */ React.createElement(Grid, { item: true, md: 6, className: "doraGrid" }, /* @__PURE__ */ React.createElement(InfoCard, { title: dfTitle, className: "doraCard" }, /* @__PURE__ */ React.createElement(Box, { position: "relative" }, /* @__PURE__ */ React.createElement(Box, { display: "flex", justifyContent: "flex-end" }, /* @__PURE__ */ React.createElement("div", { style: { width: "800px", height: "200px" } }, /* @__PURE__ */ React.createElement( DeploymentFrequencyGraph, { data, loading, includeWeekendsInCalculations: includeWeekends, graphStart: startDate, graphEnd: endDate, message, theme } )))))), /* @__PURE__ */ React.createElement(Grid, { item: true, md: 6, className: "doraGrid" }, /* @__PURE__ */ React.createElement(InfoCard, { title: cltTitle, className: "doraCard" }, /* @__PURE__ */ React.createElement(Box, { position: "relative" }, /* @__PURE__ */ React.createElement(Box, { display: "flex", justifyContent: "flex-end" }, /* @__PURE__ */ React.createElement("div", { style: { width: "800px", height: "200px" } }, /* @__PURE__ */ React.createElement( ChangeLeadTimeGraph, { data, loading, includeWeekendsInCalculations: includeWeekends, graphStart: startDate, graphEnd: endDate, message, theme } )))))), /* @__PURE__ */ React.createElement(Grid, { item: true, md: 6 }, /* @__PURE__ */ React.createElement(InfoCard, { title: cfrTitle, className: "doraCard" }, /* @__PURE__ */ React.createElement(Box, { position: "relative" }, /* @__PURE__ */ React.createElement(Box, { display: "flex", justifyContent: "flex-end" }, /* @__PURE__ */ React.createElement("div", { style: { width: "800px", height: "200px" } }, /* @__PURE__ */ React.createElement( ChangeFailureRateGraph, { data, loading, includeWeekendsInCalculations: includeWeekends, graphStart: startDate, graphEnd: endDate, message, theme } )))))), /* @__PURE__ */ React.createElement(Grid, { item: true, md: 6 }, /* @__PURE__ */ React.createElement(InfoCard, { title: rtTitle, className: "doraCard" }, /* @__PURE__ */ React.createElement(Box, { position: "relative" }, /* @__PURE__ */ React.createElement(Box, { display: "flex", justifyContent: "flex-end" }, /* @__PURE__ */ React.createElement("div", { style: { width: "800px", height: "200px" } }, /* @__PURE__ */ React.createElement( RecoverTimeGraph, { data, loading, includeWeekendsInCalculations: includeWeekends, graphStart: startDate, graphEnd: endDate, message, theme } )))))) )); }; export { Charts }; //# sourceMappingURL=Charts.esm.js.map