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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 81 additions & 18 deletions apps/dashboard/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,70 @@ import { QK } from "@/lib/query-keys";
import { useQuery } from "@tanstack/react-query";
import { Command } from "cmdk";
import {
Activity,
BarChart2,
Bell,
Clock,
Code,
Database,
FolderOpen,
Globe,
HardDrive,
Key,
LayoutDashboard,
ScrollText,
Settings,
Shield,
Users,
Webhook,
Zap,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router";

interface CommandPaletteProps {
open: boolean;
onClose: () => void;
}

const staticCommands = [
interface CommandItem {
label: string;
href: string;
icon: React.ElementType;
keywords?: string;
}

const staticCommands: CommandItem[] = [
{ label: "Overview", href: "/", icon: LayoutDashboard },
{ label: "Projects", href: "/projects", icon: FolderOpen },
{ label: "Storage", href: "/storage", icon: HardDrive },
{ label: "Logs", href: "/logs", icon: ScrollText },
{ label: "Observability", href: "/observability", icon: Activity },
{ label: "Metrics", href: "/metrics", icon: BarChart2 },
{ label: "Audit Log", href: "/audit", icon: Shield },
{ label: "Team", href: "/team", icon: Users },
{ label: "Settings", href: "/settings", icon: Settings },
{ label: "Settings", href: "/settings", icon: Settings, keywords: "general" },
{ label: "SMTP Settings", href: "/settings/smtp", icon: Settings },
{ label: "API Keys", href: "/settings/api-keys", icon: Settings },
{ label: "Notifications", href: "/settings/notifications", icon: Bell },
{ label: "API Keys", href: "/settings/api-keys", icon: Key },
{ label: "Inngest", href: "/settings/inngest", icon: Zap },
];

const projectTabCommands = (projectId: string): CommandItem[] => [
{ label: "Overview", href: `/projects/${projectId}`, icon: FolderOpen },
{ label: "Observability", href: `/projects/${projectId}/observability`, icon: Activity },
Comment on lines +55 to +57
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Deduplicate per-project commands before rendering.

Line 90 and Line 56 both produce the same route (/projects/${projectId}), then Line 167 uses href as the item identity. This introduces duplicate command identities in the Projects list and unstable item behavior.

Proposed fix
 const allCommands = useMemo(() => {
 	const commands = [...staticCommands];
 	if (projectsData?.projects) {
 		for (const project of projectsData.projects) {
 			commands.push({
 				label: project.name,
 				href: `/projects/${project.id}`,
 				icon: FolderOpen,
 				keywords: `project ${project.name}`,
 			});
 			for (const tab of projectTabCommands(project.id)) {
 				commands.push({
 					label: `${project.name} > ${tab.label}`,
 					href: tab.href,
 					icon: tab.icon,
 					keywords: `project ${project.name} ${tab.label}`,
 				});
 			}
 		}
 	}
-	return commands;
+	const seen = new Set<string>();
+	return commands.filter((cmd) => {
+		if (seen.has(cmd.href)) return false;
+		seen.add(cmd.href);
+		return true;
+	});
 }, [projectsData]);

Also applies to: 90-103, 163-168

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/CommandPalette.tsx` around lines 55 - 57,
projectTabCommands currently returns duplicate CommandItem entries (same href
`/projects/${projectId}`) which causes unstable identity in the Projects list;
update the code that builds per-project commands (projectTabCommands and any
other arrays producing per-project items) to deduplicate by href before
rendering — e.g., produce a unique list of CommandItem where each href is unique
or ensure items use a stable unique id instead of duplicated href; specifically
modify projectTabCommands and the Projects list builder (the component that uses
href as item identity) to filter out duplicates (by href) so each
CommandItem.href is unique when passed to the renderer.

{ label: "Users", href: `/projects/${projectId}/users`, icon: Users },
{ label: "Auth", href: `/projects/${projectId}/auth`, icon: Key },
{ label: "Database", href: `/projects/${projectId}/database`, icon: Database },
{ label: "Environment", href: `/projects/${projectId}/env`, icon: Globe },
{ label: "Webhooks", href: `/projects/${projectId}/webhooks`, icon: Webhook },
{ label: "Functions", href: `/projects/${projectId}/functions`, icon: Zap },
{ label: "IaC Schema", href: `/projects/${projectId}/iac/schema`, icon: Code },
{ label: "IaC Functions", href: `/projects/${projectId}/iac/functions`, icon: Code },
{ label: "IaC Jobs", href: `/projects/${projectId}/iac/jobs`, icon: Code },
{ label: "IaC Realtime", href: `/projects/${projectId}/iac/realtime`, icon: Code },
{ label: "IaC Query", href: `/projects/${projectId}/iac/query`, icon: Code },
{ label: "Realtime", href: `/projects/${projectId}/realtime`, icon: Clock },
];

export function CommandPalette({ open, onClose }: CommandPaletteProps) {
Expand All @@ -45,6 +83,29 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) {
if (!open) setQuery("");
}, [open]);

const allCommands = useMemo(() => {
const commands = [...staticCommands];
if (projectsData?.projects) {
for (const project of projectsData.projects) {
commands.push({
label: project.name,
href: `/projects/${project.id}`,
icon: FolderOpen,
keywords: `project ${project.name}`,
});
for (const tab of projectTabCommands(project.id)) {
commands.push({
label: `${project.name} > ${tab.label}`,
href: tab.href,
icon: tab.icon,
keywords: `project ${project.name} ${tab.label}`,
});
}
}
}
return commands;
}, [projectsData]);

if (!open) return null;

function go(href: string) {
Expand All @@ -68,13 +129,13 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) {
<Command.Input
value={query}
onValueChange={setQuery}
placeholder="Search pages, projects..."
placeholder="Search pages, projects, settings..."
className="w-full bg-transparent outline-none text-sm"
style={{ color: "var(--color-text-primary)" }}
autoFocus
/>
</div>
<Command.List className="max-h-80 overflow-y-auto p-2">
<Command.List className="max-h-96 overflow-y-auto p-2">
<Command.Empty
className="py-8 text-center text-sm"
style={{ color: "var(--color-text-muted)" }}
Expand All @@ -99,18 +160,20 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) {

{(projectsData?.projects?.length ?? 0) > 0 && (
<Command.Group heading="Projects">
{projectsData!.projects.map((p) => (
<Command.Item
key={p.id}
value={p.name}
onSelect={() => go(`/projects/${p.id}`)}
className="flex items-center gap-2.5 px-3 py-2 rounded-lg cursor-pointer text-sm"
style={{ color: "var(--color-text-secondary)" }}
>
<FolderOpen size={14} />
{p.name}
</Command.Item>
))}
{allCommands
.filter((c) => c.keywords?.includes("project"))
.map((cmd) => (
<Command.Item
key={cmd.href}
value={cmd.label}
onSelect={() => go(cmd.href)}
className="flex items-center gap-2.5 px-3 py-2 rounded-lg cursor-pointer text-sm"
style={{ color: "var(--color-text-secondary)" }}
>
<cmd.icon size={14} />
{cmd.label}
</Command.Item>
))}
</Command.Group>
)}
</Command.List>
Expand Down
Loading
Loading