diff --git a/CHANGELOG.md b/CHANGELOG.md index 7764bd16..056466e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -587,3 +587,11 @@ Added: - Preserved existing API clients, callbacks and export/QA behavior. - Added regression coverage for the QA/QC and Exports workspace structure. - No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced. + +## Sprint 52 selected context inspector tabs (2026-06-17) + +- Replaced the fixed dataset-only right inspector with tabbed Context, Dataset, QA/Exports and AI Runs inspection. +- Reused the existing dataset detail component for raster/vector operations so dataset behavior and callbacks remain unchanged. +- Added context cards for selected project, AOI, map feature, latest QA/QC result, latest export and selected detection/segmentation run state. +- Added regression coverage for inspector wiring and tab structure. +- No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced. diff --git a/backend/tests/test_sprint28_dataset_workflow_hook.py b/backend/tests/test_sprint28_dataset_workflow_hook.py index 0823f3da..58104bd6 100644 --- a/backend/tests/test_sprint28_dataset_workflow_hook.py +++ b/backend/tests/test_sprint28_dataset_workflow_hook.py @@ -52,19 +52,20 @@ def test_app_still_wires_dataset_ui_callbacks() -> None: assert "onUploadDataset={uploadDataset}" in app assert "onLoadDatasetDetails={loadDatasetDetails}" in app assert "onRefreshMetadata={refreshMetadata}" in app - assert " runVectorIntersect(availableVectorTargets)}" in app + assert " runVectorIntersect(availableVectorTargets)" in app assert '
onLoadDatasetDetails(selectedProjectId ?? '', dataset)}" in dataset_panel assert "onClick={() => onRefreshMetadata(dataset.id)}" in dataset_panel diff --git a/backend/tests/test_sprint29_dataset_components.py b/backend/tests/test_sprint29_dataset_components.py index a1e90dfb..c2f0cce4 100644 --- a/backend/tests/test_sprint29_dataset_components.py +++ b/backend/tests/test_sprint29_dataset_components.py @@ -8,11 +8,16 @@ ROOT = Path(__file__).resolve().parents[2] def test_app_uses_dataset_presentational_components() -> None: app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + inspector = ( + ROOT / "frontend" / "src" / "components" / "inspector" / "WorkbenchInspector.tsx" + ).read_text(encoding="utf-8") assert "from './components/datasets/DatasetPanel'" in app - assert "from './components/datasets/DatasetDetailPanel'" in app + assert "from './components/inspector/WorkbenchInspector'" in app assert "" in inspector assert "Raster operations" not in app assert "

Vector operations

