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>
|
</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"}>
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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"}>
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"}>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user