diff --git a/CHANGELOG.md b/CHANGELOG.md index 457dc1e4..32b530f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ # Changelog +## Sprint 29 dataset component decomposition (2026-06-17) + +- Moved dataset upload/list UI into `DatasetPanel`. +- Moved dataset details and job list UI into `DatasetDetailPanel`. +- Split raster and vector controls into `RasterControls` and `VectorControls`. +- Added regression coverage to verify `App.tsx` wires the new presentational dataset components without taking dataset markup back inline. +- No API contracts, backend behavior, migrations, product features, provider fetching, AI behavior or UI redesign were introduced. + ## Sprint 28 dataset workflow hook hardening (2026-06-17) - Moved dataset selection, upload, detail loading, dataset jobs and raster/vector operation orchestration from `App.tsx` into `useDatasetWorkflow`. diff --git a/backend/tests/test_sprint28_dataset_workflow_hook.py b/backend/tests/test_sprint28_dataset_workflow_hook.py index c857c928..c10e4762 100644 --- a/backend/tests/test_sprint28_dataset_workflow_hook.py +++ b/backend/tests/test_sprint28_dataset_workflow_hook.py @@ -41,19 +41,32 @@ def test_dataset_workflow_hook_owns_dataset_api_calls() -> None: def test_app_still_wires_dataset_ui_callbacks() -> None: app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + dataset_panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetPanel.tsx").read_text( + encoding="utf-8" + ) + detail_panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetDetailPanel.tsx").read_text( + encoding="utf-8" + ) - assert "
loadDatasetDetails(selectedProjectId ?? '', dataset)}" in app - assert "onClick={() => refreshMetadata(dataset.id)}" in app - assert "onClick={runRasterInspect}" in app - assert "onClick={runRasterPreview}" in app - assert "onClick={runRasterStats}" in app - assert "onClick={runRasterReproject}" in app - assert "onClick={runRasterClip}" in app - assert "onClick={runRasterTile}" in app - assert "onClick={runRasterNdvi}" in app - assert "onClick={runRasterNdwi}" in app - assert "onClick={runRasterNdbi}" in app - assert "onClick={runVectorClip}" in app - assert "onClick={runVectorBuffer}" in app - assert "onClick={() => runVectorIntersect(availableVectorTargets)}" in app + assert " runVectorIntersect(availableVectorTargets)}" in app + assert " onLoadDatasetDetails(selectedProjectId ?? '', dataset)}" in dataset_panel + assert "onClick={() => onRefreshMetadata(dataset.id)}" in dataset_panel + assert "onRunRasterInspect={onRunRasterInspect}" in detail_panel + assert "onRunVectorIntersect={onRunVectorIntersect}" in detail_panel diff --git a/backend/tests/test_sprint29_dataset_components.py b/backend/tests/test_sprint29_dataset_components.py new file mode 100644 index 00000000..a1e90dfb --- /dev/null +++ b/backend/tests/test_sprint29_dataset_components.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_app_uses_dataset_presentational_components() -> None: + app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + + assert "from './components/datasets/DatasetPanel'" in app + assert "from './components/datasets/DatasetDetailPanel'" in app + assert "Raster operations" not in app + assert "

Vector operations

