Extract QA and map workbench components
This commit is contained in:
@@ -7,6 +7,13 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Sprint 30 workbench component decomposition (2026-06-17)
|
||||||
|
|
||||||
|
- Moved persisted QA/QC result rendering into `QualityResultsPanel`.
|
||||||
|
- Moved map controls, MapLibre composition and feature inspector rendering into `MapWorkspace`.
|
||||||
|
- Added regression coverage to verify `App.tsx` wires these presentational components without taking QA/map markup back inline.
|
||||||
|
- No API contracts, backend behavior, migrations, product features, provider fetching, AI behavior or UI redesign were introduced.
|
||||||
|
|
||||||
## Sprint 29 dataset component decomposition (2026-06-17)
|
## Sprint 29 dataset component decomposition (2026-06-17)
|
||||||
|
|
||||||
- Moved dataset upload/list UI into `DatasetPanel`.
|
- Moved dataset upload/list UI into `DatasetPanel`.
|
||||||
|
|||||||
@@ -19,13 +19,16 @@ def test_geomap_exposes_v1_layer_controls_and_feature_inspection_contract() -> N
|
|||||||
assert "setPaintProperty('dataset-fill', 'fill-opacity', opacity)" in geomap
|
assert "setPaintProperty('dataset-fill', 'fill-opacity', opacity)" in geomap
|
||||||
|
|
||||||
|
|
||||||
def test_app_wires_map_workbench_controls_and_property_inspector() -> None:
|
def test_app_wires_map_workbench_component() -> None:
|
||||||
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
component = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "mapLayerVisible" in app
|
assert "mapLayerVisible" in app
|
||||||
assert "mapLayerOpacity" in app
|
assert "mapLayerOpacity" in app
|
||||||
assert "selectedMapFeature" in app
|
assert "selectedMapFeature" in app
|
||||||
assert "Layer visible" in app
|
assert "<MapWorkspace" in app
|
||||||
assert "Layer opacity" in app
|
assert "onSelectMapFeature={setSelectedMapFeature}" in app
|
||||||
assert "Feature inspector" in app
|
assert "Layer visible" in component
|
||||||
assert "onFeatureSelect={setSelectedMapFeature}" in app
|
assert "Layer opacity" in component
|
||||||
|
assert "Feature inspector" in component
|
||||||
|
assert "onFeatureSelect={onSelectMapFeature}" in component
|
||||||
|
|||||||
@@ -47,12 +47,14 @@ def test_frontend_wires_selected_area_map_overlay_contract() -> None:
|
|||||||
root = __import__("pathlib").Path(__file__).resolve().parents[2]
|
root = __import__("pathlib").Path(__file__).resolve().parents[2]
|
||||||
app = (root / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
app = (root / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
geomap = (root / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(encoding="utf-8")
|
geomap = (root / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(encoding="utf-8")
|
||||||
|
map_workspace = (root / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
||||||
area_panel = (root / "frontend" / "src" / "components" / "project" / "AreaPanel.tsx").read_text(encoding="utf-8")
|
area_panel = (root / "frontend" / "src" / "components" / "project" / "AreaPanel.tsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "selectedMapAreaId" in app
|
assert "selectedMapAreaId" in app
|
||||||
assert "areaFeatureCollection" in app
|
assert "areaFeatureCollection" in app
|
||||||
assert "areaData={areaFeatureCollection}" in app
|
assert "areaFeatureCollection={areaFeatureCollection}" in app
|
||||||
assert "Area visible" in app
|
assert "areaData={areaFeatureCollection}" in map_workspace
|
||||||
|
assert "Area visible" in map_workspace
|
||||||
assert "area-fill" in geomap
|
assert "area-fill" in geomap
|
||||||
assert "area-line" in geomap
|
assert "area-line" in geomap
|
||||||
assert "onSelectMapArea" in area_panel
|
assert "onSelectMapArea" in area_panel
|
||||||
|
|||||||
@@ -41,9 +41,14 @@ def test_quality_workflow_hook_owns_quality_api_calls() -> None:
|
|||||||
|
|
||||||
def test_app_still_wires_quality_results_and_export_center() -> None:
|
def test_app_still_wires_quality_results_and_export_center() -> None:
|
||||||
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
quality_panel = (
|
||||||
|
ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "Refresh QA/QC results" in app
|
assert "<QualityResultsPanel" in app
|
||||||
assert "onClick={() => loadQualityChecks()}" in app
|
assert "onRefresh={() => loadQualityChecks()}" in app
|
||||||
|
assert "Refresh QA/QC results" in quality_panel
|
||||||
|
assert "onClick={onRefresh}" in quality_panel
|
||||||
assert "<ExportCenter" in app
|
assert "<ExportCenter" in app
|
||||||
assert "onExportDataset={exportSelectedDatasetGeoJson}" in app
|
assert "onExportDataset={exportSelectedDatasetGeoJson}" in app
|
||||||
assert "onExportDetectionRun={exportSelectedDetectionRunGeoJson}" in app
|
assert "onExportDetectionRun={exportSelectedDetectionRunGeoJson}" in app
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_uses_quality_and_map_presentational_components() -> None:
|
||||||
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "from './components/quality/QualityResultsPanel'" in app
|
||||||
|
assert "from './components/map/MapWorkspace'" in app
|
||||||
|
assert "<QualityResultsPanel" in app
|
||||||
|
assert "<MapWorkspace" in app
|
||||||
|
assert "<h2>QA/QC Results</h2>" not in app
|
||||||
|
assert "<h2>Map workspace</h2>" not in app
|
||||||
|
assert "<GeoMap" not in app
|
||||||
|
|
||||||
|
|
||||||
|
def test_quality_results_panel_owns_persisted_quality_check_markup() -> None:
|
||||||
|
quality_panel = (
|
||||||
|
ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "QualityCheckRead" in quality_panel
|
||||||
|
assert "Refresh QA/QC results" in quality_panel
|
||||||
|
assert "No persisted QA/QC results yet" in quality_panel
|
||||||
|
assert "check.metrics.map" in quality_panel
|
||||||
|
assert "fetch(" not in quality_panel
|
||||||
|
assert "api" not in quality_panel.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_workspace_owns_map_controls_and_feature_inspector_markup() -> None:
|
||||||
|
map_workspace = (
|
||||||
|
ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "GeoMap" in map_workspace
|
||||||
|
assert "Selected area" in map_workspace
|
||||||
|
assert "Area visible" in map_workspace
|
||||||
|
assert "Layer visible" in map_workspace
|
||||||
|
assert "Feature inspector" in map_workspace
|
||||||
|
assert "onFeatureSelect={onSelectMapFeature}" in map_workspace
|
||||||
|
assert "fetch(" not in map_workspace
|
||||||
|
assert "api" not in map_workspace.lower()
|
||||||
@@ -1,3 +1,27 @@
|
|||||||
|
## Sprint 30 workbench component decomposition (2026-06-17)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Moved persisted QA/QC result rendering from `frontend/src/App.tsx` into `frontend/src/components/quality/QualityResultsPanel.tsx`.
|
||||||
|
- Moved map layer controls, MapLibre composition and feature inspector rendering into `frontend/src/components/map/MapWorkspace.tsx`.
|
||||||
|
- Updated map/workbench and QA regression tests for the new component boundaries.
|
||||||
|
- Added Sprint 30 component wiring tests to keep QA and map markup out of `App.tsx`.
|
||||||
|
- Updated frontend README, TODO and changelog docs.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `python -m pytest backend/tests/test_sprint30_workbench_components.py backend/tests/test_sprint27_frontend_workflow_hooks.py backend/tests/test_sprint19_map_workbench.py` passed: 9 tests.
|
||||||
|
- `python -m compileall backend/app` passed.
|
||||||
|
- `cd backend && python -m pytest` passed: 173 tests.
|
||||||
|
- `cd frontend && npm run typecheck` passed.
|
||||||
|
- `cd frontend && npm run build` passed.
|
||||||
|
- `bash scripts/run_readiness_check.sh` passed: 173 backend tests, frontend typecheck/build, Alembic head check and script syntax checks.
|
||||||
|
- `cd backend && python -m alembic heads` passed: single head `202606120900`.
|
||||||
|
- `cd backend && python -m alembic upgrade head --sql` passed.
|
||||||
|
- `bash -n scripts/live_migration_smoke.sh` passed.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- No API contracts, backend behavior, migrations, product features, provider fetching, AI behavior or UI redesign changed.
|
||||||
|
- Next maintainability pass should split export preview and remaining shared workbench orchestration into focused components/hooks.
|
||||||
|
|
||||||
## Sprint 29 dataset component decomposition (2026-06-17)
|
## Sprint 29 dataset component decomposition (2026-06-17)
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
|
|||||||
+2
-1
@@ -40,7 +40,8 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Export and QA/QC workflow hook extraction beyond Sprint 10.
|
- [x] Export and QA/QC workflow hook extraction beyond Sprint 10.
|
||||||
- [x] Dataset, raster and vector workflow hook extraction beyond Sprint 10.
|
- [x] Dataset, raster and vector workflow hook extraction beyond Sprint 10.
|
||||||
- [x] Dataset detail, raster controls and vector controls component decomposition.
|
- [x] Dataset detail, raster controls and vector controls component decomposition.
|
||||||
- [ ] Further frontend component decomposition for change detection, QA/QC results and map workspace controls.
|
- [x] QA/QC results and map workspace component decomposition.
|
||||||
|
- [ ] Further frontend component decomposition for export preview and shared workbench orchestration.
|
||||||
|
|
||||||
## Sprint 8 status
|
## Sprint 8 status
|
||||||
|
|
||||||
|
|||||||
@@ -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`.
|
- 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.
|
- `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
|
## Release hardening updates
|
||||||
|
|
||||||
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
||||||
|
|||||||
+28
-115
@@ -1,6 +1,5 @@
|
|||||||
import { FormEvent, useEffect, useMemo, useState } from 'react'
|
import { FormEvent, useEffect, useMemo, useState } from 'react'
|
||||||
import './styles/app.css'
|
import './styles/app.css'
|
||||||
import GeoMap from './components/GeoMap'
|
|
||||||
import { areasApi } from './services/api/areas'
|
import { areasApi } from './services/api/areas'
|
||||||
import { datasetsApi } from './services/api/datasets'
|
import { datasetsApi } from './services/api/datasets'
|
||||||
import { projectsApi } from './services/api/projects'
|
import { projectsApi } from './services/api/projects'
|
||||||
@@ -10,8 +9,10 @@ import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel'
|
|||||||
import { DatasetPanel } from './components/datasets/DatasetPanel'
|
import { DatasetPanel } from './components/datasets/DatasetPanel'
|
||||||
import { DetectionLab } from './components/detection/DetectionLab'
|
import { DetectionLab } from './components/detection/DetectionLab'
|
||||||
import { ExportCenter } from './components/exports/ExportCenter'
|
import { ExportCenter } from './components/exports/ExportCenter'
|
||||||
|
import { MapWorkspace } from './components/map/MapWorkspace'
|
||||||
import { AreaPanel } from './components/project/AreaPanel'
|
import { AreaPanel } from './components/project/AreaPanel'
|
||||||
import { ProjectPanel } from './components/project/ProjectPanel'
|
import { ProjectPanel } from './components/project/ProjectPanel'
|
||||||
|
import { QualityResultsPanel } from './components/quality/QualityResultsPanel'
|
||||||
import { WorkbenchStatusStrip } from './components/WorkbenchStatusStrip'
|
import { WorkbenchStatusStrip } from './components/WorkbenchStatusStrip'
|
||||||
import type {
|
import type {
|
||||||
ApiError,
|
ApiError,
|
||||||
@@ -713,33 +714,12 @@ function App(): JSX.Element {
|
|||||||
onRunQa={runSegmentationQa}
|
onRunQa={runSegmentationQa}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section>
|
<QualityResultsPanel
|
||||||
<h2>QA/QC Results</h2>
|
selectedProjectId={selectedProjectId}
|
||||||
<button type="button" onClick={() => loadQualityChecks()} disabled={!selectedProjectId}>
|
qualityChecks={qualityChecks}
|
||||||
Refresh QA/QC results
|
qualityChecksError={qualityChecksError}
|
||||||
</button>
|
onRefresh={() => loadQualityChecks()}
|
||||||
{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>
|
|
||||||
|
|
||||||
<ExportCenter
|
<ExportCenter
|
||||||
selectedProjectId={selectedProjectId}
|
selectedProjectId={selectedProjectId}
|
||||||
@@ -836,93 +816,26 @@ function App(): JSX.Element {
|
|||||||
onPickDerivedDataset={pickDerivedDataset}
|
onPickDerivedDataset={pickDerivedDataset}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section>
|
<MapWorkspace
|
||||||
<h2>Map workspace</h2>
|
areas={areas}
|
||||||
<p>{mapLayerLabel}</p>
|
selectedMapAreaId={selectedMapAreaId}
|
||||||
<div className="map-controls">
|
areaFeatureCollection={areaFeatureCollection}
|
||||||
<label>
|
mapFeatureCollection={mapFeatureCollection}
|
||||||
Selected area
|
mapLayerLabel={mapLayerLabel}
|
||||||
<select
|
mapLayerVisible={mapLayerVisible}
|
||||||
value={selectedMapAreaId}
|
mapLayerOpacity={mapLayerOpacity}
|
||||||
onChange={(event) => setSelectedMapAreaId(event.target.value)}
|
areaLayerVisible={areaLayerVisible}
|
||||||
disabled={areas.length === 0}
|
areaLayerOpacity={areaLayerOpacity}
|
||||||
>
|
mapFeatureCount={mapFeatureCount}
|
||||||
<option value="">No area</option>
|
areaFeatureCount={areaFeatureCount}
|
||||||
{areas.map((area) => (
|
selectedMapFeature={selectedMapFeature}
|
||||||
<option key={area.id} value={area.id}>
|
onSelectMapArea={setSelectedMapAreaId}
|
||||||
{area.name}
|
onSetAreaLayerVisible={setAreaLayerVisible}
|
||||||
</option>
|
onSetAreaLayerOpacity={setAreaLayerOpacity}
|
||||||
))}
|
onSetMapLayerVisible={setMapLayerVisible}
|
||||||
</select>
|
onSetMapLayerOpacity={setMapLayerOpacity}
|
||||||
</label>
|
onSelectMapFeature={setSelectedMapFeature}
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user