Skip to content

Commit

Permalink
Add action bar for the bulk operation on variable list (#45392)
Browse files Browse the repository at this point in the history
* add action bar snippet

* initial setup

* separate

* improve

* add close trigger

* fix

* fix skeleton width

* add tooltip for upcoming implementation

* move to datatable
  • Loading branch information
shubhamraj-git authored Jan 4, 2025
1 parent 013401b commit a562a85
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 5 deletions.
87 changes: 87 additions & 0 deletions airflow/ui/src/components/DataTable/useRowSelection.ts
Original file line number Diff line number Diff line change
@@ -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<T> = {
data?: Array<T>;
getKey: (item: T) => string;
};

export type GetColumnsParams = {
allRowsSelected: boolean;
onRowSelect: (key: string, isChecked: boolean) => void;
onSelectAll: (isChecked: boolean) => void;
selectedRows: Map<string, boolean>;
};

export const useRowSelection = <T>({ data = [], getKey }: UseRowSelectionProps<T>) => {
const [selectedRows, setSelectedRows] = useState<Map<string, boolean>>(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,
};
};
39 changes: 39 additions & 0 deletions airflow/ui/src/components/ui/ActionBar/BarContent.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>;
} & ActionBar.ContentProps;

export const Content = forwardRef<HTMLDivElement, ActionBarContentProps>((props, ref) => {
const { children, portalled = true, portalRef, ...rest } = props;

return (
<Portal container={portalRef} disabled={!portalled}>
<ActionBar.Positioner>
<ActionBar.Content ref={ref} {...rest} asChild={false}>
{children}
</ActionBar.Content>
</ActionBar.Positioner>
</Portal>
);
});
28 changes: 28 additions & 0 deletions airflow/ui/src/components/ui/ActionBar/CloseTrigger.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement, ActionBar.CloseTriggerProps>((props, ref) => (
<ActionBar.CloseTrigger {...props} asChild ref={ref}>
<CloseButton size="sm" />
</ActionBar.CloseTrigger>
));
28 changes: 28 additions & 0 deletions airflow/ui/src/components/ui/ActionBar/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
76 changes: 71 additions & 5 deletions airflow/ui/src/pages/Variables/Variables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,49 @@
*/
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";
import AddVariableButton from "./ManageVariable/AddVariableButton";
import DeleteVariableButton from "./ManageVariable/DeleteVariableButton";
import EditVariableButton from "./ManageVariable/EditVariableButton";

const columns: Array<ColumnDef<VariableResponse>> = [
const getColumns = ({
allRowsSelected,
onRowSelect,
onSelectAll,
selectedRows,
}: GetColumnsParams): Array<ColumnDef<VariableResponse>> => [
{
accessorKey: "select",
cell: ({ row }) => (
<Checkbox
checked={selectedRows.get(row.original.key)}
onCheckedChange={(event) => onRowSelect(row.original.key, Boolean(event.checked))}
/>
),
enableSorting: false,
header: () => (
<Checkbox checked={allRowsSelected} onCheckedChange={(event) => onSelectAll(Boolean(event.checked))} />
),
meta: {
skeletonWidth: 10,
},
},
{
accessorKey: "key",
header: "Key",
Expand All @@ -61,6 +87,9 @@ const columns: Array<ColumnDef<VariableResponse>> = [
),
enableSorting: false,
header: "",
meta: {
skeletonWidth: 10,
},
},
];

Expand All @@ -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);
Expand Down Expand Up @@ -114,16 +160,36 @@ export const Variables = () => {
<Box overflow="auto">
<DataTable
columns={columns}
data={data ? data.variables : []}
data={data?.variables ?? []}
errorMessage={<ErrorAlert error={error} />}
initialState={tableURLState}
isFetching={isFetching}
isLoading={isLoading}
modelName="Variable"
onStateChange={setTableURLState}
total={data ? data.total_entries : 0}
total={data?.total_entries ?? 0}
/>
</Box>
<ActionBar.Root closeOnInteractOutside={false} open={Boolean(selectedRows.size)}>
<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>
</Tooltip>
<Tooltip content="Export selected variable coming soon..">
<Button disabled size="sm" variant="outline">
<FiShare />
Export
</Button>
</Tooltip>
<ActionBar.CloseTrigger onClick={clearSelections} />
</ActionBar.Content>
</ActionBar.Root>
</>
);
};

0 comments on commit a562a85

Please sign in to comment.