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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import type { QuickNavItem } from "@/components/navbar-popover";
import type { Navbar } from "@/components/navigation/navbar";
import { collection } from "@/lib/collections";
import { shortenId } from "@/lib/shorten-id";
import { eq, useLiveQuery } from "@tanstack/react-db";
import { useParams } from "next/navigation";
import { useMemo } from "react";
import type { ComponentPropsWithoutRef } from "react";
Expand All @@ -26,36 +28,61 @@ export function useDeploymentBreadcrumbConfig(): BreadcrumbItem[] {
const params = useParams();

const workspaceSlug = params.workspaceSlug as string;
const { projectId } = useProjectData();
const { projectId, project } = useProjectData();
const { deploymentId } = useDeployment();

const { data: deployments } = useLiveQuery((q) =>
q
.from({ deployment: collection.deployments })
.where(({ deployment }) => eq(deployment.projectId, projectId))
.select(({ deployment }) => ({
id: deployment.id,
createdAt: deployment.createdAt,
}))
.orderBy(({ deployment }) => deployment.createdAt, "desc"),
);

const projects = useLiveQuery((q) =>
q.from({ project: collection.projects }).select(({ project }) => ({
id: project.id,
name: project.name,
})),
);

const activeProject = project
? { id: project.id, name: project.name, repositoryFullName: project.repositoryFullName }
: undefined;

return useMemo(() => {
const basePath = `/${workspaceSlug}/projects/${projectId}`;

const deploymentQuickNavItems: QuickNavItem[] = (deployments ?? []).map((d) => ({
id: d.id,
label: shortenId(d.id),
href: `${basePath}/deployments/${d.id}`,
}));

return [
{
id: "projects",
href: `/${workspaceSlug}/projects`,
children: "Projects",
shouldRender: true,
active: false,
isLast: false,
},
{
id: "project",
href: `${basePath}`,
children: projectId,
shouldRender: true,
active: false,
isLast: false,
},
{
id: "deployments",
href: `${basePath}/deployments`,
children: "Deployments",
children: activeProject?.name || projectId,
href: `${basePath}/${projectId}`,
isIdentifier: true,
shouldRender: true,
active: false,
isLast: false,
noop: true,
className: "flex",
quickNavConfig: {
items: projects.data.map((project) => ({
id: project.id,
label: project.name,
href: `${basePath}/${project.id}`,
})),
shortcutKey: "N",
},
},

{
id: "deployment",
href: `${basePath}/deployments/${deploymentId}`,
Expand All @@ -64,7 +91,14 @@ export function useDeploymentBreadcrumbConfig(): BreadcrumbItem[] {
shouldRender: true,
active: false,
isLast: false,
noop: true,
className: "flex",
quickNavConfig: {
items: deploymentQuickNavItems,
activeItemId: deploymentId,
shortcutKey: "D",
},
},
];
}, [workspaceSlug, projectId, deploymentId]);
}, [workspaceSlug, projectId, deploymentId, deployments, projects, activeProject?.name]);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"use client";

import type { Navbar } from "@/components/navigation/navbar";
import { shortenId } from "@/lib/shorten-id";
import { useParams, useSelectedLayoutSegments } from "next/navigation";
import type { ComponentPropsWithoutRef } from "react";

export type QuickNavItem = {
Expand All @@ -26,15 +24,6 @@ export type BreadcrumbItem = ComponentPropsWithoutRef<typeof Navbar.Breadcrumbs.
};
};

type SubPage = {
id: string;
label: string;
href: string;
segment: string | undefined;
disabled?: boolean;
disabledTooltip?: string;
};

