Skip to content

Commit 6d9ff51

Browse files
committed
fixup! Show current task executions in the UI
The loop choices sat in a checkbox stack below the options row, so the same dialog presented its clear scope in two different widgets. They are now options of the existing segmented control, shown only when they apply: later iterations whenever a loop member is selected, whole expansion only when the selection also holds mapped members. Their booleans are read from the selected options instead of separate state, and the initial selection already includes later iterations in loop mode so the first dry run matches what is shown. The selection is a copy of the saved defaults, so the loop options are never written back to them. The now unused execution.clearLater and execution.clearWhole keys are replaced by short labels beside the other clear options.
1 parent bfa9106 commit 6d9ff51

5 files changed

Lines changed: 56 additions & 36 deletions

File tree

‎airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@
6969
"upcomingCount_other": "{{count}} Upcoming Deadlines"
7070
},
7171
"execution": {
72-
"clearLater": "Clear later loop iterations",
7372
"clearSelected": "Clear selected executions",
74-
"clearWhole": "Clear whole mapped expansions",
7573
"empty": "No executions in this run.",
7674
"iteration": "{{node}} · Iteration {{index}}",
7775
"mapped_one": "{{task}} ({{count}} mapped task on this page)",

‎airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,15 @@
9292
"existingTasks": "Clear existing tasks",
9393
"future": "Future",
9494
"keepTaskState": "Keep task state and resume",
95+
"laterIterations": "Later iterations",
9596
"onlyFailed": "Clear only failed tasks",
9697
"past": "Past",
9798
"preventRunningTasks": "Prevent rerun if task is running",
9899
"queueNew": "Queue up new tasks",
99100
"runOnLatestVersion": "Run with latest bundle version",
100101
"runOnLatestVersionForced": "Always uses the latest — there's no earlier version to go back to",
101-
"upstream": "Upstream"
102+
"upstream": "Upstream",
103+
"wholeExpansion": "Whole expansion"
102104
}
103105
},
104106
"runStateCounts": {

‎airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.test.tsx‎

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,26 @@ describe("ClearTaskInstanceDialog", () => {
211211
);
212212

213213
expect(await screen.findByRole("checkbox", { name: /keepTaskState/iu })).toBeVisible();
214-
expect(screen.queryByRole("checkbox", { name: /execution\.clearLater/iu }) !== null).toBe(shown);
214+
expect(screen.queryByRole("button", { name: /options\.laterIterations/iu }) !== null).toBe(shown);
215215
},
216216
);
217217

