Extract QA and map workbench components
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-17 04:14:35 +02:00
parent 016926e256
commit acf95905ca
11 changed files with 312 additions and 125 deletions
+7
View File
@@ -197,6 +197,13 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow.
- Raster controls and vector controls now live in `src/components/datasets/RasterControls.tsx` and `src/components/datasets/VectorControls.tsx`.
- `App.tsx` still owns cross-module orchestration and passes the same `useDatasetWorkflow` state/actions into these presentational components.
## Sprint 30 maintainability updates
- Persisted QA/QC result rendering moved into `src/components/quality/QualityResultsPanel.tsx`.
- Map layer controls, MapLibre rendering and the feature inspector are now composed by `src/components/map/MapWorkspace.tsx`.
- `App.tsx` still owns selected project, selected area, active layer and inspector state; the extracted components receive the same state and callbacks as props.
- No map behavior, QA/QC API behavior, backend behavior, migrations or product features changed.
## Release hardening updates
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
+28 -115
View File
@@ -1,6 +1,5 @@
import { FormEvent, useEffect, useMemo, useState } from 'react'
import './styles/app.css'
import GeoMap from './components/GeoMap'
import { areasApi } from './services/api/areas'
import { datasetsApi } from './services/api/datasets'
import { projectsApi } from './services/api/projects'
@@ -10,8 +9,10 @@ import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel'
import { DatasetPanel } from './components/datasets/DatasetPanel'
import { DetectionLab } from './components/detection/DetectionLab'
import { ExportCenter } from './components/exports/ExportCenter'
import { MapWorkspace } from './components/map/MapWorkspace'
import { AreaPanel } from './components/project/AreaPanel'
import { ProjectPanel } from './components/project/ProjectPanel'
import { QualityResultsPanel } from './components/quality/QualityResultsPanel'
import { WorkbenchStatusStrip } from './components/WorkbenchStatusStrip'
import type {
ApiError,
@@ -713,33 +714,12 @@ function App(): JSX.Element {
onRunQa={runSegmentationQa}
/>
<section>
<h2>QA/QC Results</h2>
<button type="button" onClick={() => loadQualityChecks()} disabled={!selectedProjectId}>
Refresh QA/QC results
</button>
{qualityChecksError ? <p className="error">{qualityChecksError}</p> : null}
{qualityChecks.length === 0 ? <p>No persisted QA/QC results yet</p> : null}
<ul>
{qualityChecks.map((check) => (
<li key={check.id}>
<strong>{check.check_type}</strong>
<div>status: {check.status}</div>
<div>score: {check.score ?? 'n/a'}</div>
<div>candidate: {check.candidate_dataset_id ?? 'n/a'}</div>
<div>reference: {check.reference_dataset_id}</div>
<div>quality check: {check.id}</div>
<ul>
{check.metrics.map((metric) => (
<li key={metric.id}>
{metric.metric_key}: {metric.metric_value ?? 'n/a'}
</li>
))}
</ul>
</li>
))}
</ul>
</section>
<QualityResultsPanel
selectedProjectId={selectedProjectId}
qualityChecks={qualityChecks}
qualityChecksError={qualityChecksError}
onRefresh={() => loadQualityChecks()}
/>
<ExportCenter
selectedProjectId={selectedProjectId}
@@ -836,93 +816,26 @@ function App(): JSX.Element {
onPickDerivedDataset={pickDerivedDataset}
/>
<section>
<h2>Map workspace</h2>
<p>{mapLayerLabel}</p>
<div className="map-controls">
<label>
Selected area
<select
value={selectedMapAreaId}
onChange={(event) => setSelectedMapAreaId(event.target.value)}
disabled={areas.length === 0}
>
<option value="">No area</option>
{areas.map((area) => (
<option key={area.id} value={area.id}>
{area.name}
</option>
))}
</select>
</label>
<label className="checkbox-row">
<input
checked={areaLayerVisible}
disabled={!areaFeatureCollection}
type="checkbox"
onChange={(event) => setAreaLayerVisible(event.target.checked)}
/>
Area visible
</label>
<label>
Area opacity
<input
disabled={!areaFeatureCollection}
max="0.7"
min="0.05"
step="0.05"
type="range"
value={areaLayerOpacity}
onChange={(event) => setAreaLayerOpacity(Number(event.target.value))}
/>
</label>
<label className="checkbox-row">
<input
checked={mapLayerVisible}
disabled={!mapFeatureCollection}
type="checkbox"
onChange={(event) => setMapLayerVisible(event.target.checked)}
/>
Layer visible
</label>
<label>
Layer opacity
<input
disabled={!mapFeatureCollection}
max="1"
min="0.05"
step="0.05"
type="range"
value={mapLayerOpacity}
onChange={(event) => setMapLayerOpacity(Number(event.target.value))}
/>
</label>
<div className="map-status">
{areaFeatureCollection ? `${areaFeatureCount} area loaded` : 'No area loaded'} -{' '}
{mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector layer loaded'}
</div>
</div>
<GeoMap
data={mapFeatureCollection}
areaData={areaFeatureCollection}
visible={mapLayerVisible}
opacity={mapLayerOpacity}
areaVisible={areaLayerVisible}
areaOpacity={areaLayerOpacity}
onFeatureSelect={setSelectedMapFeature}
/>
<div className="feature-inspector">
<h3>Feature inspector</h3>
{selectedMapFeature ? (
<>
<p>Geometry: {selectedMapFeature.geometry?.type ?? 'n/a'}</p>
<pre className="job-result">{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}</pre>
</>
) : (
<p className="muted">Click a visible map feature to inspect its properties.</p>
)}
</div>
</section>
<MapWorkspace
areas={areas}
selectedMapAreaId={selectedMapAreaId}
areaFeatureCollection={areaFeatureCollection}
mapFeatureCollection={mapFeatureCollection}
mapLayerLabel={mapLayerLabel}
mapLayerVisible={mapLayerVisible}
mapLayerOpacity={mapLayerOpacity}
areaLayerVisible={areaLayerVisible}
areaLayerOpacity={areaLayerOpacity}
mapFeatureCount={mapFeatureCount}
areaFeatureCount={areaFeatureCount}
selectedMapFeature={selectedMapFeature}
onSelectMapArea={setSelectedMapAreaId}
onSetAreaLayerVisible={setAreaLayerVisible}
onSetAreaLayerOpacity={setAreaLayerOpacity}
onSetMapLayerVisible={setMapLayerVisible}
onSetMapLayerOpacity={setMapLayerOpacity}
onSelectMapFeature={setSelectedMapFeature}
/>
</div>
)
}
@@ -0,0 +1,134 @@
import GeoMap from '../GeoMap'
import type { AreaRead } from '../../types'
interface MapWorkspaceProps {
areas: AreaRead[]
selectedMapAreaId: string
areaFeatureCollection: GeoJSON.FeatureCollection | null
mapFeatureCollection: GeoJSON.FeatureCollection | null
mapLayerLabel: string
mapLayerVisible: boolean
mapLayerOpacity: number
areaLayerVisible: boolean
areaLayerOpacity: number
mapFeatureCount: number
areaFeatureCount: number
selectedMapFeature: GeoJSON.Feature | null
onSelectMapArea: (areaId: string) => void
onSetAreaLayerVisible: (visible: boolean) => void
onSetAreaLayerOpacity: (opacity: number) => void
onSetMapLayerVisible: (visible: boolean) => void
onSetMapLayerOpacity: (opacity: number) => void
onSelectMapFeature: (feature: GeoJSON.Feature | null) => void
}
export function MapWorkspace({
areas,
selectedMapAreaId,
areaFeatureCollection,
mapFeatureCollection,
mapLayerLabel,
mapLayerVisible,
mapLayerOpacity,
areaLayerVisible,
areaLayerOpacity,
mapFeatureCount,
areaFeatureCount,
selectedMapFeature,
onSelectMapArea,
onSetAreaLayerVisible,
onSetAreaLayerOpacity,
onSetMapLayerVisible,
onSetMapLayerOpacity,
onSelectMapFeature,
}: MapWorkspaceProps): JSX.Element {
return (
<section>
<h2>Map workspace</h2>
<p>{mapLayerLabel}</p>
<div className="map-controls">
<label>
Selected area
<select
value={selectedMapAreaId}
onChange={(event) => onSelectMapArea(event.target.value)}
disabled={areas.length === 0}
>
<option value="">No area</option>
{areas.map((area) => (
<option key={area.id} value={area.id}>
{area.name}
</option>
))}
</select>
</label>
<label className="checkbox-row">
<input
checked={areaLayerVisible}
disabled={!areaFeatureCollection}
type="checkbox"
onChange={(event) => onSetAreaLayerVisible(event.target.checked)}
/>
Area visible
</label>
<label>
Area opacity
<input
disabled={!areaFeatureCollection}
max="0.7"
min="0.05"
step="0.05"
type="range"
value={areaLayerOpacity}
onChange={(event) => onSetAreaLayerOpacity(Number(event.target.value))}
/>
</label>
<label className="checkbox-row">
<input
checked={mapLayerVisible}
disabled={!mapFeatureCollection}
type="checkbox"
onChange={(event) => onSetMapLayerVisible(event.target.checked)}
/>
Layer visible
</label>
<label>
Layer opacity
<input
disabled={!mapFeatureCollection}
max="1"
min="0.05"
step="0.05"
type="range"
value={mapLayerOpacity}
onChange={(event) => onSetMapLayerOpacity(Number(event.target.value))}
/>
</label>
<div className="map-status">
{areaFeatureCollection ? `${areaFeatureCount} area loaded` : 'No area loaded'} -{' '}
{mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector layer loaded'}
</div>
</div>
<GeoMap
data={mapFeatureCollection}
areaData={areaFeatureCollection}
visible={mapLayerVisible}
opacity={mapLayerOpacity}
areaVisible={areaLayerVisible}
areaOpacity={areaLayerOpacity}
onFeatureSelect={onSelectMapFeature}
/>
<div className="feature-inspector">
<h3>Feature inspector</h3>
{selectedMapFeature ? (
<>
<p>Geometry: {selectedMapFeature.geometry?.type ?? 'n/a'}</p>
<pre className="job-result">{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}</pre>
</>
) : (
<p className="muted">Click a visible map feature to inspect its properties.</p>
)}
</div>
</section>
)
}
@@ -0,0 +1,45 @@
import type { QualityCheckRead } from '../../types'
interface QualityResultsPanelProps {
selectedProjectId: string | null
qualityChecks: QualityCheckRead[]
qualityChecksError: string | null
onRefresh: () => void
}
export function QualityResultsPanel({
selectedProjectId,
qualityChecks,
qualityChecksError,
onRefresh,
}: QualityResultsPanelProps): JSX.Element {
return (
<section>
<h2>QA/QC Results</h2>
<button type="button" onClick={onRefresh} disabled={!selectedProjectId}>
Refresh QA/QC results
</button>
{qualityChecksError ? <p className="error">{qualityChecksError}</p> : null}
{qualityChecks.length === 0 ? <p>No persisted QA/QC results yet</p> : null}
<ul>
{qualityChecks.map((check) => (
<li key={check.id}>
<strong>{check.check_type}</strong>
<div>status: {check.status}</div>
<div>score: {check.score ?? 'n/a'}</div>
<div>candidate: {check.candidate_dataset_id ?? 'n/a'}</div>
<div>reference: {check.reference_dataset_id}</div>
<div>quality check: {check.id}</div>
<ul>
{check.metrics.map((metric) => (
<li key={metric.id}>
{metric.metric_key}: {metric.metric_value ?? 'n/a'}
</li>
))}
</ul>
</li>
))}
</ul>
</section>
)
}