feat: fix Cmd-K shortcut and add exchange full-text search to command palette
All checks were successful
CI / build (push) Successful in 1m43s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m17s
CI / deploy (push) Successful in 40s
CI / deploy-feature (push) Has been skipped

- 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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-25 08:57:24 +01:00
parent 552f02d25c
commit b32c97c02b
4 changed files with 56 additions and 7 deletions

View File

@@ -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 . .

8
ui/package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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<T>(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() {
<CommandPalette
open={paletteOpen}
onClose={() => setPaletteOpen(false)}
onOpen={() => setPaletteOpen(true)}
onSelect={handlePaletteSelect}
onQueryChange={setPaletteQuery}
data={searchData}
/>
<main style={{ flex: 1, overflow: 'auto', padding: '1.5rem' }}>