" not in app + + +def test_dataset_panel_owns_upload_and_list_markup() -> None: + panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetPanel.tsx").read_text(encoding="utf-8") + + assert "Upload dataset" in panel + assert "Refresh metadata" in panel + assert "Select / details" in panel + assert "onLoadDatasetDetails" in panel + assert "onRefreshMetadata" in panel + + +def test_dataset_detail_panel_composes_raster_and_vector_controls() -> None: + detail_panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetDetailPanel.tsx").read_text( + encoding="utf-8" + ) + raster_controls = (ROOT / "frontend" / "src" / "components" / "datasets" / "RasterControls.tsx").read_text( + encoding="utf-8" + ) + vector_controls = (ROOT / "frontend" / "src" / "components" / "datasets" / "VectorControls.tsx").read_text( + encoding="utf-8" + ) + + assert "= 1024 && index < units.length - 1) { - size /= 1024 - index += 1 - } - return `${size.toFixed(1)} ${units[index]}` -} - -function formatBounds(bounds: Record | null | undefined): string { - if (!bounds) { - return 'n/a' - } - const keys = ['min_x', 'min_y', 'max_x', 'max_y'] - if (!keys.every((key) => key in bounds)) { - return 'n/a' - } - return `${bounds.min_x?.toFixed(4)}, ${bounds.min_y?.toFixed(4)} -> ${bounds.max_x?.toFixed(4)}, ${bounds.max_y?.toFixed(4)}` -} - function App(): JSX.Element { const [projects, setProjects] = useState([]) const [selectedProjectId, setSelectedProjectId] = useState(null) @@ -368,14 +345,6 @@ function App(): JSX.Element { }, [changeDetectionResult, datasetContent, detectionGeoJson, segmentationGeoJson, selectedDataset]) const mapFeatureCount = mapFeatureCollection?.features.length ?? 0 const areaFeatureCount = areaFeatureCollection?.features.length ?? 0 - const formatRasterBounds = (bounds: number[] | undefined | null): string => { - if (!bounds || bounds.length < 4) { - return 'n/a' - } - const [minX, minY, maxX, maxY] = bounds - return `${minX.toFixed(4)}, ${minY.toFixed(4)} -> ${maxX.toFixed(4)}, ${maxY.toFixed(4)}` - } - const loadProjects = async () => { setLoadingProjects(true) setErrorMessage(null) @@ -798,344 +767,74 @@ function App(): JSX.Element { ) : null} -
-

Datasets

- - - setDatasetForm((previous) => ({ ...previous, source: event.target.value }))} - placeholder="user_upload" - /> - - setDatasetForm((previous) => ({ ...previous, file: event.target.files?.[0] ?? null }))} - /> - - - - {loadingDatasets ?

Loading datasets...

: null} - {datasets.length === 0 ?

No datasets yet

: null} -
    - {datasets.map((dataset) => ( -
  • - {dataset.name} -
    type: {dataset.dataset_type}
    -
    status: {dataset.status}
    -
    readiness: {dataset.status === 'ready' ? 'ready' : dataset.status === 'failed' ? 'failed' : 'pending'}
    -
    size: {formatBytes(dataset.size_bytes)}
    -
    features: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'n/a'}
    -
    bbox: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)}
    - - -
  • - ))} -
-
+ -
-

Dataset details

