diff --git a/frontend/src/components/util.tsx b/frontend/src/components/util.tsx index 6786616d4..bd1aa8cc3 100644 --- a/frontend/src/components/util.tsx +++ b/frontend/src/components/util.tsx @@ -20,7 +20,7 @@ import { DialogContent, DialogFooter, } from "@ui/dialog"; -import { toast } from "@ui/use-toast"; +import { toast, useToast } from "@ui/use-toast"; import { RESOURCE_TARGETS, cn } from "@lib/utils"; import { useInvalidate, @@ -434,3 +434,34 @@ export const UserDropdown = () => { ); }; + +export const CopyButton = ({ content }: { content: string | undefined }) => { + const { toast } = useToast(); + const [copied, set] = useState(false); + + useEffect(() => { + if (copied) { + toast({ title: `Copied "${content}"` }); + const timeout = setTimeout(() => set(false), 3000); + return () => { + clearTimeout(timeout); + }; + } + }, [content, copied, toast]); + + return ( + + ); +}; diff --git a/frontend/src/pages/keys.tsx b/frontend/src/pages/keys.tsx index 71371f7cd..24c561b77 100644 --- a/frontend/src/pages/keys.tsx +++ b/frontend/src/pages/keys.tsx @@ -1,19 +1,32 @@ -import { Page, Section } from "@components/layouts"; -import { ConfirmButton } from "@components/util"; +import { Page } from "@components/layouts"; +import { ConfirmButton, CopyButton } from "@components/util"; import { useInvalidate, useRead, useWrite } from "@lib/hooks"; import { fmt_date } from "@lib/utils"; import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@ui/card"; + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@ui/dialog"; +import { Button } from "@ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@ui/card"; import { useToast } from "@ui/use-toast"; -import { Key, Trash } from "lucide-react"; +import { Trash, PlusCircle, Loader2, Check } from "lucide-react"; +import { useState } from "react"; +import { Input } from "@ui/input"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@ui/dropdown-menu"; export const Keys = () => { return ( - + }> ); @@ -22,29 +35,145 @@ export const Keys = () => { export const ApiKeysList = () => { const keys = useRead("ListApiKeys", {}).data; return ( -
}> -
- {keys?.map((key) => ( - - - {key.name} - - - -
created at: {fmt_date(new Date(key.created_at))}
-
- expires:{" "} - {key.expires === 0 ? "never" : fmt_date(new Date(key.expires))} +
+ {keys?.map((key) => ( + + + {key.name} + + + +
created at: {fmt_date(new Date(key.created_at))}
+
+ expires:{" "} + {key.expires === 0 ? "never" : fmt_date(new Date(key.expires))} +
+
{key.key}
+
+
+ ))} +
+ ); +}; + +const ONE_DAY_MS = 1000 * 60 * 60 * 24; + +type ExpiresOptions = "90 days" | "180 days" | "1 year" | "never"; + +const CreateKey = () => { + const [open, setOpen] = useState(false); + const [name, setName] = useState(""); + const [expires, setExpires] = useState("never"); + const [submitted, setSubmitted] = useState<{ key: string; secret: string }>(); + const invalidate = useInvalidate(); + const { mutate, isPending } = useWrite("CreateApiKey", { + onSuccess: ({ key, secret }) => { + invalidate(["ListApiKeys"]); + setSubmitted({ key, secret }); + }, + }); + const now = Date.now(); + const expiresOptions: Record = { + "90 days": now + ONE_DAY_MS * 90, + "180 days": now + ONE_DAY_MS * 180, + "1 year": now + ONE_DAY_MS * 365, + never: 0, + }; + const submit = () => mutate({ name, expires: expiresOptions[expires] }); + const onOpenChange = (open: boolean) => { + setOpen(open); + if (!open) { + setName(""); + setExpires("never"); + setSubmitted(undefined); + } + }; + return ( + + + + + + {submitted ? ( + <> + + Api Key Created + +
+
+ Key + +
-
{key.key}
- - - ))} -
-
+
+ Secret + + +
+ + + + + + ) : ( + <> + + Create Api Key + +
+
+ Name + setName(e.target.value)} + /> +
+
+ Expiry + + + + + + + {Object.keys(expiresOptions) + .filter((option) => option !== expires) + .map((option) => ( + setExpires(option as any)} + > + {option} + + ))} + + + +
+
+ + + + + )} + + ); }; @@ -57,8 +186,8 @@ const DeleteKey = ({ api_key }: { api_key: string }) => { toast({ title: "Api Key Deleted" }); }, onError: () => { - toast({ title: "Failed to delte api key", }) - } + toast({ title: "Failed to delte api key" }); + }, }); return (