Tons of minor fixes to the based on cloud tests
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user