diff --git a/apps/dashboard/app/(app)/settings/general/copy-workspace-id.tsx b/apps/dashboard/app/(app)/settings/general/copy-workspace-id.tsx
new file mode 100644
index 0000000000..6e435d8033
--- /dev/null
+++ b/apps/dashboard/app/(app)/settings/general/copy-workspace-id.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import { toast } from "@/components/ui/toaster";
+import { Clone } from "@unkey/icons";
+import { Input, SettingCard } from "@unkey/ui";
+
+export const CopyWorkspaceId = ({ workspaceId }: { workspaceId: string }) => {
+ return (
+
+
+ {
+ navigator.clipboard.writeText(workspaceId);
+ toast.success("Copied to clipboard", {
+ description: workspaceId,
+ });
+ }}
+ >
+
+
+ }
+ />
+
+
+ );
+};
diff --git a/apps/dashboard/app/(app)/settings/general/page.tsx b/apps/dashboard/app/(app)/settings/general/page.tsx
index 24fcb92ac1..66211d76f9 100644
--- a/apps/dashboard/app/(app)/settings/general/page.tsx
+++ b/apps/dashboard/app/(app)/settings/general/page.tsx
@@ -1,16 +1,9 @@
-import { CopyButton } from "@/components/dashboard/copy-button";
-import { Navbar as SubMenu } from "@/components/dashboard/navbar";
-import { Navigation } from "@/components/navigation/navigation";
-import { PageContent } from "@/components/page-content";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Code } from "@/components/ui/code";
import { getOrgId } from "@/lib/auth";
import { db } from "@/lib/db";
-import { Gear } from "@unkey/icons";
import { redirect } from "next/navigation";
-import { navigation } from "../constants";
+import { WorkspaceNavbar } from "../workspace-navbar";
+import { CopyWorkspaceId } from "./copy-workspace-id";
import { UpdateWorkspaceName } from "./update-workspace-name";
-// import { UpdateWorkspaceImage } from "./update-workspace-image";
/**
* TODO: WorkOS doesn't have workspace images
@@ -30,30 +23,19 @@ export default async function SettingsPage() {
return (
-
} />
-
-
-
-
- {/*
*/}
-
-
- Workspace ID
-
- This is your workspace id. It's used in some API calls.
-
-
-
-
- {workspace.id}
-
-
-
-
-
-
+
+
+
+
+ Workspace Settings
+
+
+
+ {/* */}
+
+
-
+
);
}
diff --git a/apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx b/apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx
index 5428cd5232..7d4cfddaac 100644
--- a/apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx
+++ b/apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx
@@ -1,23 +1,13 @@
"use client";
-import { Loading } from "@/components/dashboard/loading";
-import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
-import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { Button } from "@unkey/ui";
+import { Button, FormInput, SettingCard } from "@unkey/ui";
import { useRouter } from "next/navigation";
+import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
-export const dynamic = "force-dynamic";
-
-const formSchema = z.object({
- workspaceId: z.string(),
- name: z.string().trim().min(3),
-});
-
type Props = {
workspace: {
id: string;
@@ -28,67 +18,88 @@ type Props = {
export const UpdateWorkspaceName: React.FC = ({ workspace }) => {
const router = useRouter();
const utils = trpc.useUtils();
- const form = useForm>({
+ const [name, setName] = useState(workspace.name);
+
+ const formSchema = z.object({
+ workspaceId: z.string(),
+ workspaceName: z
+ .string()
+ .trim()
+ .min(3, { message: "Workspace name must be at least 3 characters long" })
+ .max(50, { message: "Workspace name must be less than 50 characters long" }),
+ });
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid, isSubmitting },
+ watch,
+ } = useForm>({
resolver: zodResolver(formSchema),
- mode: "all",
- shouldFocusError: true,
- delayError: 100,
+ mode: "onChange",
defaultValues: {
workspaceId: workspace.id,
- name: workspace.name,
+ workspaceName: name,
},
});
+
const updateName = trpc.workspace.updateName.useMutation({
onSuccess() {
toast.success("Workspace name updated");
// invalidate the current user so it refetches
utils.user.getCurrentUser.invalidate();
+ setName(watch("workspaceName"));
router.refresh();
},
onError(err) {
- toast.error(err.message);
+ toast.error("Failed to update namespace name", {
+ description: err.message,
+ });
},
});
- function onSubmit(values: z.infer) {
- updateName.mutateAsync(values);
- }
- const isDisabled = form.formState.isLoading || !form.formState.isValid || updateName.isLoading;
- return (
-
-
+ await updateName.mutateAsync({ workspaceId: workspace.id, name: values.workspaceName });
+ };
+
+ return (
+
);
};
diff --git a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/navigation.tsx b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/navigation.tsx
index 0ac45b712d..50764583aa 100644
--- a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/navigation.tsx
+++ b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/navigation.tsx
@@ -7,6 +7,7 @@ export function Navigation({ keyId }: { keyId: string }) {
return (
}>
+ Settings
Root Keys
- }>
-
- Settings
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/dashboard/app/(app)/settings/root-keys/new/navigation.tsx b/apps/dashboard/app/(app)/settings/root-keys/new/navigation.tsx
new file mode 100644
index 0000000000..e5dd9ad07d
--- /dev/null
+++ b/apps/dashboard/app/(app)/settings/root-keys/new/navigation.tsx
@@ -0,0 +1,56 @@
+"use client";
+
+import { QuickNavPopover } from "@/components/navbar-popover";
+import { Navbar } from "@/components/navigation/navbar";
+import { ChevronExpandY, Gear } from "@unkey/icons";
+
+const settingsNavbar = [
+ {
+ id: "general",
+ href: "general",
+ text: "General",
+ },
+ {
+ id: "team",
+ href: "team",
+ text: "Team",
+ },
+ {
+ id: "root-keys",
+ href: "root-keys",
+ text: "Root Keys",
+ },
+ {
+ id: "billing",
+ href: "billing",
+ text: "Billing",
+ },
+];
+
+export function Navigation() {
+ return (
+
+ }>
+ Settings
+
+ [
+ {
+ id: setting.href,
+ label: setting.text,
+ href: `/settings/${setting.href}`,
+ },
+ ])}
+ shortcutKey="M"
+ >
+
+ {"Root Keys"}
+
+
+
+
+ New
+
+
+ );
+}
diff --git a/apps/dashboard/app/(app)/settings/root-keys/new/page.tsx b/apps/dashboard/app/(app)/settings/root-keys/new/page.tsx
index fd0b3cbf6c..870f368add 100644
--- a/apps/dashboard/app/(app)/settings/root-keys/new/page.tsx
+++ b/apps/dashboard/app/(app)/settings/root-keys/new/page.tsx
@@ -3,8 +3,8 @@ import { PageContent } from "@/components/page-content";
import { getOrgId } from "@/lib/auth";
import { db } from "@/lib/db";
import { redirect } from "next/navigation";
-import { Navigation } from "../navigation";
import { Client } from "./client";
+import { Navigation } from "./navigation";
export const revalidate = 0;
diff --git a/apps/dashboard/app/(app)/settings/root-keys/page.tsx b/apps/dashboard/app/(app)/settings/root-keys/page.tsx
index 8b4f4c18c4..f6268644e4 100644
--- a/apps/dashboard/app/(app)/settings/root-keys/page.tsx
+++ b/apps/dashboard/app/(app)/settings/root-keys/page.tsx
@@ -1,12 +1,9 @@
-import { Navbar as SubMenu } from "@/components/dashboard/navbar";
import { PageHeader } from "@/components/dashboard/page-header";
import { RootKeyTable } from "@/components/dashboard/root-key-table";
-import { PageContent } from "@/components/page-content";
import { getOrgId } from "@/lib/auth";
import { db } from "@/lib/db";
import { redirect } from "next/navigation";
-import { navigation } from "../constants";
-import { Navigation } from "./navigation";
+import { WorkspaceNavbar } from "../workspace-navbar";
export const revalidate = 0;
@@ -34,19 +31,20 @@ export default async function SettingsKeysPage(_props: {
return (
-
-
-
-
+
+
);
}
diff --git a/apps/dashboard/app/(app)/settings/root-keys/root-key-nav.tsx b/apps/dashboard/app/(app)/settings/root-keys/root-key-nav.tsx
new file mode 100644
index 0000000000..d3a3827b2a
--- /dev/null
+++ b/apps/dashboard/app/(app)/settings/root-keys/root-key-nav.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import { QuickNavPopover } from "@/components/navbar-popover";
+import { Navbar } from "@/components/navigation/navbar";
+import { ChevronExpandY, Gear } from "@unkey/icons";
+import { Button } from "@unkey/ui";
+import Link from "next/link";
+
+const settingsNavbar = [
+ {
+ id: "general",
+ href: "general",
+ text: "General",
+ },
+ {
+ id: "team",
+ href: "team",
+ text: "Team",
+ },
+ {
+ id: "root-keys",
+ href: "root-keys",
+ text: "Root Keys",
+ },
+ {
+ id: "billing",
+ href: "billing",
+ text: "Billing",
+ },
+];
+
+export const RootKeyNav = ({
+ activePage,
+}: {
+ activePage: {
+ href: string;
+ text: string;
+ };
+}) => {
+ return (
+
+ }>
+ Root Keys
+
+ [
+ {
+ id: setting.href,
+ label: setting.text,
+ href: `/settings/root-keys/${setting.href}`,
+ },
+ ])}
+ shortcutKey="M"
+ >
+
+ {activePage.text}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/dashboard/app/(app)/settings/team/client.tsx b/apps/dashboard/app/(app)/settings/team/client.tsx
index 2fed8f3f3c..ebc7050d27 100644
--- a/apps/dashboard/app/(app)/settings/team/client.tsx
+++ b/apps/dashboard/app/(app)/settings/team/client.tsx
@@ -69,8 +69,8 @@ export default function TeamPageClient({ team }: { team: boolean }) {
if (!team) {
return (
-
-
+
+
Upgrade Your Plan to Add Team Members
You can try it out for free for 14 days.
diff --git a/apps/dashboard/app/(app)/settings/team/page.tsx b/apps/dashboard/app/(app)/settings/team/page.tsx
index 0a4298e90a..51e8dd6526 100644
--- a/apps/dashboard/app/(app)/settings/team/page.tsx
+++ b/apps/dashboard/app/(app)/settings/team/page.tsx
@@ -1,32 +1,32 @@
-import { Navbar as SubMenu } from "@/components/dashboard/navbar";
-import { Navigation } from "@/components/navigation/navigation";
-import { PageContent } from "@/components/page-content";
import { getOrgId } from "@/lib/auth";
import { db } from "@/lib/db";
-import { Gear } from "@unkey/icons";
-import { navigation } from "../constants";
+import { WorkspaceNavbar } from "../workspace-navbar";
import TeamPageClient from "./client";
export const revalidate = 0;
export default async function SettingTeamPage() {
const orgId = await getOrgId();
- const ws = await db.query.workspaces.findFirst({
+ const workspace = await db.query.workspaces.findFirst({
where: (table, { and, eq, isNull }) => and(eq(table.orgId, orgId), isNull(table.deletedAtM)),
with: { quotas: true },
});
- const team = ws?.quotas?.team ?? false;
+ const team = workspace?.quotas?.team ?? false;
- return (
+ return workspace ? (
+ <>
+
+
+
+
+ >
+ ) : (
-
} />
-
-
-
-
-
-
+
Workspace not found
);
}
diff --git a/apps/dashboard/app/(app)/settings/workspace-navbar.tsx b/apps/dashboard/app/(app)/settings/workspace-navbar.tsx
new file mode 100644
index 0000000000..a08ab2cb97
--- /dev/null
+++ b/apps/dashboard/app/(app)/settings/workspace-navbar.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { CopyButton } from "@/components/dashboard/copy-button";
+import { QuickNavPopover } from "@/components/navbar-popover";
+import { Navbar } from "@/components/navigation/navbar";
+import { Badge } from "@/components/ui/badge";
+import { ChevronExpandY, Gear } from "@unkey/icons";
+import { Button } from "@unkey/ui";
+import Link from "next/link";
+
+const settingsNavbar = [
+ {
+ id: "general",
+ href: "general",
+ text: "General",
+ },
+ {
+ id: "team",
+ href: "team",
+ text: "Team",
+ },
+ {
+ id: "root-keys",
+ href: "root-keys",
+ text: "Root Keys",
+ },
+ {
+ id: "billing",
+ href: "billing",
+ text: "Billing",
+ },
+];
+
+export const WorkspaceNavbar = ({
+ workspace,
+ activePage,
+}: {
+ workspace: {
+ id: string;
+ name: string;
+ };
+ activePage: {
+ href: string;
+ text: string;
+ };
+}) => {
+ return (
+
+
+ }>
+ Settings
+
+ [
+ {
+ id: setting.href,
+ label: setting.text,
+ href: `/settings/${setting.href}`,
+ },
+ ])}
+ shortcutKey="M"
+ >
+
+ {activePage.text}
+
+
+
+
+
+
+ {activePage.href === "general" && (
+
+ {workspace.id}
+
+
+ )}
+ {activePage.href === "root-keys" && (
+
+
+
+ )}
+ {activePage.href === "billing" && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ );
+};
diff --git a/apps/dashboard/components/navigation/sidebar/workspace-navigations.tsx b/apps/dashboard/components/navigation/sidebar/workspace-navigations.tsx
index e526409e3e..c83b2615b6 100644
--- a/apps/dashboard/components/navigation/sidebar/workspace-navigations.tsx
+++ b/apps/dashboard/components/navigation/sidebar/workspace-navigations.tsx
@@ -117,6 +117,12 @@ export const createWorkspaceNavigation = (
label: "Settings",
active: segments.at(0) === "settings",
items: [
+ {
+ icon: null,
+ href: "/settings/general",
+ label: "General",
+ active: segments.some((s) => s === "general"),
+ },
{
icon: null,
href: "/settings/team",
diff --git a/apps/dashboard/lib/trpc/routers/workspace/changeName.ts b/apps/dashboard/lib/trpc/routers/workspace/changeName.ts
index da0a05cca2..328b8bbd1b 100644
--- a/apps/dashboard/lib/trpc/routers/workspace/changeName.ts
+++ b/apps/dashboard/lib/trpc/routers/workspace/changeName.ts
@@ -9,7 +9,10 @@ export const changeWorkspaceName = t.procedure
.use(requireWorkspace)
.input(
z.object({
- name: z.string().min(3, "workspace names must contain at least 3 characters"),
+ name: z
+ .string()
+ .min(3, "Workspace names must contain at least 3 characters")
+ .max(50, "Workspace names must contain less than 50 characters"),
workspaceId: z.string(),
}),
)
diff --git a/internal/ui/src/components/settings-card.tsx b/internal/ui/src/components/settings-card.tsx
index 960b7e75a7..95a22e0409 100644
--- a/internal/ui/src/components/settings-card.tsx
+++ b/internal/ui/src/components/settings-card.tsx
@@ -35,7 +35,7 @@ export function SettingCard({
return (