- {selectedDatasetId ?

Selected dataset: {selectedDatasetId}

:

No dataset selected

} - {selectedDataset ? ( -
-

- {selectedDataset.name} -

-

Type: {selectedDataset.dataset_type}

-

Status: {selectedDataset.status}

-

Original file: {selectedDataset.original_filename ?? 'n/a'}

-

Stored file: {selectedDataset.stored_filename ?? 'n/a'}

-

Content type: {selectedDataset.content_type ?? 'n/a'}

-

File size: {formatBytes(selectedDataset.size_bytes)}

-

SHA256: {selectedDataset.checksum_sha256 ?? 'n/a'}

-

Feature count: {selectedDatasetSummary?.feature_count ?? selectedDataset.feature_count ?? 'n/a'}

-

BBox: {formatBounds(selectedDatasetSummary?.bounds_json ?? selectedDataset.bounds_json)}

- {selectedDataset.dataset_type === 'raster' ? ( -
-

Raster driver: {selectedRasterMetadata?.driver ?? 'n/a'}

-

Raster size: {selectedRasterMetadata ? `${selectedRasterMetadata.width} x ${selectedRasterMetadata.height}` : 'n/a'}

-

Raster checksum: {selectedRasterMetadata?.checksum_sha256 ?? 'n/a'}

-

- Profile: CRS {selectedRasterMetadata?.crs ?? 'n/a'} | bands {selectedRasterMetadata?.band_count ?? 'n/a'} | dtype { - (selectedRasterMetadata?.dtype as string[] | undefined)?.join(', ') ?? 'n/a'} -

-

Bounds: {formatRasterBounds(selectedRasterMetadata?.bounds)}

-

Resolution: {selectedRasterMetadata?.resolution ? selectedRasterMetadata.resolution.join(', ') : 'n/a'}

- {rasterUnavailableMessage ?

Raster unavailable: {rasterUnavailableMessage}

: null} -

Raster operations

-

Available operations: inspect, stats, reproject, preview, clip by selected area, tile generation.

-

Preview: {rasterPreview?.preview.path ?? 'not generated'}

-

Preview size: {rasterPreview?.preview.width ?? 'n/a'} x {rasterPreview?.preview.height ?? 'n/a'}

- - - - {selectedRasterStats ? ( -
-

Band statistics

-

Generated: {selectedRasterStats.generated_at ?? 'n/a'}

-
    - {selectedRasterStats.bands.map((band) => ( -
  • - Band {band.band_index}: min {band.min ?? 'n/a'}, max {band.max ?? 'n/a'}, mean {band.mean ?? 'n/a'}, std {band.std ?? 'n/a'}, - valid {band.valid_pixel_count}, nodata ratio {(band.nodata_ratio * 100).toFixed(2)}%, dtype {band.dtype ?? 'n/a'} -
  • - ))} -
-
- ) : null} -
- - - -
-
- - - {areas.length === 0 ?

Create an area before raster clipping.

: null} -
-
- - - - - {!isRasterTileInputValid ? ( -

- Tile size must be {'>'} 0 and overlap must be {'>='} 0 and smaller than tile size. -

- ) : null} -
-

Spectral indices

-
-

Use available band indexes from the raster file (1-based).

-
-

NDVI

- - - -
-
-

NDWI

- - - -
-
-

NDBI

- - - -
-
-
- ) : null} - - {isVectorDatasetType(selectedDataset.dataset_type) ? ( -
-

Vector operations

-
- - -
- -
- - -
-
- ) : null} - -

Jobs

- {jobs.length === 0 ?

No jobs yet.

: null} -
    - {jobs.map((job) => ( -
  • -
    - {job.job_type} · {job.status} -
    - {job.result_json ? ( -
    {JSON.stringify(job.result_json, null, 2)}
    - ) : null} - {job.error_message ?
    error: {job.error_message}
    : null} - {job.result_json?.output_dataset_id ? ( - - ) : null} -
  • - ))} -
-
- ) : null} - {loadingDatasetDetails ?

Loading dataset details...

: null} - {datasetDetailError ?

Dataset detail error: {datasetDetailError}

: null} -
+ runVectorIntersect(availableVectorTargets)} + onPickDerivedDataset={pickDerivedDataset} + />

Map workspace

