feat: Cmd+K Enter applies full-text search to dashboard
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m47s
CI / docker (push) Successful in 1m16s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s

When pressing Enter in the command palette without explicitly selecting
a result (via arrow keys or mouse), the search query is now applied as
a server-side full-text filter on the Dashboard table. Explicit
selection still navigates to the exchange. Updates design system to
v0.1.18 for the new onSubmit prop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-27 23:33:39 +01:00
parent 004574d442
commit 1702200a60
5 changed files with 61 additions and 11 deletions

9
ui/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "ui", "name": "ui",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@cameleer/design-system": "^0.1.17", "@cameleer/design-system": "^0.1.18",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"lucide-react": "^1.7.0", "lucide-react": "^1.7.0",
"openapi-fetch": "^0.17.0", "openapi-fetch": "^0.17.0",
@@ -277,10 +277,11 @@
} }
}, },
"node_modules/@cameleer/design-system": { "node_modules/@cameleer/design-system": {
"version": "0.1.17", "version": "0.1.18",
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.17/design-system-0.1.17.tgz", "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.18/design-system-0.1.18.tgz",
"integrity": "sha512-THK6yN+xSrxEJadEQ4AZiVhPvoI2rq6gvmMonpxVhUw93dOPO5p06pRS5csJc1miFD1thOrazsoDzSTAbNaELw==", "integrity": "sha512-uvGr4PFw6Eya+h9DSD0wBnzjIXhZpcndR2dDJX2tMvQqgy+32WTTTQ8BZZWZjOKLSv63UpBN/fwVSXtkA4dnqA==",
"dependencies": { "dependencies": {
"lucide-react": "^1.7.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^7.0.0" "react-router-dom": "^7.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" "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": { "dependencies": {
"@cameleer/design-system": "^0.1.17", "@cameleer/design-system": "^0.1.18",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"lucide-react": "^1.7.0", "lucide-react": "^1.7.0",
"openapi-fetch": "^0.17.0", "openapi-fetch": "^0.17.0",

View File

@@ -199,6 +199,14 @@ function LayoutContent() {
setPaletteOpen(false); setPaletteOpen(false);
}, [navigate, setPaletteOpen]); }, [navigate, setPaletteOpen]);
const handlePaletteSubmit = useCallback((query: string) => {
// Navigate to dashboard with full-text search applied
const currentPath = location.pathname;
// Stay on the current app/route context if we're already there
const basePath = currentPath.startsWith('/apps/') ? currentPath.split('/').slice(0, 4).join('/') : '/apps';
navigate(`${basePath}?text=${encodeURIComponent(query)}`);
}, [navigate, location.pathname]);
return ( return (
<AppShell <AppShell
sidebar={ sidebar={
@@ -217,6 +225,7 @@ function LayoutContent() {
onClose={() => setPaletteOpen(false)} onClose={() => setPaletteOpen(false)}
onOpen={() => setPaletteOpen(true)} onOpen={() => setPaletteOpen(true)}
onSelect={handlePaletteSelect} onSelect={handlePaletteSelect}
onSubmit={handlePaletteSubmit}
onQueryChange={setPaletteQuery} onQueryChange={setPaletteQuery}
data={searchData} data={searchData}
/> />

View File

@@ -30,11 +30,34 @@
} }
.tableTitle { .tableTitle {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
} }
.clearSearch {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin-left: 4px;
border: none;
background: var(--bg-hover, #F5F0EA);
color: var(--text-secondary, #5C5347);
border-radius: 50%;
cursor: pointer;
padding: 0;
}
.clearSearch:hover {
background: var(--border, #E4DFD8);
color: var(--text-primary, #1A1612);
}
.tableRight { .tableRight {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -1,6 +1,6 @@
import { useState, useMemo, useCallback } from 'react' import { useState, useMemo, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router' import { useParams, useNavigate, useSearchParams } from 'react-router'
import { ExternalLink, AlertTriangle } from 'lucide-react' import { ExternalLink, AlertTriangle, X, Search } from 'lucide-react'
import { import {
DataTable, DataTable,
DetailPanel, DetailPanel,
@@ -196,6 +196,8 @@ const SHORTCUTS = [
export default function Dashboard() { export default function Dashboard() {
const { appId, routeId } = useParams<{ appId: string; routeId: string }>() const { appId, routeId } = useParams<{ appId: string; routeId: string }>()
const navigate = useNavigate() const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const textFilter = searchParams.get('text') || undefined
const [selectedId, setSelectedId] = useState<string | undefined>() const [selectedId, setSelectedId] = useState<string | undefined>()
const [panelOpen, setPanelOpen] = useState(false) const [panelOpen, setPanelOpen] = useState(false)
const [sortField, setSortField] = useState<string>('startTime') const [sortField, setSortField] = useState<string>('startTime')
@@ -226,12 +228,13 @@ export default function Dashboard() {
routeId: routeId || undefined, routeId: routeId || undefined,
application: appId || undefined, application: appId || undefined,
status: statusParam, status: statusParam,
text: textFilter,
sortField, sortField,
sortDir, sortDir,
offset: 0, offset: 0,
limit: 50, limit: textFilter ? 200 : 50,
}, },
true, !textFilter,
) )
const { data: detail } = useExecutionDetail(selectedId ?? null) const { data: detail } = useExecutionDetail(selectedId ?? null)
const { data: diagram } = useDiagramLayout(detail?.diagramContentHash ?? null) const { data: diagram } = useDiagramLayout(detail?.diagramContentHash ?? null)
@@ -398,12 +401,26 @@ export default function Dashboard() {
{/* Exchanges table */} {/* Exchanges table */}
<div className={styles.tableSection}> <div className={styles.tableSection}>
<div className={styles.tableHeader}> <div className={styles.tableHeader}>
<span className={styles.tableTitle}>Recent Exchanges</span> <span className={styles.tableTitle}>
{textFilter ? (
<>
<Search size={14} style={{ marginRight: 4, verticalAlign: -2 }} />
Search: &ldquo;{textFilter}&rdquo;
<button
className={styles.clearSearch}
onClick={() => setSearchParams({})}
title="Clear search"
>
<X size={12} />
</button>
</>
) : 'Recent Exchanges'}
</span>
<div className={styles.tableRight}> <div className={styles.tableRight}>
<span className={styles.tableMeta}> <span className={styles.tableMeta}>
{rows.length.toLocaleString()} of {(searchResult?.total ?? 0).toLocaleString()} exchanges {rows.length.toLocaleString()} of {(searchResult?.total ?? 0).toLocaleString()} exchanges
</span> </span>
<Badge label="LIVE" color="success" /> {!textFilter && <Badge label="LIVE" color="success" />}
</div> </div>
</div> </div>