From acf95905cace610e069975389d75a70d8f6b9285 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 17 Jun 2026 04:14:35 +0200 Subject: [PATCH] Extract QA and map workbench components --- CHANGELOG.md | 7 + backend/tests/test_sprint19_map_workbench.py | 13 +- .../tests/test_sprint20_area_map_overlay.py | 6 +- .../test_sprint27_frontend_workflow_hooks.py | 9 +- .../test_sprint30_workbench_components.py | 46 ++++++ docs/CODEX_EXECUTION_LOG.md | 24 +++ docs/TODO.md | 3 +- frontend/README.md | 7 + frontend/src/App.tsx | 143 ++++-------------- frontend/src/components/map/MapWorkspace.tsx | 134 ++++++++++++++++ .../quality/QualityResultsPanel.tsx | 45 ++++++ 11 files changed, 312 insertions(+), 125 deletions(-) create mode 100644 backend/tests/test_sprint30_workbench_components.py create mode 100644 frontend/src/components/map/MapWorkspace.tsx create mode 100644 frontend/src/components/quality/QualityResultsPanel.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 32b530f0..d68bdc44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ # 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) - Moved dataset upload/list UI into `DatasetPanel`. diff --git a/backend/tests/test_sprint19_map_workbench.py b/backend/tests/test_sprint19_map_workbench.py index 716ab5a5..54b2b0d1 100644 --- a/backend/tests/test_sprint19_map_workbench.py +++ b/backend/tests/test_sprint19_map_workbench.py @@ -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 -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") + component = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8") assert "mapLayerVisible" in app assert "mapLayerOpacity" in app assert "selectedMapFeature" in app - assert "Layer visible" in app - assert "Layer opacity" in app - assert "Feature inspector" in app - assert "onFeatureSelect={setSelectedMapFeature}" in app + assert " None: root = __import__("pathlib").Path(__file__).resolve().parents[2] app = (root / "frontend" / "src" / "App.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") assert "selectedMapAreaId" in app assert "areaFeatureCollection" in app - assert "areaData={areaFeatureCollection}" in app - assert "Area visible" in app + assert "areaFeatureCollection={areaFeatureCollection}" in app + assert "areaData={areaFeatureCollection}" in map_workspace + assert "Area visible" in map_workspace assert "area-fill" in geomap assert "area-line" in geomap assert "onSelectMapArea" in area_panel diff --git a/backend/tests/test_sprint27_frontend_workflow_hooks.py b/backend/tests/test_sprint27_frontend_workflow_hooks.py index e5e5922c..4b03acf9 100644 --- a/backend/tests/test_sprint27_frontend_workflow_hooks.py +++ b/backend/tests/test_sprint27_frontend_workflow_hooks.py @@ -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: 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 "onClick={() => loadQualityChecks()}" in app + assert " loadQualityChecks()}" in app + assert "Refresh QA/QC results" in quality_panel + assert "onClick={onRefresh}" in quality_panel assert " 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 "QA/QC Results" not in app + assert "

Map workspace

" not in app + assert " 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() diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 7c913a26..abe84f42 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -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) Changed: diff --git a/docs/TODO.md b/docs/TODO.md index 42a8fe5a..d0cc8c0f 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -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] Dataset, raster and vector workflow hook extraction beyond Sprint 10. - [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 diff --git a/frontend/README.md b/frontend/README.md index 9bd2382e..ef1da1c9 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -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. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 810d78c6..49eb4fc4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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} /> -
-

QA/QC Results

- - {qualityChecksError ?

{qualityChecksError}

: null} - {qualityChecks.length === 0 ?

No persisted QA/QC results yet

: null} -
    - {qualityChecks.map((check) => ( -
  • - {check.check_type} -
    status: {check.status}
    -
    score: {check.score ?? 'n/a'}
    -
    candidate: {check.candidate_dataset_id ?? 'n/a'}
    -
    reference: {check.reference_dataset_id}
    -
    quality check: {check.id}
    -
      - {check.metrics.map((metric) => ( -
    • - {metric.metric_key}: {metric.metric_value ?? 'n/a'} -
    • - ))} -
    -
  • - ))} -
-
+ loadQualityChecks()} + /> -
-

Map workspace

-

{mapLayerLabel}

-
- - - - - -
- {areaFeatureCollection ? `${areaFeatureCount} area loaded` : 'No area loaded'} -{' '} - {mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector layer loaded'} -
-
- -
-

Feature inspector

- {selectedMapFeature ? ( - <> -

Geometry: {selectedMapFeature.geometry?.type ?? 'n/a'}

-
{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}
- - ) : ( -

Click a visible map feature to inspect its properties.

- )} -
-
+ ) } diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx new file mode 100644 index 00000000..1ab576f3 --- /dev/null +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -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 ( +
+

Map workspace

+

{mapLayerLabel}

+
+ + + + + +
+ {areaFeatureCollection ? `${areaFeatureCount} area loaded` : 'No area loaded'} -{' '} + {mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector layer loaded'} +
+
+ +
+

Feature inspector

+ {selectedMapFeature ? ( + <> +

Geometry: {selectedMapFeature.geometry?.type ?? 'n/a'}

+
{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}
+ + ) : ( +

Click a visible map feature to inspect its properties.

+ )} +
+
+ ) +} diff --git a/frontend/src/components/quality/QualityResultsPanel.tsx b/frontend/src/components/quality/QualityResultsPanel.tsx new file mode 100644 index 00000000..45230938 --- /dev/null +++ b/frontend/src/components/quality/QualityResultsPanel.tsx @@ -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 ( +
+

QA/QC Results

+ + {qualityChecksError ?

{qualityChecksError}

: null} + {qualityChecks.length === 0 ?

No persisted QA/QC results yet

: null} +
    + {qualityChecks.map((check) => ( +
  • + {check.check_type} +
    status: {check.status}
    +
    score: {check.score ?? 'n/a'}
    +
    candidate: {check.candidate_dataset_id ?? 'n/a'}
    +
    reference: {check.reference_dataset_id}
    +
    quality check: {check.id}
    +
      + {check.metrics.map((metric) => ( +
    • + {metric.metric_key}: {metric.metric_value ?? 'n/a'} +
    • + ))} +
    +
  • + ))} +
+
+ ) +}