218+
it("includes later iterations in the first dry run and leaves the saved defaults untouched", async () => {
219+
localStorage.setItem(CLEAR_TASK_INSTANCE_DEFAULT_OPTIONS_KEY, JSON.stringify(["downstream"]));
220+
render(<ClearTaskInstanceDialog onClose={vi.fn()} open taskInstance={loopMember} />, {
221+
wrapper: Wrapper,
222+
});
223+
224+
fireEvent.click(await screen.findByRole("button", { name: /options\.laterIterations/iu }));
225+
226+
expect(dryRunsMock.mock.calls[0]?.[0]).toMatchObject({
227+
requests: [{ requestBody: { include_later_loop_iterations: true } }],
228+
});
229+
expect(localStorage.getItem(CLEAR_TASK_INSTANCE_DEFAULT_OPTIONS_KEY)).toBe(
230+
JSON.stringify(["downstream"]),
231+
);
232+
});
233+
218234
it("clears a loop member by execution id with the later-iterations choice and never sends past or future", async () => {
219235
localStorage.setItem(
220236
CLEAR_TASK_INSTANCE_DEFAULT_OPTIONS_KEY,
@@ -224,7 +240,7 @@ describe("ClearTaskInstanceDialog", () => {
224240
wrapper: Wrapper,
225241
});
226242

227-
fireEvent.click(await screen.findByRole("checkbox", { name: /execution\.clearLater/iu }));
243+
fireEvent.click(await screen.findByRole("button", { name: /options\.laterIterations/iu }));
228244
fireEvent.click(await screen.findByRole("button", { name: /modal\.confirm/iu }));
229245

230246
await waitFor(() => expect(mutateMock).toHaveBeenCalledTimes(1));
@@ -248,7 +264,7 @@ describe("ClearTaskInstanceDialog", () => {
248264
wrapper: Wrapper,
249265
});
250266

251-
fireEvent.click(await screen.findByRole("checkbox", { name: /execution\.clearWhole/iu }));
267+
fireEvent.click(await screen.findByRole("button", { name: /options\.wholeExpansion/iu }));
252268
fireEvent.click(await screen.findByRole("button", { name: /modal\.confirm/iu }));
253269

254270
await waitFor(() => expect(mutateMock).toHaveBeenCalledTimes(1));
@@ -265,8 +281,8 @@ describe("ClearTaskInstanceDialog", () => {
265281
wrapper: Wrapper,
266282
});
267283

268-
expect(await screen.findByRole("checkbox", { name: /execution\.clearLater/iu })).toBeVisible();
269-
expect(screen.queryByRole("checkbox", { name: /execution\.clearWhole/iu })).not.toBeInTheDocument();
284+
expect(await screen.findByRole("button", { name: /options\.laterIterations/iu })).toBeVisible();
285+
expect(screen.queryByRole("button", { name: /options\.wholeExpansion/iu })).not.toBeInTheDocument();
270286
});
271287

272288
it("does not offer the exclusion checkboxes for a loop clear", async () => {
@@ -437,6 +453,6 @@ describe("ClearTaskInstanceDialog", () => {
437453

438454
fireEvent.click(screen.getByRole("button", { name: /clear\.button/iu }));
439455

440-
expect(await screen.findByRole("checkbox", { name: /execution\.clearLater/iu })).toBeVisible();
456+
expect(await screen.findByRole("button", { name: /options\.laterIterations/iu })).toBeVisible();
441457
});
442458
});

‎airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.tsx‎

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919
import { useEffect, useState } from "react";
2020

21-
import { Button, Flex, Stack, useDisclosure } from "@chakra-ui/react";
21+
import { Button, Flex, useDisclosure } from "@chakra-ui/react";
2222
import { useTranslation } from "react-i18next";
2323
import { CgRedo } from "react-icons/cg";
2424

@@ -119,6 +119,9 @@ const getRunGroups = (targets: Array<ClearTarget>): Array<RunGroup> => {
119119
return [...groups.values()];
120120
};
121121

122+
const LATER_ITERATIONS = "laterIterations";
123+
const WHOLE_EXPANSION = "wholeExpansion";
124+
122125
// An empty mapped expansion is a placeholder at region index -1.
123126
const isMappedTarget = (target: ClearTarget) => target.map_index >= 0 || target.region_index === -1;
124127

@@ -160,17 +163,20 @@ const ClearTaskInstanceDialog = (props: Props) => {
160163
const [clearTaskInstanceDefaultOptions] = useClearTaskInstanceDefaultOptions();
161164
const [preventRunningTaskDefault] = useClearPreventRunningTaskDefault();
162165
const [keepTaskStateDefault] = useClearKeepTaskStateDefault();
163-
const [selectedOptions, setSelectedOptions] = useState<Array<string>>(clearTaskInstanceDefaultOptions);
166+
const initialOptions = loopMode
167+
? [...clearTaskInstanceDefaultOptions, LATER_ITERATIONS]
168+
: clearTaskInstanceDefaultOptions;
169+
const [selectedOptions, setSelectedOptions] = useState<Array<string>>(initialOptions);
164170

165171
const onlyFailed = selectedOptions.includes("onlyFailed");
166172
const past = selectedOptions.includes("past");
167173
const future = selectedOptions.includes("future");
168174
const upstream = selectedOptions.includes("upstream");
169175
const downstream = selectedOptions.includes("downstream");
176+
const laterLoopIterations = selectedOptions.includes(LATER_ITERATIONS);
177+
const wholeExpansions = selectedOptions.includes(WHOLE_EXPANSION);
170178
const [keepTaskState, setKeepTaskState] = useState(keepTaskStateDefault);
171179
const [preventRunningTask, setPreventRunningTask] = useState(preventRunningTaskDefault);
172-
const [laterLoopIterations, setLaterLoopIterations] = useState(true);
173-
const [wholeExpansions, setWholeExpansions] = useState(false);
174180

175181
const [note, setNote] = useState<string | null>(initialNote);
176182

@@ -186,8 +192,6 @@ const ClearTaskInstanceDialog = (props: Props) => {
186192
useEffect(() => {
187193
if (openDialog) {
188194
setKeepTaskState(keepTaskStateDefault);
189-
setLaterLoopIterations(true);
190-
setWholeExpansions(false);
191195
}
192196
}, [openDialog, keepTaskStateDefault]);
193197

@@ -432,7 +436,7 @@ const ClearTaskInstanceDialog = (props: Props) => {
432436
>
433437
<Flex justifyContent="center">
434438
<SegmentedControl
435-
defaultValues={clearTaskInstanceDefaultOptions}
439+
defaultValues={initialOptions}
436440
multiple
437441
onChange={setSelectedOptions}
438442
options={[
@@ -458,27 +462,25 @@ const ClearTaskInstanceDialog = (props: Props) => {
458462
label: translate("dags:runAndTaskActions.options.onlyFailed"),
459463
value: "onlyFailed",
460464
},
465+
...(loopMode
466+
? [
467+
{
468+
label: translate("dags:runAndTaskActions.options.laterIterations"),
469+
value: LATER_ITERATIONS,
470+
},
471+
]
472+
: []),
473+
...(loopMode && hasMappedTargets
474+
? [
475+
{
476+
label: translate("dags:runAndTaskActions.options.wholeExpansion"),
477+
value: WHOLE_EXPANSION,
478+
},
479+
]
480+
: []),
461481
]}
462482
/>
463483
</Flex>
464-
{loopMode ? (
465-
<Stack alignItems="center" gap={2} mt={4}>
466-
<Checkbox
467-
checked={laterLoopIterations}
468-
onCheckedChange={(event) => setLaterLoopIterations(event.checked === true)}
469-
>
470-
{translate("dag:execution.clearLater")}
471-
</Checkbox>
472-
{hasMappedTargets ? (
473-
<Checkbox
474-
checked={wholeExpansions}
475-
onCheckedChange={(event) => setWholeExpansions(event.checked === true)}
476-
>
477-
{translate("dag:execution.clearWhole")}
478-
</Checkbox>
479-
) : undefined}
480-
</Stack>
481-
) : undefined}
482484
<ErrorAlert error={dryRunError} />
483485
<ActionAccordion
484486
affectedTasks={data}

‎airflow-core/src/airflow/ui/src/pages/Run/Execution.test.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { BaseWrapper } from "src/utils/Wrapper";
3737

3838
import commonTranslations from "../../../public/i18n/locales/en/common.json";
3939
import dagTranslations from "../../../public/i18n/locales/en/dag.json";
40+
import dagsTranslations from "../../../public/i18n/locales/en/dags.json";
4041
import { Execution } from "./Execution";
4142

4243
vi.mock("src/router", () => ({ taskInstanceRoutes: [] }));
@@ -66,6 +67,7 @@ vi.mock("src/components/Clear/TaskInstance/ClearTaskInstanceConfirmationDialog",
6667
beforeAll(() => {
6768
i18n.addResourceBundle("en", "dag", dagTranslations, true, true);
6869
i18n.addResourceBundle("en", "common", commonTranslations, true, true);
70+
i18n.addResourceBundle("en", "dags", dagsTranslations, true, true);
6971
});
7072

7173
const refresh = vi.hoisted<{ interval: number | false }>(() => ({ interval: false }));
@@ -417,7 +419,7 @@ describe("Run Execution", () => {
417419
);
418420
fireEvent.click(screen.getByRole("button", { name: "Clear selected executions" }));
419421

420-
expect(await screen.findByRole("checkbox", { name: "Clear later loop iterations" })).toBeChecked();
422+
expect(await screen.findByRole("button", { name: "Later iterations" })).toBeVisible();
421423
await waitFor(() =>
422424
expect(clear.mock.lastCall?.[0].requestBody).toMatchObject({
423425
include_later_loop_iterations: true,
@@ -450,7 +452,7 @@ describe("Run Execution", () => {
450452
expect(clear.mock.lastCall?.[0].requestBody).toMatchObject({ task_ids: [["plain", 0]] }),
451453
);
452454
expect(clear.mock.lastCall?.[0].requestBody).not.toHaveProperty("task_instance_ids");
453-
expect(screen.queryByRole("checkbox", { name: "Clear later loop iterations" })).not.toBeInTheDocument();
455+
expect(screen.queryByRole("button", { name: "Later iterations" })).not.toBeInTheDocument();
454456
});
455457

456458
it("selects the current execution once a selected one has been retried", async () => {

0 commit comments

Comments
 (0)