Files
netbird-dashboard/src/modules/setup-keys/SetupKeyModal.tsx
Maycon Santos cd3e75b640 Add setup-key improvements (#420)
- Add support to key deletion
- Add custom and unlimited expiration
2024-11-01 16:04:43 +01:00

336 lines
10 KiB
TypeScript

import Button from "@components/Button";
import Code from "@components/Code";
import FancyToggleSwitch from "@components/FancyToggleSwitch";
import HelpText from "@components/HelpText";
import InlineLink from "@components/InlineLink";
import { Input } from "@components/Input";
import { Label } from "@components/Label";
import {
Modal,
ModalClose,
ModalContent,
ModalFooter,
ModalTrigger,
} from "@components/modal/Modal";
import ModalHeader from "@components/modal/ModalHeader";
import { notify } from "@components/Notification";
import Paragraph from "@components/Paragraph";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import Separator from "@components/Separator";
import { IconRepeat } from "@tabler/icons-react";
import { useApiCall } from "@utils/api";
import { cn } from "@utils/helpers";
import { trim } from "lodash";
import {
AlarmClock,
CopyIcon,
ExternalLinkIcon,
MonitorSmartphoneIcon,
PlusCircle,
PowerOffIcon,
} from "lucide-react";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
import useCopyToClipboard from "@/hooks/useCopyToClipboard";
import { SetupKey } from "@/interfaces/SetupKey";
import useGroupHelper from "@/modules/groups/useGroupHelper";
type Props = {
children?: React.ReactNode;
open: boolean;
setOpen: (open: boolean) => void;
};
const copyMessage = "Setup-Key was copied to your clipboard!";
export default function SetupKeyModal({
children,
open,
setOpen,
}: Readonly<Props>) {
const [successModal, setSuccessModal] = useState(false);
const [setupKey, setSetupKey] = useState<SetupKey>();
const [, copy] = useCopyToClipboard(setupKey?.key);
const handleSuccess = (setupKey: SetupKey) => {
setSetupKey(setupKey);
setSuccessModal(true);
};
return (
<>
<Modal open={open} onOpenChange={setOpen} key={open ? 1 : 0}>
{children && <ModalTrigger asChild>{children}</ModalTrigger>}
<SetupKeyModalContent onSuccess={handleSuccess} />
</Modal>
<Modal
open={successModal}
onOpenChange={(open) => {
setSuccessModal(open);
setOpen(open);
}}
>
<ModalContent
onEscapeKeyDown={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
maxWidthClass={"max-w-md"}
className={"mt-20"}
showClose={false}
>
<div className={"pb-6 px-8"}>
<div className={"flex flex-col items-center justify-center gap-3"}>
<div>
<h2 className={"text-2xl text-center mb-2"}>
Setup key created successfully!
</h2>
<Paragraph className={"mt-0 text-sm text-center"}>
This key will not be shown again, so be sure to copy it and
store in a secure location.
</Paragraph>
</div>
</div>
</div>
<div
className={"px-8 pb-6"}
data-cy={"setup-key-copy-input"}
data-cy-setup-key-value={setupKey?.key || ""}
>
<Code message={copyMessage}>
<Code.Line>
{setupKey?.key || "Setup key could not be created..."}
</Code.Line>
</Code>
</div>
<ModalFooter className={"items-center"}>
<div className={"flex gap-3 w-full"}>
<ModalClose asChild={true}>
<Button
variant={"secondary"}
className={"w-full"}
tabIndex={-1}
data-cy={"setup-key-close"}
>
Close
</Button>
</ModalClose>
<Button
variant={"primary"}
className={"w-full"}
data-cy={"setup-key-copy"}
onClick={() => copy(copyMessage)}
>
<CopyIcon size={14} />
Copy to clipboard
</Button>
</div>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
}
type ModalProps = {
onSuccess?: (setupKey: SetupKey) => void;
};
export function SetupKeyModalContent({ onSuccess }: Readonly<ModalProps>) {
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys", true);
const { mutate } = useSWRConfig();
const [name, setName] = useState("");
const [reusable, setReusable] = useState(false);
const [usageLimit, setUsageLimit] = useState("");
const [expiresIn, setExpiresIn] = useState("7");
const [ephemeralPeers, setEphemeralPeers] = useState(false);
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({
initial: [],
});
const usageLimitPlaceholder = useMemo(() => {
return reusable ? "Unlimited" : "1";
}, [reusable]);
const isDisabled = useMemo(() => {
const trimmedName = trim(name);
return trimmedName.length === 0;
}, [name]);
const submit = () => {
if (!selectedGroups) return;
notify({
title: "Create Setup Key",
description:
"Setup key created successfully. You can now enroll peers with your new key.",
promise: saveGroups().then(async (groups) => {
return setupKeyRequest
.post({
name,
type: reusable ? "reusable" : "one-off",
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,
ephemeral: ephemeralPeers,
})
.then((setupKey) => {
onSuccess && onSuccess(setupKey);
mutate("/setup-keys");
mutate("/groups");
});
}),
loadingMessage: "Creating your setup key...",
});
};
return (
<ModalContent maxWidthClass={"max-w-xl"}>
<ModalHeader
icon={<SetupKeysIcon className={"fill-netbird"} />}
title={"Create New Setup Key"}
description={"Use this key to register new machines in your network"}
color={"netbird"}
/>
<Separator />
<div className={"px-8 py-6 flex flex-col gap-8"}>
<div>
<Label>Name</Label>
<HelpText>Set an easily identifiable name for your key</HelpText>
<Input
placeholder={"e.g., AWS Servers"}
value={name}
data-cy={"setup-key-name"}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<FancyToggleSwitch
value={reusable}
onChange={setReusable}
label={
<>
<IconRepeat size={15} />
Make this key reusable
</>
}
helpText={"Use this type to enroll multiple peers"}
/>
</div>
<div className={cn("flex justify-between", !reusable && "opacity-50")}>
<div>
<Label>Usage limit</Label>
<HelpText className={"max-w-[200px]"}>
For example, set to 30 if you want to enroll 30 peers
</HelpText>
</div>
<Input
min={1}
maxWidthClass={"max-w-[200px]"}
disabled={!reusable}
value={usageLimit}
type={"number"}
data-cy={"setup-key-usage-limit"}
onChange={(e) => setUsageLimit(e.target.value)}
placeholder={usageLimitPlaceholder}
customPrefix={
<MonitorSmartphoneIcon size={16} className={"text-nb-gray-300"} />
}
customSuffix={"Peer(s)"}
/>
</div>
<div className={"flex justify-between"}>
<div>
<Label>Expires in</Label>
<HelpText>
Days until the key expires. <br />
Leave empty for no expiration.
</HelpText>
</div>
<Input
maxWidthClass={"max-w-[202px]"}
placeholder={"Unlimited"}
min={1}
value={expiresIn}
errorTooltip={true}
type={"number"}
data-cy={"setup-key-expire-in-days"}
onChange={(e) => setExpiresIn(e.target.value)}
customPrefix={
<AlarmClock size={16} className={"text-nb-gray-300"} />
}
customSuffix={"Day(s)"}
/>
</div>
<div>
<FancyToggleSwitch
value={ephemeralPeers}
onChange={setEphemeralPeers}
label={
<>
<PowerOffIcon size={15} />
Ephemeral Peers
</>
}
helpText={
"Peers that are offline for over 10 minutes will be removed automatically"
}
/>
</div>
<div>
<Label>Auto-assigned groups</Label>
<HelpText>
These groups will be automatically assigned to peers enrolled with
this key
</HelpText>
<PeerGroupSelector
onChange={setSelectedGroups}
values={selectedGroups}
hideAllGroup={true}
/>
</div>
</div>
<ModalFooter className={"items-center"}>
<div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}>
Learn more about
<InlineLink
href={
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
}
target={"_blank"}
>
Setup Keys
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
<Button
variant={"primary"}
onClick={submit}
disabled={isDisabled}
data-cy={"create-setup-key"}
>
<PlusCircle size={16} />
Create Setup Key
</Button>
</div>
</ModalFooter>
</ModalContent>
);
}