diff --git a/frontend/src/components/datasets/DatasetDetailPanel.tsx b/frontend/src/components/datasets/DatasetDetailPanel.tsx new file mode 100644 index 00000000..a172d8df --- /dev/null +++ b/frontend/src/components/datasets/DatasetDetailPanel.tsx @@ -0,0 +1,252 @@ +import type { + AreaRead, + DatasetCreateResponse, + JobRead, + RasterMetadataResponse, + RasterPreviewResponse, + RasterStatsResponse, + VectorSummary, +} from '../../types' +import { RasterControls } from './RasterControls' +import { VectorControls } from './VectorControls' + +interface DatasetDetailPanelProps { + areas: AreaRead[] + availableVectorTargets: DatasetCreateResponse[] + selectedDatasetId: string | null + selectedDataset: DatasetCreateResponse | null + selectedDatasetSummary: VectorSummary | null + selectedRasterMetadata: RasterMetadataResponse | null + selectedRasterStats: RasterStatsResponse | null + rasterPreview: RasterPreviewResponse | null + rasterUnavailableMessage: string | null + selectedClipAreaId: string + selectedIntersectTargetId: string + rasterTileSize: number + rasterTileOverlap: number + rasterTileOutputName: string + rasterReprojectCrs: string + rasterReprojectResampling: string + ndviNirBand: number + ndviRedBand: number + ndwiGreenBand: number + ndwiNirBand: number + ndbiSwirBand: number + ndbiNirBand: number + isRasterTileInputValid: boolean + jobs: JobRead[] + loadingDatasetDetails: boolean + datasetDetailError: string | null + isVectorDatasetType: (datasetType: string) => boolean + onSetSelectedClipAreaId: (value: string) => void + onSetSelectedIntersectTargetId: (value: string) => void + onSetRasterTileSize: (value: number) => void + onSetRasterTileOverlap: (value: number) => void + onSetRasterTileOutputName: (value: string) => void + onSetRasterReprojectCrs: (value: string) => void + onSetRasterReprojectResampling: (value: string) => void + onSetNdviNirBand: (value: number) => void + onSetNdviRedBand: (value: number) => void + onSetNdwiGreenBand: (value: number) => void + onSetNdwiNirBand: (value: number) => void + onSetNdbiSwirBand: (value: number) => void + onSetNdbiNirBand: (value: number) => void + onRunRasterInspect: () => void + onRunRasterPreview: () => void + onRunRasterStats: () => void + onRunRasterReproject: () => void + onRunRasterClip: () => void + onRunRasterTile: () => void + onRunRasterNdvi: () => void + onRunRasterNdwi: () => void + onRunRasterNdbi: () => void + onRunVectorClip: () => void + onRunVectorBuffer: () => void + onRunVectorIntersect: () => void + onPickDerivedDataset: (datasetId: string) => void +} + +function formatBytes(value: number | null | undefined): string { + if (!value && value !== 0) { + return 'n/a' + } + const units = ['B', 'KB', 'MB', 'GB'] + let size = value + let index = 0 + while (size >= 1024 && index < units.length - 1) { + size /= 1024 + index += 1 + } + return `${size.toFixed(1)} ${units[index]}` +} + +function formatBounds(bounds: Record | null | undefined): string { + if (!bounds) { + return 'n/a' + } + const keys = ['min_x', 'min_y', 'max_x', 'max_y'] + if (!keys.every((key) => key in bounds)) { + return 'n/a' + } + return `${bounds.min_x?.toFixed(4)}, ${bounds.min_y?.toFixed(4)} -> ${bounds.max_x?.toFixed(4)}, ${bounds.max_y?.toFixed(4)}` +} + +export function DatasetDetailPanel({ + areas, + availableVectorTargets, + selectedDatasetId, + selectedDataset, + selectedDatasetSummary, + selectedRasterMetadata, + selectedRasterStats, + rasterPreview, + rasterUnavailableMessage, + selectedClipAreaId, + selectedIntersectTargetId, + rasterTileSize, + rasterTileOverlap, + rasterTileOutputName, + rasterReprojectCrs, + rasterReprojectResampling, + ndviNirBand, + ndviRedBand, + ndwiGreenBand, + ndwiNirBand, + ndbiSwirBand, + ndbiNirBand, + isRasterTileInputValid, + jobs, + loadingDatasetDetails, + datasetDetailError, + isVectorDatasetType, + onSetSelectedClipAreaId, + onSetSelectedIntersectTargetId, + onSetRasterTileSize, + onSetRasterTileOverlap, + onSetRasterTileOutputName, + onSetRasterReprojectCrs, + onSetRasterReprojectResampling, + onSetNdviNirBand, + onSetNdviRedBand, + onSetNdwiGreenBand, + onSetNdwiNirBand, + onSetNdbiSwirBand, + onSetNdbiNirBand, + onRunRasterInspect, + onRunRasterPreview, + onRunRasterStats, + onRunRasterReproject, + onRunRasterClip, + onRunRasterTile, + onRunRasterNdvi, + onRunRasterNdwi, + onRunRasterNdbi, + onRunVectorClip, + onRunVectorBuffer, + onRunVectorIntersect, + onPickDerivedDataset, +}: DatasetDetailPanelProps) { + return ( +
+

Dataset details

+ {selectedDatasetId ?

Selected dataset: {selectedDatasetId}

:

No dataset selected

} + {selectedDataset ? ( +
+

+ {selectedDataset.name} +

+

Type: {selectedDataset.dataset_type}

+

Status: {selectedDataset.status}

+

Original file: {selectedDataset.original_filename ?? 'n/a'}

+

Stored file: {selectedDataset.stored_filename ?? 'n/a'}

+

Content type: {selectedDataset.content_type ?? 'n/a'}

+

File size: {formatBytes(selectedDataset.size_bytes)}

+

SHA256: {selectedDataset.checksum_sha256 ?? 'n/a'}

+

Feature count: {selectedDatasetSummary?.feature_count ?? selectedDataset.feature_count ?? 'n/a'}

+

BBox: {formatBounds(selectedDatasetSummary?.bounds_json ?? selectedDataset.bounds_json)}

+ {selectedDataset.dataset_type === 'raster' ? ( + + ) : null} + + {isVectorDatasetType(selectedDataset.dataset_type) ? ( + + ) : null} + +

Jobs

+ {jobs.length === 0 ?

No jobs yet.

: null} +
    + {jobs.map((job) => ( +
  • +
    + {job.job_type} - {job.status} +
    + {job.result_json ? ( +
    {JSON.stringify(job.result_json, null, 2)}
    + ) : null} + {job.error_message ?
    error: {job.error_message}
    : null} + {job.result_json?.output_dataset_id ? ( + + ) : null} +
  • + ))} +
+
+ ) : null} + {loadingDatasetDetails ?

Loading dataset details...

: null} + {datasetDetailError ?

Dataset detail error: {datasetDetailError}

: null} +
+ ) +} diff --git a/frontend/src/components/datasets/DatasetPanel.tsx b/frontend/src/components/datasets/DatasetPanel.tsx new file mode 100644 index 00000000..cf9d27db --- /dev/null +++ b/frontend/src/components/datasets/DatasetPanel.tsx @@ -0,0 +1,129 @@ +import type { Dispatch, FormEvent, SetStateAction } from 'react' +import type { AreaRead, DatasetCreateResponse } from '../../types' + +interface DatasetFormState { + datasetType: string + source: string + datasetRole: string + sourceName: string + referenceLayerName: string + sourceMetadataJson: string + provenanceMetadataJson: string + areaId: string + file: File | null +} + +interface DatasetPanelProps { + selectedProjectId: string | null + areas: AreaRead[] + datasets: DatasetCreateResponse[] + datasetForm: DatasetFormState + loadingDatasets: boolean + onDatasetFormChange: Dispatch> + onUploadDataset: (event: FormEvent) => void + onLoadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => void + onRefreshMetadata: (datasetId: string) => void +} + +function formatBytes(value: number | null | undefined): string { + if (!value && value !== 0) { + return 'n/a' + } + const units = ['B', 'KB', 'MB', 'GB'] + let size = value + let index = 0 + while (size >= 1024 && index < units.length - 1) { + size /= 1024 + index += 1 + } + return `${size.toFixed(1)} ${units[index]}` +} + +function formatBounds(bounds: Record | null | undefined): string { + if (!bounds) { + return 'n/a' + } + const keys = ['min_x', 'min_y', 'max_x', 'max_y'] + if (!keys.every((key) => key in bounds)) { + return 'n/a' + } + return `${bounds.min_x?.toFixed(4)}, ${bounds.min_y?.toFixed(4)} -> ${bounds.max_x?.toFixed(4)}, ${bounds.max_y?.toFixed(4)}` +} + +export function DatasetPanel({ + selectedProjectId, + areas, + datasets, + datasetForm, + loadingDatasets, + onDatasetFormChange, + onUploadDataset, + onLoadDatasetDetails, + onRefreshMetadata, +}: DatasetPanelProps) { + return ( +
+

Datasets

+
+ + onDatasetFormChange((previous) => ({ ...previous, source: event.target.value }))} + placeholder="user_upload" + /> + + onDatasetFormChange((previous) => ({ ...previous, file: event.target.files?.[0] ?? null }))} + /> + +
+ + {loadingDatasets ?

Loading datasets...

: null} + {datasets.length === 0 ?

No datasets yet

: null} +
    + {datasets.map((dataset) => ( +
  • + {dataset.name} +
    type: {dataset.dataset_type}
    +
    status: {dataset.status}
    +
    readiness: {dataset.status === 'ready' ? 'ready' : dataset.status === 'failed' ? 'failed' : 'pending'}
    +
    size: {formatBytes(dataset.size_bytes)}
    +
    features: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'n/a'}
    +
    bbox: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)}
    + + +
  • + ))} +
