Add one-click GIS workflow run
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ The Map workspace defaults to an OpenStreetMap road basemap with visible attribu
|
||||
|
||||
When the public OpenStreetMap fallback is active, the Map workspace shows a basemap usage notice. This keeps the local/demo default honest and reminds operators to configure a managed style URL before production or heavier tile traffic.
|
||||
|
||||
Operational GIS testing is now available directly in the Map workspace. Users can choose a persisted vector database layer, load it on the map, reuse the selected AOI or active layer extent, run the existing persisted `vector_features` bbox query, save the result as a derived dataset, export the selection GeoJSON, choose a reference dataset and launch QA/QC without creating fake data or a parallel backend path.
|
||||
Operational GIS testing is now available directly in the Map workspace. Users can choose a persisted vector database layer, load it on the map, reuse the selected AOI or active layer extent, run the existing persisted `vector_features` bbox query, save the result as a derived dataset, export the selection GeoJSON, choose a reference dataset and launch QA/QC without creating fake data or a parallel backend path. The guided workflow also includes a one-click full run action that executes query, derived dataset save, GeoJSON export and optional QA/QC in sequence with visible status.
|
||||
|
||||
QA/QC and Exports follow the same calmer density model. QA/QC keeps metric evidence, feature ids and raw findings available but compresses provenance and history surfaces so review starts from the selected check and map evidence actions. Exports uses denser handoff cards, latest-artifact cards and history filters so artifact creation and download paths are easier to scan.
|
||||
|
||||
|
||||
@@ -218,12 +218,12 @@ interface MapWorkspaceProps {
|
||||
onSetMapLayerOpacity: (opacity: number) => void
|
||||
onSelectMapFeature: (feature: GeoJSON.Feature | null) => void
|
||||
onSetMapSelectionBbox: (bbox: VectorSelectionBBox | null) => void
|
||||
onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => void
|
||||
onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => Promise<VectorSelectionResponse | null>
|
||||
onClearMapSelectionExtract: () => void
|
||||
onExportMapSelection: (bbox: VectorSelectionBBox) => void
|
||||
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => void
|
||||
onExportMapSelection: (bbox: VectorSelectionBBox) => Promise<unknown>
|
||||
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => Promise<DatasetCreateResponse | null>
|
||||
onSelectMapQaReferenceDataset: (datasetId: string) => void
|
||||
onRunMapSelectionQa: () => void
|
||||
onRunMapSelectionQa: (candidateDataset?: DatasetCreateResponse | null) => Promise<QaComparisonResult | null>
|
||||
onOpenMapSelectionQualityEvidence: () => void
|
||||
onClearQualityEvidence?: () => void
|
||||
}
|
||||
@@ -287,6 +287,9 @@ export function MapWorkspace({
|
||||
const [bboxSelectionMode, setBboxSelectionMode] = useState(false)
|
||||
const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null)
|
||||
const [bboxInput, setBboxInput] = useState(bboxToInputState(mapSelectionBbox))
|
||||
const [fullWorkflowRunning, setFullWorkflowRunning] = useState(false)
|
||||
const [fullWorkflowStatus, setFullWorkflowStatus] = useState('Ready to run persisted GIS workflow.')
|
||||
const [fullWorkflowError, setFullWorkflowError] = useState<string | null>(null)
|
||||
const selectedMapArea = areas.find((area) => area.id === selectedMapAreaId)
|
||||
const featureProperties = selectedMapFeature?.properties ?? null
|
||||
const featureSummaryEntries = featureProperties
|
||||
@@ -404,6 +407,51 @@ export function MapWorkspace({
|
||||
onRunMapSelectionExtract(bbox)
|
||||
}
|
||||
|
||||
const runFullGisWorkflow = async () => {
|
||||
const bbox = currentSelectionBbox ?? selectedAreaBbox ?? activeLayerBbox
|
||||
if (!selectedMapDataset || !bbox) {
|
||||
setFullWorkflowError('Select a database layer and AOI/layer extent before running the full workflow.')
|
||||
return
|
||||
}
|
||||
|
||||
setFullWorkflowRunning(true)
|
||||
setFullWorkflowError(null)
|
||||
try {
|
||||
setFullWorkflowStatus('1/4 Querying persisted vector_features...')
|
||||
setSelectionBbox(bbox)
|
||||
const selection = await onRunMapSelectionExtract(bbox)
|
||||
if (!selection) {
|
||||
setFullWorkflowError('Persisted vector query did not complete.')
|
||||
setFullWorkflowStatus('Stopped at query.')
|
||||
return
|
||||
}
|
||||
|
||||
setFullWorkflowStatus('2/4 Saving derived result dataset...')
|
||||
const derived = await onDeriveMapSelectionDataset(bbox)
|
||||
if (!derived) {
|
||||
setFullWorkflowError('Derived result dataset was not created.')
|
||||
setFullWorkflowStatus('Stopped at dataset save.')
|
||||
return
|
||||
}
|
||||
|
||||
setFullWorkflowStatus('3/4 Saving GeoJSON export artifact...')
|
||||
await onExportMapSelection(bbox)
|
||||
|
||||
if (selectedMapQaReferenceDatasetId) {
|
||||
setFullWorkflowStatus('4/4 Running QA/QC against selected reference...')
|
||||
const qaResult = await onRunMapSelectionQa(derived)
|
||||
setFullWorkflowStatus(qaResult ? 'Full GIS workflow complete with QA/QC result.' : 'Dataset/export complete; QA/QC did not complete.')
|
||||
} else {
|
||||
setFullWorkflowStatus('Dataset/export complete. Select a reference dataset to add QA/QC.')
|
||||
}
|
||||
} catch (error) {
|
||||
setFullWorkflowError(error instanceof Error ? error.message : 'Full GIS workflow failed.')
|
||||
setFullWorkflowStatus('Workflow stopped.')
|
||||
} finally {
|
||||
setFullWorkflowRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="map-workspace-shell" data-testid="map-workspace">
|
||||
<div className="panel-title-row">
|
||||
@@ -705,6 +753,18 @@ export function MapWorkspace({
|
||||
</div>
|
||||
</div>
|
||||
<div className="guided-gis-actions">
|
||||
<button
|
||||
className="primary-action guided-gis-full-run"
|
||||
disabled={!selectedMapDataset || !mapFeatureCollection || (!currentSelectionBbox && !selectedAreaBbox && !activeLayerBbox) || fullWorkflowRunning}
|
||||
type="button"
|
||||
onClick={runFullGisWorkflow}
|
||||
>
|
||||
{fullWorkflowRunning ? 'Running full workflow...' : 'Run full GIS workflow'}
|
||||
</button>
|
||||
<div className="guided-gis-batch-status" aria-live="polite">
|
||||
<strong>Query, save, QA and export</strong>
|
||||
<span>{fullWorkflowStatus}</span>
|
||||
</div>
|
||||
<button
|
||||
className="secondary-action"
|
||||
disabled={!mapSelectionResult || !currentSelectionBbox || selectionDatasetSaving}
|
||||
@@ -740,7 +800,7 @@ export function MapWorkspace({
|
||||
className="primary-action"
|
||||
disabled={!latestSelectionDatasetName || !selectedMapQaReferenceDatasetId || mapSelectionQaRunning}
|
||||
type="button"
|
||||
onClick={onRunMapSelectionQa}
|
||||
onClick={() => onRunMapSelectionQa()}
|
||||
>
|
||||
{mapSelectionQaRunning ? 'Running QA...' : 'Run QA/QC'}
|
||||
</button>
|
||||
@@ -756,6 +816,7 @@ export function MapWorkspace({
|
||||
{selectionDatasetError ? <p className="error">{selectionDatasetError}</p> : null}
|
||||
{selectionExportError ? <p className="error">{selectionExportError}</p> : null}
|
||||
{mapSelectionQaError ? <p className="error">{mapSelectionQaError}</p> : null}
|
||||
{fullWorkflowError ? <p className="error">{fullWorkflowError}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bbox-select-surface" aria-label="Area selection and extract">
|
||||
@@ -915,7 +976,7 @@ export function MapWorkspace({
|
||||
className="primary-action"
|
||||
disabled={!selectedMapQaReferenceDatasetId || mapSelectionQaRunning}
|
||||
type="button"
|
||||
onClick={onRunMapSelectionQa}
|
||||
onClick={() => onRunMapSelectionQa()}
|
||||
>
|
||||
{mapSelectionQaRunning ? 'Running QA...' : 'Run QA on saved dataset'}
|
||||
</button>
|
||||
|
||||
@@ -112,7 +112,7 @@ export function useExportWorkflow({
|
||||
const exportMapSelectionGeoJson = async (bbox: VectorSelectionBBox) => {
|
||||
if (!selectedDataset || !isVectorDatasetType(selectedDataset.dataset_type)) {
|
||||
setSelectionExportError('Select a vector dataset before saving an area export.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
setSelectionExporting(true)
|
||||
setSelectionExportError(null)
|
||||
@@ -127,8 +127,10 @@ export function useExportWorkflow({
|
||||
setLatestExport(response)
|
||||
setLatestSelectionExport(response)
|
||||
await loadExports(selectedDataset.project_id)
|
||||
return response
|
||||
} catch (error) {
|
||||
setSelectionExportError(formatError(error, 'Failed to save area export'))
|
||||
return null
|
||||
} finally {
|
||||
setSelectionExporting(false)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function useMapSelectionDataset({
|
||||
const deriveMapSelectionDataset = async (bbox: VectorSelectionBBox) => {
|
||||
if (!selectedProjectId || !selectedDataset || !isVectorDatasetType(selectedDataset.dataset_type)) {
|
||||
setSelectionDatasetError('Select a vector dataset before saving the area as a dataset.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
setSelectionDatasetSaving(true)
|
||||
setSelectionDatasetError(null)
|
||||
@@ -41,8 +41,10 @@ export function useMapSelectionDataset({
|
||||
await loadProjectData(selectedProjectId)
|
||||
await loadDatasetDetails(selectedProjectId, derived)
|
||||
setMapLayerVisible(true)
|
||||
return derived
|
||||
} catch (error) {
|
||||
setSelectionDatasetError(formatError(error, 'Failed to save area as dataset'))
|
||||
return null
|
||||
} finally {
|
||||
setSelectionDatasetSaving(false)
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@ export function useMapSelectionExtract({
|
||||
const runMapSelectionExtract = async (bbox: VectorSelectionBBox) => {
|
||||
if (!selectedProjectId || !selectedDataset) {
|
||||
setMapSelectionError('Open a vector dataset before extracting a map area.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
if (!isVectorDatasetType(selectedDataset.dataset_type)) {
|
||||
setMapSelectionError('Area extraction requires an active vector dataset.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
setMapSelectionLoading(true)
|
||||
@@ -44,9 +44,11 @@ export function useMapSelectionExtract({
|
||||
limit: 250,
|
||||
})
|
||||
setMapSelectionResult(response)
|
||||
return response
|
||||
} catch (error) {
|
||||
setMapSelectionResult(null)
|
||||
setMapSelectionError(formatError(error, 'Area extraction failed'))
|
||||
return null
|
||||
} finally {
|
||||
setMapSelectionLoading(false)
|
||||
}
|
||||
|
||||
@@ -22,56 +22,58 @@ export function useMapSelectionQa({
|
||||
const [mapSelectionQaResult, setMapQaResult] = useState<QaComparisonResult | null>(null)
|
||||
const [latestMapSelectionQualityCheckId, setLatestMapSelectionQualityCheckId] = useState<string | null>(null)
|
||||
|
||||
const runMapSelectionQa = async () => {
|
||||
const runMapSelectionQa = async (candidateDataset = latestSelectionDataset) => {
|
||||
if (!selectedProjectId) {
|
||||
setMapSelectionQaError('Select a project before running QA/QC.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
if (!latestSelectionDataset) {
|
||||
if (!candidateDataset) {
|
||||
setMapSelectionQaError('Save the map selection as a dataset before running QA/QC.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
if (!selectedMapQaReferenceDatasetId) {
|
||||
setMapSelectionQaError('Select a reference dataset for QA/QC.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
if (latestSelectionDataset.id === selectedMapQaReferenceDatasetId) {
|
||||
if (candidateDataset.id === selectedMapQaReferenceDatasetId) {
|
||||
setMapSelectionQaError('Candidate and reference datasets must be different.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
setMapSelectionQaRunning(true)
|
||||
setMapSelectionQaError(null)
|
||||
setMapQaResult(null)
|
||||
setLatestMapSelectionQualityCheckId(null)
|
||||
setLatestMapSelectionQualityCheckId(null)
|
||||
try {
|
||||
const request: QaComparisonRequest = {
|
||||
candidate_dataset_id: latestSelectionDataset.id,
|
||||
candidate_dataset_id: candidateDataset.id,
|
||||
reference_dataset_id: selectedMapQaReferenceDatasetId,
|
||||
iou_threshold: 0.5,
|
||||
area_id: latestSelectionDataset.area_id ?? null,
|
||||
area_id: candidateDataset.area_id ?? null,
|
||||
}
|
||||
const job: JobRead = await qaApi.runQa(request)
|
||||
if (job.status === 'failed') {
|
||||
setMapSelectionQaError(job.error_message || 'Map selection QA/QC failed')
|
||||
return
|
||||
return null
|
||||
}
|
||||
const payload = job.result_json
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
setMapSelectionQaError('Map selection QA/QC result was not available')
|
||||
return
|
||||
return null
|
||||
}
|
||||
const parsed = payload as unknown as QaComparisonResult
|
||||
if (!parsed || typeof parsed.status !== 'string') {
|
||||
setMapSelectionQaError('Map selection QA/QC result format was unexpected')
|
||||
return
|
||||
return null
|
||||
}
|
||||
setMapQaResult(parsed)
|
||||
setLatestMapSelectionQualityCheckId(parsed.quality_check_id ?? null)
|
||||
await loadQualityChecks(selectedProjectId)
|
||||
await loadProjectData(selectedProjectId)
|
||||
return parsed
|
||||
} catch (error) {
|
||||
setMapSelectionQaError(formatError(error, 'Map selection QA/QC failed'))
|
||||
return null
|
||||
} finally {
|
||||
setMapSelectionQaRunning(false)
|
||||
}
|
||||
|
||||
@@ -4873,6 +4873,40 @@ section {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.guided-gis-full-run {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.guided-gis-batch-status {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
min-width: 0;
|
||||
border: 1px solid rgba(15, 118, 110, 0.22);
|
||||
border-radius: 6px;
|
||||
padding: 0.44rem 0.52rem;
|
||||
background: #f8fbf9;
|
||||
}
|
||||
|
||||
.guided-gis-batch-status strong {
|
||||
overflow: hidden;
|
||||
color: #0f766e;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 850;
|
||||
letter-spacing: 0.045em;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.guided-gis-batch-status span {
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bbox-select-surface {
|
||||
border-left-width: 3px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user