Skip to content

Commit

Permalink
feat(ui/tokens): delete multiple tokens (#2424)
Browse files Browse the repository at this point in the history
* feat(ui/tokens): delete multiple tokens

Refs: #2275, FLI-215

* - added delete button per spec
- implemented the logic for multiple tokens deletion
- some ui changes and cleanup

* feat: updates for delete multiple tokens PR

* cleanup selected rows after token deletions

* chore: use existing tokensVersion (#2)

---------

Co-authored-by: Mark Phelps <[email protected]>
  • Loading branch information
erka and markphelps authored Nov 22, 2023
1 parent b054623 commit 8a72265
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 54 deletions.
61 changes: 26 additions & 35 deletions ui/src/app/tokens/Tokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import ShowTokenPanel from '~/components/tokens/ShowTokenPanel';
import TokenForm from '~/components/tokens/TokenForm';
import TokenTable from '~/components/tokens/TokenTable';
import Well from '~/components/Well';
import { deleteToken, listAuthMethods, listTokens } from '~/data/api';
import { deleteTokens, listAuthMethods, listTokens } from '~/data/api';
import { useError } from '~/data/hooks/error';
import { IAuthMethod, IAuthMethodList } from '~/types/Auth';
import {
Expand All @@ -19,7 +19,6 @@ import {
} from '~/types/auth/Token';

export default function Tokens() {
// const checkbox = useRef();
const [tokenAuthEnabled, setTokenAuthEnabled] = useState<boolean>(false);

const [tokens, setTokens] = useState<IAuthToken[]>([]);
Expand All @@ -37,7 +36,9 @@ export default function Tokens() {

const [showDeleteTokenModal, setShowDeleteTokenModal] =
useState<boolean>(false);
const [deletingToken, setDeletingToken] = useState<IAuthToken | null>(null);
const [deletingTokens, setDeletingTokens] = useState<IAuthToken[] | null>(
null
);

const tokenFormRef = useRef(null);

Expand Down Expand Up @@ -88,28 +89,6 @@ export default function Tokens() {
checkTokenAuthEnabled();
}, [checkTokenAuthEnabled]);

// const [checked, setChecked] = useState(false);
// const [indeterminate, setIndeterminate] = useState(false);
// const [selectedTokens, setSelectedTokens] = useState<IAuthenticationToken[]>(
// []
// );

// useLayoutEffect(() => {
// const isIndeterminate =
// selectedTokens.length > 0 && selectedTokens.length < tokens.length;
// setChecked(selectedTokens.length === tokens.length);
// setIndeterminate(isIndeterminate);
// if (checkbox && checkbox.current) {
// checkbox.current.indeterminate = isIndeterminate;
// }
// }, [selectedTokens]);

// const toggleAll = () => {
// setSelectedTokens(checked || indeterminate ? [] : tokens);
// setChecked(!checked && !indeterminate);
// setIndeterminate(false);
// };

return (
<>
{/* token create form */}
Expand All @@ -134,17 +113,28 @@ export default function Tokens() {
<Modal open={showDeleteTokenModal} setOpen={setShowDeleteTokenModal}>
<DeletePanel
panelMessage={
<>
Are you sure you want to delete the token{' '}
<span className="text-violet-500 font-medium">
{deletingToken?.name}
</span>
? This action cannot be undone.
</>
deletingTokens && deletingTokens.length === 1 ? (
<>
Are you sure you want to delete the token{' '}
<span className="text-violet-500 font-medium">
{deletingTokens[0].name}
</span>
? This action cannot be undone.
</>
) : (
<>
Are you sure you want to delete the selected tokens? This action
cannot be undone.
</>
)
}
panelType="Token"
panelType="Tokens"
setOpen={setShowDeleteTokenModal}
handleDelete={() => deleteToken(deletingToken?.id ?? '')} // TODO: Determine impact of blank ID param
handleDelete={() =>
deleteTokens(deletingTokens?.map((t) => t.id) || []).then(() => {
incrementTokensVersion();
})
}
onSuccess={() => {
incrementTokensVersion();
}}
Expand Down Expand Up @@ -186,8 +176,9 @@ export default function Tokens() {
{tokens && tokens.length > 0 ? (
<TokenTable
tokens={tokens}
setDeletingToken={setDeletingToken}
setDeletingTokens={setDeletingTokens}
setShowDeleteTokenModal={setShowDeleteTokenModal}
tokensVersion={tokensVersion}
/>
) : (
<EmptyState
Expand Down
122 changes: 105 additions & 17 deletions ui/src/components/tokens/TokenTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,41 @@ import {
getFilteredRowModel,
getSortedRowModel,
Row,
RowSelectionState,
SortingState,
Table,
useReactTable
} from '@tanstack/react-table';
import { format, parseISO } from 'date-fns';
import { useState } from 'react';
import React, { HTMLProps, useEffect, useState } from 'react';
import Button from '~/components/forms/buttons/Button';
import Searchbox from '~/components/Searchbox';
import { IAuthToken } from '~/types/auth/Token';

type TokenRowActionsProps = {
row: Row<IAuthToken>;
setDeletingToken: (token: IAuthToken) => void;
setDeletingTokens: (tokens: IAuthToken[]) => void;
setShowDeleteTokenModal: (show: boolean) => void;
};

function TokenRowActions(props: TokenRowActionsProps) {
const { row, setDeletingToken, setShowDeleteTokenModal } = props;
const { row, setDeletingTokens, setShowDeleteTokenModal } = props;

let className = 'text-violet-600 hover:text-violet-900';
if (row.getIsSelected()) {
className = 'text-gray-400 hover:cursor-not-allowed';
}

return (
<a
href="#"
className="text-violet-600 hover:text-violet-900"
className={className}
onClick={(e) => {
e.preventDefault();
setDeletingToken(row.original);
setShowDeleteTokenModal(true);
if (!row.getIsSelected()) {
setDeletingTokens([row.original]);
setShowDeleteTokenModal(true);
}
}}
>
Delete
Expand All @@ -38,26 +49,101 @@ function TokenRowActions(props: TokenRowActionsProps) {
);
}

function IndeterminateCheckbox({
indeterminate,
className = '',
...rest
}: { indeterminate?: boolean } & HTMLProps<HTMLInputElement>) {
const ref = React.useRef<HTMLInputElement>(null!);

useEffect(() => {
if (typeof indeterminate === 'boolean') {
ref.current.indeterminate = !rest.checked && indeterminate;
}
}, [ref, indeterminate, rest.checked]);

return (
<input
type="checkbox"
ref={ref}
className={
className +
' text-purple-600 bg-gray-100 border-gray-300 h-4 w-4 cursor-pointer rounded focus:ring-purple-500'
}
{...rest}
/>
);
}

type TokenTableProps = {
tokens: IAuthToken[];
setDeletingToken: (token: IAuthToken) => void;
setDeletingTokens: (tokens: IAuthToken[]) => void;
setShowDeleteTokenModal: (show: boolean) => void;
tokensVersion: number;
};

export default function TokenTable(props: TokenTableProps) {
const { tokens, setDeletingToken, setShowDeleteTokenModal } = props;

const { tokens, setDeletingTokens, setShowDeleteTokenModal, tokensVersion } =
props;
const searchThreshold = 10;

const [sorting, setSorting] = useState<SortingState>([]);

const [filter, setFilter] = useState<string>('');

const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const columnHelper = createColumnHelper<IAuthToken>();

const deleteMultipleTokens = (table: Table<IAuthToken>) => {
setDeletingTokens(table.getSelectedRowModel().rows.map((r) => r.original));
setShowDeleteTokenModal(true);
};

useEffect(() => {
setRowSelection({});
}, [tokensVersion]);

const columns = [
columnHelper.display({
id: 'select',
header: ({ table }) => (
<IndeterminateCheckbox
{...{
checked: table.getIsAllRowsSelected(),
indeterminate: table.getIsSomeRowsSelected(),
onChange: table.getToggleAllRowsSelectedHandler()
}}
/>
),
cell: ({ row }) => (
<IndeterminateCheckbox
{...{
checked: row.getIsSelected(),
disabled: !row.getCanSelect(),
indeterminate: row.getIsSomeSelected(),
onChange: row.getToggleSelectedHandler()
}}
/>
),
meta: {
className: 'whitespace-nowrap py-4 pl-3 pr-4 text-left'
}
}),
columnHelper.accessor('name', {
header: 'Name',
header: ({ table }) => {
if (table.getSelectedRowModel().rows.length > 0) {
return (
<Button
onClick={(e) => {
e.stopPropagation();
deleteMultipleTokens(table);
}}
title="Delete Selected Token(s)"
>
Delete
</Button>
);
}
return 'Name';
},
cell: (info) => info.getValue(),
meta: {
className:
Expand Down Expand Up @@ -107,11 +193,11 @@ export default function TokenTable(props: TokenTableProps) {
),
columnHelper.display({
id: 'actions',
cell: (props) => (
cell: ({ row }) => (
<TokenRowActions
// eslint-disable-next-line react/prop-types
row={props.row}
setDeletingToken={setDeletingToken}
row={row}
setDeletingTokens={setDeletingTokens}
setShowDeleteTokenModal={setShowDeleteTokenModal}
/>
),
Expand All @@ -127,13 +213,15 @@ export default function TokenTable(props: TokenTableProps) {
columns,
state: {
globalFilter: filter,
sorting
sorting,
rowSelection
},
globalFilterFn: 'includesString',
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel()
getFilteredRowModel: getFilteredRowModel(),
onRowSelectionChange: setRowSelection
});

return (
Expand Down
4 changes: 2 additions & 2 deletions ui/src/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ export async function createToken(values: IAuthTokenBase) {
return post('/method/token', values, authURL);
}

export async function deleteToken(id: string) {
return del(`/tokens/${id}`, authURL);
export async function deleteTokens(ids: string[]) {
return Promise.all(ids.map((id) => del(`/tokens/${id}`, authURL)));
}

export async function listTokens(method = 'METHOD_TOKEN') {
Expand Down

0 comments on commit 8a72265

Please sign in to comment.