diff --git a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx deleted file mode 100644 index 26b3c9085f..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { apiId: string; keyAuthId: string; keyId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const tenantId = getTenantId(); - - const getApiById = cache( - async (apiId: string) => - db.query.apis.findFirst({ - where: (table, { eq, and, isNull }) => and(eq(table.id, apiId), isNull(table.deletedAt)), - - with: { - workspace: true, - }, - }), - ["apiById"], - ); - - const api = await getApiById(props.params.apiId); - if (!api || api.workspace.tenantId !== tenantId) { - return null; - } - - return ( - - - - APIs - - - - {api.name} - - - - - Keys - - - - - - ... - {props.params.keyId.substring(props.params.keyId.length - 4, props.params.keyId.length)} - - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx deleted file mode 100644 index 1c81f2c3a2..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { BreadcrumbSkeleton } from "@/components/dashboard/breadcrumb-skeleton"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { apiId: string; keyAuthId: string; keyId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const tenantId = getTenantId(); - - const getApiById = cache( - async (apiId: string) => - db.query.apis.findFirst({ - where: (table, { eq, and, isNull }) => and(eq(table.id, apiId), isNull(table.deletedAt)), - - with: { - workspace: true, - }, - }), - ["apiById"], - ); - - const api = await getApiById(props.params.apiId); - if (!api || api.workspace.tenantId !== tenantId) { - return null; - } - - return ( - - - - APIs - - - - {api.name} - - - - - Keys - - - - - - ... - {props.params.keyId.substring(props.params.keyId.length - 4, props.params.keyId.length)} - - - - - Settings - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - }> - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/new/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/new/page.tsx deleted file mode 100644 index 8e4b4de9cd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/new/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { BreadcrumbSkeleton } from "@/components/dashboard/breadcrumb-skeleton"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { apiId: string; keyAuthId: string; keyId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const tenantId = getTenantId(); - - const getApiById = cache( - async (apiId: string) => - db.query.apis.findFirst({ - where: (table, { eq, and, isNull }) => and(eq(table.id, apiId), isNull(table.deletedAt)), - - with: { - workspace: true, - }, - }), - ["apiById"], - ); - - const api = await getApiById(props.params.apiId); - if (!api || api.workspace.tenantId !== tenantId) { - return null; - } - - return ( - - - - APIs - - - - {api.name} - - - - - Keys - - - - - Create new key - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - }> - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/page.tsx deleted file mode 100644 index 818cae0682..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/keys/[keyAuthId]/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { BreadcrumbSkeleton } from "@/components/dashboard/breadcrumb-skeleton"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; -import { tags } from "@/lib/cache"; - -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { apiId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const tenantId = getTenantId(); - - const getApiById = cache( - async (apiId: string) => - db.query.apis.findFirst({ - where: (table, { eq, and, isNull }) => and(eq(table.id, apiId), isNull(table.deletedAt)), - - with: { - workspace: true, - }, - }), - ["apiById"], - { tags: [tags.api(props.params.apiId)] }, - ); - - const api = await getApiById(props.params.apiId); - if (!api || api.workspace.tenantId !== tenantId) { - return null; - } - - return ( - - - - APIs - - - - {api.name} - - - - Keys - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - }> - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/page.tsx deleted file mode 100644 index b586b79e24..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; -import { unstable_cache as cache } from "next/cache"; - -import { BreadcrumbSkeleton } from "@/components/dashboard/breadcrumb-skeleton"; -import { getTenantId } from "@/lib/auth"; -import { tags } from "@/lib/cache"; -import { db } from "@/lib/db"; -import { Suspense } from "react"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { apiId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const tenantId = getTenantId(); - - const getApiById = cache( - async (apiId: string) => - db.query.apis.findFirst({ - where: (table, { eq, and, isNull }) => and(eq(table.id, apiId), isNull(table.deletedAt)), - - with: { - workspace: true, - }, - }), - ["apiById"], - { tags: [tags.api(props.params.apiId)] }, - ); - - const api = await getApiById(props.params.apiId); - if (!api || api.workspace.tenantId !== tenantId) { - return null; - } - - return ( - - - - APIs - - - - {api.name} - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - }> - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/settings/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/settings/page.tsx deleted file mode 100644 index dc69639b2d..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/apis/[apiId]/settings/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { BreadcrumbSkeleton } from "@/components/dashboard/breadcrumb-skeleton"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; -import { getTenantId } from "@/lib/auth"; -import { tags } from "@/lib/cache"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { params: { apiId: string } }; - -async function AsyncPageBreadcrumb(props: PageProps) { - const tenantId = getTenantId(); - - const getApiById = cache( - async (apiId: string) => - db.query.apis.findFirst({ - where: (table, { eq, and, isNull }) => and(eq(table.id, apiId), isNull(table.deletedAt)), - - with: { - workspace: true, - }, - }), - ["apiById"], - { tags: [tags.api(props.params.apiId)] }, - ); - - const api = await getApiById(props.params.apiId); - if (!api || api.workspace.tenantId !== tenantId) { - return null; - } - - return ( - - - - APIs - - - - {api.name} - - - - Settings - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - }> - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/apis/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/apis/page.tsx deleted file mode 100644 index 75983528a7..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/apis/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { PageHeader } from "@/components/dashboard/page-header"; - -export default function PageBreadcrumb() { - return ; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/audit/[...path]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/audit/[...path]/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/audit/[...path]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/audit/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/audit/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/audit/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/authorization/permissions/[permissionId]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/authorization/permissions/[permissionId]/page.tsx deleted file mode 100644 index 0c031fa95e..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/authorization/permissions/[permissionId]/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -import { BreadcrumbSkeleton } from "@/components/dashboard/breadcrumb-skeleton"; -import { tags } from "@/lib/cache"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { permissionId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const getPermissionById = cache( - async (permissionId: string) => - db.query.permissions.findFirst({ - where: (table, { eq }) => eq(table.id, permissionId), - }), - ["permissionById"], - { tags: [tags.permission(props.params.permissionId)] }, - ); - - const permissions = await getPermissionById(props.params.permissionId); - if (!permissions) { - return null; - } - - return ( - - - - Authorization - - - - Permissions - - - - {permissions.name} - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - }> - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/authorization/permissions/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/authorization/permissions/page.tsx deleted file mode 100644 index e121ae339a..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/authorization/permissions/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -export default function PageBreadcrumb() { - return ( - - - - Authorization - - - - Permissions - - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/authorization/roles/[roleId]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/authorization/roles/[roleId]/page.tsx deleted file mode 100644 index 45aae47e92..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/authorization/roles/[roleId]/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -import { BreadcrumbSkeleton } from "@/components/dashboard/breadcrumb-skeleton"; -import { tags } from "@/lib/cache"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { roleId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const getWorkspaceByRoleId = cache( - async (roleId: string) => - await db.query.roles.findFirst({ - where: (table, { eq }) => eq(table.id, roleId), - }), - ["roleById"], - { tags: [tags.role(props.params.roleId)] }, - ); - - const role = await getWorkspaceByRoleId(props.params.roleId); - if (!role) { - return null; - } - - return ( - - - - Authorization - - - - Roles - - - - {role.name} - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - }> - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/authorization/roles/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/authorization/roles/page.tsx deleted file mode 100644 index a4ddaadccf..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/authorization/roles/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -export default function PageBreadcrumb() { - return ( - - - - Authorization - - - - Roles - - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/debug/[...path]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/debug/[...path]/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/debug/[...path]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/debug/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/debug/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/debug/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/default.tsx b/apps/dashboard/app/(app)/@breadcrumb/default.tsx deleted file mode 100644 index 6ddf1b76fa..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Default() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/identities/[identityId]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/identities/[identityId]/page.tsx deleted file mode 100644 index e2fa0b4ab9..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/identities/[identityId]/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { identityId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const tenantId = getTenantId(); - - const getIdentityById = cache( - async (identityId: string) => - db.query.identities.findFirst({ - where: (table, { eq }) => eq(table.id, identityId), - - with: { - workspace: true, - }, - }), - ["identityById"], - ); - - const identity = await getIdentityById(props.params.identityId); - if (!identity || identity.workspace.tenantId !== tenantId) { - return null; - } - - return ( - - - - Identities - - - - - {identity.externalId} - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/identities/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/identities/page.tsx deleted file mode 100644 index 72cad67391..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/identities/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, -} from "@/components/ui/breadcrumb"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -export default function PageBreadcrumb() { - return ( -
- - - - Identities - - - - -
- Beta -
-
- ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/logs/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/logs/page.tsx deleted file mode 100644 index 382fddc8c0..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/logs/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/monitors/[...path]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/monitors/[...path]/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/monitors/[...path]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/monitors/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/monitors/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/monitors/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/overview/[...path]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/overview/[...path]/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/overview/[...path]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/overview/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/overview/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/overview/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/page.tsx deleted file mode 100644 index 85911e6c31..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./default"; diff --git a/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/logs/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/logs/page.tsx deleted file mode 100644 index 51f11f99d6..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/logs/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -import { BreadcrumbSkeleton } from "@/components/dashboard/breadcrumb-skeleton"; -import { tags } from "@/lib/cache"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { namespaceId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const getNamespaceById = cache( - async (namespaceId: string) => - db.query.ratelimitNamespaces.findFirst({ - where: (table, { eq, and, isNull }) => - and(eq(table.id, namespaceId), isNull(table.deletedAt)), - }), - ["namespaceById"], - { tags: [tags.namespace(props.params.namespaceId)] }, - ); - - const namespace = await getNamespaceById(props.params.namespaceId); - if (!namespace) { - return null; - } - - return ( - - - - Ratelimits - - - - - {namespace.name} - - - - - Logs - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - }> - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/overrides/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/overrides/page.tsx deleted file mode 100644 index 858a7cd0f1..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/overrides/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -import { BreadcrumbSkeleton } from "@/components/dashboard/breadcrumb-skeleton"; -import { tags } from "@/lib/cache"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { namespaceId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const getNamespaceById = cache( - async (namespaceId: string) => - db.query.ratelimitNamespaces.findFirst({ - where: (table, { eq, and, isNull }) => - and(eq(table.id, namespaceId), isNull(table.deletedAt)), - }), - ["namespaceById"], - { tags: [tags.namespace(props.params.namespaceId)] }, - ); - - const namespace = await getNamespaceById(props.params.namespaceId); - if (!namespace) { - return null; - } - - return ( - - - - Ratelimits - - - - - {namespace.name} - - - - - Overrides - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - }> - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/page.tsx deleted file mode 100644 index 8fbce359e9..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -import { BreadcrumbSkeleton } from "@/components/dashboard/breadcrumb-skeleton"; -import { tags } from "@/lib/cache"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { namespaceId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const getNamespaceById = cache( - async (namespaceId: string) => - db.query.ratelimitNamespaces.findFirst({ - where: (table, { eq, and, isNull }) => - and(eq(table.id, namespaceId), isNull(table.deletedAt)), - }), - - ["namespaceById"], - { tags: [tags.namespace(props.params.namespaceId)] }, - ); - - const namespace = await getNamespaceById(props.params.namespaceId); - if (!namespace) { - return null; - } - - return ( - - - - Ratelimits - - - - {namespace.name} - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - }> - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/settings/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/settings/page.tsx deleted file mode 100644 index 176cbbba31..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/ratelimits/[namespaceId]/settings/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; - -import { BreadcrumbSkeleton } from "@/components/dashboard/breadcrumb-skeleton"; -import { tags } from "@/lib/cache"; -import { db } from "@/lib/db"; -import { unstable_cache as cache } from "next/cache"; -import { Suspense } from "react"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type PageProps = { - params: { namespaceId: string }; -}; - -async function AsyncPageBreadcrumb(props: PageProps) { - const getNamespaceById = cache( - async (namespaceId: string) => - db.query.ratelimitNamespaces.findFirst({ - where: (table, { eq, and, isNull }) => - and(eq(table.id, namespaceId), isNull(table.deletedAt)), - }), - ["namespaceById"], - { tags: [tags.namespace(props.params.namespaceId)] }, - ); - - const namespace = await getNamespaceById(props.params.namespaceId); - if (!namespace) { - return null; - } - - return ( - - - - Ratelimits - - - - - {namespace.name} - - - - - Settings - - - - ); -} - -export default function PageBreadcrumb(props: PageProps) { - return ( - }> - - - ); -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/ratelimits/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/ratelimits/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/ratelimits/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/secrets/[...path]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/secrets/[...path]/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/secrets/[...path]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/secrets/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/secrets/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/secrets/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/semantic-cache/[...path]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/semantic-cache/[...path]/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/semantic-cache/[...path]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/semantic-cache/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/semantic-cache/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/semantic-cache/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/settings/[...path]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/settings/[...path]/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/settings/[...path]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/settings/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/settings/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/settings/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/success/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/success/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/success/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/apis/[apiId]/constants.ts b/apps/dashboard/app/(app)/apis/[apiId]/constants.ts new file mode 100644 index 0000000000..9268499db4 --- /dev/null +++ b/apps/dashboard/app/(app)/apis/[apiId]/constants.ts @@ -0,0 +1,17 @@ +export const navigation = (apiId: string, keyAuthId: string) => [ + { + label: "Overview", + href: `/apis/${apiId}`, + segment: "overview", + }, + { + label: "Keys", + href: `/apis/${apiId}/keys/${keyAuthId}`, + segment: "keys", + }, + { + label: "API Settings", + href: `/apis/${apiId}/settings`, + segment: "settings", + }, +]; diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx index 12f8f1468d..f94ecb0bdd 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx @@ -6,7 +6,11 @@ import { CreateNewPermission } from "@/app/(app)/authorization/permissions/creat import type { NestedPermissions } from "@/app/(app)/authorization/roles/[roleId]/tree"; import { CreateNewRole } from "@/app/(app)/authorization/roles/create-new-role"; import { StackedColumnChart } from "@/components/dashboard/charts"; +import { CopyButton } from "@/components/dashboard/copy-button"; +import { CreateKeyButton } from "@/components/dashboard/create-key-button"; import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Metric } from "@/components/ui/metric"; @@ -15,6 +19,7 @@ import { getTenantId } from "@/lib/auth"; import { clickhouse } from "@/lib/clickhouse"; import { and, db, eq, isNull, schema } from "@/lib/db"; import { formatNumber } from "@/lib/fmt"; +import { Nodes } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { BarChart, Minus } from "lucide-react"; import ms from "ms"; @@ -36,6 +41,7 @@ export default async function APIKeyDetailPage(props: { const key = await db.query.keys.findFirst({ where: and(eq(schema.keys.id, props.params.keyId), isNull(schema.keys.deletedAt)), with: { + keyAuth: true, roles: { with: { role: { @@ -70,6 +76,7 @@ export default async function APIKeyDetailPage(props: { if (!key || key.workspace.tenantId !== tenantId) { return notFound(); } + const api = await db.query.apis.findFirst({ where: (table, { eq, and, isNull }) => and(eq(table.keyAuthId, key.keyAuthId), isNull(table.deletedAt)), @@ -96,7 +103,11 @@ export default async function APIKeyDetailPage(props: { keyId: key.id, }), clickhouse.verifications - .latest({ workspaceId: key.workspaceId, keySpaceId: key.keyAuthId, keyId: key.id }) + .latest({ + workspaceId: key.workspaceId, + keySpaceId: key.keyAuthId, + keyId: key.id, + }) .then((res) => res.val?.at(0)?.time ?? 0), ]); @@ -144,7 +155,10 @@ export default async function APIKeyDetailPage(props: { ...ratelimitedOverTime.map((d) => ({ ...d, category: "Ratelimited" })), ...usageExceededOverTime.map((d) => ({ ...d, category: "Usage Exceeded" })), ...disabledOverTime.map((d) => ({ ...d, category: "Disabled" })), - ...insufficientPermissionsOverTime.map((d) => ({ ...d, category: "Insufficient Permissions" })), + ...insufficientPermissionsOverTime.map((d) => ({ + ...d, + category: "Insufficient Permissions", + })), ...expiredOverTime.map((d) => ({ ...d, category: "Expired" })), ...forbiddenOverTime.map((d) => ({ ...d, category: "Forbidden" })), ]; @@ -228,124 +242,160 @@ export default async function APIKeyDetailPage(props: { }); return ( -
-
- - Back to API Keys listing - - - - -
+
+ + }> + APIs + + {api.name} + + + + {key.id} + + + + + {key.id} + + -
- - - } - /> - } - /> - } - /> - - - + + + -
-

Verifications

- -
- + +
+
+ + Back to API Keys listing + + + +
-
- {verificationsData.some(({ y }) => y > 0) ? ( - - -
- - - - +
+ + } /> - - + + } + /> + } + /> + + + + +
+

Verifications

+ +
+
- - - = 1000 * 60 * 60 * 24 * 30 - ? "month" - : granularity >= 1000 * 60 * 60 * 24 - ? "day" - : "hour" - } - /> - - - ) : ( - - - - - Not used - - This key was not used in the last {interval} - - - )} +
+ + {verificationsData.some(({ y }) => y > 0) ? ( + + +
+ + + + + + + +
+
+ + = 1000 * 60 * 60 * 24 * 30 + ? "month" + : granularity >= 1000 * 60 * 60 * 24 + ? "day" + : "hour" + } + /> + +
+ ) : ( + + + + + Not used + + This key was not used in the last {interval} + + + )} + + {latestVerifications.val && latestVerifications.val.length > 0 ? ( + <> + +

+ Latest Verifications +

+ + + ) : null} - {latestVerifications.val && latestVerifications.val.length > 0 ? ( - <> -

- Latest Verifications -

- - - ) : null} +
+
+ + {Intl.NumberFormat().format(key.roles.length)} Roles{" "} + + + {Intl.NumberFormat().format(transientPermissionIds.size)} Permissions + +
+
+ Create New Role} + permissions={key.workspace.permissions} + /> + Create New Permission} /> +
+
- -
-
- - {Intl.NumberFormat().format(key.roles.length)} Roles{" "} - - - {Intl.NumberFormat().format(transientPermissionIds.size)} Permissions - -
-
- Create New Role} - permissions={key.workspace.permissions} - /> - Create New Permission} /> +
- - -
+
); } diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx index 44dfb57fa8..4e551027c5 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx @@ -1,8 +1,13 @@ import { CopyButton } from "@/components/dashboard/copy-button"; +import { CreateKeyButton } from "@/components/dashboard/create-key-button"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Code } from "@/components/ui/code"; import { getTenantId } from "@/lib/auth"; import { and, db, eq, isNull, schema } from "@/lib/db"; +import { Nodes } from "@unkey/icons"; import { ArrowLeft } from "lucide-react"; import Link from "next/link"; import { notFound } from "next/navigation"; @@ -28,9 +33,9 @@ export default async function SettingsPage(props: Props) { const key = await db.query.keys.findFirst({ where: and(eq(schema.keys.id, props.params.keyId), isNull(schema.keys.deletedAt)), - with: { workspace: true, + keyAuth: { with: { api: true } }, }, }); if (!key || key.workspace.tenantId !== tenantId) { @@ -38,37 +43,72 @@ export default async function SettingsPage(props: Props) { } return ( -
- - API Key Overview - +
+ + }> + APIs + + {key.keyAuth.api.name} + + + + {key.id} + + + Settings + + + + + {key.id} + + + + + + + +
+ + API Key Overview + - - - - - - - - - - Key ID - This is your key id. It's used in some API calls. - - - -
{key.id}
-
- -
-
-
-
- + + + + + + + + + + Key ID + This is your key id. It's used in some API calls. + + + +
{key.id}
+
+ +
+
+
+
+ +
+
); } -// diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/page.tsx index 78dbfc00f7..73dc3a6766 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/page.tsx @@ -1,5 +1,10 @@ +import { CopyButton } from "@/components/dashboard/copy-button"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; +import { Badge } from "@/components/ui/badge"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { Nodes } from "@unkey/icons"; import { notFound } from "next/navigation"; import { CreateKey } from "./client"; @@ -24,11 +29,43 @@ export default async function CreateKeypage(props: { } return ( - +
+ + }> + APIs + + {keyAuth.api.name} + + + Keys + + + Create new key + + + + + {keyAuth.api.id} + + + + + + + + +
); } diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/page.tsx index fb55ea0a09..d98dc40fd9 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/page.tsx @@ -2,6 +2,14 @@ import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; import { notFound } from "next/navigation"; +import { CopyButton } from "@/components/dashboard/copy-button"; +import { CreateKeyButton } from "@/components/dashboard/create-key-button"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; +import { Badge } from "@/components/ui/badge"; +import { Nodes } from "@unkey/icons"; +import { navigation } from "../../constants"; import { Keys } from "./keys"; export const dynamic = "force-dynamic"; @@ -28,8 +36,37 @@ export default async function APIKeysPage(props: { } return ( -
- +
+ + }> + APIs + + {keyAuth.api.name} + + + Keys + + + + + {keyAuth.api.id} + + + + + + + + + +
+ +
+
); } diff --git a/apps/dashboard/app/(app)/apis/[apiId]/layout.tsx b/apps/dashboard/app/(app)/apis/[apiId]/layout.tsx deleted file mode 100644 index 35c2c14593..0000000000 --- a/apps/dashboard/app/(app)/apis/[apiId]/layout.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { CopyButton } from "@/components/dashboard/copy-button"; -import { CreateKeyButton } from "@/components/dashboard/create-key-button"; -import { Navbar } from "@/components/dashboard/navbar"; -import { PageHeader } from "@/components/dashboard/page-header"; -import { Badge } from "@/components/ui/badge"; -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { notFound } from "next/navigation"; -import type { PropsWithChildren } from "react"; - -type Props = PropsWithChildren<{ - params: { - apiId: string; - }; -}>; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -export default async function ApiPageLayout(props: Props) { - const tenantId = getTenantId(); - - const api = await db.query.apis.findFirst({ - where: (table, { eq, and, isNull }) => - and(eq(table.id, props.params.apiId), isNull(table.deletedAt)), - with: { - workspace: true, - }, - }); - if (!api || api.workspace.tenantId !== tenantId) { - return notFound(); - } - const navigation = [ - { - label: "Overview", - href: `/apis/${api.id}`, - segment: null, - }, - { - label: "Keys", - href: `/apis/${api.id}/keys/${api.keyAuthId}`, - segment: "keys", - }, - { - label: "API Settings", - href: `/apis/${api.id}/settings`, - segment: "settings", - }, - ]; - - return ( -
- - {api.id} - - , - , - ]} - /> - - - -
{props.children}
-
- ); -} diff --git a/apps/dashboard/app/(app)/apis/[apiId]/loading.tsx b/apps/dashboard/app/(app)/apis/[apiId]/loading.tsx index 550d892be5..2f3be9eef2 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/loading.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/loading.tsx @@ -1,7 +1,8 @@ +import { PageContent } from "@/components/page-content"; import { Skeleton } from "@/components/ui/skeleton"; export default async function Loading() { return ( -
+
@@ -55,6 +56,6 @@ export default async function Loading() {
-
+ ); } diff --git a/apps/dashboard/app/(app)/apis/[apiId]/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/page.tsx index 2e5fb67bcc..33f2c9fc20 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/page.tsx @@ -1,5 +1,11 @@ import { AreaChart, StackedColumnChart } from "@/components/dashboard/charts"; +import { CopyButton } from "@/components/dashboard/copy-button"; +import { CreateKeyButton } from "@/components/dashboard/create-key-button"; import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Metric } from "@/components/ui/metric"; import { Separator } from "@/components/ui/separator"; @@ -7,9 +13,12 @@ import { getTenantId } from "@/lib/auth"; import { clickhouse } from "@/lib/clickhouse"; import { db } from "@/lib/db"; import { formatNumber } from "@/lib/fmt"; +import { Nodes } from "@unkey/icons"; import { BarChart } from "lucide-react"; import { redirect } from "next/navigation"; +import { navigation } from "./constants"; import { type Interval, IntervalSelect } from "./select"; + export const dynamic = "force-dynamic"; export const runtime = "edge"; @@ -118,7 +127,10 @@ export default async function ApiPage(props: { ...ratelimitedOverTime.map((d) => ({ ...d, category: "Ratelimited" })), ...usageExceededOverTime.map((d) => ({ ...d, category: "Usage Exceeded" })), ...disabledOverTime.map((d) => ({ ...d, category: "Disabled" })), - ...insufficientPermissionsOverTime.map((d) => ({ ...d, category: "Insufficient Permissions" })), + ...insufficientPermissionsOverTime.map((d) => ({ + ...d, + category: "Insufficient Permissions", + })), ...expiredOverTime.map((d) => ({ ...d, category: "Expired" })), ...forbiddenOverTime.map((d) => ({ ...d, category: "Forbidden" })), ].sort((a, b) => new Date(a.x).getTime() - new Date(b.x).getTime()); @@ -129,138 +141,167 @@ export default async function ApiPage(props: { })); return ( -
- - - - sum + day.count, 0), - )} - /> - - - - +
+ + }> + APIs + + {api.name} + + + + + {api.id} + + + + + -
-

Verifications

- -
- -
-
+ + - {verificationsData.some((d) => d.y > 0) ? ( - - -
- sum + day.y, 0))} - /> - sum + day.y, 0))} - /> - sum + day.y, 0))} - /> - sum + day.y, 0))} - /> +
+ + + sum + day.y, 0), + verificationsInBillingCycle.reduce((sum, day) => sum + day.count, 0), )} /> sum + day.y, 0))} - /> - sum + day.y, 0))} + label={`Active Keys in ${new Date().toLocaleString("en-US", { + month: "long", + })}`} + value={formatNumber(activeKeysTotal?.keys ?? 0)} /> + + + + +
+

