Extract export and QA frontend workflows
This commit is contained in:
+47
-228
@@ -4,7 +4,7 @@ import GeoMap from './components/GeoMap'
|
||||
import { areasApi } from './services/api/areas'
|
||||
import { datasetsApi } from './services/api/datasets'
|
||||
import { projectsApi } from './services/api/projects'
|
||||
import { analysisApi, demoApi, jobsApi, externalApi, exportsApi, qaApi } from './services/api'
|
||||
import { analysisApi, demoApi, jobsApi, externalApi } from './services/api'
|
||||
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
||||
import { DetectionLab } from './components/detection/DetectionLab'
|
||||
import { ExportCenter } from './components/exports/ExportCenter'
|
||||
@@ -17,7 +17,6 @@ import type {
|
||||
DatasetCreateResponse,
|
||||
DatasetListResponse,
|
||||
JobRead,
|
||||
QaComparisonRequest,
|
||||
RasterMetadataResponse,
|
||||
RasterInspectResponse,
|
||||
RasterStatsResponse,
|
||||
@@ -25,18 +24,16 @@ import type {
|
||||
VectorSummary,
|
||||
ProjectRead,
|
||||
ProjectCreate,
|
||||
QualityCheckRead,
|
||||
ExportCreateResponse,
|
||||
ExportRead,
|
||||
AreaCreate,
|
||||
AreaListResponse,
|
||||
AreaRead,
|
||||
ProviderCapability,
|
||||
QaComparisonResult,
|
||||
} from './types'
|
||||
import { ProviderPanel } from './components/providers/ProviderPanel'
|
||||
import { SegmentationLab } from './components/segmentation/SegmentationLab'
|
||||
import { useDetectionWorkflow } from './hooks/useDetectionWorkflow'
|
||||
import { useExportWorkflow } from './hooks/useExportWorkflow'
|
||||
import { useQualityWorkflow } from './hooks/useQualityWorkflow'
|
||||
import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow'
|
||||
import { formatError } from './lib/formatError'
|
||||
|
||||
@@ -100,21 +97,6 @@ function App(): JSX.Element {
|
||||
const [rasterPreview, setRasterPreview] = useState<RasterPreviewResponse | null>(null)
|
||||
const [selectedIntersectTargetId, setSelectedIntersectTargetId] = useState('')
|
||||
const [selectedClipAreaId, setSelectedClipAreaId] = useState('')
|
||||
const [qaCandidateDatasetId, setQaCandidateDatasetId] = useState('')
|
||||
const [qaReferenceDatasetId, setQaReferenceDatasetId] = useState('')
|
||||
const [qaAreaId, setQaAreaId] = useState('')
|
||||
const [qaIouThreshold, setQaIouThreshold] = useState(0.5)
|
||||
const [qaRunning, setQaRunning] = useState(false)
|
||||
const [qaResult, setQaResult] = useState<QaComparisonResult | null>(null)
|
||||
const [qaError, setQaError] = useState<string | null>(null)
|
||||
const [qualityChecks, setQualityChecks] = useState<QualityCheckRead[]>([])
|
||||
const [qualityChecksError, setQualityChecksError] = useState<string | null>(null)
|
||||
const [exports, setExports] = useState<ExportRead[]>([])
|
||||
const [latestExport, setLatestExport] = useState<ExportCreateResponse | null>(null)
|
||||
const [exportError, setExportError] = useState<string | null>(null)
|
||||
const [loadingExports, setLoadingExports] = useState(false)
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const [exportPreview, setExportPreview] = useState<Record<string, unknown> | null>(null)
|
||||
const [rasterTileSize, setRasterTileSize] = useState(512)
|
||||
const [rasterTileOverlap, setRasterTileOverlap] = useState(64)
|
||||
const [rasterTileOutputName, setRasterTileOutputName] = useState('')
|
||||
@@ -187,6 +169,26 @@ function App(): JSX.Element {
|
||||
const candidateDatasets = availableVectorDatasets
|
||||
const providers = useMemo(() => providerCapabilities, [providerCapabilities])
|
||||
const rasterDatasets = useMemo(() => datasets.filter((item) => item.dataset_type === 'raster'), [datasets])
|
||||
const {
|
||||
qaCandidateDatasetId,
|
||||
qaReferenceDatasetId,
|
||||
qaAreaId,
|
||||
qaIouThreshold,
|
||||
qaRunning,
|
||||
qaResult,
|
||||
qaError,
|
||||
qualityChecks,
|
||||
qualityChecksError,
|
||||
loadQualityChecks,
|
||||
runQaComparison,
|
||||
setQaCandidateDatasetId,
|
||||
setQaReferenceDatasetId,
|
||||
setQaAreaId,
|
||||
setQaIouThreshold,
|
||||
} = useQualityWorkflow({
|
||||
selectedProjectId,
|
||||
loadProjectData,
|
||||
})
|
||||
const {
|
||||
detectionModels,
|
||||
loadingDetectionModels,
|
||||
@@ -272,6 +274,29 @@ function App(): JSX.Element {
|
||||
loadProjectData,
|
||||
loadQualityChecks,
|
||||
})
|
||||
const {
|
||||
exports,
|
||||
latestExport,
|
||||
exportError,
|
||||
loadingExports,
|
||||
exporting,
|
||||
exportPreview,
|
||||
loadExports,
|
||||
exportSelectedDatasetGeoJson,
|
||||
exportSelectedDetectionRunGeoJson,
|
||||
exportSelectedSegmentationRunGeoJson,
|
||||
exportProjectMetadata,
|
||||
exportProjectReport,
|
||||
previewExportContent,
|
||||
downloadExportArtifact,
|
||||
resetExportsForProject,
|
||||
} = useExportWorkflow({
|
||||
selectedProjectId,
|
||||
selectedDataset,
|
||||
selectedDetectionRunId,
|
||||
selectedSegmentationRunId,
|
||||
isVectorDatasetType,
|
||||
})
|
||||
const selectedMapArea = useMemo(
|
||||
() => areas.find((area) => area.id === selectedMapAreaId) ?? null,
|
||||
[areas, selectedMapAreaId],
|
||||
@@ -398,152 +423,6 @@ function App(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadQualityChecks(projectId = selectedProjectId) {
|
||||
if (!projectId) {
|
||||
setQualityChecks([])
|
||||
return
|
||||
}
|
||||
setQualityChecksError(null)
|
||||
try {
|
||||
const response = await qaApi.listQualityChecks(projectId)
|
||||
setQualityChecks(response.items)
|
||||
return response.items
|
||||
} catch (error) {
|
||||
setQualityChecksError(formatError(error, 'Failed to load QA/QC results'))
|
||||
}
|
||||
}
|
||||
|
||||
const loadExports = async (projectId = selectedProjectId) => {
|
||||
if (!projectId) {
|
||||
setExports([])
|
||||
return
|
||||
}
|
||||
setLoadingExports(true)
|
||||
setExportError(null)
|
||||
try {
|
||||
const response = await exportsApi.listProjectExports(projectId)
|
||||
setExports(response.items)
|
||||
} catch (error) {
|
||||
setExportError(formatError(error, 'Failed to load exports'))
|
||||
} finally {
|
||||
setLoadingExports(false)
|
||||
}
|
||||
}
|
||||
|
||||
const exportSelectedDatasetGeoJson = async () => {
|
||||
if (!selectedDataset || !isVectorDatasetType(selectedDataset.dataset_type)) {
|
||||
setExportError('Select a vector dataset before exporting GeoJSON.')
|
||||
return
|
||||
}
|
||||
setExporting(true)
|
||||
setExportError(null)
|
||||
try {
|
||||
const response = await exportsApi.exportGeojson({
|
||||
dataset_id: selectedDataset.id,
|
||||
export_kind: 'dataset',
|
||||
name: selectedDataset.name.replace(/\.(geo)?json$/i, ''),
|
||||
})
|
||||
setLatestExport(response)
|
||||
await loadExports(selectedDataset.project_id)
|
||||
} catch (error) {
|
||||
setExportError(formatError(error, 'Failed to export selected dataset'))
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const exportSelectedDetectionRunGeoJson = async () => {
|
||||
if (!selectedDetectionRunId) {
|
||||
setExportError('Select a detection run before exporting GeoJSON.')
|
||||
return
|
||||
}
|
||||
setExporting(true)
|
||||
setExportError(null)
|
||||
try {
|
||||
const response = await exportsApi.exportGeojson({
|
||||
analysis_run_id: selectedDetectionRunId,
|
||||
export_kind: 'detection_run',
|
||||
})
|
||||
setLatestExport(response)
|
||||
await loadExports(selectedProjectId)
|
||||
} catch (error) {
|
||||
setExportError(formatError(error, 'Failed to export detection run'))
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const exportSelectedSegmentationRunGeoJson = async () => {
|
||||
if (!selectedSegmentationRunId) {
|
||||
setExportError('Select a segmentation run before exporting GeoJSON.')
|
||||
return
|
||||
}
|
||||
setExporting(true)
|
||||
setExportError(null)
|
||||
try {
|
||||
const response = await exportsApi.exportGeojson({
|
||||
analysis_run_id: selectedSegmentationRunId,
|
||||
export_kind: 'segmentation_run',
|
||||
})
|
||||
setLatestExport(response)
|
||||
await loadExports(selectedProjectId)
|
||||
} catch (error) {
|
||||
setExportError(formatError(error, 'Failed to export segmentation run'))
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const exportProjectMetadata = async () => {
|
||||
if (!selectedProjectId) {
|
||||
setExportError('Select a project before exporting metadata.')
|
||||
return
|
||||
}
|
||||
setExporting(true)
|
||||
setExportError(null)
|
||||
try {
|
||||
const response = await exportsApi.exportProjectMetadata(selectedProjectId)
|
||||
setLatestExport(response)
|
||||
await loadExports(selectedProjectId)
|
||||
} catch (error) {
|
||||
setExportError(formatError(error, 'Failed to export project metadata'))
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const exportProjectReport = async () => {
|
||||
if (!selectedProjectId) {
|
||||
setExportError('Select a project before exporting a report.')
|
||||
return
|
||||
}
|
||||
setExporting(true)
|
||||
setExportError(null)
|
||||
try {
|
||||
const response = await exportsApi.exportProjectReport(selectedProjectId)
|
||||
setLatestExport(response)
|
||||
await loadExports(selectedProjectId)
|
||||
} catch (error) {
|
||||
setExportError(formatError(error, 'Failed to export project report'))
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const previewExportContent = async (exportId: string) => {
|
||||
setExportError(null)
|
||||
try {
|
||||
const response = await exportsApi.getContent(exportId)
|
||||
setExportPreview(response.content)
|
||||
} catch (error) {
|
||||
setExportError(formatError(error, 'Failed to load export content'))
|
||||
}
|
||||
}
|
||||
|
||||
const downloadExportArtifact = (exportId: string) => {
|
||||
window.open(exportsApi.downloadUrl(exportId), '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
const loadDatasetJobs = async (projectId: string, datasetId: string) => {
|
||||
const response = await jobsApi.list(projectId, { dataset_id: datasetId, limit: 20, offset: 0 })
|
||||
setJobs(response.items)
|
||||
@@ -609,9 +488,7 @@ function App(): JSX.Element {
|
||||
setJobs([])
|
||||
resetDetectionForProject()
|
||||
resetSegmentationForProject()
|
||||
setExports([])
|
||||
setLatestExport(null)
|
||||
setExportPreview(null)
|
||||
resetExportsForProject()
|
||||
return
|
||||
}
|
||||
loadProjectData(selectedProjectId).catch(() => null)
|
||||
@@ -1008,64 +885,6 @@ function App(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
const runQaComparison = async () => {
|
||||
if (!selectedProjectId) {
|
||||
setQaError('Select a project first')
|
||||
return
|
||||
}
|
||||
if (!qaCandidateDatasetId) {
|
||||
setQaError('Select candidate dataset')
|
||||
return
|
||||
}
|
||||
if (!qaReferenceDatasetId) {
|
||||
setQaError('Select reference dataset')
|
||||
return
|
||||
}
|
||||
if (qaCandidateDatasetId === qaReferenceDatasetId) {
|
||||
setQaError('Candidate and reference datasets must be different')
|
||||
return
|
||||
}
|
||||
if (!Number.isFinite(qaIouThreshold) || qaIouThreshold < 0 || qaIouThreshold > 1) {
|
||||
setQaError('IoU threshold must be between 0 and 1')
|
||||
return
|
||||
}
|
||||
setQaError(null)
|
||||
setQaResult(null)
|
||||
setQaRunning(true)
|
||||
try {
|
||||
const request: QaComparisonRequest = {
|
||||
candidate_dataset_id: qaCandidateDatasetId,
|
||||
reference_dataset_id: qaReferenceDatasetId,
|
||||
iou_threshold: qaIouThreshold,
|
||||
area_id: qaAreaId || null,
|
||||
}
|
||||
const job: JobRead = await qaApi.runQa(request)
|
||||
if (job.status === 'failed') {
|
||||
setQaError(job.error_message || 'QA comparison failed')
|
||||
return
|
||||
}
|
||||
const payload = job.result_json
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
setQaError('QA result was not available')
|
||||
return
|
||||
}
|
||||
const parsed = payload as unknown as QaComparisonResult
|
||||
if (!parsed || typeof parsed.status !== 'string') {
|
||||
setQaError('QA result format was unexpected')
|
||||
return
|
||||
}
|
||||
setQaResult(parsed)
|
||||
await loadQualityChecks(selectedProjectId)
|
||||
if (job.output_dataset_id) {
|
||||
await loadProjectData(selectedProjectId)
|
||||
}
|
||||
} catch (error) {
|
||||
setQaError(error instanceof Error ? error.message : 'QA comparison failed')
|
||||
} finally {
|
||||
setQaRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
const runChangeDetection = async () => {
|
||||
const sourceDatasetId = changeSourceDatasetId || availableVectorDatasets[0]?.id
|
||||
const targetDatasetId =
|
||||
|
||||
Reference in New Issue
Block a user