Extract project workspace hook
This commit is contained in:
+26
-130
@@ -1,8 +1,5 @@
|
||||
import { FormEvent, useEffect, useMemo, useState } from 'react'
|
||||
import './styles/app.css'
|
||||
import { areasApi } from './services/api/areas'
|
||||
import { datasetsApi } from './services/api/datasets'
|
||||
import { projectsApi } from './services/api/projects'
|
||||
import { demoApi } from './services/api'
|
||||
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
||||
import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel'
|
||||
@@ -15,15 +12,7 @@ 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 {
|
||||
DatasetCreateResponse,
|
||||
DatasetListResponse,
|
||||
ProjectRead,
|
||||
ProjectCreate,
|
||||
AreaCreate,
|
||||
AreaListResponse,
|
||||
AreaRead,
|
||||
} from './types'
|
||||
import type { DatasetCreateResponse } from './types'
|
||||
import { ProviderPanel } from './components/providers/ProviderPanel'
|
||||
import { SegmentationLab } from './components/segmentation/SegmentationLab'
|
||||
import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow'
|
||||
@@ -32,6 +21,7 @@ import { useDatasetWorkflow } from './hooks/useDatasetWorkflow'
|
||||
import { useExportWorkflow } from './hooks/useExportWorkflow'
|
||||
import { useMapWorkspaceState } from './hooks/useMapWorkspaceState'
|
||||
import { useProviderCapabilities } from './hooks/useProviderCapabilities'
|
||||
import { useProjectWorkspace } from './hooks/useProjectWorkspace'
|
||||
import { useQualityWorkflow } from './hooks/useQualityWorkflow'
|
||||
import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow'
|
||||
import { formatError } from './lib/formatError'
|
||||
@@ -41,32 +31,31 @@ function isVectorDatasetType(datasetType: string): boolean {
|
||||
}
|
||||
|
||||
function App(): JSX.Element {
|
||||
const [projects, setProjects] = useState<ProjectRead[]>([])
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||
const [areas, setAreas] = useState<AreaRead[]>([])
|
||||
const [datasets, setDatasets] = useState<DatasetCreateResponse[]>([])
|
||||
const [loadingProjects, setLoadingProjects] = useState(false)
|
||||
const [loadingDemoWorkflow, setLoadingDemoWorkflow] = useState(false)
|
||||
const [demoWorkflowMessage, setDemoWorkflowMessage] = useState<string | null>(null)
|
||||
const [loadingAreas, setLoadingAreas] = useState(false)
|
||||
const [loadingDatasets, setLoadingDatasets] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
|
||||
const [projectForm, setProjectForm] = useState<ProjectCreate>({
|
||||
name: '',
|
||||
description: '',
|
||||
region: 'Kempen',
|
||||
})
|
||||
const [areaForm, setAreaForm] = useState({
|
||||
name: 'Demo AOI',
|
||||
geometry:
|
||||
'{"type":"MultiPolygon","coordinates":[[[[4.35,51.28],[4.55,51.28],[4.55,51.46],[4.35,51.46],[4.35,51.28]]]]}',
|
||||
crs: 'EPSG:4326',
|
||||
})
|
||||
const selectedProject = useMemo(
|
||||
() => projects.find((project) => project.id === selectedProjectId) ?? null,
|
||||
[projects, selectedProjectId],
|
||||
)
|
||||
const {
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedProjectId,
|
||||
areas,
|
||||
datasets,
|
||||
loadingProjects,
|
||||
loadingAreas,
|
||||
loadingDatasets,
|
||||
errorMessage,
|
||||
projectForm,
|
||||
areaForm,
|
||||
loadProjects,
|
||||
loadProjectData,
|
||||
createProject,
|
||||
createArea,
|
||||
resetProjectData,
|
||||
setSelectedProjectId,
|
||||
setErrorMessage,
|
||||
setProjectForm,
|
||||
setAreaForm,
|
||||
} = useProjectWorkspace()
|
||||
const {
|
||||
selectedDatasetId,
|
||||
selectedDataset,
|
||||
@@ -128,6 +117,7 @@ function App(): JSX.Element {
|
||||
setDatasetForm,
|
||||
} = useDatasetWorkflow({
|
||||
selectedProjectId,
|
||||
areas,
|
||||
datasets,
|
||||
loadProjectData,
|
||||
setErrorMessage,
|
||||
@@ -334,52 +324,6 @@ function App(): JSX.Element {
|
||||
datasetContent,
|
||||
selectedDataset,
|
||||
})
|
||||
const loadProjects = async () => {
|
||||
setLoadingProjects(true)
|
||||
setErrorMessage(null)
|
||||
try {
|
||||
const response = await projectsApi.list()
|
||||
setProjects(response.items)
|
||||
if (!selectedProjectId && response.items.length > 0) {
|
||||
setSelectedProjectId(response.items[0].id)
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to load projects')
|
||||
} finally {
|
||||
setLoadingProjects(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjectData(projectId: string) {
|
||||
setLoadingAreas(true)
|
||||
setLoadingDatasets(true)
|
||||
setErrorMessage(null)
|
||||
try {
|
||||
const [areaResponse, datasetResponse]: [AreaListResponse, DatasetListResponse] = await Promise.all([
|
||||
areasApi.list(projectId),
|
||||
datasetsApi.list(projectId),
|
||||
])
|
||||
setAreas(areaResponse.items)
|
||||
setDatasets(datasetResponse.items)
|
||||
if (areaResponse.items.length === 0) {
|
||||
setSelectedClipAreaId('')
|
||||
} else if (!selectedClipAreaId || !areaResponse.items.some((area) => area.id === selectedClipAreaId)) {
|
||||
setSelectedClipAreaId(areaResponse.items[0].id)
|
||||
}
|
||||
if (areaResponse.items.length === 0) {
|
||||
setSelectedMapAreaId('')
|
||||
} else if (!selectedMapAreaId || !areaResponse.items.some((area) => area.id === selectedMapAreaId)) {
|
||||
setSelectedMapAreaId(areaResponse.items[0].id)
|
||||
}
|
||||
return { areas: areaResponse.items, datasets: datasetResponse.items }
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data')
|
||||
return null
|
||||
} finally {
|
||||
setLoadingAreas(false)
|
||||
setLoadingDatasets(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadProjects().catch(() => null)
|
||||
@@ -390,10 +334,8 @@ function App(): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedProjectId) {
|
||||
setAreas([])
|
||||
setDatasets([])
|
||||
resetProjectData()
|
||||
resetDatasetForProject()
|
||||
setSelectedMapAreaId('')
|
||||
resetDetectionForProject()
|
||||
resetSegmentationForProject()
|
||||
resetExportsForProject()
|
||||
@@ -414,25 +356,6 @@ function App(): JSX.Element {
|
||||
loadSegmentationResults().catch(() => null)
|
||||
}, [selectedSegmentationRunId, segmentationClassFilter, segmentationMinConfidenceFilter])
|
||||
|
||||
const createProject = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!projectForm.name.trim()) {
|
||||
setErrorMessage('Project name is required')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await projectsApi.create({
|
||||
name: projectForm.name.trim(),
|
||||
description: projectForm.description?.trim() || undefined,
|
||||
region: projectForm.region?.trim() || 'Kempen',
|
||||
})
|
||||
setProjectForm((previous) => ({ ...previous, name: '', description: '' }))
|
||||
await loadProjects()
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to create project')
|
||||
}
|
||||
}
|
||||
|
||||
const loadDemoWorkflow = async () => {
|
||||
setLoadingDemoWorkflow(true)
|
||||
setDemoWorkflowMessage(null)
|
||||
@@ -467,33 +390,6 @@ function App(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
const createArea = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!selectedProjectId) {
|
||||
setErrorMessage('Select a project first')
|
||||
return
|
||||
}
|
||||
let geometry: AreaCreate['geometry']
|
||||
try {
|
||||
geometry = JSON.parse(areaForm.geometry) as AreaCreate['geometry']
|
||||
} catch {
|
||||
setErrorMessage('Invalid GeoJSON geometry JSON')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const createdArea = await areasApi.create(selectedProjectId, {
|
||||
name: areaForm.name,
|
||||
crs: areaForm.crs,
|
||||
geometry,
|
||||
})
|
||||
setSelectedMapAreaId(createdArea.id)
|
||||
await loadProjectData(selectedProjectId)
|
||||
setAreaForm((previous) => ({ ...previous, name: '' }))
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to create area')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<header>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FormEvent, useMemo, useState } from 'react'
|
||||
import { FormEvent, useEffect, useMemo, useState } from 'react'
|
||||
import { datasetsApi, jobsApi } from '../services/api'
|
||||
import type {
|
||||
AreaRead,
|
||||
DatasetCreateResponse,
|
||||
JobRead,
|
||||
RasterMetadataResponse,
|
||||
@@ -12,6 +13,7 @@ import { formatError } from '../lib/formatError'
|
||||
|
||||
interface DatasetWorkflowOptions {
|
||||
selectedProjectId: string | null
|
||||
areas: AreaRead[]
|
||||
datasets: DatasetCreateResponse[]
|
||||
loadProjectData: (projectId: string) => Promise<{ datasets: DatasetCreateResponse[] } | null>
|
||||
setErrorMessage: (message: string | null) => void
|
||||
@@ -27,6 +29,7 @@ function toRasterMetadata(metadata: Record<string, unknown> | null | undefined):
|
||||
|
||||
export function useDatasetWorkflow({
|
||||
selectedProjectId,
|
||||
areas,
|
||||
datasets,
|
||||
loadProjectData,
|
||||
setErrorMessage,
|
||||
@@ -72,6 +75,14 @@ export function useDatasetWorkflow({
|
||||
[rasterTileSize, rasterTileOverlap],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (areas.length === 0) {
|
||||
setSelectedClipAreaId('')
|
||||
} else if (!selectedClipAreaId || !areas.some((area) => area.id === selectedClipAreaId)) {
|
||||
setSelectedClipAreaId(areas[0].id)
|
||||
}
|
||||
}, [areas, selectedClipAreaId])
|
||||
|
||||
const loadDatasetJobs = async (projectId: string, datasetId: string) => {
|
||||
const response = await jobsApi.list(projectId, { dataset_id: datasetId, limit: 20, offset: 0 })
|
||||
setJobs(response.items)
|
||||
|
||||
@@ -25,6 +25,14 @@ export function useMapWorkspaceState({
|
||||
const [areaLayerOpacity, setAreaLayerOpacity] = useState(0.18)
|
||||
const [selectedMapFeature, setSelectedMapFeature] = useState<GeoJSON.Feature | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (areas.length === 0) {
|
||||
setSelectedMapAreaId('')
|
||||
} else if (!selectedMapAreaId || !areas.some((area) => area.id === selectedMapAreaId)) {
|
||||
setSelectedMapAreaId(areas[0].id)
|
||||
}
|
||||
}, [areas, selectedMapAreaId])
|
||||
|
||||
const selectedMapArea = useMemo(
|
||||
() => areas.find((area) => area.id === selectedMapAreaId) ?? null,
|
||||
[areas, selectedMapAreaId],
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { FormEvent, useMemo, useState } from 'react'
|
||||
import { areasApi } from '../services/api/areas'
|
||||
import { datasetsApi } from '../services/api/datasets'
|
||||
import { projectsApi } from '../services/api/projects'
|
||||
import type {
|
||||
AreaCreate,
|
||||
AreaListResponse,
|
||||
AreaRead,
|
||||
DatasetCreateResponse,
|
||||
DatasetListResponse,
|
||||
ProjectCreate,
|
||||
ProjectRead,
|
||||
} from '../types'
|
||||
|
||||
export function useProjectWorkspace() {
|
||||
const [projects, setProjects] = useState<ProjectRead[]>([])
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||
const [areas, setAreas] = useState<AreaRead[]>([])
|
||||
const [datasets, setDatasets] = useState<DatasetCreateResponse[]>([])
|
||||
const [loadingProjects, setLoadingProjects] = useState(false)
|
||||
const [loadingAreas, setLoadingAreas] = useState(false)
|
||||
const [loadingDatasets, setLoadingDatasets] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
|
||||
const [projectForm, setProjectForm] = useState<ProjectCreate>({
|
||||
name: '',
|
||||
description: '',
|
||||
region: 'Kempen',
|
||||
})
|
||||
const [areaForm, setAreaForm] = useState({
|
||||
name: 'Demo AOI',
|
||||
geometry:
|
||||
'{"type":"MultiPolygon","coordinates":[[[[4.35,51.28],[4.55,51.28],[4.55,51.46],[4.35,51.46],[4.35,51.28]]]]}',
|
||||
crs: 'EPSG:4326',
|
||||
})
|
||||
|
||||
const selectedProject = useMemo(
|
||||
() => projects.find((project) => project.id === selectedProjectId) ?? null,
|
||||
[projects, selectedProjectId],
|
||||
)
|
||||
|
||||
const loadProjects = async () => {
|
||||
setLoadingProjects(true)
|
||||
setErrorMessage(null)
|
||||
try {
|
||||
const response = await projectsApi.list()
|
||||
setProjects(response.items)
|
||||
if (!selectedProjectId && response.items.length > 0) {
|
||||
setSelectedProjectId(response.items[0].id)
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to load projects')
|
||||
} finally {
|
||||
setLoadingProjects(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadProjectData = async (projectId: string) => {
|
||||
setLoadingAreas(true)
|
||||
setLoadingDatasets(true)
|
||||
setErrorMessage(null)
|
||||
try {
|
||||
const [areaResponse, datasetResponse]: [AreaListResponse, DatasetListResponse] = await Promise.all([
|
||||
areasApi.list(projectId),
|
||||
datasetsApi.list(projectId),
|
||||
])
|
||||
setAreas(areaResponse.items)
|
||||
setDatasets(datasetResponse.items)
|
||||
return { areas: areaResponse.items, datasets: datasetResponse.items }
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data')
|
||||
return null
|
||||
} finally {
|
||||
setLoadingAreas(false)
|
||||
setLoadingDatasets(false)
|
||||
}
|
||||
}
|
||||
|
||||
const createProject = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!projectForm.name.trim()) {
|
||||
setErrorMessage('Project name is required')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await projectsApi.create({
|
||||
name: projectForm.name.trim(),
|
||||
description: projectForm.description?.trim() || undefined,
|
||||
region: projectForm.region?.trim() || 'Kempen',
|
||||
})
|
||||
setProjectForm((previous) => ({ ...previous, name: '', description: '' }))
|
||||
await loadProjects()
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to create project')
|
||||
}
|
||||
}
|
||||
|
||||
const createArea = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!selectedProjectId) {
|
||||
setErrorMessage('Select a project first')
|
||||
return
|
||||
}
|
||||
let geometry: AreaCreate['geometry']
|
||||
try {
|
||||
geometry = JSON.parse(areaForm.geometry) as AreaCreate['geometry']
|
||||
} catch {
|
||||
setErrorMessage('Invalid GeoJSON geometry JSON')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await areasApi.create(selectedProjectId, {
|
||||
name: areaForm.name,
|
||||
crs: areaForm.crs,
|
||||
geometry,
|
||||
})
|
||||
await loadProjectData(selectedProjectId)
|
||||
setAreaForm((previous) => ({ ...previous, name: '' }))
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to create area')
|
||||
}
|
||||
}
|
||||
|
||||
const resetProjectData = () => {
|
||||
setAreas([])
|
||||
setDatasets([])
|
||||
}
|
||||
|
||||
return {
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedProjectId,
|
||||
areas,
|
||||
datasets,
|
||||
loadingProjects,
|
||||
loadingAreas,
|
||||
loadingDatasets,
|
||||
errorMessage,
|
||||
projectForm,
|
||||
areaForm,
|
||||
loadProjects,
|
||||
loadProjectData,
|
||||
createProject,
|
||||
createArea,
|
||||
resetProjectData,
|
||||
setSelectedProjectId,
|
||||
setErrorMessage,
|
||||
setProjectForm,
|
||||
setAreaForm,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user