Add QA result filtering controls
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-20 00:48:40 +02:00
parent 8cbaafa713
commit 15f0e1f522
7 changed files with 202 additions and 2 deletions
+2
View File
@@ -287,6 +287,8 @@ The QA/QC workspace promotes core metrics into evidence cards before the raw met
The workbench shell clamps page-level horizontal overflow on mobile while keeping the sidebar and workspace shortcut rows intentionally scrollable. Long QA identifiers and inspector values wrap inside their cards instead of widening the viewport.
The QA/QC result list includes client-side search, status and check-type filters plus a latest-results cap so long-lived demo projects remain scan-friendly without changing the API response shape.
## Useful repository scripts
- `bash scripts/frontend_install.sh`
@@ -1,4 +1,4 @@
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import type { DatasetCreateResponse, MetricRead, QualityCheckRead } from '../../types'
const CORE_METRIC_ORDER = [
@@ -42,6 +42,30 @@ function qualityMetricValue(metric: MetricRead | undefined): string {
return Number.isInteger(value) ? String(value) : value.toFixed(3)
}
function qualityMatchesSearch(check: QualityCheckRead, query: string, datasetNameById: Map<string, string>): boolean {
const normalizedQuery = query.trim().toLowerCase()
if (!normalizedQuery) {
return true
}
const candidateName = check.candidate_dataset_id ? datasetNameById.get(check.candidate_dataset_id) : null
const referenceName = datasetNameById.get(check.reference_dataset_id)
const searchable = [
check.id,
check.check_type,
check.status,
check.candidate_dataset_id,
check.reference_dataset_id,
check.analysis_run_id,
check.job_id,
candidateName,
referenceName,
]
.filter(Boolean)
.join(' ')
.toLowerCase()
return searchable.includes(normalizedQuery)
}
export function QualityResultsPanel({
selectedProjectId,
qualityChecks,
@@ -50,6 +74,10 @@ export function QualityResultsPanel({
referenceDatasets,
onRefresh,
}: QualityResultsPanelProps): JSX.Element {
const [showAllQualityChecks, setShowAllQualityChecks] = useState(false)
const [qualityStatusFilter, setQualityStatusFilter] = useState('all')
const [qualityTypeFilter, setQualityTypeFilter] = useState('all')
const [qualitySearchQuery, setQualitySearchQuery] = useState('')
const completedChecks = qualityChecks.filter((check) => check.status === 'ok' || check.status === 'completed').length
const latestCheck = qualityChecks[0] ?? null
const datasetNameById = useMemo(() => {
@@ -61,6 +89,20 @@ export function QualityResultsPanel({
}, [candidateDatasets, referenceDatasets])
const latestCandidateName = latestCheck?.candidate_dataset_id ? datasetNameById.get(latestCheck.candidate_dataset_id) : null
const latestReferenceName = latestCheck?.reference_dataset_id ? datasetNameById.get(latestCheck.reference_dataset_id) : null
const qualityStatuses = useMemo(() => Array.from(new Set(qualityChecks.map((check) => check.status))).sort(), [qualityChecks])
const qualityTypes = useMemo(() => Array.from(new Set(qualityChecks.map((check) => check.check_type))).sort(), [qualityChecks])
const filteredQualityChecks = useMemo(
() =>
qualityChecks.filter((check) => {
const statusMatches = qualityStatusFilter === 'all' || check.status === qualityStatusFilter
const typeMatches = qualityTypeFilter === 'all' || check.check_type === qualityTypeFilter
return statusMatches && typeMatches && qualityMatchesSearch(check, qualitySearchQuery, datasetNameById)
}),
[qualityChecks, qualityStatusFilter, qualityTypeFilter, qualitySearchQuery, datasetNameById],
)
const visibleQualityChecks = showAllQualityChecks ? filteredQualityChecks : filteredQualityChecks.slice(0, 8)
const hiddenQualityCheckCount = Math.max(filteredQualityChecks.length - visibleQualityChecks.length, 0)
const hasActiveQualityFilters = qualityStatusFilter !== 'all' || qualityTypeFilter !== 'all' || qualitySearchQuery.trim().length > 0
return (
<section className="quality-results-panel" data-testid="quality-results-panel">
@@ -122,8 +164,73 @@ export function QualityResultsPanel({
<p>Run dataset, detection or segmentation QA against a reference layer to populate this workspace.</p>
</div>
) : null}
{qualityChecks.length > 0 ? (
<div className="quality-history-controls" aria-label="QA/QC result filters">
<label>
Search QA/QC results
<input
type="search"
placeholder="Type, id or dataset"
value={qualitySearchQuery}
onChange={(event) => setQualitySearchQuery(event.target.value)}
/>
</label>
<label>
Status
<select value={qualityStatusFilter} onChange={(event) => setQualityStatusFilter(event.target.value)}>
<option value="all">All statuses</option>
{qualityStatuses.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</label>
<label>
Check type
<select value={qualityTypeFilter} onChange={(event) => setQualityTypeFilter(event.target.value)}>
<option value="all">All check types</option>
{qualityTypes.map((checkType) => (
<option key={checkType} value={checkType}>
{checkType}
</option>
))}
</select>
</label>
<button
type="button"
className="secondary-action"
onClick={() => {
setQualitySearchQuery('')
setQualityStatusFilter('all')
setQualityTypeFilter('all')
setShowAllQualityChecks(false)
}}
disabled={!hasActiveQualityFilters && !showAllQualityChecks}
>
Reset view
</button>
</div>
) : null}
{filteredQualityChecks.length > 8 ? (
<div className="list-limit-banner">
<span className="quality-list-count">
Showing {visibleQualityChecks.length} of {filteredQualityChecks.length} matching QA/QC results.
{hiddenQualityCheckCount > 0 ? ` ${hiddenQualityCheckCount} older checks hidden.` : ' All matching checks shown.'}
</span>
<button type="button" className="secondary-action" onClick={() => setShowAllQualityChecks((value) => !value)}>
{showAllQualityChecks ? 'Show latest 8' : 'Show all QA/QC results'}
</button>
</div>
) : null}
{qualityChecks.length > 0 && filteredQualityChecks.length === 0 ? (
<div className="empty-state">
<strong>No QA/QC results match the current filters.</strong>
<p>Clear the search, status or check type filter to return to the latest quality results.</p>
</div>
) : null}
<ul className="quality-check-list">
{qualityChecks.map((check) => (
{visibleQualityChecks.map((check) => (
<li className="quality-check-card" key={check.id}>
<div className="quality-check-header">
<div>
+16
View File
@@ -1573,6 +1573,7 @@ button.entity-card {
background: var(--accent-soft);
}
.quality-history-controls,
.export-history-controls {
display: grid;
grid-template-columns: minmax(14rem, 1.2fr) minmax(10rem, 0.9fr) minmax(9rem, 0.8fr) auto;
@@ -1585,15 +1586,29 @@ button.entity-card {
background: #ffffff;
}
.quality-history-controls label,
.export-history-controls label {
margin-top: 0;
}
.quality-history-controls button,
.export-history-controls button {
min-height: 2.35rem;
width: auto;
}
.quality-history-controls {
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
}
.quality-history-controls button {
width: 100%;
}
.quality-list-count {
overflow-wrap: anywhere;
}
.list-limit-banner {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -1951,6 +1966,7 @@ button.entity-card {
.quality-score-row,
.export-action-grid,
.handoff-action-grid,
.quality-history-controls,
.export-history-controls,
.inspector-action-bar,
.list-limit-banner {