From e1ca68371c23f296bfc3073b9eff3dcbe68f4d6e Mon Sep 17 00:00:00 2001 From: Elliot Williams Date: Fri, 29 May 2026 21:33:43 +0000 Subject: [PATCH] Fix dashboard percentage calculation when state counts exceed API cap (1000) When the backend caps state counts at STATE_COUNT_CAP (1000), the frontend computes 'total' as the sum of these capped values. This makes the total denominator artificially low, causing percentages for ALL states (not just the capped one) to be wrong. For example, if success=5000 (capped to 1000), failed=5, running=2, queued=3: - Old total: 1000+5+2+3 = 1010 - Failed %: 5/1010 = 0.50% (wrong, should be ~0.10%) Fix: When any state count reaches the cap, hide percentage displays for ALL states since the total is unreliable. The individual state counts (shown as '1000+') remain accurate and informative. Closes #67336 --- .../src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx | 2 ++ .../src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx | 4 +++- .../pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx index a6578c0f3839e..2345510561129 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx @@ -35,6 +35,7 @@ const DAGRUN_STATES: Array = ["queued", "running", "success" export const DagRunMetrics = ({ dagRunStates, endDate, startDate, stateCountLimit }: DagRunMetricsProps) => { const { t: translate } = useTranslation(); const total = Object.values(dagRunStates).reduce((sum, count) => sum + count, 0); + const anyCapped = DAGRUN_STATES.some((state) => dagRunStates[state] >= stateCountLimit); return ( @@ -48,6 +49,7 @@ export const DagRunMetrics = ({ dagRunStates, endDate, startDate, stateCountLimi = stateCountLimit} endDate={endDate} + hidePercentages={anyCapped} key={state} kind="dag_runs" runs={dagRunStates[state]} diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx index e0cfff1f01c8a..73a08bd66c048 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx @@ -30,6 +30,7 @@ const BAR_HEIGHT = 5; type MetricSectionProps = { readonly capped?: boolean; readonly endDate?: string; + readonly hidePercentages?: boolean; readonly kind: string; readonly runs: number; readonly startDate: string; @@ -40,6 +41,7 @@ type MetricSectionProps = { export const MetricSection = ({ capped = false, endDate, + hidePercentages = false, kind, runs, startDate, @@ -48,7 +50,7 @@ export const MetricSection = ({ }: MetricSectionProps) => { const stateWidth = capped ? BAR_WIDTH : total === 0 ? 0 : (runs / total) * BAR_WIDTH; const remainingWidth = BAR_WIDTH - stateWidth; - const statePercent = capped ? undefined : total === 0 ? 0 : ((runs / total) * 100).toFixed(2); + const statePercent = capped || hidePercentages ? undefined : total === 0 ? 0 : ((runs / total) * 100).toFixed(2); const stateParam = kind === "task_instances" ? SearchParamsKeys.TASK_STATE : SearchParamsKeys.STATE; const searchParams = new URLSearchParams( diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx index f36c0a4635b3d..e578b326a6072 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx @@ -54,6 +54,7 @@ export const TaskInstanceMetrics = ({ }: TaskInstanceMetricsProps) => { const { t: translate } = useTranslation(); const total = Object.values(taskInstanceStates).reduce((sum, count) => sum + count, 0); + const anyCapped = TASK_STATES.some((state) => taskInstanceStates[state] >= stateCountLimit); return ( @@ -70,6 +71,7 @@ export const TaskInstanceMetrics = ({ = stateCountLimit} endDate={endDate} + hidePercentages={anyCapped} key={state} kind="task_instances" runs={taskInstanceStates[state]}