Files
kandev/apps/web/components/settings/settings-app-sidebar.tsx
T
2026-05-17 20:16:15 +01:00

377 lines
13 KiB
TypeScript

"use client";
import { useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
IconSettings,
IconFolder,
IconRobot,
IconBell,
IconCode,
IconCpu,
IconKey,
IconMessageCircle,
IconBrandGithub,
IconBrandSlack,
IconHexagon,
IconSparkles,
IconWand,
IconGitBranch,
IconArrowsShuffle,
IconTicket,
IconPlugConnected,
} from "@tabler/icons-react";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupLabel,
SidebarGroupContent,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarMenuSub,
SidebarMenuSubItem,
SidebarMenuSubButton,
SidebarHeader,
useSidebar,
} from "@kandev/ui/sidebar";
import { ScrollArea } from "@kandev/ui/scroll-area";
import { ScrollOnOverflow } from "@kandev/ui/scroll-on-overflow";
import { useAppStore } from "@/components/state-provider";
import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents";
import { AgentLogo } from "@/components/agent-logo";
import { getExecutorIcon } from "@/lib/executor-icons";
import { getCapabilityWarning } from "@/lib/capability-warning";
import type { Agent, AgentProfile, Executor } from "@/lib/types/http";
import type { WorkspaceState } from "@/lib/state/slices";
type WorkspaceItem = WorkspaceState["items"][number];
type GeneralSidebarSectionProps = {
pathname: string;
};
function GeneralSidebarSection({ pathname }: GeneralSidebarSectionProps) {
return (
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="General">
<Link href="/settings/general">
<IconSettings className="h-4 w-4" />
<span>General</span>
</Link>
</SidebarMenuButton>
<SidebarMenuSub className="ml-3 mt-1">
<SidebarMenuSubItem>
<SidebarMenuSubButton
asChild
size="sm"
isActive={pathname === "/settings/general/notifications"}
>
<Link href="/settings/general/notifications">
<IconBell className="h-4 w-4" />
<span>Notifications</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton
asChild
size="sm"
isActive={pathname === "/settings/general/editors"}
>
<Link href="/settings/general/editors">
<IconCode className="h-4 w-4" />
<span>Editors</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</SidebarMenuItem>
);
}
type WorkspacesSidebarSectionProps = {
pathname: string;
workspaces: WorkspaceItem[];
};
function WorkspacesSidebarSection({ pathname, workspaces }: WorkspacesSidebarSectionProps) {
return (
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Workspaces">
<Link href="/settings/workspace">
<IconFolder className="h-4 w-4" />
<span>Workspaces</span>
</Link>
</SidebarMenuButton>
{workspaces.length > 0 && (
<SidebarMenuSub className="ml-3 mt-1">
{workspaces.map((workspace) => {
const workspacePath = `/settings/workspace/${workspace.id}`;
const workflowsPath = `${workspacePath}/workflows`;
const repositoriesPath = `${workspacePath}/repositories`;
return (
<SidebarMenuSubItem key={workspace.id}>
<SidebarMenuSubButton asChild isActive={pathname === workspacePath}>
<Link href={workspacePath}>
<span>{workspace.name}</span>
</Link>
</SidebarMenuSubButton>
<SidebarMenuSub className="ml-3">
<SidebarMenuSubItem>
<SidebarMenuSubButton
asChild
size="sm"
isActive={pathname === repositoriesPath}
>
<Link href={repositoriesPath}>
<IconGitBranch className="h-3.5 w-3.5" />
<span>Repositories</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton asChild size="sm" isActive={pathname === workflowsPath}>
<Link href={workflowsPath}>
<IconArrowsShuffle className="h-3.5 w-3.5" />
<span>Workflows</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</SidebarMenuSubItem>
);
})}
</SidebarMenuSub>
)}
</SidebarMenuItem>
);
}
function IntegrationsSidebarSection({ pathname }: { pathname: string }) {
const items: Array<{ href: string; label: string; Icon: typeof IconBrandGithub }> = [
{ href: "/settings/integrations/github", label: "GitHub", Icon: IconBrandGithub },
{ href: "/settings/integrations/jira", label: "Jira", Icon: IconTicket },
{ href: "/settings/integrations/linear", label: "Linear", Icon: IconHexagon },
{ href: "/settings/integrations/slack", label: "Slack", Icon: IconBrandSlack },
];
return (
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Integrations">
<Link href="/settings/integrations">
<IconPlugConnected className="h-4 w-4" />
<span>Integrations</span>
</Link>
</SidebarMenuButton>
<SidebarMenuSub className="ml-3 mt-1">
{items.map(({ href, label, Icon }) => (
<SidebarMenuSubItem key={href}>
<SidebarMenuSubButton asChild size="sm" isActive={pathname === href}>
<Link href={href}>
<Icon className="h-3.5 w-3.5" />
<span>{label}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</SidebarMenuItem>
);
}
type AgentsSidebarSectionProps = {
pathname: string;
agents: Agent[];
};
function AgentsSidebarSection({ pathname, agents }: AgentsSidebarSectionProps) {
return (
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Agents">
<Link href="/settings/agents">
<IconRobot className="h-4 w-4" />
<span>Agents</span>
</Link>
</SidebarMenuButton>
{agents.length > 0 && (
<SidebarMenuSub className="ml-3 mt-1">
{agents.flatMap((agent: Agent) =>
agent.profiles.map((profile: AgentProfile) => {
const encodedAgent = encodeURIComponent(agent.name);
const profilePath = `/settings/agents/${encodedAgent}/profiles/${profile.id}`;
const agentLabel = profile.agentDisplayName || agent.name;
const warning = getCapabilityWarning(agent.capability_status, agent.capability_error);
return (
<SidebarMenuSubItem key={profile.id} className="min-w-0">
<SidebarMenuSubButton asChild isActive={pathname === profilePath}>
<Link
href={profilePath}
className="!flex min-w-0 items-center gap-1.5"
title={warning?.title || `${agentLabel} • ${profile.name}`}
>
<AgentLogo agentName={agent.name} className="shrink-0" />
<ScrollOnOverflow className="min-w-0">
{agentLabel} • {profile.name}
</ScrollOnOverflow>
{warning && <warning.Icon className={`size-3.5 shrink-0 ${warning.color}`} />}
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
);
}),
)}
</SidebarMenuSub>
)}
</SidebarMenuItem>
);
}
type ExecutorsSidebarSectionProps = {
pathname: string;
executors: Executor[];
};
function ExecutorsSidebarSection({ pathname, executors }: ExecutorsSidebarSectionProps) {
const allProfiles = executors.flatMap((e) =>
(e.profiles ?? []).map((p) => ({ ...p, executorType: e.type })),
);
return (
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Executors" isActive={pathname === "/settings/executors"}>
<Link href="/settings/executors">
<IconCpu className="h-4 w-4" />
<span>Executors</span>
</Link>
</SidebarMenuButton>
{allProfiles.length > 0 && (
<SidebarMenuSub className="ml-3 mt-1">
{allProfiles.map((profile) => {
const Icon = getExecutorIcon(profile.executorType);
const profilePath = `/settings/executors/${profile.id}`;
return (
<SidebarMenuSubItem key={profile.id}>
<SidebarMenuSubButton asChild size="sm" isActive={pathname === profilePath}>
<Link href={profilePath} className="!flex items-center gap-1.5">
<Icon className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span>{profile.name}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
);
})}
</SidebarMenuSub>
)}
</SidebarMenuItem>
);
}
function SecretsSidebarSection({ pathname }: { pathname: string }) {
return (
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname === "/settings/general/secrets"}>
<Link href="/settings/general/secrets">
<IconKey className="h-4 w-4" />
<span>Secrets</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
export function SettingsAppSidebar() {
const pathname = usePathname();
const { setOpenMobile, isMobile } = useSidebar();
const workspaces = useAppStore((state) => state.workspaces.items);
const executors = useAppStore((state) => state.executors.items);
const agents = useAppStore((state) => state.settingsAgents.items);
useAvailableAgents();
// Close mobile sidebar when navigating to a new page
useEffect(() => {
if (isMobile) {
setOpenMobile(false);
}
}, [pathname, isMobile, setOpenMobile]);
return (
<Sidebar variant="inset">
<SidebarHeader className="h-16 justify-center">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<Link href="/">
<span className="text-2xl font-bold">Kandev</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent className="overflow-hidden">
<ScrollArea
className="h-full [&_[data-slot=scroll-area-viewport]>div]:!block [&_[data-slot=scroll-area-viewport]>div]:!min-w-0"
type="always"
>
<SidebarGroup>
<SidebarGroupLabel>Settings</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<GeneralSidebarSection pathname={pathname} />
<WorkspacesSidebarSection pathname={pathname} workspaces={workspaces} />
<IntegrationsSidebarSection pathname={pathname} />
<AgentsSidebarSection pathname={pathname} agents={agents} />
{/* Prompts */}
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname === "/settings/prompts"}>
<Link href="/settings/prompts">
<IconMessageCircle className="h-4 w-4" />
<span>Prompts</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
{/* Utility Agents */}
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname === "/settings/utility-agents"}>
<Link href="/settings/utility-agents">
<IconWand className="h-4 w-4" />
<span>Utility Agents</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<ExecutorsSidebarSection pathname={pathname} executors={executors} />
<SecretsSidebarSection pathname={pathname} />
{/* External MCP */}
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname === "/settings/external-mcp"}>
<Link href="/settings/external-mcp">
<IconPlugConnected className="h-4 w-4" />
<span>External MCP</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
{/* Changelog */}
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname === "/settings/changelog"}>
<Link href="/settings/changelog">
<IconSparkles className="h-4 w-4" />
<span>Changelog</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</ScrollArea>
</SidebarContent>
</Sidebar>
);
}