diff --git a/src/App.tsx b/src/App.tsx index ff1e5ee..4c08c6f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ -import { Routes, Route, Navigate } from 'react-router-dom' +import { useMemo, useCallback } from 'react' +import { Routes, Route, Navigate, useNavigate } from 'react-router-dom' import { Dashboard } from './pages/Dashboard/Dashboard' import { Metrics } from './pages/Metrics/Metrics' import { RouteDetail } from './pages/RouteDetail/RouteDetail' @@ -8,19 +9,92 @@ import { Inventory } from './pages/Inventory/Inventory' import { Admin } from './pages/Admin/Admin' import { ApiDocs } from './pages/ApiDocs/ApiDocs' +import { CommandPalette } from './design-system/composites/CommandPalette/CommandPalette' +import type { SearchResult } from './design-system/composites/CommandPalette/types' +import { useCommandPalette } from './design-system/providers/CommandPaletteProvider' +import { useGlobalFilters } from './design-system/providers/GlobalFilterProvider' +import { buildSearchData } from './mocks/searchData' +import { exchanges } from './mocks/exchanges' +import { routes } from './mocks/routes' +import { agents } from './mocks/agents' +import { SIDEBAR_APPS } from './mocks/sidebar' + +/** Compute which sidebar path to reveal for a given search result */ +function computeSidebarRevealPath(result: SearchResult): string | undefined { + if (!result.path) return undefined + + if (result.category === 'application') { + // /apps/:id — already a sidebar node path + return result.path + } + + if (result.category === 'route') { + // /routes/:id — already a sidebar node path + return result.path + } + + if (result.category === 'agent') { + // /agents/:appId/:agentId — already a sidebar node path + return result.path + } + + if (result.category === 'exchange') { + // /exchanges/:id — no sidebar entry; resolve to the parent route + const exchange = exchanges.find((e) => e.id === result.id) + if (exchange) { + return `/routes/${exchange.route}` + } + } + + return result.path +} + export default function App() { + const navigate = useNavigate() + const { open: paletteOpen, setOpen } = useCommandPalette() + const { isInTimeRange, statusFilters } = useGlobalFilters() + + const filteredSearchData = useMemo(() => { + // Filter exchanges by time range and status + let filteredExchanges = exchanges.filter((e) => isInTimeRange(e.timestamp)) + if (statusFilters.size > 0) { + filteredExchanges = filteredExchanges.filter((e) => statusFilters.has(e.status)) + } + return buildSearchData(filteredExchanges, routes, agents) + }, [isInTimeRange, statusFilters]) + + const handleSelect = useCallback( + (result: SearchResult) => { + if (result.path) { + const sidebarReveal = computeSidebarRevealPath(result) + navigate(result.path, { state: sidebarReveal ? { sidebarReveal } : undefined }) + } + setOpen(false) + }, + [navigate, setOpen], + ) + return ( - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + <> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + setOpen(false)} + onOpen={() => setOpen(true)} + data={filteredSearchData} + onSelect={handleSelect} + /> + ) } diff --git a/src/design-system/composites/CommandPalette/CommandPalette.tsx b/src/design-system/composites/CommandPalette/CommandPalette.tsx index 29b2102..77cc96a 100644 --- a/src/design-system/composites/CommandPalette/CommandPalette.tsx +++ b/src/design-system/composites/CommandPalette/CommandPalette.tsx @@ -16,6 +16,7 @@ interface CommandPaletteProps { const CATEGORY_LABELS: Record = { all: 'All', + application: 'Applications', exchange: 'Exchanges', route: 'Routes', agent: 'Agents', @@ -23,6 +24,7 @@ const CATEGORY_LABELS: Record = { const ALL_CATEGORIES: Array = [ 'all', + 'application', 'exchange', 'route', 'agent', diff --git a/src/design-system/composites/CommandPalette/types.ts b/src/design-system/composites/CommandPalette/types.ts index af2541c..6b86343 100644 --- a/src/design-system/composites/CommandPalette/types.ts +++ b/src/design-system/composites/CommandPalette/types.ts @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' -export type SearchCategory = 'exchange' | 'route' | 'agent' +export type SearchCategory = 'application' | 'exchange' | 'route' | 'agent' export interface SearchResult { id: string @@ -10,6 +10,7 @@ export interface SearchResult { meta: string timestamp?: string icon?: ReactNode + path?: string expandedContent?: string matchRanges?: [number, number][] } diff --git a/src/design-system/layout/Sidebar/Sidebar.tsx b/src/design-system/layout/Sidebar/Sidebar.tsx index 0faac7e..59590ed 100644 --- a/src/design-system/layout/Sidebar/Sidebar.tsx +++ b/src/design-system/layout/Sidebar/Sidebar.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react' +import { useState, useEffect, useMemo } from 'react' import { useNavigate, useLocation } from 'react-router-dom' import styles from './Sidebar.module.css' import camelLogoUrl from '../../../assets/camel-logo.svg' @@ -220,6 +220,31 @@ export function Sidebar({ apps, className }: SidebarProps) { const appNodes = useMemo(() => buildAppTreeNodes(apps), [apps]) const agentNodes = useMemo(() => buildAgentTreeNodes(apps), [apps]) + // Sidebar reveal from Cmd-K navigation (passed via location state) + const sidebarRevealPath = (location.state as { sidebarReveal?: string } | null)?.sidebarReveal ?? null + + useEffect(() => { + if (!sidebarRevealPath) return + + // Uncollapse Applications section if reveal path matches an apps tree node + const matchesAppTree = appNodes.some((node) => + node.path === sidebarRevealPath || node.children?.some((child) => child.path === sidebarRevealPath), + ) + if (matchesAppTree && appsCollapsed) { + _setAppsCollapsed(false) + localStorage.setItem('cameleer:sidebar:apps-collapsed', 'false') + } + + // Uncollapse Agents section if reveal path matches an agents tree node + const matchesAgentTree = agentNodes.some((node) => + node.path === sidebarRevealPath || node.children?.some((child) => child.path === sidebarRevealPath), + ) + if (matchesAgentTree && agentsCollapsed) { + _setAgentsCollapsed(false) + localStorage.setItem('cameleer:sidebar:agents-collapsed', 'false') + } + }, [sidebarRevealPath]) // eslint-disable-line react-hooks/exhaustive-deps + // Build starred items const starredItems = useMemo( () => collectStarredItems(apps, starredIds), @@ -231,6 +256,12 @@ export function Sidebar({ apps, className }: SidebarProps) { const starredAgents = starredItems.filter((i) => i.type === 'agent') const hasStarred = starredItems.length > 0 + // For exchange detail pages, use the reveal path for sidebar selection so + // the parent route is highlighted (exchanges have no sidebar entry of their own) + const effectiveSelectedPath = location.pathname.startsWith('/exchanges/') && sidebarRevealPath + ? sidebarRevealPath + : location.pathname + return (