Skip to content

Commit

Permalink
chore: reimplement breadcrumbs using route handles
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanhopperlowe committed Dec 13, 2024
1 parent 93037b2 commit 408a540
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 200 deletions.
236 changes: 39 additions & 197 deletions ui/admin/app/components/header/HeaderNav.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { Link } from "@remix-run/react";
import { Link, UIMatch, useLocation, useMatches } from "@remix-run/react";
import React from "react";
import { $path } from "remix-routes";
import useSWR from "swr";

import { AgentService } from "~/lib/service/api/agentService";
import { ThreadsService } from "~/lib/service/api/threadsService";
import { WebhookApiService } from "~/lib/service/api/webhookApiService";
import { WorkflowService } from "~/lib/service/api/workflowService";
import { RouteHandle } from "~/lib/service/routeHandles";
import { cn } from "~/lib/utils";

import { DarkModeToggle } from "~/components/DarkModeToggle";
Expand All @@ -19,7 +16,6 @@ import {
} from "~/components/ui/breadcrumb";
import { SidebarTrigger } from "~/components/ui/sidebar";
import { UserMenu } from "~/components/user/UserMenu";
import { useUnknownPathParams } from "~/hooks/useRouteInfo";

export function HeaderNav() {
const headerHeight = "h-[60px]";
Expand All @@ -36,7 +32,7 @@ export function HeaderNav() {
<div className="flex-grow flex justify-start items-center p-4">
<SidebarTrigger className="h-4 w-4" />
<div className="border-r h-4 mx-4" />
<RouteBreadcrumbs />
<RouteBreadcrumbHandles />
</div>

<div className="flex items-center justify-center p-4 mr-4">
Expand All @@ -49,8 +45,39 @@ export function HeaderNav() {
);
}

function RouteBreadcrumbs() {
const routeInfo = useUnknownPathParams();
function RouteBreadcrumbHandles() {
const matches = useMatches() as UIMatch<unknown, RouteHandle>[];
const location = useLocation();
const filtered = matches.filter((match) => match.handle?.breadcrumb);

const renderItem = (
match: UIMatch<unknown, RouteHandle>,
isLeaf: boolean
) => {
if (!match.handle?.breadcrumb) return;

return match.handle.breadcrumb(location).map((item, i, arr) => {
const withHref = isLeaf && i === arr.length - 1;

return (
<React.Fragment key={`${match.id}-${i}`}>
<BreadcrumbSeparator />

<BreadcrumbItem>
{withHref ? (
<BreadcrumbPage>{item.content}</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link to={item.href ?? match.pathname}>
{item.content}
</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
</React.Fragment>
);
});
};

return (
<Breadcrumb>
Expand All @@ -60,196 +87,11 @@ function RouteBreadcrumbs() {
<Link to={$path("/")}>Home</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />

{routeInfo?.path === "/agents/:agent" && (
<>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={$path("/agents")}>Agents</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
<AgentName
agentId={routeInfo.pathParams.agent || ""}
/>
</BreadcrumbPage>
</BreadcrumbItem>
</>
)}

{routeInfo?.path === "/agents" && (
<BreadcrumbItem>
<BreadcrumbPage>Agents</BreadcrumbPage>
</BreadcrumbItem>
)}

{routeInfo?.path === "/threads" && (
<>
{routeInfo.query?.from && (
<>
<BreadcrumbItem>
<BreadcrumbLink asChild>
{renderThreadFrom(routeInfo.query.from)}
</BreadcrumbLink>
</BreadcrumbItem>

<BreadcrumbSeparator />
</>
)}

<BreadcrumbItem>
<BreadcrumbPage>Threads</BreadcrumbPage>
</BreadcrumbItem>
</>
)}

{routeInfo?.path === "/threads/:id" && (
<>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={$path("/threads")}>Threads</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
<ThreadName
threadId={routeInfo.pathParams.id || ""}
/>
</BreadcrumbPage>
</BreadcrumbItem>
</>
)}

{routeInfo?.path === "/workflows/:workflow" && (
<>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={$path("/workflows")}>Workflows</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
<WorkflowName
workflowId={
routeInfo.pathParams.workflow || ""
}
/>
</BreadcrumbPage>
</BreadcrumbItem>
</>
)}

{routeInfo?.path === "/webhooks" && (
<BreadcrumbItem>
<BreadcrumbPage>Webhooks</BreadcrumbPage>
</BreadcrumbItem>
)}

{routeInfo?.path === "/webhooks/create" && (
<>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={$path("/webhooks")}>Webhooks</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Create Webhook</BreadcrumbPage>
</BreadcrumbItem>
</>
)}

{routeInfo?.path === "/webhooks/:webhook" && (
<>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={$path("/webhooks")}>Webhooks</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
<WebhookName
webhookId={
routeInfo.pathParams.webhook || ""
}
/>
</BreadcrumbPage>
</BreadcrumbItem>
</>
)}

{routeInfo?.path === "/tools" && (
<BreadcrumbItem>
<BreadcrumbPage>Tools</BreadcrumbPage>
</BreadcrumbItem>
)}
{routeInfo?.path === "/users" && (
<BreadcrumbItem>
<BreadcrumbPage>Users</BreadcrumbPage>
</BreadcrumbItem>
)}
{routeInfo?.path === "/oauth-apps" && (
<BreadcrumbItem>
<BreadcrumbPage>OAuth Apps</BreadcrumbPage>
</BreadcrumbItem>
)}
{routeInfo?.path === "/model-providers" && (
<BreadcrumbItem>
<BreadcrumbPage>Model Providers</BreadcrumbPage>
</BreadcrumbItem>
{filtered.map((match, i, arr) =>
renderItem(match, i === arr.length - 1)
)}
</BreadcrumbList>
</Breadcrumb>
);
}

const renderThreadFrom = (from: "agents" | "workflows" | "users") => {
if (from === "agents") return <Link to={$path("/agents")}>Agents</Link>;

if (from === "workflows")
return <Link to={$path("/workflows")}>Workflows</Link>;

if (from === "users") return <Link to={$path("/users")}>Users</Link>;
};

const AgentName = ({ agentId }: { agentId: string }) => {
const { data: agent } = useSWR(
AgentService.getAgentById.key(agentId),
({ agentId }) => AgentService.getAgentById(agentId)
);

return <>{agent?.name || "New Agent"}</>;
};

const WorkflowName = ({ workflowId }: { workflowId: string }) => {
const { data: workflow } = useSWR(
WorkflowService.getWorkflowById.key(workflowId),
({ workflowId }) => WorkflowService.getWorkflowById(workflowId)
);

return <>{workflow?.name || "New Workflow"}</>;
};

const ThreadName = ({ threadId }: { threadId: string }) => {
const { data: thread } = useSWR(
ThreadsService.getThreadById.key(threadId),
({ threadId }) => ThreadsService.getThreadById(threadId)
);

return <>{thread?.description || threadId}</>;
};

const WebhookName = ({ webhookId }: { webhookId: string }) => {
const { data } = useSWR(
WebhookApiService.getWebhookById.key(webhookId),
({ id }) => WebhookApiService.getWebhookById(id)
);

return <>{data?.name || webhookId}</>;
};
13 changes: 13 additions & 0 deletions ui/admin/app/lib/service/routeHandles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type BreadcrumbItem = {
content?: React.ReactNode;
href?: string;
};

type BreadcrumbProps = {
pathname: string;
search: string;
};

export type RouteHandle = {
breadcrumb?: (props: BreadcrumbProps) => BreadcrumbItem[];
};
1 change: 1 addition & 0 deletions ui/admin/app/lib/service/routeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,5 @@ export const RouteService = {
getUnknownRouteInfo,
getRouteInfo,
getQueryParams,
getPathParams: $params,
};
19 changes: 18 additions & 1 deletion ui/admin/app/routes/_auth.agents.$agent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import {
ClientLoaderFunctionArgs,
redirect,
useLoaderData,
useMatch,
useNavigate,
} from "@remix-run/react";
import { useCallback } from "react";
import { $path } from "remix-routes";
import { preload } from "swr";
import useSWR, { preload } from "swr";

import { AgentService } from "~/lib/service/api/agentService";
import { DefaultModelAliasApiService } from "~/lib/service/api/defaultModelAliasApiService";
import { RouteHandle } from "~/lib/service/routeHandles";
import { RouteQueryParams, RouteService } from "~/lib/service/routeService";
import { noop } from "~/lib/utils";

Expand Down Expand Up @@ -92,3 +94,18 @@ export default function ChatAgent() {
</div>
);
}

const AgentBreadcrumb = () => {
const match = useMatch("/agents/:agent");

const { data: agent } = useSWR(
AgentService.getAgentById.key(match?.params.agent),
({ agentId }) => AgentService.getAgentById(agentId)
);

return <>{agent?.name || "New Agent"}</>;
};

export const handle: RouteHandle = {
breadcrumb: () => [{ content: <AgentBreadcrumb /> }],
};
5 changes: 5 additions & 0 deletions ui/admin/app/routes/_auth.agents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { RouteHandle } from "~/lib/service/routeHandles";

export const handle: RouteHandle = {
breadcrumb: () => [{ content: "Agents" }],
};
5 changes: 5 additions & 0 deletions ui/admin/app/routes/_auth.model-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ModelProvider } from "~/lib/model/modelProviders";
import { DefaultModelAliasApiService } from "~/lib/service/api/defaultModelAliasApiService";
import { ModelApiService } from "~/lib/service/api/modelApiService";
import { ModelProviderApiService } from "~/lib/service/api/modelProviderApiService";
import { RouteHandle } from "~/lib/service/routeHandles";

import { TypographyH2 } from "~/components/Typography";
import { WarningAlert } from "~/components/composed/WarningAlert";
Expand Down Expand Up @@ -90,3 +91,7 @@ export default function ModelProviders() {
</div>
);
}

export const handle: RouteHandle = {
breadcrumb: () => [{ content: "Model Providers" }],
};
5 changes: 5 additions & 0 deletions ui/admin/app/routes/_auth.oauth-apps.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { preload } from "swr";

import { OauthAppService } from "~/lib/service/api/oauthAppService";
import { RouteHandle } from "~/lib/service/routeHandles";

import { TypographyH2 } from "~/components/Typography";
import { OAuthAppList } from "~/components/oauth-apps/OAuthAppList";
Expand Down Expand Up @@ -33,3 +34,7 @@ export default function OauthApps() {
</div>
);
}

export const handle: RouteHandle = {
breadcrumb: () => [{ content: "OAuth Apps" }],
};
8 changes: 8 additions & 0 deletions ui/admin/app/routes/_auth.threads.$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import {
Link,
redirect,
useLoaderData,
useMatch,
} from "@remix-run/react";
import { ArrowLeftIcon } from "lucide-react";

import { AgentService } from "~/lib/service/api/agentService";
import { ThreadsService } from "~/lib/service/api/threadsService";
import { WorkflowService } from "~/lib/service/api/workflowService";
import { RouteHandle } from "~/lib/service/routeHandles";
import { RouteService } from "~/lib/service/routeService";
import { noop } from "~/lib/utils";

Expand Down Expand Up @@ -118,3 +120,9 @@ export default function ChatAgent() {
</div>
);
}

const ThreadBreadcrumb = () => useMatch("/threads/:id")?.params.id;

export const handle: RouteHandle = {
breadcrumb: () => [{ content: <ThreadBreadcrumb /> }],
};
Loading

0 comments on commit 408a540

Please sign in to comment.