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:
@@ -139,3 +139,23 @@
|
|||||||
color: var(--text-muted);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
import { useEnvironmentStore } from '../../api/environment-store'
|
import { useEnvironmentStore } from '../../api/environment-store'
|
||||||
import type { ExecutionSummary } from '../../api/types'
|
import type { ExecutionSummary } from '../../api/types'
|
||||||
import { attributeBadgeColor } from '../../utils/attribute-color'
|
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 { formatDuration, statusLabel } from '../../utils/format-utils'
|
||||||
import styles from './Dashboard.module.css'
|
import styles from './Dashboard.module.css'
|
||||||
import tableStyles from '../../styles/table-section.module.css'
|
import tableStyles from '../../styles/table-section.module.css'
|
||||||
@@ -84,7 +86,7 @@ function buildColumns(hasAttributes: boolean): Column<Row>[] {
|
|||||||
<div className={styles.attrCell}>
|
<div className={styles.attrCell}>
|
||||||
{shown.map(([k, v]) => (
|
{shown.map(([k, v]) => (
|
||||||
<span key={k} title={k}>
|
<span key={k} title={k}>
|
||||||
<Badge label={String(v)} color={attributeBadgeColor(String(v))} />
|
<Badge label={String(v)} color={attributeBadgeColor(k)} />
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{overflow > 0 && <span className={styles.attrOverflow}>+{overflow}</span>}
|
{overflow > 0 && <span className={styles.attrOverflow}>+{overflow}</span>}
|
||||||
@@ -147,6 +149,12 @@ export default function Dashboard({ onExchangeSelect, activeExchangeId }: Dashbo
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const textFilter = searchParams.get('text') || undefined
|
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 [selectedId, setSelectedId] = useState<string | undefined>(activeExchangeId)
|
||||||
const [sortField, setSortField] = useState<string>('startTime')
|
const [sortField, setSortField] = useState<string>('startTime')
|
||||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
|
||||||
@@ -180,12 +188,13 @@ export default function Dashboard({ onExchangeSelect, activeExchangeId }: Dashbo
|
|||||||
environment: selectedEnv,
|
environment: selectedEnv,
|
||||||
status: statusParam,
|
status: statusParam,
|
||||||
text: textFilter,
|
text: textFilter,
|
||||||
|
attributeFilters: attributeFilters.length > 0 ? attributeFilters : undefined,
|
||||||
sortField,
|
sortField,
|
||||||
sortDir,
|
sortDir,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: textFilter ? 200 : 50,
|
limit: textFilter || attributeFilters.length > 0 ? 200 : 50,
|
||||||
},
|
},
|
||||||
!textFilter,
|
!textFilter && attributeFilters.length === 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
// ─── Rows ────────────────────────────────────────────────────────────────
|
// ─── Rows ────────────────────────────────────────────────────────────────
|
||||||
@@ -221,17 +230,46 @@ export default function Dashboard({ onExchangeSelect, activeExchangeId }: Dashbo
|
|||||||
<div className={`${tableStyles.tableSection} ${styles.tableWrap}`}>
|
<div className={`${tableStyles.tableSection} ${styles.tableWrap}`}>
|
||||||
<div className={tableStyles.tableHeader}>
|
<div className={tableStyles.tableHeader}>
|
||||||
<span className={tableStyles.tableTitle}>
|
<span className={tableStyles.tableTitle}>
|
||||||
{textFilter ? (
|
{textFilter || attributeFilters.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Search size={14} style={{ marginRight: 4, verticalAlign: -2 }} />
|
<Search size={14} style={{ marginRight: 4, verticalAlign: -2 }} />
|
||||||
Search: “{textFilter}”
|
{textFilter && (
|
||||||
<button
|
<>
|
||||||
className={styles.clearSearch}
|
Search: “{textFilter}”
|
||||||
onClick={() => setSearchParams({})}
|
<button
|
||||||
title="Clear search"
|
className={styles.clearSearch}
|
||||||
>
|
onClick={() => {
|
||||||
<X size={12} />
|
const next = new URLSearchParams(searchParams);
|
||||||
</button>
|
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'}
|
) : 'Recent Exchanges'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user