Polish QA workspace density
This commit is contained in:
@@ -106,194 +106,216 @@ export function QualityResultsPanel({
|
||||
|
||||
return (
|
||||
<section className="quality-results-panel" data-testid="quality-results-panel">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<h2>QA/QC Results</h2>
|
||||
<p className="muted">Persisted quality checks from dataset, detection and segmentation comparisons.</p>
|
||||
<div className="quality-results-shell">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<h2>QA/QC Results</h2>
|
||||
<p className="muted">Persisted quality checks from dataset, detection and segmentation comparisons.</p>
|
||||
</div>
|
||||
<span className="count-pill">{qualityChecks.length} checks</span>
|
||||
</div>
|
||||
<span className="count-pill">{qualityChecks.length} checks</span>
|
||||
</div>
|
||||
<div className="quality-summary-grid">
|
||||
<div>
|
||||
<span>Completed</span>
|
||||
<strong>{completedChecks}</strong>
|
||||
|
||||
<div className="quality-summary-surface" aria-label="QA/QC result summary">
|
||||
<div className="quality-summary-grid">
|
||||
<div>
|
||||
<span>Completed</span>
|
||||
<strong>{completedChecks}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Latest score</span>
|
||||
<strong>{latestCheck?.score ?? 'n/a'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Latest status</span>
|
||||
<strong>{latestCheck?.status ?? 'waiting'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>Latest score</span>
|
||||
<strong>{latestCheck?.score ?? 'n/a'}</strong>
|
||||
|
||||
<div className="quality-evidence-surface" aria-label="QA/QC dataset evidence">
|
||||
<div className="quality-handoff-grid" aria-label="QA/QC dataset handoff context">
|
||||
<div>
|
||||
<span>Candidate layers</span>
|
||||
<strong>{candidateDatasets.length}</strong>
|
||||
<p className="quality-dataset-name">{candidateDatasets[0]?.name ?? 'No candidate layer selected yet'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Reference layers</span>
|
||||
<strong>{referenceDatasets.length}</strong>
|
||||
<p className="quality-dataset-name">{referenceDatasets[0]?.name ?? 'No reference layer available yet'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Latest comparison</span>
|
||||
<strong>{latestCheck?.status ?? 'waiting'}</strong>
|
||||
<p className="quality-dataset-name">
|
||||
Candidate {latestCandidateName ?? latestCheck?.candidate_dataset_id ?? 'n/a'} / Reference {latestReferenceName ?? latestCheck?.reference_dataset_id ?? 'n/a'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>Latest status</span>
|
||||
<strong>{latestCheck?.status ?? 'waiting'}</strong>
|
||||
|
||||
<div className="quality-control-surface" aria-label="QA/QC refresh and filters">
|
||||
<div className="button-row">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-action"
|
||||
onClick={onRefresh}
|
||||
disabled={!selectedProjectId}
|
||||
data-testid="refresh-quality-results"
|
||||
>
|
||||
Refresh QA/QC results
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="quality-result-state-stack">
|
||||
{qualityChecksError ? (
|
||||
<div className="result-state result-state-error">
|
||||
<strong>QA/QC results could not be loaded.</strong>
|
||||
<p>{qualityChecksError}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{qualityChecks.length === 0 ? (
|
||||
<div className="result-state result-state-empty">
|
||||
<strong>No persisted QA/QC results yet</strong>
|
||||
<p>Run dataset, detection or segmentation QA against a reference layer to populate this workspace.</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{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="result-state result-state-empty">
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
<div className="quality-handoff-grid" aria-label="QA/QC dataset handoff context">
|
||||
<div>
|
||||
<span>Candidate layers</span>
|
||||
<strong>{candidateDatasets.length}</strong>
|
||||
<p className="quality-dataset-name">{candidateDatasets[0]?.name ?? 'No candidate layer selected yet'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Reference layers</span>
|
||||
<strong>{referenceDatasets.length}</strong>
|
||||
<p className="quality-dataset-name">{referenceDatasets[0]?.name ?? 'No reference layer available yet'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Latest comparison</span>
|
||||
<strong>{latestCheck?.status ?? 'waiting'}</strong>
|
||||
<p className="quality-dataset-name">
|
||||
Candidate {latestCandidateName ?? latestCheck?.candidate_dataset_id ?? 'n/a'} / Reference {latestReferenceName ?? latestCheck?.reference_dataset_id ?? 'n/a'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="button-row">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-action"
|
||||
onClick={onRefresh}
|
||||
disabled={!selectedProjectId}
|
||||
data-testid="refresh-quality-results"
|
||||
>
|
||||
Refresh QA/QC results
|
||||
</button>
|
||||
</div>
|
||||
{qualityChecksError ? (
|
||||
<div className="result-state result-state-error">
|
||||
<strong>QA/QC results could not be loaded.</strong>
|
||||
<p>{qualityChecksError}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{qualityChecks.length === 0 ? (
|
||||
<div className="result-state result-state-empty">
|
||||
<strong>No persisted QA/QC results yet</strong>
|
||||
<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="result-state result-state-empty">
|
||||
<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">
|
||||
{visibleQualityChecks.map((check) => (
|
||||
<li className="quality-check-card" key={check.id}>
|
||||
<div className="quality-check-header">
|
||||
<div>
|
||||
<strong>{check.check_type}</strong>
|
||||
<div className="entity-meta">
|
||||
<span className="quality-check-dataset-link">
|
||||
candidate: {check.candidate_dataset_id ? datasetNameById.get(check.candidate_dataset_id) ?? check.candidate_dataset_id : 'n/a'}
|
||||
</span>
|
||||
<span className="quality-check-dataset-link">
|
||||
reference: {datasetNameById.get(check.reference_dataset_id) ?? check.reference_dataset_id}
|
||||
|
||||
<div className="quality-history-surface" aria-label="QA/QC result history">
|
||||
<div className="panel-title-row">
|
||||
<h3>Result history</h3>
|
||||
<span className="count-pill">{visibleQualityChecks.length} shown</span>
|
||||
</div>
|
||||
<ul className="quality-check-list">
|
||||
{visibleQualityChecks.map((check) => (
|
||||
<li className="quality-check-card" key={check.id}>
|
||||
<div className="quality-check-header">
|
||||
<div>
|
||||
<strong>{check.check_type}</strong>
|
||||
<div className="entity-meta">
|
||||
<span className="quality-check-dataset-link">
|
||||
candidate: {check.candidate_dataset_id ? datasetNameById.get(check.candidate_dataset_id) ?? check.candidate_dataset_id : 'n/a'}
|
||||
</span>
|
||||
<span className="quality-check-dataset-link">
|
||||
reference: {datasetNameById.get(check.reference_dataset_id) ?? check.reference_dataset_id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className={check.status === 'ok' || check.status === 'completed' ? 'status-badge status-badge-ready' : 'status-badge'}>
|
||||
{check.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className={check.status === 'ok' || check.status === 'completed' ? 'status-badge status-badge-ready' : 'status-badge'}>
|
||||
{check.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="quality-score-row">
|
||||
<div>
|
||||
<span>Score</span>
|
||||
<strong>{check.score ?? 'n/a'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Metrics</span>
|
||||
<strong>{check.metrics.length}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Quality check</span>
|
||||
<strong>{check.id}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="quality-metric-section">
|
||||
<span>Metric evidence</span>
|
||||
<div className="quality-metric-grid">
|
||||
{CORE_METRIC_ORDER.map((metricKey) => {
|
||||
const metric = check.metrics.find((item) => item.metric_key === metricKey)
|
||||
const isCriticalCount = metricKey === 'false_positive_count' || metricKey === 'false_negative_count'
|
||||
return (
|
||||
<div className={isCriticalCount ? 'quality-metric-card quality-metric-card-critical' : 'quality-metric-card'} key={metricKey}>
|
||||
<span>{qualityMetricLabel(metricKey)}</span>
|
||||
<div className="quality-score-row">
|
||||
<div>
|
||||
<span>Score</span>
|
||||
<strong>{check.score ?? 'n/a'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Metrics</span>
|
||||
<strong>{check.metrics.length}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Quality check</span>
|
||||
<strong>{check.id}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="quality-metric-section">
|
||||
<span>Metric evidence</span>
|
||||
<div className="quality-metric-grid">
|
||||
{CORE_METRIC_ORDER.map((metricKey) => {
|
||||
const metric = check.metrics.find((item) => item.metric_key === metricKey)
|
||||
const isCriticalCount = metricKey === 'false_positive_count' || metricKey === 'false_negative_count'
|
||||
return (
|
||||
<div className={isCriticalCount ? 'quality-metric-card quality-metric-card-critical' : 'quality-metric-card'} key={metricKey}>
|
||||
<span>{qualityMetricLabel(metricKey)}</span>
|
||||
<strong>{qualityMetricValue(metric)}</strong>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<span className="metric-list-label">Raw metrics</span>
|
||||
<ul className="metric-list">
|
||||
{check.metrics.map((metric) => (
|
||||
<li key={metric.id}>
|
||||
<span>{qualityMetricLabel(metric.metric_key)}</span>
|
||||
<strong>{qualityMetricValue(metric)}</strong>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<span className="metric-list-label">Raw metrics</span>
|
||||
<ul className="metric-list">
|
||||
{check.metrics.map((metric) => (
|
||||
<li key={metric.id}>
|
||||
<span>{qualityMetricLabel(metric.metric_key)}</span>
|
||||
<strong>{qualityMetricValue(metric)}</strong>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1420,18 +1420,63 @@ button.entity-card {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.quality-summary-grid {
|
||||
.quality-results-shell {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.quality-summary-surface,
|
||||
.quality-evidence-surface,
|
||||
.quality-control-surface,
|
||||
.quality-history-surface {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 0.72rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.quality-summary-surface {
|
||||
background: linear-gradient(180deg, #ffffff, #f7fbf8);
|
||||
}
|
||||
|
||||
.quality-evidence-surface {
|
||||
background: #f8fbf9;
|
||||
}
|
||||
|
||||
.quality-control-surface,
|
||||
.quality-history-surface {
|
||||
display: grid;
|
||||
gap: 0.68rem;
|
||||
}
|
||||
|
||||
.quality-control-surface .button-row,
|
||||
.quality-control-surface .quality-history-controls,
|
||||
.quality-control-surface .list-limit-banner {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.quality-result-state-stack {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.quality-results-shell .quality-check-list {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quality-results-shell .quality-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
margin: 0.75rem 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quality-handoff-grid {
|
||||
.quality-results-shell .quality-handoff-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
|
||||
gap: 0.65rem;
|
||||
margin: 0 0 0.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quality-summary-grid > div {
|
||||
@@ -1446,7 +1491,7 @@ button.entity-card {
|
||||
border: 1px solid rgba(15, 118, 110, 0.18);
|
||||
border-radius: 8px;
|
||||
padding: 0.72rem;
|
||||
background: #f8fbf9;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.quality-summary-grid span,
|
||||
@@ -2750,6 +2795,27 @@ button.entity-card {
|
||||
padding: 0.58rem;
|
||||
}
|
||||
|
||||
.quality-summary-surface,
|
||||
.quality-evidence-surface,
|
||||
.quality-control-surface,
|
||||
.quality-history-surface {
|
||||
padding: 0.58rem;
|
||||
}
|
||||
|
||||
.quality-results-shell .quality-summary-grid,
|
||||
.quality-results-shell .quality-history-controls {
|
||||
grid-template-columns: repeat(auto-fit, minmax(8.25rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.quality-results-shell .quality-handoff-grid,
|
||||
.quality-results-shell .quality-score-row,
|
||||
.quality-results-shell .quality-metric-grid,
|
||||
.quality-results-shell .metric-list {
|
||||
grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-control-card {
|
||||
padding: 0.52rem 0.58rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user