Ships accumulated WIP that was sitting uncommitted: - New (home) dashboard route pages: overview, code, data/tables, hosting, infrastructure, services, domains, integrations, agents, analytics, api, automations, billing, logs, market, marketing(+seo/social), product, security, storage, users, settings(app/auth). - dashboard-sidebar, project-icon-rail, chat-panel updates; mcp + anatomy route changes; package.json/lock dependency bumps. - Coolify log tooling (scripts/fetch-app-logs.mjs + fetch-app-logs-ssh.mjs) and ai-new-thread.md "Fetching Production Logs" section. Excludes throwaway debug scripts and telemetry audit dumps (the latter contain live credentials and must not be committed).
344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import {
|
|
Search,
|
|
LayoutGrid,
|
|
ClipboardList,
|
|
Database,
|
|
BarChart2,
|
|
Globe,
|
|
Plug,
|
|
ShieldCheck,
|
|
Code2,
|
|
Terminal,
|
|
Settings,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Users,
|
|
HardDrive,
|
|
Blocks,
|
|
} from "lucide-react";
|
|
|
|
import { useAnatomy } from "@/components/project/use-anatomy";
|
|
|
|
export function DashboardSidebar({
|
|
workspace,
|
|
projectId,
|
|
children,
|
|
}: {
|
|
workspace: string;
|
|
projectId: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
const pathname = usePathname() ?? "";
|
|
const projectBase = `/${workspace}/project/${projectId}`;
|
|
const isPreview =
|
|
pathname === `${projectBase}/preview` ||
|
|
pathname.startsWith(`${projectBase}/preview/`);
|
|
|
|
const [expandedSections, setExpandedSections] = useState<
|
|
Record<string, boolean>
|
|
>({
|
|
settings: true,
|
|
data: true,
|
|
});
|
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
const { anatomy } = useAnatomy(projectId);
|
|
const databases = anatomy?.infrastructure?.databases ?? [];
|
|
|
|
if (isPreview) {
|
|
return <>{children}</>;
|
|
}
|
|
|
|
const handleSectionClick = (segment: string) => {
|
|
if (!expandedSections[segment]) {
|
|
setExpandedSections((prev) => ({ ...prev, [segment]: true }));
|
|
}
|
|
};
|
|
|
|
const menuItems = [
|
|
{ segment: "overview", label: "Overview", Icon: LayoutGrid },
|
|
{ segment: "plan", label: "Plan & Specs", Icon: ClipboardList },
|
|
{ segment: "code", label: "Code", Icon: Code2 },
|
|
{
|
|
segment: "data",
|
|
label: "Data",
|
|
Icon: Database,
|
|
hasChildren: true,
|
|
children: databases.map((db) => ({
|
|
segment: `data/tables?db=${db.uuid}`,
|
|
label: db.name,
|
|
})),
|
|
},
|
|
{ segment: "storage", label: "Storage", Icon: HardDrive },
|
|
{ segment: "services", label: "Services", Icon: Blocks },
|
|
{ segment: "users", label: "Auth / Users", Icon: Users },
|
|
{ segment: "integrations", label: "Integrations", Icon: Plug },
|
|
{ segment: "security", label: "Security", Icon: ShieldCheck },
|
|
{ segment: "logs", label: "Logs", Icon: Terminal },
|
|
{ segment: "domains", label: "Domains", Icon: Globe },
|
|
{
|
|
segment: "analytics",
|
|
label: "Analytics",
|
|
Icon: BarChart2,
|
|
badge: "Soon",
|
|
},
|
|
{
|
|
segment: "marketing",
|
|
label: "Marketing",
|
|
Icon: BarChart2,
|
|
badge: "New",
|
|
hasChildren: true,
|
|
children: [
|
|
{ segment: "marketing/seo", label: "SEO & GEO" },
|
|
{ segment: "marketing/social", label: "Social content" },
|
|
],
|
|
},
|
|
{
|
|
segment: "settings",
|
|
label: "Settings",
|
|
Icon: Settings,
|
|
hasChildren: true,
|
|
children: [
|
|
{ segment: "settings/app", label: "App Settings" },
|
|
{ segment: "settings/auth", label: "Authentication" },
|
|
],
|
|
},
|
|
];
|
|
|
|
const filteredItems = menuItems.filter(
|
|
(item) =>
|
|
item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
(item.children &&
|
|
item.children.some((child) =>
|
|
child.label.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
)),
|
|
);
|
|
|
|
return (
|
|
<div style={{ display: "flex", flex: 1, minHeight: 0, minWidth: 0 }}>
|
|
<div
|
|
style={{
|
|
width: 250,
|
|
borderRight: "1px solid #e4e4e7",
|
|
background: "#ffffff",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
padding: "16px 12px",
|
|
gap: 4,
|
|
overflowY: "auto",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
fontSize: "0.85rem",
|
|
fontWeight: 600,
|
|
color: "#18181b",
|
|
padding: "0 8px 12px",
|
|
}}
|
|
>
|
|
Dashboard
|
|
</div>
|
|
|
|
{/* Search Bar */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
background: "#f4f4f5",
|
|
borderRadius: 8,
|
|
padding: "6px 10px",
|
|
marginBottom: 12,
|
|
margin: "0 4px 12px 4px",
|
|
}}
|
|
>
|
|
<Search size={14} color="#a1a1aa" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
style={{
|
|
border: "none",
|
|
background: "transparent",
|
|
outline: "none",
|
|
width: "100%",
|
|
fontSize: "0.8rem",
|
|
color: "#18181b",
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
{filteredItems.map((item) => {
|
|
const isMainActive =
|
|
pathname === `${projectBase}/${item.segment}` ||
|
|
pathname.startsWith(`${projectBase}/${item.segment}/`);
|
|
const isExpanded = expandedSections[item.segment];
|
|
|
|
return (
|
|
<div key={item.segment}>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
padding: "8px 10px",
|
|
borderRadius: 8,
|
|
cursor: "pointer",
|
|
background:
|
|
isMainActive && !item.hasChildren
|
|
? "#eff6ff"
|
|
: "transparent",
|
|
color:
|
|
isMainActive && !item.hasChildren ? "#1d4ed8" : "#52525b",
|
|
transition: "all 0.1s ease",
|
|
}}
|
|
onClick={() => {
|
|
if (item.hasChildren) {
|
|
setExpandedSections((prev) => ({
|
|
...prev,
|
|
[item.segment]: !prev[item.segment],
|
|
}));
|
|
}
|
|
}}
|
|
>
|
|
{item.hasChildren ? (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 10,
|
|
flex: 1,
|
|
}}
|
|
>
|
|
<item.Icon size={16} />
|
|
<span style={{ fontSize: "0.85rem", fontWeight: 500 }}>
|
|
{item.label}
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<Link
|
|
href={`${projectBase}/${item.segment}`}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 10,
|
|
flex: 1,
|
|
textDecoration: "none",
|
|
color: "inherit",
|
|
}}
|
|
>
|
|
<item.Icon size={16} />
|
|
<span style={{ fontSize: "0.85rem", fontWeight: 500 }}>
|
|
{item.label}
|
|
</span>
|
|
</Link>
|
|
)}
|
|
|
|
<div
|
|
style={{ display: "flex", alignItems: "center", gap: 6 }}
|
|
>
|
|
{item.badge && (
|
|
<span
|
|
style={{
|
|
background: "#eef2ff",
|
|
color: "#4f46e5",
|
|
fontSize: "0.65rem",
|
|
fontWeight: 600,
|
|
padding: "2px 6px",
|
|
borderRadius: 999,
|
|
}}
|
|
>
|
|
{item.badge}
|
|
</span>
|
|
)}
|
|
{item.hasChildren &&
|
|
(isExpanded ? (
|
|
<ChevronDown size={14} color="#a1a1aa" />
|
|
) : (
|
|
<ChevronRight size={14} color="#a1a1aa" />
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Render Children if expanded */}
|
|
{item.hasChildren && isExpanded && item.children && (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
marginTop: 2,
|
|
}}
|
|
>
|
|
{item.children.map((child) => {
|
|
const href = child.segment.includes("?")
|
|
? `${projectBase}/${child.segment.split("?")[0]}?${child.segment.split("?")[1]}`
|
|
: `${projectBase}/${child.segment}`;
|
|
let isChildActive = false;
|
|
if (child.segment.includes("?")) {
|
|
const [basePath, searchStr] = child.segment.split("?");
|
|
isChildActive =
|
|
pathname === `${projectBase}/${basePath}` &&
|
|
(typeof window !== "undefined"
|
|
? window.location.search.includes(searchStr)
|
|
: false);
|
|
} else {
|
|
isChildActive =
|
|
pathname === `${projectBase}/${child.segment}`;
|
|
}
|
|
|
|
return (
|
|
<Link
|
|
key={child.segment}
|
|
href={href}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
padding: "6px 10px 6px 14px",
|
|
marginLeft: "18px",
|
|
borderRadius: "0 8px 8px 0",
|
|
fontSize: "0.8rem",
|
|
fontWeight: 500,
|
|
textDecoration: "none",
|
|
color: isChildActive ? "#18181b" : "#52525b",
|
|
background: "transparent",
|
|
transition: "all 0.1s ease",
|
|
borderLeft: isChildActive
|
|
? "2px solid #18181b"
|
|
: "2px solid transparent",
|
|
}}
|
|
>
|
|
{child.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
minWidth: 0,
|
|
overflow: "auto",
|
|
background: "#fff",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|