feat(ui): exchange list reads ?attr= URL params and renders filter chips

(carries forward pre-existing attribute-badge color-by-key tweak)
This commit is contained in:
hsiegeln
2026-04-24 11:05:50 +02:00
parent 510206c752
commit 68704e15b4
2 changed files with 70 additions and 12 deletions

View File

@@ -139,3 +139,23 @@
color: var(--text-muted);
}
.attrChip {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 8px;
padding: 2px 8px;
background: var(--bg-hover);
border: 1px solid var(--border);
border-radius: 10px;
font-size: 11px;
font-family: var(--font-mono);
color: var(--text-primary);
}
.attrChip code {
background: transparent;
font-family: inherit;
color: var(--text-primary);
}

View File

@@ -15,6 +15,8 @@ import {
import { useEnvironmentStore } from '../../api/environment-store'
import type { ExecutionSummary } from '../../api/types'
import { attributeBadgeColor } from '../../utils/attribute-color'
import { parseAttrParam, formatAttrParam } from '../../utils/attribute-filter';
import type { AttributeFilter } from '../../utils/attribute-filter';
import { formatDuration, statusLabel } from '../../utils/format-utils'
import styles from './Dashboard.module.css'
import tableStyles from '../../styles/table-section.module.css'
@@ -84,7 +86,7 @@ function buildColumns(hasAttributes: boolean): Column<Row>[] {
<div className={styles.attrCell}>
{shown.map(([k, v]) => (
<span key={k} title={k}>
<Badge label={String(v)} color={attributeBadgeColor(String(v))} />
<Badge label={String(v)} color={attributeBadgeColor(k)} />
</span>
))}
{overflow > 0 && <span className={styles.attrOverflow}>+{overflow}</span>}
@@ -147,6 +149,12 @@ export default function Dashboard({ onExchangeSelect, activeExchangeId }: Dashbo
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const textFilter = searchParams.get('text') || undefined
const attributeFilters = useMemo<AttributeFilter[]>(
() => searchParams.getAll('attr')
.map(parseAttrParam)
.filter((f): f is AttributeFilter => f != null),
[searchParams],
);
const [selectedId, setSelectedId] = useState<string | undefined>(activeExchangeId)
const [sortField, setSortField] = useState<string>('startTime')
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
@@ -180,12 +188,13 @@ export default function Dashboard({ onExchangeSelect, activeExchangeId }: Dashbo
environment: selectedEnv,
status: statusParam,
text: textFilter,
attributeFilters: attributeFilters.length > 0 ? attributeFilters : undefined,
sortField,
sortDir,
offset: 0,
limit: textFilter ? 200 : 50,
limit: textFilter || attributeFilters.length > 0 ? 200 : 50,
},
!textFilter,
!textFilter && attributeFilters.length === 0,
)
// ─── Rows ────────────────────────────────────────────────────────────────
@@ -221,18 +230,47 @@ export default function Dashboard({ onExchangeSelect, activeExchangeId }: Dashbo
<div className={`${tableStyles.tableSection} ${styles.tableWrap}`}>
<div className={tableStyles.tableHeader}>
<span className={tableStyles.tableTitle}>
{textFilter ? (
{textFilter || attributeFilters.length > 0 ? (
<>
<Search size={14} style={{ marginRight: 4, verticalAlign: -2 }} />
{textFilter && (
<>
Search: &ldquo;{textFilter}&rdquo;
<button
className={styles.clearSearch}
onClick={() => setSearchParams({})}
title="Clear search"
onClick={() => {
const next = new URLSearchParams(searchParams);
next.delete('text');
setSearchParams(next);
}}
title="Clear text search"
>
<X size={12} />
</button>
</>
)}
{attributeFilters.map((f, i) => (
<span key={`${f.key}:${f.value ?? ''}:${i}`} className={styles.attrChip}>
{f.value === undefined
? <>has <code>{f.key}</code></>
: <><code>{f.key}</code> = <code>{f.value}</code></>}
<button
className={styles.clearSearch}
onClick={() => {
const next = new URLSearchParams(searchParams);
const remaining = next.getAll('attr')
.filter(a => a !== formatAttrParam(f));
next.delete('attr');
remaining.forEach(a => next.append('attr', a));
setSearchParams(next);
}}
title="Remove filter"
>
<X size={12} />
</button>
</span>
))}
</>
) : 'Recent Exchanges'}
</span>
<div className={tableStyles.tableRight}>