Scale regional data catalogs
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-17 16:39:28 +02:00
parent 132c4feed8
commit 6fd3ed49b2
9 changed files with 321 additions and 31 deletions
@@ -1,7 +1,9 @@
import type { Dispatch, FormEvent, SetStateAction } from 'react'
import { useEffect, useMemo, useState, type Dispatch, type FormEvent, type SetStateAction } from 'react'
import type { AreaRead, DatasetCreateResponse } from '../../types'
import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay'
const DATASET_CATALOG_PAGE_SIZE = 10
interface DatasetFormState {
datasetType: string
source: string
@@ -123,6 +125,9 @@ export function DatasetPanel({
onOpenDatasetInMap,
onOpenDatasetExport,
}: DatasetPanelProps) {
const [historyOpen, setHistoryOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [page, setPage] = useState(1)
const selectedDataset = datasets.find((dataset) => dataset.id === selectedDatasetId)
const latestDatasetBySeries = new Map<string, DatasetCreateResponse>()
datasets.forEach((dataset) => {
@@ -140,6 +145,27 @@ export function DatasetPanel({
const historicalDatasets = datasets.filter(
(dataset) => dataset.temporal_series_key && latestDatasetBySeries.get(dataset.temporal_series_key)?.id !== dataset.id,
)
const filteredPrimaryDatasets = useMemo(() => {
const query = searchQuery.trim().toLocaleLowerCase('nl-BE')
if (!query) return primaryDatasets
return primaryDatasets.filter((dataset) => {
const searchable = [
getDatasetDisplayName(dataset),
getDatasetSourceDisplayName(dataset),
dataset.source,
dataset.source_name,
dataset.reference_layer_name,
String(dataset.source_metadata?.['municipality'] ?? ''),
datasetRoleLabel(normalizeDatasetRole(dataset)),
]
return searchable.some((value) => String(value ?? '').toLocaleLowerCase('nl-BE').includes(query))
})
}, [primaryDatasets, searchQuery])
const pageCount = Math.max(1, Math.ceil(filteredPrimaryDatasets.length / DATASET_CATALOG_PAGE_SIZE))
const visiblePrimaryDatasets = filteredPrimaryDatasets.slice(
(page - 1) * DATASET_CATALOG_PAGE_SIZE,
page * DATASET_CATALOG_PAGE_SIZE,
)
const readyDatasets = primaryDatasets.filter((dataset) => dataset.status === 'ready').length
const roleSummaries = [
{
@@ -172,6 +198,16 @@ export function DatasetPanel({
},
]
useEffect(() => {
setPage((current) => Math.min(current, pageCount))
}, [pageCount])
useEffect(() => {
setSearchQuery('')
setPage(1)
setHistoryOpen(false)
}, [selectedProjectId])
return (
<section className="workspace-panel" data-testid="dataset-panel">
<div className="panel-title-row">
@@ -257,8 +293,42 @@ export function DatasetPanel({
) : null}
<div className="data-panel-list-block dataset-catalog-block">
<p className="data-section-label">Beschikbare bronnen</p>
<div className="catalog-browser-toolbar catalog-browser-toolbar-datasets">
<label className="catalog-search-field">
Zoek in beschikbare bronnen
<input
type="search"
value={searchQuery}
onChange={(event) => {
setSearchQuery(event.target.value)
setPage(1)
}}
placeholder="Naam, bron, gemeente of type"
/>
</label>
<div className="catalog-pagination" aria-label="Paginering van bronnen">
<span>{filteredPrimaryDatasets.length} van {primaryDatasets.length}</span>
<button
className="secondary-action"
type="button"
disabled={page === 1}
onClick={() => setPage((current) => Math.max(1, current - 1))}
>
Vorige
</button>
<strong>{page} / {pageCount}</strong>
<button
className="secondary-action"
type="button"
disabled={page === pageCount}
onClick={() => setPage((current) => Math.min(pageCount, current + 1))}
>
Volgende
</button>
</div>
</div>
<ul className="dataset-list">
{primaryDatasets.map((dataset) => {
{visiblePrimaryDatasets.map((dataset) => {
const datasetRole = normalizeDatasetRole(dataset)
const roleLabel = datasetRoleLabel(datasetRole)
return (
@@ -337,13 +407,19 @@ export function DatasetPanel({
)
})}
</ul>
{visiblePrimaryDatasets.length === 0 && datasets.length > 0 ? (
<p className="muted">Geen bron gevonden voor deze zoekterm.</p>
) : null}
{historicalDatasets.length > 0 ? (
<details className="dataset-history-disclosure">
<details
className="dataset-history-disclosure"
onToggle={(event) => setHistoryOpen(event.currentTarget.open)}
>
<summary>
<span>Historische meetmomenten</span>
<strong>{historicalDatasets.length} oudere lagen</strong>
</summary>
<ul className="dataset-history-list">
{historyOpen ? <ul className="dataset-history-list">
{historicalDatasets.map((dataset) => (
<li key={dataset.id}>
<div>
@@ -356,7 +432,7 @@ export function DatasetPanel({
</div>
</li>
))}
</ul>
</ul> : null}
</details>
) : null}
</div>
+91 -21
View File
@@ -1,6 +1,8 @@
import type { FormEvent } from 'react'
import { useEffect, useMemo, useState, type FormEvent } from 'react'
import type { AreaRead, ProjectRead } from '../../types'
const AREA_CATALOG_PAGE_SIZE = 12
function projectDisplayName(project: ProjectRead | null): string {
if (project?.name === 'Kempen Regional Workbench') {
return 'Kempen · volledige regio'
@@ -40,7 +42,30 @@ export function AreaPanel({
onUpdateAreaForm,
onSelectMapArea,
}: AreaPanelProps): JSX.Element {
const [catalogOpen, setCatalogOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [page, setPage] = useState(1)
const selectedArea = areas.find((area) => area.id === selectedMapAreaId)
const filteredAreas = useMemo(() => {
const query = searchQuery.trim().toLocaleLowerCase('nl-BE')
if (!query) return areas
return areas.filter((area) => area.name.toLocaleLowerCase('nl-BE').includes(query))
}, [areas, searchQuery])
const pageCount = Math.max(1, Math.ceil(filteredAreas.length / AREA_CATALOG_PAGE_SIZE))
const visibleAreas = filteredAreas.slice(
(page - 1) * AREA_CATALOG_PAGE_SIZE,
page * AREA_CATALOG_PAGE_SIZE,
)
useEffect(() => {
setPage((current) => Math.min(current, pageCount))
}, [pageCount])
useEffect(() => {
setSearchQuery('')
setPage(1)
setCatalogOpen(false)
}, [selectedProjectId])
return (
<section data-testid="area-panel">
@@ -94,28 +119,73 @@ export function AreaPanel({
{loadingAreas ? <p>Gebieden laden...</p> : null}
{areas.length === 0 ? <p>Nog geen gebieden beschikbaar.</p> : null}
<details className="data-panel-list-block area-catalog-disclosure">
<details
className="data-panel-list-block area-catalog-disclosure"
onToggle={(event) => setCatalogOpen(event.currentTarget.open)}
>
<summary>{areas.length} beschikbare gemeenten en regiogrenzen</summary>
<ul className="entity-list">
{areas.map((area) => (
<li className={selectedMapAreaId === area.id ? 'entity-card entity-card-active' : 'entity-card'} key={area.id}>
<strong>{area.name}</strong>
<div className="entity-meta">
<span>{area.area_m2 ? `${(area.area_m2 / 1_000_000).toLocaleString('nl-BE', { maximumFractionDigits: 2 })} km2` : 'Oppervlakte onbekend'}</span>
<span>{area.geometry?.type ?? area.geometry_type ?? 'Geometrie onbekend'}</span>
{catalogOpen ? (
<>
<div className="catalog-browser-toolbar">
<label className="catalog-search-field">
Zoek gemeente of regio
<input
type="search"
value={searchQuery}
onChange={(event) => {
setSearchQuery(event.target.value)
setPage(1)
}}
placeholder="Bijvoorbeeld Mol"
/>
</label>
<div className="catalog-pagination" aria-label="Paginering van gebieden">
<span>{filteredAreas.length} gevonden</span>
<button
className="secondary-action"
type="button"
disabled={page === 1}
onClick={() => setPage((current) => Math.max(1, current - 1))}
>
Vorige
</button>
<strong>{page} / {pageCount}</strong>
<button
className="secondary-action"
type="button"
disabled={page === pageCount}
onClick={() => setPage((current) => Math.min(pageCount, current + 1))}
>
Volgende
</button>
</div>
<button
className="secondary-action"
type="button"
onClick={() => onSelectMapArea(area.id)}
disabled={!area.geometry}
data-testid={`area-show-${area.id}`}
>
{selectedMapAreaId === area.id ? 'Op kaart' : 'Toon op kaart'}
</button>
</li>
))}
</ul>
</div>
{visibleAreas.length > 0 ? (
<ul className="entity-list">
{visibleAreas.map((area) => (
<li className={selectedMapAreaId === area.id ? 'entity-card entity-card-active' : 'entity-card'} key={area.id}>
<strong>{area.name}</strong>
<div className="entity-meta">
<span>{area.area_m2 ? `${(area.area_m2 / 1_000_000).toLocaleString('nl-BE', { maximumFractionDigits: 2 })} km2` : 'Oppervlakte onbekend'}</span>
<span>{area.geometry?.type ?? area.geometry_type ?? 'Geometrie onbekend'}</span>
</div>
<button
className="secondary-action"
type="button"
onClick={() => onSelectMapArea(area.id)}
disabled={!area.geometry}
data-testid={`area-show-${area.id}`}
>
{selectedMapAreaId === area.id ? 'Op kaart' : 'Toon op kaart'}
</button>
</li>
))}
</ul>
) : (
<p className="muted">Geen gebied gevonden voor deze zoekterm.</p>
)}
</>
) : null}
</details>
</section>
)