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
@@ -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 {