Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { FilterValue, StringConfig } from "@/components/logs/validation/filter.types";
import { parseAsFilterValueArray } from "@/components/logs/validation/utils/nuqs-parsers";
import { createFilterOutputSchema } from "@/components/logs/validation/utils/structured-output-schema-generator";
import { z } from "zod";

const commonStringOperators = ["is", "contains"] as const;

export const rootKeysFilterOperatorEnum = z.enum(commonStringOperators);
export type RootKeysFilterOperator = z.infer<typeof rootKeysFilterOperatorEnum>;

export type FilterFieldConfigs = {
name: StringConfig<RootKeysFilterOperator>;
start: StringConfig<RootKeysFilterOperator>;
identity: StringConfig<RootKeysFilterOperator>;
permission: StringConfig<RootKeysFilterOperator>;
};

export const rootKeysFilterFieldConfig: FilterFieldConfigs = {
name: {
type: "string",
operators: [...commonStringOperators],
},
start: {
// Start of the `key` for security reasons
type: "string",
operators: [...commonStringOperators],
},
identity: {
// ExternalId of creator user
type: "string",
operators: [...commonStringOperators],
},
permission: {
type: "string",
operators: [...commonStringOperators],
},
};

const allFilterFieldNames = Object.keys(rootKeysFilterFieldConfig) as (keyof FilterFieldConfigs)[];

if (allFilterFieldNames.length === 0) {
throw new Error("rootKeysFilterFieldConfig must contain at least one field definition.");
}

const [firstFieldName, ...restFieldNames] = allFilterFieldNames;

export const rootKeysFilterFieldEnum = z.enum([firstFieldName, ...restFieldNames]);
export const rootKeysListFilterFieldNames = allFilterFieldNames;
export type RootKeysFilterField = z.infer<typeof rootKeysFilterFieldEnum>;

export const filterOutputSchema = createFilterOutputSchema(
rootKeysFilterFieldEnum,
rootKeysFilterOperatorEnum,
rootKeysFilterFieldConfig,
);

export type AllOperatorsUrlValue = {
value: string;
operator: RootKeysFilterOperator;
};

export type RootKeysFilterValue = FilterValue<RootKeysFilterField, RootKeysFilterOperator>;

export type RootKeysQuerySearchParams = {
[K in RootKeysFilterField]?: AllOperatorsUrlValue[] | null;
};

export const parseAsAllOperatorsFilterArray = parseAsFilterValueArray<RootKeysFilterOperator>([
...commonStringOperators,
]);
102 changes: 102 additions & 0 deletions apps/dashboard/app/(app)/settings/root-keys-v2/navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";
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, CopyButton } from "@unkey/ui";
import Link from "next/link";

const settingsNavbar = [
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a blocker here, but we should definitely put these kind of link lists into a constant to reuse in other pages, wdyt?

{
id: "general",
href: "general",
text: "General",
},
{
id: "team",
href: "team",
text: "Team",
},
{
id: "root-keys-v2",
href: "root-keys-v2",
text: "Root Keys",
},
{
id: "billing",
href: "billing",
text: "Billing",
},
];

export const Navigation = ({
workspace,
activePage,
}: {
workspace: {
id: string;
name: string;
};
activePage: {
href: string;
text: string;
};
}) => {
return (
<div className="flex flex-col w-full h-full">
<Navbar>
<Navbar.Breadcrumbs icon={<Gear />}>
<Navbar.Breadcrumbs.Link href="/settings">Settings</Navbar.Breadcrumbs.Link>
<Navbar.Breadcrumbs.Link href={activePage.href} noop active>
<QuickNavPopover
items={settingsNavbar.flatMap((setting) => [
{
id: setting.href,
label: setting.text,
href: `/settings/${setting.href}`,
},
])}
shortcutKey="M"
>
<div className="flex items-center gap-1 p-1 rounded-lg hover:bg-gray-3">
{activePage.text}
<ChevronExpandY className="size-4" />
</div>
</QuickNavPopover>
</Navbar.Breadcrumbs.Link>
</Navbar.Breadcrumbs>
<Navbar.Actions>
{activePage.href === "general" && (
<Badge
variant="secondary"
className="max-w-[160px] truncate whitespace-nowrap"
title={workspace.id}
>
{workspace.id}
<CopyButton value={workspace.id} />
</Badge>
)}
{activePage.href === "root-keys-v2" && (
<Link key="create-root-key" href="/settings/root-keys/new">
<Button variant="primary">Create New Root Key</Button>
</Link>
)}
{activePage.href === "billing" && (
<>
<Link href="https://cal.com/james-r-perkins/sales" target="_blank">
<Button type="button" variant="outline">
Schedule a call
</Button>
</Link>
<Link href="mailto:support@unkey.dev">
<Button type="button" variant="primary">
Contact us
</Button>
</Link>
</>
)}
</Navbar.Actions>
</Navbar>
</div>
);
};
52 changes: 52 additions & 0 deletions apps/dashboard/app/(app)/settings/root-keys-v2/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";
import { trpc } from "@/lib/trpc/client";
import { Navigation } from "./navigation";

export default function RootKeysPage() {
const { data, isLoading, error } = trpc.settings.rootKeys.query.useQuery({
limit: 10,
});

return (
<div>
<Navigation
workspace={{
id: "will-add-soon",
name: "will-add-soon",
}}
activePage={{
href: "root-keys",
text: "Root Keys",
}}
/>
<div className="flex flex-col p-6">
<h1 className="text-2xl font-bold mb-4 text-foreground">Root Keys</h1>

{isLoading && <div className="text-grayA-11">Loading root keys...</div>}

{error && (
<div className="text-red-11 bg-red-2 border border-red-6 rounded-md p-3">
Error loading root keys: {error.message}
</div>
)}

{data && (
<div className="space-y-4">
<div className="text-sm text-grayA-11">
Showing {data.keys.length} of {data.total} keys
{data.hasMore && " (more available)"}
</div>

<div className="space-y-3">
{data.keys.map((key) => (
<pre key={key.id} className="border-b ">
{JSON.stringify(key, null, 2)}
</pre>
))}
</div>
</div>
)}
</div>
</div>
);
}
6 changes: 6 additions & 0 deletions apps/dashboard/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import { disconnectRoleFromKey } from "./rbac/disconnectRoleFromKey";
import { removePermissionFromRootKey } from "./rbac/removePermissionFromRootKey";
import { updatePermission } from "./rbac/updatePermission";
import { updateRole } from "./rbac/updateRole";
import { queryRootKeys } from "./settings/root-keys/query";
import { cancelSubscription } from "./stripe/cancelSubscription";
import { createSubscription } from "./stripe/createSubscription";
import { uncancelSubscription } from "./stripe/uncancelSubscription";
Expand Down Expand Up @@ -121,6 +122,11 @@ export const router = t.router({
name: updateRootKeyName,
}),
}),
settings: t.router({
rootKeys: t.router({
query: queryRootKeys,
}),
}),
api: t.router({
create: createApi,
delete: deleteApi,
Expand Down
Loading
Loading