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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCreateIdentity } from "@/app/(app)/apis/[apiId]/_components/create-key/hooks/use-create-identity";
import { useFetchIdentities } from "@/app/(app)/apis/[apiId]/_components/create-key/hooks/use-fetch-identities";
import { createIdentityOptions } from "@/app/(app)/apis/[apiId]/_components/create-key/hooks/use-fetch-identities/create-identity-options";
import { useCreateIdentity } from "@/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/hooks/use-create-identity";
import { useFetchIdentities } from "@/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/hooks/use-fetch-identities";
import { createIdentityOptions } from "@/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/hooks/use-fetch-identities/create-identity-options";
import { FormCombobox } from "@/components/ui/form-combobox";
import type { Identity } from "@unkey/db";
import { TriangleWarning2 } from "@unkey/icons";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { ConfirmPopover } from "@/components/confirmation-popover";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation";
import { ArrowRight, Check, Key2, Plus } from "@unkey/icons";
import { Button, InfoTooltip, toast } from "@unkey/ui";
import { useRouter } from "next/navigation";
Expand All @@ -24,13 +25,13 @@ export const KeyCreatedSuccessDialog = ({
keyspaceId?: string | null;
onCreateAnother?: () => void;
}) => {
const workspace = useWorkspaceNavigation();
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const [pendingAction, setPendingAction] = useState<
"close" | "create-another" | "go-to-details" | null
>(null);
const dividerRef = useRef<HTMLDivElement>(null);
const router = useRouter();

// Prevent accidental tab/window close when dialog is open
useEffect(() => {
if (!isOpen) {
Expand Down Expand Up @@ -90,7 +91,7 @@ export const KeyCreatedSuccessDialog = ({
});
return;
}
router.push(`/apis/${apiId}/keys/${keyspaceId}/${keyData.id}`);
router.push(`/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}/${keyData.id}`);
break;

default:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { SecretKey } from "@/app/(app)/apis/[apiId]/_components/create-key/components/secret-key";
import { SecretKey } from "@/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/secret-key";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { CircleInfo } from "@unkey/icons";
import { Code, CopyButton, VisibleButton } from "@unkey/ui";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Plus } from "@unkey/icons";
import type { IconProps } from "@unkey/icons/src/props";
import {
Button,
Loading,
NavigableDialogBody,
NavigableDialogContent,
NavigableDialogFooter,
Expand All @@ -16,7 +17,7 @@ import {
NavigableDialogRoot,
toast,
} from "@unkey/ui";
import { type FC, useEffect, useState } from "react";
import { type FC, Suspense, useEffect, useState } from "react";
import { FormProvider } from "react-hook-form";
import { KeyCreatedSuccessDialog } from "./components/key-created-success-dialog";
import { SectionLabel } from "./components/section-label";
Expand Down Expand Up @@ -208,14 +209,16 @@ export const CreateKeyDialog = ({
</form>
</FormProvider>
{/* Success Dialog */}
<KeyCreatedSuccessDialog
apiId={apiId}
keyspaceId={keyspaceId}
isOpen={successDialogOpen}
onClose={handleSuccessDialogClose}
keyData={createdKeyData}
onCreateAnother={openNewKeyDialog}
/>
<Suspense fallback={<Loading type="spinner" />}>
<KeyCreatedSuccessDialog
apiId={apiId}
keyspaceId={keyspaceId}
isOpen={successDialogOpen}
onClose={handleSuccessDialogClose}
keyData={createdKeyData}
onCreateAnother={openNewKeyDialog}
/>
</Suspense>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"use client";
import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation";
import { shortenId } from "@/lib/shorten-id";
import { cn } from "@/lib/utils";
import type { KeysOverviewLog } from "@unkey/clickhouse/src/keys/keys";
import { TriangleWarning2 } from "@unkey/icons";
import { InfoTooltip, Loading } from "@unkey/ui";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { Suspense, useCallback, useState } from "react";
import { getErrorPercentage, getErrorSeverity } from "../utils/calculate-blocked-percentage";

type KeyIdentifierColumnProps = {
Expand Down Expand Up @@ -44,11 +45,12 @@ const getWarningMessage = (severity: string, errorRate: number) => {
};

export const KeyIdentifierColumn = ({ log, apiId, onNavigate }: KeyIdentifierColumnProps) => {
const workspace = useWorkspaceNavigation();

const router = useRouter();
const errorPercentage = getErrorPercentage(log);
const severity = getErrorSeverity(log);
const hasErrors = severity !== "none";

const [isNavigating, setIsNavigating] = useState(false);

const handleLinkClick = useCallback(
Expand All @@ -58,9 +60,11 @@ export const KeyIdentifierColumn = ({ log, apiId, onNavigate }: KeyIdentifierCol

onNavigate?.();

router.push(`/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`);
router.push(
`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`,
);
},
[apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router.push],
[apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router.push, workspace.slug],
);

return (
Expand All @@ -80,16 +84,18 @@ export const KeyIdentifierColumn = ({ log, apiId, onNavigate }: KeyIdentifierCol
</div>
)}
</InfoTooltip>
<Link
title={`View details for ${log.key_id}`}
className="font-mono group-hover:underline decoration-dotted"
href={`/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`}
onClick={handleLinkClick}
>
<div className="font-mono font-medium truncate flex items-center">
{shortenId(log.key_id)}
</div>
</Link>
<Suspense fallback={<Loading type="spinner" />}>
<Link
title={`View details for ${log.key_id}`}
className="font-mono group-hover:underline decoration-dotted"
href={`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`}
onClick={handleLinkClick}
>
<div className="font-mono font-medium truncate flex items-center">
{shortenId(log.key_id)}
</div>
</Link>
</Suspense>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
"use client";

import { QuickNavPopover } from "@/components/navbar-popover";
import { NavbarActionButton } from "@/components/navigation/action-button";
import { Navbar } from "@/components/navigation/navbar";
import { useIsMobile } from "@/hooks/use-mobile";
import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation";
import { trpc } from "@/lib/trpc/client";
import type { Workspace } from "@unkey/db";
import { ChevronExpandY, Nodes, Plus, TaskUnchecked } from "@unkey/icons";
import { CreateKeyDialog } from "./_components/create-key";

// Types for better type safety
interface ApiLayoutData {
currentApi: {
id: string;
name: string;
workspaceId: string;
keyAuthId: string | null;
keyspaceDefaults: {
prefix?: string;
bytes?: number;
} | null;
deleteProtection: boolean | null;
ipWhitelist: string | null;
};
workspaceApis: Array<{
id: string;
name: string;
}>;
keyAuth: {
id: string;
defaultPrefix: string | null;
defaultBytes: number | null;
sizeApprox: number;
} | null;
workspace: {
id: string;
ipWhitelist: boolean;
};
}

interface ApisNavbarProps {
apiId: string;
keyspaceId?: string;
keyId?: string;
activePage?: {
href: string;
text: string;
};
}

interface LoadingNavbarProps {
workspace: Workspace;
}

interface NavbarContentProps {
apiId: string;
keyspaceId?: string;
keyId?: string;
activePage?: {
href: string;
text: string;
};
workspace: Workspace;
isMobile: boolean;
layoutData: ApiLayoutData;
}

// Loading state component
const LoadingNavbar = ({ workspace }: LoadingNavbarProps) => (
<Navbar>
<Navbar.Breadcrumbs icon={<Nodes />}>
<Navbar.Breadcrumbs.Link href={`/${workspace.slug}/apis`}>APIs</Navbar.Breadcrumbs.Link>
<Navbar.Breadcrumbs.Link href="#" isIdentifier className="group" noop>
<div className="h-6 w-20 bg-grayA-3 rounded animate-pulse transition-all " />
</Navbar.Breadcrumbs.Link>
<Navbar.Breadcrumbs.Link href="#" noop active>
<div className="hover:bg-gray-3 rounded-lg flex items-center gap-1 p-1">
<div className="h-6 w-16 bg-grayA-3 rounded animate-pulse transition-all " />
<ChevronExpandY className="size-4" />
</div>
</Navbar.Breadcrumbs.Link>
</Navbar.Breadcrumbs>
<Navbar.Actions>
<NavbarActionButton disabled>
<Plus />
Create new key
</NavbarActionButton>
<div className="h-7 bg-grayA-2 border border-gray-6 rounded-md animate-pulse px-3 flex gap-2 items-center justify-center w-[190px] transition-all ">
<div className="h-3 w-[190px] bg-grayA-3 rounded" />
<div>
<TaskUnchecked size="md-regular" className="!size-4" />
</div>
</div>
</Navbar.Actions>
</Navbar>
);

// Main navbar content component
const NavbarContent = ({
keyspaceId,
keyId,
activePage,
workspace,
isMobile,
layoutData,
}: NavbarContentProps) => {
const shouldFetchKey = Boolean(keyspaceId && keyId);

// If we expected to find a key but this component doesn't handle key details,
// we should handle this at a higher level or in a different component
if (shouldFetchKey) {
console.warn("Key fetching logic should be handled at a higher level");
}

const { currentApi } = layoutData;

// Define base path for API navigation
const base = `/${workspace.slug}/apis/${currentApi.id}`;

// Create navigation items for QuickNavPopover
const navigationItems = [
{
id: "requests",
label: "Requests",
href: `/${workspace.slug}/apis/${currentApi.id}`,
},
];

// Add Keys navigation if keyAuthId exists
if (currentApi.keyAuthId) {
navigationItems.push({
id: "keys",
label: "Keys",
href: `/${workspace.slug}/apis/${currentApi.id}/keys/${currentApi.keyAuthId}`,
});
}

// Add Settings navigation
navigationItems.push({
id: "settings",
label: "Settings",
href: `/${workspace.slug}/apis/${currentApi.id}/settings`,
});

return (
<div className="w-full">
<Navbar className="w-full flex justify-between">
<Navbar.Breadcrumbs className="flex-1 w-full" icon={<Nodes />}>
<Navbar.Breadcrumbs.Link
href={`/${workspace.slug}/apis`}
className={isMobile ? "hidden" : "max-md:hidden"}
>
APIs
</Navbar.Breadcrumbs.Link>
<Navbar.Breadcrumbs.Link
href={base}
isIdentifier
className={isMobile ? "hidden" : "group max-md:hidden"}
noop
>
<div className="text-accent-10 group-hover:text-accent-12">{currentApi.name}</div>
</Navbar.Breadcrumbs.Link>
<Navbar.Breadcrumbs.Link href={activePage?.href ?? ""} noop active={!shouldFetchKey}>
<QuickNavPopover items={navigationItems} shortcutKey="M">
<div className="hover:bg-gray-3 rounded-lg flex items-center gap-1 p-1">
{activePage?.text ?? ""}
<ChevronExpandY className="size-4" />
</div>
</QuickNavPopover>
</Navbar.Breadcrumbs.Link>
</Navbar.Breadcrumbs>
{layoutData.keyAuth ? (
<CreateKeyDialog
keyspaceId={layoutData.keyAuth.id}
apiId={currentApi.id}
copyIdValue={currentApi.id}
keyspaceDefaults={currentApi.keyspaceDefaults}
/>
) : (
<NavbarActionButton disabled>
<Plus />
Create new key
</NavbarActionButton>
)}
</Navbar>
</div>
);
};

// Main component
export const ApisNavbar = ({ apiId, keyspaceId, keyId, activePage }: ApisNavbarProps) => {
const workspace = useWorkspaceNavigation();

const isMobile = useIsMobile();

// Only make the query if we have a valid apiId
const {
data: layoutData,
isLoading,
error,
} = trpc.api.queryApiKeyDetails.useQuery(
{ apiId },
{
enabled: Boolean(apiId), // Only run query if apiId exists
retry: 3,
retryDelay: 1000,
},
);

// Show loading state while fetching data
if (isLoading || !layoutData) {
return <LoadingNavbar workspace={workspace} />;
}

// Handle error state
if (error) {
console.error("Failed to fetch API layout data:", error);
return <LoadingNavbar workspace={workspace} />;
}

return (
<NavbarContent
apiId={apiId}
keyspaceId={keyspaceId}
keyId={keyId}
activePage={activePage}
workspace={workspace}
isMobile={isMobile}
layoutData={layoutData}
/>
);
};
Loading
Loading