Add tabbed workbench inspector
This commit is contained in:
@@ -587,3 +587,11 @@ Added:
|
|||||||
- Preserved existing API clients, callbacks and export/QA behavior.
|
- Preserved existing API clients, callbacks and export/QA behavior.
|
||||||
- Added regression coverage for the QA/QC and Exports workspace structure.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -52,19 +52,20 @@ def test_app_still_wires_dataset_ui_callbacks() -> None:
|
|||||||
assert "onUploadDataset={uploadDataset}" in app
|
assert "onUploadDataset={uploadDataset}" in app
|
||||||
assert "onLoadDatasetDetails={loadDatasetDetails}" in app
|
assert "onLoadDatasetDetails={loadDatasetDetails}" in app
|
||||||
assert "onRefreshMetadata={refreshMetadata}" in app
|
assert "onRefreshMetadata={refreshMetadata}" in app
|
||||||
assert "<DatasetDetailPanel" in app
|
assert "<WorkbenchInspector" in app
|
||||||
assert "onRunRasterInspect={runRasterInspect}" in app
|
assert "datasetDetailProps={{" in app
|
||||||
assert "onRunRasterPreview={runRasterPreview}" in app
|
assert "onRunRasterInspect: runRasterInspect" in app
|
||||||
assert "onRunRasterStats={runRasterStats}" in app
|
assert "onRunRasterPreview: runRasterPreview" in app
|
||||||
assert "onRunRasterReproject={runRasterReproject}" in app
|
assert "onRunRasterStats: runRasterStats" in app
|
||||||
assert "onRunRasterClip={runRasterClip}" in app
|
assert "onRunRasterReproject: runRasterReproject" in app
|
||||||
assert "onRunRasterTile={runRasterTile}" in app
|
assert "onRunRasterClip: runRasterClip" in app
|
||||||
assert "onRunRasterNdvi={runRasterNdvi}" in app
|
assert "onRunRasterTile: runRasterTile" in app
|
||||||
assert "onRunRasterNdwi={runRasterNdwi}" in app
|
assert "onRunRasterNdvi: runRasterNdvi" in app
|
||||||
assert "onRunRasterNdbi={runRasterNdbi}" in app
|
assert "onRunRasterNdwi: runRasterNdwi" in app
|
||||||
assert "onRunVectorClip={runVectorClip}" in app
|
assert "onRunRasterNdbi: runRasterNdbi" in app
|
||||||
assert "onRunVectorBuffer={runVectorBuffer}" in app
|
assert "onRunVectorClip: runVectorClip" in app
|
||||||
assert "onRunVectorIntersect={() => runVectorIntersect(availableVectorTargets)}" in app
|
assert "onRunVectorBuffer: runVectorBuffer" in app
|
||||||
|
assert "onRunVectorIntersect: () => runVectorIntersect(availableVectorTargets)" in app
|
||||||
assert '<form className="dataset-upload-form" onSubmit={onUploadDataset}' in dataset_panel
|
assert '<form className="dataset-upload-form" onSubmit={onUploadDataset}' in dataset_panel
|
||||||
assert "onClick={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)}" in dataset_panel
|
assert "onClick={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)}" in dataset_panel
|
||||||
assert "onClick={() => onRefreshMetadata(dataset.id)}" in dataset_panel
|
assert "onClick={() => onRefreshMetadata(dataset.id)}" in dataset_panel
|
||||||
|
|||||||
@@ -8,11 +8,16 @@ ROOT = Path(__file__).resolve().parents[2]
|
|||||||
|
|
||||||
def test_app_uses_dataset_presentational_components() -> None:
|
def test_app_uses_dataset_presentational_components() -> None:
|
||||||
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
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/DatasetPanel'" in app
|
||||||
assert "from './components/datasets/DatasetDetailPanel'" in app
|
assert "from './components/inspector/WorkbenchInspector'" in app
|
||||||
assert "<DatasetPanel" in app
|
assert "<DatasetPanel" in app
|
||||||
assert "<DatasetDetailPanel" in app
|
assert "<WorkbenchInspector" in app
|
||||||
|
assert "from '../datasets/DatasetDetailPanel'" in inspector
|
||||||
|
assert "<DatasetDetailPanel {...datasetDetailProps} />" in inspector
|
||||||
assert "<form onSubmit={uploadDataset}" not in app
|
assert "<form onSubmit={uploadDataset}" not in app
|
||||||
assert "<h3>Raster operations</h3>" not in app
|
assert "<h3>Raster operations</h3>" not in app
|
||||||
assert "<h3>Vector operations</h3>" not in app
|
assert "<h3>Vector operations</h3>" not in app
|
||||||
|
|||||||
@@ -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 "<WorkbenchInspector" in app
|
||||||
|
assert "selectedProject={selectedProject}" in app
|
||||||
|
assert "selectedArea={selectedArea}" in app
|
||||||
|
assert "qualityChecks={qualityChecks}" in app
|
||||||
|
assert "latestExport={latestExport}" in app
|
||||||
|
assert "detectionRuns={detectionRuns}" in app
|
||||||
|
assert "segmentationRuns={segmentationRuns}" in app
|
||||||
|
assert "datasetDetailProps={{" in app
|
||||||
|
|
||||||
|
|
||||||
|
def test_workbench_inspector_exposes_context_dataset_quality_and_ai_tabs() -> 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 "<DatasetDetailPanel {...datasetDetailProps} />" 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
|
||||||
@@ -2118,3 +2118,29 @@ Limitations:
|
|||||||
|
|
||||||
Next recommended pass:
|
Next recommended pass:
|
||||||
- Verify QA/QC and Exports on the deployed Tower workbench, then continue with selected-object inspector detail tabs.
|
- 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.
|
||||||
|
|||||||
+2
-1
@@ -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] Deploy the shell refactor to Tower and run live browser smoke on port 1202.
|
||||||
- [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.
|
||||||
- [ ] 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.
|
||||||
|
|||||||
+70
-54
@@ -1,11 +1,11 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import './styles/app.css'
|
import './styles/app.css'
|
||||||
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
||||||
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 { ExportPreview } from './components/exports/ExportPreview'
|
import { ExportPreview } from './components/exports/ExportPreview'
|
||||||
|
import { WorkbenchInspector } from './components/inspector/WorkbenchInspector'
|
||||||
import { MapWorkspace } from './components/map/MapWorkspace'
|
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'
|
||||||
@@ -690,60 +690,76 @@ function App(): JSX.Element {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<aside className="workbench-inspector" aria-label="Current selection inspector">
|
<aside className="workbench-inspector" aria-label="Current selection inspector">
|
||||||
<DatasetDetailPanel
|
<WorkbenchInspector
|
||||||
|
selectedProject={selectedProject}
|
||||||
|
selectedArea={selectedArea}
|
||||||
|
selectedMapFeature={selectedMapFeature}
|
||||||
areas={areas}
|
areas={areas}
|
||||||
availableVectorTargets={availableVectorTargets}
|
datasetsCount={datasets.length}
|
||||||
selectedDatasetId={selectedDatasetId}
|
qualityChecks={qualityChecks}
|
||||||
selectedDataset={selectedDataset}
|
exports={exports}
|
||||||
selectedDatasetSummary={selectedDatasetSummary}
|
latestExport={latestExport}
|
||||||
selectedRasterMetadata={selectedRasterMetadata}
|
detectionRuns={detectionRuns}
|
||||||
selectedRasterStats={selectedRasterStats}
|
selectedDetectionRunId={selectedDetectionRunId}
|
||||||
rasterPreview={rasterPreview}
|
detectionItems={detectionItems}
|
||||||
rasterUnavailableMessage={rasterUnavailableMessage}
|
segmentationRuns={segmentationRuns}
|
||||||
selectedClipAreaId={selectedClipAreaId}
|
selectedSegmentationRunId={selectedSegmentationRunId}
|
||||||
selectedIntersectTargetId={selectedIntersectTargetId}
|
segmentationItems={segmentationItems}
|
||||||
rasterTileSize={rasterTileSize}
|
datasetDetailProps={{
|
||||||
rasterTileOverlap={rasterTileOverlap}
|
areas,
|
||||||
rasterTileOutputName={rasterTileOutputName}
|
availableVectorTargets,
|
||||||
rasterReprojectCrs={rasterReprojectCrs}
|
selectedDatasetId,
|
||||||
rasterReprojectResampling={rasterReprojectResampling}
|
selectedDataset,
|
||||||
ndviNirBand={ndviNirBand}
|
selectedDatasetSummary,
|
||||||
ndviRedBand={ndviRedBand}
|
selectedRasterMetadata,
|
||||||
ndwiGreenBand={ndwiGreenBand}
|
selectedRasterStats,
|
||||||
ndwiNirBand={ndwiNirBand}
|
rasterPreview,
|
||||||
ndbiSwirBand={ndbiSwirBand}
|
rasterUnavailableMessage,
|
||||||
ndbiNirBand={ndbiNirBand}
|
selectedClipAreaId,
|
||||||
isRasterTileInputValid={isRasterTileInputValid}
|
selectedIntersectTargetId,
|
||||||
jobs={jobs}
|
rasterTileSize,
|
||||||
loadingDatasetDetails={loadingDatasetDetails}
|
rasterTileOverlap,
|
||||||
datasetDetailError={datasetDetailError}
|
rasterTileOutputName,
|
||||||
isVectorDatasetType={isVectorDatasetType}
|
rasterReprojectCrs,
|
||||||
onSetSelectedClipAreaId={setSelectedClipAreaId}
|
rasterReprojectResampling,
|
||||||
onSetSelectedIntersectTargetId={setSelectedIntersectTargetId}
|
ndviNirBand,
|
||||||
onSetRasterTileSize={setRasterTileSize}
|
ndviRedBand,
|
||||||
onSetRasterTileOverlap={setRasterTileOverlap}
|
ndwiGreenBand,
|
||||||
onSetRasterTileOutputName={setRasterTileOutputName}
|
ndwiNirBand,
|
||||||
onSetRasterReprojectCrs={setRasterReprojectCrs}
|
ndbiSwirBand,
|
||||||
onSetRasterReprojectResampling={setRasterReprojectResampling}
|
ndbiNirBand,
|
||||||
onSetNdviNirBand={setNdviNirBand}
|
isRasterTileInputValid,
|
||||||
onSetNdviRedBand={setNdviRedBand}
|
jobs,
|
||||||
onSetNdwiGreenBand={setNdwiGreenBand}
|
loadingDatasetDetails,
|
||||||
onSetNdwiNirBand={setNdwiNirBand}
|
datasetDetailError,
|
||||||
onSetNdbiSwirBand={setNdbiSwirBand}
|
isVectorDatasetType,
|
||||||
onSetNdbiNirBand={setNdbiNirBand}
|
onSetSelectedClipAreaId: setSelectedClipAreaId,
|
||||||
onRunRasterInspect={runRasterInspect}
|
onSetSelectedIntersectTargetId: setSelectedIntersectTargetId,
|
||||||
onRunRasterPreview={runRasterPreview}
|
onSetRasterTileSize: setRasterTileSize,
|
||||||
onRunRasterStats={runRasterStats}
|
onSetRasterTileOverlap: setRasterTileOverlap,
|
||||||
onRunRasterReproject={runRasterReproject}
|
onSetRasterTileOutputName: setRasterTileOutputName,
|
||||||
onRunRasterClip={runRasterClip}
|
onSetRasterReprojectCrs: setRasterReprojectCrs,
|
||||||
onRunRasterTile={runRasterTile}
|
onSetRasterReprojectResampling: setRasterReprojectResampling,
|
||||||
onRunRasterNdvi={runRasterNdvi}
|
onSetNdviNirBand: setNdviNirBand,
|
||||||
onRunRasterNdwi={runRasterNdwi}
|
onSetNdviRedBand: setNdviRedBand,
|
||||||
onRunRasterNdbi={runRasterNdbi}
|
onSetNdwiGreenBand: setNdwiGreenBand,
|
||||||
onRunVectorClip={runVectorClip}
|
onSetNdwiNirBand: setNdwiNirBand,
|
||||||
onRunVectorBuffer={runVectorBuffer}
|
onSetNdbiSwirBand: setNdbiSwirBand,
|
||||||
onRunVectorIntersect={() => runVectorIntersect(availableVectorTargets)}
|
onSetNdbiNirBand: setNdbiNirBand,
|
||||||
onPickDerivedDataset={pickDerivedDataset}
|
onRunRasterInspect: runRasterInspect,
|
||||||
|
onRunRasterPreview: runRasterPreview,
|
||||||
|
onRunRasterStats: runRasterStats,
|
||||||
|
onRunRasterReproject: runRasterReproject,
|
||||||
|
onRunRasterClip: runRasterClip,
|
||||||
|
onRunRasterTile: runRasterTile,
|
||||||
|
onRunRasterNdvi: runRasterNdvi,
|
||||||
|
onRunRasterNdwi: runRasterNdwi,
|
||||||
|
onRunRasterNdbi: runRasterNdbi,
|
||||||
|
onRunVectorClip: runVectorClip,
|
||||||
|
onRunVectorBuffer: runVectorBuffer,
|
||||||
|
onRunVectorIntersect: () => runVectorIntersect(availableVectorTargets),
|
||||||
|
onPickDerivedDataset: pickDerivedDataset,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
import { RasterControls } from './RasterControls'
|
import { RasterControls } from './RasterControls'
|
||||||
import { VectorControls } from './VectorControls'
|
import { VectorControls } from './VectorControls'
|
||||||
|
|
||||||
interface DatasetDetailPanelProps {
|
export interface DatasetDetailPanelProps {
|
||||||
areas: AreaRead[]
|
areas: AreaRead[]
|
||||||
availableVectorTargets: DatasetCreateResponse[]
|
availableVectorTargets: DatasetCreateResponse[]
|
||||||
selectedDatasetId: string | null
|
selectedDatasetId: string | null
|
||||||
@@ -147,7 +147,7 @@ export function DatasetDetailPanel({
|
|||||||
onPickDerivedDataset,
|
onPickDerivedDataset,
|
||||||
}: DatasetDetailPanelProps) {
|
}: DatasetDetailPanelProps) {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section className="dataset-detail-panel">
|
||||||
<h2>Dataset details</h2>
|
<h2>Dataset details</h2>
|
||||||
{selectedDatasetId ? <p>Selected dataset: {selectedDatasetId}</p> : <p>No dataset selected</p>}
|
{selectedDatasetId ? <p>Selected dataset: {selectedDatasetId}</p> : <p>No dataset selected</p>}
|
||||||
{selectedDataset ? (
|
{selectedDataset ? (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="inspector-field">
|
||||||
|
<span>{label}</span>
|
||||||
|
<strong>{formatValue(value)}</strong>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<InspectorTab>('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 (
|
||||||
|
<section className="workbench-inspector-panel" data-testid="workbench-inspector-panel">
|
||||||
|
<div className="inspector-header">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Inspector</p>
|
||||||
|
<h2>Selection details</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="inspector-tabs" role="tablist" aria-label="Inspector detail tabs">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.key}
|
||||||
|
type="button"
|
||||||
|
className={activeTab === tab.key ? 'inspector-tab inspector-tab-active' : 'inspector-tab'}
|
||||||
|
onClick={() => setActiveTab(tab.key)}
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeTab === tab.key}
|
||||||
|
data-testid={`inspector-tab-${tab.key}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === 'context' ? (
|
||||||
|
<div className="inspector-tab-panel">
|
||||||
|
<div className="inspector-card">
|
||||||
|
<h3>Project context</h3>
|
||||||
|
<InspectorField label="Project" value={selectedProject?.name} />
|
||||||
|
<InspectorField label="Region" value={selectedProject?.region} />
|
||||||
|
<InspectorField label="Status" value={selectedProject?.status} />
|
||||||
|
<InspectorField label="AOIs" value={areas.length} />
|
||||||
|
<InspectorField label="Datasets" value={datasetsCount} />
|
||||||
|
</div>
|
||||||
|
<div className="inspector-card">
|
||||||
|
<h3>Active AOI</h3>
|
||||||
|
<InspectorField label="Name" value={selectedArea?.name} />
|
||||||
|
<InspectorField label="Area m2" value={selectedArea?.area_m2} />
|
||||||
|
<InspectorField label="CRS" value={selectedArea?.original_crs} />
|
||||||
|
<InspectorField label="Geometry" value={selectedArea?.geometry?.type} />
|
||||||
|
</div>
|
||||||
|
<div className="inspector-card">
|
||||||
|
<h3>Map feature</h3>
|
||||||
|
{selectedMapFeature ? (
|
||||||
|
<pre className="job-result">{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}</pre>
|
||||||
|
) : (
|
||||||
|
<p className="muted">No map feature selected.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{activeTab === 'dataset' ? (
|
||||||
|
<div className="inspector-tab-panel">
|
||||||
|
<DatasetDetailPanel {...datasetDetailProps} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{activeTab === 'quality' ? (
|
||||||
|
<div className="inspector-tab-panel">
|
||||||
|
<div className="inspector-card">
|
||||||
|
<h3>Latest QA/QC</h3>
|
||||||
|
<InspectorField label="Check type" value={latestQualityCheck?.check_type} />
|
||||||
|
<InspectorField label="Status" value={latestQualityCheck?.status} />
|
||||||
|
<InspectorField label="Score" value={latestQualityCheck?.score} />
|
||||||
|
<InspectorField label="Metrics" value={latestQualityCheck?.metrics.length} />
|
||||||
|
<InspectorField label="Reference" value={latestQualityCheck?.reference_dataset_id} />
|
||||||
|
</div>
|
||||||
|
<div className="inspector-card">
|
||||||
|
<h3>Latest export</h3>
|
||||||
|
<InspectorField label="Created now" value={latestExport?.export_type} />
|
||||||
|
<InspectorField label="Persisted type" value={latestPersistedExport?.export_type} />
|
||||||
|
<InspectorField label="Status" value={latestPersistedExport?.status ?? latestExport?.status} />
|
||||||
|
<InspectorField label="Path" value={latestPersistedExport?.storage_path ?? latestExport?.path} />
|
||||||
|
<InspectorField label="Export count" value={exports.length} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{activeTab === 'ai' ? (
|
||||||
|
<div className="inspector-tab-panel">
|
||||||
|
<div className="inspector-card">
|
||||||
|
<h3>Detection run</h3>
|
||||||
|
<InspectorField label="Selected run" value={selectedDetectionRunId} />
|
||||||
|
<InspectorField label="Status" value={selectedDetectionRun?.status} />
|
||||||
|
<InspectorField label="Model" value={selectedDetectionRun?.model_name} />
|
||||||
|
<InspectorField label="Detections loaded" value={detectionItems.length} />
|
||||||
|
</div>
|
||||||
|
<div className="inspector-card">
|
||||||
|
<h3>Segmentation run</h3>
|
||||||
|
<InspectorField label="Selected run" value={selectedSegmentationRunId} />
|
||||||
|
<InspectorField label="Status" value={selectedSegmentationRun?.status} />
|
||||||
|
<InspectorField label="Model" value={selectedSegmentationRun?.model_name} />
|
||||||
|
<InspectorField label="Segmentations loaded" value={segmentationItems.length} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
+101
-1
@@ -764,7 +764,7 @@ section th {
|
|||||||
max-height: 16rem;
|
max-height: 16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-inspector h2 {
|
.inspector-header h2 {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -1121,6 +1121,106 @@ button.entity-card {
|
|||||||
grid-template-columns: 1fr;
|
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) {
|
@media (max-width: 1120px) {
|
||||||
.workbench-layout {
|
.workbench-layout {
|
||||||
grid-template-columns: 12rem minmax(0, 1fr);
|
grid-template-columns: 12rem minmax(0, 1fr);
|
||||||
|
|||||||
Reference in New Issue
Block a user