export const useBreadcrumbConfig = ({
projectId,
basePath,
Expand All @@ -46,64 +35,7 @@ export const useBreadcrumbConfig = ({
projects: Array<{ id: string; name: string }>;
activeProject: { id: string; name: string } | undefined;
}): BreadcrumbItem[] => {
const segments = useSelectedLayoutSegments() ?? [];
const params = useParams();
// Find base indices using the segment-based pattern
const projectsIndex = segments.findIndex((s) => s === "projects");
const currentSegment = segments.at(projectsIndex + 2); // After [projectId]
const deploymentId = params?.deploymentId as string | undefined;

// Sub-pages configuration - matches the existing structure
const subPages: SubPage[] = [
{
id: "overview",
label: "Overview",
href: `${basePath}/${projectId}`,
segment: undefined,
},
{
id: "deployments",
label: "Deployments",
href: `${basePath}/${projectId}/deployments`,
segment: "deployments",
},
{
id: "requests",
label: "Requests",
href: `${basePath}/${projectId}/requests`,
segment: "requests",
},
{
id: "logs",
label: "Logs",
href: `${basePath}/${projectId}/logs`,
segment: "logs",
},
{
id: "settings",
label: "Settings",
href: `${basePath}/${projectId}/settings`,
segment: "settings",
},
];

// Determine active subpage based on segment
const activeSubPage = subPages.find((p) => p.segment === currentSegment) || subPages[0];
const isOnDeploymentDetail = Boolean(deploymentId);

// Build breadcrumbs declaratively
const breadcrumbs: BreadcrumbItem[] = [
// 1. Projects root
{
id: "projects",
children: "Projects",
href: basePath,
shouldRender: true,
active: false,
isLast: false,
},

// 2. Current project with QuickNav
{
id: "project",
children: activeProject?.name || projectId,
Expand All @@ -123,40 +55,6 @@ export const useBreadcrumbConfig = ({
shortcutKey: "N",
},
},

// 3. Sub-page with QuickNav (Overview, Deployments, etc.)
{
id: "subpage",
children: isOnDeploymentDetail ? "Deployments" : activeSubPage.label,
href: isOnDeploymentDetail ? `${basePath}/${projectId}/deployments` : activeSubPage.href,
shouldRender: true,
active: !isOnDeploymentDetail, // Active if not on detail page
isLast: !isOnDeploymentDetail, // Last if not on detail page
noop: true,
quickNavConfig: {
items: subPages.map((page) => ({
id: page.id,
label: page.label,
href: page.href,
disabled: page.disabled,
disabledTooltip: page.disabledTooltip,
})),
activeItemId: isOnDeploymentDetail ? "deployments" : undefined,
shortcutKey: "M",
},
},

// 4. Deployment ID
{
id: "deployment-detail",
children: shortenId(deploymentId || ""),
href: `${basePath}/${projectId}/deployments/${deploymentId}`,
isIdentifier: true,
shouldRender: Boolean(deploymentId),
active: Boolean(deploymentId),
isLast: Boolean(deploymentId),
className: "font-mono",
},
];

return breadcrumbs.filter((b) => b.shouldRender);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";
import { useSidebar } from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
import { ChevronLeft } from "@unkey/icons";
import Link from "next/link";

type ProjectSidebarHeaderProps = {
backHref: string;
};

export const ProjectSidebarHeader = ({ backHref }: ProjectSidebarHeaderProps) => {
const { state } = useSidebar();
const isCollapsed = state === "collapsed";

return (
<div className="flex flex-col gap-1 px-2 mb-2">
<Link
href={backHref}
className={cn(
"flex items-center gap-1 text-gray-9 hover:text-gray-12 transition-colors text-sm py-1 rounded-md",
isCollapsed && "justify-center",
)}
>
<ChevronLeft iconSize="xl-medium" />
<div className="flex items-center gap-2 px-1 text-gray-12 font-medium text-sm truncate">
<span className="truncate">Projects</span>
</div>
</Link>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"use client";
import type { NavItem } from "@/components/navigation/sidebar/workspace-navigations";
import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation";
import { collection } from "@/lib/collections";
import { useLiveQuery } from "@tanstack/react-db";
import { ArrowOppositeDirectionY, Cube, Gear, InputSearch, Layers3, Nodes } from "@unkey/icons";
import { useMemo } from "react";

type ProjectScopeResult =
| { isInsideProject: false }
| {
isInsideProject: true;
projectId: string;
projectName: string;
backHref: string;
navItems: NavItem[];
};

export const useProjectScopedNavigation = (segments: string[]): ProjectScopeResult => {
const workspace = useWorkspaceNavigation();
const cleanSegments = segments.filter((s) => !s.startsWith("("));
const projectsIndex = cleanSegments.findIndex((s) => s === "projects");
const projectId = projectsIndex !== -1 ? cleanSegments.at(projectsIndex + 1) : undefined;

const { data: projectData } = useLiveQuery((q) =>
q.from({ project: collection.projects }).orderBy(({ project }) => project.id, "desc"),
);

return useMemo(() => {
if (!projectId || projectsIndex === -1) {
return { isInsideProject: false };
}

const project = projectData?.find((p) => p.id === projectId);
const projectName = project?.name ?? projectId;
const basePath = `/${workspace.slug}/projects`;
const currentSegment = cleanSegments.at(projectsIndex + 2);

// Parse deployment context
const isOnDeployments = currentSegment === "deployments";
const deploymentId = isOnDeployments ? cleanSegments.at(projectsIndex + 3) : undefined;
const deploymentSubSegment = deploymentId ? cleanSegments.at(projectsIndex + 4) : undefined;

// Build deployment nested children when a specific deployment is selected
const deploymentItems: NavItem[] = [];
if (deploymentId) {
const deploymentPath = `${basePath}/${projectId}/deployments/${deploymentId}`;

deploymentItems.push(
{
icon: Cube,
href: deploymentPath,
label: "Overview",
active: deploymentSubSegment === undefined,
tooltip: "Overview",
},
{
icon: Nodes,
href: `${deploymentPath}/network`,
label: "Network",
active: deploymentSubSegment === "network",
tooltip: "Network",
},
);
}

const navItems: NavItem[] = [
{
icon: Cube,
href: `${basePath}/${projectId}`,
label: "Overview",
active: currentSegment === undefined,
tooltip: "Overview",
},
{
icon: Layers3,
href: `${basePath}/${projectId}/deployments`,
label: "Deployments",
active: isOnDeployments,
tooltip: "Deployments",
showSubItems: deploymentId !== undefined,
items: deploymentItems.length > 0 ? deploymentItems : undefined,
},
{
icon: ArrowOppositeDirectionY,
href: `${basePath}/${projectId}/requests`,
label: "Requests",
active: currentSegment === "requests",
tooltip: "Requests",
},
{
icon: InputSearch,
href: `${basePath}/${projectId}/logs`,
label: "Logs",
active: currentSegment === "logs",
tooltip: "Logs",
},
{
icon: Gear,
href: `${basePath}/${projectId}/settings`,
label: "Settings",
active: currentSegment === "settings",
tooltip: "Settings",
},
];

return {
isInsideProject: true,
projectId,
projectName,
backHref: basePath,
navItems,
};
}, [projectId, projectsIndex, projectData, workspace.slug, cleanSegments]);
};
Loading