Improve dataset selection ergonomics
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 21:45:21 +02:00
parent cbdb990d51
commit b3548bfd52
8 changed files with 197 additions and 3 deletions
+9
View File
@@ -595,3 +595,12 @@ Added:
- Added context cards for selected project, AOI, map feature, latest QA/QC result, latest export and selected detection/segmentation run state. - 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. - 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. - No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced.
## Sprint 53 map/dataset selection ergonomics (2026-06-17)
- Added active selected-state styling to dataset cards.
- Added dataset quick actions for opening the selected dataset in the Map workspace or Exports workspace.
- Added inspector navigation actions for Data, Map, QA/QC, Exports and AI Labs workspaces.
- Preserved existing dataset detail loading, map layer state, export actions and API client behavior.
- Added regression coverage for dataset quick actions and inspector navigation wiring.
- No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced.
@@ -0,0 +1,50 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_dataset_panel_exposes_map_and_export_quick_actions() -> None:
panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetPanel.tsx").read_text(
encoding="utf-8"
)
assert "selectedDatasetId" in panel
assert "dataset-card-active" in panel
assert "onOpenDatasetInMap" in panel
assert "onOpenDatasetExport" in panel
assert "Open in map" in panel
assert "Export / QA" in panel
assert "disabled={!(dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson')}" in panel
def test_app_wires_dataset_quick_actions_to_existing_workspaces() -> None:
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
assert "const openDatasetInMap = (dataset: DatasetCreateResponse) => {" in app
assert "loadDatasetDetails(selectedProjectId, dataset)" in app
assert "setMapLayerVisible(true)" in app
assert "setActiveWorkspace('map')" in app
assert "const openDatasetExport = (dataset: DatasetCreateResponse) => {" in app
assert "setActiveWorkspace('exports')" in app
assert "selectedDatasetId={selectedDatasetId}" in app
assert "onOpenDatasetInMap={openDatasetInMap}" in app
assert "onOpenDatasetExport={openDatasetExport}" in app
def test_inspector_exposes_navigation_actions_without_api_calls() -> None:
inspector = (
ROOT / "frontend" / "src" / "components" / "inspector" / "WorkbenchInspector.tsx"
).read_text(encoding="utf-8")
assert "onOpenDataWorkspace" in inspector
assert "onOpenMapWorkspace" in inspector
assert "onOpenQualityWorkspace" in inspector
assert "onOpenExportsWorkspace" in inspector
assert "onOpenAiWorkspace" in inspector
assert "Data catalog" in inspector
assert "Map layer" in inspector
assert "Open QA/QC" in inspector
assert "Open exports" in inspector
assert "Open AI Labs" in inspector
assert "fetch(" not in inspector
+25
View File
@@ -2147,3 +2147,28 @@ Limitations:
Next recommended pass: Next recommended pass:
- Verify the tabbed inspector live on Tower, then continue with map/dataset selection ergonomics. - Verify the tabbed inspector live on Tower, then continue with map/dataset selection ergonomics.
## Sprint 53 map/dataset selection ergonomics (2026-06-17)
Changed:
- Added active-state styling to dataset cards so the selected dataset is visibly anchored in the catalog.
- Added dataset quick actions to open a dataset directly in the Map workspace or Exports workspace after loading details.
- Added inspector navigation actions to jump to Data, Map, QA/QC, Exports and AI Labs without hunting through the left navigation.
- Kept the existing dataset loading, map layer state, export flow and API clients unchanged.
- Added regression coverage for selection quick actions and inspector navigation wiring.
Tested:
- `cd frontend && npm run typecheck`
- `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_sprint53_selection_ergonomics.py -q`
- `cd frontend && npm run build`
- `bash scripts/run_readiness_check.sh` (`213 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 dataset quick actions with demo data on Tower, then improve populated map/detail readability if needed.
+2 -1
View File
@@ -322,4 +322,5 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Polish Data, Map and AI Labs workspaces. - [x] Polish Data, Map and AI Labs workspaces.
- [x] Polish QA/QC and Exports workspaces. - [x] Polish QA/QC and Exports workspaces.
- [x] 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. - [x] Improve map/dataset selection ergonomics from the workbench canvas and inspector.
- [ ] Improve populated map/detail readability after a demo workflow run.
+21
View File
@@ -385,6 +385,19 @@ function App(): JSX.Element {
const selectedArea = areas.find((area) => area.id === selectedMapAreaId) ?? null const selectedArea = areas.find((area) => area.id === selectedMapAreaId) ?? null
const activeWorkspaceItem = workspaceNavItems.find((item) => item.key === activeWorkspace) ?? workspaceNavItems[0] const activeWorkspaceItem = workspaceNavItems.find((item) => item.key === activeWorkspace) ?? workspaceNavItems[0]
const openDatasetInMap = (dataset: DatasetCreateResponse) => {
if (selectedProjectId) {
loadDatasetDetails(selectedProjectId, dataset)
}
setMapLayerVisible(true)
setActiveWorkspace('map')
}
const openDatasetExport = (dataset: DatasetCreateResponse) => {
if (selectedProjectId) {
loadDatasetDetails(selectedProjectId, dataset)
}
setActiveWorkspace('exports')
}
return ( return (
<div className="app-shell workbench-shell"> <div className="app-shell workbench-shell">
@@ -510,6 +523,7 @@ function App(): JSX.Element {
<DatasetPanel <DatasetPanel
selectedProjectId={selectedProjectId} selectedProjectId={selectedProjectId}
selectedDatasetId={selectedDatasetId}
areas={areas} areas={areas}
datasets={datasets} datasets={datasets}
datasetForm={datasetForm} datasetForm={datasetForm}
@@ -518,6 +532,8 @@ function App(): JSX.Element {
onUploadDataset={uploadDataset} onUploadDataset={uploadDataset}
onLoadDatasetDetails={loadDatasetDetails} onLoadDatasetDetails={loadDatasetDetails}
onRefreshMetadata={refreshMetadata} onRefreshMetadata={refreshMetadata}
onOpenDatasetInMap={openDatasetInMap}
onOpenDatasetExport={openDatasetExport}
/> />
</div> </div>
) : null} ) : null}
@@ -705,6 +721,11 @@ function App(): JSX.Element {
segmentationRuns={segmentationRuns} segmentationRuns={segmentationRuns}
selectedSegmentationRunId={selectedSegmentationRunId} selectedSegmentationRunId={selectedSegmentationRunId}
segmentationItems={segmentationItems} segmentationItems={segmentationItems}
onOpenDataWorkspace={() => setActiveWorkspace('data')}
onOpenMapWorkspace={() => setActiveWorkspace('map')}
onOpenQualityWorkspace={() => setActiveWorkspace('analysis')}
onOpenExportsWorkspace={() => setActiveWorkspace('exports')}
onOpenAiWorkspace={() => setActiveWorkspace('ai')}
datasetDetailProps={{ datasetDetailProps={{
areas, areas,
availableVectorTargets, availableVectorTargets,
@@ -15,6 +15,7 @@ interface DatasetFormState {
interface DatasetPanelProps { interface DatasetPanelProps {
selectedProjectId: string | null selectedProjectId: string | null
selectedDatasetId: string | null
areas: AreaRead[] areas: AreaRead[]
datasets: DatasetCreateResponse[] datasets: DatasetCreateResponse[]
datasetForm: DatasetFormState datasetForm: DatasetFormState
@@ -23,6 +24,8 @@ interface DatasetPanelProps {
onUploadDataset: (event: FormEvent) => void onUploadDataset: (event: FormEvent) => void
onLoadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => void onLoadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => void
onRefreshMetadata: (datasetId: string) => void onRefreshMetadata: (datasetId: string) => void
onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void
onOpenDatasetExport: (dataset: DatasetCreateResponse) => void
} }
function formatBytes(value: number | null | undefined): string { function formatBytes(value: number | null | undefined): string {
@@ -52,6 +55,7 @@ function formatBounds(bounds: Record<string, number> | null | undefined): string
export function DatasetPanel({ export function DatasetPanel({
selectedProjectId, selectedProjectId,
selectedDatasetId,
areas, areas,
datasets, datasets,
datasetForm, datasetForm,
@@ -60,6 +64,8 @@ export function DatasetPanel({
onUploadDataset, onUploadDataset,
onLoadDatasetDetails, onLoadDatasetDetails,
onRefreshMetadata, onRefreshMetadata,
onOpenDatasetInMap,
onOpenDatasetExport,
}: DatasetPanelProps) { }: DatasetPanelProps) {
const readyDatasets = datasets.filter((dataset) => dataset.status === 'ready').length const readyDatasets = datasets.filter((dataset) => dataset.status === 'ready').length
@@ -124,7 +130,7 @@ export function DatasetPanel({
{datasets.length === 0 ? <p>No datasets yet</p> : null} {datasets.length === 0 ? <p>No datasets yet</p> : null}
<ul className="dataset-list"> <ul className="dataset-list">
{datasets.map((dataset) => ( {datasets.map((dataset) => (
<li className="dataset-card" key={dataset.id}> <li className={dataset.id === selectedDatasetId ? 'dataset-card dataset-card-active' : 'dataset-card'} key={dataset.id}>
<div className="dataset-card-header"> <div className="dataset-card-header">
<div> <div>
<strong>{dataset.name}</strong> <strong>{dataset.name}</strong>
@@ -152,6 +158,17 @@ export function DatasetPanel({
> >
Select / details Select / details
</button> </button>
<button className="secondary-action" type="button" onClick={() => onOpenDatasetInMap(dataset)}>
Open in map
</button>
<button
className="secondary-action"
type="button"
onClick={() => onOpenDatasetExport(dataset)}
disabled={!(dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson')}
>
Export / QA
</button>
<button <button
className="secondary-action" className="secondary-action"
type="button" type="button"
@@ -30,6 +30,11 @@ interface WorkbenchInspectorProps {
selectedSegmentationRunId: string selectedSegmentationRunId: string
segmentationItems: SegmentationRead[] segmentationItems: SegmentationRead[]
datasetDetailProps: DatasetDetailPanelProps datasetDetailProps: DatasetDetailPanelProps
onOpenDataWorkspace: () => void
onOpenMapWorkspace: () => void
onOpenQualityWorkspace: () => void
onOpenExportsWorkspace: () => void
onOpenAiWorkspace: () => void
} }
function formatValue(value: string | number | null | undefined): string { function formatValue(value: string | number | null | undefined): string {
@@ -64,6 +69,11 @@ export function WorkbenchInspector({
selectedSegmentationRunId, selectedSegmentationRunId,
segmentationItems, segmentationItems,
datasetDetailProps, datasetDetailProps,
onOpenDataWorkspace,
onOpenMapWorkspace,
onOpenQualityWorkspace,
onOpenExportsWorkspace,
onOpenAiWorkspace,
}: WorkbenchInspectorProps): JSX.Element { }: WorkbenchInspectorProps): JSX.Element {
const [activeTab, setActiveTab] = useState<InspectorTab>('context') const [activeTab, setActiveTab] = useState<InspectorTab>('context')
const selectedDetectionRun = useMemo( const selectedDetectionRun = useMemo(
@@ -117,6 +127,11 @@ export function WorkbenchInspector({
<InspectorField label="Status" value={selectedProject?.status} /> <InspectorField label="Status" value={selectedProject?.status} />
<InspectorField label="AOIs" value={areas.length} /> <InspectorField label="AOIs" value={areas.length} />
<InspectorField label="Datasets" value={datasetsCount} /> <InspectorField label="Datasets" value={datasetsCount} />
<div className="button-row">
<button type="button" className="secondary-action" onClick={onOpenDataWorkspace}>
Open data setup
</button>
</div>
</div> </div>
<div className="inspector-card"> <div className="inspector-card">
<h3>Active AOI</h3> <h3>Active AOI</h3>
@@ -124,6 +139,11 @@ export function WorkbenchInspector({
<InspectorField label="Area m2" value={selectedArea?.area_m2} /> <InspectorField label="Area m2" value={selectedArea?.area_m2} />
<InspectorField label="CRS" value={selectedArea?.original_crs} /> <InspectorField label="CRS" value={selectedArea?.original_crs} />
<InspectorField label="Geometry" value={selectedArea?.geometry?.type} /> <InspectorField label="Geometry" value={selectedArea?.geometry?.type} />
<div className="button-row">
<button type="button" className="secondary-action" onClick={onOpenMapWorkspace}>
Open map
</button>
</div>
</div> </div>
<div className="inspector-card"> <div className="inspector-card">
<h3>Map feature</h3> <h3>Map feature</h3>
@@ -138,6 +158,17 @@ export function WorkbenchInspector({
{activeTab === 'dataset' ? ( {activeTab === 'dataset' ? (
<div className="inspector-tab-panel"> <div className="inspector-tab-panel">
<div className="inspector-action-bar">
<button type="button" className="secondary-action" onClick={onOpenDataWorkspace}>
Data catalog
</button>
<button type="button" className="secondary-action" onClick={onOpenMapWorkspace}>
Map layer
</button>
<button type="button" className="secondary-action" onClick={onOpenExportsWorkspace}>
Export
</button>
</div>
<DatasetDetailPanel {...datasetDetailProps} /> <DatasetDetailPanel {...datasetDetailProps} />
</div> </div>
) : null} ) : null}
@@ -151,6 +182,11 @@ export function WorkbenchInspector({
<InspectorField label="Score" value={latestQualityCheck?.score} /> <InspectorField label="Score" value={latestQualityCheck?.score} />
<InspectorField label="Metrics" value={latestQualityCheck?.metrics.length} /> <InspectorField label="Metrics" value={latestQualityCheck?.metrics.length} />
<InspectorField label="Reference" value={latestQualityCheck?.reference_dataset_id} /> <InspectorField label="Reference" value={latestQualityCheck?.reference_dataset_id} />
<div className="button-row">
<button type="button" className="secondary-action" onClick={onOpenQualityWorkspace}>
Open QA/QC
</button>
</div>
</div> </div>
<div className="inspector-card"> <div className="inspector-card">
<h3>Latest export</h3> <h3>Latest export</h3>
@@ -159,6 +195,11 @@ export function WorkbenchInspector({
<InspectorField label="Status" value={latestPersistedExport?.status ?? latestExport?.status} /> <InspectorField label="Status" value={latestPersistedExport?.status ?? latestExport?.status} />
<InspectorField label="Path" value={latestPersistedExport?.storage_path ?? latestExport?.path} /> <InspectorField label="Path" value={latestPersistedExport?.storage_path ?? latestExport?.path} />
<InspectorField label="Export count" value={exports.length} /> <InspectorField label="Export count" value={exports.length} />
<div className="button-row">
<button type="button" className="secondary-action" onClick={onOpenExportsWorkspace}>
Open exports
</button>
</div>
</div> </div>
</div> </div>
) : null} ) : null}
@@ -171,6 +212,11 @@ export function WorkbenchInspector({
<InspectorField label="Status" value={selectedDetectionRun?.status} /> <InspectorField label="Status" value={selectedDetectionRun?.status} />
<InspectorField label="Model" value={selectedDetectionRun?.model_name} /> <InspectorField label="Model" value={selectedDetectionRun?.model_name} />
<InspectorField label="Detections loaded" value={detectionItems.length} /> <InspectorField label="Detections loaded" value={detectionItems.length} />
<div className="button-row">
<button type="button" className="secondary-action" onClick={onOpenAiWorkspace}>
Open AI Labs
</button>
</div>
</div> </div>
<div className="inspector-card"> <div className="inspector-card">
<h3>Segmentation run</h3> <h3>Segmentation run</h3>
+26 -1
View File
@@ -913,6 +913,11 @@ button.entity-card {
padding: 0.85rem; padding: 0.85rem;
} }
.dataset-card-active {
border-color: rgba(15, 118, 110, 0.5);
box-shadow: inset 4px 0 0 var(--accent);
}
.dataset-card-header, .dataset-card-header,
.model-card, .model-card,
.quality-check-header, .quality-check-header,
@@ -1167,6 +1172,25 @@ button.entity-card {
gap: 0.75rem; gap: 0.75rem;
} }
.inspector-action-bar {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.4rem;
position: sticky;
top: 6.3rem;
z-index: 2;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.45rem;
background: #ffffff;
}
.inspector-action-bar button {
min-height: 2.15rem;
padding: 0.34rem 0.42rem;
font-size: 0.78rem;
}
.inspector-card { .inspector-card {
display: grid; display: grid;
gap: 0.55rem; gap: 0.55rem;
@@ -1286,7 +1310,8 @@ button.entity-card {
.lab-form-grid, .lab-form-grid,
.quality-summary-grid, .quality-summary-grid,
.quality-score-row, .quality-score-row,
.export-action-grid { .export-action-grid,
.inspector-action-bar {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }