@enginuity-io/dora-metrics
Version:
DORA Metrics frontend plugin for Backstage
514 lines (509 loc) • 20.7 kB
JavaScript
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { useState, useEffect } from 'react';
import { ErrorOutline } from '@material-ui/icons';
import { makeStyles } from '@material-ui/core/styles';
import { Card, CardContent, Typography, Tooltip, Box, Button, Grid, FormControl, InputLabel, Select, MenuItem, Paper, CircularProgress } from '@material-ui/core';
import Alert from '@material-ui/lab/Alert';
import { Page, Header, Content, ContentHeader, SupportButton } from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Tooltip as Tooltip$1, Legend, Bar, LineChart, Line } from 'recharts';
import HelpOutlineIcon from '@material-ui/icons/HelpOutline';
function getDoraMetricsConfig(config) {
const doraMetricsConfig = config.getOptionalConfig("doraMetrics");
let backendUrl = (doraMetricsConfig == null ? void 0 : doraMetricsConfig.getOptionalString("backendUrl")) || "/api/dora-metrics";
if (backendUrl.endsWith("/")) {
backendUrl = backendUrl.slice(0, -1);
}
let apiPath = (doraMetricsConfig == null ? void 0 : doraMetricsConfig.getOptionalString("apiPath")) || "";
if (apiPath && !apiPath.startsWith("/")) {
apiPath = "/" + apiPath;
}
if (apiPath && apiPath.endsWith("/")) {
apiPath = apiPath.slice(0, -1);
}
const apiUrl = backendUrl.startsWith("http") ? `${backendUrl}${apiPath}` : backendUrl;
const frontendUrl = (doraMetricsConfig == null ? void 0 : doraMetricsConfig.getOptionalString("frontendUrl")) || "http://localhost:8000";
console.log("Config helper - backendUrl:", backendUrl);
console.log("Config helper - apiPath:", apiPath);
console.log("Config helper - apiUrl:", apiUrl);
console.log("Config helper - frontendUrl:", frontendUrl);
return {
backendUrl,
apiPath,
apiUrl,
frontendUrl
};
}
const useStyles$1 = makeStyles((theme) => ({
root: {
minWidth: 275,
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between"
},
elite: {
backgroundColor: "#c8e6c9"
// Light green
},
high: {
backgroundColor: "#fff9c4"
// Light yellow
},
medium: {
backgroundColor: "#ffccbc"
// Light orange
},
low: {
backgroundColor: "#ffcdd2"
// Light red
},
title: {
fontSize: 16,
display: "flex",
alignItems: "center",
marginBottom: theme.spacing(2)
},
value: {
fontSize: 36,
fontWeight: "bold"
},
helpIcon: {
fontSize: 16,
marginLeft: theme.spacing(1),
cursor: "pointer"
},
description: {
marginTop: theme.spacing(1)
}
}));
const ScoreCard = ({
title,
value,
level,
description,
helpText
}) => {
const classes = useStyles$1();
return /* @__PURE__ */ jsx(Card, { className: `${classes.root} ${classes[level]}`, children: /* @__PURE__ */ jsxs(CardContent, { children: [
/* @__PURE__ */ jsxs(Typography, { className: classes.title, color: "textSecondary", gutterBottom: true, children: [
title,
/* @__PURE__ */ jsx(Tooltip, { title: helpText, arrow: true, children: /* @__PURE__ */ jsx(HelpOutlineIcon, { className: classes.helpIcon }) })
] }),
/* @__PURE__ */ jsx(Typography, { className: classes.value, variant: "h5", component: "div", children: value }),
/* @__PURE__ */ jsx(Box, { mt: 1, children: /* @__PURE__ */ jsxs(Typography, { variant: "caption", color: "textSecondary", children: [
"Performance: ",
/* @__PURE__ */ jsx("strong", { children: level.toUpperCase() })
] }) }),
/* @__PURE__ */ jsx(Typography, { className: classes.description, variant: "body2", component: "p", children: description })
] }) });
};
const useStyles = makeStyles((theme) => ({
root: {
maxWidth: 1200,
margin: "0 auto"
},
filters: {
display: "flex",
justifyContent: "space-between",
marginBottom: theme.spacing(2)
},
formControl: {
minWidth: 200,
marginRight: theme.spacing(2)
},
chartContainer: {
marginBottom: theme.spacing(4)
},
loading: {
display: "flex",
justifyContent: "center",
padding: theme.spacing(4)
},
scoreCardGrid: {
marginBottom: theme.spacing(4)
}
}));
const timeRangeOptions = [
{ value: "7d", label: "Last 7 days" },
{ value: "30d", label: "Last 30 days" },
{ value: "90d", label: "Last 90 days" },
{ value: "180d", label: "Last 180 days" },
{ value: "365d", label: "Last 365 days" }
];
const DoraMetricsPage = () => {
const configApi = useApi(configApiRef);
const doraMetricsConfig = getDoraMetricsConfig(configApi);
const apiUrl = doraMetricsConfig.apiUrl;
console.log("Using API URL from helper:", apiUrl);
const classes = useStyles();
const [projects, setProjects] = useState([]);
const [selectedProject, setSelectedProject] = useState("");
const [timeRange, setTimeRange] = useState("30d");
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [apiStatus, setApiStatus] = useState("idle");
const [deploymentFrequency, setDeploymentFrequency] = useState([]);
const [leadTime, setLeadTime] = useState([]);
const [mttr, setMttr] = useState([]);
const [failureRate, setFailureRate] = useState([]);
const [metricsSummary, setMetricsSummary] = useState({
deploymentFrequency: { value: "N/A", level: "unknown" },
leadTimeForChanges: { value: "N/A", level: "unknown" },
meanTimeToRestore: { value: "N/A", level: "unknown" },
changeFailureRate: { value: "N/A", level: "unknown" }
});
const fetchProjects = async () => {
try {
setApiStatus("loading");
console.log("Fetching projects from:", `${apiUrl}/projects`);
const response = await fetch(`${apiUrl}/projects`);
if (!response.ok) {
throw new Error(`API returned status ${response.status}`);
}
const data = await response.json();
console.log("Projects data received:", data);
if (!Array.isArray(data)) {
throw new Error("Invalid data format received from API");
}
setProjects(data);
setApiStatus("success");
if (data.length > 0) {
setSelectedProject(data[0]);
} else {
setError("No projects found. Please make sure Apache DevLake has data available.");
}
} catch (err) {
setApiStatus("error");
const errorMessage = err instanceof Error ? err.message : "Unknown error";
console.error("Error fetching projects:", errorMessage);
if (errorMessage.includes("Failed to fetch") || errorMessage.includes("NetworkError")) {
setError("Network error: Unable to connect to the backend server. Please ensure the backend server is running and properly configured.");
} else if (errorMessage.includes("404")) {
setError("API endpoint not found. Please check if the backend server is configured correctly.");
} else if (errorMessage.includes("500")) {
setError("Backend server error. Please check the server logs for more information.");
} else {
setError(`Failed to fetch projects: ${errorMessage}. Please check if the backend server is running and properly configured.`);
}
}
};
const fetchMetrics = async () => {
if (!selectedProject)
return;
setLoading(true);
setError(null);
setApiStatus("loading");
try {
const [dfResponse, ltResponse, mttrResponse, frResponse, summaryResponse] = await Promise.all([
fetch(`${apiUrl}/metrics/deployment-frequency?projectKey=${selectedProject}&timeRange=${timeRange}`),
fetch(`${apiUrl}/metrics/lead-time?projectKey=${selectedProject}&timeRange=${timeRange}`),
fetch(`${apiUrl}/metrics/mttr?projectKey=${selectedProject}&timeRange=${timeRange}`),
fetch(`${apiUrl}/metrics/failure-rate?projectKey=${selectedProject}&timeRange=${timeRange}`),
fetch(`${apiUrl}/metrics/summary?projectKey=${selectedProject}&timeRange=${timeRange}`)
]);
const responses = [dfResponse, ltResponse, mttrResponse, frResponse, summaryResponse];
const failedResponse = responses.find((response) => !response.ok);
if (failedResponse) {
throw new Error(`API returned status ${failedResponse.status}`);
}
const dfData = await dfResponse.json();
const ltData = await ltResponse.json();
const mttrData = await mttrResponse.json();
const frData = await frResponse.json();
const summaryData = await summaryResponse.json();
setDeploymentFrequency(Array.isArray(dfData) ? dfData : []);
setLeadTime(Array.isArray(ltData) ? ltData : []);
setMttr(Array.isArray(mttrData) ? mttrData : []);
setFailureRate(Array.isArray(frData) ? frData : []);
setMetricsSummary(summaryData);
setApiStatus("success");
} catch (err) {
setApiStatus("error");
setError("Failed to fetch metrics data. The backend server might not be running or is experiencing issues.");
console.error("Error fetching metrics:", err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchProjects();
}, []);
useEffect(() => {
if (selectedProject) {
fetchMetrics();
}
}, [selectedProject, timeRange]);
const handleProjectChange = (event) => {
setSelectedProject(event.target.value);
};
const handleTimeRangeChange = (event) => {
setTimeRange(event.target.value);
};
const handleRetry = () => {
setApiStatus("idle");
setError(null);
fetchProjects();
};
const handleRefresh = () => {
fetchMetrics();
};
const renderErrorContent = () => {
if (apiStatus !== "error")
return null;
return /* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsx(Paper, { className: classes.loading, children: /* @__PURE__ */ jsxs(Alert, { severity: "error", icon: /* @__PURE__ */ jsx(ErrorOutline, {}), children: [
error,
/* @__PURE__ */ jsx("div", { style: { marginTop: "16px" }, children: /* @__PURE__ */ jsx(
Button,
{
variant: "contained",
color: "primary",
onClick: handleRetry,
children: "Retry Connection"
}
) })
] }) }) });
};
const renderScoreCards = () => {
if (!metricsSummary)
return null;
return /* @__PURE__ */ jsxs(Grid, { container: true, spacing: 3, className: classes.scoreCardGrid, children: [
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, sm: 6, md: 3, children: /* @__PURE__ */ jsx(
ScoreCard,
{
title: "Deployment Frequency",
value: metricsSummary.deploymentFrequency.value,
level: metricsSummary.deploymentFrequency.level,
description: "How often code is deployed to production",
helpText: "Deployment Frequency measures how often your team successfully releases to production."
}
) }),
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, sm: 6, md: 3, children: /* @__PURE__ */ jsx(
ScoreCard,
{
title: "Lead Time for Changes",
value: metricsSummary.leadTimeForChanges.value,
level: metricsSummary.leadTimeForChanges.level,
description: "Time from code commit to deployment",
helpText: "Lead Time for Changes measures the amount of time it takes for code to go from commit to successfully running in production."
}
) }),
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, sm: 6, md: 3, children: /* @__PURE__ */ jsx(
ScoreCard,
{
title: "Mean Time to Restore",
value: metricsSummary.meanTimeToRestore.value,
level: metricsSummary.meanTimeToRestore.level,
description: "Time to restore service after failure",
helpText: "Mean Time to Restore measures how long it takes to restore service when an incident or defect impacts users."
}
) }),
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, sm: 6, md: 3, children: /* @__PURE__ */ jsx(
ScoreCard,
{
title: "Change Failure Rate",
value: metricsSummary.changeFailureRate.value,
level: metricsSummary.changeFailureRate.level,
description: "Percentage of deployments causing failures",
helpText: "Change Failure Rate measures the percentage of deployments that cause a failure in production."
}
) })
] });
};
const renderDeploymentFrequencyChart = () => {
if (deploymentFrequency.length === 0)
return null;
return /* @__PURE__ */ jsxs(Paper, { className: classes.chartContainer, children: [
/* @__PURE__ */ jsx(Typography, { variant: "h6", gutterBottom: true, style: { padding: 16 }, children: "Deployment Frequency" }),
/* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: 300, children: /* @__PURE__ */ jsxs(
BarChart,
{
data: deploymentFrequency,
margin: {
top: 5,
right: 30,
left: 20,
bottom: 30
},
children: [
/* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3" }),
/* @__PURE__ */ jsx(XAxis, { dataKey: "period" }),
/* @__PURE__ */ jsx(YAxis, {}),
/* @__PURE__ */ jsx(Tooltip$1, {}),
/* @__PURE__ */ jsx(Legend, {}),
/* @__PURE__ */ jsx(
Bar,
{
dataKey: "deploymentCount",
name: "Deployments",
fill: "#8884d8"
}
)
]
}
) })
] });
};
const renderLeadTimeChart = () => {
if (leadTime.length === 0)
return null;
return /* @__PURE__ */ jsxs(Paper, { className: classes.chartContainer, children: [
/* @__PURE__ */ jsx(Typography, { variant: "h6", gutterBottom: true, style: { padding: 16 }, children: "Lead Time for Changes" }),
/* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: 300, children: /* @__PURE__ */ jsxs(
LineChart,
{
data: leadTime,
margin: {
top: 5,
right: 30,
left: 20,
bottom: 30
},
children: [
/* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3" }),
/* @__PURE__ */ jsx(XAxis, { dataKey: "date" }),
/* @__PURE__ */ jsx(YAxis, {}),
/* @__PURE__ */ jsx(Tooltip$1, {}),
/* @__PURE__ */ jsx(Legend, {}),
/* @__PURE__ */ jsx(
Line,
{
type: "monotone",
dataKey: "leadTime",
name: "Lead Time (days)",
stroke: "#82ca9d",
activeDot: { r: 8 }
}
)
]
}
) })
] });
};
const renderMTTRChart = () => {
if (mttr.length === 0)
return null;
return /* @__PURE__ */ jsxs(Paper, { className: classes.chartContainer, children: [
/* @__PURE__ */ jsx(Typography, { variant: "h6", gutterBottom: true, style: { padding: 16 }, children: "Mean Time to Restore" }),
/* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: 300, children: /* @__PURE__ */ jsxs(
LineChart,
{
data: mttr,
margin: {
top: 5,
right: 30,
left: 20,
bottom: 30
},
children: [
/* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3" }),
/* @__PURE__ */ jsx(XAxis, { dataKey: "date" }),
/* @__PURE__ */ jsx(YAxis, {}),
/* @__PURE__ */ jsx(Tooltip$1, {}),
/* @__PURE__ */ jsx(Legend, {}),
/* @__PURE__ */ jsx(
Line,
{
type: "monotone",
dataKey: "mttr",
name: "Mean Time to Restore (minutes)",
stroke: "#ff7300",
activeDot: { r: 8 }
}
)
]
}
) })
] });
};
const renderFailureRateChart = () => {
if (failureRate.length === 0)
return null;
return /* @__PURE__ */ jsxs(Paper, { className: classes.chartContainer, children: [
/* @__PURE__ */ jsx(Typography, { variant: "h6", gutterBottom: true, style: { padding: 16 }, children: "Change Failure Rate" }),
/* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: 300, children: /* @__PURE__ */ jsxs(
LineChart,
{
data: failureRate.map((item) => ({
...item,
formattedRate: item.rate.toFixed(1)
})),
margin: {
top: 5,
right: 30,
left: 20,
bottom: 30
},
children: [
/* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3" }),
/* @__PURE__ */ jsx(XAxis, { dataKey: "date" }),
/* @__PURE__ */ jsx(YAxis, { unit: "%" }),
/* @__PURE__ */ jsx(Tooltip$1, { formatter: (value, name) => [
name === "formattedRate" ? `${value}%` : value,
name === "formattedRate" ? "Failure Rate" : name
] }),
/* @__PURE__ */ jsx(Legend, {}),
/* @__PURE__ */ jsx(
Line,
{
type: "monotone",
dataKey: "formattedRate",
name: "Failure Rate (%)",
stroke: "#ff0000",
activeDot: { r: 8 }
}
)
]
}
) })
] });
};
return /* @__PURE__ */ jsxs(Page, { themeId: "tool", children: [
/* @__PURE__ */ jsx(Header, { title: "DORA Metrics", subtitle: "DevOps Research and Assessment Metrics", children: /* @__PURE__ */ jsx(Button, { variant: "contained", color: "primary", onClick: handleRefresh, children: "Refresh" }) }),
/* @__PURE__ */ jsxs(Content, { children: [
/* @__PURE__ */ jsx(ContentHeader, { title: "Overview", children: /* @__PURE__ */ jsx(SupportButton, { children: "View DORA metrics extracted from Apache DevLake data. These metrics help teams assess their DevOps performance." }) }),
/* @__PURE__ */ jsxs(Grid, { container: true, className: classes.root, children: [
renderErrorContent(),
apiStatus !== "error" && /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsx("div", { className: classes.filters, children: /* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs(FormControl, { className: classes.formControl, children: [
/* @__PURE__ */ jsx(InputLabel, { id: "project-select-label", children: "Project" }),
/* @__PURE__ */ jsx(
Select,
{
labelId: "project-select-label",
id: "project-select",
value: selectedProject,
onChange: handleProjectChange,
children: projects.map((project) => /* @__PURE__ */ jsx(MenuItem, { value: project, children: project }, project))
}
)
] }),
/* @__PURE__ */ jsxs(FormControl, { className: classes.formControl, children: [
/* @__PURE__ */ jsx(InputLabel, { id: "time-range-select-label", children: "Time Range" }),
/* @__PURE__ */ jsx(
Select,
{
labelId: "time-range-select-label",
id: "time-range-select",
value: timeRange,
onChange: handleTimeRangeChange,
children: timeRangeOptions.map((option) => /* @__PURE__ */ jsx(MenuItem, { value: option.value, children: option.label }, option.value))
}
)
] })
] }) }) }),
loading ? /* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsx(Paper, { className: classes.loading, children: /* @__PURE__ */ jsx(CircularProgress, {}) }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
renderScoreCards(),
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, md: 6, children: renderDeploymentFrequencyChart() }),
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, md: 6, children: renderLeadTimeChart() }),
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, md: 6, children: renderMTTRChart() }),
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, md: 6, children: renderFailureRateChart() })
] })
] })
] })
] })
] });
};
export { DoraMetricsPage };
//# sourceMappingURL=index-c74741df.esm.js.map