Verifications

+ +
+
- - - = 1000 * 60 * 60 * 24 * 30 - ? "month" - : granularity >= 1000 * 60 * 60 * 24 - ? "day" - : "hour" - } - /> - - - ) : ( - - - - - No usage - - Verify a key or change the range - - - )} +
- -
-

Active Keys

+ {verificationsData.some((d) => d.y > 0) ? ( + + +
+ sum + day.y, 0))} + /> + sum + day.y, 0))} + /> + sum + day.y, 0))} + /> + sum + day.y, 0))} + /> + sum + day.y, 0), + )} + /> + sum + day.y, 0))} + /> + sum + day.y, 0))} + /> +
+
+ + = 1000 * 60 * 60 * 24 * 30 + ? "month" + : granularity >= 1000 * 60 * 60 * 24 + ? "day" + : "hour" + } + /> + +
+ ) : ( + + + + + No usage + + Verify a key or change the range + + + )} -
- -
-
- {activeKeysOverTime.some((k) => k.y > 0) ? ( - - -
- + +
+

Active Keys

+ +
+
- - - = 1000 * 60 * 60 * 24 * 30 - ? "month" - : granularity >= 1000 * 60 * 60 * 24 - ? "day" - : "hour" - } - /> - - - ) : ( - - - - - No usage - - Verify a key or change the range - - - )} +
+ {activeKeysOverTime.some((k) => k.y > 0) ? ( + + +
+ +
+
+ + = 1000 * 60 * 60 * 24 * 30 + ? "month" + : granularity >= 1000 * 60 * 60 * 24 + ? "day" + : "hour" + } + /> + +
+ ) : ( + + + + + No usage + + Verify a key or change the range + + + )} +
+
); } diff --git a/apps/dashboard/app/(app)/apis/[apiId]/settings/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/settings/page.tsx index b22d545cc7..8f563649ed 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/settings/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/settings/page.tsx @@ -1,9 +1,16 @@ import { CopyButton } from "@/components/dashboard/copy-button"; +import { CreateKeyButton } from "@/components/dashboard/create-key-button"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Code } from "@/components/ui/code"; import { getTenantId } from "@/lib/auth"; import { db, eq, schema } from "@/lib/db"; +import { Nodes } from "@unkey/icons"; import { notFound, redirect } from "next/navigation"; +import { navigation } from "../constants"; import { DefaultBytes } from "./default-bytes"; import { DefaultPrefix } from "./default-prefix"; import { DeleteApi } from "./delete-api"; @@ -49,27 +56,56 @@ export default async function SettingsPage(props: Props) { } return ( -
- - - - - - - API ID - This is your api id. It's used in some API calls. - - - -
{api.id}
-
- -
-
-
-
- - +
+ + }> + APIs + + {api.name} + + + Settings + + + + + {api.id} + + + + + + + + + +
+ + + + + + + API ID + This is your api id. It's used in some API calls. + + + +
{api.id}
+
+ +
+
+
+
+ + +
+
); } diff --git a/apps/dashboard/app/(app)/apis/client.tsx b/apps/dashboard/app/(app)/apis/client.tsx index a2633aec8c..eee7cdd620 100644 --- a/apps/dashboard/app/(app)/apis/client.tsx +++ b/apps/dashboard/app/(app)/apis/client.tsx @@ -1,7 +1,6 @@ "use client"; import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; import { PostHogIdentify } from "@/providers/PostHogProvider"; import { useUser } from "@clerk/nextjs"; import { Button } from "@unkey/ui"; @@ -19,19 +18,22 @@ type ApiWithKeys = { export function ApiList({ apis }: { apis: ApiWithKeys }) { const { user, isLoaded } = useUser(); + + const [localData, setLocalData] = useState(apis); + + if (isLoaded && user) { + PostHogIdentify({ user }); + } + useEffect(() => { if (apis.length) { setLocalData(apis); } }, [apis]); - const [localData, setLocalData] = useState(apis); - if (isLoaded && user) { - PostHogIdentify({ user }); - } + return (
- -
+
-
{apis.length ? (
    diff --git a/apps/dashboard/app/(app)/apis/loading.tsx b/apps/dashboard/app/(app)/apis/loading.tsx index eb0de19b6f..cde2adf520 100644 --- a/apps/dashboard/app/(app)/apis/loading.tsx +++ b/apps/dashboard/app/(app)/apis/loading.tsx @@ -1,8 +1,9 @@ +import { PageContent } from "@/components/page-content"; import { Skeleton } from "@/components/ui/skeleton"; export default function Loading() { return ( -
    +
    @@ -38,6 +39,6 @@ export default function Loading() {
    -
+ ); } diff --git a/apps/dashboard/app/(app)/apis/page.tsx b/apps/dashboard/app/(app)/apis/page.tsx index 173ab966b0..d3ccf44c40 100644 --- a/apps/dashboard/app/(app)/apis/page.tsx +++ b/apps/dashboard/app/(app)/apis/page.tsx @@ -1,9 +1,10 @@ -import { PageHeader } from "@/components/dashboard/page-header"; import { CreateApiButton } from "./create-api-button"; -import { Separator } from "@/components/ui/separator"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { getTenantId } from "@/lib/auth"; import { and, db, eq, isNull, schema, sql } from "@/lib/db"; +import { Nodes } from "@unkey/icons"; import { Search } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -42,40 +43,49 @@ export default async function ApisOverviewPage() { const unpaid = workspace.tenantId.startsWith("org_") && workspace.plan === "free"; return ( -
- {unpaid ? ( -
- - -
-
- - +
+ + }> + APIs + + + + + + + {unpaid ? ( +
+
+
+ + +
+ +
+
+

+ Upgrade your plan +

+

+ Team workspaces is a paid feature. Please switch to a paid plan to continue using + it. +

+ + Subscribe +
- -
-
-

- Upgrade your plan -

-

- Team workspaces is a paid feature. Please switch to a paid plan to continue using it. -

- - Subscribe -
-
- ) : ( - - )} + ) : ( + + )} +
); } diff --git a/apps/dashboard/app/(app)/audit/[bucket]/bucket-select.tsx b/apps/dashboard/app/(app)/audit/[bucket]/bucket-select.tsx index b24aa0e18d..f42f4a2424 100644 --- a/apps/dashboard/app/(app)/audit/[bucket]/bucket-select.tsx +++ b/apps/dashboard/app/(app)/audit/[bucket]/bucket-select.tsx @@ -10,6 +10,7 @@ import { import { useRouter } from "next/navigation"; import type React from "react"; import { useTransition } from "react"; + type Props = { selected: string; ratelimitNamespaces: { id: string; name: string }[]; diff --git a/apps/dashboard/app/(app)/audit/[bucket]/filter.tsx b/apps/dashboard/app/(app)/audit/[bucket]/filter.tsx index ff4444902e..58e8efa5ee 100644 --- a/apps/dashboard/app/(app)/audit/[bucket]/filter.tsx +++ b/apps/dashboard/app/(app)/audit/[bucket]/filter.tsx @@ -17,6 +17,7 @@ import { Button } from "@unkey/ui"; import { Check, ChevronDown } from "lucide-react"; import { parseAsArrayOf, parseAsString, useQueryState } from "nuqs"; import type React from "react"; + type Props = { options: { value: string; label: string }[]; title: string; @@ -33,6 +34,13 @@ export const Filter: React.FC = ({ options, title, param }) => { }), ); + const handleSelection = (optionValue: string, isSelected: boolean) => { + const next = isSelected + ? selected.filter((v) => v !== optionValue) + : Array.from(new Set([...selected, optionValue])); + setSelected(next); + }; + return ( @@ -72,11 +80,12 @@ export const Filter: React.FC = ({ options, title, param }) => { const isSelected = selected.includes(option.value); return (
{ - const next = isSelected - ? selected.filter((v) => v !== option.value) - : Array.from(new Set([...selected, option.value])); - setSelected(next); + onClick={() => handleSelection(option.value, isSelected)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleSelection(option.value, isSelected); + } }} > }; +type AuditLogWithTargets = SelectAuditLog & { + targets: Array; +}; /** * Parse searchParam string arrays @@ -116,79 +120,91 @@ export default async function AuditPage(props: Props) { return (
- -
-
- - ({ - value, - label: value, - })) - : [ - { value: "ratelimit.success", label: "Ratelimit success" }, - { value: "ratelimit.denied", label: "Ratelimit denied" }, - ] - } - /> + + }> + Audit + + {workspace.ratelimitNamespaces.find((ratelimit) => ratelimit.id === props.params.bucket) + ?.name ?? props.params.bucket} + + + + +
+
+ + ({ + value, + label: value, + })) + : [ + { + value: "ratelimit.success", + label: "Ratelimit success", + }, + { value: "ratelimit.denied", label: "Ratelimit denied" }, + ] + } + /> - {props.params.bucket === "unkey_mutations" ? ( - }> - + {props.params.bucket === "unkey_mutations" ? ( + }> + + + ) : null} + }> + - ) : null} - }> - + {selectedEvents.length > 0 || + selectedUsers.length > 0 || + selectedRootKeys.length > 0 ? ( + + + + ) : null} +
+ + + + + + } + > + {!bucket ? ( + + + + + Bucket Not Found + + The specified audit log bucket does not exist or you do not have access to it. + + + ) : ( + + )} - {selectedEvents.length > 0 || selectedUsers.length > 0 || selectedRootKeys.length > 0 ? ( - - - - ) : null} -
- - - - - - } - > - {!bucket ? ( - - - - - Bucket Not Found - - The specified audit log bucket does not exist or you do not have access to it. - - - ) : ( - - )} - -
+ +
); } diff --git a/apps/dashboard/app/(app)/authorization/constants.ts b/apps/dashboard/app/(app)/authorization/constants.ts new file mode 100644 index 0000000000..6739597796 --- /dev/null +++ b/apps/dashboard/app/(app)/authorization/constants.ts @@ -0,0 +1,12 @@ +export const navigation = [ + { + label: "Roles", + href: "/authorization/roles", + segment: "roles", + }, + { + label: "Permissions", + href: "/authorization/permissions", + segment: "permissions", + }, +]; diff --git a/apps/dashboard/app/(app)/authorization/layout.tsx b/apps/dashboard/app/(app)/authorization/layout.tsx index 70fcb4677b..029be01d9a 100644 --- a/apps/dashboard/app/(app)/authorization/layout.tsx +++ b/apps/dashboard/app/(app)/authorization/layout.tsx @@ -1,6 +1,5 @@ import type * as React from "react"; -import { Navbar } from "@/components/dashboard/navbar"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; import { redirect } from "next/navigation"; @@ -21,24 +20,5 @@ export default async function AuthorizationLayout({ return redirect("/auth/sign-in"); } - const navigation = [ - { - label: "Roles", - href: "/authorization/roles", - segment: "roles", - }, - { - label: "Permissions", - href: "/authorization/permissions", - segment: "permissions", - }, - ]; - - return ( -
- - -
{children}
-
- ); + return children; } diff --git a/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/page.tsx b/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/page.tsx index 44259688a7..773c714bff 100644 --- a/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/page.tsx +++ b/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/page.tsx @@ -1,18 +1,14 @@ import { CopyButton } from "@/components/dashboard/copy-button"; -import { PageHeader } from "@/components/dashboard/page-header"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Badge } from "@/components/ui/badge"; +import { Metric } from "@/components/ui/metric"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { ShieldKey } from "@unkey/icons"; import { Button } from "@unkey/ui"; -import { - Activity, - CalendarPlus, - KeySquare, - type LucideIcon, - Minus, - SquareStack, -} from "lucide-react"; +import { format } from "date-fns"; import { notFound, redirect } from "next/navigation"; import { Client } from "./client"; import { DeletePermission } from "./delete-permission"; @@ -73,11 +69,24 @@ export default async function RolesPage(props: Props) { const shouldShowTooltip = permission.name.length > 16; return ( -
- {permission.name}} - description={permission.description ?? undefined} - actions={[ +
+ + }> + + Authorization + + + Permissions + + + {props.params.permissionId} + + + )} - , + {permission.id} - , + Delete} permission={permission} - />, - ]} - /> - -
-
- - - - + />{" "} + + + +
+
+
+ + + + +
+ +
- -
+
); } - -const Metric: React.FC<{ label: string; value?: string; Icon: LucideIcon }> = ({ - label, - value, - Icon, -}) => { - return ( -
- -
-

{label}

-
- {value ?? } -
-
-
- ); -}; diff --git a/apps/dashboard/app/(app)/authorization/permissions/page.tsx b/apps/dashboard/app/(app)/authorization/permissions/page.tsx index 140b96b733..0480d246b6 100644 --- a/apps/dashboard/app/(app)/authorization/permissions/page.tsx +++ b/apps/dashboard/app/(app)/authorization/permissions/page.tsx @@ -1,12 +1,17 @@ import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Badge } from "@/components/ui/badge"; import { getTenantId } from "@/lib/auth"; import { asc, db } from "@/lib/db"; import { permissions } from "@unkey/db/src/schema"; +import { ShieldKey } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { ChevronRight, Scan } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; +import { navigation } from "../constants"; import { CreateNewPermission } from "./create-new-permission"; export const revalidate = 0; @@ -51,61 +56,85 @@ export default async function RolesPage() { return permission; }); return ( -
-
-

Permissions

-
+
+ + }> + + Authorization + + + Permissions + + + {Intl.NumberFormat().format(workspace.permissions.length)} /{" "} {Intl.NumberFormat().format(Number.POSITIVE_INFINITY)} used{" "} Create New Permission} /> -
-
- {workspace.permissions.length === 0 ? ( - - - - - No permissions found - Create your first permission - Create New Permission} /> - - ) : ( -
    - {workspace.permissions.map((p) => ( - -
    -
    {p.name}
    - {p.description} -
    + + -
    - - {Intl.NumberFormat(undefined, { notation: "compact" }).format(p.roles.length)}{" "} - Role - {p.roles.length !== 1 ? "s" : ""} - + + +
    +
    + {workspace.permissions.length === 0 ? ( + + + + + No permissions found + + Create your first permission + + Create New Permission} + /> + + ) : ( +
      + {workspace.permissions.map((p) => ( + +
      +
      {p.name}
      + {p.description} +
      - - {Intl.NumberFormat(undefined, { notation: "compact" }).format(p.keys.length)} Key - {p.keys.length !== 1 ? "s" : ""} - -
    +
    + + {Intl.NumberFormat(undefined, { + notation: "compact", + }).format(p.roles.length)}{" "} + Role + {p.roles.length !== 1 ? "s" : ""} + -
    - -
    - - ))} -
- )} + + {Intl.NumberFormat(undefined, { + notation: "compact", + }).format(p.keys.length)}{" "} + Key + {p.keys.length !== 1 ? "s" : ""} + +
+ +
+ +
+ + ))} + + )} +
+
+
); } diff --git a/apps/dashboard/app/(app)/authorization/roles/[roleId]/page.tsx b/apps/dashboard/app/(app)/authorization/roles/[roleId]/page.tsx index 222c4b8c71..2037a639f1 100644 --- a/apps/dashboard/app/(app)/authorization/roles/[roleId]/page.tsx +++ b/apps/dashboard/app/(app)/authorization/roles/[roleId]/page.tsx @@ -1,5 +1,8 @@ +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { ShieldKey } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { notFound, redirect } from "next/navigation"; import { DeleteRole } from "./delete-role"; @@ -106,23 +109,39 @@ export default async function RolesPage(props: Props) { const sortedNestedPermissions = sortNestedPermissions(nested); return ( -
-
-
-
-

- {role.name} -

-
-

{role.description}

-
-
+
+ + }> + + Authorization + + Roles + + {props.params.roleId} + + + Update Role} /> Delete Role} /> + + + +
+
+
+
+

{role.name}

+
+

{role.description}

+
+
+
-
- - +
); } diff --git a/apps/dashboard/app/(app)/authorization/roles/[roleId]/tree.tsx b/apps/dashboard/app/(app)/authorization/roles/[roleId]/tree.tsx index 8e5746694d..eafe23b387 100644 --- a/apps/dashboard/app/(app)/authorization/roles/[roleId]/tree.tsx +++ b/apps/dashboard/app/(app)/authorization/roles/[roleId]/tree.tsx @@ -87,7 +87,7 @@ export const RecursivePermission: React.FC< ); } return ( - +
{k}
diff --git a/apps/dashboard/app/(app)/authorization/roles/page.tsx b/apps/dashboard/app/(app)/authorization/roles/page.tsx index 77a5fb9739..13a7c21aa2 100644 --- a/apps/dashboard/app/(app)/authorization/roles/page.tsx +++ b/apps/dashboard/app/(app)/authorization/roles/page.tsx @@ -1,11 +1,16 @@ import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Badge } from "@/components/ui/badge"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { ShieldKey } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { ChevronRight, Scan } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; +import { navigation } from "../constants"; import { CreateNewRole } from "./create-new-role"; export const revalidate = 0; @@ -51,10 +56,17 @@ export default async function RolesPage() { }); return ( -
-
-

Roles

-
+
+ + }> + + Authorization + + + Roles + + + {Intl.NumberFormat().format(workspace.roles.length)} /{" "} {Intl.NumberFormat().format(Number.POSITIVE_INFINITY)} used{" "} @@ -63,58 +75,70 @@ export default async function RolesPage() { trigger={} permissions={workspace.permissions} /> -
-
+ + - {workspace.roles.length === 0 ? ( - - - - - No roles found - Create your first role - Create New Role} - permissions={workspace.permissions} - /> - - ) : ( -
    - {workspace.roles.map((r) => ( - -
    -
    {r.name}
    - {r.description} -
    + + +
    +
    + {workspace.roles.length === 0 ? ( + + + + + No roles found + Create your first role + Create New Role} + permissions={workspace.permissions} + /> + + ) : ( +
      + {workspace.roles.map((r) => ( + +
      +
      {r.name}
      + + {r.description} + +
      -
      - - {Intl.NumberFormat(undefined, { notation: "compact" }).format( - r.permissions.length, - )}{" "} - Permission - {r.permissions.length !== 1 ? "s" : ""} - +
      + + {Intl.NumberFormat(undefined, { + notation: "compact", + }).format(r.permissions.length)}{" "} + Permission + {r.permissions.length !== 1 ? "s" : ""} + - - {Intl.NumberFormat(undefined, { notation: "compact" }).format(r.keys.length)} Key - {r.keys.length !== 1 ? "s" : ""} - -
      + + {Intl.NumberFormat(undefined, { + notation: "compact", + }).format(r.keys.length)}{" "} + Key + {r.keys.length !== 1 ? "s" : ""} + +
      -
      - -
      - - ))} -
    - )} +
    + +
    + + ))} +
+ )} +
+
+
); } diff --git a/apps/dashboard/app/(app)/identities/[identityId]/page.tsx b/apps/dashboard/app/(app)/identities/[identityId]/page.tsx index 7e7c15ffd9..ebb03aa9f4 100644 --- a/apps/dashboard/app/(app)/identities/[identityId]/page.tsx +++ b/apps/dashboard/app/(app)/identities/[identityId]/page.tsx @@ -1,6 +1,8 @@ import { notFound } from "next/navigation"; import { CopyButton } from "@/components/dashboard/copy-button"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Code } from "@/components/ui/code"; @@ -15,6 +17,7 @@ import { import { getTenantId } from "@/lib/auth"; import { clickhouse } from "@/lib/clickhouse"; import { db } from "@/lib/db"; +import { Fingerprint } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { ChevronRight, Minus } from "lucide-react"; import ms from "ms"; @@ -54,115 +57,140 @@ export default async function Page(props: Props) { } return ( -
-
-
- Identity ID: - + + }> + Identities + - {identity.id} - - -
-
- External ID: + {props.params.identityId} + + + + +
+
+
+ Identity ID: + + {identity.id} + + +
+
+ External ID: - - {identity.externalId} - - -
-
-

Meta

- {identity.meta ? ( - {JSON.stringify(identity.meta, null, 2)} - ) : ( - - No metadata - This identity has no metadata. - - )} + + {identity.externalId} + + +
+
+

Meta

+ {identity.meta ? ( + {JSON.stringify(identity.meta, null, 2)} + ) : ( + + No metadata + This identity has no metadata. + + )} -

Ratelimits

- {identity.ratelimits.length === 0 ? ( - - No ratelimits - This identity has no ratelimits attached. - - ) : ( - - - - Name - Limit - Duration - - - - {identity.ratelimits.map((ratelimit) => ( - - {ratelimit.name} - - {Intl.NumberFormat(undefined, { notation: "compact" }).format(ratelimit.limit)} - - {ms(ratelimit.duration)} - - ))} - -
- )} -

Keys

- {identity.keys.length === 0 ? ( - - No keys - This identity has no keys attached. - - ) : ( - - - - ID - Meta - Last Used - - - - - {identity.keys.map((key) => ( - - {key.id} - - {key.meta ? ( - JSON.stringify(JSON.parse(key.meta), null, 2) - ) : ( - - )} - - - - - - - - - ))} - -
- )} +

Ratelimits

+ {identity.ratelimits.length === 0 ? ( + + No ratelimits + This identity has no ratelimits attached. + + ) : ( + + + + Name + Limit + Duration + + + + {identity.ratelimits.map((ratelimit) => ( + + {ratelimit.name} + + {Intl.NumberFormat(undefined, { + notation: "compact", + }).format(ratelimit.limit)} + + {ms(ratelimit.duration)} + + ))} + +
+ )} +

Keys

+ {identity.keys.length === 0 ? ( + + No keys + This identity has no keys attached. + + ) : ( + + + + ID + Meta + Last Used + + + + + {identity.keys.map((key) => ( + + {key.id} + + {key.meta ? ( + JSON.stringify(JSON.parse(key.meta), null, 2) + ) : ( + + )} + + + + + + + + + ))} + +
+ )} +
+
); } -const LastUsed: React.FC<{ workspaceId: string; keySpaceId: string; keyId: string }> = async ( - props, -) => { +const LastUsed: React.FC<{ + workspaceId: string; + keySpaceId: string; + keyId: string; +}> = async (props) => { const lastUsed = await clickhouse.verifications .latest({ workspaceId: props.workspaceId, diff --git a/apps/dashboard/app/(app)/identities/layout.tsx b/apps/dashboard/app/(app)/identities/layout.tsx deleted file mode 100644 index 165b1b041f..0000000000 --- a/apps/dashboard/app/(app)/identities/layout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type * as React from "react"; - -import { OptIn } from "@/components/opt-in"; -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { redirect } from "next/navigation"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -export default async function AuthorizationLayout({ - children, -}: { - children: React.ReactNode; -}) { - const tenantId = getTenantId(); - const workspace = await db.query.workspaces.findFirst({ - where: (table, { eq }) => eq(table.tenantId, tenantId), - }); - if (!workspace) { - return redirect("/auth/sign-in"); - } - - if (!workspace.betaFeatures.identities) { - children = ( - - ); - } - return ( -
-
{children}
-
- ); -} diff --git a/apps/dashboard/app/(app)/identities/page.tsx b/apps/dashboard/app/(app)/identities/page.tsx index 849f1cb592..a388709b19 100644 --- a/apps/dashboard/app/(app)/identities/page.tsx +++ b/apps/dashboard/app/(app)/identities/page.tsx @@ -1,9 +1,13 @@ import { redirect } from "next/navigation"; import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; +import { Navbar } from "@/components/navbar"; +import { OptIn } from "@/components/opt-in"; +import { PageContent } from "@/components/page-content"; import { Table, TableBody, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { Fingerprint } from "@unkey/icons"; import { Loader2 } from "lucide-react"; import { unstable_cache as cache } from "next/cache"; import { parseAsInteger, parseAsString } from "nuqs/server"; @@ -21,23 +25,44 @@ export default async function Page(props: Props) { const search = parseAsString.withDefault("").parse(props.searchParams.search ?? ""); const limit = parseAsInteger.withDefault(10).parse(props.searchParams.limit ?? "10"); - return ( -
- + const tenantId = getTenantId(); + const workspace = await db.query.workspaces.findFirst({ + where: (table, { eq }) => eq(table.tenantId, tenantId), + }); + + if (!workspace) { + return redirect("/auth/sign-in"); + } + + if (!workspace.betaFeatures.identities) { + return ; + } -
- - - - - - } - > - - -
+ return ( +
+ + }> + + Identities + + + + + +
+ + + + + + } + > + + +
+
); } diff --git a/apps/dashboard/app/(app)/layout.tsx b/apps/dashboard/app/(app)/layout.tsx index 18ec728800..605f8cd54d 100644 --- a/apps/dashboard/app/(app)/layout.tsx +++ b/apps/dashboard/app/(app)/layout.tsx @@ -10,10 +10,9 @@ import { MobileSideBar } from "./mobile-sidebar"; interface LayoutProps { children: React.ReactNode; - breadcrumb: React.ReactNode; } -export default async function Layout({ children, breadcrumb }: LayoutProps) { +export default async function Layout({ children }: LayoutProps) { const tenantId = getTenantId(); const workspace = await db.query.workspaces.findFirst({ where: (table, { and, eq, isNull }) => @@ -43,13 +42,9 @@ export default async function Layout({ children, breadcrumb }: LayoutProps) { />
-
+
{workspace.enabled ? ( - <> - {/* Hacky way to make the breadcrumbs line up with the Teamswitcher on the left, because that also has h12 */} - {breadcrumb &&
{breadcrumb}
} - {children} - + children ) : (
diff --git a/apps/dashboard/app/(app)/logs/logs-page.tsx b/apps/dashboard/app/(app)/logs/logs-page.tsx index 67b1c40bc1..d9aa08ae77 100644 --- a/apps/dashboard/app/(app)/logs/logs-page.tsx +++ b/apps/dashboard/app/(app)/logs/logs-page.tsx @@ -1,5 +1,8 @@ "use client"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import type { Log, LogsTimeseriesDataPoint } from "@unkey/clickhouse/src/logs"; +import { Layers3 } from "@unkey/icons"; import { LogsChart } from "./components/charts"; import { LogsFilters } from "./components/filters"; import { LogsTable } from "./components/table/logs-table"; @@ -12,13 +15,22 @@ export function LogsPage({ initialTimeseries: LogsTimeseriesDataPoint[]; }) { return ( -
- -
- - {/* Chart is using more space than it should to be able to display tooltip correctly so we can -margin that empty space */} -
- +
+ + }> + Logs + + + +
+ +
+ + {/* Chart is using more space than it should to be able to display tooltip correctly so we can -margin that empty space */} +
+ +
+
); } diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/constants.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/constants.ts new file mode 100644 index 0000000000..361fdb74a1 --- /dev/null +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/constants.ts @@ -0,0 +1,23 @@ +export const navigation = (namespaceId: string) => [ + { + label: "Overview", + href: `/ratelimits/${namespaceId}`, + segment: "overview", + }, + + { + label: "Settings", + href: `/ratelimits/${namespaceId}/settings`, + segment: "settings", + }, + { + label: "Logs", + href: `/ratelimits/${namespaceId}/logs`, + segment: "logs", + }, + { + label: "Overrides", + href: `/ratelimits/${namespaceId}/overrides`, + segment: "overrides", + }, +]; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/layout.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/layout.tsx deleted file mode 100644 index 7eebd35dde..0000000000 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/layout.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { CopyButton } from "@/components/dashboard/copy-button"; -import { Navbar } from "@/components/dashboard/navbar"; -import { PageHeader } from "@/components/dashboard/page-header"; -import { Badge } from "@/components/ui/badge"; -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { notFound } from "next/navigation"; -import type { PropsWithChildren } from "react"; - -type Props = PropsWithChildren<{ - params: { - namespaceId: string; - }; -}>; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -export default async function RatelimitNamespacePageLayout(props: Props) { - const tenantId = getTenantId(); - - const namespace = await db.query.ratelimitNamespaces.findFirst({ - where: (table, { eq, and, isNull }) => - and(eq(table.id, props.params.namespaceId), isNull(table.deletedAt)), - with: { - workspace: true, - }, - }); - if (!namespace || namespace.workspace.tenantId !== tenantId) { - return notFound(); - } - const navigation = [ - { - label: "Overview", - href: `/ratelimits/${namespace.id}`, - segment: null, - }, - - { - label: "Settings", - href: `/ratelimits/${namespace.id}/settings`, - segment: "settings", - }, - { - label: "Logs", - href: `/ratelimits/${namespace.id}/logs`, - segment: "logs", - }, - { - label: "Overrides", - href: `/ratelimits/${namespace.id}/overrides`, - segment: "overrides", - }, - ]; - - return ( -
- - {namespace.id} - - , - ]} - /> - - - -
{props.children}
-
- ); -} diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/page.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/page.tsx index 49b3931962..277f26d626 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/page.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/page.tsx @@ -2,8 +2,12 @@ import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; import { notFound } from "next/navigation"; +import { CopyButton } from "@/components/dashboard/copy-button"; import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; import { Loading } from "@/components/dashboard/loading"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { TimestampInfo } from "@/components/timestamp-info"; import { Badge } from "@/components/ui/badge"; import { @@ -15,11 +19,14 @@ import { TableRow, } from "@/components/ui/table"; import { clickhouse } from "@/lib/clickhouse"; +import { Gauge } from "@unkey/icons"; import { Box, Check, X } from "lucide-react"; import { parseAsArrayOf, parseAsBoolean, parseAsIsoDateTime, parseAsString } from "nuqs/server"; import { Suspense } from "react"; +import { navigation } from "../constants"; import { Filters } from "./filter"; import { Menu } from "./menu"; + export const dynamic = "force-dynamic"; export const runtime = "edge"; @@ -67,25 +74,49 @@ export default async function AuditPage(props: Props) { return (
-
- + + }> + Ratelimits + + {namespace.name} + + + Logs{" "} + + + + + {props.params.namespaceId} + + + + + + - - - - - - } - > - - -
+
+ + + + + + + } + > + + +
+
); } diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/overrides/[overrideId]/page.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/overrides/[overrideId]/page.tsx index 5c3763568f..7f0af67713 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/overrides/[overrideId]/page.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/overrides/[overrideId]/page.tsx @@ -1,8 +1,11 @@ -import { BackLink } from "@/components/back"; import { CopyButton } from "@/components/dashboard/copy-button"; +import { PageHeader } from "@/components/dashboard/page-header"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Badge } from "@/components/ui/badge"; import { getTenantId } from "@/lib/auth"; import { db, schema } from "@/lib/db"; +import { Gauge } from "@unkey/icons"; import { notFound } from "next/navigation"; import { UpdateCard } from "./settings"; @@ -28,6 +31,7 @@ export default async function OverrideSettings(props: Props) { ), with: { workspace: true, + namespace: true, }, }); if (!override || override.workspace.tenantId !== tenantId) { @@ -35,23 +39,61 @@ export default async function OverrideSettings(props: Props) { } return ( -
- - - {override.identifier} - - - +
+ + }> + Ratelimits + + {override.namespace.name} + + + Overrides + + + {override.identifier} + + + + + {override.namespace.id} + + + + + +
+ + {override.identifier} + + , + ]} + /> + +
+
); } diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/overrides/page.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/overrides/page.tsx index 521dcfdd70..e98849d81c 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/overrides/page.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/overrides/page.tsx @@ -1,11 +1,16 @@ +import { CopyButton } from "@/components/dashboard/copy-button"; import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; import { PageHeader } from "@/components/dashboard/page-header"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Badge } from "@/components/ui/badge"; -import { Separator } from "@/components/ui/separator"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { Gauge } from "@unkey/icons"; import { Scan } from "lucide-react"; import { notFound } from "next/navigation"; +import { navigation } from "../constants"; import { CreateNewOverride } from "./create-new-override"; import { Overrides } from "./table"; @@ -49,37 +54,67 @@ export default async function OverridePage(props: Props) { } return ( - <> - - {Intl.NumberFormat().format(namespace.overrides.length)} /{" "} - {Intl.NumberFormat().format(namespace.workspace.features.ratelimitOverrides ?? 5)} used{" "} - , - ]} - /> - +
+ + }> + Ratelimits + + {namespace.name} + + + Overrides + + + + + {namespace.id} + + + + + + - +
+ + {Intl.NumberFormat().format(namespace.overrides.length)} /{" "} + {Intl.NumberFormat().format(namespace.workspace.features.ratelimitOverrides ?? 5)}{" "} + used{" "} + , + ]} + /> - {namespace.overrides.length === 0 ? ( - - - - - No custom ratelimits found - - Create your first override below - - - ) : ( - - )} - + + {namespace.overrides.length === 0 ? ( + + + + + No custom ratelimits found + + Create your first override below + + + ) : ( + + )} +
+
+
); } diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/page.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/page.tsx index b3e181d4fe..8188fc2c94 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/page.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/page.tsx @@ -1,6 +1,10 @@ import { StackedColumnChart } from "@/components/dashboard/charts"; import { CopyButton } from "@/components/dashboard/copy-button"; import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Code } from "@/components/ui/code"; import { Metric } from "@/components/ui/metric"; @@ -9,10 +13,12 @@ import { getTenantId } from "@/lib/auth"; import { clickhouse } from "@/lib/clickhouse"; import { db, eq, schema, sql } from "@/lib/db"; import { formatNumber } from "@/lib/fmt"; +import { Gauge } from "@unkey/icons"; import { BarChart } from "lucide-react"; import ms from "ms"; import { redirect } from "next/navigation"; import { parseAsArrayOf, parseAsString, parseAsStringEnum } from "nuqs/server"; +import { navigation } from "./constants"; import { Filters, type Interval } from "./filters"; export const dynamic = "force-dynamic"; @@ -95,13 +101,6 @@ export default async function RatelimitNamespacePage(props: { ratelimitedOverTime.push({ x, y: d.total - d.passed }); } - // const dataOverTime = [ - // ...ratelimitedOverTime.map((d) => ({ ...d, category: "Ratelimited" })), - // ...successOverTime.map((d) => ({ - // ...d, - // category: "Successful", - // })), - // ]; const dataOverTime = ratelimitEvents.flatMap((d) => [ { x: new Date(d.time).toISOString(), @@ -115,11 +114,6 @@ export default async function RatelimitNamespacePage(props: { }, ]); - // const activeKeysOverTime = activeKeys.data.map(({ time, keys }) => ({ - // x: new Date(time).toISOString(), - // y: keys, - // })); - const snippet = `curl -XPOST 'https://api.unkey.dev/v1/ratelimits.limit' \\ -H 'Content-Type: application/json' \\ -H 'Authorization: Bearer ' \\ @@ -130,100 +124,122 @@ export default async function RatelimitNamespacePage(props: { "duration": 10000 }'`; return ( -
- - - - sum + day.passed, 0))} - /> - - - {/* */} - - - - -
-
-

