From a562a8583fb92e11ee3885b1b9cd50d5271a3804 Mon Sep 17 00:00:00 2001 From: Shubham Raj <48172486+shubhamraj-git@users.noreply.github.com> Date: Sat, 4 Jan 2025 20:01:14 +0530 Subject: [PATCH] Add action bar for the bulk operation on variable list (#45392) * add action bar snippet * initial setup * separate * improve * add close trigger * fix * fix skeleton width * add tooltip for upcoming implementation * move to datatable --- .../components/DataTable/useRowSelection.ts | 87 +++++++++++++++++++ .../components/ui/ActionBar/BarContent.tsx | 39 +++++++++ .../components/ui/ActionBar/CloseTrigger.tsx | 28 ++++++ .../ui/src/components/ui/ActionBar/index.ts | 28 ++++++ airflow/ui/src/pages/Variables/Variables.tsx | 76 ++++++++++++++-- 5 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 airflow/ui/src/components/DataTable/useRowSelection.ts create mode 100644 airflow/ui/src/components/ui/ActionBar/BarContent.tsx create mode 100644 airflow/ui/src/components/ui/ActionBar/CloseTrigger.tsx create mode 100644 airflow/ui/src/components/ui/ActionBar/index.ts diff --git a/airflow/ui/src/components/DataTable/useRowSelection.ts b/airflow/ui/src/components/DataTable/useRowSelection.ts new file mode 100644 index 0000000000000..debcdedaa542c --- /dev/null +++ b/airflow/ui/src/components/DataTable/useRowSelection.ts @@ -0,0 +1,87 @@ +/*! + * 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 { useState, useCallback, useMemo } from "react"; + +type UseRowSelectionProps = { + data?: Array; + getKey: (item: T) => string; +}; + +export type GetColumnsParams = { + allRowsSelected: boolean; + onRowSelect: (key: string, isChecked: boolean) => void; + onSelectAll: (isChecked: boolean) => void; + selectedRows: Map; +}; + +export const useRowSelection = ({ data = [], getKey }: UseRowSelectionProps) => { + const [selectedRows, setSelectedRows] = useState>(new Map()); + + const handleRowSelect = useCallback((key: string, isChecked: boolean) => { + setSelectedRows((prev) => { + const isAlreadySelected = prev.has(key); + + if (isChecked && !isAlreadySelected) { + return new Map(prev).set(key, true); + } else if (!isChecked && isAlreadySelected) { + const newMap = new Map(prev); + + newMap.delete(key); + + return newMap; + } + + return prev; + }); + }, []); + + const handleSelectAll = useCallback( + (isChecked: boolean) => { + setSelectedRows((prev) => { + const newMap = new Map(prev); + + if (isChecked) { + data.forEach((item) => newMap.set(getKey(item), true)); + } else { + data.forEach((item) => newMap.delete(getKey(item))); + } + + return newMap; + }); + }, + [data, getKey], + ); + + const allRowsSelected = useMemo( + () => data.length > 0 && data.every((item) => selectedRows.has(getKey(item))), + [data, selectedRows, getKey], + ); + + const clearSelections = useCallback(() => { + setSelectedRows(new Map()); + }, []); + + return { + allRowsSelected, + clearSelections, + handleRowSelect, + handleSelectAll, + selectedRows, + }; +}; diff --git a/airflow/ui/src/components/ui/ActionBar/BarContent.tsx b/airflow/ui/src/components/ui/ActionBar/BarContent.tsx new file mode 100644 index 0000000000000..286d73e419ce8 --- /dev/null +++ b/airflow/ui/src/components/ui/ActionBar/BarContent.tsx @@ -0,0 +1,39 @@ +/*! + * 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 { ActionBar, Portal } from "@chakra-ui/react"; +import { forwardRef } from "react"; + +type ActionBarContentProps = { + portalled?: boolean; + portalRef?: React.RefObject; +} & ActionBar.ContentProps; + +export const Content = forwardRef((props, ref) => { + const { children, portalled = true, portalRef, ...rest } = props; + + return ( + + + + {children} + + + + ); +}); diff --git a/airflow/ui/src/components/ui/ActionBar/CloseTrigger.tsx b/airflow/ui/src/components/ui/ActionBar/CloseTrigger.tsx new file mode 100644 index 0000000000000..1dab9343e51d3 --- /dev/null +++ b/airflow/ui/src/components/ui/ActionBar/CloseTrigger.tsx @@ -0,0 +1,28 @@ +/*! + * 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 { ActionBar } from "@chakra-ui/react"; +import { forwardRef } from "react"; + +import { CloseButton } from "./../CloseButton"; + +export const CloseTrigger = forwardRef((props, ref) => ( + + + +)); diff --git a/airflow/ui/src/components/ui/ActionBar/index.ts b/airflow/ui/src/components/ui/ActionBar/index.ts new file mode 100644 index 0000000000000..f8cff1157ddb8 --- /dev/null +++ b/airflow/ui/src/components/ui/ActionBar/index.ts @@ -0,0 +1,28 @@ +/*! + * 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 { ActionBar as ChakraActionBar } from "@chakra-ui/react"; + +import { Content } from "./BarContent"; +import { CloseTrigger } from "./CloseTrigger"; + +export const ActionBar = { + ...ChakraActionBar, + CloseTrigger, + Content, +}; diff --git a/airflow/ui/src/pages/Variables/Variables.tsx b/airflow/ui/src/pages/Variables/Variables.tsx index 8436fbc364e6c..6f46deca34374 100644 --- a/airflow/ui/src/pages/Variables/Variables.tsx +++ b/airflow/ui/src/pages/Variables/Variables.tsx @@ -18,15 +18,20 @@ */ import { Box, Flex, HStack, Spacer, VStack } from "@chakra-ui/react"; import type { ColumnDef } from "@tanstack/react-table"; -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { FiShare, FiTrash2 } from "react-icons/fi"; import { useSearchParams } from "react-router-dom"; import { useVariableServiceGetVariables } from "openapi/queries"; import type { VariableResponse } from "openapi/requests/types.gen"; import { DataTable } from "src/components/DataTable"; +import { useRowSelection, type GetColumnsParams } from "src/components/DataTable/useRowSelection"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { ErrorAlert } from "src/components/ErrorAlert"; import { SearchBar } from "src/components/SearchBar"; +import { Button, Tooltip } from "src/components/ui"; +import { ActionBar } from "src/components/ui/ActionBar"; +import { Checkbox } from "src/components/ui/Checkbox"; import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams"; import ImportVariablesButton from "./ImportVariablesButton"; @@ -34,7 +39,28 @@ import AddVariableButton from "./ManageVariable/AddVariableButton"; import DeleteVariableButton from "./ManageVariable/DeleteVariableButton"; import EditVariableButton from "./ManageVariable/EditVariableButton"; -const columns: Array> = [ +const getColumns = ({ + allRowsSelected, + onRowSelect, + onSelectAll, + selectedRows, +}: GetColumnsParams): Array> => [ + { + accessorKey: "select", + cell: ({ row }) => ( + onRowSelect(row.original.key, Boolean(event.checked))} + /> + ), + enableSorting: false, + header: () => ( + onSelectAll(Boolean(event.checked))} /> + ), + meta: { + skeletonWidth: 10, + }, + }, { accessorKey: "key", header: "Key", @@ -61,6 +87,9 @@ const columns: Array> = [ ), enableSorting: false, header: "", + meta: { + skeletonWidth: 10, + }, }, ]; @@ -79,9 +108,26 @@ export const Variables = () => { limit: pagination.pageSize, offset: pagination.pageIndex * pagination.pageSize, orderBy, - variableKeyPattern: Boolean(variableKeyPattern) ? `${variableKeyPattern}` : undefined, + variableKeyPattern: variableKeyPattern ?? undefined, }); + const { allRowsSelected, clearSelections, handleRowSelect, handleSelectAll, selectedRows } = + useRowSelection({ + data: data?.variables, + getKey: (variable) => variable.key, + }); + + const columns = useMemo( + () => + getColumns({ + allRowsSelected, + onRowSelect: handleRowSelect, + onSelectAll: handleSelectAll, + selectedRows, + }), + [allRowsSelected, handleRowSelect, handleSelectAll, selectedRows], + ); + const handleSearchChange = (value: string) => { if (value) { searchParams.set(NAME_PATTERN_PARAM, value); @@ -114,16 +160,36 @@ export const Variables = () => { } initialState={tableURLState} isFetching={isFetching} isLoading={isLoading} modelName="Variable" onStateChange={setTableURLState} - total={data ? data.total_entries : 0} + total={data?.total_entries ?? 0} /> + + + {selectedRows.size} selected + + {/* TODO: Implement the delete and export selected */} + + + + + + + + + ); };