mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Add setup-key improvements (#420)
- Add support to key deletion - Add custom and unlimited expiration
This commit is contained in:
@@ -47,6 +47,14 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</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")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
export default function EmptyRow() {
|
||||
return <div className={"text-nb-gray-600"}>-</div>;
|
||||
import { cn } from "@utils/helpers";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function EmptyRow({ className }: Readonly<Props>) {
|
||||
return <div className={cn("text-nb-gray-600", className)}>-</div>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Button from "@components/Button";
|
||||
import { notify } from "@components/Notification";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { Trash2, Undo2Icon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
@@ -10,16 +10,26 @@ import { SetupKey } from "@/interfaces/SetupKey";
|
||||
type Props = {
|
||||
setupKey: SetupKey;
|
||||
};
|
||||
export default function SetupKeyActionCell({ setupKey }: Props) {
|
||||
export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
|
||||
const { confirm } = useDialog();
|
||||
const deleteRequest = useApiCall<SetupKey>("/setup-keys/" + setupKey.id);
|
||||
const request = useApiCall<SetupKey>("/setup-keys/" + setupKey.id);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
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({
|
||||
title: setupKey?.name || "Setup Key",
|
||||
description: "Setup key was successfully revoked",
|
||||
promise: deleteRequest
|
||||
promise: request
|
||||
.put({
|
||||
name: setupKey?.name || "Setup Key",
|
||||
type: setupKey.type,
|
||||
@@ -37,17 +47,26 @@ export default function SetupKeyActionCell({ setupKey }: Props) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
const handleDelete = async () => {
|
||||
const choice = await confirm({
|
||||
title: `Revoke '${setupKey?.name || "Setup Key"}'?`,
|
||||
title: `Delete '${setupKey?.name || "Setup Key"}'?`,
|
||||
description:
|
||||
"Are you sure you want to revoke the setup key? This action cannot be undone.",
|
||||
confirmText: "Revoke",
|
||||
"Are you sure you want to delete the setup key? This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
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 (
|
||||
@@ -55,12 +74,16 @@ export default function SetupKeyActionCell({ setupKey }: Props) {
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"sm"}
|
||||
onClick={handleConfirm}
|
||||
onClick={handleRevoke}
|
||||
disabled={setupKey.revoked || !setupKey.valid}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
<Undo2Icon size={16} />
|
||||
Revoke
|
||||
</Button>
|
||||
<Button variant={"danger-outline"} size={"sm"} onClick={handleDelete}>
|
||||
<Trash2 size={16} />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import GroupsRow from "@/modules/common-table-rows/GroupsRow";
|
||||
type Props = {
|
||||
setupKey: SetupKey;
|
||||
};
|
||||
export default function SetupKeyGroupsCell({ setupKey }: Props) {
|
||||
export default function SetupKeyGroupsCell({ setupKey }: Readonly<Props>) {
|
||||
const [modal, setModal] = useState(false);
|
||||
const request = useApiCall<SetupKey>("/setup-keys/" + setupKey.id);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
@@ -5,7 +5,7 @@ type Props = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export default function SetupKeyKeyCell({ text }: Props) {
|
||||
export default function SetupKeyKeyCell({ text }: Readonly<Props>) {
|
||||
return (
|
||||
<div className={"flex"}>
|
||||
<Badge variant={"gray"} className={"text-xs font-mono"}>
|
||||
|
||||
@@ -42,7 +42,11 @@ type Props = {
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
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 [setupKey, setSetupKey] = useState<SetupKey>();
|
||||
const [, copy] = useCopyToClipboard(setupKey?.key);
|
||||
@@ -131,7 +135,7 @@ type ModalProps = {
|
||||
onSuccess?: (setupKey: SetupKey) => void;
|
||||
};
|
||||
|
||||
export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
export function SetupKeyModalContent({ onSuccess }: Readonly<ModalProps>) {
|
||||
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys", true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
@@ -149,18 +153,10 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
return reusable ? "Unlimited" : "1";
|
||||
}, [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 trimmedName = trim(name);
|
||||
return trimmedName.length === 0 || expiresInError.length > 0;
|
||||
}, [name, expiresInError]);
|
||||
return trimmedName.length === 0;
|
||||
}, [name]);
|
||||
|
||||
const submit = () => {
|
||||
if (!selectedGroups) return;
|
||||
@@ -174,7 +170,7 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
.post({
|
||||
name,
|
||||
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,
|
||||
auto_groups: groups.map((group) => group.id),
|
||||
usage_limit: reusable ? parseInt(usageLimit) : 1,
|
||||
@@ -253,15 +249,16 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
<div className={"flex justify-between"}>
|
||||
<div>
|
||||
<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>
|
||||
<Input
|
||||
maxWidthClass={"max-w-[200px]"}
|
||||
placeholder={"7"}
|
||||
maxWidthClass={"max-w-[202px]"}
|
||||
placeholder={"Unlimited"}
|
||||
min={1}
|
||||
max={365}
|
||||
value={expiresIn}
|
||||
error={expiresInError}
|
||||
errorTooltip={true}
|
||||
type={"number"}
|
||||
data-cy={"setup-key-expire-in-days"}
|
||||
|
||||
@@ -5,7 +5,11 @@ type Props = {
|
||||
valid: boolean;
|
||||
secret?: string;
|
||||
};
|
||||
export default function SetupKeyNameCell({ name, valid, secret }: Props) {
|
||||
export default function SetupKeyNameCell({
|
||||
name,
|
||||
valid,
|
||||
secret,
|
||||
}: Readonly<Props>) {
|
||||
return (
|
||||
<ActiveInactiveRow
|
||||
active={valid || false}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Repeat1 } from "lucide-react";
|
||||
type Props = {
|
||||
reusable: boolean;
|
||||
};
|
||||
export default function SetupKeyTypeCell({ reusable }: Props) {
|
||||
export default function SetupKeyTypeCell({ reusable }: Readonly<Props>) {
|
||||
return (
|
||||
<div className={"flex"}>
|
||||
<Badge className={"text-xs"} variant={"gray"}>
|
||||
|
||||
@@ -8,6 +8,7 @@ import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
|
||||
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
|
||||
import GetStartedTest from "@components/ui/GetStartedTest";
|
||||
import { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import dayjs from "dayjs";
|
||||
import { ExternalLinkIcon, PlusCircle } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
@@ -15,6 +16,7 @@ import { useSWRConfig } from "swr";
|
||||
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { SetupKey } from "@/interfaces/SetupKey";
|
||||
import EmptyRow from "@/modules/common-table-rows/EmptyRow";
|
||||
import ExpirationDateRow from "@/modules/common-table-rows/ExpirationDateRow";
|
||||
import LastTimeRow from "@/modules/common-table-rows/LastTimeRow";
|
||||
import SetupKeyActionCell from "@/modules/setup-keys/SetupKeyActionCell";
|
||||
@@ -94,7 +96,15 @@ export const SetupKeysTableColumns: ColumnDef<SetupKey>[] = [
|
||||
header: ({ column }) => {
|
||||
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,
|
||||
isLoading,
|
||||
headingTarget,
|
||||
}: Props) {
|
||||
}: Readonly<Props>) {
|
||||
const { mutate } = useSWRConfig();
|
||||
const path = usePathname();
|
||||
|
||||
@@ -216,6 +226,20 @@ export default function SetupKeysTable({
|
||||
{(table) => (
|
||||
<>
|
||||
<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
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
@@ -233,16 +257,16 @@ export default function SetupKeysTable({
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("valid")?.setFilterValue("");
|
||||
table.getColumn("valid")?.setFilterValue(false);
|
||||
}}
|
||||
disabled={setupKeys?.length == 0}
|
||||
variant={
|
||||
table.getColumn("valid")?.getFilterValue() != true
|
||||
table.getColumn("valid")?.getFilterValue() == false
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
All
|
||||
Expired
|
||||
</ButtonGroup.Button>
|
||||
</ButtonGroup>
|
||||
<DataTableRowsPerPage
|
||||
|
||||
Reference in New Issue
Block a user