Skip to content

Commit

Permalink
Enable Bulk Delete variables on variables list page (apache#45761)
Browse files Browse the repository at this point in the history
* initial setup

* disable other buttons

* delete dialog text

* checkbox colour

* decrease limit
  • Loading branch information
shubhamraj-git authored Jan 18, 2025
1 parent 6c4eb3c commit 2b1b399
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 35 deletions.
2 changes: 2 additions & 0 deletions airflow/ui/src/components/ui/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Props = {
const ActionButton = ({
actionName,
colorPalette,
disabled = false,
icon,
onClick,
text,
Expand All @@ -47,6 +48,7 @@ const ActionButton = ({
<ButtonComponent
aria-label={actionName}
colorPalette={withText ? colorPalette : "blue"}
disabled={disabled}
onClick={onClick}
size={withText ? "md" : "sm"}
variant={withText ? variant : "ghost"}
Expand Down
101 changes: 101 additions & 0 deletions airflow/ui/src/pages/Variables/DeleteVariablesButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Flex, useDisclosure, Text, VStack, Heading, Code } from "@chakra-ui/react";
import { FiTrash, FiTrash2 } from "react-icons/fi";

import { ErrorAlert } from "src/components/ErrorAlert";
import { Button, Dialog } from "src/components/ui";
import { useBulkDeleteVariables } from "src/queries/useBulkDeleteVariables";

type Props = {
readonly clearSelections: VoidFunction;
readonly deleteKeys: Array<string>;
};

const DeleteVariablesButton = ({ clearSelections, deleteKeys: variableKeys }: Props) => {
const { onClose, onOpen, open } = useDisclosure();
const { error, isPending, mutate } = useBulkDeleteVariables({ clearSelections, onSuccessConfirm: onClose });

return (
<>
<Button
onClick={() => {
onOpen();
}}
size="sm"
variant="outline"
>
<FiTrash2 />
Delete
</Button>

<Dialog.Root onOpenChange={onClose} open={open} size="xl">
<Dialog.Content backdrop>
<Dialog.Header>
<VStack align="start" gap={4}>
<Heading size="xl">Delete Variable{variableKeys.length > 1 ? "s" : ""}</Heading>
</VStack>
</Dialog.Header>

<Dialog.CloseTrigger />

<Dialog.Body width="full">
<Text color="gray.solid" fontSize="md" fontWeight="semibold" mb={4}>
You are about to delete{" "}
<strong>
{variableKeys.length} variable{variableKeys.length > 1 ? "s" : ""}.
</strong>
<br />
<Code mb={2} mt={2} p={4}>
{variableKeys.join(", ")}
</Code>
<br />
This action is permanent and cannot be undone.{" "}
<strong>Are you sure you want to proceed?</strong>
</Text>
<ErrorAlert error={error} />
<Flex justifyContent="end" mt={3}>
<Button
colorPalette="red"
loading={isPending}
onClick={() => {
mutate({
requestBody: {
actions: [
{
action: "delete" as const,
action_if_not_exists: "fail",
keys: variableKeys,
},
],
},
});
}}
>
<FiTrash /> <Text fontWeight="bold">Yes, Delete</Text>
</Button>
</Flex>
</Dialog.Body>
</Dialog.Content>
</Dialog.Root>
</>
);
};

export default DeleteVariablesButton;
8 changes: 6 additions & 2 deletions airflow/ui/src/pages/Variables/ImportVariablesButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,16 @@ import { Button, Dialog } from "src/components/ui";

import ImportVariablesForm from "./ImportVariablesForm";

const ImportVariablesButton = () => {
type Props = {
readonly disabled: boolean;
};

const ImportVariablesButton = ({ disabled }: Props) => {
const { onClose, onOpen, open } = useDisclosure();

return (
<>
<Button colorPalette="blue" onClick={onOpen}>
<Button colorPalette="blue" disabled={disabled} onClick={onOpen}>
<FiUploadCloud /> Import Variables
</Button>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import { useAddVariable } from "src/queries/useAddVariable";

import VariableForm, { type VariableBody } from "./VariableForm";

const AddVariableButton = () => {
type Props = {
readonly disabled: boolean;
};

const AddVariableButton = ({ disabled }: Props) => {
const { onClose, onOpen, open } = useDisclosure();
const { addVariable, error, isPending, setError } = useAddVariable({
onSuccessConfirm: onClose,
Expand All @@ -44,7 +48,7 @@ const AddVariableButton = () => {
return (
<>
<Toaster />
<Button colorPalette="blue" onClick={onOpen}>
<Button colorPalette="blue" disabled={disabled} onClick={onOpen}>
<FiPlusCircle /> Add Variable
</Button>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,20 @@ import { useDeleteVariable } from "src/queries/useDeleteVariable";

type Props = {
readonly deleteKey: string;
readonly disabled: boolean;
};

const DeleteVariableButton = ({ deleteKey: variableKey }: Props) => {
const DeleteVariableButton = ({ deleteKey: variableKey, disabled }: Props) => {
const { onClose, onOpen, open } = useDisclosure();
const { isPending, mutate } = useDeleteVariable({
onSuccessConfirm: onClose,
});

const renderDeleteButton = () => (
<Button
colorPalette="red"
loading={isPending}
onClick={() => {
mutate({
variableKey,
});
}}
>
<FiTrash /> Delete
</Button>
);

return (
<>
<ActionButton
actionName="Delete Variable"
disabled={disabled}
icon={<FiTrash />}
onClick={() => {
onOpen();
Expand All @@ -70,9 +58,24 @@ const DeleteVariableButton = ({ deleteKey: variableKey }: Props) => {
<Dialog.CloseTrigger />

<Dialog.Body width="full">
<Text>Are you sure you want to delete the variable key: `{variableKey}`?</Text>
<Text color="gray.solid" fontSize="md" fontWeight="semibold" mb={4}>
You are about to delete variable with key <strong>{variableKey}</strong>.
<br />
This action is permanent and cannot be undone.{" "}
<strong>Are you sure you want to proceed?</strong>
</Text>
<Flex justifyContent="end" mt={3}>
{renderDeleteButton()}
<Button
colorPalette="red"
loading={isPending}
onClick={() => {
mutate({
variableKey,
});
}}
>
<FiTrash /> <Text fontWeight="bold">Yes, Delete</Text>
</Button>
</Flex>
</Dialog.Body>
</Dialog.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ import type { VariableBody } from "./VariableForm";
import VariableForm from "./VariableForm";

type Props = {
readonly disabled: boolean;
readonly variable: VariableResponse;
};

const EditVariableButton = ({ variable }: Props) => {
const EditVariableButton = ({ disabled, variable }: Props) => {
const { onClose, onOpen, open } = useDisclosure();
const initialVariableValue: VariableBody = {
description: variable.description ?? "",
Expand All @@ -51,6 +52,7 @@ const EditVariableButton = ({ variable }: Props) => {
<>
<ActionButton
actionName="Edit Variable"
disabled={disabled}
icon={<FiEdit />}
onClick={() => {
onOpen();
Expand Down
31 changes: 18 additions & 13 deletions airflow/ui/src/pages/Variables/Variables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { Box, Flex, HStack, Spacer, VStack } from "@chakra-ui/react";
import type { ColumnDef } from "@tanstack/react-table";
import { useMemo, useState } from "react";
import { FiShare, FiTrash2 } from "react-icons/fi";
import { FiShare } from "react-icons/fi";
import { useSearchParams } from "react-router-dom";

import { useVariableServiceGetVariables } from "openapi/queries";
Expand All @@ -35,6 +35,7 @@ import { Checkbox } from "src/components/ui/Checkbox";
import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams";
import { TrimText } from "src/utils/TrimText";

import DeleteVariablesButton from "./DeleteVariablesButton";
import ImportVariablesButton from "./ImportVariablesButton";
import AddVariableButton from "./ManageVariable/AddVariableButton";
import DeleteVariableButton from "./ManageVariable/DeleteVariableButton";
Expand All @@ -51,12 +52,17 @@ const getColumns = ({
cell: ({ row }) => (
<Checkbox
checked={selectedRows.get(row.original.key)}
colorPalette="blue"
onCheckedChange={(event) => onRowSelect(row.original.key, Boolean(event.checked))}
/>
),
enableSorting: false,
header: () => (
<Checkbox checked={allRowsSelected} onCheckedChange={(event) => onSelectAll(Boolean(event.checked))} />
<Checkbox
checked={allRowsSelected}
colorPalette="blue"
onCheckedChange={(event) => onSelectAll(Boolean(event.checked))}
/>
),
meta: {
skeletonWidth: 10,
Expand Down Expand Up @@ -85,8 +91,8 @@ const getColumns = ({
accessorKey: "actions",
cell: ({ row: { original } }) => (
<Flex justifyContent="end">
<EditVariableButton variable={original} />
<DeleteVariableButton deleteKey={original.key} />
<EditVariableButton disabled={selectedRows.size > 0} variable={original} />
<DeleteVariableButton deleteKey={original.key} disabled={selectedRows.size > 0} />
</Flex>
),
enableSorting: false,
Expand All @@ -98,7 +104,9 @@ const getColumns = ({
];

export const Variables = () => {
const { setTableURLState, tableURLState } = useTableURLState();
const { setTableURLState, tableURLState } = useTableURLState({
pagination: { pageIndex: 0, pageSize: 30 },
}); // To make multiselection smooth
const [searchParams, setSearchParams] = useSearchParams();
const { NAME_PATTERN: NAME_PATTERN_PARAM }: SearchParamsKeysType = SearchParamsKeys;
const [variableKeyPattern, setVariableKeyPattern] = useState(
Expand Down Expand Up @@ -156,9 +164,9 @@ export const Variables = () => {
placeHolder="Search Keys"
/>
<HStack gap={4} mt={2}>
<ImportVariablesButton />
<ImportVariablesButton disabled={selectedRows.size > 0} />
<Spacer />
<AddVariableButton />
<AddVariableButton disabled={selectedRows.size > 0} />
</HStack>
</VStack>
<Box overflow="auto">
Expand All @@ -178,12 +186,9 @@ export const Variables = () => {
<ActionBar.Content>
<ActionBar.SelectionTrigger>{selectedRows.size} selected</ActionBar.SelectionTrigger>
<ActionBar.Separator />
{/* TODO: Implement the delete and export selected */}
<Tooltip content="Delete selected variable coming soon..">
<Button disabled size="sm" variant="outline">
<FiTrash2 />
Delete
</Button>
{/* TODO: Implement the export selected */}
<Tooltip content="Delete selected variables">
<DeleteVariablesButton clearSelections={clearSelections} deleteKeys={[...selectedRows.keys()]} />
</Tooltip>
<Tooltip content="Export selected variable coming soon..">
<Button disabled size="sm" variant="outline">
Expand Down
70 changes: 70 additions & 0 deletions airflow/ui/src/queries/useBulkDeleteVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";

import { useVariableServiceBulkVariables, useVariableServiceGetVariablesKey } from "openapi/queries";
import { toaster } from "src/components/ui";

type Props = {
readonly clearSelections: VoidFunction;
readonly onSuccessConfirm: VoidFunction;
};

export const useBulkDeleteVariables = ({ clearSelections, onSuccessConfirm }: Props) => {
const queryClient = useQueryClient();
const [error, setError] = useState<unknown>(undefined);

const onSuccess = async (responseData: { delete?: { errors: Array<unknown>; success: Array<string> } }) => {
await queryClient.invalidateQueries({
queryKey: [useVariableServiceGetVariablesKey],
});

if (responseData.delete) {
const { errors, success } = responseData.delete;

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) {
toaster.create({
description: `${success.length} variables deleted successfully. Keys: ${success.join(", ")}`,
title: "Delete Variables Request Successful",
type: "success",
});
clearSelections();
onSuccessConfirm();
}
}
};

const onError = (_error: unknown) => {
setError(_error);
};

const { isPending, mutate } = useVariableServiceBulkVariables({
onError,
onSuccess,
});

return { error, isPending, mutate };
};

0 comments on commit 2b1b399

Please sign in to comment.