Skip to content

Commit 1cdb277

Browse files
committed
Pause and cancel backfills in table
1 parent c0351fb commit 1cdb277

File tree

3 files changed

+180
-41
lines changed

3 files changed

+180
-41
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
"backfillInProgress": "Backfill in progress",
3434
"cancel": "Cancel backfill",
3535
"pause": "Pause backfill",
36-
"unpause": "Unpause backfill"
36+
"unpause": "Unpause backfill",
37+
"viewMore_one": "1 More Active Backfill",
38+
"viewMore_other": "{{count}} More Active Backfills"
3739
},
3840
"clipboard": {
3941
"copy": "Copy"

airflow-core/src/airflow/ui/src/components/Banner/BackfillBanner.tsx

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
import { Box, Button, HStack, Spacer, Text, type ButtonProps } from "@chakra-ui/react";
19+
import { Box, Button, HStack, Link, Spacer, Text, VStack, type ButtonProps } from "@chakra-ui/react";
2020
import { useQueryClient } from "@tanstack/react-query";
2121
import { useTranslation } from "react-i18next";
2222
import { MdPause, MdPlayArrow, MdStop } from "react-icons/md";
2323
import { RiArrowGoBackFill } from "react-icons/ri";
24+
import { Link as RouterLink } from "react-router-dom";
2425

2526
import {
2627
useBackfillServiceCancelBackfill,
@@ -65,7 +66,8 @@ const BackfillBanner = ({ dagId }: Props) => {
6566
: false,
6667
},
6768
);
68-
const [backfill] = data?.backfills.filter((bf: BackfillResponse) => bf.completed_at === null) ?? [];
69+
const [backfill] = data?.backfills ?? [];
70+
const additionalBackfillsCount = (data?.backfills.length ?? 0) - 1;
6971

7072
const queryClient = useQueryClient();
7173
const onSuccess = async () => {
@@ -105,35 +107,49 @@ const BackfillBanner = ({ dagId }: Props) => {
105107
return (
106108
<Box bg="info.solid" borderRadius="full" color="info.contrast" my="1" px="2" py="1">
107109
<HStack alignItems="center" ml={3}>
108-
<RiArrowGoBackFill />
109-
<Text key="backfill">{translate("banner.backfillInProgress")}:</Text>
110-
<Text fontSize="sm">
111-
{" "}
112-
<Time datetime={backfill.from_date} /> - <Time datetime={backfill.to_date} />
113-
</Text>
114-
115-
<Spacer flex="max-content" />
116-
<ProgressBar size="xs" visibility="visible" />
117-
<Button
118-
aria-label={backfill.is_paused ? translate("banner.unpause") : translate("banner.pause")}
119-
loading={isPausePending || isUnPausePending}
120-
onClick={() => {
121-
togglePause();
122-
}}
123-
{...buttonProps}
124-
>
125-
{backfill.is_paused ? <MdPlayArrow /> : <MdPause />}
126-
</Button>
127-
<Button
128-
aria-label={translate("banner.cancel")}
129-
loading={isStopPending}
130-
onClick={() => {
131-
cancel();
132-
}}
133-
{...buttonProps}
134-
>
135-
<MdStop />
136-
</Button>
110+
<VStack align="stretch" flex={1} gap={1}>
111+
<HStack alignItems="center">
112+
<RiArrowGoBackFill />
113+
<Text key="backfill">{translate("banner.backfillInProgress")}:</Text>
114+
<Text fontSize="sm">
115+
{" "}
116+
<Time datetime={backfill.from_date} /> - <Time datetime={backfill.to_date} />
117+
</Text>
118+
<Spacer flex="max-content" />
119+
<ProgressBar size="xs" visibility="visible" />
120+
</HStack>
121+
{additionalBackfillsCount > 0 && (
122+
<HStack mb={1}>
123+
<Link asChild color="info.contrast" fontSize="sm" textDecoration="underline">
124+
<RouterLink to={`/dags/${dagId}/backfills`}>
125+
{translate("banner.viewMore", { count: additionalBackfillsCount })}
126+
</RouterLink>
127+
</Link>
128+
</HStack>
129+
)}
130+
</VStack>
131+
<HStack>
132+
<Button
133+
aria-label={backfill.is_paused ? translate("banner.unpause") : translate("banner.pause")}
134+
loading={isPausePending || isUnPausePending}
135+
onClick={() => {
136+
togglePause();
137+
}}
138+
{...buttonProps}
139+
>
140+
{backfill.is_paused ? <MdPlayArrow /> : <MdPause />}
141+
</Button>
142+
<Button
143+
aria-label={translate("banner.cancel")}
144+
loading={isStopPending}
145+
onClick={() => {
146+
cancel();
147+
}}
148+
{...buttonProps}
149+
>
150+
<MdStop />
151+
</Button>
152+
</HStack>
137153
</HStack>
138154
</Box>
139155
);

airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx

Lines changed: 130 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,51 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
import { Box, Heading, Text } from "@chakra-ui/react";
19+
import { Box, Button, Flex, Heading, Text, type ButtonProps } from "@chakra-ui/react";
20+
import { useQueryClient } from "@tanstack/react-query";
2021
import type { ColumnDef } from "@tanstack/react-table";
22+
import { useCallback } from "react";
2123
import { useTranslation } from "react-i18next";
24+
import { MdPause, MdPlayArrow, MdStop } from "react-icons/md";
2225
import { useParams } from "react-router-dom";
2326

24-
import { useBackfillServiceListBackfillsUi } from "openapi/queries";
27+
import {
28+
useBackfillServiceCancelBackfill,
29+
useBackfillServiceListBackfillsUi,
30+
useBackfillServiceListBackfillsUiKey,
31+
useBackfillServicePauseBackfill,
32+
useBackfillServiceUnpauseBackfill,
33+
} from "openapi/queries";
2534
import type { BackfillResponse } from "openapi/requests/types.gen";
2635
import { DataTable } from "src/components/DataTable";
2736
import { useTableURLState } from "src/components/DataTable/useTableUrlState";
2837
import { ErrorAlert } from "src/components/ErrorAlert";
2938
import Time from "src/components/Time";
30-
import { getDuration } from "src/utils";
39+
import { getDuration, useAutoRefresh } from "src/utils";
3140

32-
const getColumns = (translate: (key: string) => string): Array<ColumnDef<BackfillResponse>> => [
41+
const buttonProps = {
42+
rounded: "full",
43+
size: "xs",
44+
variant: "outline",
45+
} satisfies ButtonProps;
46+
47+
type GetColumnsParams = {
48+
readonly isCancelPending: boolean;
49+
readonly isPausePending: boolean;
50+
readonly isUnpausePending: boolean;
51+
readonly onCancel: (backfill: BackfillResponse) => void;
52+
readonly onPause: (backfill: BackfillResponse) => void;
53+
readonly translate: (key: string) => string;
54+
};
55+
56+
const getColumns = ({
57+
isCancelPending,
58+
isPausePending,
59+
isUnpausePending,
60+
onCancel,
61+
onPause,
62+
translate,
63+
}: GetColumnsParams): Array<ColumnDef<BackfillResponse>> => [
3364
{
3465
accessorKey: "date_from",
3566
cell: ({ row }) => (
@@ -101,6 +132,46 @@ const getColumns = (translate: (key: string) => string): Array<ColumnDef<Backfil
101132
enableSorting: false,
102133
header: translate("table.maxActiveRuns"),
103134
},
135+
{
136+
accessorKey: "actions",
137+
cell: ({ row }) => {
138+
const backfill = row.original;
139+
140+
if (backfill.completed_at !== null) {
141+
return null;
142+
}
143+
144+
return (
145+
<Flex gap={2} justifyContent="end">
146+
<Button
147+
aria-label={
148+
backfill.is_paused
149+
? translate("components:banner.unpause")
150+
: translate("components:banner.pause")
151+
}
152+
loading={isPausePending || isUnpausePending}
153+
onClick={() => onPause(backfill)}
154+
{...buttonProps}
155+
>
156+
{backfill.is_paused ? <MdPlayArrow /> : <MdPause />}
157+
</Button>
158+
<Button
159+
aria-label={translate("components:banner.cancel")}
160+
loading={isCancelPending}
161+
onClick={() => onCancel(backfill)}
162+
{...buttonProps}
163+
>
164+
<MdStop />
165+
</Button>
166+
</Flex>
167+
);
168+
},
169+
enableSorting: false,
170+
header: "",
171+
meta: {
172+
skeletonWidth: 10,
173+
},
174+
},
104175
];
105176

106177
export const Backfills = () => {
@@ -110,14 +181,64 @@ export const Backfills = () => {
110181
const { pagination } = tableURLState;
111182

112183
const { dagId = "" } = useParams();
184+
const refetchInterval = useAutoRefresh({ dagId });
113185

114-
const { data, error, isFetching, isLoading } = useBackfillServiceListBackfillsUi({
115-
dagId,
116-
limit: pagination.pageSize,
117-
offset: pagination.pageIndex * pagination.pageSize,
186+
const { data, error, isFetching, isLoading } = useBackfillServiceListBackfillsUi(
187+
{
188+
dagId,
189+
limit: pagination.pageSize,
190+
offset: pagination.pageIndex * pagination.pageSize,
191+
},
192+
undefined,
193+
{
194+
refetchInterval: (query) =>
195+
query.state.data?.backfills.some((bf: BackfillResponse) => bf.completed_at === null && !bf.is_paused)
196+
? refetchInterval
197+
: false,
198+
},
199+
);
200+
201+
const queryClient = useQueryClient();
202+
const onSuccess = async () => {
203+
await queryClient.invalidateQueries({
204+
queryKey: [useBackfillServiceListBackfillsUiKey],
205+
});
206+
};
207+
208+
const { isPending: isPausePending, mutate: pauseMutate } = useBackfillServicePauseBackfill({ onSuccess });
209+
const { isPending: isUnpausePending, mutate: unpauseMutate } = useBackfillServiceUnpauseBackfill({
210+
onSuccess,
118211
});
212+
const { isPending: isCancelPending, mutate: cancelMutate } = useBackfillServiceCancelBackfill({
213+
onSuccess,
214+
});
215+
216+
const handlePause = useCallback(
217+
(backfill: BackfillResponse) => {
218+
if (backfill.is_paused) {
219+
unpauseMutate({ backfillId: backfill.id });
220+
} else {
221+
pauseMutate({ backfillId: backfill.id });
222+
}
223+
},
224+
[pauseMutate, unpauseMutate],
225+
);
226+
227+
const handleCancel = useCallback(
228+
(backfill: BackfillResponse) => {
229+
cancelMutate({ backfillId: backfill.id });
230+
},
231+
[cancelMutate],
232+
);
119233

120-
const columns = getColumns(translate);
234+
const columns = getColumns({
235+
isCancelPending,
236+
isPausePending,
237+
isUnpausePending,
238+
onCancel: handleCancel,
239+
onPause: handlePause,
240+
translate,
241+
});
121242

122243
return (
123244
<Box>

0 commit comments

Comments
 (0)