From b32c97c02bbd60e1d0e3b9bdd2cdcc9d23697792 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:57:24 +0100 Subject: [PATCH] feat: fix Cmd-K shortcut and add exchange full-text search to command palette - Add missing onOpen prop to CommandPalette (fixes Ctrl+K/Cmd+K) - Wire server-side exchange search with debounced text query - Use design system dev snapshot from Gitea registry in CI builds Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/Dockerfile | 1 + ui/package-lock.json | 8 ++--- ui/package.json | 2 +- ui/src/components/LayoutShell.tsx | 52 +++++++++++++++++++++++++++++-- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/ui/Dockerfile b/ui/Dockerfile index 726bbb4b..80d77046 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -5,6 +5,7 @@ ARG REGISTRY_TOKEN COPY package.json package-lock.json .npmrc ./ RUN echo "//gitea.siegeln.net/api/packages/cameleer/npm/:_authToken=${REGISTRY_TOKEN}" >> .npmrc && \ npm ci && \ + npm install @cameleer/design-system@dev && \ rm -f .npmrc COPY . . diff --git a/ui/package-lock.json b/ui/package-lock.json index 96e3c76a..366ccd10 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,7 +8,7 @@ "name": "ui", "version": "0.0.0", "dependencies": { - "@cameleer/design-system": "^0.1.8", + "@cameleer/design-system": "^0.0.0-snapshot.20260325.499c86b", "@tanstack/react-query": "^5.90.21", "openapi-fetch": "^0.17.0", "react": "^19.2.4", @@ -276,9 +276,9 @@ } }, "node_modules/@cameleer/design-system": { - "version": "0.1.8", - "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.8/design-system-0.1.8.tgz", - "integrity": "sha512-mc7IQOYYez0UItvwiNbbYFrJehG3JtdVlOUsdLXcN8zmgtpImleVro4MsPxCX4/OOGI4EGoX1oIVpFi91qEI6A==", + "version": "0.0.0-snapshot.20260325.499c86b", + "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.0.0-snapshot.20260325.499c86b/design-system-0.0.0-snapshot.20260325.499c86b.tgz", + "integrity": "sha512-uiBdWYTT0wzIgL8QX21oHyb7xjeepnXvGAl/YHapd1o4u+GuXuB23kcyECurM/OTUN4dM7RGjGBp46Mbe8xcIQ==", "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/ui/package.json b/ui/package.json index 3c0bc1b5..9c7150a1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,7 +14,7 @@ "generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts" }, "dependencies": { - "@cameleer/design-system": "^0.1.8", + "@cameleer/design-system": "^0.0.0-snapshot.20260325.499c86b", "@tanstack/react-query": "^5.90.21", "openapi-fetch": "^0.17.0", "react": "^19.2.4", diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index f3abaef2..15afcf06 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -3,8 +3,9 @@ import { AppShell, Sidebar, TopBar, CommandPalette, CommandPaletteProvider, Glob import type { SidebarApp, SearchResult } from '@cameleer/design-system'; import { useRouteCatalog } from '../api/queries/catalog'; import { useAgents } from '../api/queries/agents'; +import { useSearchExecutions } from '../api/queries/executions'; import { useAuthStore } from '../auth/auth-store'; -import { useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback, useEffect } from 'react'; function healthToColor(health: string): string { switch (health) { @@ -61,6 +62,30 @@ function buildSearchData( return results; } +function formatDuration(ms: number): string { + if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`; + if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`; + return `${ms}ms`; +} + +function statusToColor(status: string): string { + switch (status) { + case 'COMPLETED': return 'success'; + case 'FAILED': return 'error'; + case 'RUNNING': return 'running'; + default: return 'warning'; + } +} + +function useDebouncedValue(value: T, delayMs: number): T { + const [debounced, setDebounced] = useState(value); + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delayMs); + return () => clearTimeout(timer); + }, [value, delayMs]); + return debounced; +} + function LayoutContent() { const navigate = useNavigate(); const location = useLocation(); @@ -69,6 +94,14 @@ function LayoutContent() { const { username, logout } = useAuthStore(); const { open: paletteOpen, setOpen: setPaletteOpen } = useCommandPalette(); + // Exchange full-text search via command palette + const [paletteQuery, setPaletteQuery] = useState(''); + const debouncedQuery = useDebouncedValue(paletteQuery, 300); + const { data: exchangeResults } = useSearchExecutions( + { text: debouncedQuery || undefined, offset: 0, limit: 10 }, + false, + ); + const sidebarApps: SidebarApp[] = useMemo(() => { if (!catalog) return []; return catalog.map((app: any) => ({ @@ -90,11 +123,24 @@ function LayoutContent() { })); }, [catalog]); - const searchData = useMemo( + const catalogData = useMemo( () => buildSearchData(catalog, agents as any[]), [catalog, agents], ); + const searchData: SearchResult[] = useMemo(() => { + const exchangeItems: SearchResult[] = (exchangeResults?.data || []).map((e: any) => ({ + id: e.executionId, + category: 'exchange' as const, + title: e.executionId, + badges: [{ label: e.status, color: statusToColor(e.status) }], + meta: `${e.routeId} · ${e.applicationName ?? ''} · ${formatDuration(e.durationMs)}`, + path: `/exchanges/${e.executionId}`, + serverFiltered: true, + })); + return [...catalogData, ...exchangeItems]; + }, [catalogData, exchangeResults]); + const breadcrumb = useMemo(() => { const parts = location.pathname.split('/').filter(Boolean); return parts.map((part, i) => ({ @@ -131,7 +177,9 @@ function LayoutContent() { setPaletteOpen(false)} + onOpen={() => setPaletteOpen(true)} onSelect={handlePaletteSelect} + onQueryChange={setPaletteQuery} data={searchData} />