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
+9
View File
@@ -7,6 +7,15 @@
# Changelog # Changelog
## Sprint 73 QA/QC result filtering (2026-06-20)
- Added client-side QA/QC result search, status and check-type filters.
- Added latest-eight density control with a show-all toggle for long-lived demo projects.
- Added a no-match empty state and reset action for filtered QA/QC result views.
- Kept metric evidence cards and raw persisted metrics unchanged.
- Added static regression coverage for QA/QC filtering and dense history styles.
- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed.
## Sprint 72 Mobile overflow hardening (2026-06-20) ## Sprint 72 Mobile overflow hardening (2026-06-20)
- Clamped page-level horizontal overflow for the workbench shell on mobile. - Clamped page-level horizontal overflow for the workbench shell on mobile.
@@ -0,0 +1,35 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_quality_panel_adds_result_filters_and_density_limit() -> None:
quality_panel = (
ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx"
).read_text(encoding="utf-8")
assert "useState" in quality_panel
assert "qualityStatusFilter" in quality_panel
assert "qualityTypeFilter" in quality_panel
assert "qualitySearchQuery" in quality_panel
assert "qualityMatchesSearch" in quality_panel
assert "filteredQualityChecks = useMemo" in quality_panel
assert "visibleQualityChecks = showAllQualityChecks ? filteredQualityChecks : filteredQualityChecks.slice(0, 8)" in quality_panel
assert "quality-history-controls" in quality_panel
assert "Search QA/QC results" in quality_panel
assert "All statuses" in quality_panel
assert "All check types" in quality_panel
assert "Reset view" in quality_panel
assert "Show all QA/QC results" in quality_panel
assert "No QA/QC results match the current filters." in quality_panel
def test_quality_filter_styles_reuse_dense_history_patterns() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
assert ".quality-history-controls" in css
assert "repeat(auto-fit, minmax(9rem, 1fr))" in css
assert ".quality-history-controls label" in css
assert ".quality-history-controls button" in css
assert ".quality-list-count" in css
+30
View File
@@ -2729,3 +2729,33 @@ Limitations:
Next recommended pass: Next recommended pass:
- Continue with QA/QC result filtering/density for long-lived demo projects, or a broader mobile visual pass across Data and Map once the overflow baseline is stable. - Continue with QA/QC result filtering/density for long-lived demo projects, or a broader mobile visual pass across Data and Map once the overflow baseline is stable.
## Sprint 73 QA/QC result filtering (2026-06-20)
Changed:
- Added client-side QA/QC result search across check id, type, status, candidate/reference dataset ids, analysis/job ids and resolved dataset names.
- Added status and check-type filters derived from the loaded quality-check list.
- Added latest-eight result density control with a show-all toggle.
- Added a no-match empty state and reset action for filtered result views.
- Reused the dense history control styling pattern while allowing the QA/QC controls to wrap inside narrower workspace columns.
- Added `backend/tests/test_sprint73_quality_result_filtering.py`.
- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
Tested:
- Red step: `python -m pytest backend/tests/test_sprint73_quality_result_filtering.py -q` failed on missing filter state, filtered list logic and styles.
- `python -m pytest backend/tests/test_sprint73_quality_result_filtering.py backend/tests/test_sprint72_mobile_overflow_hardening.py backend/tests/test_sprint71_quality_metric_polish.py backend/tests/test_sprint70_quality_handoff_polish.py backend/tests/test_sprint51_quality_export_polish.py -q` (`13 passed`)
- `cd frontend && npm run typecheck`
- `cd frontend && npm run build`
- Local browser check against `http://127.0.0.1:5177` with live API proxy: QA/QC filters rendered against 2 persisted demo checks; search/status conflict showed the no-match state; reset restored 2 checks and 12 metric cards; no console warnings/errors and no body-level overflow.
- Local mobile browser check at a 390px viewport: filter controls stayed within viewport width, with 2 checks and 12 metric cards visible.
- Screenshots captured under `artifacts/sprint73-quality-result-filtering/`.
- `bash scripts/run_readiness_check.sh` (`257 passed`)
Open:
- Redeploy Tower and verify live QA/QC filtering after deployment.
Limitations:
- Frontend filtering/density pass only; filtering is client-side over already-loaded persisted checks and does not change API pagination, persistence, QA calculations, migrations, provider behavior or AI/model behavior.
Next recommended pass:
- Continue with Data/Map mobile visual polish, especially dataset upload/action forms and map toolbar density on narrow screens.
+1
View File
@@ -344,3 +344,4 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add QA/QC handoff polish for candidate/reference context and persisted results. - [x] Add QA/QC handoff polish for candidate/reference context and persisted results.
- [x] Add QA/QC metric card polish for precision, recall, F1, IoU and error counts. - [x] Add QA/QC metric card polish for precision, recall, F1, IoU and error counts.
- [x] Add mobile overflow hardening for workbench navigation, inspector and long QA identifiers. - [x] Add mobile overflow hardening for workbench navigation, inspector and long QA identifiers.
- [x] Add QA/QC result filtering and density controls for long-running demo projects.
+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 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 ## Useful repository scripts
- `bash scripts/frontend_install.sh` - `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' import type { DatasetCreateResponse, MetricRead, QualityCheckRead } from '../../types'
const CORE_METRIC_ORDER = [ const CORE_METRIC_ORDER = [
@@ -42,6 +42,30 @@ function qualityMetricValue(metric: MetricRead | undefined): string {
return Number.isInteger(value) ? String(value) : value.toFixed(3) 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({ export function QualityResultsPanel({
selectedProjectId, selectedProjectId,
qualityChecks, qualityChecks,
@@ -50,6 +74,10 @@ export function QualityResultsPanel({
referenceDatasets, referenceDatasets,
onRefresh, onRefresh,
}: QualityResultsPanelProps): JSX.Element { }: 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 completedChecks = qualityChecks.filter((check) => check.status === 'ok' || check.status === 'completed').length
const latestCheck = qualityChecks[0] ?? null const latestCheck = qualityChecks[0] ?? null
const datasetNameById = useMemo(() => { const datasetNameById = useMemo(() => {
@@ -61,6 +89,20 @@ export function QualityResultsPanel({
}, [candidateDatasets, referenceDatasets]) }, [candidateDatasets, referenceDatasets])
const latestCandidateName = latestCheck?.candidate_dataset_id ? datasetNameById.get(latestCheck.candidate_dataset_id) : null 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 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 ( return (
<section className="quality-results-panel" data-testid="quality-results-panel"> <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> <p>Run dataset, detection or segmentation QA against a reference layer to populate this workspace.</p>
</div> </div>
) : null} ) : 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"> <ul className="quality-check-list">
{qualityChecks.map((check) => ( {visibleQualityChecks.map((check) => (
<li className="quality-check-card" key={check.id}> <li className="quality-check-card" key={check.id}>
<div className="quality-check-header"> <div className="quality-check-header">
<div> <div>
+16
View File
@@ -1573,6 +1573,7 @@ button.entity-card {
background: var(--accent-soft); background: var(--accent-soft);
} }
.quality-history-controls,
.export-history-controls { .export-history-controls {
display: grid; display: grid;
grid-template-columns: minmax(14rem, 1.2fr) minmax(10rem, 0.9fr) minmax(9rem, 0.8fr) auto; 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; background: #ffffff;
} }
.quality-history-controls label,
.export-history-controls label { .export-history-controls label {
margin-top: 0; margin-top: 0;
} }
.quality-history-controls button,
.export-history-controls button { .export-history-controls button {
min-height: 2.35rem; min-height: 2.35rem;
width: auto; 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 { .list-limit-banner {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
@@ -1951,6 +1966,7 @@ button.entity-card {
.quality-score-row, .quality-score-row,
.export-action-grid, .export-action-grid,
.handoff-action-grid, .handoff-action-grid,
.quality-history-controls,
.export-history-controls, .export-history-controls,
.inspector-action-bar, .inspector-action-bar,
.list-limit-banner { .list-limit-banner {