Extract QA and map workbench components
This commit is contained in:
+28
-115
@@ -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}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<h2>QA/QC Results</h2>
|
||||
<button type="button" onClick={() => loadQualityChecks()} 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>
|
||||
<QualityResultsPanel
|
||||
selectedProjectId={selectedProjectId}
|
||||
qualityChecks={qualityChecks}
|
||||
qualityChecksError={qualityChecksError}
|
||||
onRefresh={() => loadQualityChecks()}
|
||||
/>
|
||||
|
||||
<ExportCenter
|
||||
selectedProjectId={selectedProjectId}
|
||||
@@ -836,93 +816,26 @@ function App(): JSX.Element {
|
||||
onPickDerivedDataset={pickDerivedDataset}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<h2>Map workspace</h2>
|
||||
<p>{mapLayerLabel}</p>
|
||||
<div className="map-controls">
|
||||
<label>
|
||||
Selected area
|
||||
<select
|
||||
value={selectedMapAreaId}
|
||||
onChange={(event) => setSelectedMapAreaId(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) => 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>
|
||||
<MapWorkspace
|
||||
areas={areas}
|
||||
selectedMapAreaId={selectedMapAreaId}
|
||||
areaFeatureCollection={areaFeatureCollection}
|
||||
mapFeatureCollection={mapFeatureCollection}
|
||||
mapLayerLabel={mapLayerLabel}
|
||||
mapLayerVisible={mapLayerVisible}
|
||||
mapLayerOpacity={mapLayerOpacity}
|
||||
areaLayerVisible={areaLayerVisible}
|
||||
areaLayerOpacity={areaLayerOpacity}
|
||||
mapFeatureCount={mapFeatureCount}
|
||||
areaFeatureCount={areaFeatureCount}
|
||||
selectedMapFeature={selectedMapFeature}
|
||||
onSelectMapArea={setSelectedMapAreaId}
|
||||
onSetAreaLayerVisible={setAreaLayerVisible}
|
||||
onSetAreaLayerOpacity={setAreaLayerOpacity}
|
||||
onSetMapLayerVisible={setMapLayerVisible}
|
||||
onSetMapLayerOpacity={setMapLayerOpacity}
|
||||
onSelectMapFeature={setSelectedMapFeature}
|
||||
/>
|
||||
</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