diff --git a/apps/dashboard/app/(app)/settings/root-keys/components/create-root-key/create-rootkey-button.tsx b/apps/dashboard/app/(app)/settings/root-keys/components/create-root-key/create-rootkey-button.tsx index 48faebb666..5726d7e3b1 100644 --- a/apps/dashboard/app/(app)/settings/root-keys/components/create-root-key/create-rootkey-button.tsx +++ b/apps/dashboard/app/(app)/settings/root-keys/components/create-root-key/create-rootkey-button.tsx @@ -1,16 +1,26 @@ "use client"; +import { SecretKey } from "@/app/(app)/apis/[apiId]/_components/create-key/components/secret-key"; +import { ConfirmPopover } from "@/components/confirmation-popover"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; import { trpc } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Plus } from "@unkey/icons"; -import { type UnkeyPermission, unkeyPermissionValidation } from "@unkey/rbac"; -import { Button, FormInput, toast } from "@unkey/ui"; +import { ArrowRight, Check, CircleInfo, Key2, Plus } from "@unkey/icons"; +import type { UnkeyPermission } from "@unkey/rbac"; +import { + Button, + Code, + CopyButton, + Dialog, + DialogContent, + FormInput, + InfoTooltip, + VisibleButton, + toast, +} from "@unkey/ui"; import dynamic from "next/dynamic"; -import { useCallback, useMemo, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; +import { useRouter } from "next/navigation"; +import { useCallback, useMemo, useRef, useState } from "react"; import { PermissionBadgeList } from "./components/permission-badge-list"; import { PermissionSheet } from "./components/permission-sheet"; @@ -24,22 +34,79 @@ const DynamicDialogContainer = dynamic( const DEFAULT_LIMIT = 12; -const formSchema = z.object({ - name: z.string().trim().min(3, "Name must be at least 3 characters long").max(50), - permissions: z.array(unkeyPermissionValidation).min(1, "At least one permission is required"), -}); - type Props = { className?: string; } & React.ComponentProps; -export const CreateRootKeyButton = ({ ...props }: Props) => { +export const CreateRootKeyButton = ({ className, ...props }: Props) => { + const UNNAMED_KEY = "Unnamed"; const trpcUtils = trpc.useUtils(); + const [name, setName] = useState(""); const [isOpen, setIsOpen] = useState(false); + const [showKeyInSnippet, setShowKeyInSnippet] = useState(false); + const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const [pendingAction, setPendingAction] = useState< + "close" | "create-another" | "go-to-details" | null + >(null); + const dividerRef = useRef(null); const [selectedPermissions, setSelectedPermissions] = useState([]); + + const router = useRouter(); + + const handleCloseAttempt = (action: "close" | "create-another" | "go-to-details" = "close") => { + setPendingAction(action); + setIsConfirmOpen(true); + }; + + const handleConfirmClose = () => { + if (!pendingAction) { + console.error("No pending action when confirming close"); + return; + } + + setIsConfirmOpen(false); + + try { + // Always close the dialog first + setIsOpen(false); + key.reset(); + setSelectedPermissions([]); + setName(""); + + // Then execute the specific action + switch (pendingAction) { + case "create-another": + // Reset form for creating another key + setIsOpen(true); + break; + + case "go-to-details": + router.push(`/settings/root-keys/${key.data?.keyId}`); + break; + + default: + // Dialog already closed, nothing more to do + + router.push("/settings/root-keys"); + break; + } + } catch (error) { + console.error("Error executing pending action:", error); + toast.error("Action Failed", { + description: "An unexpected error occurred. Please try again.", + }); + } finally { + setPendingAction(null); + } + }; + + const handleDialogOpenChange = (open: boolean) => { + if (!open) { + handleCloseAttempt("close"); + } + }; const { data: apisData, - isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, @@ -62,16 +129,6 @@ export const CreateRootKeyButton = ({ ...props }: Props) => { }); }, [apisData]); - const { - register, - handleSubmit, - setValue, - formState: { errors, isValid, isSubmitting }, - } = useForm>({ - resolver: zodResolver(formSchema), - mode: "onChange", - }); - const key = trpc.rootKey.create.useMutation({ onSuccess() { trpcUtils.settings.rootKeys.query.invalidate(); @@ -81,28 +138,32 @@ export const CreateRootKeyButton = ({ ...props }: Props) => { toast.error(err.message); }, }); + function fetchMoreApis() { if (hasNextPage) { fetchNextPage(); } } + const snippet = `curl -XPOST '${ + process.env.NEXT_PUBLIC_UNKEY_API_URL ?? "https://api.unkey.dev" + }/v1/keys.createKey' \\ + -H 'Authorization: Bearer ${key.data?.key}' \\ + -H 'Content-Type: application/json' \\ + -d '{ + "prefix": "hello", + "apiId": "" + }'`; - async function onSubmit(values: z.infer) { - await key.mutateAsync({ - name: values.name, - permissions: values.permissions, - }); + const split = key.data?.key?.split("_") ?? []; + const maskedKey = + split.length >= 2 + ? `${split.at(0)}_${"*".repeat(split.at(1)?.length ?? 0)}` + : "*".repeat(split.at(0)?.length ?? 0); - setIsOpen(false); - } + const handlePermissionChange = useCallback((permissions: UnkeyPermission[]) => { + setSelectedPermissions(permissions); + }, []); - const handlePermissionChange = useCallback( - (permissions: UnkeyPermission[]) => { - setSelectedPermissions(permissions); - setValue("permissions", permissions); - }, - [setValue], - ); return ( <> @@ -140,34 +204,33 @@ export const CreateRootKeyButton = ({ ...props }: Props) => { } > -
-
-
- -
-
- - - - -
+
+
+ setName(e.target.value)} + /> +
+
+ + + +
- +
{
+ + handleCloseAttempt("close")} + > + <> +
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+ Root Key Created +
+
+ You've successfully generated a new Root key. +
+
+
+
+
+
+
Key Details
+
+
+
+ +
+
+
{key.data?.keyId}
+ +
+ {name ?? UNNAMED_KEY} +
+
+
+ +
+
+
+
+
Key Secret
+ +
+ + + Copy and save this key secret as it won't be shown again.{" "} + + Learn more + + +
+
+
+
Try It Out
+ + } + copyButton={} + > + {showKeyInSnippet ? snippet : snippet.replace(key.data?.key ?? "", maskedKey)} + +
+
+
+ All set! You can now create another key or explore the docs to learn more +
+
+
+ e.preventDefault(), + }} + /> + + +
); };