nathadfield opened a new issue, #67541:
URL: https://github.com/apache/airflow/issues/67541
### Under which category would you file this issue?
Airflow Core
### Apache Airflow version
main (development)
### What happened and how to reproduce it?
**Issue Description**
The grid API endpoint serializes Python `None` task-instance states as the
JSON dict key `"None"` (Pydantic conversion of `dict[TaskInstanceState | None,
int]` in `LightGridTaskInstanceSummary.child_states`). Three UI surfaces
introduced by #61854 iterate over `child_states` directly and render the raw
`"None"` key, producing tokenless / untranslated output:
1.
`airflow-core/src/airflow/ui/src/components/Graph/SegmentedStateBar.tsx:44` —
`<Box bg={`${state}.solid`}>` per entry; `"None.solid"` does not resolve to a
Chakra theme token, so the no-status slice renders without a colour.
2.
`airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx:146-149` —
per-state breakdown row uses the same `bg={`${state}.solid`}` swatch
(tokenless) and `translate(`common:states.${state}`)` (no translation for
`"None"`), so the row shows a colourless swatch beside the literal key
`common:states.None`.
3.
`airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx:35-40` and
`airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Header.tsx:37-43` —
Header stats translate each `child_states` key directly with no normalization;
same untranslated `states.None` label.
**Minimal reproducer**
```python
import time
from datetime import datetime
from airflow.sdk import DAG, task
with DAG(
dag_id="none_child_state_demo",
description="Demonstrates frontend rendering bug for serialized
no-status child_states key",
start_date=datetime(2024, 1, 1),
schedule=None,
catchup=False,
) as dag:
@task
def make_args():
# Sleep so the mapped consumer stays unexpanded long enough to
# observe the bug in the UI.
time.sleep(60)
return ["a", "b", "c"]
@task
def consume(item):
return item
# Until `make_args` returns, `consume` has no rows and the grid API
# reports child_states={"None": 1} for it.
consume.expand(item=make_args())
```
**Steps to reproduce**
1. Save the DAG above as `none_child_state_demo.py` in your `dags` folder.
2. Trigger the DAG manually.
3. While `make_args` is still running (≤60s window), open the grid view and
locate the `consume` mapped task.
4. Observe:
- **Segmented state bar (graph view, collapsed group containing
`consume`):** the no-status slice renders without a colour token.
- **Tooltip on the `consume` cell:** breakdown row shows `1
common:states.None` with no colour swatch.
- **Click into the mapped task details page:** Header stats row shows
`Total states.None`.
5. Confirm the API payload by querying
`/grid/ti_summaries/none_child_state_demo?run_ids=<run_id>` during the same
window — the `consume` entry will include `"child_states": {"None": 1}`. The
wire format is also asserted in
`airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py:839,848`.
### What you think should happen instead?
Each render site should normalize the serialized `"None"` key to the
existing `"none"` / `"no_status"` UI convention so the no-status slice shows
the same neutral colour and the "No status" translated label that the rest of
the UI uses for tasks without a state.
Suggested approach: add a single helper (e.g. `normalizeStateKey(key:
string): string`) to `airflow-core/src/airflow/ui/src/utils/stateUtils.ts` that
maps `"None"` → `"none"`, and call it at the four render sites:
- For `bg={`${state}.solid`}` → use `normalizeStateKey(state)` (mirrors the
existing pattern at
`airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx:79`,
which already does `state === "no_status" ? "none" : state`).
- For `translate(`common:states.${state}`)` → map `"None"` to `"no_status"`
so it picks up the existing localized label.
**Acceptance criteria**
- Group with `child_states={"None": 2, "success": 1}` shows a coloured and
labelled no-status slice in the segmented bar.
- Tooltip breakdown row for `"None": N` shows the localized "No status"
label beside a visible colour swatch.
- Group / Mapped Header stats row for no-status children uses the "No
status" label, not `states.None`.
- Existing labels (`success`, `running`, etc.) and colours unchanged.
- Add unit coverage for the helper and at least one render site (suggested:
the tooltip breakdown, since it's the cheapest surface to assert on).
### Operating System
Not Applicable (frontend-only)
### Deployment
Other
### Apache Airflow Provider(s)
_None — not provider-related._
### Versions of Apache Airflow Providers
Not Applicable
### Official Helm Chart version
Not Applicable
### Kubernetes Version
Not Applicable
### Helm Chart configuration
Not Applicable
### Docker Image customizations
Not Applicable
### Anything else?
This is a deferred follow-up identified during review of work to wire up
`getDisplayState` — a helper that fixes the dominant-state colouring of
collapsed groups / mapped tasks across the badge / border / MiniMap / state
filter, all of which correctly fall back to `null` when `"None"` is the
dominant child key. That work deliberately scopes itself to the dominant-state
surfaces; the three breakdown / segmented-bar / Header-stats sites listed above
iterate `child_states` directly and remain on the pre-existing rendering path.
The fix is small but cross-cuts four UI files and a new helper + tests — clean
isolated scope for a separate PR.
Original feature PR: #61854.
---
Drafted-by: Claude Code (Opus 4.7); reviewed by @nathadfield before posting
### Are you willing to submit PR?
- [X] Yes I am willing to submit a PR!
### Code of Conduct
- [X] I agree to follow this project's [Code of
Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]