Files
shuffle-cracked/frontend/src/components/RunsOverTimeWidget.jsx
T
2025-12-19 18:05:04 +05:30

784 lines
29 KiB
React

import React, { useEffect, useMemo, useState } from 'react';
import { makeStyles, } from "@mui/styles";
import {
Typography,
ToggleButton,
ToggleButtonGroup,
Select,
MenuItem,
FormControl,
InputLabel,
Box,
Autocomplete,
TextField,
Tooltip,
IconButton,
} from '@mui/material';
import { BarChart, BarSeries, Bar, GridlineSeries, Gridline, TooltipArea, ChartTooltip, LinearYAxis, LinearYAxisTickSeries, LinearYAxisTickLabel } from 'reaviz';
import theme from '../theme.jsx';
import {green, yellow, red} from "../views/AngularWorkflow.jsx"
import {
OpenInNew as OpenInNewIcon,
} from '@mui/icons-material';
// Compact number formatter for axis ticks (e.g. 12,000 -> 12k, 12,000,000 -> 12M)
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}`;
}
// Helpers to keep date and "today" logic concise and consistent
function toYMD(date) {
const d = new Date(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}`;
}
function entriesContainDateYMD(entries, date) {
const target = toYMD(date);
return Array.isArray(entries) && entries.some((e) => toYMD(e.date) === target);
}
function computeTodayValueForAll(key, data) {
if (key === 'app_executions') {
return Number(data?.daily_app_executions ?? 0) + Number(data?.daily_child_app_executions ?? 0);
}
if (key === 'workflow_executions') {
const parent = Number(data?.daily_workflow_executions ?? 0);
const parentFinished = Number(data?.daily_workflow_executions_finished ?? 0);
const parentFailed = Number(data?.daily_workflow_executions_failed ?? 0);
const childFinished = Number(data?.daily_child_workflow_executions_finished ?? 0);
const childFailed = Number(data?.daily_child_workflow_executions_failed ?? 0);
const parentVal = parent > 0 ? parent : (parentFinished + parentFailed);
return parentVal + childFinished + childFailed;
}
return 0;
}
function computeTodayValueForOrg(key, orgStats) {
if (key === 'workflow_executions') {
const total = orgStats?.daily_workflow_executions;
const finished = Number(orgStats?.daily_workflow_executions_finished ?? 0);
const failed = Number(orgStats?.daily_workflow_executions_failed ?? 0);
return total !== undefined ? Number(total || 0) : (finished + failed);
}
if (key === 'app_executions') {
return Number(orgStats?.daily_app_executions ?? 0);
}
return 0;
}
const RunsOverTimeWidget = (props) => {
const { globalUrl, onLoadingChange, monthOverride, dummyMode, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit, loadingSelectedOrgStats } = props;
const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows'
const [selectedStatType, setSelectedStatType] = useState({
"value": 'total_workflow_executions',
"label": 'Workflows',
"amount": 0,
})
const [series, setSeries] = useState([]);
const [days, setDays] = useState(365); // aggregate to last 12 months by default
const [selectedMonth, setSelectedMonth] = useState(null); // Date representing first day of target month, or null for yearly view
const [statTypeOptions, setStatTypeOptions] = useState([])
const [loading, setLoading] = useState(false);
const useStyles = makeStyles({
notchedOutline: {
borderColor: "#FF8544 !important",
},
root: {
"& .MuiAutocomplete-listbox": {
border: "2px solid #FF8544",
color: theme.palette.text.primary,
fontSize: 18,
"& li:nth-child(even)": {
backgroundColor: "#CCC",
},
"& li:nth-child(odd)": {
backgroundColor: "#FFF",
},
},
},
inputRoot: {
color: theme.palette.text.primary,
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "#f86a3e",
},
},
});
const classes = useStyles();
// Helper: fetch time series for a specific statistics key
const fetchSeriesForKey = async (key) => {
if (statTypeOptions?.length < 3) {
var foundkeys = {}
for (var key in orgStats) {
if (!key.startsWith("total_")) {
continue
}
// Remove total
foundkeys[key] = orgStats[key]
}
for (var foundKey in orgStats?.daily_statistics) {
const dailyStats = orgStats?.daily_statistics[foundKey]
for (var additionKey in dailyStats?.additions) {
const addition = dailyStats?.additions[additionKey];
if (foundkeys[addition?.key] === undefined) {
foundkeys[addition?.key] = addition?.value
} else {
foundkeys[addition?.key] += addition?.value
}
}
}
var newarray = []
for (var key in foundkeys) {
const foundkey = key
var parsedname = key
if (parsedname?.startsWith("total_")) {
parsedname = parsedname.replace("total_", "")
}
if (parsedname?.startsWith("categorylabel")) {
parsedname = parsedname.replace("categorylabel", "")
}
parsedname = (parsedname.charAt(0).toUpperCase() + parsedname.substring(1)).replaceAll("_", " ")
newarray.push({
"value": key,
"label": parsedname,
"amount": foundkeys[key],
})
}
if (newarray.length > 0) {
setStatTypeOptions(newarray)
}
}
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);
if (key.startsWith("total_")) {
key = key.replace("total_", "")
}
for (const day of dailyStats) {
if (!day?.date) continue;
const dayDate = new Date(day.date);
if (dayDate < cutoff) continue;
let value = 0;
if (day[key] === undefined) {
for (var additionKey in day?.additions) {
const addition = day?.additions[additionKey];
if (addition?.key === key) {
value += Number(addition?.value || 0);
break
}
}
} else {
value = Number(day[key] || 0);
}
processedEntries.push({ date: day.date, value });
}
// Append today's values if not present
try {
if (!entriesContainDateYMD(processedEntries, new Date())) {
const todayVal = computeTodayValueForOrg(key, orgStats);
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', headers: { 'Content-Type': 'application/json' } });
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 === 'workflow_executions') {
// For workflow_executions, sum both child_workflow_executions_finished and child_workflow_executions_failed
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 {
if (!entriesContainDateYMD(processedEntries, new Date())) {
const todayVal = computeTodayValueForAll(key, data);
if (!Number.isNaN(todayVal)) {
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
}
}
} catch {}
return processedEntries;
}
}
// // If a specific org is selected
// if (selectedOrgForStats && selectedOrgForStats !== 'ALL') {
// const url = `${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrgForStats)}/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
// const r = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
// if (r.ok) {
// const j = await r.json();
// return Array.isArray(j?.entries) ? j.entries : [];
// }
// }
// 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', headers: { 'Content-Type': 'application/json' } });
if (!r.ok) return [];
const j = await r.json();
return Array.isArray(j?.entries) ? j.entries : [];
};
const a = await doFetch(urlA);
if (a.length > 0) return a;
// // Final fallback: old aggregate endpoint returning daily_statistics
// const fallback = await fetch(`${globalUrl}/api/v1/stats`, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
// if (fallback.ok) {
// const data = await fallback.json();
// const daily = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
// const valField = key;
// return daily.map((d) => ({ date: d?.date, value: Number(d?.[valField] || 0) }));
// }
// return [];
} catch (e) {
return [];
}
};
// Load and transform into monthly aggregation for last 12 months
const loadStats = async (curMode, inputType) => {
setLoading(true);
try {
// Clear current series immediately to avoid any visual overlap while switching views
setSeries([]);
if (dummyMode) {
// Bring back the older dummy series with emphasis on earlier months
const now = new Date();
const months = [];
for (let i = 11; i >= 0; i--) {
const dt = new Date(now.getFullYear(), now.getMonth() - i, 1);
months.push(new Date(dt.getFullYear(), dt.getMonth(), 1));
}
const base = [20, 18, 22, 24, 23, 21, 15, 12, 9, 15, 24, 20];
const dummy = months.map((m, idx) => ({ key: m, id: `${m.getFullYear()}-${m.getMonth()}`, data: base[idx] }));
setSeries(dummy);
return;
}
const key = inputType !== undefined ? inputType : selectedStatType?.value?.length > 0 ? selectedStatType?.value : curMode === 'apps' ? 'app_executions' : 'workflow_executions';
const entries = await fetchSeriesForKey(key);
// Normalize variants: {Date, Value} or {date, value}
const normalized = (entries || []).map((d) => ({
date: d?.Date ? new Date(d.Date) : (d?.date ? new Date(d.date) : new Date()),
value: Number(d?.Value ?? d?.value ?? 0),
}));
// If a month is selected, show DAILY bars for that month
if (selectedMonth instanceof Date) {
const year = selectedMonth.getFullYear();
const month = selectedMonth.getMonth();
// Build all days for selected month
const firstDay = new Date(year, month, 1);
const nextMonthFirst = new Date(year, month + 1, 1);
const numDays = Math.round((nextMonthFirst - firstDay) / (1000 * 60 * 60 * 24));
// Sum values per day (normalize time to midnight)
const byDayKey = (d) => `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
const sumPerDay = new Map();
normalized.forEach((p) => {
if (!p?.date || Number.isNaN(p.value)) return;
const d = p.date;
if (d.getFullYear() !== year || d.getMonth() !== month) return;
const dayKey = byDayKey(new Date(d.getFullYear(), d.getMonth(), d.getDate()));
sumPerDay.set(dayKey, (sumPerDay.get(dayKey) || 0) + p.value);
});
const dailySeries = Array.from({ length: numDays }, (_, i) => {
const d = new Date(year, month, i + 1);
const k = byDayKey(d);
const v = sumPerDay.get(k) || 0;
return { key: d, id: k, data: v };
});
// Ensure consistent ordering
setSeries(dailySeries.sort((a, b) => a.key - b.key));
return;
}
// Otherwise, show MONTHLY aggregation for last 12 months including current month
const now = new Date();
const months = [];
for (let i = 11; i >= 0; i--) {
const dt = new Date(now.getFullYear(), now.getMonth() - i, 1);
months.push({ y: dt.getFullYear(), m: dt.getMonth(), key: new Date(dt.getFullYear(), dt.getMonth(), 1) });
}
const byMonthKey = (d) => `${d.getFullYear()}-${d.getMonth()}`;
const sumPerMonth = new Map();
normalized.forEach((p) => {
if (!p?.date || Number.isNaN(p.value)) return;
const k = byMonthKey(new Date(p.date.getFullYear(), p.date.getMonth(), 1));
sumPerMonth.set(k, (sumPerMonth.get(k) || 0) + p.value);
});
const monthlySeries = months.map((mm) => {
const k = `${mm.y}-${mm.m}`;
const v = sumPerMonth.get(k) || 0;
return { key: mm.key, id: k, data: v };
});
setSeries(monthlySeries);
} finally {
setLoading(false);
}
};
useEffect(() => {
setTimeout(() => {
const starterWidgetStatType = localStorage.getItem("runsOverTimeWidgetStatType")
if (starterWidgetStatType) {
setSelectedStatType({
"value": starterWidgetStatType,
"label": (starterWidgetStatType.charAt(0).toUpperCase() + starterWidgetStatType.substring(1)).replaceAll("_", " "),
"amount": 0,
})
loadStats(mode, starterWidgetStatType)
}
}, 1500)
}, [])
// Apply month override (e.g. onboarding Explore Now) - consolidated with main load effect
useEffect(() => {
if (monthOverride instanceof Date) {
// Clear series immediately to prevent visual overlap
setSeries([]);
setSelectedMonth(new Date(monthOverride.getFullYear(), monthOverride.getMonth(), 1));
setDays(370);
}
}, [monthOverride]);
useEffect(() => {
if (loadingSelectedOrgStats === true) {
setSeries([]);
return;
}
loadStats(mode);
}, [selectedStatType, mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]);
// Notify parent on loading changes
useEffect(() => {
if (typeof onLoadingChange === 'function') {
onLoadingChange(loading);
}
}, [loading, onLoadingChange]);
const barData = useMemo(() => (
(Array.isArray(series) ? series : []).map((d) => {
const dt = new Date(d.key);
const label = selectedMonth instanceof Date
? `${dt.toLocaleString('default', { month: 'short' })} ${dt.getDate()}` // e.g., 'Oct 1'
: dt.toLocaleString('default', { month: 'short' });
return { key: label, data: Number(d?.data || 0) };
})
), [series, mode, selectedMonth]);
// Build month dropdown options for last 12 months
const monthOptions = useMemo(() => {
const now = new Date();
const opts = [];
for (let i = 0; i < 12; i++) {
const dt = new Date(now.getFullYear(), now.getMonth() - i, 1);
opts.push(dt);
}
return opts;
}, []);
const tooltip = <TooltipArea
tooltip={
<ChartTooltip
placement={"top"}
followCursor={true}
content={(data) => (
<div style={{
borderRadius: 4,
backgroundColor: "#1A1A1A",
border: "1px solid rgba(255,255,255,0.22)",
color: theme.palette.text.primary,
padding: 10,
maxWidth: 240,
pointerEvents: 'none',
}}>
<Typography variant="body2" style={{fontWeight: 600, color: "#ffffff"}}>{data?.x ?? ''}</Typography>
<Typography variant="body2" style={{color: "#ffffff"}}>{data?.y ?? ''}</Typography>
</div>
)}
/>
}
/>;
const barColorscheme = [
"#f85a3e", // anchor orange
"#ff7a57", // brighter, more playful
"#e14b2e", // slightly darker + redder
"#ff9b6b", // soft peachy highlight
"#c83f24", // deep burnt orange
]
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography sx={{ fontSize: 18, fontWeight: 500, fontFamily: theme.typography.fontFamily, paddingLeft: 1 }}>{selectedStatType?.label} ({selectedStatType?.amount})</Typography>
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', justifyContent: 'center' }}>
<Tooltip title="Learn about custom stats" arrow>
<a href="/docs/API#count-stats-for-custom-key" target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }}>
<IconButton>
<OpenInNewIcon style={{ color: theme.palette.text.secondary, }} />
</IconButton>
</a>
</Tooltip>
<FormControl size="small" variant="outlined" style={{ minWidth: 350, }} sx={{
'& .MuiInputBase-root': {
height: 40,
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: '20px',
},
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.22)' },
}}>
<Autocomplete
labelId="data-type-choice"
label="Select Datatype"
autoHighlight
value={selectedStatType}
classes={{ inputRoot: classes.inputRoot }}
ListboxProps={{
style: {
backgroundColor: theme.palette.inputColor,
color: theme.palette.text.primary,
},
}}
sx={{
'& .MuiOutlinedInput-root': {
height: 40, // Adjust the input height
},
'& .MuiAutocomplete-input': {
padding: '8px', // Adjust the text padding
},
}}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={(option) => {
if (
option === undefined ||
option === null ||
option.label === undefined ||
option.label === null
) {
if (option.value !== undefined && option.value !== null) {
return option.value
} else {
return option
}
}
const newname = (
option.label.charAt(0).toUpperCase() + option.label.substring(1)
).replaceAll("_", " ");
return newname;
}}
options={statTypeOptions}
fullWidth
style={{
backgroundColor: theme.palette?.inputColor,
borderRadius: theme.palette?.borderRadius,
}}
onChange={(event, newValue) => {
console.log("CHANGE: ", newValue)
}}
renderOption={(props, data, state) => {
// Format to thousand or million
const formattedamount = formatCompactNumber(data?.amount)
const numbercolor = data?.amount >= 1000000 ? red : (data?.amount >= 100000 ? yellow : green)
return (
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
}}
value={data}
onClick={() => {
setSelectedStatType(data)
// Set local storage for the key
if (data?.value) {
localStorage.setItem("runsOverTimeWidgetStatType", data?.value)
}
}}
>
<span style={{minWidth: 60, maxWidth: 60, color: numbercolor, }}>{formattedamount}</span> {data?.label}
</MenuItem>
)
}}
renderInput={(params) => {
return (
<div style={{ display: "flex", }}>
<TextField
style={theme.palette.textFieldStyle}
{...params}
label="Find your Stat"
variant="outlined"
/>
</div>
)
}}
/>
</FormControl>
<FormControl size="small" variant="outlined" style={{ minWidth: 170 }} sx={{
'& .MuiInputBase-root': {
height: 40,
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: '20px',
},
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.22)' },
}}>
<InputLabel id="runs-month-select-label">View Month</InputLabel>
<Select
labelId="runs-month-select-label"
label="View Month"
value={selectedMonth ? selectedMonth.getTime() : 'ALL'}
onChange={(e) => {
const v = e.target.value;
if (v === 'ALL') {
setSelectedMonth(null);
setDays(365);
} else {
const dt = new Date(Number(v));
setSelectedMonth(new Date(dt.getFullYear(), dt.getMonth(), 1));
setDays(370); // ensure enough historical range; backend param only hints
}
}}
displayEmpty
>
<MenuItem value={'ALL'}><em>All months</em></MenuItem>
{monthOptions.map((dt) => (
<MenuItem key={dt.getTime()} value={dt.getTime()}>
{dt.toLocaleString('default', { month: 'long', year: 'numeric' })}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
</div>
<div style={{ marginTop: 24 }}>
<div style={{ width: '100%', position: 'relative' }}>
{loadingSelectedOrgStats === true ? (
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="body2" color="textSecondary">Loading</Typography>
</div>
) : (
<BarChart
key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`}
height={300}
data={barData}
series={<BarSeries colorScheme={barColorscheme} tooltip={tooltip} bar={<Bar rounded={true} />} />}
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
yAxis={
<LinearYAxis
tickSeries={
<LinearYAxisTickSeries
label={
<LinearYAxisTickLabel format={(d) => formatCompactNumber(d)} />
}
/>
}
/>
}
animated={false}
/>
)}
{/* Custom reference line overlays for app execution limits (suborg and parent) */}
{mode === 'apps' && barData.length > 0 && (() => {
// Determine limits
const parentAppExecutionLimit = selectedOrganization?.sync_features?.app_executions?.limit ? Number(selectedOrganization.sync_features.app_executions.limit) : null;
const suborgAppExecutionLimit = orgForLimit?.sync_features?.app_executions?.limit ? Number(orgForLimit.sync_features.app_executions.limit) : null;
const maxValue = Math.max(...barData.map(d => Number(d.data) || 0), 0);
if (maxValue <= 0) return null;
const chartTopPadding = 1;
const chartBottomPadding = 8;
const usableHeight = 100 - chartTopPadding - chartBottomPadding;
// Helpers to compute Y position in percentage
const posFor = (limit) => {
const perc = Math.max(0, Math.min(1, (Number(limit) || 0) / maxValue));
return chartTopPadding + ((1 - perc) * usableHeight);
};
let showParentLimit = false;
let showSuborgLimit = false;
// Suborg/current limit: only when viewing a specific org (not ALL)
if (selectedOrgForStats !== 'ALL' && typeof suborgAppExecutionLimit === 'number' && suborgAppExecutionLimit > 0 && suborgAppExecutionLimit <= maxValue && selectedOrgForStats === orgForLimit?.id) {
showSuborgLimit = true;
}
if (typeof parentAppExecutionLimit === 'number' && parentAppExecutionLimit > 0 && parentAppExecutionLimit <= maxValue) {
if (selectedOrgForStats === 'ALL') {
showParentLimit = true;
} else if (parentAppExecutionLimit !== suborgAppExecutionLimit) {
showParentLimit = true;
}
}
if (!showParentLimit && !showSuborgLimit) return null;
const suborgTop = showSuborgLimit ? posFor(suborgAppExecutionLimit) : null;
const parentTop = showParentLimit ? posFor(parentAppExecutionLimit) : null;
return (
<>
{showSuborgLimit && (
<>
<div
style={{
position: 'absolute',
top: `${suborgTop}%`,
left: '2%',
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: `${suborgTop}%`,
right: '-5%',
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(suborgAppExecutionLimit)} limit
</Typography>
</div>
</>
)}
{showParentLimit && (
<>
<div
style={{
position: 'absolute',
top: `${parentTop}%`,
left: '2%',
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: `${parentTop}%`,
right: '-5%',
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>
</div>
);
};
export default RunsOverTimeWidget;