+
+ ) +} diff --git a/frontend/src/components/datasets/RasterControls.tsx b/frontend/src/components/datasets/RasterControls.tsx new file mode 100644 index 00000000..e5e98a58 --- /dev/null +++ b/frontend/src/components/datasets/RasterControls.tsx @@ -0,0 +1,256 @@ +import type { AreaRead, RasterMetadataResponse, RasterPreviewResponse, RasterStatsResponse } from '../../types' + +interface RasterControlsProps { + areas: AreaRead[] + selectedDatasetId: string | null + selectedRasterMetadata: RasterMetadataResponse | null + selectedRasterStats: RasterStatsResponse | null + rasterPreview: RasterPreviewResponse | null + rasterUnavailableMessage: string | null + selectedClipAreaId: string + rasterTileSize: number + rasterTileOverlap: number + rasterTileOutputName: string + rasterReprojectCrs: string + rasterReprojectResampling: string + ndviNirBand: number + ndviRedBand: number + ndwiGreenBand: number + ndwiNirBand: number + ndbiSwirBand: number + ndbiNirBand: number + isRasterTileInputValid: boolean + onSetSelectedClipAreaId: (value: string) => void + onSetRasterTileSize: (value: number) => void + onSetRasterTileOverlap: (value: number) => void + onSetRasterTileOutputName: (value: string) => void + onSetRasterReprojectCrs: (value: string) => void + onSetRasterReprojectResampling: (value: string) => void + onSetNdviNirBand: (value: number) => void + onSetNdviRedBand: (value: number) => void + onSetNdwiGreenBand: (value: number) => void + onSetNdwiNirBand: (value: number) => void + onSetNdbiSwirBand: (value: number) => void + onSetNdbiNirBand: (value: number) => void + onRunRasterInspect: () => void + onRunRasterPreview: () => void + onRunRasterStats: () => void + onRunRasterReproject: () => void + onRunRasterClip: () => void + onRunRasterTile: () => void + onRunRasterNdvi: () => void + onRunRasterNdwi: () => void + onRunRasterNdbi: () => void +} + +function formatRasterBounds(bounds: number[] | undefined | null): string { + if (!bounds || bounds.length < 4) { + return 'n/a' + } + const [minX, minY, maxX, maxY] = bounds + return `${minX.toFixed(4)}, ${minY.toFixed(4)} -> ${maxX.toFixed(4)}, ${maxY.toFixed(4)}` +} + +export function RasterControls({ + areas, + selectedDatasetId, + selectedRasterMetadata, + selectedRasterStats, + rasterPreview, + rasterUnavailableMessage, + selectedClipAreaId, + rasterTileSize, + rasterTileOverlap, + rasterTileOutputName, + rasterReprojectCrs, + rasterReprojectResampling, + ndviNirBand, + ndviRedBand, + ndwiGreenBand, + ndwiNirBand, + ndbiSwirBand, + ndbiNirBand, + isRasterTileInputValid, + onSetSelectedClipAreaId, + onSetRasterTileSize, + onSetRasterTileOverlap, + onSetRasterTileOutputName, + onSetRasterReprojectCrs, + onSetRasterReprojectResampling, + onSetNdviNirBand, + onSetNdviRedBand, + onSetNdwiGreenBand, + onSetNdwiNirBand, + onSetNdbiSwirBand, + onSetNdbiNirBand, + onRunRasterInspect, + onRunRasterPreview, + onRunRasterStats, + onRunRasterReproject, + onRunRasterClip, + onRunRasterTile, + onRunRasterNdvi, + onRunRasterNdwi, + onRunRasterNdbi, +}: RasterControlsProps) { + return ( +
+

Raster driver: {selectedRasterMetadata?.driver ?? 'n/a'}

+

Raster size: {selectedRasterMetadata ? `${selectedRasterMetadata.width} x ${selectedRasterMetadata.height}` : 'n/a'}

+

Raster checksum: {selectedRasterMetadata?.checksum_sha256 ?? 'n/a'}

+

+ Profile: CRS {selectedRasterMetadata?.crs ?? 'n/a'} | bands {selectedRasterMetadata?.band_count ?? 'n/a'} | dtype { + (selectedRasterMetadata?.dtype as string[] | undefined)?.join(', ') ?? 'n/a'} +

+

Bounds: {formatRasterBounds(selectedRasterMetadata?.bounds)}

+

Resolution: {selectedRasterMetadata?.resolution ? selectedRasterMetadata.resolution.join(', ') : 'n/a'}

+ {rasterUnavailableMessage ?

Raster unavailable: {rasterUnavailableMessage}

: null} +

Raster operations

+

Available operations: inspect, stats, reproject, preview, clip by selected area, tile generation.

+

Preview: {rasterPreview?.preview.path ?? 'not generated'}

+

Preview size: {rasterPreview?.preview.width ?? 'n/a'} x {rasterPreview?.preview.height ?? 'n/a'}

+ + + + {selectedRasterStats ? ( +
+

Band statistics

+

Generated: {selectedRasterStats.generated_at ?? 'n/a'}

+
    + {selectedRasterStats.bands.map((band) => ( +
  • + Band {band.band_index}: min {band.min ?? 'n/a'}, max {band.max ?? 'n/a'}, mean {band.mean ?? 'n/a'}, std {band.std ?? 'n/a'}, + valid {band.valid_pixel_count}, nodata ratio {(band.nodata_ratio * 100).toFixed(2)}%, dtype {band.dtype ?? 'n/a'} +
  • + ))} +
+
+ ) : null} +
+ + + +
+
+ + + {areas.length === 0 ?

Create an area before raster clipping.

: null} +
+
+ + + + + {!isRasterTileInputValid ? ( +

+ Tile size must be {'>'} 0 and overlap must be {'>='} 0 and smaller than tile size. +

+ ) : null} +
+

Spectral indices

+
+

Use available band indexes from the raster file (1-based).

+
+

NDVI

+ + + +
+
+

NDWI

+ + + +
+
+

NDBI

+ + + +
+
+
+ ) +} diff --git a/frontend/src/components/datasets/VectorControls.tsx b/frontend/src/components/datasets/VectorControls.tsx new file mode 100644 index 00000000..16787bab --- /dev/null +++ b/frontend/src/components/datasets/VectorControls.tsx @@ -0,0 +1,65 @@ +import type { AreaRead, DatasetCreateResponse } from '../../types' + +interface VectorControlsProps { + areas: AreaRead[] + availableVectorTargets: DatasetCreateResponse[] + selectedClipAreaId: string + selectedIntersectTargetId: string + onSetSelectedClipAreaId: (value: string) => void + onSetSelectedIntersectTargetId: (value: string) => void + onRunVectorClip: () => void + onRunVectorBuffer: () => void + onRunVectorIntersect: () => void +} + +export function VectorControls({ + areas, + availableVectorTargets, + selectedClipAreaId, + selectedIntersectTargetId, + onSetSelectedClipAreaId, + onSetSelectedIntersectTargetId, + onRunVectorClip, + onRunVectorBuffer, + onRunVectorIntersect, +}: VectorControlsProps) { + return ( +
+

Vector operations

+
+ + +
+ +
+ + +
+
+ ) +}