import React, { useEffect, useMemo, useState } from "react"; import { Typography, FormControl, InputLabel, Select, MenuItem, ToggleButton, ToggleButtonGroup, Box, } from "@mui/material"; import { AreaChart, AreaSeries, Area, GridlineSeries, Gridline, ChartTooltip, TooltipArea, LinearXAxis, LinearXAxisTickSeries, LinearXAxisTickLabel, LinearYAxis, LinearYAxisTickSeries, LinearYAxisTickLabel, } from "reaviz"; import theme from "../theme.jsx"; // KPI configuration constants const RUN_MINUTES_SAVED_PER_WORKFLOW = 15; // minutes saved per workflow run const RUN_DOLLARS_SAVED_PER_WORKFLOW = 25; // dollars saved per workflow run // Filters for status selection const statusOptions = [ { key: "ALL", label: "All" }, { key: "FINISHED", label: "Success" }, { key: "FAILED", label: "Failed" }, ]; // Response Object (Coming from backend) // { // "key": "app_executions", // "value": 70, // "date": "2025-10-15T19:36:03.928872+05:30" // } // Date formatting helpers // This is used to format the date in the format of YYYY-MM-DD function formatDay(dateInput) { try { const dt = new Date(dateInput); const y = dt.getFullYear(); const m = String(dt.getMonth() + 1).padStart(2, "0"); const d = String(dt.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; // local date to avoid UTC shift dropping "today" } catch { return String(dateInput); } } // This is used to format the date in the format of YYYY-MM function formatMonth(dateInput) { const dt = new Date(dateInput); const month = String(dt.getMonth() + 1).padStart(2, "0"); return `${dt.getFullYear()}-${month}`; } // Aggregate values by day or by month // This is used for area chart toggle button (Daily / Monthly) function bucketSeries(items, resolution) { const map = new Map(); for (const item of items) { const key = resolution === "monthly" ? formatMonth(item.key) : formatDay(item.key); const value = Number(item.data || 0); map.set(key, (map.get(key) || 0) + value); } return map; } // Normalize API entries to a consistent structure // for e.g, {date: "2025-10-15T19:36:03.928872+05:30", value: 70} // will be normalized to {key: "2025-10-15", id: "2025-10-15", data: 70} function normalizeEntries(arr) { return (arr || []).map((d) => ({ key: d?.date ? new Date(d.date) : new Date(), id: d?.date || Math.random().toString(36).slice(2), data: Number(d?.value ?? 0), })); } // Build continuous key sequence from start to end, aligned by resolution (Daily / Monthly) function buildBackfilledKeys(allKeys, resolution) { if (allKeys.length === 0) return allKeys; const start = new Date(allKeys[0]); let end = new Date(allKeys[allKeys.length - 1]); const today = new Date(); if (resolution === "monthly") { const monthToday = new Date(today.getFullYear(), today.getMonth(), 1); if (monthToday > end) end = monthToday; } else { // To ensure that the last day is included in the series const dayToday = new Date( today.getFullYear(), today.getMonth(), today.getDate() ); if (dayToday > end) end = dayToday; } const addKey = (dt) => resolution === "monthly" ? formatMonth(dt) : formatDay(dt); const step = (dt) => { if (resolution === "monthly") { dt.setMonth(dt.getMonth() + 1); dt.setDate(1); } else { dt.setDate(dt.getDate() + 1); } }; const sequence = []; const cursor = new Date(start); if (resolution === "monthly") cursor.setDate(1); while (cursor <= end) { sequence.push(addKey(cursor)); step(cursor); } return sequence; } // Ensure area series has at least two points function ensureMinTwoPoints(arr) { if (arr.length === 1) { return [ { key: 0, data: arr[0].data }, { key: 1, data: arr[0].data }, ]; } return arr; } // Compact number formatter for axis ticks (e.g. 12,000 -> 12k, 12000000 -> 12M, 12000000000 -> 12B) function formatCompactNumber(value) { const n = Number(value) || 0; const abs = Math.abs(n); if (abs >= 1e9) return `${Math.round((n / 1e9) * 10) / 10}B`; if (abs >= 1e6) return `${Math.round((n / 1e6) * 10) / 10}M`; if (abs >= 1e3) return `${Math.round((n / 1e3) * 10) / 10}k`; return `${n}`; } // Compute X axis ticks and label formatter function computeTicks(allKeys, days, resolution) { const maxTicks = 12; const xInterval = Math.max(1, Math.floor(allKeys.length / maxTicks)); let tickValues = Array.from( { length: Math.ceil(allKeys.length / xInterval) }, (_, i) => i * xInterval ); const lastIdx = allKeys.length - 1; if (lastIdx >= 0 && tickValues[tickValues.length - 1] !== lastIdx) { tickValues = [...tickValues, lastIdx]; } // Format the label for the x axis // if resolution is monthly, it will return the date in the format of YYYY-MM // if resolution is daily, it will return the date in the format of YYYY-MM-DD // if days is greater than 90, it will return the date in the format of YYYY-MM-DD // else it will return the date in the format of MM-DD const formatLabel = (idx) => { const key = allKeys[idx]; if (!key) return ""; if (resolution === "monthly") return key; if (days > 90) return key; const parts = key.split("-"); if (parts.length >= 3) return `${parts[1]}-${parts[2]}`; return key; }; return { tickValues, formatLabel }; } // Compute upper bound for Y axis with padding // Just to ensure that the area chart is not touching the top of the chart function computePaddedMax(okArr, failArr) { const rawMaxOk = okArr.reduce((m, p) => Math.max(m, Number(p?.data || 0)), 0); const rawMaxFail = failArr.reduce( (m, p) => Math.max(m, Number(p?.data || 0)), 0 ); return Math.max(1, Math.ceil(Math.max(rawMaxOk, rawMaxFail) * 1.1 + 1)); } // Build grouped series and matching color scheme based on filter selection // This is used to build the grouped series and matching color scheme based on filter selection (Success / Failed / All) function buildGroupedSeries(selectedStatus, okArr, failArr) { let grouped = []; let scheme = []; if (selectedStatus === "ALL") { if (failArr.length > okArr.length) { grouped = [ { key: "Successful Runs", data: okArr }, { key: "Failed Runs", data: failArr }, ]; scheme = ["#ef4444", "#22c55e"]; } else { grouped = [ { key: "Failed Runs", data: failArr }, { key: "Successful Runs", data: okArr }, ]; scheme = ["#22c55e", "#ef4444"]; } } else if (selectedStatus === "FAILED") { grouped = [{ key: "Failed Runs", data: failArr }]; scheme = ["#ef4444"]; } else { grouped = [{ key: "Successful Runs", data: okArr }]; scheme = ["#22c55e"]; } return { grouped, scheme }; } const SuccessFailedRunsWidget = (props) => { const { globalUrl, workflows, onControlsChange, onLoadingChange, onTotalsChange, overrideDays, dummyMode, loadingSelectedOrgStats, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit } = props; const [mode, setMode] = useState("workflows"); // 'workflows' | 'apps' const [days, setDays] = useState(30); const daysOptions = [5, 10, 15, 30, 60, 90, 180, 230, 365]; const [resolution, setResolution] = useState("daily"); // 'daily' | 'monthly' const [selectedWorkflow, setSelectedWorkflow] = useState("ALL"); const [selectedStatus, setSelectedStatus] = useState("ALL"); const [seriesOk, setSeriesOk] = useState([]); const [seriesFail, setSeriesFail] = useState([]); const [loading, setLoading] = useState(false); const [wfTotals, setWfTotals] = useState({ ok: 0, fail: 0, activeDays: 0 }); const appExecutionLimit = useMemo(() => { if (mode !== 'apps') return null; if(orgForLimit?.id !== selectedOrgForStats && selectedOrgForStats !== "ALL") { return null; } try { let org = orgForLimit if(selectedOrgForStats === "ALL") { org = selectedOrganization; } if (!org?.sync_features?.app_executions?.limit) return null; return Number(org.sync_features.app_executions.limit) || null; } catch { return null; } }, [mode, selectedOrganization, orgForLimit, selectedOrgForStats]); useEffect(() => { try { if (typeof onTotalsChange !== "function") return; const totalOk = Math.max(0, Number(wfTotals.ok) || 0); const totalFail = Math.max(0, Number(wfTotals.fail) || 0); const totalRuns = totalOk + totalFail; const activeDays = Math.max(0, Number(wfTotals.activeDays) || 0); const timeSavedMinutes = totalRuns * RUN_MINUTES_SAVED_PER_WORKFLOW; const moneySavedDollars = totalRuns * RUN_DOLLARS_SAVED_PER_WORKFLOW; // Do not trigger parent updates when switching mode to avoid page blink onTotalsChange({ days, totalRuns, successRuns: totalOk, failedRuns: totalFail, activeDays, timeSavedMinutes, moneySavedDollars }); } catch { onTotalsChange({ days, totalRuns: 0, successRuns: 0, failedRuns: 0, activeDays: 0, timeSavedMinutes: 0, moneySavedDollars: 0 }); } }, [wfTotals, days, onTotalsChange]); const workflowItems = useMemo(() => { const base = [{ id: "ALL", name: "All Workflows" }]; if (!Array.isArray(workflows)) return base; return base.concat( workflows .filter((w) => w?.id && w?.name) .map((w) => ({ id: w.id, name: w.name })) ); }, [workflows]); const fetchSeriesForKey = async (key) => { try { // If specific org is selected and pre-fetched stats are available, use them directly if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) { const dailyStats = orgStats.daily_statistics; const processedEntries = []; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); for (const day of dailyStats) { if (!day?.date) continue; const dayDate = new Date(day.date); if (dayDate < cutoff) continue; let value = 0; if (key === 'workflow_executions') { const finished = Number(day?.workflow_executions_finished || 0); const failed = Number(day?.workflow_executions_failed || 0); value = finished + failed; } else { value = Number(day?.[key] || 0); } processedEntries.push({ date: day.date, value }); } // Append today's numbers if missing from daily_statistics, using top-level daily_* fields try { const todayStr = (() => { const d = new Date(); const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; })(); const hasToday = processedEntries.some(e => { const dt = new Date(e.date); const y=dt.getFullYear(); const m=String(dt.getMonth()+1).padStart(2,'0'); const dd=String(dt.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}` === todayStr; }); if (!hasToday) { let todayVal = 0; if (key === 'workflow_executions') { const finished = Number(orgStats?.daily_workflow_executions_finished ?? orgStats?.daily_workflow_executions ?? 0); const failed = Number(orgStats?.daily_workflow_executions_failed ?? 0); // If daily_workflow_executions exists, prefer that; otherwise sum finished + failed todayVal = orgStats?.daily_workflow_executions !== undefined ? Number(orgStats.daily_workflow_executions || 0) : (finished + failed); } else if (key === 'workflow_executions_finished') { todayVal = Number(orgStats?.daily_workflow_executions_finished ?? 0); } else if (key === 'workflow_executions_failed') { todayVal = Number(orgStats?.daily_workflow_executions_failed ?? 0); } else if (key === 'app_executions') { todayVal = Number(orgStats?.daily_app_executions ?? 0); } else if (key === 'app_executions_failed') { todayVal = Number(orgStats?.daily_app_executions_failed ?? 0); } // Only add when we have a numeric value (including 0) if (!Number.isNaN(todayVal)) { processedEntries.push({ date: new Date().toISOString(), value: todayVal }); } } } catch {} return processedEntries; } // If selectedOrgForStats is 'ALL', use parent org's full stats endpoint and combine app_executions + child_app_executions if (selectedOrgForStats === 'ALL' && selectedOrganization?.id) { // Use the full stats endpoint to get daily_statistics const url = `${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrganization.id)}/stats`; const r = await fetch(url, { method: "GET", credentials: "include" }); if (r.ok) { const data = await r.json(); const dailyStats = Array.isArray(data?.daily_statistics) ? data.daily_statistics : []; // Process each day's data, combining parent + child org runs const processedEntries = []; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); for (const day of dailyStats) { if (day?.date) { const dayDate = new Date(day.date); if (dayDate < cutoff) continue; // Filter by days let value = day?.[key] || 0; // Add child org executions based on the key if (key === 'app_executions' && day?.child_app_executions !== undefined) { value += (day.child_app_executions || 0); } else if (key === 'app_executions_failed' && day?.child_app_executions_failed !== undefined) { value += (day.child_app_executions_failed || 0); } else if (key === 'workflow_executions_finished' && day?.child_workflow_executions_finished !== undefined) { value += (day.child_workflow_executions_finished || 0); } else if (key === 'workflow_executions_failed' && day?.child_workflow_executions_failed !== undefined) { value += (day.child_workflow_executions_failed || 0); } else if (key === 'workflow_executions') { // workflow_executions is total (finished + failed), so add both child fields const childFinished = day?.child_workflow_executions_finished || 0; const childFailed = day?.child_workflow_executions_failed || 0; value += (childFinished + childFailed); } processedEntries.push({ date: day.date, value }); } } // Append today's datapoint for ALL by combining parent + child daily_* counters try { const todayStr = (() => { const d = new Date(); const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; })(); const hasToday = processedEntries.some(e => { const dt=new Date(e.date); const y=dt.getFullYear(); const m=String(dt.getMonth()+1).padStart(2,'0'); const dd=String(dt.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`===todayStr; }); if (!hasToday) { let todayVal = 0; if (key === 'app_executions') { todayVal = Number(data?.daily_app_executions ?? 0) + Number(data?.daily_child_app_executions ?? 0); } else if (key === 'app_executions_failed') { todayVal = Number(data?.daily_app_executions_failed ?? 0) + Number(data?.daily_child_app_executions_failed ?? 0); } else if (key === 'workflow_executions_finished') { todayVal = Number(data?.daily_workflow_executions_finished ?? 0) + Number(data?.daily_child_workflow_executions_finished ?? 0); } else if (key === 'workflow_executions_failed') { todayVal = Number(data?.daily_workflow_executions_failed ?? 0) + Number(data?.daily_child_workflow_executions_failed ?? 0); } else if (key === 'workflow_executions') { const p = Number(data?.daily_workflow_executions ?? 0); const cf = Number(data?.daily_child_workflow_executions_finished ?? 0); const cfa = Number(data?.daily_child_workflow_executions_failed ?? 0); todayVal = p > 0 ? p : (Number(data?.daily_workflow_executions_finished ?? 0) + Number(data?.daily_workflow_executions_failed ?? 0)); todayVal += (cf + cfa); } if (!Number.isNaN(todayVal)) { processedEntries.push({ date: new Date().toISOString(), value: todayVal }); } } } catch {} return processedEntries; } } // Fallback to global stats const urlA = `${globalUrl}/api/v1/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`; const doFetch = async (u) => { const r = await fetch(u, { method: "GET", credentials: "include" }); if (!r.ok) return []; const j = await r.json(); return Array.isArray(j?.entries) ? j.entries : []; }; const a = await doFetch(urlA); return a; } catch (e) { return []; } }; const fetchSeries = async () => { setLoading(true); try { // This will only run if dummyMode is true (Onboarding preview) if (dummyMode) { // 10-day wave with a couple of bumps for a more dynamic preview const today = new Date(); const mk = (n, v) => ({ key: new Date(today.getFullYear(), today.getMonth(), today.getDate() - n), id: `${n}`, data: v }); // Success shows two bumps (days -8..-6 and -2..0) const okVals = [7, 4, 9, 4, 6, 9, 7, 5, 8, 6]; // oldest -> newest const failVals = [1, 0, 1, 2, 1, 1, 0, 1, 2, 1]; // small, non-zero noise const okSeries = okVals.map((v, idx) => mk(okVals.length - 1 - idx, v)); const failSeries = failVals.map((v, idx) => mk(failVals.length - 1 - idx, v)); setSeriesOk(okSeries); setSeriesFail(failSeries); return; } const successKey = mode === "workflows" ? "workflow_executions_finished" : "app_executions"; const failedKey = mode === "workflows" ? "workflow_executions_failed" : "app_executions_failed"; const [succ, fail] = await Promise.all([ fetchSeriesForKey(successKey), fetchSeriesForKey(failedKey), ]); let okSeries = normalizeEntries(succ); let failSeries = normalizeEntries(fail); setSeriesOk(okSeries); setSeriesFail(failSeries); } catch (e) { setSeriesOk([]); setSeriesFail([]); } finally { setLoading(false); } }; useEffect(() => { if (loadingSelectedOrgStats === true) { setSeriesOk([]); setSeriesFail([]); return; } fetchSeries(); }, [dummyMode, days, globalUrl, mode, selectedOrgForStats, selectedOrganization, loadingSelectedOrgStats]); // Apply external days override (e.g. after onboarding completes) useEffect(() => { if (typeof overrideDays === 'number' && overrideDays > 0 && overrideDays !== days) { setDays(overrideDays); } }, [overrideDays]); // For the KPIs : Time saved and Money saved useEffect(() => { let aborted = false; const run = async () => { // Skip fetching/storing real stats while onboarding preview is shown // if (dummyMode) { // if (!aborted) setWfTotals({ ok: 0, fail: 0, activeDays: 0 }); // return; // } try { const totalEntries = await fetchSeriesForKey("workflow_executions"); const series = normalizeEntries(totalEntries); const dayKey = (d) => { try { const dt = new Date(d?.date || d?.key); const y = dt.getFullYear(); const m = String(dt.getMonth() + 1).padStart(2, "0"); const day = String(dt.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; // local date string } catch { return null; } }; const dayTotals = new Map(); for (const it of series) { const k = dayKey(it); if (!k) continue; dayTotals.set(k, (dayTotals.get(k) || 0) + (Number(it?.data)||0)); } const activeDays = Array.from(dayTotals.values()).filter(v => v > 0).length; const ok = series.reduce((s, p) => s + (Number(p?.data)||0), 0); if (!aborted) setWfTotals({ ok, fail: 0, activeDays }); } catch { if (!aborted) setWfTotals({ ok: 0, fail: 0, activeDays: 0 }); } }; run(); return () => { aborted = true; }; }, [globalUrl, days, dummyMode, overrideDays, selectedOrgForStats]); // Notify parent about loading state changes useEffect(() => { if (typeof onLoadingChange === "function") { onLoadingChange(loadingSelectedOrgStats || loading); } }, [loading, loadingSelectedOrgStats, onLoadingChange]); // Build filters UI once here; optionally render externally via onControlsChange const controlsNode = React.useMemo(() => (
{/* Workflow */} Filter Last v && setMode(v)} sx={{ height: 37, backgroundColor: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.22)', borderRadius: '30px', padding: '2px', "& .MuiToggleButton-root": { border: "none", borderRadius: "30px", color: "#fff", padding: "6px 16px", textTransform: "none", fontSize: "14px", "&.Mui-selected": { backgroundColor: "#fff", color: "#222", fontWeight: "600", "&:hover": { backgroundColor: "#fff", }, }, "&:hover": { backgroundColor: "rgba(255, 255, 255, 0.2)", }, }, }} > Workflows Apps { if (!v) return; setResolution(v); if (v === 'monthly' && days !== 180) { setDays(180); } else if (v === 'daily' && days !== 30) { setDays(30); } }} sx={{ height: 37, backgroundColor: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.22)', borderRadius: '30px', padding: '2px', "& .MuiToggleButton-root": { border: "none", borderRadius: "30px", color: "#fff", padding: "6px 16px", textTransform: "none", fontSize: "14px", "&.Mui-selected": { backgroundColor: "#fff", color: "#222", fontWeight: "600", "&:hover": { backgroundColor: "#fff", }, }, "&:hover": { backgroundColor: "rgba(255, 255, 255, 0.2)", }, }, }} > Daily Monthly
), [selectedStatus, mode, days, resolution, workflowItems] ); useEffect(() => { if (typeof onControlsChange === "function") { onControlsChange(controlsNode); return () => { onControlsChange(null); }; } }, [onControlsChange, controlsNode]); var showParentLimit = false; var showSuborgLimit = false; return (
{/* Top controls row: title left, all filters on the right */}
{/* Title moved inside the area chart card */} {!onControlsChange && ( <> {controlsNode} )}
{/* Content row: area chart (left) + ring gauges (right) in separate sub-cards */}
Successful vs Failed Runs ({mode === "workflows" ? "Workflows" : "Apps"}) {loadingSelectedOrgStats === true ? (
Loading…
) : (() => { // Build unified timeline by day or by month const useOk = Array.isArray(seriesOk) ? seriesOk : []; const useFail = Array.isArray(seriesFail) ? seriesFail : []; const okMap = bucketSeries(useOk, resolution); const failMap = bucketSeries(useFail, resolution); let allKeys = Array.from( new Set([...okMap.keys(), ...failMap.keys()]) ); allKeys.sort((a, b) => new Date(a) - new Date(b)); allKeys = buildBackfilledKeys(allKeys, resolution); if (allKeys.length === 0) { return (
No data available
); } let okArr = allKeys.map((k, i) => ({ key: i, data: okMap.get(k) || 0, })); let failArr = allKeys.map((k, i) => ({ key: i, data: failMap.get(k) || 0, })); okArr = ensureMinTwoPoints(okArr); failArr = ensureMinTwoPoints(failArr); const { grouped, scheme } = buildGroupedSeries( selectedStatus, okArr, failArr ); const { tickValues, formatLabel } = computeTicks( allKeys, days, resolution ); const paddedMax = computePaddedMax(okArr, failArr); // Extract parent and suborg app execution limits let parentAppExecutionLimit = null; let suborgAppExecutionLimit = null; if (mode === 'apps') { parentAppExecutionLimit = selectedOrganization?.sync_features?.app_executions?.limit ? Number(selectedOrganization.sync_features.app_executions.limit) : null; suborgAppExecutionLimit = orgForLimit?.sync_features?.app_executions?.limit ? Number(orgForLimit.sync_features.app_executions.limit) : null; console.log("Mode appExecutionLimit found in sub orgs parentAppExecutionLimit", parentAppExecutionLimit); console.log("Mode appExecutionLimit found in sub orgs suborgAppExecutionLimit", suborgAppExecutionLimit); } if ( typeof appExecutionLimit === 'number' && appExecutionLimit > 0 && appExecutionLimit <= paddedMax && selectedOrgForStats !== "ALL" ) { showSuborgLimit = true; } return (
formatCompactNumber(d)} />} /> } /> } xAxis={ formatLabel(Number(d))} /> } tickValues={tickValues} /> } /> } gridlines={} />} series={ } colorScheme={scheme} tooltip={ { const idx = Math.max(0, Number(d?.x ?? 0)); const rows = (grouped || []).map( (seriesItem, i) => { const point = Array.isArray(seriesItem?.data) ? seriesItem.data[ Math.min( idx, seriesItem.data.length - 1 ) ] : null; const value = Number(point?.data || 0); return { label: seriesItem?.key, value, color: scheme[scheme.length - 1 - i], }; } ); return (
{formatLabel(idx)}
{rows.reverse().map((r) => (
{r.label} {r.value}
))}
); }} /> } /> } /> } /> {/* Custom reference line overlay - only show if limit is within visible range */} {appExecutionLimit && mode === 'apps' && (() => { const maxValue = paddedMax || 1; const limitPercentage = appExecutionLimit / maxValue; const chartTopPadding = 1; const chartBottomPadding = 8; const usableHeight = 100 - chartTopPadding - chartBottomPadding; const topPercentage = chartTopPadding + ((1 - limitPercentage) * usableHeight); let parentLimitPos = null; if ( typeof parentAppExecutionLimit === 'number' && parentAppExecutionLimit > 0 && parentAppExecutionLimit !== appExecutionLimit && parentAppExecutionLimit <= paddedMax ) { showParentLimit = true; const parentLimitPerc = parentAppExecutionLimit / paddedMax; parentLimitPos = chartTopPadding + ((1 - parentLimitPerc) * usableHeight); } if (selectedOrgForStats === "ALL") { showParentLimit = true; const parentLimitPerc = parentAppExecutionLimit / paddedMax; parentLimitPos = chartTopPadding + ((1 - parentLimitPerc) * usableHeight); } return ( <> {/* Suborg or Current Limit Reference Line & Label (orange now) */} { selectedOrgForStats !== "ALL" && showSuborgLimit && ( <>
{formatCompactNumber(appExecutionLimit)} limit
) } {/* Parent Org Limit Reference Line & Label (red now) */} {showParentLimit && ( <>
{formatCompactNumber(parentAppExecutionLimit)} limit
)} ); })()}
); })()}
Successful Runs
Failed Runs
{/* Chart limit legends */} {showParentLimit && (
Parent Org Limit
)} {showSuborgLimit && (
Current Org Limit
)}
X: {resolution === "monthly" ? "Date (month)" : "Date (MM-DD)"} |Y: Runs
{/* Ring gauges */}
{mode === "workflows" ? "Workflow" : "App"} Success Rates
{(() => { const totalOk = (seriesOk || []).reduce( (s, p) => s + (p?.data || 0), 0 ); const totalFail = (seriesFail || []).reduce( (s, p) => s + (p?.data || 0), 0 ); const total = totalOk + totalFail; const okPct = total > 0 ? Math.round((totalOk / total) * 100) : 0; const failPct = total > 0 ? Math.round((totalFail / total) * 100) : 0; return ( <> ); })()}
); }; export default SuccessFailedRunsWidget; // Lightweight SVG ring to avoid RadialGauge runtime issues function Ring({ title, color, bg, percent }) { const stroke = 9; const r = 60; const c = 2 * Math.PI * r; const filled = (Math.max(0, Math.min(100, Number(percent) || 0)) / 100) * c; return (
{Math.round(Math.max(0, Math.min(100, Number(percent) || 0)))}% {title}
); } // (Old) custom area/line removed in favor of Reaviz AreaChart grouped function LegendDot({ color }) { return ( ); } function LegendLineDash({ color, height = 10 }) { // Render a small thinned dashed SVG line return ( ); }