Tons of minor fixes to the based on cloud tests

This commit is contained in:
Frikky
2025-11-12 11:39:18 +01:00
parent e470dbef7f
commit 822e7c1eb3
36 changed files with 3353 additions and 1882 deletions
@@ -24,7 +24,7 @@ import {
LinearYAxisTickSeries,
LinearYAxisTickLabel,
} from "reaviz";
import theme from "../theme";
import theme from "../theme.jsx";
// KPI configuration constants
const RUN_MINUTES_SAVED_PER_WORKFLOW = 15; // minutes saved per workflow run
@@ -48,7 +48,11 @@ const statusOptions = [
// This is used to format the date in the format of YYYY-MM-DD
function formatDay(dateInput) {
try {
return new Date(dateInput).toISOString().slice(0, 10);
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);
}
@@ -221,8 +225,8 @@ function buildGroupedSeries(selectedStatus, okArr, failArr) {
return { grouped, scheme };
}
const SuccessFailedRunsWidget = (props) => {
const { globalUrl, workflows, onControlsChange, onLoadingChange, onTotalsChange, overrideDays, dummyMode } = props;
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);
@@ -236,6 +240,25 @@ const SuccessFailedRunsWidget = (props) => {
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;
@@ -264,7 +287,132 @@ const SuccessFailedRunsWidget = (props) => {
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" });
@@ -272,25 +420,17 @@ const SuccessFailedRunsWidget = (props) => {
const j = await r.json();
return Array.isArray(j?.entries) ? j.entries : [];
};
const a = await doFetch(urlA);
if (a.length > 0) return a;
// Optional orgId fallback if exposed globally in app
// const orgId = (window && window.selectedOrganization && window.selectedOrganization.id) || null;
// if (orgId) {
// const urlB = `${globalUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
// const b = await doFetch(urlB);
// if (b.length > 0) return b;
// }
// return [];
} catch (e) {
return [];
}
};
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();
@@ -324,47 +464,6 @@ const SuccessFailedRunsWidget = (props) => {
let okSeries = normalizeEntries(succ);
let failSeries = normalizeEntries(fail);
// Fallback: if empty, derive from /api/v1/stats daily_statistics
if (okSeries.length === 0 && failSeries.length === 0) {
const resp = await fetch(`${globalUrl}/api/v1/stats`, {
method: "GET",
credentials: "include",
headers: { "Content-Type": "application/json" },
});
if (resp.ok) {
const data = await resp.json();
const fieldOk =
mode === "workflows"
? "workflow_executions_finished"
: "app_executions";
const fieldFail =
mode === "workflows"
? "workflow_executions_failed"
: "app_executions_failed";
const list = Array.isArray(data?.daily_statistics)
? data.daily_statistics
: [];
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
okSeries = list
.filter(Boolean)
.map((d) => ({
key: new Date(d?.date || Date.now()),
id: d?.date || Math.random().toString(36).slice(2),
data: Number(d?.[fieldOk] || 0),
}))
.filter((p) => p.key >= cutoff);
failSeries = list
.filter(Boolean)
.map((d) => ({
key: new Date(d?.date || Date.now()),
id: `f-${d?.date || Math.random().toString(36).slice(2)}`,
data: Number(d?.[fieldFail] || 0),
}))
.filter((p) => p.key >= cutoff);
}
}
setSeriesOk(okSeries);
setSeriesFail(failSeries);
} catch (e) {
@@ -375,16 +474,15 @@ const SuccessFailedRunsWidget = (props) => {
}
};
// Only fetch on first render and when days window or mode changes
const firstLoadRef = React.useRef(true);
useEffect(() => {
if (firstLoadRef.current) {
firstLoadRef.current = false;
if (loadingSelectedOrgStats === true) {
setSeriesOk([]);
setSeriesFail([]);
return;
}
fetchSeries();
return;
}
fetchSeries();
}, [dummyMode, days, globalUrl, mode]);
}, [dummyMode, days, globalUrl, mode, selectedOrgForStats, selectedOrganization, loadingSelectedOrgStats]);
// Apply external days override (e.g. after onboarding completes)
useEffect(() => {
@@ -406,7 +504,13 @@ const SuccessFailedRunsWidget = (props) => {
const totalEntries = await fetchSeriesForKey("workflow_executions");
const series = normalizeEntries(totalEntries);
const dayKey = (d) => {
try { return new Date(d?.date || d?.key).toISOString().slice(0,10); } catch { return null; }
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) {
@@ -421,14 +525,14 @@ const SuccessFailedRunsWidget = (props) => {
};
run();
return () => { aborted = true; };
}, [globalUrl, days, dummyMode, overrideDays]);
}, [globalUrl, days, dummyMode, overrideDays, selectedOrgForStats]);
// Notify parent about loading state changes
useEffect(() => {
if (typeof onLoadingChange === "function") {
onLoadingChange(loading);
onLoadingChange(loadingSelectedOrgStats || loading);
}
}, [loading, onLoadingChange]);
}, [loading, loadingSelectedOrgStats, onLoadingChange]);
// Build filters UI once here; optionally render externally via onControlsChange
const controlsNode = React.useMemo(() => (
@@ -600,6 +704,9 @@ const SuccessFailedRunsWidget = (props) => {
}
}, [onControlsChange, controlsNode]);
var showParentLimit = false;
var showSuborgLimit = false;
return (
<div style={{ width: "100%", display: "flex", flexDirection: "column" }}>
{/* Top controls row: title left, all filters on the right */}
@@ -637,7 +744,11 @@ const SuccessFailedRunsWidget = (props) => {
}}
>
<Typography style={{ fontWeight: 500, marginBottom: 22, fontSize: 18 }}>Successful vs Failed Runs ({mode === "workflows" ? "Workflows" : "Apps"})</Typography>
{(() => {
{loadingSelectedOrgStats === true ? (
<div style={{ height: 300, display: "flex", alignItems: "center", justifyContent: "center" }}>
<Typography variant="body2" color="textSecondary">Loading</Typography>
</div>
) : (() => {
// Build unified timeline by day or by month
const useOk = Array.isArray(seriesOk) ? seriesOk : [];
const useFail = Array.isArray(seriesFail) ? seriesFail : [];
@@ -690,39 +801,60 @@ const SuccessFailedRunsWidget = (props) => {
);
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 (
<AreaChart
height={300}
width={"100%"}
data={grouped}
yAxis={
<LinearYAxis
domain={[0, paddedMax]}
tickSeries={
<LinearYAxisTickSeries
label={<LinearYAxisTickLabel format={(d) => formatCompactNumber(d)} />}
/>
}
/>
}
xAxis={
<LinearXAxis
tickSeries={
<LinearXAxisTickSeries
tickSize={0}
label={
<LinearXAxisTickLabel
padding={8}
format={(d) => formatLabel(Number(d))}
/>
}
tickValues={tickValues}
/>
}
/>
}
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
series={
<div style={{ position: 'relative', width: '100%', height: 300 }}>
<AreaChart
height={300}
width={"100%"}
data={grouped}
yAxis={
<LinearYAxis
domain={[0, paddedMax]}
tickSeries={
<LinearYAxisTickSeries
label={<LinearYAxisTickLabel format={(d) => formatCompactNumber(d)} />}
/>
}
/>
}
xAxis={
<LinearXAxis
tickSeries={
<LinearXAxisTickSeries
tickSize={0}
label={
<LinearXAxisTickLabel
padding={8}
format={(d) => formatLabel(Number(d))}
/>
}
tickValues={tickValues}
/>
}
/>
}
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
series={
<AreaSeries
type="grouped"
interpolation={"smooth"}
@@ -822,6 +954,119 @@ const SuccessFailedRunsWidget = (props) => {
/>
}
/>
{/* 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 && (
<>
<div
style={{
position: 'absolute',
top: `${topPercentage}%`,
left: '3.9%',
right: '0%',
height: '2px',
backgroundImage: 'repeating-linear-gradient(to right, #ff8544 0px, #ff8544 8px, transparent 8px, transparent 16px)',
pointerEvents: 'none',
zIndex: 10,
}}
/>
<div
style={{
position: 'absolute',
top: `${topPercentage}%`,
right: '-8%',
transform: 'translate(0, -50%)',
pointerEvents: 'none',
zIndex: 10,
backgroundColor: 'rgba(26, 26, 26, 0.95)',
padding: '2px 8px',
borderRadius: '4px',
border: '1px solid #ff8544',
}}
>
<Typography
style={{ fontSize: '12px', fontWeight: 600, color: '#ff8544' }}
>
{formatCompactNumber(appExecutionLimit)} limit
</Typography>
</div>
</>
)
}
{/* Parent Org Limit Reference Line & Label (red now) */}
{showParentLimit && (
<>
<div
style={{
position: 'absolute',
top: `${parentLimitPos}%`,
left: '3.9%',
right: '0%',
height: '2px',
backgroundImage: 'repeating-linear-gradient(to right, #FD4C62 0px, #FD4C62 12px, transparent 12px, transparent 22px)',
pointerEvents: 'none',
zIndex: 9,
opacity: 0.8,
}}
/>
<div
style={{
position: 'absolute',
top: `${parentLimitPos}%`,
right: '-8%',
transform: 'translate(0, -50%)',
pointerEvents: 'none',
zIndex: 9,
backgroundColor: 'rgba(26, 26, 26, 0.93)',
padding: '2px 8px',
borderRadius: '4px',
border: '1px solid #FD4C62',
display: 'flex', alignItems: 'center', gap: 6
}}
>
<Typography
style={{ fontSize: '12px', fontWeight: 600, color: '#FD4C62' }}
>
{formatCompactNumber(parentAppExecutionLimit)} limit
</Typography>
</div>
</>
)}
</>
);
})()}
</div>
);
})()}
<div
@@ -844,8 +1089,19 @@ const SuccessFailedRunsWidget = (props) => {
<LegendDot color="#22c55e" /> Successful Runs
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<LegendDot color="#ef4444" /> Failed Runs
<LegendDot color="#FD4C62" /> Failed Runs
</div>
{/* Chart limit legends */}
{showParentLimit && (
<div style={{ display: "flex", alignItems: "center", gap: 4, marginLeft: 18 }}>
<LegendLineDash color="#FD4C62" height="10" /> <span>Parent Org Limit</span>
</div>
)}
{showSuborgLimit && (
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
<LegendLineDash color="#ff8544" height="10" /> <span>Current Org Limit</span>
</div>
)}
</div>
<div
style={{
@@ -999,3 +1255,18 @@ function LegendDot({ color }) {
/>
);
}
function LegendLineDash({ color, height = 10 }) {
// Render a small thinned dashed SVG line
return (
<svg width="24" height={height} style={{ margin: '0 2px' }}>
<line
x1="2" x2="32" y1={height / 2} y2={height / 2}
stroke={color}
strokeWidth="2"
strokeDasharray="8, 5"
strokeLinecap="round"
/>
</svg>
);
}