UNPKG

@enginuity-io/dora-metrics

Version:
514 lines (509 loc) 20.7 kB
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