Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Box, Button, Flex, Heading, Text, useDisclosure, VStack } from "@chakra-ui/react";
import { Box, Button, Flex, Heading, Stack, Text, useDisclosure, VStack } from "@chakra-ui/react";
import type { ColumnDef } from "@tanstack/react-table";
import type { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
Expand All @@ -27,7 +27,7 @@ import { DataTable } from "src/components/DataTable";
import { ErrorAlert } from "src/components/ErrorAlert";
import { StateBadge } from "src/components/StateBadge";
import Time from "src/components/Time";
import { Accordion, Dialog } from "src/components/ui";
import { Accordion, Alert, Dialog } from "src/components/ui";
import { useBulkDeleteDagRuns } from "src/queries/useBulkDeleteDagRuns";

type Props = {
Expand Down Expand Up @@ -61,11 +61,13 @@ const getColumns = (translate: TFunction): Array<ColumnDef<DAGRunResponse>> => [
const BulkDeleteDagRunsButton = ({ clearSelections, selectedDagRuns }: Props) => {
const { t: translate } = useTranslation(["common", "dags"]);
const { onClose, onOpen, open } = useDisclosure();
const { bulkAction, error, isPending } = useBulkDeleteDagRuns({
const { bulkAction, data, error, isPending } = useBulkDeleteDagRuns({
clearSelections,
onSuccessConfirm: onClose,
});

const actionErrors = (data?.delete?.errors ?? []) as Array<{ error: string; status_code?: number }>;

const columns = getColumns(translate);

const byDagId = new Map<string, Array<DAGRunResponse>>();
Expand Down Expand Up @@ -143,6 +145,14 @@ const BulkDeleteDagRunsButton = ({ clearSelections, selectedDagRuns }: Props) =>
</Box>

<ErrorAlert error={error} />
{actionErrors.length > 0 ? (
<Stack gap={2} mt={3}>
{actionErrors.map((actionError, index) => (
// eslint-disable-next-line react/no-array-index-key -- per-entity errors have no stable id
<Alert key={index} status="error" title={actionError.error} />
))}
</Stack>
) : undefined}
Comment on lines +148 to +155
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make an ActionErrors component to reuse all this error logic.

<Flex justifyContent="end" mt={3}>
<Button
colorPalette="danger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Box, Button, Flex, Heading, Text, useDisclosure, VStack } from "@chakra-ui/react";
import { Box, Button, Flex, Heading, Stack, Text, useDisclosure, VStack } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { FiTrash2 } from "react-icons/fi";

import type { TaskInstanceResponse } from "openapi/requests/types.gen";
import { getColumns } from "src/components/ActionAccordion/columns";
import { DataTable } from "src/components/DataTable";
import { ErrorAlert } from "src/components/ErrorAlert";
import { Accordion, Dialog } from "src/components/ui";
import { Accordion, Alert, Dialog } from "src/components/ui";
import { useBulkTaskInstances } from "src/queries/useBulkTaskInstances";

type Props = {
Expand All @@ -35,11 +35,13 @@ type Props = {
const BulkDeleteTaskInstancesButton = ({ clearSelections, selectedTaskInstances }: Props) => {
const { t: translate } = useTranslation();
const { onClose, onOpen, open } = useDisclosure();
const { bulkAction, error, isPending } = useBulkTaskInstances({
const { bulkAction, data, error, isPending } = useBulkTaskInstances({
clearSelections,
onSuccessConfirm: onClose,
});

const actionErrors = (data?.delete?.errors ?? []) as Array<{ error: string; status_code?: number }>;

const columns = getColumns(translate);

// Group by dag_run_id for display
Expand Down Expand Up @@ -118,6 +120,14 @@ const BulkDeleteTaskInstancesButton = ({ clearSelections, selectedTaskInstances
</Box>

<ErrorAlert error={error} />
{actionErrors.length > 0 ? (
<Stack gap={2} mt={3}>
{actionErrors.map((actionError, index) => (
// eslint-disable-next-line react/no-array-index-key -- per-entity errors have no stable id
<Alert key={index} status="error" title={actionError.error} />
))}
</Stack>
) : undefined}
<Flex justifyContent="end" mt={3}>
<Button
colorPalette="danger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Badge, Box, Button, Flex, Heading, HStack, VStack, useDisclosure } from "@chakra-ui/react";
import { Badge, Box, Button, Flex, Heading, HStack, Stack, VStack, useDisclosure } from "@chakra-ui/react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { FiX } from "react-icons/fi";
Expand All @@ -27,7 +27,7 @@ import { ActionAccordion } from "src/components/ActionAccordion";
import { ErrorAlert } from "src/components/ErrorAlert";
import { allowedStates } from "src/components/MarkAs/utils";
import { StateBadge } from "src/components/StateBadge";
import { Dialog, Menu } from "src/components/ui";
import { Alert, Dialog, Menu } from "src/components/ui";
import SegmentedControl from "src/components/ui/SegmentedControl";
import { useBulkMarkAsDryRun } from "src/queries/useBulkMarkAsDryRun";
import { useBulkTaskInstances } from "src/queries/useBulkTaskInstances";
Expand All @@ -43,11 +43,13 @@ const BulkMarkTaskInstancesAsButton = ({ clearSelections, selectedTaskInstances
const [state, setState] = useState<TaskInstanceState>("success");
const [selectedOptions, setSelectedOptions] = useState<Array<string>>([]);
const [note, setNote] = useState<string | null>(null);
const { bulkAction, error, isPending, setError } = useBulkTaskInstances({
const { bulkAction, data, error, isPending, reset } = useBulkTaskInstances({
clearSelections,
onSuccessConfirm: onClose,
});

const actionErrors = (data?.update?.errors ?? []) as Array<{ error: string; status_code?: number }>;

const past = selectedOptions.includes("past");
const future = selectedOptions.includes("future");
const upstream = selectedOptions.includes("upstream");
Expand All @@ -73,7 +75,7 @@ const BulkMarkTaskInstancesAsButton = ({ clearSelections, selectedTaskInstances
setState(newState);
setSelectedOptions([]);
setNote(null);
setError(undefined);
reset();
onOpen();
};

Expand Down Expand Up @@ -164,6 +166,14 @@ const BulkMarkTaskInstancesAsButton = ({ clearSelections, selectedTaskInstances
</Flex>
<ActionAccordion affectedTasks={affectedTasks} groupByRunId note={note} setNote={setNote} />
<ErrorAlert error={error} />
{actionErrors.length > 0 ? (
<Stack gap={2} mt={3}>
{actionErrors.map((actionError, index) => (
// eslint-disable-next-line react/no-array-index-key -- per-entity errors have no stable id
<Alert key={index} status="error" title={actionError.error} />
))}
</Stack>
) : undefined}
<Flex justifyContent="end" mt={3}>
<Button
disabled={affectedTasks.total_entries === 0}
Expand Down
74 changes: 31 additions & 43 deletions airflow-core/src/airflow/ui/src/queries/useBulkDeleteDagRuns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
* under the License.
*/
import { useQueryClient } from "@tanstack/react-query";
import { useRef, useState } from "react";
import { useRef } from "react";
import { useTranslation } from "react-i18next";

import {
useDagRunServiceBulkDagRuns,
useDagRunServiceGetDagRunsKey,
useTaskInstanceServiceGetTaskInstancesKey,
} from "openapi/queries";
import type { BulkActionResponse, BulkBody_BulkDAGRunBody_, BulkResponse } from "openapi/requests/types.gen";
import type { BulkBody_BulkDAGRunBody_, BulkResponse } from "openapi/requests/types.gen";
import { toaster } from "src/components/ui";

import { gridQueryKeys, tiPerAttemptQueryKeys } from "./gridViewQueryKeys";
Expand All @@ -35,25 +35,8 @@ type Props = {
readonly onSuccessConfirm: VoidFunction;
};

const handleActionResult = (
actionResult: BulkActionResponse,
setError: (error: unknown) => void,
onSuccess: (count: number, keys: Array<string>) => void,
) => {
const { errors, success } = actionResult;

if (Array.isArray(errors) && errors.length > 0) {
const apiError = errors[0] as { error: string };

setError({ body: { detail: apiError.error } });
} else if (Array.isArray(success) && success.length > 0) {
onSuccess(success.length, success);
}
};

export const useBulkDeleteDagRuns = ({ clearSelections, onSuccessConfirm }: Props) => {
const queryClient = useQueryClient();
const [error, setError] = useState<unknown>(undefined);
const affectedDagIds = useRef<Set<string>>(new Set());
const { t: translate } = useTranslation(["common", "dags"]);

Expand All @@ -67,36 +50,41 @@ export const useBulkDeleteDagRuns = ({ clearSelections, onSuccessConfirm }: Prop
),
]);

if (responseData.delete) {
handleActionResult(responseData.delete, setError, (count, keys) => {
toaster.create({
description: translate("toaster.bulkDelete.success.description", {
count,
keys: keys.join(", "),
resourceName: translate("dagRun_other"),
}),
title: translate("toaster.bulkDelete.success.title", {
resourceName: translate("dagRun_other"),
}),
type: "success",
});
clearSelections();
onSuccessConfirm();
const deleteResult = responseData.delete;

if (!deleteResult) {
return;
}

const successKeys = deleteResult.success ?? [];
const actionErrors = deleteResult.errors ?? [];

if (successKeys.length > 0) {
toaster.create({
description: translate("toaster.bulkDelete.success.description", {
count: successKeys.length,
keys: successKeys.join(", "),
resourceName: translate("dagRun_other"),
}),
title: translate("toaster.bulkDelete.success.title", {
resourceName: translate("dagRun_other"),
}),
type: "success",
});
clearSelections();
}
};

const onError = (_error: unknown) => {
setError(_error);
// Per-entity failures (status 200 with items in ``errors``) keep the dialog open
// so the user can see what failed; the consumer renders ``data.delete.errors``.
if (actionErrors.length === 0) {
onSuccessConfirm();
}
};

const { isPending, mutate } = useDagRunServiceBulkDagRuns({
onError,
onSuccess,
});
const { data, error, isPending, mutate, reset } = useDagRunServiceBulkDagRuns({ onSuccess });

const bulkAction = (requestBody: BulkBody_BulkDAGRunBody_) => {
setError(undefined);
reset();
const dagIds = new Set<string>();

for (const action of requestBody.actions) {
Expand All @@ -110,5 +98,5 @@ export const useBulkDeleteDagRuns = ({ clearSelections, onSuccessConfirm }: Prop
mutate({ dagId: "~", requestBody });
};

return { bulkAction, error, isPending, setError };
return { bulkAction, data, error, isPending, reset };
};
75 changes: 28 additions & 47 deletions airflow-core/src/airflow/ui/src/queries/useBulkTaskInstances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,14 @@
* under the License.
*/
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { useTranslation } from "react-i18next";

import {
useDagRunServiceGetDagRunsKey,
useTaskInstanceServiceBulkTaskInstances,
useTaskInstanceServiceGetTaskInstancesKey,
} from "openapi/queries";
import type {
BulkActionResponse,
BulkBody_BulkTaskInstanceBody_,
BulkResponse,
} from "openapi/requests/types.gen";
import type { BulkBody_BulkTaskInstanceBody_, BulkResponse } from "openapi/requests/types.gen";
import { toaster } from "src/components/ui";

import { tiPerAttemptQueryKeys } from "./gridViewQueryKeys";
Expand All @@ -39,25 +34,8 @@ type Props = {
readonly onSuccessConfirm: VoidFunction;
};

const handleActionResult = (
actionResult: BulkActionResponse,
setError: (error: unknown) => void,
onSuccess: (count: number, keys: Array<string>) => void,
) => {
const { errors, success } = actionResult;

if (Array.isArray(errors) && errors.length > 0) {
const apiError = errors[0] as { error: string };

setError({ body: { detail: apiError.error } });
} else if (Array.isArray(success) && success.length > 0) {
onSuccess(success.length, success);
}
};

export const useBulkTaskInstances = ({ clearSelections, onSuccessConfirm }: Props) => {
const queryClient = useQueryClient();
const [error, setError] = useState<unknown>(undefined);
const { t: translate } = useTranslation(["common", "dags"]);

const onSuccess = async (responseData: BulkResponse) => {
Expand All @@ -71,38 +49,41 @@ export const useBulkTaskInstances = ({ clearSelections, onSuccessConfirm }: Prop
const actionResult = responseData.delete ?? responseData.update;
const toasterKey = isDelete ? "toaster.bulkDelete" : "toaster.bulkUpdate";

if (actionResult) {
handleActionResult(actionResult, setError, (count, keys) => {
toaster.create({
description: translate(`${toasterKey}.success.description`, {
count,
keys: keys.join(", "),
resourceName: translate("taskInstance_other"),
}),
title: translate(`${toasterKey}.success.title`, {
resourceName: translate("taskInstance_other"),
}),
type: "success",
});
clearSelections();
onSuccessConfirm();
if (!actionResult) {
return;
}

const successKeys = actionResult.success ?? [];
const actionErrors = actionResult.errors ?? [];

if (successKeys.length > 0) {
toaster.create({
description: translate(`${toasterKey}.success.description`, {
count: successKeys.length,
keys: successKeys.join(", "),
resourceName: translate("taskInstance_other"),
}),
title: translate(`${toasterKey}.success.title`, {
resourceName: translate("taskInstance_other"),
}),
type: "success",
});
clearSelections();
}
};

const onError = (_error: unknown) => {
setError(_error);
// Per-entity failures (status 200 with items in ``errors``) keep the dialog open
// so the user can see what failed; the consumer renders ``data.<action>.errors``.
if (actionErrors.length === 0) {
onSuccessConfirm();
}
};

const { isPending, mutate } = useTaskInstanceServiceBulkTaskInstances({
onError,
onSuccess,
});
const { data, error, isPending, mutate, reset } = useTaskInstanceServiceBulkTaskInstances({ onSuccess });

const bulkAction = (requestBody: BulkBody_BulkTaskInstanceBody_) => {
setError(undefined);
reset();
mutate({ dagId: "~", dagRunId: "~", requestBody });
};

return { bulkAction, error, isPending, setError };
return { bulkAction, data, error, isPending, reset };
};
Loading