@liatrio/backstage-dora-plugin
Version:
A Backstage plugin for DORA metrics
549 lines (546 loc) • 18.4 kB
JavaScript
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