- Requests -

-
- - -
- - {dataOverTime.some((d) => d.y > 0) ? ( - - -
- sum + day.passed, 0))} - /> +
+ + }> + Ratelimits + + {namespace.name} + + + + + {props.params.namespaceId} + + + + + + +
+ + + sum + (day.total - day.passed), 0), + ratelimitsInBillingCycle.reduce((sum, day) => sum + day.passed, 0), )} /> sum + day.total, 0))} - /> - sum + day.passed, 0) / - ratelimitEvents.reduce((sum, day) => sum + day.total, 0)) * - 100, - )}%`} + label="Last used" + value={lastUsed ? `${ms(Date.now() - lastUsed)} ago` : "never"} /> + + + + +
+
+

+ Requests +

- - - = 1000 * 60 * 60 * 24 * 30 - ? "month" - : granularity >= 1000 * 60 * 60 * 24 - ? "day" - : granularity >= 1000 * 60 * 60 - ? "hour" - : "minute" - } - /> - - - ) : ( - - - - - No usage - - Ratelimit something or change the range - - - {snippet} - - - - )} + + +
+ + {dataOverTime.some((d) => d.y > 0) ? ( + + +
+ sum + day.passed, 0))} + /> + sum + (day.total - day.passed), 0), + )} + /> + sum + day.total, 0))} + /> + sum + day.passed, 0) / + ratelimitEvents.reduce((sum, day) => sum + day.total, 0)) * + 100, + )}%`} + /> +
+
+ + = 1000 * 60 * 60 * 24 * 30 + ? "month" + : granularity >= 1000 * 60 * 60 * 24 + ? "day" + : granularity >= 1000 * 60 * 60 + ? "hour" + : "minute" + } + /> + +
+ ) : ( + + + + + No usage + + Ratelimit something or change the range + + + {snippet} + + + + )} +
+
); } diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/settings/page.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/settings/page.tsx index 0db9450eaf..031660f178 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/settings/page.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/settings/page.tsx @@ -1,9 +1,15 @@ import { CopyButton } from "@/components/dashboard/copy-button"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Code } from "@/components/ui/code"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { Gauge } from "@unkey/icons"; import { notFound, redirect } from "next/navigation"; +import { navigation } from "../constants"; import { DeleteNamespace } from "./delete-namespace"; import { UpdateNamespaceName } from "./update-namespace-name"; @@ -27,34 +33,66 @@ export default async function SettingsPage(props: Props) { }, }, }); + if (!workspace || workspace.tenantId !== tenantId) { return redirect("/new"); } + const namespace = workspace.ratelimitNamespaces.find( (namespace) => namespace.id === props.params.namespaceId, ); + if (!namespace) { return notFound(); } return ( -
- - - - Namespace ID - This is your namespace id. It's used in some API calls. - - - -
{namespace.id}
-
- -
-
-
-
- +
+ + }> + Ratelimits + + {namespace.name} + + + Settings{" "} + + + + + {props.params.namespaceId} + + + + + + + +
+ + + + Namespace ID + + This is your namespace id. It's used in some API calls. + + + + +
{namespace.id}
+
+ +
+
+
+
+ +
+
); } diff --git a/apps/dashboard/app/(app)/ratelimits/loading.tsx b/apps/dashboard/app/(app)/ratelimits/loading.tsx index eb0de19b6f..cde2adf520 100644 --- a/apps/dashboard/app/(app)/ratelimits/loading.tsx +++ b/apps/dashboard/app/(app)/ratelimits/loading.tsx @@ -1,8 +1,9 @@ +import { PageContent } from "@/components/page-content"; import { Skeleton } from "@/components/ui/skeleton"; export default function Loading() { return ( -
+
@@ -38,6 +39,6 @@ export default function Loading() {
-
+ ); } diff --git a/apps/dashboard/app/(app)/ratelimits/page.tsx b/apps/dashboard/app/(app)/ratelimits/page.tsx index 2e55b47e65..6b87aa2cda 100644 --- a/apps/dashboard/app/(app)/ratelimits/page.tsx +++ b/apps/dashboard/app/(app)/ratelimits/page.tsx @@ -1,11 +1,11 @@ -import { PageHeader } from "@/components/dashboard/page-header"; - import { CopyButton } from "@/components/dashboard/copy-button"; import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Code } from "@/components/ui/code"; -import { Separator } from "@/components/ui/separator"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { Gauge } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { BookOpen, Scan } from "lucide-react"; import Link from "next/link"; @@ -49,47 +49,50 @@ export default async function RatelimitOverviewPage() { return (
- ]} - /> - - - {workspace.ratelimitNamespaces.length > 0 ? ( -
    - {workspace.ratelimitNamespaces.map((namespace) => ( - - - - - - ))} -
- ) : ( - - - - - No Namespaces found - - You haven't created any Namespaces yet. Create one by performing a limit request as - shown below. - - - {snippet} - - -
- - - -
-
- )} + + }> + Ratelimits + + + + + + + {workspace.ratelimitNamespaces.length > 0 ? ( +
    + {workspace.ratelimitNamespaces.map((namespace) => ( + + + + + + ))} +
+ ) : ( + + + + + No Namespaces found + + You haven't created any Namespaces yet. Create one by performing a limit request + as shown below. + + + {snippet} + + +
+ + + +
+
+ )} +
); } diff --git a/apps/dashboard/app/(app)/settings/billing/page.tsx b/apps/dashboard/app/(app)/settings/billing/page.tsx index 8c55c290ed..23c7223ed8 100644 --- a/apps/dashboard/app/(app)/settings/billing/page.tsx +++ b/apps/dashboard/app/(app)/settings/billing/page.tsx @@ -1,3 +1,6 @@ +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Card, CardContent, @@ -13,11 +16,13 @@ import { type Workspace, db } from "@/lib/db"; import { stripeEnv } from "@/lib/env"; import { cn } from "@/lib/utils"; import { type BillingTier, QUOTA, calculateTieredPrices } from "@unkey/billing"; +import { Gear } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { Check, ExternalLink } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; import Stripe from "stripe"; +import { navigation } from "../constants"; import { UserPaymentMethod } from "./user-payment-method"; export const revalidate = 0; @@ -29,20 +34,33 @@ export default async function BillingPage() { where: (table, { and, eq, isNull }) => and(eq(table.tenantId, tenantId), isNull(table.deletedAt)), }); + if (!workspace) { return redirect("/new"); } return ( -
-
- {workspace.plan === "free" ? ( - - ) : ( - - )} -
- +
+ + }> + + Settings + + + + + +
+
+ {workspace.plan === "free" ? ( + + ) : ( + + )} +
+ +
+
); } @@ -77,7 +95,13 @@ const FreeUsage: React.FC<{ workspace: Workspace }> = async ({ workspace }) => {
@@ -235,7 +259,10 @@ const PaidUsage: React.FC<{ workspace: Workspace }> = async ({ workspace }) => { Current billing cycle:{" "} - {startOfMonth.toLocaleString("en-US", { month: "long", year: "numeric" })} + {startOfMonth.toLocaleString("en-US", { + month: "long", + year: "numeric", + })} {" "} @@ -360,7 +387,11 @@ const MeteredLineItem: React.FC<{ .filter((tier) => props.used >= tier.firstUnit) .map((tier, i) => ( - +
- - Back to Billing - - -
- {(["free", "pro", "custom"] as const).map((tier) => ( -
+ }> + Billing + + Plans + + + + +
+ -
-

- {tiers[tier].name} -

-
-

- {tiers[tier].description} -

-

- {typeof tiers[tier].price === "number" ? ( - <> - - {`$${tiers[tier].price}`} - - - {"/month"} - - - ) : ( - - {tiers[tier].price} - - )} -

+ Back to Billing + + +
+ {(["free", "pro", "custom"] as const).map((tier) => ( +
+
+

+ {tiers[tier].name} +

+
+

+ {tiers[tier].description} +

+

+ {typeof tiers[tier].price === "number" ? ( + <> + + {`$${tiers[tier].price}`} + + + {"/month"} + + + ) : ( + + {tiers[tier].price} + + )} +

-
-
    - {tiers[tier].features.map((feature) => ( -
  • - +
    +
      + {tiers[tier].features.map((feature) => ( +
    • + - {feature} -
    • - ))} -
    - {tiers[tier].footnotes && ( -
      - {tiers[tier].footnotes.map((footnote, i) => ( -
    • - {footnote} -
    • - ))} -
    - )} - {tier === "custom" ? ( - - - - ) : ( - - )} -
    + {feature} +
  • + ))} +
+ {tiers[tier].footnotes && ( +
    + {tiers[tier].footnotes.map((footnote, i) => ( +
  • + {footnote} +
  • + ))} +
+ )} + {tier === "custom" ? ( + + + + ) : ( + + )} +
+
+ ))}
- ))} -
+
+
); } diff --git a/apps/dashboard/app/(app)/settings/constants.ts b/apps/dashboard/app/(app)/settings/constants.ts new file mode 100644 index 0000000000..2d58f6c7d8 --- /dev/null +++ b/apps/dashboard/app/(app)/settings/constants.ts @@ -0,0 +1,32 @@ +export const navigation = [ + { + label: "General", + href: "/settings/general", + segment: "general", + }, + { + label: "Team", + href: "/settings/team", + segment: "team", + }, + { + label: "Root Keys", + href: "/settings/root-keys", + segment: "root-keys", + }, + { + label: "Billing", + href: "/settings/billing", + segment: "billing", + }, + { + label: "Vercel Integration", + href: "/settings/vercel", + segment: "vercel", + }, + { + label: "User", + href: "/settings/user", + segment: "user", + }, +]; diff --git a/apps/dashboard/app/(app)/settings/general/page.tsx b/apps/dashboard/app/(app)/settings/general/page.tsx index d6ef5e5bee..84b6ff3622 100644 --- a/apps/dashboard/app/(app)/settings/general/page.tsx +++ b/apps/dashboard/app/(app)/settings/general/page.tsx @@ -1,11 +1,17 @@ import { CopyButton } from "@/components/dashboard/copy-button"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Code } from "@/components/ui/code"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { Gear } from "@unkey/icons"; import { redirect } from "next/navigation"; +import { navigation } from "../constants"; import { UpdateWorkspaceImage } from "./update-workspace-image"; import { UpdateWorkspaceName } from "./update-workspace-name"; + export const dynamic = "force-dynamic"; export default async function SettingsPage() { @@ -20,23 +26,37 @@ export default async function SettingsPage() { } return ( -
- - - - - Workspace ID - This is your workspace id. It's used in some API calls. - - - -
{workspace.id}
-
- -
-
-
-
+
+ + }> + + Settings + + + + + +
+ + + + + Workspace ID + + This is your workspace id. It's used in some API calls. + + + + +
{workspace.id}
+
+ +
+
+
+
+
+
); } diff --git a/apps/dashboard/app/(app)/settings/layout.tsx b/apps/dashboard/app/(app)/settings/layout.tsx deleted file mode 100644 index 5cbbb76e90..0000000000 --- a/apps/dashboard/app/(app)/settings/layout.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type * as React from "react"; - -import { Navbar } from "@/components/dashboard/navbar"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -export default function SettingsLayout({ - children, -}: { - children: React.ReactNode; -}) { - const navigation = [ - { - label: "General", - href: "/settings/general", - segment: "general", - }, - { - label: "Team", - href: "/settings/team", - segment: "team", - }, - { - label: "Root Keys", - href: "/settings/root-keys", - segment: "root-keys", - }, - { - label: "Billing", - href: "/settings/billing", - segment: "billing", - }, - { - label: "Vercel Integration", - href: "/settings/vercel", - segment: "vercel", - }, - // { - // label: "Webhooks", - // href: "/settings/webhooks", - // segment: "webhooks", - // }, - { - label: "User", - href: "/settings/user", - segment: "user", - }, - ]; - - return ( -
-
-

Settings

-

Manage your workspace settings.

-
- - - -
{children}
-
- ); -} diff --git a/apps/dashboard/app/(app)/settings/loading.tsx b/apps/dashboard/app/(app)/settings/loading.tsx index b04687a5ae..70879711de 100644 --- a/apps/dashboard/app/(app)/settings/loading.tsx +++ b/apps/dashboard/app/(app)/settings/loading.tsx @@ -1,8 +1,9 @@ +import { PageContent } from "@/components/page-content"; import { Skeleton } from "@/components/ui/skeleton"; export default function Loading() { return ( -
+
@@ -27,6 +28,6 @@ export default function Loading() {
-
+ ); } diff --git a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/history/loading.tsx b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/history/loading.tsx deleted file mode 100644 index 853422fa66..0000000000 --- a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/history/loading.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Card } from "@/components/ui/card"; -import { Loader2 } from "lucide-react"; - -export default function Loading() { - return ( - - - - ); -} diff --git a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/history/page.tsx b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/history/page.tsx deleted file mode 100644 index 3e8995320f..0000000000 --- a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/history/page.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { getTenantId } from "@/lib/auth"; -import { clickhouse } from "@/lib/clickhouse"; -import { db } from "@/lib/db"; -import { env } from "@/lib/env"; -import { notFound } from "next/navigation"; -import { AccessTable } from "./access-table"; - -export const runtime = "edge"; - -export default async function HistoryPage(props: { - params: { keyId: string }; -}) { - const tenantId = getTenantId(); - - const workspace = await db.query.workspaces.findFirst({ - where: (table, { eq }) => eq(table.tenantId, tenantId), - }); - if (!workspace) { - return notFound(); - } - const { UNKEY_WORKSPACE_ID } = env(); - - const key = await db.query.keys.findFirst({ - where: (table, { and, eq, isNull }) => - and( - eq(table.workspaceId, UNKEY_WORKSPACE_ID), - eq(table.forWorkspaceId, workspace.id), - eq(table.id, props.params.keyId), - isNull(table.deletedAt), - ), - with: { - keyAuth: { - with: { - api: true, - }, - }, - }, - }); - if (!key?.keyAuth?.api) { - return notFound(); - } - const history = await clickhouse.verifications - .logs({ - workspaceId: UNKEY_WORKSPACE_ID, - keySpaceId: key.keyAuthId, - keyId: key.id, - }) - .then((res) => res.val!); - - return ; -} diff --git a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/layout.tsx b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/page-layout.tsx similarity index 69% rename from apps/dashboard/app/(app)/settings/root-keys/[keyId]/layout.tsx rename to apps/dashboard/app/(app)/settings/root-keys/[keyId]/page-layout.tsx index 71e512064f..de0d78d85e 100644 --- a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/layout.tsx +++ b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/page-layout.tsx @@ -1,39 +1,21 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Metric } from "@/components/ui/metric"; -import { getTenantId } from "@/lib/auth"; import { clickhouse } from "@/lib/clickhouse"; -import { db } from "@/lib/db"; +import type { Key } from "@unkey/db"; import { ArrowLeft } from "lucide-react"; import ms from "ms"; import Link from "next/link"; -import { notFound } from "next/navigation"; import { Suspense } from "react"; type Props = { params: { keyId: string; }; + rootKey: Key; children: React.ReactNode; }; -export default async function Layout({ children, params: { keyId } }: Props) { - const tenantId = getTenantId(); - - const workspace = await db.query.workspaces.findFirst({ - where: (table, { and, eq, isNull }) => - and(eq(table.tenantId, tenantId), isNull(table.deletedAt)), - }); - if (!workspace) { - return notFound(); - } - - const key = await db.query.keys.findFirst({ - where: (table, { eq, and }) => and(eq(table.forWorkspaceId, workspace.id), eq(table.id, keyId)), - }); - if (!key) { - return notFound(); - } - +export function PageLayout({ children, rootKey: key, params: { keyId } }: Props) { return (
= async ({ - workspaceId, - keySpaceId, - keyId, -}) => { +const LastUsed: React.FC<{ + workspaceId: string; + keySpaceId: string; + keyId: string; +}> = async ({ workspaceId, keySpaceId, keyId }) => { const lastUsed = await clickhouse.verifications .latest({ workspaceId, keySpaceId, keyId }) .then((res) => res.val?.at(0)?.time ?? 0); diff --git a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/page.tsx b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/page.tsx index bb1de70698..d45eb1ce54 100644 --- a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/page.tsx +++ b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/page.tsx @@ -1,12 +1,16 @@ +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { DialogTrigger } from "@/components/ui/dialog"; import { getTenantId } from "@/lib/auth"; import { clickhouse } from "@/lib/clickhouse"; import { type Permission, db, eq, schema } from "@/lib/db"; import { env } from "@/lib/env"; +import { Gear } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { notFound } from "next/navigation"; import { AccessTable } from "./history/access-table"; +import { PageLayout } from "./page-layout"; import { DialogAddPermissionsForAPI } from "./permissions/add-permission-for-api"; import { Api } from "./permissions/api"; import { Legacy } from "./permissions/legacy"; @@ -123,45 +127,73 @@ export default async function RootKeyPage(props: { const apisWithoutActivePermissions = apis.filter((api) => !api.hasActivePermissions); return ( -
- {permissions.some((p) => p.name === "*") ? ( - - ) : null} - - - - - - {apisWithActivePermissions.map((api) => ( - - ))} - - - {apisWithoutActivePermissions.length > 0 && ( - - - - - - )} - - - +
+ + }> + Root Keys + + {key.id} + + + + + + +
+ {permissions.some((p) => p.name === "*") ? ( + + ) : null} + + + + + + {apisWithActivePermissions.map((api) => ( + + ))} + + + {apisWithoutActivePermissions.length > 0 && ( + + + + + + )} + + + +
+
+
); } -function UsageHistoryCard(props: { accessTableProps: React.ComponentProps }) { +function UsageHistoryCard(props: { + accessTableProps: React.ComponentProps; +}) { return ( diff --git a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/permissions/loading.tsx b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/permissions/loading.tsx deleted file mode 100644 index 853422fa66..0000000000 --- a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/permissions/loading.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Card } from "@/components/ui/card"; -import { Loader2 } from "lucide-react"; - -export default function Loading() { - return ( - - - - ); -} 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 64c38b1682..e0f858fb41 100644 --- a/apps/dashboard/app/(app)/settings/root-keys/new/page.tsx +++ b/apps/dashboard/app/(app)/settings/root-keys/new/page.tsx @@ -1,6 +1,9 @@ import { PageHeader } from "@/components/dashboard/page-header"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { Gear } from "@unkey/icons"; import { redirect } from "next/navigation"; import { Client } from "./client"; @@ -20,18 +23,29 @@ export default async function SettingsKeysPage(_props: { }, }, }); + if (!workspace) { return redirect("/new"); } return ( -
- +
+ + }> + Root Keys + + Create new key + + + + + - + +
); } diff --git a/apps/dashboard/app/(app)/settings/root-keys/page.tsx b/apps/dashboard/app/(app)/settings/root-keys/page.tsx index 6dc6d1c548..2fb584f3a8 100644 --- a/apps/dashboard/app/(app)/settings/root-keys/page.tsx +++ b/apps/dashboard/app/(app)/settings/root-keys/page.tsx @@ -1,10 +1,15 @@ +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; import { PageHeader } from "@/components/dashboard/page-header"; import { RootKeyTable } from "@/components/dashboard/root-key-table"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { Gear } from "@unkey/icons"; import { Button } from "@unkey/ui"; import Link from "next/link"; import { redirect } from "next/navigation"; +import { navigation } from "../constants"; export const revalidate = 0; @@ -32,19 +37,31 @@ export default async function SettingsKeysPage(_props: { }); return ( -
- + + }> + + Settings + + + - , - ]} - /> -
- -
+ +
+
+ + + + +
+ +
+
); } diff --git a/apps/dashboard/app/(app)/settings/team/page.tsx b/apps/dashboard/app/(app)/settings/team/page.tsx index 3697973dcb..593fb53433 100644 --- a/apps/dashboard/app/(app)/settings/team/page.tsx +++ b/apps/dashboard/app/(app)/settings/team/page.tsx @@ -20,6 +20,9 @@ import { import { useAuth, useClerk, useOrganization } from "@clerk/nextjs"; import { Loading } from "@/components/dashboard/loading"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Select, SelectContent, @@ -30,7 +33,9 @@ import { } from "@/components/ui/select"; import { toast } from "@/components/ui/toaster"; import type { MembershipRole } from "@clerk/types"; +import { Gear } from "@unkey/icons"; import Link from "next/link"; +import { navigation } from "../constants"; type Member = { id: string; @@ -45,16 +50,30 @@ export default function TeamPage() { if (!organization) { return ( - - This is a personal account - - You can only manage teams in paid workspaces. - +
+ + }> + + Settings + + + + + +
+ + This is a personal account + + You can only manage teams in paid workspaces. + - - - - + + + + +
+
+
); } @@ -89,9 +108,21 @@ export default function TeamPage() { return (
- + + }> + + Settings + + + + + +
+ - {tab === "members" ? : } + {tab === "members" ? : } +
+
); } diff --git a/apps/dashboard/app/(app)/settings/user/page.tsx b/apps/dashboard/app/(app)/settings/user/page.tsx index 7dba3926b3..2f18a2b2a6 100644 --- a/apps/dashboard/app/(app)/settings/user/page.tsx +++ b/apps/dashboard/app/(app)/settings/user/page.tsx @@ -1,10 +1,16 @@ +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; +import { Gear } from "@unkey/icons"; import { redirect } from "next/navigation"; +import { navigation } from "../constants"; import { UpdateTheme } from "./update-theme"; import { UpdateUserEmail } from "./update-user-email"; import { UpdateUserImage } from "./update-user-image"; import { UpdateUserName } from "./update-user-name"; + export const revalidate = 0; export default async function SettingsPage() { @@ -19,11 +25,24 @@ export default async function SettingsPage() { } return ( -
- - - - +
+ + }> + + Settings + + + + + + +
+ + + + +
+
); } diff --git a/apps/dashboard/app/(app)/settings/vercel/page.tsx b/apps/dashboard/app/(app)/settings/vercel/page.tsx index 7ab4a902a4..0100d91874 100644 --- a/apps/dashboard/app/(app)/settings/vercel/page.tsx +++ b/apps/dashboard/app/(app)/settings/vercel/page.tsx @@ -1,12 +1,17 @@ import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; import { Code } from "@/components/ui/code"; import { getTenantId } from "@/lib/auth"; import { type Api, type Key, type VercelBinding, db, eq, schema } from "@/lib/db"; import { clerkClient } from "@clerk/nextjs"; +import { Gear } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { Vercel } from "@unkey/vercel"; import Link from "next/link"; import { notFound } from "next/navigation"; +import { navigation } from "../constants"; import { Client } from "./client"; type Props = { searchParams: { @@ -33,6 +38,7 @@ export default async function Page(props: Props) { }, }, }); + if (!workspace) { console.warn("no workspace"); return notFound(); @@ -41,16 +47,32 @@ export default async function Page(props: Props) { const integration = props.searchParams.configurationId ? workspace.vercelIntegrations.find((i) => i.id === props.searchParams.configurationId) : workspace.vercelIntegrations.at(0); + if (!integration) { return ( - - Vercel is not connected to this workspace - - - - - - +
+ + }> + + Settings + + + + + +
+ + + Vercel is not connected to this workspace + + + + + + + + +
); } @@ -60,6 +82,7 @@ export default async function Page(props: Props) { }); const { val: rawProjects, err } = await vercel.listProjects(); + if (err) { return ( @@ -145,5 +168,20 @@ export default async function Page(props: Props) { })), ); - return ; + return ( +
+ + }> + + Settings + + + + + +
+ + +
+ ); } diff --git a/apps/dashboard/components/dashboard/breadcrumb-skeleton.tsx b/apps/dashboard/components/dashboard/breadcrumb-skeleton.tsx deleted file mode 100644 index c9f41dd66c..0000000000 --- a/apps/dashboard/components/dashboard/breadcrumb-skeleton.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Breadcrumb, BreadcrumbList, BreadcrumbSeparator } from "@/components/ui/breadcrumb"; -import { Fragment } from "react"; -import { Skeleton } from "../ui/skeleton"; - -export function BreadcrumbSkeleton(props: { levels: number }) { - return ( - - - {Array.from({ length: props.levels }).map((_, idx) => ( - - - {idx < props.levels - 1 && } - - ))} - - - ); -} diff --git a/apps/dashboard/components/dashboard/navbar.tsx b/apps/dashboard/components/dashboard/navbar.tsx index 3d7d4f1104..1547060b43 100644 --- a/apps/dashboard/components/dashboard/navbar.tsx +++ b/apps/dashboard/components/dashboard/navbar.tsx @@ -8,17 +8,35 @@ import { cn } from "@/lib/utils"; import { useRouter, useSelectedLayoutSegment } from "next/navigation"; type Props = { - navigation: { label: string; href: string; segment: string | null; tag?: string }[]; + navigation: { + label: string; + href: string; + segment: string | null; + tag?: string; + isActive?: boolean; + }[]; + segment?: string; className?: string; }; -export const Navbar: React.FC> = ({ navigation, className }) => { +export const Navbar: React.FC> = ({ + navigation, + className, + segment, +}) => { return (