Add setup-key improvements (#420)

- Add support to key deletion
- Add custom and unlimited expiration
This commit is contained in:
Maycon Santos
2024-11-01 16:04:43 +01:00
committed by GitHub
parent f8281c8057
commit cd3e75b640
9 changed files with 102 additions and 40 deletions

View File

@@ -47,6 +47,14 @@ export default function ActivityDescription({ event }: Props) {
</div> </div>
); );
if (event.activity_code == "setupkey.delete")
return (
<div className={"inline"}>
Setup-Key <Value> {m.name}</Value> with key <Value>{m.key}</Value> was
deleted
</div>
);
if (event.activity_code == "setupkey.add") if (event.activity_code == "setupkey.add")
return ( return (
<div className={"inline"}> <div className={"inline"}>

View File

@@ -1,3 +1,9 @@
export default function EmptyRow() { import { cn } from "@utils/helpers";
return <div className={"text-nb-gray-600"}>-</div>;
type Props = {
className?: string;
};
export default function EmptyRow({ className }: Readonly<Props>) {
return <div className={cn("text-nb-gray-600", className)}>-</div>;
} }

View File

@@ -1,7 +1,7 @@
import Button from "@components/Button"; import Button from "@components/Button";
import { notify } from "@components/Notification"; import { notify } from "@components/Notification";
import { useApiCall } from "@utils/api"; import { useApiCall } from "@utils/api";
import { Trash2 } from "lucide-react"; import { Trash2, Undo2Icon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { useSWRConfig } from "swr"; import { useSWRConfig } from "swr";
import { useDialog } from "@/contexts/DialogProvider"; import { useDialog } from "@/contexts/DialogProvider";
@@ -10,16 +10,26 @@ import { SetupKey } from "@/interfaces/SetupKey";
type Props = { type Props = {
setupKey: SetupKey; setupKey: SetupKey;
}; };
export default function SetupKeyActionCell({ setupKey }: Props) { export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
const { confirm } = useDialog(); const { confirm } = useDialog();
const deleteRequest = useApiCall<SetupKey>("/setup-keys/" + setupKey.id); const request = useApiCall<SetupKey>("/setup-keys/" + setupKey.id);
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const handleRevoke = async () => { const handleRevoke = async () => {
const choice = await confirm({
title: `Revoke '${setupKey?.name || "Setup Key"}'?`,
description:
"Are you sure you want to revoke the setup key? This action cannot be undone.",
confirmText: "Revoke",
cancelText: "Cancel",
type: "danger",
});
if (!choice) return;
notify({ notify({
title: setupKey?.name || "Setup Key", title: setupKey?.name || "Setup Key",
description: "Setup key was successfully revoked", description: "Setup key was successfully revoked",
promise: deleteRequest promise: request
.put({ .put({
name: setupKey?.name || "Setup Key", name: setupKey?.name || "Setup Key",
type: setupKey.type, type: setupKey.type,
@@ -37,17 +47,26 @@ export default function SetupKeyActionCell({ setupKey }: Props) {
}); });
}; };
const handleConfirm = async () => { const handleDelete = async () => {
const choice = await confirm({ const choice = await confirm({
title: `Revoke '${setupKey?.name || "Setup Key"}'?`, title: `Delete '${setupKey?.name || "Setup Key"}'?`,
description: description:
"Are you sure you want to revoke the setup key? This action cannot be undone.", "Are you sure you want to delete the setup key? This action cannot be undone.",
confirmText: "Revoke", confirmText: "Delete",
cancelText: "Cancel", cancelText: "Cancel",
type: "danger", type: "danger",
}); });
if (!choice) return; if (!choice) return;
handleRevoke().then();
notify({
title: setupKey?.name || "Setup Key",
description: "Setup key was successfully deleted",
promise: request.del().then(() => {
mutate("/setup-keys");
mutate("/groups");
}),
loadingMessage: "Deleting the setup key...",
});
}; };
return ( return (
@@ -55,12 +74,16 @@ export default function SetupKeyActionCell({ setupKey }: Props) {
<Button <Button
variant={"danger-outline"} variant={"danger-outline"}
size={"sm"} size={"sm"}
onClick={handleConfirm} onClick={handleRevoke}
disabled={setupKey.revoked || !setupKey.valid} disabled={setupKey.revoked || !setupKey.valid}
> >
<Trash2 size={16} /> <Undo2Icon size={16} />
Revoke Revoke
</Button> </Button>
<Button variant={"danger-outline"} size={"sm"} onClick={handleDelete}>
<Trash2 size={16} />
Delete
</Button>
</div> </div>
); );
} }

View File

@@ -9,7 +9,7 @@ import GroupsRow from "@/modules/common-table-rows/GroupsRow";
type Props = { type Props = {
setupKey: SetupKey; setupKey: SetupKey;
}; };
export default function SetupKeyGroupsCell({ setupKey }: Props) { export default function SetupKeyGroupsCell({ setupKey }: Readonly<Props>) {
const [modal, setModal] = useState(false); const [modal, setModal] = useState(false);
const request = useApiCall<SetupKey>("/setup-keys/" + setupKey.id); const request = useApiCall<SetupKey>("/setup-keys/" + setupKey.id);
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();

View File

@@ -5,7 +5,7 @@ type Props = {
text: string; text: string;
}; };
export default function SetupKeyKeyCell({ text }: Props) { export default function SetupKeyKeyCell({ text }: Readonly<Props>) {
return ( return (
<div className={"flex"}> <div className={"flex"}>
<Badge variant={"gray"} className={"text-xs font-mono"}> <Badge variant={"gray"} className={"text-xs font-mono"}>

View File

@@ -42,7 +42,11 @@ type Props = {
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
}; };
const copyMessage = "Setup-Key was copied to your clipboard!"; const copyMessage = "Setup-Key was copied to your clipboard!";
export default function SetupKeyModal({ children, open, setOpen }: Props) { export default function SetupKeyModal({
children,
open,
setOpen,
}: Readonly<Props>) {
const [successModal, setSuccessModal] = useState(false); const [successModal, setSuccessModal] = useState(false);
const [setupKey, setSetupKey] = useState<SetupKey>(); const [setupKey, setSetupKey] = useState<SetupKey>();
const [, copy] = useCopyToClipboard(setupKey?.key); const [, copy] = useCopyToClipboard(setupKey?.key);
@@ -131,7 +135,7 @@ type ModalProps = {
onSuccess?: (setupKey: SetupKey) => void; onSuccess?: (setupKey: SetupKey) => void;
}; };
export function SetupKeyModalContent({ onSuccess }: ModalProps) { export function SetupKeyModalContent({ onSuccess }: Readonly<ModalProps>) {
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys", true); const setupKeyRequest = useApiCall<SetupKey>("/setup-keys", true);
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
@@ -149,18 +153,10 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
return reusable ? "Unlimited" : "1"; return reusable ? "Unlimited" : "1";
}, [reusable]); }, [reusable]);
const expiresInError = useMemo(() => {
const expires = parseInt(expiresIn);
if (expires < 1 || expires > 365) {
return "Days should be between 1 and 365";
}
return "";
}, [expiresIn]);
const isDisabled = useMemo(() => { const isDisabled = useMemo(() => {
const trimmedName = trim(name); const trimmedName = trim(name);
return trimmedName.length === 0 || expiresInError.length > 0; return trimmedName.length === 0;
}, [name, expiresInError]); }, [name]);
const submit = () => { const submit = () => {
if (!selectedGroups) return; if (!selectedGroups) return;
@@ -174,7 +170,7 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
.post({ .post({
name, name,
type: reusable ? "reusable" : "one-off", type: reusable ? "reusable" : "one-off",
expires_in: parseInt(expiresIn ? expiresIn : "7") * 24 * 60 * 60, // Days to seconds, defaults to 7 days expires_in: parseInt(expiresIn || "0") * 24 * 60 * 60, // Days to seconds, defaults to 7 days
revoked: false, revoked: false,
auto_groups: groups.map((group) => group.id), auto_groups: groups.map((group) => group.id),
usage_limit: reusable ? parseInt(usageLimit) : 1, usage_limit: reusable ? parseInt(usageLimit) : 1,
@@ -253,15 +249,16 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
<div className={"flex justify-between"}> <div className={"flex justify-between"}>
<div> <div>
<Label>Expires in</Label> <Label>Expires in</Label>
<HelpText>Should be between 1 and 365 days.</HelpText> <HelpText>
Days until the key expires. <br />
Leave empty for no expiration.
</HelpText>
</div> </div>
<Input <Input
maxWidthClass={"max-w-[200px]"} maxWidthClass={"max-w-[202px]"}
placeholder={"7"} placeholder={"Unlimited"}
min={1} min={1}
max={365}
value={expiresIn} value={expiresIn}
error={expiresInError}
errorTooltip={true} errorTooltip={true}
type={"number"} type={"number"}
data-cy={"setup-key-expire-in-days"} data-cy={"setup-key-expire-in-days"}

View File

@@ -5,7 +5,11 @@ type Props = {
valid: boolean; valid: boolean;
secret?: string; secret?: string;
}; };
export default function SetupKeyNameCell({ name, valid, secret }: Props) { export default function SetupKeyNameCell({
name,
valid,
secret,
}: Readonly<Props>) {
return ( return (
<ActiveInactiveRow <ActiveInactiveRow
active={valid || false} active={valid || false}

View File

@@ -5,7 +5,7 @@ import { Repeat1 } from "lucide-react";
type Props = { type Props = {
reusable: boolean; reusable: boolean;
}; };
export default function SetupKeyTypeCell({ reusable }: Props) { export default function SetupKeyTypeCell({ reusable }: Readonly<Props>) {
return ( return (
<div className={"flex"}> <div className={"flex"}>
<Badge className={"text-xs"} variant={"gray"}> <Badge className={"text-xs"} variant={"gray"}>

View File

@@ -8,6 +8,7 @@ import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage"; import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
import GetStartedTest from "@components/ui/GetStartedTest"; import GetStartedTest from "@components/ui/GetStartedTest";
import { ColumnDef, SortingState } from "@tanstack/react-table"; import { ColumnDef, SortingState } from "@tanstack/react-table";
import dayjs from "dayjs";
import { ExternalLinkIcon, PlusCircle } from "lucide-react"; import { ExternalLinkIcon, PlusCircle } from "lucide-react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import React, { useState } from "react"; import React, { useState } from "react";
@@ -15,6 +16,7 @@ import { useSWRConfig } from "swr";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon"; import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
import { useLocalStorage } from "@/hooks/useLocalStorage"; import { useLocalStorage } from "@/hooks/useLocalStorage";
import { SetupKey } from "@/interfaces/SetupKey"; import { SetupKey } from "@/interfaces/SetupKey";
import EmptyRow from "@/modules/common-table-rows/EmptyRow";
import ExpirationDateRow from "@/modules/common-table-rows/ExpirationDateRow"; import ExpirationDateRow from "@/modules/common-table-rows/ExpirationDateRow";
import LastTimeRow from "@/modules/common-table-rows/LastTimeRow"; import LastTimeRow from "@/modules/common-table-rows/LastTimeRow";
import SetupKeyActionCell from "@/modules/setup-keys/SetupKeyActionCell"; import SetupKeyActionCell from "@/modules/setup-keys/SetupKeyActionCell";
@@ -94,7 +96,15 @@ export const SetupKeysTableColumns: ColumnDef<SetupKey>[] = [
header: ({ column }) => { header: ({ column }) => {
return <DataTableHeader column={column}>Expires</DataTableHeader>; return <DataTableHeader column={column}>Expires</DataTableHeader>;
}, },
cell: ({ row }) => <ExpirationDateRow date={row.original.expires} />, cell: ({ row }) => {
let expires = dayjs(row.original.expires);
let isNeverExpiring = expires?.year() == 1 || false;
return !isNeverExpiring ? (
<ExpirationDateRow date={row.original.expires} />
) : (
<EmptyRow className={"px-3"} />
);
},
}, },
{ {
@@ -116,7 +126,7 @@ export default function SetupKeysTable({
setupKeys, setupKeys,
isLoading, isLoading,
headingTarget, headingTarget,
}: Props) { }: Readonly<Props>) {
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const path = usePathname(); const path = usePathname();
@@ -216,6 +226,20 @@ export default function SetupKeysTable({
{(table) => ( {(table) => (
<> <>
<ButtonGroup disabled={setupKeys?.length == 0}> <ButtonGroup disabled={setupKeys?.length == 0}>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("valid")?.setFilterValue(undefined);
}}
disabled={setupKeys?.length == 0}
variant={
table.getColumn("valid")?.getFilterValue() == undefined
? "tertiary"
: "secondary"
}
>
All
</ButtonGroup.Button>
<ButtonGroup.Button <ButtonGroup.Button
onClick={() => { onClick={() => {
table.setPageIndex(0); table.setPageIndex(0);
@@ -233,16 +257,16 @@ export default function SetupKeysTable({
<ButtonGroup.Button <ButtonGroup.Button
onClick={() => { onClick={() => {
table.setPageIndex(0); table.setPageIndex(0);
table.getColumn("valid")?.setFilterValue(""); table.getColumn("valid")?.setFilterValue(false);
}} }}
disabled={setupKeys?.length == 0} disabled={setupKeys?.length == 0}
variant={ variant={
table.getColumn("valid")?.getFilterValue() != true table.getColumn("valid")?.getFilterValue() == false
? "tertiary" ? "tertiary"
: "secondary" : "secondary"
} }
> >
All Expired
</ButtonGroup.Button> </ButtonGroup.Button>
</ButtonGroup> </ButtonGroup>
<DataTableRowsPerPage <DataTableRowsPerPage