" not in app diff --git a/backend/tests/test_sprint52_workbench_inspector_tabs.py b/backend/tests/test_sprint52_workbench_inspector_tabs.py new file mode 100644 index 00000000..e31dbae9 --- /dev/null +++ b/backend/tests/test_sprint52_workbench_inspector_tabs.py @@ -0,0 +1,45 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_app_wires_tabbed_workbench_inspector() -> None: + app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + + assert "from './components/inspector/WorkbenchInspector'" in app + assert " None: + inspector = ( + ROOT / "frontend" / "src" / "components" / "inspector" / "WorkbenchInspector.tsx" + ).read_text(encoding="utf-8") + + assert "type InspectorTab = 'context' | 'dataset' | 'quality' | 'ai'" in inspector + assert 'data-testid="workbench-inspector-panel"' in inspector + assert "data-testid={`inspector-tab-${tab.key}`}" in inspector + assert "" in inspector + assert "Latest QA/QC" in inspector + assert "Latest export" in inspector + assert "Detection run" in inspector + assert "Segmentation run" in inspector + assert "fetch(" not in inspector + + +def test_dataset_detail_props_remain_exported_for_inspector_reuse() -> None: + dataset_panel = ( + ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetDetailPanel.tsx" + ).read_text(encoding="utf-8") + + assert "export interface DatasetDetailPanelProps" in dataset_panel + assert 'className="dataset-detail-panel"' in dataset_panel + assert "onRunRasterInspect" in dataset_panel + assert "onRunVectorIntersect" in dataset_panel diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index b5a04f6c..a8b24215 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -2118,3 +2118,29 @@ Limitations: Next recommended pass: - Verify QA/QC and Exports on the deployed Tower workbench, then continue with selected-object inspector detail tabs. + +## Sprint 52 selected context inspector tabs (2026-06-17) + +Changed: +- Replaced the dataset-only right inspector with a tabbed `WorkbenchInspector`. +- Added Context, Dataset, QA/Exports and AI Runs tabs using existing project, AOI, dataset, QA, export and AI-run state. +- Kept the existing `DatasetDetailPanel` as the Dataset tab so raster/vector operations and callbacks remain behavior-compatible. +- Added inspector cards for selected map feature properties, latest QA/QC, latest export and selected detection/segmentation run summaries. +- Added regression coverage for inspector wiring and tab structure. + +Tested: +- `cd frontend && npm run typecheck` +- `cd backend && python -m pytest tests/test_sprint49_workbench_shell_refactor.py tests/test_sprint50_workspace_usability_polish.py tests/test_sprint51_quality_export_polish.py -q` +- `cd backend && python -m pytest tests/test_sprint28_dataset_workflow_hook.py tests/test_sprint29_dataset_components.py tests/test_sprint52_workbench_inspector_tabs.py -q` +- `cd backend && python -m pytest tests/test_sprint52_workbench_inspector_tabs.py -q` +- `cd frontend && npm run build` +- `bash scripts/run_readiness_check.sh` (`210 passed`) + +Open: +- Commit, push, deploy to Tower and verify browser-facing runtime. + +Limitations: +- This pass remains UI orchestration only. It does not add features, change API contracts, alter migrations, fetch live providers or enable new AI models. + +Next recommended pass: +- Verify the tabbed inspector live on Tower, then continue with map/dataset selection ergonomics. diff --git a/docs/TODO.md b/docs/TODO.md index 7e97d780..293bc3e1 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -321,4 +321,5 @@ This file now starts with the current implementation status. Older preparation/b - [x] Deploy the shell refactor to Tower and run live browser smoke on port 1202. - [x] Polish Data, Map and AI Labs workspaces. - [x] Polish QA/QC and Exports workspaces. -- [ ] Add selected-object inspector detail tabs for project, AOI, dataset, QA check, export and AI run context. +- [x] Add selected-object inspector detail tabs for project, AOI, dataset, QA check, export and AI run context. +- [ ] Improve map/dataset selection ergonomics from the workbench canvas and inspector. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 23170316..dafd9f91 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,11 @@ import { useMemo, useState } from 'react' import './styles/app.css' import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel' -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 { ExportPreview } from './components/exports/ExportPreview' +import { WorkbenchInspector } from './components/inspector/WorkbenchInspector' import { MapWorkspace } from './components/map/MapWorkspace' import { AreaPanel } from './components/project/AreaPanel' import { ProjectPanel } from './components/project/ProjectPanel' @@ -690,60 +690,76 @@ function App(): JSX.Element { diff --git a/frontend/src/components/datasets/DatasetDetailPanel.tsx b/frontend/src/components/datasets/DatasetDetailPanel.tsx index a172d8df..835bcc1b 100644 --- a/frontend/src/components/datasets/DatasetDetailPanel.tsx +++ b/frontend/src/components/datasets/DatasetDetailPanel.tsx @@ -10,7 +10,7 @@ import type { import { RasterControls } from './RasterControls' import { VectorControls } from './VectorControls' -interface DatasetDetailPanelProps { +export interface DatasetDetailPanelProps { areas: AreaRead[] availableVectorTargets: DatasetCreateResponse[] selectedDatasetId: string | null @@ -147,7 +147,7 @@ export function DatasetDetailPanel({ onPickDerivedDataset, }: DatasetDetailPanelProps) { return ( -
+

Dataset details

{selectedDatasetId ?

Selected dataset: {selectedDatasetId}

:

No dataset selected

} {selectedDataset ? ( diff --git a/frontend/src/components/inspector/WorkbenchInspector.tsx b/frontend/src/components/inspector/WorkbenchInspector.tsx new file mode 100644 index 00000000..19681661 --- /dev/null +++ b/frontend/src/components/inspector/WorkbenchInspector.tsx @@ -0,0 +1,186 @@ +import { useMemo, useState } from 'react' +import type { + AreaRead, + DetectionRead, + DetectionRunRead, + ExportCreateResponse, + ExportRead, + ProjectRead, + QualityCheckRead, + SegmentationRead, + SegmentationRunRead, +} from '../../types' +import { DatasetDetailPanel, type DatasetDetailPanelProps } from '../datasets/DatasetDetailPanel' + +type InspectorTab = 'context' | 'dataset' | 'quality' | 'ai' + +interface WorkbenchInspectorProps { + selectedProject: ProjectRead | null + selectedArea: AreaRead | null + selectedMapFeature: GeoJSON.Feature | null + areas: AreaRead[] + datasetsCount: number + qualityChecks: QualityCheckRead[] + exports: ExportRead[] + latestExport: ExportCreateResponse | null + detectionRuns: DetectionRunRead[] + selectedDetectionRunId: string + detectionItems: DetectionRead[] + segmentationRuns: SegmentationRunRead[] + selectedSegmentationRunId: string + segmentationItems: SegmentationRead[] + datasetDetailProps: DatasetDetailPanelProps +} + +function formatValue(value: string | number | null | undefined): string { + if (value === null || value === undefined || value === '') { + return 'n/a' + } + return String(value) +} + +function InspectorField({ label, value }: { label: string; value: string | number | null | undefined }): JSX.Element { + return ( +
+ {label} + {formatValue(value)} +
+ ) +} + +export function WorkbenchInspector({ + selectedProject, + selectedArea, + selectedMapFeature, + areas, + datasetsCount, + qualityChecks, + exports, + latestExport, + detectionRuns, + selectedDetectionRunId, + detectionItems, + segmentationRuns, + selectedSegmentationRunId, + segmentationItems, + datasetDetailProps, +}: WorkbenchInspectorProps): JSX.Element { + const [activeTab, setActiveTab] = useState('context') + const selectedDetectionRun = useMemo( + () => detectionRuns.find((run) => run.id === selectedDetectionRunId) ?? null, + [detectionRuns, selectedDetectionRunId], + ) + const selectedSegmentationRun = useMemo( + () => segmentationRuns.find((run) => run.id === selectedSegmentationRunId) ?? null, + [segmentationRuns, selectedSegmentationRunId], + ) + const latestQualityCheck = qualityChecks[0] ?? null + const latestPersistedExport = exports[0] ?? null + + const tabs: Array<{ key: InspectorTab; label: string }> = [ + { key: 'context', label: 'Context' }, + { key: 'dataset', label: 'Dataset' }, + { key: 'quality', label: 'QA/Exports' }, + { key: 'ai', label: 'AI Runs' }, + ] + + return ( +
+
+
+

Inspector

+

Selection details

+
+
+
+ {tabs.map((tab) => ( + + ))} +
+ + {activeTab === 'context' ? ( +
+
+

Project context

+ + + + + +
+
+

Active AOI

+ + + + +
+
+

Map feature

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

No map feature selected.

+ )} +
+
+ ) : null} + + {activeTab === 'dataset' ? ( +
+ +
+ ) : null} + + {activeTab === 'quality' ? ( +
+
+

Latest QA/QC

+ + + + + +
+
+

Latest export

+ + + + + +
+
+ ) : null} + + {activeTab === 'ai' ? ( +
+
+

Detection run

+ + + + +
+
+

Segmentation run

+ + + + +
+
+ ) : null} +
+ ) +} diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index a1413f48..5f63f37e 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -764,7 +764,7 @@ section th { max-height: 16rem; } -.workbench-inspector h2 { +.inspector-header h2 { position: sticky; top: 0; z-index: 2; @@ -1121,6 +1121,106 @@ button.entity-card { grid-template-columns: 1fr; } +.workbench-inspector-panel { + display: grid; + gap: 0.8rem; +} + +.inspector-header { + display: grid; + gap: 0.2rem; +} + +.inspector-header h2 { + margin-bottom: 0; +} + +.inspector-tabs { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.35rem; + position: sticky; + top: 3.4rem; + z-index: 3; + border-bottom: 1px solid var(--line); + padding-bottom: 0.65rem; + background: #f8fbf9; +} + +.inspector-tab { + min-height: 2.2rem; + padding: 0.38rem 0.42rem; + background: #ffffff; + box-shadow: none; + font-size: 0.78rem; + line-height: 1.2; +} + +.inspector-tab-active { + border-color: rgba(15, 118, 110, 0.46); + background: var(--accent-soft); + color: var(--accent-strong); +} + +.inspector-tab-panel { + display: grid; + gap: 0.75rem; +} + +.inspector-card { + display: grid; + gap: 0.55rem; + border: 1px solid var(--line); + border-radius: 8px; + padding: 0.8rem; + background: #ffffff; +} + +.inspector-card h3 { + margin-top: 0; +} + +.inspector-field { + display: grid; + gap: 0.16rem; + min-width: 0; + border-top: 1px solid var(--line); + padding-top: 0.5rem; +} + +.inspector-field:first-of-type { + border-top: 0; + padding-top: 0; +} + +.inspector-field span { + color: var(--muted); + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.inspector-field strong { + overflow: hidden; + font-size: 0.88rem; + line-height: 1.28; + text-overflow: ellipsis; +} + +.workbench-inspector-panel .dataset-detail-panel { + border: 0; + padding: 0; + background: transparent; + box-shadow: none; +} + +.dataset-detail-panel > div, +.dataset-detail-panel ul { + display: grid; + gap: 0.55rem; +} + @media (max-width: 1120px) { .workbench-layout { grid-template-columns: 12rem minmax(0, 1fr);