From aca0d5e398392faf0022bf7b0ed89ef363c7d375 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 27 Nov 2025 14:32:02 +0100 Subject: [PATCH 01/70] Add radio group component --- yarn.lock | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/yarn.lock b/yarn.lock index 280da2c28..d122eb12c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3310,6 +3310,34 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-radio-group@npm:^1.3.8": + version: 1.3.8 + resolution: "@radix-ui/react-radio-group@npm:1.3.8" + dependencies: + "@radix-ui/primitive": 1.1.3 + "@radix-ui/react-compose-refs": 1.1.2 + "@radix-ui/react-context": 1.1.2 + "@radix-ui/react-direction": 1.1.1 + "@radix-ui/react-presence": 1.1.5 + "@radix-ui/react-primitive": 2.1.3 + "@radix-ui/react-roving-focus": 1.1.11 + "@radix-ui/react-use-controllable-state": 1.2.2 + "@radix-ui/react-use-previous": 1.1.1 + "@radix-ui/react-use-size": 1.1.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 5e35970ec8965f398f15ff9b7d52c74125119d4af303fe7cd0c91f95d00692e4630d3db01e068eba833669dfb813a14b06cd80587acde028bd4cd747cb2aff41 + languageName: node + linkType: hard + "@radix-ui/react-roving-focus@npm:1.1.11": version: 1.1.11 resolution: "@radix-ui/react-roving-focus@npm:1.1.11" From c14766fee592b8cfffd8af63ab40ce6f7d21ff23 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 27 Nov 2025 14:50:44 +0100 Subject: [PATCH 02/70] Add Command and Dialog --- packages/base/style/base.css | 1 + yarn.lock | 30 +----------------------------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/packages/base/style/base.css b/packages/base/style/base.css index 1a6bf1c58..9b639fb68 100644 --- a/packages/base/style/base.css +++ b/packages/base/style/base.css @@ -24,6 +24,7 @@ @import url('./shared/radioGroup.css'); @import url('./shared/dialog.css'); @import url('./shared/switch.css'); +@import url('./shared/dialog.css'); .errors { color: var(--jp-warn-color0); diff --git a/yarn.lock b/yarn.lock index d122eb12c..7218c1b5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3310,34 +3310,6 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-radio-group@npm:^1.3.8": - version: 1.3.8 - resolution: "@radix-ui/react-radio-group@npm:1.3.8" - dependencies: - "@radix-ui/primitive": 1.1.3 - "@radix-ui/react-compose-refs": 1.1.2 - "@radix-ui/react-context": 1.1.2 - "@radix-ui/react-direction": 1.1.1 - "@radix-ui/react-presence": 1.1.5 - "@radix-ui/react-primitive": 2.1.3 - "@radix-ui/react-roving-focus": 1.1.11 - "@radix-ui/react-use-controllable-state": 1.2.2 - "@radix-ui/react-use-previous": 1.1.1 - "@radix-ui/react-use-size": 1.1.1 - peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - "@types/react": - optional: true - "@types/react-dom": - optional: true - checksum: 5e35970ec8965f398f15ff9b7d52c74125119d4af303fe7cd0c91f95d00692e4630d3db01e068eba833669dfb813a14b06cd80587acde028bd4cd747cb2aff41 - languageName: node - linkType: hard - "@radix-ui/react-roving-focus@npm:1.1.11": version: 1.1.11 resolution: "@radix-ui/react-roving-focus@npm:1.1.11" @@ -3380,7 +3352,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-slot@npm:1.2.4, @radix-ui/react-slot@npm:^1.2.3": +"@radix-ui/react-slot@npm:1.2.4": version: 1.2.4 resolution: "@radix-ui/react-slot@npm:1.2.4" dependencies: From 23b7019ef018a89aeaf4105c7db6586c58a695e7 Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 26 Nov 2025 17:03:14 +0100 Subject: [PATCH 03/70] playing --- .eslintrc.js | 2 +- packages/base/src/mainview/mainView.tsx | 3 + packages/base/src/panelview/leftpanel.tsx | 9 +- .../stacBrowser/components/QueryableCombo.tsx | 99 +++++++ .../components/StacGenericFilterPanel.tsx | 254 ++++++++++++++++++ .../src/stacBrowser/components/StacPanel.tsx | 48 +++- .../{ => geodes}/StacFilterSection.tsx | 0 .../StacGeodesFilterPanel.tsx} | 19 +- .../shared/StacCheckboxWithLabel.tsx | 26 ++ .../shared/StacQueryableFilterList.tsx | 103 +++++++ .../shared/StacSearchDatePicker.tsx | 64 +++++ tsconfigbase.json | 2 +- 12 files changed, 604 insertions(+), 25 deletions(-) create mode 100644 packages/base/src/stacBrowser/components/QueryableCombo.tsx create mode 100644 packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx rename packages/base/src/stacBrowser/components/{ => geodes}/StacFilterSection.tsx (100%) rename packages/base/src/stacBrowser/components/{StacPanelFilters.tsx => geodes/StacGeodesFilterPanel.tsx} (91%) create mode 100644 packages/base/src/stacBrowser/components/shared/StacCheckboxWithLabel.tsx create mode 100644 packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx create mode 100644 packages/base/src/stacBrowser/components/shared/StacSearchDatePicker.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 570699b3f..d0f680c68 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -58,7 +58,7 @@ module.exports = { } ], "prefer-arrow-callback": "error", - "no-console": ["error", {"allow": ["error", "warn", "debug"]}], + // "no-console": ["error", {"allow": ["error", "warn", "debug"]}], "no-duplicate-imports": "error", }, }; diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 2fdab03f7..bc3dc906b 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -1111,6 +1111,9 @@ export class MainView extends React.Component { data: layerParameters.data, opacity: layerParameters.opacity, visible: layer.visible, + // ! is passing the entire assets too much? + // Ideally we could parse the metadata to add a full blown layer if possivle + // and only use this layer type for overviews assets: Object.keys(layerParameters.data.assets), extent: layerParameters.data.bbox, }); diff --git a/packages/base/src/panelview/leftpanel.tsx b/packages/base/src/panelview/leftpanel.tsx index 2c6000652..e0ce4431d 100644 --- a/packages/base/src/panelview/leftpanel.tsx +++ b/packages/base/src/panelview/leftpanel.tsx @@ -16,8 +16,10 @@ import { TabsList, TabsTrigger, } from '../shared/components/Tabs'; -import StacPanel from '../stacBrowser/components/StacPanel'; +// import StacPanel from '../stacBrowser/components/StacPanel'; import FilterComponent from './components/filter-panel/Filter'; +import StacGenericFilterPanel from '../stacBrowser/components/StacGenericFilterPanel'; +import StacPanel from '../stacBrowser/components/StacPanel'; export interface ILeftPanelClickHandlerParams { type: SelectionType; @@ -208,6 +210,11 @@ export const LeftPanel: React.FC = ( > )} + {!settings.stacBrowserDisabled && ( + + + + )} {!settings.stacBrowserDisabled && ( diff --git a/packages/base/src/stacBrowser/components/QueryableCombo.tsx b/packages/base/src/stacBrowser/components/QueryableCombo.tsx new file mode 100644 index 000000000..ec43bbee2 --- /dev/null +++ b/packages/base/src/stacBrowser/components/QueryableCombo.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'; +import * as React from 'react'; + +import { Button } from '@/src/shared/components/Button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/src/shared/components/Command'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/src/shared/components/Popover'; +import { cn } from '@/src/shared/components/utils'; + +const queryables = [ + { + value: 'next.js', + label: 'Next.js', + }, + { + value: 'sveltekit', + label: 'SvelteKit', + }, + { + value: 'nuxt.js', + label: 'Nuxt.js', + }, + { + value: 'remix', + label: 'Remix', + }, + { + value: 'astro', + label: 'Astro', + }, +]; + +interface IQueryableComboProps { + queryables: [string, any][]; +} + +export function QueryableCombo({ queryables }: IQueryableComboProps) { + const [open, setOpen] = React.useState(false); + const [value, setValue] = React.useState(''); + + return ( + + + + + + + + + No queryable found. + + {queryables.map(([key, val]) => ( + { + setValue(currentValue === value ? '' : currentValue); + setOpen(false); + }} + > + + {val.title} + + ))} + + + + + + ); +} diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx new file mode 100644 index 000000000..cb63349cf --- /dev/null +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -0,0 +1,254 @@ +import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; +import { UUID } from '@lumino/coreutils'; +import { + endOfToday, + endOfTomorrow, + startOfToday, + startOfYesterday, +} from 'date-fns'; +import React, { useEffect, useState } from 'react'; + +import { fetchWithProxies } from '@/src/tools'; +import StacCheckboxWithLabel from './shared/StacCheckboxWithLabel'; +import StacQueryableFilterList from './shared/StacQueryableFilterList'; +import StacSearchDatePicker from './shared/StacSearchDatePicker'; +import useStacSearch from '../hooks/useStacSearch'; +import { IStacAsset, IStacCollection } from '../types/types'; + +interface IStacBrowser2Props { + model?: IJupyterGISModel; +} + +type FilteredCollection = Pick; + +// { +// title?: string; +// id: string; +// }; + +const API_URL = 'https://stac.dataspace.copernicus.eu/v1/'; +// This is a generic UI for apis that support filter extension +function StacGenericFilterPanel({ model }: IStacBrowser2Props) { + const [queryableProps, setQueryableProps] = useState<[string, any][]>(); + const [collections, setCollections] = useState([]); + // temp + const [selectedCollection, setSelectedCollection] = useState(''); + const [currentBBox, setCurrentBBox] = useState< + [number, number, number, number] + >([-180, -90, 180, 90]); + + const { + startTime, + endTime, + setStartTime, + setEndTime, + useWorldBBox, + setUseWorldBBox, + } = useStacSearch({ + model, + }); + + if (!model) { + console.log('no model'); + return; + } + + // for collections + useEffect(() => { + const fatch = async () => { + const data = await fetchWithProxies( + API_URL + 'collections', + model, + async response => await response.json(), + undefined, + 'internal', + ); + + const collections: FilteredCollection[] = data.collections + .map((collection: any) => ({ + title: collection.title ?? collection.id, + id: collection.id, + })) + .sort((a: FilteredCollection, b: FilteredCollection) => { + const titleA = a.title?.toLowerCase() ?? ''; + const titleB = b.title?.toLowerCase() ?? ''; + return titleA.localeCompare(titleB); + }); + + console.log('collections', collections); + setCollections(collections); + }; + + fatch(); + }, []); + + // for queryables + // should listen for colletion changes and requery + // need a way to handle querying multiple collections without refetching everything + // collection id -> queryables map as a basic cache thing?? + useEffect(() => { + const fatch = async () => { + const data = await fetchWithProxies( + API_URL + 'queryables', + model, + async response => await response.json(), + undefined, + 'internal', + ); + + setQueryableProps(Object.entries(data.properties)); + }; + + fatch(); + }, []); + + useEffect(() => { + const listenToModel = ( + sender: IJupyterGISModel, + bBoxIn4326: [number, number, number, number], + ) => { + if (useWorldBBox) { + setCurrentBBox([-180, -90, 180, 90]); + } else { + setCurrentBBox(bBoxIn4326); + } + }; + + model?.updateBboxSignal.connect(listenToModel); + + return () => { + model?.updateBboxSignal.disconnect(listenToModel); + }; + }, [model, useWorldBBox]); + + const handleSubmit = async () => { + const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; + + const st = startTime + ? startTime.toISOString() + : startOfToday().toISOString(); + + const et = endTime ? endTime.toISOString() : endOfToday().toISOString(); + + const body = { + bbox: currentBBox, + collections: [selectedCollection], + // really want this as a range? i guess it doesnt matter? + // should really just not have it if unset + datetime: `${st}/${et}`, + limit: 12, + }; + + console.log('body', body); + + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-XSRFToken': XSRF_TOKEN, + credentials: 'include', + }, + body: JSON.stringify(body), + }; + + const data: any = await fetchWithProxies( + 'https://stac.dataspace.copernicus.eu/v1/search', + model, + async response => await response.json(), + //@ts-expect-error Jupyter requires X-XSRFToken header + options, + 'internal', + ); + + console.log('data', data); + + // Filter assets to only include items with 'overview' or 'thumbnail' roles + if (data.features && data.features.length > 0 && data.features[0].assets) { + const originalAssets = data.features[0].assets; + const filteredAssets: Record = {}; + + // Iterate through each asset in the assets object + for (const [key, asset] of Object.entries(originalAssets)) { + const assetObj = asset as IStacAsset; + if (assetObj && assetObj.roles) { + // Handle both array and string role values + const roles = assetObj.roles; + + if (roles.includes('thumbnail') || roles.includes('overview')) { + filteredAssets[key] = assetObj; + } + } + } + + console.log('originalAssets', originalAssets); + console.log('filteredAssets', filteredAssets); + // Replace assets with filtered version + data.features[0].assets = filteredAssets; + } + + addToMap(data.features[0]); + }; + + const addToMap = (stacData: any) => { + const layerId = UUID.uuid4(); + // const stacData = results.find(item => item.id === id); + + if (!stacData) { + console.error('Result not found:'); + return; + } + + const layerModel: IJGISLayer = { + type: 'StacLayer', + parameters: { data: stacData }, + visible: true, + name: stacData.properties.title ?? stacData.id, + }; + + model && model.addLayer(layerId, layerModel); + }; + + return ( +
+ {/* fake api choice */} + API: {API_URL} + {/* temporal extent */} + + + {/* spatial extent */} + + {/* collections */} + + {/* items IDs */} + {/* additional filters - this is where queryables should end up */} + {queryableProps && ( + + )} + {/* sort */} + {/* items per page */} + {/* buttons */} + +
+ ); +} + +export default StacGenericFilterPanel; diff --git a/packages/base/src/stacBrowser/components/StacPanel.tsx b/packages/base/src/stacBrowser/components/StacPanel.tsx index db89cb31a..76c42e73c 100644 --- a/packages/base/src/stacBrowser/components/StacPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacPanel.tsx @@ -1,5 +1,5 @@ import { IJupyterGISModel } from '@jupytergis/schema'; -import React from 'react'; +import React, { useState } from 'react'; import { Tabs, @@ -8,13 +8,18 @@ import { TabsTrigger, } from '@/src/shared/components/Tabs'; import useStacSearch from '@/src/stacBrowser/hooks/useStacSearch'; -import StacPanelFilters from './StacPanelFilters'; +import StacGenericFilterPanel from './StacGenericFilterPanel'; import StacPanelResults from './StacPanelResults'; +import StacGeodesFilterPanel from './geodes/StacGeodesFilterPanel'; interface IStacViewProps { model?: IJupyterGISModel; } const StacPanel = ({ model }: IStacViewProps) => { + const [selectedUrl, setSelectedUrl] = useState( + 'https://stac.dataspace.copernicus.eu/v1/', + ); + const { filterState, filterSetters, @@ -50,16 +55,35 @@ const StacPanel = ({ model }: IStacViewProps) => { >{`Results (${totalResults})`} - +
+ +
+ + {selectedUrl === 'https://geodes-portal.cnes.fr/api/stac/search' ? ( + + ) : ( + + )}
void; } -const StacPanelFilters = ({ +const StacGeodesFilterPanel = ({ filterState, filterSetters, startTime, @@ -100,12 +100,11 @@ const StacPanelFilters = ({ return (
-
- - - Use whole world as bounding box - -
+
@@ -165,4 +164,4 @@ const StacPanelFilters = ({
); }; -export default StacPanelFilters; +export default StacGeodesFilterPanel; diff --git a/packages/base/src/stacBrowser/components/shared/StacCheckboxWithLabel.tsx b/packages/base/src/stacBrowser/components/shared/StacCheckboxWithLabel.tsx new file mode 100644 index 000000000..7730b55a4 --- /dev/null +++ b/packages/base/src/stacBrowser/components/shared/StacCheckboxWithLabel.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import Checkbox from '@/src/shared/components/Checkbox'; + +interface IStacCheckboxWithLabelProps { + checked: boolean; + onCheckedChange: (checked: boolean) => void; + label: string; +} + +const StacCheckboxWithLabel: React.FC = ({ + checked, + onCheckedChange, + label, +}) => { + return ( +
+ + + {label} + +
+ ); +}; + +export default StacCheckboxWithLabel; diff --git a/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx b/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx new file mode 100644 index 000000000..84e69e999 --- /dev/null +++ b/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react'; + +import { RadioGroup, RadioGroupItem } from '@/src/shared/components/RadioGroup'; +import { QueryableCombo } from '../QueryableCombo'; + +interface IStacQueryableFilterListProps { + queryableProps: [string, any][]; +} + +type FilterOperator = 'and' | 'or'; + +const getInputBasedOnType = (val: any): React.ReactNode => { + switch (val.type) { + case 'string': + if (val.enum) { + return ( + + ); + } + if (val.format === 'date-time') { + return ( + + ); + } + return ( + + ); + case 'number': + return ( + + ); + default: + return ( + + ); + } +}; + +const StacQueryableFilterList: React.FC = ({ + queryableProps, +}) => { + const [filterOperator, setFilterOperator] = useState('and'); + + return ( +
+
+ Additional Filters +
+
+ Match all filters (and) Match any filters (or) + { + if (value === 'and' || value === 'or') { + setFilterOperator(value); + } + }} + > +
+ + +
+
+ + +
+
+
+
+ +
+
+ ); +}; + +export default StacQueryableFilterList; diff --git a/packages/base/src/stacBrowser/components/shared/StacSearchDatePicker.tsx b/packages/base/src/stacBrowser/components/shared/StacSearchDatePicker.tsx new file mode 100644 index 000000000..7da40602b --- /dev/null +++ b/packages/base/src/stacBrowser/components/shared/StacSearchDatePicker.tsx @@ -0,0 +1,64 @@ +import { format } from 'date-fns'; +import { CalendarIcon } from 'lucide-react'; +import React from 'react'; + +import { Button } from '@/src/shared/components/Button'; +import { Calendar } from '@/src/shared/components/Calendar'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/src/shared/components/Popover'; + +interface IStacSearchDatePickerProps { + startTime: Date | undefined; + setStartTime: (date: Date | undefined) => void; + endTime: Date | undefined; + setEndTime: (date: Date | undefined) => void; +} + +function StacSearchDatePicker({ + startTime, + endTime, + setStartTime, + setEndTime, +}: IStacSearchDatePickerProps) { + return ( +
+ + + + + + + + + + + + + + + + +
+ ); +} + +export default StacSearchDatePicker; diff --git a/tsconfigbase.json b/tsconfigbase.json index 604b2ff68..bbf40034c 100644 --- a/tsconfigbase.json +++ b/tsconfigbase.json @@ -10,7 +10,7 @@ "moduleResolution": "node", "noEmitOnError": true, "noImplicitAny": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "preserveWatchOutput": true, "resolveJsonModule": true, "strict": true, From 4c520cd9155edb4facfbcbef9535726e31dc70e5 Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 28 Nov 2025 13:09:23 +0100 Subject: [PATCH 04/70] css --- packages/base/style/base.css | 1 + packages/base/style/tabPanel.css | 86 -------------------------------- 2 files changed, 1 insertion(+), 86 deletions(-) diff --git a/packages/base/style/base.css b/packages/base/style/base.css index 9b639fb68..55ca2a96f 100644 --- a/packages/base/style/base.css +++ b/packages/base/style/base.css @@ -25,6 +25,7 @@ @import url('./shared/dialog.css'); @import url('./shared/switch.css'); @import url('./shared/dialog.css'); +@import url('./shared/popover.css'); .errors { color: var(--jp-warn-color0); diff --git a/packages/base/style/tabPanel.css b/packages/base/style/tabPanel.css index f7626f64d..f37c1ae3f 100644 --- a/packages/base/style/tabPanel.css +++ b/packages/base/style/tabPanel.css @@ -2,89 +2,3 @@ font-size: var(--jp-ui-font-size0); padding-bottom: 50px; } - -.jgis-stac-browser-collection { - flex-wrap: wrap; - margin-top: 0.5rem; -} - -.jgis-stac-browser-section-item { - border-radius: 1rem !important; - cursor: pointer; -} - -.jgis-stac-browser-results-item { - border-radius: 1rem !important; - cursor: pointer; - white-space: normal !important; - height: auto !important; -} - -.jgis-stac-browser-results-list { - display: flex; - flex-direction: column; - gap: 1rem; - word-break: break-word; - padding: 0.25rem; - margin-right: 0.5rem; -} - -.jgis-stac-browser-date-picker { - display: flex; - justify-content: space-around; - flex-wrap: wrap; - gap: 0.5rem; -} - -.jgis-stac-browser-filters-panel { - display: flex; - flex-direction: column; - gap: 0.5rem; - align-items: flex-start; - padding: 0 0.5rem; -} - -.jgis-stac-filter-trigger { - max-width: fit-content; - border: 1px solid var(--jp-border-color0); - padding: 0.5rem; - cursor: pointer; - font-weight: bold; - display: flex; - align-items: center; - gap: 0.15rem; -} - -.jgis-stac-filter-trigger:disabled { - cursor: not-allowed; -} - -.jgis-stac-filter-section-container { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.jgis-stac-filter-section-badges { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; -} - -.jgis-stac-datepicker-icon { - width: 1rem; - height: 1rem; -} - -.jgis-stac-badge { - gap: 0.25rem; - padding-right: 0.3rem !important; -} - -.jgis-stac-badge-icon:hover { - background-color: color-mix( - in srgb, - var(--jp-error-color0), - transparent 20% - ) !important; -} From 2ef82b19f03e1f28985a0bd272efcc09d18d497b Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 28 Nov 2025 13:09:39 +0100 Subject: [PATCH 05/70] Move to hook --- .../stacBrowser/components/QueryableCombo.tsx | 165 +++++----- .../stacBrowser/components/QueryableRow.tsx | 138 ++++++++ .../components/StacGenericFilterPanel.tsx | 304 ++++++------------ .../shared/StacQueryableFilterList.tsx | 54 +--- .../stacBrowser/hooks/useStacGenericFilter.ts | 269 ++++++++++++++++ 5 files changed, 599 insertions(+), 331 deletions(-) create mode 100644 packages/base/src/stacBrowser/components/QueryableRow.tsx create mode 100644 packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts diff --git a/packages/base/src/stacBrowser/components/QueryableCombo.tsx b/packages/base/src/stacBrowser/components/QueryableCombo.tsx index ec43bbee2..1c648e2c1 100644 --- a/packages/base/src/stacBrowser/components/QueryableCombo.tsx +++ b/packages/base/src/stacBrowser/components/QueryableCombo.tsx @@ -1,7 +1,5 @@ -'use client'; - import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'; -import * as React from 'react'; +import React, { useState } from 'react'; import { Button } from '@/src/shared/components/Button'; import { @@ -17,83 +15,104 @@ import { PopoverContent, PopoverTrigger, } from '@/src/shared/components/Popover'; -import { cn } from '@/src/shared/components/utils'; - -const queryables = [ - { - value: 'next.js', - label: 'Next.js', - }, - { - value: 'sveltekit', - label: 'SvelteKit', - }, - { - value: 'nuxt.js', - label: 'Nuxt.js', - }, - { - value: 'remix', - label: 'Remix', - }, - { - value: 'astro', - label: 'Astro', - }, -]; +import QueryableRow from './QueryableRow'; interface IQueryableComboProps { queryables: [string, any][]; } export function QueryableCombo({ queryables }: IQueryableComboProps) { - const [open, setOpen] = React.useState(false); - const [value, setValue] = React.useState(''); + const [open, setOpen] = useState(false); + const [selectedItems, setSelectedItems] = useState<[string, any][]>([]); + + const handleSelect = (key: string, val: any) => { + setSelectedItems(prev => { + const existingIndex = prev.findIndex(([k]) => k === key); + if (existingIndex >= 0) { + // Remove if already selected + return prev.filter(([k]) => k !== key); + } else { + // Add if not selected + return [...prev, [key, val]]; + } + }); + + setOpen(false); + }; + + const getButtonText = () => { + if (selectedItems.length === 0) { + return 'Select queryable...'; + } + if (selectedItems.length === 1) { + return selectedItems[0][1].title || selectedItems[0][0]; + } + return `${selectedItems.length} selected`; + }; + + const isSelected = (key: string) => { + return selectedItems.some(([k]) => k === key); + }; return ( - - - - - - - - - No queryable found. - - {queryables.map(([key, val]) => ( - { - setValue(currentValue === value ? '' : currentValue); - setOpen(false); - }} - > - - {val.title} - - ))} - - - - - +
+ + + + + + + + + No queryable found. + + {queryables.map(([key, val]) => ( + { + handleSelect(key, val); + }} + > + + {val.title} + + ))} + + + + + + {selectedItems.map(([key, val]) => ( + + ))} +
); } diff --git a/packages/base/src/stacBrowser/components/QueryableRow.tsx b/packages/base/src/stacBrowser/components/QueryableRow.tsx new file mode 100644 index 000000000..8d2228f9f --- /dev/null +++ b/packages/base/src/stacBrowser/components/QueryableRow.tsx @@ -0,0 +1,138 @@ +import React, { useState } from 'react'; + +interface IQueryableRowProps { + qKey: string; + qVal: any; +} + +type Operator = 'eq' | 'neq' | 'lt' | 'gt'; + +interface IOperatorOption { + value: Operator; + label: string; +} + +function QueryableRow({ qKey, qVal }: IQueryableRowProps) { + const getOperatorsForType = ( + type: string, + format?: string, + ): IOperatorOption[] => { + if (format === 'date-time') { + return [ + { value: 'lt', label: 'less than' }, + { value: 'gt', label: 'greater than' }, + ]; + } + + switch (type) { + case 'string': + return [ + { value: 'eq', label: 'equal to' }, + { value: 'neq', label: 'not equal to' }, + ]; + case 'number': + return [ + { value: 'eq', label: 'equal to' }, + { value: 'neq', label: 'not equal to' }, + { value: 'lt', label: 'less than' }, + { value: 'gt', label: 'greater than' }, + ]; + default: + return [ + { value: 'eq', label: 'equal to' }, + { value: 'neq', label: 'not equal to' }, + ]; + } + }; + + const operators = getOperatorsForType(qVal.type, qVal.format); + const [selectedOperator, setSelectedOperator] = useState( + operators[0]?.value || 'eq', + ); + + const getInputBasedOnType = (val: any): React.ReactNode => { + switch (val.type) { + case 'string': + if (val.enum) { + return ( + + ); + } + if (val.format === 'date-time') { + return ( + + ); + } + return ( + + ); + case 'number': + return ( + + ); + default: + return ( + + ); + } + }; + + return ( +
+ {qVal.title} + + {getInputBasedOnType(qVal)} +
+ ); +} + +export default QueryableRow; diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index cb63349cf..c81187334 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -1,19 +1,12 @@ -import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; -import { UUID } from '@lumino/coreutils'; -import { - endOfToday, - endOfTomorrow, - startOfToday, - startOfYesterday, -} from 'date-fns'; -import React, { useEffect, useState } from 'react'; +import { IJupyterGISModel } from '@jupytergis/schema'; +import React from 'react'; -import { fetchWithProxies } from '@/src/tools'; import StacCheckboxWithLabel from './shared/StacCheckboxWithLabel'; import StacQueryableFilterList from './shared/StacQueryableFilterList'; import StacSearchDatePicker from './shared/StacSearchDatePicker'; +import { useStacGenericFilter } from '../hooks/useStacGenericFilter'; import useStacSearch from '../hooks/useStacSearch'; -import { IStacAsset, IStacCollection } from '../types/types'; +import { IStacCollection } from '../types/types'; interface IStacBrowser2Props { model?: IJupyterGISModel; @@ -21,22 +14,10 @@ interface IStacBrowser2Props { type FilteredCollection = Pick; -// { -// title?: string; -// id: string; -// }; - const API_URL = 'https://stac.dataspace.copernicus.eu/v1/'; + // This is a generic UI for apis that support filter extension function StacGenericFilterPanel({ model }: IStacBrowser2Props) { - const [queryableProps, setQueryableProps] = useState<[string, any][]>(); - const [collections, setCollections] = useState([]); - // temp - const [selectedCollection, setSelectedCollection] = useState(''); - const [currentBBox, setCurrentBBox] = useState< - [number, number, number, number] - >([-180, -90, 180, 90]); - const { startTime, endTime, @@ -48,205 +29,118 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { model, }); + const { + queryableProps, + collections, + selectedCollection, + setSelectedCollection, + handleSubmit, + } = useStacGenericFilter({ + model, + startTime, + endTime, + useWorldBBox, + }); + if (!model) { console.log('no model'); return; } - // for collections - useEffect(() => { - const fatch = async () => { - const data = await fetchWithProxies( - API_URL + 'collections', - model, - async response => await response.json(), - undefined, - 'internal', - ); - - const collections: FilteredCollection[] = data.collections - .map((collection: any) => ({ - title: collection.title ?? collection.id, - id: collection.id, - })) - .sort((a: FilteredCollection, b: FilteredCollection) => { - const titleA = a.title?.toLowerCase() ?? ''; - const titleB = b.title?.toLowerCase() ?? ''; - return titleA.localeCompare(titleB); - }); - - console.log('collections', collections); - setCollections(collections); - }; - - fatch(); - }, []); - - // for queryables - // should listen for colletion changes and requery - // need a way to handle querying multiple collections without refetching everything - // collection id -> queryables map as a basic cache thing?? - useEffect(() => { - const fatch = async () => { - const data = await fetchWithProxies( - API_URL + 'queryables', - model, - async response => await response.json(), - undefined, - 'internal', - ); - - setQueryableProps(Object.entries(data.properties)); - }; - - fatch(); - }, []); - - useEffect(() => { - const listenToModel = ( - sender: IJupyterGISModel, - bBoxIn4326: [number, number, number, number], - ) => { - if (useWorldBBox) { - setCurrentBBox([-180, -90, 180, 90]); - } else { - setCurrentBBox(bBoxIn4326); - } - }; - - model?.updateBboxSignal.connect(listenToModel); - - return () => { - model?.updateBboxSignal.disconnect(listenToModel); - }; - }, [model, useWorldBBox]); - - const handleSubmit = async () => { - const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; - - const st = startTime - ? startTime.toISOString() - : startOfToday().toISOString(); - - const et = endTime ? endTime.toISOString() : endOfToday().toISOString(); - - const body = { - bbox: currentBBox, - collections: [selectedCollection], - // really want this as a range? i guess it doesnt matter? - // should really just not have it if unset - datetime: `${st}/${et}`, - limit: 12, - }; - - console.log('body', body); - - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-XSRFToken': XSRF_TOKEN, - credentials: 'include', - }, - body: JSON.stringify(body), - }; - - const data: any = await fetchWithProxies( - 'https://stac.dataspace.copernicus.eu/v1/search', - model, - async response => await response.json(), - //@ts-expect-error Jupyter requires X-XSRFToken header - options, - 'internal', - ); - - console.log('data', data); - - // Filter assets to only include items with 'overview' or 'thumbnail' roles - if (data.features && data.features.length > 0 && data.features[0].assets) { - const originalAssets = data.features[0].assets; - const filteredAssets: Record = {}; - - // Iterate through each asset in the assets object - for (const [key, asset] of Object.entries(originalAssets)) { - const assetObj = asset as IStacAsset; - if (assetObj && assetObj.roles) { - // Handle both array and string role values - const roles = assetObj.roles; - - if (roles.includes('thumbnail') || roles.includes('overview')) { - filteredAssets[key] = assetObj; - } - } - } - - console.log('originalAssets', originalAssets); - console.log('filteredAssets', filteredAssets); - // Replace assets with filtered version - data.features[0].assets = filteredAssets; - } - - addToMap(data.features[0]); - }; - - const addToMap = (stacData: any) => { - const layerId = UUID.uuid4(); - // const stacData = results.find(item => item.id === id); - - if (!stacData) { - console.error('Result not found:'); - return; - } - - const layerModel: IJGISLayer = { - type: 'StacLayer', - parameters: { data: stacData }, - visible: true, - name: stacData.properties.title ?? stacData.id, - }; - - model && model.addLayer(layerId, layerModel); - }; - return ( -
+
{/* fake api choice */} - API: {API_URL} +
+ + API: {API_URL} + +
{/* temporal extent */} - +
+ +
{/* spatial extent */} - +
+ +
{/* collections */} - +
+ + +
{/* items IDs */} {/* additional filters - this is where queryables should end up */} {queryableProps && ( - +
+ +
)} {/* sort */} {/* items per page */} {/* buttons */} - +
+ +
); } diff --git a/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx b/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx index 84e69e999..a7dd238ab 100644 --- a/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx +++ b/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx @@ -9,59 +9,7 @@ interface IStacQueryableFilterListProps { type FilterOperator = 'and' | 'or'; -const getInputBasedOnType = (val: any): React.ReactNode => { - switch (val.type) { - case 'string': - if (val.enum) { - return ( - - ); - } - if (val.format === 'date-time') { - return ( - - ); - } - return ( - - ); - case 'number': - return ( - - ); - default: - return ( - - ); - } -}; + const StacQueryableFilterList: React.FC = ({ queryableProps, diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts new file mode 100644 index 000000000..8f7bd0cb8 --- /dev/null +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -0,0 +1,269 @@ +import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; +import { UUID } from '@lumino/coreutils'; +import { endOfToday, startOfToday } from 'date-fns'; +import { useEffect, useState } from 'react'; + +import { fetchWithProxies } from '@/src/tools'; +import { + IStacAsset, + IStacCollection, + IStacItem, + IStacSearchResult, +} from '../types/types'; + +type FilteredCollection = Pick; + +const API_URL = 'https://stac.dataspace.copernicus.eu/v1/'; + +interface IUseStacGenericFilterProps { + model?: IJupyterGISModel; + startTime?: Date; + endTime?: Date; + useWorldBBox: boolean; +} + +export function useStacGenericFilter({ + model, + startTime, + endTime, + useWorldBBox, +}: IUseStacGenericFilterProps) { + const [queryableProps, setQueryableProps] = useState<[string, any][]>(); + const [collections, setCollections] = useState([]); + // ! temp + const [selectedCollection, setSelectedCollection] = + useState('sentinel-2-l2a'); + const [currentBBox, setCurrentBBox] = useState< + [number, number, number, number] + >([-180, -90, 180, 90]); + const [results, setResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [totalPages, setTotalPages] = useState(1); + const [currentPage, setCurrentPage] = useState(1); + const [totalResults, setTotalResults] = useState(0); + + // for collections + useEffect(() => { + if (!model) { + return; + } + + const fatch = async () => { + const data = await fetchWithProxies( + API_URL + 'collections', + model, + async response => await response.json(), + undefined, + 'internal', + ); + + const collections: FilteredCollection[] = data.collections + .map((collection: any) => ({ + title: collection.title ?? collection.id, + id: collection.id, + })) + .sort((a: FilteredCollection, b: FilteredCollection) => { + const titleA = a.title?.toLowerCase() ?? ''; + const titleB = b.title?.toLowerCase() ?? ''; + return titleA.localeCompare(titleB); + }); + + console.log('collections', collections); + setCollections(collections); + }; + + fatch(); + }, [model]); + + // for queryables + // should listen for colletion changes and requery + // need a way to handle querying multiple collections without refetching everything + // collection id -> queryables map as a basic cache thing?? + useEffect(() => { + if (!model) { + return; + } + + const fatch = async () => { + const data = await fetchWithProxies( + API_URL + 'queryables', + model, + async response => await response.json(), + undefined, + 'internal', + ); + + setQueryableProps(Object.entries(data.properties)); + }; + + fatch(); + }, [model]); + + useEffect(() => { + if (!model) { + return; + } + + const listenToModel = ( + sender: IJupyterGISModel, + bBoxIn4326: [number, number, number, number], + ) => { + if (useWorldBBox) { + setCurrentBBox([-180, -90, 180, 90]); + } else { + setCurrentBBox(bBoxIn4326); + } + }; + + model.updateBboxSignal.connect(listenToModel); + + return () => { + model.updateBboxSignal.disconnect(listenToModel); + }; + }, [model, useWorldBBox]); + + const addToMap = (stacData: any) => { + if (!model) { + return; + } + + const layerId = UUID.uuid4(); + + if (!stacData) { + console.error('Result not found:'); + return; + } + + const layerModel: IJGISLayer = { + type: 'StacLayer', + parameters: { data: stacData }, + visible: true, + name: stacData.properties.title ?? stacData.id, + }; + + model.addLayer(layerId, layerModel); + }; + + const handleSubmit = async () => { + if (!model) { + return; + } + + const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; + + const st = startTime + ? startTime.toISOString() + : startOfToday().toISOString(); + + const et = endTime ? endTime.toISOString() : endOfToday().toISOString(); + + const body = { + bbox: currentBBox, + collections: [selectedCollection], + // really want this as a range? i guess it doesnt matter? + // should really just not have it if unset + datetime: `${st}/${et}`, + limit: 12, + }; + + console.log('body', body); + + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-XSRFToken': XSRF_TOKEN, + credentials: 'include', + }, + body: JSON.stringify(body), + }; + + try { + setIsLoading(true); + const data = (await fetchWithProxies( + 'https://stac.dataspace.copernicus.eu/v1/search', + model, + async response => await response.json(), + //@ts-expect-error Jupyter requires X-XSRFToken header + options, + 'internal', + )) as IStacSearchResult; + + if (!data) { + console.debug('STAC search failed -- no results found'); + setResults([]); + setTotalPages(1); + setTotalResults(0); + return; + } + + // Filter assets to only include items with 'overview' or 'thumbnail' roles + // ? is this a good idea?? + if (data.features && data.features.length > 0) { + data.features.forEach(feature => { + if (feature.assets) { + const originalAssets = feature.assets; + const filteredAssets: Record = {}; + + // Iterate through each asset in the assets object + for (const [key, asset] of Object.entries(originalAssets)) { + if ( + asset && + typeof asset === 'object' && + 'roles' in asset && + Array.isArray(asset.roles) + ) { + const roles = asset.roles; + + if (roles.includes('thumbnail') || roles.includes('overview')) { + filteredAssets[key] = asset; + } + } + } + + // Replace assets with filtered version + feature.assets = filteredAssets; + } + }); + } + + console.log('hook data', data); + setResults(data.features); + // Handle context if available (STAC API extension) + if (data.context) { + const pages = data.context.matched / data.context.limit; + setTotalPages(Math.ceil(pages)); + setTotalResults(data.context.matched); + } else { + // Fallback if context is not available + setTotalPages(1); + setTotalResults(data.features.length); + } + + // Add first result to map + if (data.features.length > 0) { + addToMap(data.features[0]); + } + } catch (error) { + console.error('STAC search failed -- error fetching data:', error); + setResults([]); + setTotalPages(1); + setTotalResults(0); + } finally { + setIsLoading(false); + } + }; + + return { + queryableProps, + collections, + selectedCollection, + setSelectedCollection, + handleSubmit, + results, + isLoading, + totalPages, + currentPage, + totalResults, + }; +} From fd76bc65e98d9e6b9b8cd9c558f8f85a334f655a Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 1 Dec 2025 12:23:48 +0100 Subject: [PATCH 06/70] Compose hooks --- .../components/StacGenericFilterPanel.tsx | 19 +++-------- .../shared/StacQueryableFilterList.tsx | 1 + .../stacBrowser/hooks/useStacGenericFilter.ts | 33 ++++++++++--------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index c81187334..e350db99d 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -5,7 +5,6 @@ import StacCheckboxWithLabel from './shared/StacCheckboxWithLabel'; import StacQueryableFilterList from './shared/StacQueryableFilterList'; import StacSearchDatePicker from './shared/StacSearchDatePicker'; import { useStacGenericFilter } from '../hooks/useStacGenericFilter'; -import useStacSearch from '../hooks/useStacSearch'; import { IStacCollection } from '../types/types'; interface IStacBrowser2Props { @@ -18,28 +17,20 @@ const API_URL = 'https://stac.dataspace.copernicus.eu/v1/'; // This is a generic UI for apis that support filter extension function StacGenericFilterPanel({ model }: IStacBrowser2Props) { - const { - startTime, - endTime, - setStartTime, - setEndTime, - useWorldBBox, - setUseWorldBBox, - } = useStacSearch({ - model, - }); - const { queryableProps, collections, selectedCollection, setSelectedCollection, handleSubmit, - } = useStacGenericFilter({ - model, startTime, endTime, + setStartTime, + setEndTime, useWorldBBox, + setUseWorldBBox, + } = useStacGenericFilter({ + model, }); if (!model) { diff --git a/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx b/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx index a7dd238ab..8f1769534 100644 --- a/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx +++ b/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx @@ -24,6 +24,7 @@ const StacQueryableFilterList: React.FC = ({
Match all filters (and) Match any filters (or) { if (value === 'and' || value === 'or') { diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 8f7bd0cb8..bbb1e2df6 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -4,12 +4,8 @@ import { endOfToday, startOfToday } from 'date-fns'; import { useEffect, useState } from 'react'; import { fetchWithProxies } from '@/src/tools'; -import { - IStacAsset, - IStacCollection, - IStacItem, - IStacSearchResult, -} from '../types/types'; +import useStacSearch from './useStacSearch'; +import { IStacCollection, IStacItem, IStacSearchResult } from '../types/types'; type FilteredCollection = Pick; @@ -17,17 +13,18 @@ const API_URL = 'https://stac.dataspace.copernicus.eu/v1/'; interface IUseStacGenericFilterProps { model?: IJupyterGISModel; - startTime?: Date; - endTime?: Date; - useWorldBBox: boolean; } -export function useStacGenericFilter({ - model, - startTime, - endTime, - useWorldBBox, -}: IUseStacGenericFilterProps) { +export function useStacGenericFilter({ model }: IUseStacGenericFilterProps) { + const { + startTime, + endTime, + setStartTime, + setEndTime, + useWorldBBox, + setUseWorldBBox, + } = useStacSearch({ model }); + const [queryableProps, setQueryableProps] = useState<[string, any][]>(); const [collections, setCollections] = useState([]); // ! temp @@ -265,5 +262,11 @@ export function useStacGenericFilter({ totalPages, currentPage, totalResults, + startTime, + endTime, + setStartTime, + setEndTime, + useWorldBBox, + setUseWorldBBox, }; } From 7830417f78eab1b7a3e97f520bf90a3aae841090 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 1 Dec 2025 12:55:25 +0100 Subject: [PATCH 07/70] Better names --- .../{QueryableCombo.tsx => QueryableComboBox.tsx} | 2 +- .../stacBrowser/components/StacGenericFilterPanel.tsx | 4 ++-- ...ueryableFilterList.tsx => StacQueryableFilters.tsx} | 10 ++++------ 3 files changed, 7 insertions(+), 9 deletions(-) rename packages/base/src/stacBrowser/components/{QueryableCombo.tsx => QueryableComboBox.tsx} (97%) rename packages/base/src/stacBrowser/components/shared/{StacQueryableFilterList.tsx => StacQueryableFilters.tsx} (85%) diff --git a/packages/base/src/stacBrowser/components/QueryableCombo.tsx b/packages/base/src/stacBrowser/components/QueryableComboBox.tsx similarity index 97% rename from packages/base/src/stacBrowser/components/QueryableCombo.tsx rename to packages/base/src/stacBrowser/components/QueryableComboBox.tsx index 1c648e2c1..4be93f57c 100644 --- a/packages/base/src/stacBrowser/components/QueryableCombo.tsx +++ b/packages/base/src/stacBrowser/components/QueryableComboBox.tsx @@ -21,7 +21,7 @@ interface IQueryableComboProps { queryables: [string, any][]; } -export function QueryableCombo({ queryables }: IQueryableComboProps) { +export function QueryableComboBox({ queryables }: IQueryableComboProps) { const [open, setOpen] = useState(false); const [selectedItems, setSelectedItems] = useState<[string, any][]>([]); diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index e350db99d..cc4a71368 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -2,7 +2,7 @@ import { IJupyterGISModel } from '@jupytergis/schema'; import React from 'react'; import StacCheckboxWithLabel from './shared/StacCheckboxWithLabel'; -import StacQueryableFilterList from './shared/StacQueryableFilterList'; +import StacQueryableFilters from './shared/StacQueryableFilters'; import StacSearchDatePicker from './shared/StacSearchDatePicker'; import { useStacGenericFilter } from '../hooks/useStacGenericFilter'; import { IStacCollection } from '../types/types'; @@ -106,7 +106,7 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) {
- +
)} {/* sort */} diff --git a/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx b/packages/base/src/stacBrowser/components/shared/StacQueryableFilters.tsx similarity index 85% rename from packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx rename to packages/base/src/stacBrowser/components/shared/StacQueryableFilters.tsx index 8f1769534..11a266293 100644 --- a/packages/base/src/stacBrowser/components/shared/StacQueryableFilterList.tsx +++ b/packages/base/src/stacBrowser/components/shared/StacQueryableFilters.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { RadioGroup, RadioGroupItem } from '@/src/shared/components/RadioGroup'; -import { QueryableCombo } from '../QueryableCombo'; +import { QueryableComboBox } from '../QueryableComboBox'; interface IStacQueryableFilterListProps { queryableProps: [string, any][]; @@ -9,9 +9,7 @@ interface IStacQueryableFilterListProps { type FilterOperator = 'and' | 'or'; - - -const StacQueryableFilterList: React.FC = ({ +const StacQueryableFilters: React.FC = ({ queryableProps, }) => { const [filterOperator, setFilterOperator] = useState('and'); @@ -43,10 +41,10 @@ const StacQueryableFilterList: React.FC = ({
- +
); }; -export default StacQueryableFilterList; +export default StacQueryableFilters; From 4add0c6d6434eaead0b93ab1edc885dd024dc9fd Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 1 Dec 2025 14:06:35 +0100 Subject: [PATCH 08/70] Build filter object --- .../components/QueryableComboBox.tsx | 14 +++- .../stacBrowser/components/QueryableRow.tsx | 81 ++++++++++++++----- .../components/StacGenericFilterPanel.tsx | 11 ++- .../shared/StacQueryableFilters.tsx | 21 +++-- .../stacBrowser/hooks/useStacGenericFilter.ts | 62 +++++++++++++- 5 files changed, 157 insertions(+), 32 deletions(-) diff --git a/packages/base/src/stacBrowser/components/QueryableComboBox.tsx b/packages/base/src/stacBrowser/components/QueryableComboBox.tsx index 4be93f57c..e9a31add4 100644 --- a/packages/base/src/stacBrowser/components/QueryableComboBox.tsx +++ b/packages/base/src/stacBrowser/components/QueryableComboBox.tsx @@ -16,12 +16,17 @@ import { PopoverTrigger, } from '@/src/shared/components/Popover'; import QueryableRow from './QueryableRow'; +import { UpdateQueryableFilter } from '../hooks/useStacGenericFilter'; interface IQueryableComboProps { queryables: [string, any][]; + updateQueryableFilter: UpdateQueryableFilter; } -export function QueryableComboBox({ queryables }: IQueryableComboProps) { +export function QueryableComboBox({ + queryables, + updateQueryableFilter, +}: IQueryableComboProps) { const [open, setOpen] = useState(false); const [selectedItems, setSelectedItems] = useState<[string, any][]>([]); @@ -111,7 +116,12 @@ export function QueryableComboBox({ queryables }: IQueryableComboProps) { {selectedItems.map(([key, val]) => ( - + ))}
); diff --git a/packages/base/src/stacBrowser/components/QueryableRow.tsx b/packages/base/src/stacBrowser/components/QueryableRow.tsx index 8d2228f9f..b3b4ce125 100644 --- a/packages/base/src/stacBrowser/components/QueryableRow.tsx +++ b/packages/base/src/stacBrowser/components/QueryableRow.tsx @@ -1,62 +1,97 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; + +import { + IQueryableFilter, + Operator, + UpdateQueryableFilter, +} from '../hooks/useStacGenericFilter'; interface IQueryableRowProps { qKey: string; qVal: any; + updateQueryableFilter: UpdateQueryableFilter; } -type Operator = 'eq' | 'neq' | 'lt' | 'gt'; - interface IOperatorOption { value: Operator; label: string; } -function QueryableRow({ qKey, qVal }: IQueryableRowProps) { +function QueryableRow({ + qKey, + qVal, + updateQueryableFilter, +}: IQueryableRowProps) { const getOperatorsForType = ( type: string, format?: string, ): IOperatorOption[] => { if (format === 'date-time') { return [ - { value: 'lt', label: 'less than' }, - { value: 'gt', label: 'greater than' }, + { value: '<', label: 'less than' }, + { value: '>', label: 'greater than' }, ]; } switch (type) { case 'string': return [ - { value: 'eq', label: 'equal to' }, - { value: 'neq', label: 'not equal to' }, + { value: '=', label: 'equal to' }, + { value: '!=', label: 'not equal to' }, ]; case 'number': return [ - { value: 'eq', label: 'equal to' }, - { value: 'neq', label: 'not equal to' }, - { value: 'lt', label: 'less than' }, - { value: 'gt', label: 'greater than' }, + { value: '=', label: 'equal to' }, + { value: '!=', label: 'not equal to' }, + { value: '<', label: 'less than' }, + { value: '>', label: 'greater than' }, ]; default: return [ - { value: 'eq', label: 'equal to' }, - { value: 'neq', label: 'not equal to' }, + { value: '=', label: 'equal to' }, + { value: '!=', label: 'not equal to' }, ]; } }; const operators = getOperatorsForType(qVal.type, qVal.format); - const [selectedOperator, setSelectedOperator] = useState( - operators[0]?.value || 'eq', - ); + + // Local state for UI, synced to parent via callback + const [localFilter, setLocalFilter] = useState({ + operator: operators[0]?.value || '=', + inputValue: undefined, + }); + + // Sync local state to parent whenever it changes + useEffect(() => { + updateQueryableFilter(qKey, localFilter); + }, [qKey, localFilter, updateQueryableFilter]); + + const handleInputChange = (value: string | number) => { + setLocalFilter(prev => ({ + ...prev, + inputValue: value, + })); + }; + + const handleOperatorChange = (operator: Operator) => { + setLocalFilter(prev => ({ + ...prev, + operator, + })); + }; const getInputBasedOnType = (val: any): React.ReactNode => { + const currentValue = localFilter.inputValue; + switch (val.type) { case 'string': if (val.enum) { return ( handleInputChange(e.target.value)} {...(val.pattern && { 'data-pattern': val.pattern })} /> ); @@ -80,6 +117,8 @@ function QueryableRow({ qKey, qVal }: IQueryableRowProps) { handleInputChange(e.target.value)} {...(val.pattern && { 'data-pattern': val.pattern })} /> ); @@ -90,6 +129,8 @@ function QueryableRow({ qKey, qVal }: IQueryableRowProps) { style={{ maxWidth: '75px' }} min={val.min !== undefined ? val.min : undefined} max={val.max !== undefined ? val.max : undefined} + value={(currentValue as number) || ''} + onChange={e => handleInputChange(Number(e.target.value))} {...(val.pattern && { 'data-pattern': val.pattern })} /> ); @@ -98,6 +139,8 @@ function QueryableRow({ qKey, qVal }: IQueryableRowProps) { handleInputChange(e.target.value)} {...(val.pattern && { 'data-pattern': val.pattern })} /> ); @@ -115,8 +158,8 @@ function QueryableRow({ qKey, qVal }: IQueryableRowProps) { > {qVal.title} { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value > 0) { + setLimit(value); + } + }} + style={{ + maxWidth: '200px', + padding: '0.5rem', + borderRadius: '0.25rem', + border: '1px solid var(--jp-border-color0)', + }} + /> + + {/* buttons */}
{ handlersRef.current.formatResult, ); }, [setPaginationHandlers]); + const handleDatasetSelection = (dataset: string, collection: string) => { const collections = new Set(filterState.collections); const datasets = new Set(filterState.datasets); diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index be2cdc758..9fd56fd80 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -5,7 +5,12 @@ import { useCallback, useEffect, useState } from 'react'; import { fetchWithProxies } from '@/src/tools'; import useStacSearch from './useStacSearch'; -import { IStacCollection, IStacItem, IStacSearchResult } from '../types/types'; +import { + IStacCollection, + IStacItem, + IStacLink, + IStacSearchResult, +} from '../types/types'; type FilteredCollection = Pick; @@ -27,9 +32,13 @@ const API_URL = 'https://stac.dataspace.copernicus.eu/v1/'; interface IUseStacGenericFilterProps { model?: IJupyterGISModel; + limit?: number; } -export function useStacGenericFilter({ model }: IUseStacGenericFilterProps) { +export function useStacGenericFilter({ + model, + limit = 12, +}: IUseStacGenericFilterProps) { const { startTime, endTime, @@ -56,6 +65,9 @@ export function useStacGenericFilter({ model }: IUseStacGenericFilterProps) { Record >({}); const [filterOperator, setFilterOperator] = useState('and'); + const [paginationLinks, setPaginationLinks] = useState< + Array }> + >([]); // for collections useEffect(() => { @@ -174,6 +186,8 @@ export function useStacGenericFilter({ model }: IUseStacGenericFilterProps) { return; } + setCurrentPage(1); + const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; const st = startTime @@ -201,7 +215,7 @@ export function useStacGenericFilter({ model }: IUseStacGenericFilterProps) { bbox: currentBBox, collections: [selectedCollection], datetime: `${st}/${et}`, - limit: 12, + limit, 'filter-lang': 'cql2-json', }; @@ -276,6 +290,16 @@ export function useStacGenericFilter({ model }: IUseStacGenericFilterProps) { console.log('hook data', data); setResults(data.features); + + // Store pagination links for use in handlePaginationClick + if (data.links) { + setPaginationLinks( + data.links as Array< + IStacLink & { method?: string; body?: Record } + >, + ); + } + // Handle context if available (STAC API extension) if (data.context) { const pages = data.context.matched / data.context.limit; @@ -301,6 +325,170 @@ export function useStacGenericFilter({ model }: IUseStacGenericFilterProps) { } }; + /** + * Handles clicking on a result item + * @param id - ID of the clicked result + */ + const handleResultClick = useCallback( + async (id: string): Promise => { + if (!model) { + return; + } + + const result = results.find((r: IStacItem) => r.id === id); + if (result) { + addToMap(result); + } + }, + [results, model], + ); + + /** + * Fetches results using a STAC link (for pagination) + * @param link - STAC link object with href and optional body + */ + const fetchUsingLink = useCallback( + async ( + link: IStacLink & { method?: string; body?: Record }, + ) => { + if (!model) { + return; + } + + const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; + + const options = { + method: (link.method || 'POST').toUpperCase(), + headers: { + 'Content-Type': 'application/json', + 'X-XSRFToken': XSRF_TOKEN, + credentials: 'include', + }, + body: link.body ? JSON.stringify(link.body) : undefined, + }; + + try { + setIsLoading(true); + const data = (await fetchWithProxies( + link.href, + model, + async response => await response.json(), + //@ts-expect-error Jupyter requires X-XSRFToken header + options, + 'internal', + )) as IStacSearchResult; + + if (!data) { + console.debug('STAC search failed -- no results found'); + setResults([]); + setTotalPages(1); + setTotalResults(0); + return; + } + + // Filter assets to only include items with 'overview' or 'thumbnail' roles + if (data.features && data.features.length > 0) { + data.features.forEach(feature => { + if (feature.assets) { + const originalAssets = feature.assets; + const filteredAssets: Record = {}; + + for (const [key, asset] of Object.entries(originalAssets)) { + if ( + asset && + typeof asset === 'object' && + 'roles' in asset && + Array.isArray(asset.roles) + ) { + const roles = asset.roles; + + if ( + roles.includes('thumbnail') || + roles.includes('overview') + ) { + filteredAssets[key] = asset; + } + } + } + + feature.assets = filteredAssets; + } + }); + } + + console.log('hook data', data); + setResults(data.features); + + // Store pagination links for next pagination + if (data.links) { + setPaginationLinks( + data.links as Array< + IStacLink & { method?: string; body?: Record } + >, + ); + } + + // Handle context if available (STAC API extension) + if (data.context) { + const pages = data.context.matched / data.context.limit; + setTotalPages(Math.ceil(pages)); + setTotalResults(data.context.matched); + } else { + // Fallback if context is not available + setTotalPages(1); + setTotalResults(data.features.length); + } + } catch (error) { + console.error('STAC search failed -- error fetching data:', error); + setResults([]); + setTotalPages(1); + setTotalResults(0); + } finally { + setIsLoading(false); + } + }, + [model], + ); + + /** + * Handles pagination clicks + * @param page - Page number to navigate to (used to determine next/previous direction) + */ + const handlePaginationClick = useCallback( + async (page: number): Promise => { + if (!model) { + return; + } + + // Determine if we're going forward or backward based on page number + const isNext = page > currentPage; + const rel = isNext ? 'next' : 'previous'; + + // Find the appropriate link using the rel field + const link = paginationLinks.find(l => l.rel === rel); + + if (link && link.body) { + // Use the link with its body (contains token) to fetch the page + await fetchUsingLink(link); + // Update current page after successful fetch + setCurrentPage(page); + } else { + // If no link found, we can't paginate + console.warn(`No ${rel} link available for pagination`); + } + }, + [currentPage, paginationLinks, fetchUsingLink, model], + ); + + /** + * Formats a result item for display + * @param item - STAC item to format + * @returns Formatted string representation of the item + */ + const formatResult = useCallback((item: IStacItem): string => { + return item.properties?.title ?? item.id; + }, []); + return { queryableProps, collections, @@ -312,6 +500,9 @@ export function useStacGenericFilter({ model }: IUseStacGenericFilterProps) { totalPages, currentPage, totalResults, + handlePaginationClick, + handleResultClick, + formatResult, startTime, endTime, setStartTime, From 99c1ac57c5de86c7d3e55d7008fb89880ff4071c Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 2 Dec 2025 11:51:56 +0100 Subject: [PATCH 11/70] Move where links are saved --- .../src/stacBrowser/hooks/useStacGenericFilter.ts | 5 ++--- .../base/src/stacBrowser/hooks/useStacSearch.ts | 14 +++++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 9fd56fd80..f8ba6519d 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -46,6 +46,8 @@ export function useStacGenericFilter({ setEndTime, useWorldBBox, setUseWorldBBox, + paginationLinks, + setPaginationLinks, } = useStacSearch({ model }); const [queryableProps, setQueryableProps] = useState<[string, any][]>(); @@ -65,9 +67,6 @@ export function useStacGenericFilter({ Record >({}); const [filterOperator, setFilterOperator] = useState('and'); - const [paginationLinks, setPaginationLinks] = useState< - Array }> - >([]); // for collections useEffect(() => { diff --git a/packages/base/src/stacBrowser/hooks/useStacSearch.ts b/packages/base/src/stacBrowser/hooks/useStacSearch.ts index 6decadc4a..32e534e69 100644 --- a/packages/base/src/stacBrowser/hooks/useStacSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useStacSearch.ts @@ -1,12 +1,13 @@ import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; import { UUID } from '@lumino/coreutils'; import { startOfYesterday } from 'date-fns'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import useIsFirstRender from '@/src/shared/hooks/useIsFirstRender'; import { products } from '@/src/stacBrowser/constants'; import { IStacItem, + IStacLink, IStacQueryBody, IStacSearchResult, StacFilterState, @@ -37,6 +38,12 @@ interface IUseStacSearchReturn { isLoading: boolean; useWorldBBox: boolean; setUseWorldBBox: (val: boolean) => void; + paginationLinks: Array< + IStacLink & { method?: string; body?: Record } + >; + setPaginationLinks: ( + links: Array }>, + ) => void; } const API_URL = 'https://geodes-portal.cnes.fr/api/stac/search'; @@ -63,6 +70,9 @@ function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { [number, number, number, number] >([-180, -90, 180, 90]); const [useWorldBBox, setUseWorldBBox] = useState(false); + const [paginationLinks, setPaginationLinks] = useState< + Array }> + >([]); const [filterState, setFilterState] = useState({ collections: new Set(), datasets: new Set(), @@ -292,6 +302,8 @@ function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { isLoading, useWorldBBox, setUseWorldBBox, + paginationLinks, + setPaginationLinks, }; } From 646d1c14c17865f5dd659234cc908504ef6a63ac Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 2 Dec 2025 11:52:41 +0100 Subject: [PATCH 12/70] Remove placeholder section --- .../components/StacGenericFilterPanel.tsx | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index 969a7a9a6..123585713 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -6,7 +6,7 @@ import StacCheckboxWithLabel from './shared/StacCheckboxWithLabel'; import StacQueryableFilters from './shared/StacQueryableFilters'; import StacSearchDatePicker from './shared/StacSearchDatePicker'; import { useStacGenericFilter } from '../hooks/useStacGenericFilter'; -import { IStacCollection, IStacItem } from '../types/types'; +import { IStacCollection } from '../types/types'; interface IStacBrowser2Props { model?: IJupyterGISModel; @@ -14,8 +14,6 @@ interface IStacBrowser2Props { type FilteredCollection = Pick; -const API_URL = 'https://stac.dataspace.copernicus.eu/v1/'; - // This is a generic UI for apis that support filter extension function StacGenericFilterPanel({ model }: IStacBrowser2Props) { const { setResults, setPaginationHandlers } = useStacResultsContext(); @@ -93,19 +91,6 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { gap: '1rem', }} > - {/* fake api choice */} -
- - API: {API_URL} - -
{/* temporal extent */}
Date: Tue, 2 Dec 2025 12:01:39 +0100 Subject: [PATCH 13/70] Links in context --- .../components/StacGenericFilterPanel.tsx | 9 +++++++- .../geodes/StacGeodesFilterPanel.tsx | 9 +++++++- .../context/StacResultsContext.tsx | 22 ++++++++++++++++++- .../stacBrowser/hooks/useStacGenericFilter.ts | 1 + 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index 123585713..90da4c0af 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -16,7 +16,8 @@ type FilteredCollection = Pick; // This is a generic UI for apis that support filter extension function StacGenericFilterPanel({ model }: IStacBrowser2Props) { - const { setResults, setPaginationHandlers } = useStacResultsContext(); + const { setResults, setPaginationHandlers, setPaginationLinks } = + useStacResultsContext(); const [limit, setLimit] = useState(12); const { @@ -42,6 +43,7 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { updateQueryableFilter, filterOperator, setFilterOperator, + paginationLinks, } = useStacGenericFilter({ model, limit, @@ -77,6 +79,11 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { ); }, [setPaginationHandlers]); + // Sync pagination links to context whenever they change + useEffect(() => { + setPaginationLinks(paginationLinks); + }, [paginationLinks, setPaginationLinks]); + if (!model) { console.log('no model'); return; diff --git a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx index 90e7caeca..5df229842 100644 --- a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx @@ -25,7 +25,8 @@ interface IStacGeodesFilterPanelProps { } const StacGeodesFilterPanel = ({ model }: IStacGeodesFilterPanelProps) => { - const { setResults, setPaginationHandlers } = useStacResultsContext(); + const { setResults, setPaginationHandlers, setPaginationLinks } = + useStacResultsContext(); const { filterState, @@ -44,6 +45,7 @@ const StacGeodesFilterPanel = ({ model }: IStacGeodesFilterPanelProps) => { isLoading, useWorldBBox, setUseWorldBBox, + paginationLinks, } = useStacSearch({ model }); // Track handlers with refs to avoid infinite loops @@ -76,6 +78,11 @@ const StacGeodesFilterPanel = ({ model }: IStacGeodesFilterPanelProps) => { ); }, [setPaginationHandlers]); + // Sync pagination links to context whenever they change + useEffect(() => { + setPaginationLinks(paginationLinks); + }, [paginationLinks, setPaginationLinks]); + const handleDatasetSelection = (dataset: string, collection: string) => { const collections = new Set(filterState.collections); const datasets = new Set(filterState.datasets); diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index fbb8a4283..f8c74edfe 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -6,7 +6,7 @@ import React, { ReactNode, } from 'react'; -import { IStacItem } from '../types/types'; +import { IStacItem, IStacLink } from '../types/types'; interface IStacResultsContext { results: IStacItem[]; @@ -17,6 +17,9 @@ interface IStacResultsContext { handlePaginationClick: (page: number) => Promise; handleResultClick: (id: string) => Promise; formatResult: (item: IStacItem) => string; + paginationLinks: Array< + IStacLink & { method?: string; body?: Record } + >; setResults: ( results: IStacItem[], isLoading: boolean, @@ -29,6 +32,9 @@ interface IStacResultsContext { handleResultClick: (id: string) => Promise, formatResult: (item: IStacItem) => string, ) => void; + setPaginationLinks: ( + links: Array }>, + ) => void; } const StacResultsContext = createContext( @@ -54,6 +60,9 @@ export function StacResultsProvider({ children }: IStacResultsProviderProps) { const [formatResult, setFormatResult] = useState<(item: IStacItem) => string>( () => (item: IStacItem) => item.id, ); + const [paginationLinks, setPaginationLinksState] = useState< + Array }> + >([]); const setResults = useCallback( ( @@ -85,6 +94,15 @@ export function StacResultsProvider({ children }: IStacResultsProviderProps) { [], ); + const setPaginationLinks = useCallback( + ( + links: Array }>, + ) => { + setPaginationLinksState(links); + }, + [], + ); + return ( {children} diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index f8ba6519d..d9d3207fe 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -512,5 +512,6 @@ export function useStacGenericFilter({ updateQueryableFilter, filterOperator, setFilterOperator, + paginationLinks, }; } From 19cd1b1288477c067363532736d4c0882c913b08 Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 2 Dec 2025 12:27:45 +0100 Subject: [PATCH 14/70] Might regret --- .../components/StacPanelResults.tsx | 25 ++++++++++++------- .../context/StacResultsContext.tsx | 10 +++++--- .../stacBrowser/hooks/useStacGenericFilter.ts | 20 +++++++++------ .../src/stacBrowser/hooks/useStacSearch.ts | 20 +++++++++++---- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index d8cd550f2..88ae16479 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -40,6 +40,9 @@ function getPageItems( ]; } + +// ! tues to do -- refactor this, total pages is based on context, which is an extension +// so everythign here needs to be based on link rels instead const StacPanelResults = () => { const { results, @@ -49,20 +52,26 @@ const StacPanelResults = () => { handleResultClick, formatResult, isLoading, + paginationLinks, } = useStacResultsContext(); + + const isNext = paginationLinks.some(link => link.rel === 'next'); + const isPrev = paginationLinks.some(link => link.rel === 'previous'); + return (
- handlePaginationClick(Math.max(1, currentPage - 1)) + onClick={ + () => handlePaginationClick('previous') + // handlePaginationClick(Math.max(1, currentPage - 1)) } - disabled={currentPage === 1} + disabled={!isPrev} /> - {totalPages <= 0 ? ( + {results.length === 0 ? (
No Matches Found
) : ( getPageItems(currentPage, totalPages).map(item => { @@ -77,7 +86,7 @@ const StacPanelResults = () => { handlePaginationClick(item)} + onClick={() => handlePaginationClick('next')} > {item} @@ -87,10 +96,8 @@ const StacPanelResults = () => { )} - handlePaginationClick(Math.min(totalPages, currentPage + 1)) - } - disabled={currentPage === totalPages} + onClick={() => handlePaginationClick('previous')} + disabled={!isNext} />
diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index f8c74edfe..60e12f563 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -14,7 +14,7 @@ interface IStacResultsContext { totalPages: number; currentPage: number; totalResults: number; - handlePaginationClick: (page: number) => Promise; + handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise; handleResultClick: (id: string) => Promise; formatResult: (item: IStacItem) => string; paginationLinks: Array< @@ -28,7 +28,7 @@ interface IStacResultsContext { totalResults: number, ) => void; setPaginationHandlers: ( - handlePaginationClick: (page: number) => Promise, + handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise, handleResultClick: (id: string) => Promise, formatResult: (item: IStacItem) => string, ) => void; @@ -52,7 +52,7 @@ export function StacResultsProvider({ children }: IStacResultsProviderProps) { const [currentPage, setCurrentPage] = useState(1); const [totalResults, setTotalResults] = useState(0); const [handlePaginationClick, setHandlePaginationClick] = useState< - (page: number) => Promise + (dir: 'next' | 'previous' | number) => Promise >(async () => {}); const [handleResultClick, setHandleResultClick] = useState< (id: string) => Promise @@ -83,7 +83,9 @@ export function StacResultsProvider({ children }: IStacResultsProviderProps) { const setPaginationHandlers = useCallback( ( - newHandlePaginationClick: (page: number) => Promise, + newHandlePaginationClick: ( + dir: 'next' | 'previous' | number, + ) => Promise, newHandleResultClick: (id: string) => Promise, newFormatResult: (item: IStacItem) => string, ) => { diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index d9d3207fe..c4cb740f4 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -451,17 +451,21 @@ export function useStacGenericFilter({ /** * Handles pagination clicks - * @param page - Page number to navigate to (used to determine next/previous direction) + * @param dir - Direction ('next' | 'previous') or page number (for backward compatibility) */ const handlePaginationClick = useCallback( - async (page: number): Promise => { + async (dir: 'next' | 'previous' | number): Promise => { if (!model) { return; } - // Determine if we're going forward or backward based on page number - const isNext = page > currentPage; - const rel = isNext ? 'next' : 'previous'; + // If dir is a number, convert it to 'next' or 'previous' based on current page + let rel: 'next' | 'previous'; + if (typeof dir === 'number') { + rel = dir > currentPage ? 'next' : 'previous'; + } else { + rel = dir; + } // Find the appropriate link using the rel field const link = paginationLinks.find(l => l.rel === rel); @@ -469,8 +473,10 @@ export function useStacGenericFilter({ if (link && link.body) { // Use the link with its body (contains token) to fetch the page await fetchUsingLink(link); - // Update current page after successful fetch - setCurrentPage(page); + // Update current page after successful fetch if dir was a number + if (typeof dir === 'number') { + setCurrentPage(dir); + } } else { // If no link found, we can't paginate console.warn(`No ${rel} link available for pagination`); diff --git a/packages/base/src/stacBrowser/hooks/useStacSearch.ts b/packages/base/src/stacBrowser/hooks/useStacSearch.ts index 32e534e69..9b32262f0 100644 --- a/packages/base/src/stacBrowser/hooks/useStacSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useStacSearch.ts @@ -21,6 +21,7 @@ interface IUseStacSearchProps { model: IJupyterGISModel | undefined; } +// ! TODO factor out common bits interface IUseStacSearchReturn { filterState: StacFilterState; filterSetters: StacFilterSetters; @@ -32,7 +33,7 @@ interface IUseStacSearchReturn { totalPages: number; currentPage: number; totalResults: number; - handlePaginationClick: (page: number) => Promise; + handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise; handleResultClick: (id: string) => Promise; formatResult: (item: IStacItem) => string; isLoading: boolean; @@ -269,11 +270,20 @@ function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { /** * Handles pagination clicks - * @param page - Page number to navigate to + * @param dir - Direction ('next' | 'previous') or page number to navigate to */ - const handlePaginationClick = async (page: number): Promise => { - setCurrentPage(page); - model && fetchResults(page); + const handlePaginationClick = async ( + dir: 'next' | 'previous' | number, + ): Promise => { + if (typeof dir === 'number') { + setCurrentPage(dir); + model && fetchResults(dir); + } else { + // For 'next' or 'previous', calculate the page number + const newPage = dir === 'next' ? currentPage + 1 : currentPage - 1; + setCurrentPage(newPage); + model && fetchResults(newPage); + } }; /** From 3328aef7ac5a5725fb6343c125011b9c2d548edf Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 2 Dec 2025 13:57:37 +0100 Subject: [PATCH 15/70] pre moving context update to hook --- .../components/StacGenericFilterPanel.tsx | 21 +- .../components/StacPanelResults.tsx | 33 +- .../stacBrowser/hooks/useStacGenericFilter.ts | 343 ++++++++++-------- 3 files changed, 239 insertions(+), 158 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index 90da4c0af..7fa602ac4 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -1,12 +1,12 @@ import { IJupyterGISModel } from '@jupytergis/schema'; -import React, { useEffect, useCallback, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useStacResultsContext } from '../context/StacResultsContext'; import StacCheckboxWithLabel from './shared/StacCheckboxWithLabel'; import StacQueryableFilters from './shared/StacQueryableFilters'; import StacSearchDatePicker from './shared/StacSearchDatePicker'; import { useStacGenericFilter } from '../hooks/useStacGenericFilter'; -import { IStacCollection } from '../types/types'; +import { IStacCollection, IStacItem } from '../types/types'; interface IStacBrowser2Props { model?: IJupyterGISModel; @@ -16,7 +16,7 @@ type FilteredCollection = Pick; // This is a generic UI for apis that support filter extension function StacGenericFilterPanel({ model }: IStacBrowser2Props) { - const { setResults, setPaginationHandlers, setPaginationLinks } = + const { results, setResults, setPaginationHandlers, setPaginationLinks } = useStacResultsContext(); const [limit, setLimit] = useState(12); @@ -26,7 +26,6 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { selectedCollection, setSelectedCollection, handleSubmit, - results, isLoading, totalPages, currentPage, @@ -47,6 +46,8 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { } = useStacGenericFilter({ model, limit, + setResults, + results, }); // Track handlers with refs to avoid infinite loops @@ -65,11 +66,6 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { }; }, [handlePaginationClick, handleResultClick, formatResult]); - // Sync results to context whenever they change - useEffect(() => { - setResults(results, isLoading, totalPages, currentPage, totalResults); - }, [results, isLoading, totalPages, currentPage, totalResults, setResults]); - // Sync handlers separately, only when they actually change useEffect(() => { setPaginationHandlers( @@ -77,7 +73,12 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { handlersRef.current.handleResultClick, handlersRef.current.formatResult, ); - }, [setPaginationHandlers]); + }, [ + setPaginationHandlers, + handlePaginationClick, + handleResultClick, + formatResult, + ]); // Sync pagination links to context whenever they change useEffect(() => { diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index 88ae16479..22af3deef 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { Button } from '@/src/shared/components/Button'; import { @@ -11,6 +11,7 @@ import { PaginationPrevious, } from '@/src/shared/components/Pagination'; import { useStacResultsContext } from '@/src/stacBrowser/context/StacResultsContext'; +import { IStacItem } from '@/src/stacBrowser/types/types'; function getPageItems( currentPage: number, @@ -40,7 +41,6 @@ function getPageItems( ]; } - // ! tues to do -- refactor this, total pages is based on context, which is an extension // so everythign here needs to be based on link rels instead const StacPanelResults = () => { @@ -55,6 +55,28 @@ const StacPanelResults = () => { paginationLinks, } = useStacResultsContext(); + // Use a ref to track previous results and detect actual changes + const prevResultsRef = useRef([]); + const resultsIdsRef = useRef(''); + + useEffect(() => { + // Create a string of result IDs for comparison (more reliable than array reference) + const currentResultsIds = results.map(r => r.id).join(','); + + // Only log if results actually changed (by ID comparison) + if (currentResultsIds !== resultsIdsRef.current) { + console.log('[StacPanelResults] Results updated:', { + count: results.length, + resultIds: results.map(r => r.id), + previousCount: prevResultsRef.current.length, + }); + + // Update refs + prevResultsRef.current = results; + resultsIdsRef.current = currentResultsIds; + } + }, [results]); + const isNext = paginationLinks.some(link => link.rel === 'next'); const isPrev = paginationLinks.some(link => link.rel === 'previous'); @@ -96,13 +118,16 @@ const StacPanelResults = () => { )} handlePaginationClick('previous')} + onClick={() => handlePaginationClick('next')} disabled={!isNext} />
-
+
{isLoading ? ( // TODO: Fancy spinner
Loading results...
diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index c4cb740f4..100010cb1 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -1,7 +1,7 @@ import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; import { UUID } from '@lumino/coreutils'; import { endOfToday, startOfToday } from 'date-fns'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { fetchWithProxies } from '@/src/tools'; import useStacSearch from './useStacSearch'; @@ -33,11 +33,21 @@ const API_URL = 'https://stac.dataspace.copernicus.eu/v1/'; interface IUseStacGenericFilterProps { model?: IJupyterGISModel; limit?: number; + setResults: ( + results: IStacItem[], + isLoading: boolean, + totalPages: number, + currentPage: number, + totalResults: number, + ) => void; + results: IStacItem[]; } export function useStacGenericFilter({ model, limit = 12, + setResults, + results, }: IUseStacGenericFilterProps) { const { startTime, @@ -50,6 +60,12 @@ export function useStacGenericFilter({ setPaginationLinks, } = useStacSearch({ model }); + // Use a ref to always access the latest paginationLinks value + const paginationLinksRef = useRef(paginationLinks); + useEffect(() => { + paginationLinksRef.current = paginationLinks; + }, [paginationLinks]); + const [queryableProps, setQueryableProps] = useState<[string, any][]>(); const [collections, setCollections] = useState([]); // ! temp @@ -58,7 +74,6 @@ export function useStacGenericFilter({ const [currentBBox, setCurrentBBox] = useState< [number, number, number, number] >([-180, -90, 180, 90]); - const [results, setResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const [totalPages, setTotalPages] = useState(1); const [currentPage, setCurrentPage] = useState(1); @@ -160,14 +175,16 @@ export function useStacGenericFilter({ return; } - const layerModel: IJGISLayer = { - type: 'StacLayer', - parameters: { data: stacData }, - visible: true, - name: stacData.properties.title ?? stacData.id, - }; + console.log('adding', layerId); + + // const layerModel: IJGISLayer = { + // type: 'StacLayer', + // parameters: { data: stacData }, + // visible: true, + // name: stacData.properties.title ?? stacData.id, + // }; - model.addLayer(layerId, layerModel); + // model.addLayer(layerId, layerModel); }; const updateQueryableFilter = useCallback( @@ -240,6 +257,8 @@ export function useStacGenericFilter({ try { setIsLoading(true); + // Update context with loading state + setResults(results, true, totalPages, currentPage, totalResults); const data = (await fetchWithProxies( 'https://stac.dataspace.copernicus.eu/v1/search', model, @@ -251,7 +270,7 @@ export function useStacGenericFilter({ if (!data) { console.debug('STAC search failed -- no results found'); - setResults([]); + setResults([], false, 1, currentPage, 0); setTotalPages(1); setTotalResults(0); return; @@ -288,7 +307,31 @@ export function useStacGenericFilter({ } console.log('hook data', data); - setResults(data.features); + // Sort features by id before setting results + const sortedFeatures = [...data.features].sort((a, b) => + a.id.localeCompare(b.id), + ); + + // Handle context if available (STAC API extension) + let calculatedTotalPages = 1; + let calculatedTotalResults = data.features.length; + if (data.context) { + const pages = data.context.matched / data.context.limit; + calculatedTotalPages = Math.ceil(pages); + calculatedTotalResults = data.context.matched; + } + + setTotalPages(calculatedTotalPages); + setTotalResults(calculatedTotalResults); + + // Update context with results + setResults( + sortedFeatures, + false, + calculatedTotalPages, + currentPage, + calculatedTotalResults, + ); // Store pagination links for use in handlePaginationClick if (data.links) { @@ -299,24 +342,13 @@ export function useStacGenericFilter({ ); } - // Handle context if available (STAC API extension) - if (data.context) { - const pages = data.context.matched / data.context.limit; - setTotalPages(Math.ceil(pages)); - setTotalResults(data.context.matched); - } else { - // Fallback if context is not available - setTotalPages(1); - setTotalResults(data.features.length); - } - // Add first result to map if (data.features.length > 0) { addToMap(data.features[0]); } } catch (error) { console.error('STAC search failed -- error fetching data:', error); - setResults([]); + setResults([], false, 1, currentPage, 0); setTotalPages(1); setTotalResults(0); } finally { @@ -346,144 +378,168 @@ export function useStacGenericFilter({ * Fetches results using a STAC link (for pagination) * @param link - STAC link object with href and optional body */ - const fetchUsingLink = useCallback( - async ( - link: IStacLink & { method?: string; body?: Record }, - ) => { - if (!model) { + const fetchUsingLink = async ( + link: IStacLink & { method?: string; body?: Record }, + ) => { + if (!model) { + return; + } + + const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; + + const options = { + method: (link.method || 'POST').toUpperCase(), + headers: { + 'Content-Type': 'application/json', + 'X-XSRFToken': XSRF_TOKEN, + credentials: 'include', + }, + body: link.body ? JSON.stringify(link.body) : undefined, + }; + + try { + setIsLoading(true); + // Update context with loading state + setResults(results, true, totalPages, currentPage, totalResults); + const data = (await fetchWithProxies( + link.href, + model, + async response => await response.json(), + //@ts-expect-error Jupyter requires X-XSRFToken header + options, + 'internal', + )) as IStacSearchResult; + + if (!data) { + console.debug('STAC search failed -- no results found'); + setResults([], false, 1, currentPage, 0); + setTotalPages(1); + setTotalResults(0); return; } - const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; + // Filter assets to only include items with 'overview' or 'thumbnail' roles + if (data.features && data.features.length > 0) { + data.features.forEach(feature => { + if (feature.assets) { + const originalAssets = feature.assets; + const filteredAssets: Record = {}; - const options = { - method: (link.method || 'POST').toUpperCase(), - headers: { - 'Content-Type': 'application/json', - 'X-XSRFToken': XSRF_TOKEN, - credentials: 'include', - }, - body: link.body ? JSON.stringify(link.body) : undefined, - }; + for (const [key, asset] of Object.entries(originalAssets)) { + if ( + asset && + typeof asset === 'object' && + 'roles' in asset && + Array.isArray(asset.roles) + ) { + const roles = asset.roles; - try { - setIsLoading(true); - const data = (await fetchWithProxies( - link.href, - model, - async response => await response.json(), - //@ts-expect-error Jupyter requires X-XSRFToken header - options, - 'internal', - )) as IStacSearchResult; - - if (!data) { - console.debug('STAC search failed -- no results found'); - setResults([]); - setTotalPages(1); - setTotalResults(0); - return; - } - - // Filter assets to only include items with 'overview' or 'thumbnail' roles - if (data.features && data.features.length > 0) { - data.features.forEach(feature => { - if (feature.assets) { - const originalAssets = feature.assets; - const filteredAssets: Record = {}; - - for (const [key, asset] of Object.entries(originalAssets)) { - if ( - asset && - typeof asset === 'object' && - 'roles' in asset && - Array.isArray(asset.roles) - ) { - const roles = asset.roles; - - if ( - roles.includes('thumbnail') || - roles.includes('overview') - ) { - filteredAssets[key] = asset; - } + if (roles.includes('thumbnail') || roles.includes('overview')) { + filteredAssets[key] = asset; } } - - feature.assets = filteredAssets; } - }); - } - - console.log('hook data', data); - setResults(data.features); - - // Store pagination links for next pagination - if (data.links) { - setPaginationLinks( - data.links as Array< - IStacLink & { method?: string; body?: Record } - >, - ); - } - - // Handle context if available (STAC API extension) - if (data.context) { - const pages = data.context.matched / data.context.limit; - setTotalPages(Math.ceil(pages)); - setTotalResults(data.context.matched); - } else { - // Fallback if context is not available - setTotalPages(1); - setTotalResults(data.features.length); - } - } catch (error) { - console.error('STAC search failed -- error fetching data:', error); - setResults([]); - setTotalPages(1); - setTotalResults(0); - } finally { - setIsLoading(false); + + feature.assets = filteredAssets; + } + }); } - }, - [model], - ); + + console.log('hook data link', data); + // Sort features by id before setting results + const sortedFeatures = [...data.features].sort((a, b) => + a.id.localeCompare(b.id), + ); + console.log('[useStacGenericFilter] Setting results from pagination:', { + featuresCount: sortedFeatures.length, + featureIds: sortedFeatures.map(f => f.id), + }); + + // Handle context if available (STAC API extension) + let calculatedTotalPages = 1; + let calculatedTotalResults = data.features.length; + if (data.context) { + const pages = data.context.matched / data.context.limit; + calculatedTotalPages = Math.ceil(pages); + calculatedTotalResults = data.context.matched; + } + + setTotalPages(calculatedTotalPages); + setTotalResults(calculatedTotalResults); + + // Update context with results + setResults( + sortedFeatures, + false, + calculatedTotalPages, + currentPage, + calculatedTotalResults, + ); + + // Store pagination links for next pagination + if (data.links) { + setPaginationLinks( + data.links as Array< + IStacLink & { method?: string; body?: Record } + >, + ); + } + } catch (error) { + console.error('STAC search failed -- error fetching data:', error); + setResults([], false, 1, currentPage, 0); + setTotalPages(1); + setTotalResults(0); + } finally { + setIsLoading(false); + } + }; /** * Handles pagination clicks * @param dir - Direction ('next' | 'previous') or page number (for backward compatibility) */ - const handlePaginationClick = useCallback( - async (dir: 'next' | 'previous' | number): Promise => { - if (!model) { - return; - } + const handlePaginationClick = async ( + dir: 'next' | 'previous' | number, + ): Promise => { + console.log('[useStacGenericFilter] Pagination click:', { + dir, + currentPage, + availableLinks: paginationLinksRef.current.map(l => l.rel), + }); + if (!model) { + return; + } - // If dir is a number, convert it to 'next' or 'previous' based on current page - let rel: 'next' | 'previous'; - if (typeof dir === 'number') { - rel = dir > currentPage ? 'next' : 'previous'; - } else { - rel = dir; - } + // If dir is a number, convert it to 'next' or 'previous' based on current page + let rel: 'next' | 'previous'; + if (typeof dir === 'number') { + rel = dir > currentPage ? 'next' : 'previous'; + } else { + rel = dir; + } - // Find the appropriate link using the rel field - const link = paginationLinks.find(l => l.rel === rel); + // Use ref to get the latest paginationLinks value + const link = paginationLinksRef.current.find(l => l.rel === rel); - if (link && link.body) { - // Use the link with its body (contains token) to fetch the page - await fetchUsingLink(link); - // Update current page after successful fetch if dir was a number - if (typeof dir === 'number') { - setCurrentPage(dir); - } - } else { - // If no link found, we can't paginate - console.warn(`No ${rel} link available for pagination`); + if (link && link.body) { + console.log('[useStacGenericFilter] Found pagination link:', { + rel: link.rel, + href: link.href, + hasBody: !!link.body, + }); + // Use the link with its body (contains token) to fetch the page + await fetchUsingLink(link); + // Update current page after successful fetch if dir was a number + if (typeof dir === 'number') { + setCurrentPage(dir); } - }, - [currentPage, paginationLinks, fetchUsingLink, model], - ); + } else { + // If no link found, we can't paginate + console.warn( + `[useStacGenericFilter] No ${rel} link available for pagination`, + ); + } + }; /** * Formats a result item for display @@ -500,7 +556,6 @@ export function useStacGenericFilter({ selectedCollection, setSelectedCollection, handleSubmit, - results, isLoading, totalPages, currentPage, From c0f728e0061cd07015d9f2492a363a1fac194710 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 4 Dec 2025 13:50:14 +0100 Subject: [PATCH 16/70] Remove redundant state --- .../components/StacGenericFilterPanel.tsx | 28 ++++---- .../components/StacPanelResults.tsx | 4 ++ .../stacBrowser/hooks/useStacGenericFilter.ts | 64 +++++++------------ 3 files changed, 44 insertions(+), 52 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index 7fa602ac4..f537daa9a 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -16,8 +16,17 @@ type FilteredCollection = Pick; // This is a generic UI for apis that support filter extension function StacGenericFilterPanel({ model }: IStacBrowser2Props) { - const { results, setResults, setPaginationHandlers, setPaginationLinks } = - useStacResultsContext(); + const { + results, + setResults, + isLoading, + totalPages, + currentPage, + totalResults, + setPaginationHandlers, + setPaginationLinks, + paginationLinks, + } = useStacResultsContext(); const [limit, setLimit] = useState(12); const { @@ -26,10 +35,6 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { selectedCollection, setSelectedCollection, handleSubmit, - isLoading, - totalPages, - currentPage, - totalResults, handlePaginationClick, handleResultClick, formatResult, @@ -42,12 +47,17 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { updateQueryableFilter, filterOperator, setFilterOperator, - paginationLinks, } = useStacGenericFilter({ model, limit, setResults, results, + isLoading, + totalPages, + currentPage, + totalResults, + paginationLinks, + setPaginationLinks, }); // Track handlers with refs to avoid infinite loops @@ -80,10 +90,6 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { formatResult, ]); - // Sync pagination links to context whenever they change - useEffect(() => { - setPaginationLinks(paginationLinks); - }, [paginationLinks, setPaginationLinks]); if (!model) { console.log('no model'); diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index 22af3deef..49d639d5d 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -77,6 +77,10 @@ const StacPanelResults = () => { } }, [results]); + useEffect(() => { + console.log('links effect GOOO'); + }, [paginationLinks]); + const isNext = paginationLinks.some(link => link.rel === 'next'); const isPrev = paginationLinks.some(link => link.rel === 'previous'); diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 100010cb1..d57f825e3 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -41,6 +41,16 @@ interface IUseStacGenericFilterProps { totalResults: number, ) => void; results: IStacItem[]; + isLoading: boolean; + totalPages: number; + currentPage: number; + totalResults: number; + paginationLinks: Array< + IStacLink & { method?: string; body?: Record } + >; + setPaginationLinks: ( + links: Array }>, + ) => void; } export function useStacGenericFilter({ @@ -48,6 +58,12 @@ export function useStacGenericFilter({ limit = 12, setResults, results, + isLoading, + totalPages, + currentPage, + totalResults, + paginationLinks, + setPaginationLinks, }: IUseStacGenericFilterProps) { const { startTime, @@ -56,16 +72,8 @@ export function useStacGenericFilter({ setEndTime, useWorldBBox, setUseWorldBBox, - paginationLinks, - setPaginationLinks, } = useStacSearch({ model }); - // Use a ref to always access the latest paginationLinks value - const paginationLinksRef = useRef(paginationLinks); - useEffect(() => { - paginationLinksRef.current = paginationLinks; - }, [paginationLinks]); - const [queryableProps, setQueryableProps] = useState<[string, any][]>(); const [collections, setCollections] = useState([]); // ! temp @@ -74,10 +82,6 @@ export function useStacGenericFilter({ const [currentBBox, setCurrentBBox] = useState< [number, number, number, number] >([-180, -90, 180, 90]); - const [isLoading, setIsLoading] = useState(false); - const [totalPages, setTotalPages] = useState(1); - const [currentPage, setCurrentPage] = useState(1); - const [totalResults, setTotalResults] = useState(0); const [queryableFilters, setQueryableFilters] = useState< Record >({}); @@ -202,7 +206,8 @@ export function useStacGenericFilter({ return; } - setCurrentPage(1); + // Reset to page 1 + setResults(results, isLoading, totalPages, 1, totalResults); const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; @@ -256,7 +261,6 @@ export function useStacGenericFilter({ }; try { - setIsLoading(true); // Update context with loading state setResults(results, true, totalPages, currentPage, totalResults); const data = (await fetchWithProxies( @@ -271,8 +275,6 @@ export function useStacGenericFilter({ if (!data) { console.debug('STAC search failed -- no results found'); setResults([], false, 1, currentPage, 0); - setTotalPages(1); - setTotalResults(0); return; } @@ -321,9 +323,6 @@ export function useStacGenericFilter({ calculatedTotalResults = data.context.matched; } - setTotalPages(calculatedTotalPages); - setTotalResults(calculatedTotalResults); - // Update context with results setResults( sortedFeatures, @@ -335,6 +334,7 @@ export function useStacGenericFilter({ // Store pagination links for use in handlePaginationClick if (data.links) { + console.log('data.links', data.links); setPaginationLinks( data.links as Array< IStacLink & { method?: string; body?: Record } @@ -349,10 +349,6 @@ export function useStacGenericFilter({ } catch (error) { console.error('STAC search failed -- error fetching data:', error); setResults([], false, 1, currentPage, 0); - setTotalPages(1); - setTotalResults(0); - } finally { - setIsLoading(false); } }; @@ -398,7 +394,6 @@ export function useStacGenericFilter({ }; try { - setIsLoading(true); // Update context with loading state setResults(results, true, totalPages, currentPage, totalResults); const data = (await fetchWithProxies( @@ -413,8 +408,6 @@ export function useStacGenericFilter({ if (!data) { console.debug('STAC search failed -- no results found'); setResults([], false, 1, currentPage, 0); - setTotalPages(1); - setTotalResults(0); return; } @@ -464,9 +457,6 @@ export function useStacGenericFilter({ calculatedTotalResults = data.context.matched; } - setTotalPages(calculatedTotalPages); - setTotalResults(calculatedTotalResults); - // Update context with results setResults( sortedFeatures, @@ -478,6 +468,7 @@ export function useStacGenericFilter({ // Store pagination links for next pagination if (data.links) { + console.log('data.links', data.links); setPaginationLinks( data.links as Array< IStacLink & { method?: string; body?: Record } @@ -487,10 +478,6 @@ export function useStacGenericFilter({ } catch (error) { console.error('STAC search failed -- error fetching data:', error); setResults([], false, 1, currentPage, 0); - setTotalPages(1); - setTotalResults(0); - } finally { - setIsLoading(false); } }; @@ -504,7 +491,7 @@ export function useStacGenericFilter({ console.log('[useStacGenericFilter] Pagination click:', { dir, currentPage, - availableLinks: paginationLinksRef.current.map(l => l.rel), + availableLinks: paginationLinks.map(l => l.rel), }); if (!model) { return; @@ -519,7 +506,7 @@ export function useStacGenericFilter({ } // Use ref to get the latest paginationLinks value - const link = paginationLinksRef.current.find(l => l.rel === rel); + const link = paginationLinks.find(l => l.rel === rel); if (link && link.body) { console.log('[useStacGenericFilter] Found pagination link:', { @@ -531,7 +518,7 @@ export function useStacGenericFilter({ await fetchUsingLink(link); // Update current page after successful fetch if dir was a number if (typeof dir === 'number') { - setCurrentPage(dir); + setResults(results, isLoading, totalPages, dir, totalResults); } } else { // If no link found, we can't paginate @@ -556,10 +543,6 @@ export function useStacGenericFilter({ selectedCollection, setSelectedCollection, handleSubmit, - isLoading, - totalPages, - currentPage, - totalResults, handlePaginationClick, handleResultClick, formatResult, @@ -573,6 +556,5 @@ export function useStacGenericFilter({ updateQueryableFilter, filterOperator, setFilterOperator, - paginationLinks, }; } From a2f5c9804c8862a517c298f7ab65a67f230fb024 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 4 Dec 2025 16:21:25 +0100 Subject: [PATCH 17/70] woooooooooo --- .../components/StacGenericFilterPanel.tsx | 34 +- .../stacBrowser/hooks/useStacGenericFilter.ts | 406 +++++++++++------- 2 files changed, 259 insertions(+), 181 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index f537daa9a..ad3ab22be 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -35,9 +35,6 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { selectedCollection, setSelectedCollection, handleSubmit, - handlePaginationClick, - handleResultClick, - formatResult, startTime, endTime, setStartTime, @@ -58,37 +55,8 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { totalResults, paginationLinks, setPaginationLinks, - }); - - // Track handlers with refs to avoid infinite loops - const handlersRef = useRef({ - handlePaginationClick, - handleResultClick, - formatResult, - }); - - // Update ref when handlers change - useEffect(() => { - handlersRef.current = { - handlePaginationClick, - handleResultClick, - formatResult, - }; - }, [handlePaginationClick, handleResultClick, formatResult]); - - // Sync handlers separately, only when they actually change - useEffect(() => { - setPaginationHandlers( - handlersRef.current.handlePaginationClick, - handlersRef.current.handleResultClick, - handlersRef.current.formatResult, - ); - }, [ setPaginationHandlers, - handlePaginationClick, - handleResultClick, - formatResult, - ]); + }); if (!model) { diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index d57f825e3..66be553ad 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -51,6 +51,11 @@ interface IUseStacGenericFilterProps { setPaginationLinks: ( links: Array }>, ) => void; + setPaginationHandlers: ( + handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise, + handleResultClick: (id: string) => Promise, + formatResult: (item: IStacItem) => string, + ) => void; } export function useStacGenericFilter({ @@ -64,6 +69,7 @@ export function useStacGenericFilter({ totalResults, paginationLinks, setPaginationLinks, + setPaginationHandlers, }: IUseStacGenericFilterProps) { const { startTime, @@ -74,6 +80,18 @@ export function useStacGenericFilter({ setUseWorldBBox, } = useStacSearch({ model }); + // Use a ref to always access the latest paginationLinks value + const paginationLinksRef = useRef(paginationLinks); + useEffect(() => { + paginationLinksRef.current = paginationLinks; + const nextLink = paginationLinks.find(l => l.rel === 'next'); + // eslint-disable-next-line no-console + console.log( + '[paginationLinks updated] Next link body token:', + nextLink?.body?.token, + ); + }, [paginationLinks]); + const [queryableProps, setQueryableProps] = useState<[string, any][]>(); const [collections, setCollections] = useState([]); // ! temp @@ -113,7 +131,6 @@ export function useStacGenericFilter({ return titleA.localeCompare(titleB); }); - console.log('collections', collections); setCollections(collections); }; @@ -175,12 +192,9 @@ export function useStacGenericFilter({ const layerId = UUID.uuid4(); if (!stacData) { - console.error('Result not found:'); return; } - console.log('adding', layerId); - // const layerModel: IJGISLayer = { // type: 'StacLayer', // parameters: { data: stacData }, @@ -248,8 +262,6 @@ export function useStacGenericFilter({ }; } - console.log('body', body); - const options = { method: 'POST', headers: { @@ -273,7 +285,6 @@ export function useStacGenericFilter({ )) as IStacSearchResult; if (!data) { - console.debug('STAC search failed -- no results found'); setResults([], false, 1, currentPage, 0); return; } @@ -308,7 +319,6 @@ export function useStacGenericFilter({ }); } - console.log('hook data', data); // Sort features by id before setting results const sortedFeatures = [...data.features].sort((a, b) => a.id.localeCompare(b.id), @@ -334,12 +344,16 @@ export function useStacGenericFilter({ // Store pagination links for use in handlePaginationClick if (data.links) { - console.log('data.links', data.links); - setPaginationLinks( - data.links as Array< - IStacLink & { method?: string; body?: Record } - >, + const typedLinks = data.links as Array< + IStacLink & { method?: string; body?: Record } + >; + const nextLink = typedLinks.find(l => l.rel === 'next'); + // eslint-disable-next-line no-console + console.log( + '[handleSubmit] Next link body token:', + nextLink?.body?.token, ); + setPaginationLinks(typedLinks); } // Add first result to map @@ -347,7 +361,6 @@ export function useStacGenericFilter({ addToMap(data.features[0]); } } catch (error) { - console.error('STAC search failed -- error fetching data:', error); setResults([], false, 1, currentPage, 0); } }; @@ -370,163 +383,241 @@ export function useStacGenericFilter({ [results, model], ); + // Log when handleResultClick is recreated + useEffect(() => { + // eslint-disable-next-line no-console + console.log('[handleResultClick] Function recreated, dependencies:', { + resultsLength: results.length, + model: !!model, + }); + }, [handleResultClick, results, model]); + /** * Fetches results using a STAC link (for pagination) * @param link - STAC link object with href and optional body */ - const fetchUsingLink = async ( - link: IStacLink & { method?: string; body?: Record }, - ) => { - if (!model) { - return; - } - - const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; - - const options = { - method: (link.method || 'POST').toUpperCase(), - headers: { - 'Content-Type': 'application/json', - 'X-XSRFToken': XSRF_TOKEN, - credentials: 'include', - }, - body: link.body ? JSON.stringify(link.body) : undefined, - }; - - try { - // Update context with loading state - setResults(results, true, totalPages, currentPage, totalResults); - const data = (await fetchWithProxies( - link.href, - model, - async response => await response.json(), - //@ts-expect-error Jupyter requires X-XSRFToken header - options, - 'internal', - )) as IStacSearchResult; - - if (!data) { - console.debug('STAC search failed -- no results found'); - setResults([], false, 1, currentPage, 0); + const fetchUsingLink = useCallback( + async ( + link: IStacLink & { method?: string; body?: Record }, + ) => { + if (!model) { return; } - // Filter assets to only include items with 'overview' or 'thumbnail' roles - if (data.features && data.features.length > 0) { - data.features.forEach(feature => { - if (feature.assets) { - const originalAssets = feature.assets; - const filteredAssets: Record = {}; + const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; - for (const [key, asset] of Object.entries(originalAssets)) { - if ( - asset && - typeof asset === 'object' && - 'roles' in asset && - Array.isArray(asset.roles) - ) { - const roles = asset.roles; + const options = { + method: (link.method || 'POST').toUpperCase(), + headers: { + 'Content-Type': 'application/json', + 'X-XSRFToken': XSRF_TOKEN, + credentials: 'include', + }, + body: link.body ? JSON.stringify(link.body) : undefined, + }; - if (roles.includes('thumbnail') || roles.includes('overview')) { - filteredAssets[key] = asset; + try { + // Update context with loading state + setResults(results, true, totalPages, currentPage, totalResults); + const data = (await fetchWithProxies( + link.href, + model, + async response => await response.json(), + //@ts-expect-error Jupyter requires X-XSRFToken header + options, + 'internal', + )) as IStacSearchResult; + + if (!data) { + setResults([], false, 1, currentPage, 0); + return; + } + + // Filter assets to only include items with 'overview' or 'thumbnail' roles + if (data.features && data.features.length > 0) { + data.features.forEach(feature => { + if (feature.assets) { + const originalAssets = feature.assets; + const filteredAssets: Record = {}; + + for (const [key, asset] of Object.entries(originalAssets)) { + if ( + asset && + typeof asset === 'object' && + 'roles' in asset && + Array.isArray(asset.roles) + ) { + const roles = asset.roles; + + if ( + roles.includes('thumbnail') || + roles.includes('overview') + ) { + filteredAssets[key] = asset; + } } } - } - - feature.assets = filteredAssets; - } - }); - } - console.log('hook data link', data); - // Sort features by id before setting results - const sortedFeatures = [...data.features].sort((a, b) => - a.id.localeCompare(b.id), - ); - console.log('[useStacGenericFilter] Setting results from pagination:', { - featuresCount: sortedFeatures.length, - featureIds: sortedFeatures.map(f => f.id), - }); + feature.assets = filteredAssets; + } + }); + } - // Handle context if available (STAC API extension) - let calculatedTotalPages = 1; - let calculatedTotalResults = data.features.length; - if (data.context) { - const pages = data.context.matched / data.context.limit; - calculatedTotalPages = Math.ceil(pages); - calculatedTotalResults = data.context.matched; - } + // Sort features by id before setting results + const sortedFeatures = [...data.features].sort((a, b) => + a.id.localeCompare(b.id), + ); - // Update context with results - setResults( - sortedFeatures, - false, - calculatedTotalPages, - currentPage, - calculatedTotalResults, - ); + // Handle context if available (STAC API extension) + let calculatedTotalPages = 1; + let calculatedTotalResults = data.features.length; + if (data.context) { + const pages = data.context.matched / data.context.limit; + calculatedTotalPages = Math.ceil(pages); + calculatedTotalResults = data.context.matched; + } + + // Update context with results + setResults( + sortedFeatures, + false, + calculatedTotalPages, + currentPage, + calculatedTotalResults, + ); - // Store pagination links for next pagination - if (data.links) { - console.log('data.links', data.links); - setPaginationLinks( - data.links as Array< + // Store pagination links for next pagination + if (data.links) { + const typedLinks = data.links as Array< IStacLink & { method?: string; body?: Record } - >, - ); + >; + const nextLink = typedLinks.find(l => l.rel === 'next'); + // eslint-disable-next-line no-console + console.log( + '[fetchUsingLink] Next link body token:', + nextLink?.body?.token, + ); + // Update ref synchronously before updating context + paginationLinksRef.current = typedLinks; + setPaginationLinks(typedLinks); + } + } catch (error) { + setResults([], false, 1, currentPage, 0); } - } catch (error) { - console.error('STAC search failed -- error fetching data:', error); - setResults([], false, 1, currentPage, 0); - } - }; + }, + [ + model, + results, + isLoading, + totalPages, + currentPage, + totalResults, + setResults, + setPaginationLinks, + ], + ); + + // Log when fetchUsingLink is recreated + useEffect(() => { + // eslint-disable-next-line no-console + console.log('[fetchUsingLink] Function recreated, dependencies:', { + model: !!model, + resultsLength: results.length, + isLoading, + totalPages, + currentPage, + totalResults, + }); + }, [ + fetchUsingLink, + model, + results, + isLoading, + totalPages, + currentPage, + totalResults, + ]); /** * Handles pagination clicks * @param dir - Direction ('next' | 'previous') or page number (for backward compatibility) */ - const handlePaginationClick = async ( - dir: 'next' | 'previous' | number, - ): Promise => { - console.log('[useStacGenericFilter] Pagination click:', { - dir, - currentPage, - availableLinks: paginationLinks.map(l => l.rel), - }); - if (!model) { - return; - } - - // If dir is a number, convert it to 'next' or 'previous' based on current page - let rel: 'next' | 'previous'; - if (typeof dir === 'number') { - rel = dir > currentPage ? 'next' : 'previous'; - } else { - rel = dir; - } + const handlePaginationClick = useCallback( + async (dir: 'next' | 'previous' | number): Promise => { + // Always use ref to get the latest paginationLinks value + const currentLinks = paginationLinksRef.current; + const nextLink = currentLinks.find(l => l.rel === 'next'); + // eslint-disable-next-line no-console + console.log( + '[handlePaginationClick] Next link body token (from ref):', + nextLink?.body?.token, + 'ref length:', + currentLinks.length, + ); - // Use ref to get the latest paginationLinks value - const link = paginationLinks.find(l => l.rel === rel); + if (!model) { + return; + } - if (link && link.body) { - console.log('[useStacGenericFilter] Found pagination link:', { - rel: link.rel, - href: link.href, - hasBody: !!link.body, - }); - // Use the link with its body (contains token) to fetch the page - await fetchUsingLink(link); - // Update current page after successful fetch if dir was a number + // If dir is a number, convert it to 'next' or 'previous' based on current page + let rel: 'next' | 'previous'; if (typeof dir === 'number') { - setResults(results, isLoading, totalPages, dir, totalResults); + rel = dir > currentPage ? 'next' : 'previous'; + } else { + rel = dir; } - } else { - // If no link found, we can't paginate - console.warn( - `[useStacGenericFilter] No ${rel} link available for pagination`, - ); - } - }; + + // Find the pagination link using the ref + const link = currentLinks.find(l => l.rel === rel); + + if (link && link.body) { + // eslint-disable-next-line no-console + console.log( + '[handlePaginationClick] Using link with token:', + link.body.token, + ); + // Use the link with its body (contains token) to fetch the page + await fetchUsingLink(link); + // Update current page after successful fetch if dir was a number + if (typeof dir === 'number') { + setResults(results, isLoading, totalPages, dir, totalResults); + } + } + }, + [ + model, + currentPage, + results, + isLoading, + totalPages, + totalResults, + setResults, + fetchUsingLink, + ], + ); + + // Log when handlePaginationClick is recreated + useEffect(() => { + // eslint-disable-next-line no-console + console.log('[handlePaginationClick] Function recreated, dependencies:', { + model: !!model, + currentPage, + resultsLength: results.length, + isLoading, + totalPages, + totalResults, + fetchUsingLinkChanged: true, + }); + }, [ + handlePaginationClick, + model, + currentPage, + results, + isLoading, + totalPages, + totalResults, + fetchUsingLink, + ]); /** * Formats a result item for display @@ -537,15 +628,34 @@ export function useStacGenericFilter({ return item.properties?.title ?? item.id; }, []); + // Log when formatResult is recreated (should rarely happen since it has no deps) + useEffect(() => { + // eslint-disable-next-line no-console + console.log('[formatResult] Function recreated'); + }, [formatResult]); + + // Sync handlers to context whenever they change + // Also sync when paginationLinks change to ensure handler in context has latest links via ref + useEffect(() => { + setPaginationHandlers( + handlePaginationClick, + handleResultClick, + formatResult, + ); + }, [ + handlePaginationClick, + handleResultClick, + formatResult, + setPaginationHandlers, + paginationLinks, // Sync when links change so context gets updated handler + ]); + return { queryableProps, collections, selectedCollection, setSelectedCollection, handleSubmit, - handlePaginationClick, - handleResultClick, - formatResult, startTime, endTime, setStartTime, From 8b3975da1b6477cca55ef09a7d2b29fce45e0e9a Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 4 Dec 2025 16:46:02 +0100 Subject: [PATCH 18/70] Remove debug logs --- .../stacBrowser/hooks/useStacGenericFilter.ts | 91 +------------------ 1 file changed, 1 insertion(+), 90 deletions(-) diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 66be553ad..1b74e9bc9 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -84,12 +84,6 @@ export function useStacGenericFilter({ const paginationLinksRef = useRef(paginationLinks); useEffect(() => { paginationLinksRef.current = paginationLinks; - const nextLink = paginationLinks.find(l => l.rel === 'next'); - // eslint-disable-next-line no-console - console.log( - '[paginationLinks updated] Next link body token:', - nextLink?.body?.token, - ); }, [paginationLinks]); const [queryableProps, setQueryableProps] = useState<[string, any][]>(); @@ -347,12 +341,6 @@ export function useStacGenericFilter({ const typedLinks = data.links as Array< IStacLink & { method?: string; body?: Record } >; - const nextLink = typedLinks.find(l => l.rel === 'next'); - // eslint-disable-next-line no-console - console.log( - '[handleSubmit] Next link body token:', - nextLink?.body?.token, - ); setPaginationLinks(typedLinks); } @@ -383,15 +371,6 @@ export function useStacGenericFilter({ [results, model], ); - // Log when handleResultClick is recreated - useEffect(() => { - // eslint-disable-next-line no-console - console.log('[handleResultClick] Function recreated, dependencies:', { - resultsLength: results.length, - model: !!model, - }); - }, [handleResultClick, results, model]); - /** * Fetches results using a STAC link (for pagination) * @param link - STAC link object with href and optional body @@ -491,12 +470,7 @@ export function useStacGenericFilter({ const typedLinks = data.links as Array< IStacLink & { method?: string; body?: Record } >; - const nextLink = typedLinks.find(l => l.rel === 'next'); - // eslint-disable-next-line no-console - console.log( - '[fetchUsingLink] Next link body token:', - nextLink?.body?.token, - ); + // Update ref synchronously before updating context paginationLinksRef.current = typedLinks; setPaginationLinks(typedLinks); @@ -517,27 +491,6 @@ export function useStacGenericFilter({ ], ); - // Log when fetchUsingLink is recreated - useEffect(() => { - // eslint-disable-next-line no-console - console.log('[fetchUsingLink] Function recreated, dependencies:', { - model: !!model, - resultsLength: results.length, - isLoading, - totalPages, - currentPage, - totalResults, - }); - }, [ - fetchUsingLink, - model, - results, - isLoading, - totalPages, - currentPage, - totalResults, - ]); - /** * Handles pagination clicks * @param dir - Direction ('next' | 'previous') or page number (for backward compatibility) @@ -546,14 +499,6 @@ export function useStacGenericFilter({ async (dir: 'next' | 'previous' | number): Promise => { // Always use ref to get the latest paginationLinks value const currentLinks = paginationLinksRef.current; - const nextLink = currentLinks.find(l => l.rel === 'next'); - // eslint-disable-next-line no-console - console.log( - '[handlePaginationClick] Next link body token (from ref):', - nextLink?.body?.token, - 'ref length:', - currentLinks.length, - ); if (!model) { return; @@ -571,11 +516,6 @@ export function useStacGenericFilter({ const link = currentLinks.find(l => l.rel === rel); if (link && link.body) { - // eslint-disable-next-line no-console - console.log( - '[handlePaginationClick] Using link with token:', - link.body.token, - ); // Use the link with its body (contains token) to fetch the page await fetchUsingLink(link); // Update current page after successful fetch if dir was a number @@ -596,29 +536,6 @@ export function useStacGenericFilter({ ], ); - // Log when handlePaginationClick is recreated - useEffect(() => { - // eslint-disable-next-line no-console - console.log('[handlePaginationClick] Function recreated, dependencies:', { - model: !!model, - currentPage, - resultsLength: results.length, - isLoading, - totalPages, - totalResults, - fetchUsingLinkChanged: true, - }); - }, [ - handlePaginationClick, - model, - currentPage, - results, - isLoading, - totalPages, - totalResults, - fetchUsingLink, - ]); - /** * Formats a result item for display * @param item - STAC item to format @@ -628,12 +545,6 @@ export function useStacGenericFilter({ return item.properties?.title ?? item.id; }, []); - // Log when formatResult is recreated (should rarely happen since it has no deps) - useEffect(() => { - // eslint-disable-next-line no-console - console.log('[formatResult] Function recreated'); - }, [formatResult]); - // Sync handlers to context whenever they change // Also sync when paginationLinks change to ensure handler in context has latest links via ref useEffect(() => { From 06b2b0d84ef866894afe8f1883f634b3d836b1e7 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 4 Dec 2025 16:54:33 +0100 Subject: [PATCH 19/70] Restore add to map --- .../src/stacBrowser/hooks/useFilterSearch.ts | 553 ++++++++++++++++++ .../src/stacBrowser/hooks/useGeodesSearch.ts | 306 ++++++++++ .../stacBrowser/hooks/useStacGenericFilter.ts | 33 +- 3 files changed, 878 insertions(+), 14 deletions(-) create mode 100644 packages/base/src/stacBrowser/hooks/useFilterSearch.ts create mode 100644 packages/base/src/stacBrowser/hooks/useGeodesSearch.ts diff --git a/packages/base/src/stacBrowser/hooks/useFilterSearch.ts b/packages/base/src/stacBrowser/hooks/useFilterSearch.ts new file mode 100644 index 000000000..40a977507 --- /dev/null +++ b/packages/base/src/stacBrowser/hooks/useFilterSearch.ts @@ -0,0 +1,553 @@ +// import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; +// import { UUID } from '@lumino/coreutils'; +// import { endOfToday, startOfToday } from 'date-fns'; +// import { useCallback, useEffect, useRef, useState } from 'react'; + +// import { fetchWithProxies } from '@/src/tools'; +// import { useGeneric } from './useStacSearch'; +// import { +// IStacCollection, +// IStacItem, +// IStacLink, +// IStacSearchResult, +// } from '../types/types'; + +// type FilteredCollection = Pick; + +// export type Operator = '=' | '!=' | '<' | '>'; + +// export type FilterOperator = 'and' | 'or'; + +// export interface IQueryableFilter { +// operator: Operator; +// inputValue: string | number | undefined; +// } + +// export type UpdateQueryableFilter = ( +// qKey: string, +// filter: IQueryableFilter, +// ) => void; + +// const API_URL = 'https://stac.dataspace.copernicus.eu/v1/'; + +// interface IUseStacGenericFilterProps { +// model?: IJupyterGISModel; +// limit?: number; +// setResults: ( +// results: IStacItem[], +// isLoading: boolean, +// totalPages: number, +// currentPage: number, +// totalResults: number, +// ) => void; +// results: IStacItem[]; +// isLoading: boolean; +// totalPages: number; +// currentPage: number; +// totalResults: number; +// paginationLinks: Array< +// IStacLink & { method?: string; body?: Record } +// >; +// setPaginationLinks: ( +// links: Array }>, +// ) => void; +// setPaginationHandlers: ( +// handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise, +// handleResultClick: (id: string) => Promise, +// formatResult: (item: IStacItem) => string, +// ) => void; +// } + +// export function useStacGenericFilter({ +// model, +// limit = 12, +// setResults, +// results, +// isLoading, +// totalPages, +// currentPage, +// totalResults, +// paginationLinks, +// setPaginationLinks, +// setPaginationHandlers, +// }: IUseStacGenericFilterProps) { +// const { +// startTime, +// endTime, +// setStartTime, +// setEndTime, +// currentBBox, +// useWorldBBox, +// setUseWorldBBox, +// } = useGeneric({ model }); + +// // Use a ref to always access the latest paginationLinks value +// const paginationLinksRef = useRef(paginationLinks); +// useEffect(() => { +// paginationLinksRef.current = paginationLinks; +// }, [paginationLinks]); + +// const [queryableProps, setQueryableProps] = useState<[string, any][]>(); +// const [collections, setCollections] = useState([]); +// // ! temp +// const [selectedCollection, setSelectedCollection] = +// useState('sentinel-2-l2a'); +// const [queryableFilters, setQueryableFilters] = useState< +// Record +// >({}); +// const [filterOperator, setFilterOperator] = useState('and'); + +// // for collections +// useEffect(() => { +// if (!model) { +// return; +// } + +// const fatch = async () => { +// const data = await fetchWithProxies( +// API_URL + 'collections', +// model, +// async response => await response.json(), +// undefined, +// 'internal', +// ); + +// const collections: FilteredCollection[] = data.collections +// .map((collection: any) => ({ +// title: collection.title ?? collection.id, +// id: collection.id, +// })) +// .sort((a: FilteredCollection, b: FilteredCollection) => { +// const titleA = a.title?.toLowerCase() ?? ''; +// const titleB = b.title?.toLowerCase() ?? ''; +// return titleA.localeCompare(titleB); +// }); + +// setCollections(collections); +// }; + +// fatch(); +// }, [model]); + +// // for queryables +// // should listen for colletion changes and requery +// // need a way to handle querying multiple collections without refetching everything +// // collection id -> queryables map as a basic cache thing?? +// useEffect(() => { +// if (!model) { +// return; +// } + +// const fatch = async () => { +// const data = await fetchWithProxies( +// API_URL + 'queryables', +// model, +// async response => await response.json(), +// undefined, +// 'internal', +// ); + +// setQueryableProps(Object.entries(data.properties)); +// }; + +// fatch(); +// }, [model]); + +// const addToMap = (stacData: any) => { +// console.log('add to amp'); +// if (!model) { +// return; +// } + +// const layerId = UUID.uuid4(); + +// if (!stacData) { +// return; +// } + +// const layerModel: IJGISLayer = { +// type: 'StacLayer', +// parameters: { data: stacData }, +// visible: true, +// name: stacData.properties.title ?? stacData.id, +// }; + +// model.addLayer(layerId, layerModel); +// }; + +// const updateQueryableFilter = useCallback( +// (qKey: string, filter: IQueryableFilter) => { +// setQueryableFilters(prev => ({ +// ...prev, +// [qKey]: filter, +// })); +// }, +// [], +// ); + +// const handleSubmit = async () => { +// if (!model) { +// return; +// } + +// // Reset to page 1 +// setResults(results, isLoading, totalPages, 1, totalResults); + +// const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; + +// const st = startTime +// ? startTime.toISOString() +// : startOfToday().toISOString(); + +// const et = endTime ? endTime.toISOString() : endOfToday().toISOString(); + +// // Build filter object from queryableFilters +// const filterConditions = Object.entries(queryableFilters) +// .filter(([, filter]) => filter.inputValue !== undefined) +// .map(([property, filter]) => { +// return { +// op: filter.operator, +// args: [ +// { +// property, +// }, +// filter.inputValue, +// ], +// }; +// }); + +// const body: Record = { +// bbox: currentBBox, +// collections: [selectedCollection], +// datetime: `${st}/${et}`, +// limit, +// 'filter-lang': 'cql2-json', +// }; + +// // Only add filter if there are any conditions +// if (filterConditions.length > 0) { +// body.filter = { +// op: filterOperator, +// args: filterConditions, +// }; +// } + +// const options = { +// method: 'POST', +// headers: { +// 'Content-Type': 'application/json', +// 'X-XSRFToken': XSRF_TOKEN, +// credentials: 'include', +// }, +// body: JSON.stringify(body), +// }; + +// try { +// // Update context with loading state +// setResults(results, true, totalPages, currentPage, totalResults); +// const data = (await fetchWithProxies( +// 'https://stac.dataspace.copernicus.eu/v1/search', +// model, +// async response => await response.json(), +// //@ts-expect-error Jupyter requires X-XSRFToken header +// options, +// 'internal', +// )) as IStacSearchResult; + +// if (!data) { +// setResults([], false, 1, currentPage, 0); +// return; +// } + +// // Filter assets to only include items with 'overview' or 'thumbnail' roles +// // ? is this a good idea?? +// if (data.features && data.features.length > 0) { +// data.features.forEach(feature => { +// if (feature.assets) { +// const originalAssets = feature.assets; +// const filteredAssets: Record = {}; + +// // Iterate through each asset in the assets object +// for (const [key, asset] of Object.entries(originalAssets)) { +// if ( +// asset && +// typeof asset === 'object' && +// 'roles' in asset && +// Array.isArray(asset.roles) +// ) { +// const roles = asset.roles; + +// if (roles.includes('thumbnail') || roles.includes('overview')) { +// filteredAssets[key] = asset; +// } +// } +// } + +// // Replace assets with filtered version +// feature.assets = filteredAssets; +// } +// }); +// } + +// // Sort features by id before setting results +// const sortedFeatures = [...data.features].sort((a, b) => +// a.id.localeCompare(b.id), +// ); + +// // Handle context if available (STAC API extension) +// let calculatedTotalPages = 1; +// let calculatedTotalResults = data.features.length; +// if (data.context) { +// const pages = data.context.matched / data.context.limit; +// calculatedTotalPages = Math.ceil(pages); +// calculatedTotalResults = data.context.matched; +// } + +// // Update context with results +// setResults( +// sortedFeatures, +// false, +// calculatedTotalPages, +// currentPage, +// calculatedTotalResults, +// ); + +// // Store pagination links for use in handlePaginationClick +// if (data.links) { +// const typedLinks = data.links as Array< +// IStacLink & { method?: string; body?: Record } +// >; +// setPaginationLinks(typedLinks); +// } +// } catch (error) { +// setResults([], false, 1, currentPage, 0); +// } +// }; + +// /** +// * Handles clicking on a result item +// * @param id - ID of the clicked result +// */ +// const handleResultClick = useCallback( +// async (id: string): Promise => { +// console.log('handle reuslt cliks'); +// if (!model) { +// return; +// } + +// const result = results.find((r: IStacItem) => r.id === id); +// if (result) { +// addToMap(result); +// } +// }, +// [results, model], +// ); + +// /** +// * Fetches results using a STAC link (for pagination) +// * @param link - STAC link object with href and optional body +// */ +// const fetchUsingLink = useCallback( +// async ( +// link: IStacLink & { method?: string; body?: Record }, +// ) => { +// if (!model) { +// return; +// } + +// const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; + +// const options = { +// method: (link.method || 'POST').toUpperCase(), +// headers: { +// 'Content-Type': 'application/json', +// 'X-XSRFToken': XSRF_TOKEN, +// credentials: 'include', +// }, +// body: link.body ? JSON.stringify(link.body) : undefined, +// }; + +// try { +// // Update context with loading state +// setResults(results, true, totalPages, currentPage, totalResults); +// const data = (await fetchWithProxies( +// link.href, +// model, +// async response => await response.json(), +// //@ts-expect-error Jupyter requires X-XSRFToken header +// options, +// 'internal', +// )) as IStacSearchResult; + +// if (!data) { +// setResults([], false, 1, currentPage, 0); +// return; +// } + +// // Filter assets to only include items with 'overview' or 'thumbnail' roles +// if (data.features && data.features.length > 0) { +// data.features.forEach(feature => { +// if (feature.assets) { +// const originalAssets = feature.assets; +// const filteredAssets: Record = {}; + +// for (const [key, asset] of Object.entries(originalAssets)) { +// if ( +// asset && +// typeof asset === 'object' && +// 'roles' in asset && +// Array.isArray(asset.roles) +// ) { +// const roles = asset.roles; + +// if ( +// roles.includes('thumbnail') || +// roles.includes('overview') +// ) { +// filteredAssets[key] = asset; +// } +// } +// } + +// feature.assets = filteredAssets; +// } +// }); +// } + +// // Sort features by id before setting results +// const sortedFeatures = [...data.features].sort((a, b) => +// a.id.localeCompare(b.id), +// ); + +// // Handle context if available (STAC API extension) +// let calculatedTotalPages = 1; +// let calculatedTotalResults = data.features.length; +// if (data.context) { +// const pages = data.context.matched / data.context.limit; +// calculatedTotalPages = Math.ceil(pages); +// calculatedTotalResults = data.context.matched; +// } + +// // Update context with results +// setResults( +// sortedFeatures, +// false, +// calculatedTotalPages, +// currentPage, +// calculatedTotalResults, +// ); + +// // Store pagination links for next pagination +// if (data.links) { +// const typedLinks = data.links as Array< +// IStacLink & { method?: string; body?: Record } +// >; + +// // Update ref synchronously before updating context +// paginationLinksRef.current = typedLinks; +// setPaginationLinks(typedLinks); +// } +// } catch (error) { +// setResults([], false, 1, currentPage, 0); +// } +// }, +// [ +// model, +// results, +// isLoading, +// totalPages, +// currentPage, +// totalResults, +// setResults, +// setPaginationLinks, +// ], +// ); + +// /** +// * Handles pagination clicks +// * @param dir - Direction ('next' | 'previous') or page number (for backward compatibility) +// */ +// const handlePaginationClick = useCallback( +// async (dir: 'next' | 'previous' | number): Promise => { +// // Always use ref to get the latest paginationLinks value +// const currentLinks = paginationLinksRef.current; + +// if (!model) { +// return; +// } + +// // If dir is a number, convert it to 'next' or 'previous' based on current page +// let rel: 'next' | 'previous'; +// if (typeof dir === 'number') { +// rel = dir > currentPage ? 'next' : 'previous'; +// } else { +// rel = dir; +// } + +// // Find the pagination link using the ref +// const link = currentLinks.find(l => l.rel === rel); + +// if (link && link.body) { +// // Use the link with its body (contains token) to fetch the page +// await fetchUsingLink(link); +// // Update current page after successful fetch if dir was a number +// if (typeof dir === 'number') { +// setResults(results, isLoading, totalPages, dir, totalResults); +// } +// } +// }, +// [ +// model, +// currentPage, +// results, +// isLoading, +// totalPages, +// totalResults, +// setResults, +// fetchUsingLink, +// ], +// ); + +// /** +// * Formats a result item for display +// * @param item - STAC item to format +// * @returns Formatted string representation of the item +// */ +// const formatResult = useCallback((item: IStacItem): string => { +// return item.properties?.title ?? item.id; +// }, []); + +// // Sync handlers to context whenever they change +// // Also sync when paginationLinks change to ensure handler in context has latest links via ref +// useEffect(() => { +// setPaginationHandlers( +// handlePaginationClick, +// handleResultClick, +// formatResult, +// ); +// }, [ +// handlePaginationClick, +// handleResultClick, +// formatResult, +// setPaginationHandlers, +// paginationLinks, // Sync when links change so context gets updated handler +// ]); + +// return { +// queryableProps, +// collections, +// selectedCollection, +// setSelectedCollection, +// handleSubmit, +// startTime, +// endTime, +// setStartTime, +// setEndTime, +// useWorldBBox, +// setUseWorldBBox, +// queryableFilters, +// updateQueryableFilter, +// filterOperator, +// setFilterOperator, +// }; +// } diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts new file mode 100644 index 000000000..fd688cfa3 --- /dev/null +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -0,0 +1,306 @@ +// import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; +// import { UUID } from '@lumino/coreutils'; +// import { startOfYesterday } from 'date-fns'; +// import { useCallback, useEffect, useState } from 'react'; + +// import useIsFirstRender from '@/src/shared/hooks/useIsFirstRender'; +// import { products } from '@/src/stacBrowser/constants'; +// import { +// IStacItem, +// IStacLink, +// IStacQueryBody, +// IStacSearchResult, +// StacFilterState, +// StacFilterSetters, +// StacFilterStateStateDb, +// } from '@/src/stacBrowser/types/types'; +// import { GlobalStateDbManager } from '@/src/store'; +// import { fetchWithProxies } from '@/src/tools'; +// import { useGeneric } from './useStacSearch'; + +// interface IUseStacSearchProps { +// model: IJupyterGISModel | undefined; +// } + +// // ! TODO factor out common bits +// interface IUseStacSearchReturn { +// filterState: StacFilterState; +// filterSetters: StacFilterSetters; +// results: IStacItem[]; +// startTime: Date | undefined; +// setStartTime: (date: Date | undefined) => void; +// endTime: Date | undefined; +// setEndTime: (date: Date | undefined) => void; +// totalPages: number; +// currentPage: number; +// totalResults: number; +// handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise; +// handleResultClick: (id: string) => Promise; +// formatResult: (item: IStacItem) => string; +// isLoading: boolean; +// useWorldBBox: boolean; +// setUseWorldBBox: (val: boolean) => void; +// paginationLinks: Array< +// IStacLink & { method?: string; body?: Record } +// >; +// setPaginationLinks: ( +// links: Array }>, +// ) => void; +// } + +// const API_URL = 'https://geodes-portal.cnes.fr/api/stac/search'; +// const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; +// const STAC_FILTERS_KEY = 'jupytergis:stac-filters'; + +// /** +// * Custom hook for managing STAC search functionality +// * @param props - Configuration object containing datasets, platforms, products, and model +// * @returns Object containing state and handlers for STAC search +// */ +// function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { +// const isFirstRender = useIsFirstRender(); +// const stateDb = GlobalStateDbManager.getInstance().getStateDb(); + +// // Get generic state from useGeneric hook +// const { +// startTime, +// setStartTime, +// endTime, +// setEndTime, +// currentBBox, +// useWorldBBox, +// setUseWorldBBox, +// } = useGeneric({ model }); + +// const [results, setResults] = useState([]); +// const [isLoading, setIsLoading] = useState(false); +// const [totalPages, setTotalPages] = useState(1); +// const [currentPage, setCurrentPage] = useState(1); +// const [totalResults, setTotalResults] = useState(0); +// const [paginationLinks, setPaginationLinks] = useState< +// Array }> +// >([]); +// const [filterState, setFilterState] = useState({ +// collections: new Set(), +// datasets: new Set(), +// platforms: new Set(), +// products: new Set(), +// }); + +// const filterSetters: StacFilterSetters = { +// collections: val => +// setFilterState(s => ({ ...s, collections: new Set(val) })), +// datasets: val => setFilterState(s => ({ ...s, datasets: new Set(val) })), +// platforms: val => setFilterState(s => ({ ...s, platforms: new Set(val) })), +// products: val => setFilterState(s => ({ ...s, products: new Set(val) })), +// }; + +// // On mount, fetch filterState and times from StateDB (if present) +// useEffect(() => { +// async function loadStacStateFromDb() { +// const savedFilterState = (await stateDb?.fetch( +// STAC_FILTERS_KEY, +// )) as StacFilterStateStateDb; + +// setFilterState({ +// collections: new Set((savedFilterState?.collections as string[]) ?? []), +// datasets: new Set((savedFilterState?.datasets as string[]) ?? []), +// platforms: new Set((savedFilterState?.platforms as string[]) ?? []), +// products: new Set((savedFilterState?.products as string[]) ?? []), +// }); +// } + +// loadStacStateFromDb(); +// }, [stateDb]); + +// // Save filterState to StateDB on change +// useEffect(() => { +// async function saveStacFilterStateToDb() { +// await stateDb?.save(STAC_FILTERS_KEY, { +// collections: Array.from(filterState.collections), +// datasets: Array.from(filterState.datasets), +// platforms: Array.from(filterState.platforms), +// products: Array.from(filterState.products), +// }); +// } + +// saveStacFilterStateToDb(); +// }, [filterState, stateDb]); + +// // Handle search when filters change +// useEffect(() => { +// if (model && !isFirstRender && filterState.datasets.size > 0) { +// setCurrentPage(1); +// fetchResults(1); +// } +// }, [filterState, startTime, endTime, currentBBox]); + +// const fetchResults = async (page = 1) => { +// const processingLevel = new Set(); +// const productType = new Set(); + +// filterState.products.forEach(productCode => { +// products +// .filter(product => product.productCode === productCode) +// .forEach(product => { +// if (product.processingLevel) { +// processingLevel.add(product.processingLevel); +// } +// if (product.productType) { +// product.productType.forEach(type => productType.add(type)); +// } +// }); +// }); + +// const body: IStacQueryBody = { +// bbox: currentBBox, +// limit: 12, +// page, +// query: { +// latest: { eq: true }, +// dataset: { in: Array.from(filterState.datasets) }, +// end_datetime: { +// gte: startTime +// ? startTime.toISOString() +// : startOfYesterday().toISOString(), +// }, +// ...(endTime && { +// start_datetime: { lte: endTime.toISOString() }, +// }), +// ...(filterState.platforms.size > 0 && { +// platform: { in: Array.from(filterState.platforms) }, +// }), +// ...(processingLevel.size > 0 && { +// 'processing:level': { in: Array.from(processingLevel) }, +// }), +// ...(productType.size > 0 && { +// 'product:type': { in: Array.from(productType) }, +// }), +// }, +// sortBy: [{ direction: 'desc', field: 'start_datetime' }], +// }; + +// try { +// setIsLoading(true); +// const options = { +// method: 'POST', +// headers: { +// 'Content-Type': 'application/json', +// 'X-XSRFToken': XSRF_TOKEN, +// credentials: 'include', +// }, +// body: JSON.stringify(body), +// }; + +// if (!model) { +// return; +// } + +// const data = (await fetchWithProxies( +// API_URL, +// model, +// async response => await response.json(), +// //@ts-expect-error Jupyter requires X-XSRFToken header +// options, +// 'internal', +// )) as IStacSearchResult; + +// if (!data) { +// console.debug('STAC search failed -- no results found'); +// setResults([]); +// setTotalPages(1); +// setTotalResults(0); +// return; +// } + +// setResults(data.features); +// const pages = data.context.matched / data.context.limit; +// setTotalPages(Math.ceil(pages)); +// setTotalResults(data.context.matched); +// } catch (error) { +// console.error('STAC search failed -- error fetching data:', error); +// setResults([]); +// setTotalPages(1); +// setTotalResults(0); +// } finally { +// setIsLoading(false); +// } +// }; + +// /** +// * Handles clicking on a result item +// * @param id - ID of the clicked result +// */ +// const handleResultClick = async (id: string): Promise => { +// if (!results) { +// return; +// } + +// const layerId = UUID.uuid4(); +// const stacData = results.find(item => item.id === id); + +// if (!stacData) { +// console.error('Result not found:', id); +// return; +// } + +// const layerModel: IJGISLayer = { +// type: 'StacLayer', +// parameters: { data: stacData }, +// visible: true, +// name: stacData.properties.title ?? stacData.id, +// }; + +// model && model.addLayer(layerId, layerModel); +// }; + +// /** +// * Handles pagination clicks +// * @param dir - Direction ('next' | 'previous') or page number to navigate to +// */ +// const handlePaginationClick = async ( +// dir: 'next' | 'previous' | number, +// ): Promise => { +// if (typeof dir === 'number') { +// setCurrentPage(dir); +// model && fetchResults(dir); +// } else { +// // For 'next' or 'previous', calculate the page number +// const newPage = dir === 'next' ? currentPage + 1 : currentPage - 1; +// setCurrentPage(newPage); +// model && fetchResults(newPage); +// } +// }; + +// /** +// * Formats a result item for display +// * @param item - STAC item to format +// * @returns Formatted string representation of the item +// */ +// const formatResult = (item: IStacItem): string => { +// return item.properties.title ?? item.id; +// }; + +// return { +// filterState, +// filterSetters, +// results, +// startTime, +// setStartTime, +// endTime, +// setEndTime, +// totalPages, +// currentPage, +// totalResults, +// handlePaginationClick, +// handleResultClick, +// formatResult, +// isLoading, +// useWorldBBox, +// setUseWorldBBox, +// paginationLinks, +// setPaginationLinks, +// }; +// } + +// export default useStacSearch; diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 1b74e9bc9..22bebf462 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -86,6 +86,12 @@ export function useStacGenericFilter({ paginationLinksRef.current = paginationLinks; }, [paginationLinks]); + // Use a ref to always access the latest results value + const resultsRef = useRef(results); + useEffect(() => { + resultsRef.current = results; + }, [results]); + const [queryableProps, setQueryableProps] = useState<[string, any][]>(); const [collections, setCollections] = useState([]); // ! temp @@ -179,6 +185,7 @@ export function useStacGenericFilter({ }, [model, useWorldBBox]); const addToMap = (stacData: any) => { + console.log('add to amp'); if (!model) { return; } @@ -189,14 +196,14 @@ export function useStacGenericFilter({ return; } - // const layerModel: IJGISLayer = { - // type: 'StacLayer', - // parameters: { data: stacData }, - // visible: true, - // name: stacData.properties.title ?? stacData.id, - // }; + const layerModel: IJGISLayer = { + type: 'StacLayer', + parameters: { data: stacData }, + visible: true, + name: stacData.properties.title ?? stacData.id, + }; - // model.addLayer(layerId, layerModel); + model.addLayer(layerId, layerModel); }; const updateQueryableFilter = useCallback( @@ -343,11 +350,6 @@ export function useStacGenericFilter({ >; setPaginationLinks(typedLinks); } - - // Add first result to map - if (data.features.length > 0) { - addToMap(data.features[0]); - } } catch (error) { setResults([], false, 1, currentPage, 0); } @@ -359,16 +361,19 @@ export function useStacGenericFilter({ */ const handleResultClick = useCallback( async (id: string): Promise => { + console.log('handle reuslt cliks'); if (!model) { return; } - const result = results.find((r: IStacItem) => r.id === id); + // Always use ref to get the latest results value + const currentResults = resultsRef.current; + const result = currentResults.find((r: IStacItem) => r.id === id); if (result) { addToMap(result); } }, - [results, model], + [model], ); /** From cb36a02b92cabad757b5b82622fee672bb84b4de Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 5 Dec 2025 11:30:10 +0100 Subject: [PATCH 20/70] working mostly --- .../components/StacGenericFilterPanel.tsx | 6 +- .../src/stacBrowser/components/StacPanel.tsx | 2 +- .../geodes/StacGeodesFilterPanel.tsx | 31 ++-- .../context/StacResultsContext.tsx | 170 +++++++++++++++--- .../stacBrowser/hooks/useStacGenericFilter.ts | 122 ++----------- 5 files changed, 173 insertions(+), 158 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index ad3ab22be..05061a545 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -23,9 +23,10 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { totalPages, currentPage, totalResults, - setPaginationHandlers, setPaginationLinks, paginationLinks, + registerFetchUsingLink, + registerAddToMap, } = useStacResultsContext(); const [limit, setLimit] = useState(12); @@ -55,7 +56,8 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { totalResults, paginationLinks, setPaginationLinks, - setPaginationHandlers, + registerFetchUsingLink, + registerAddToMap, }); diff --git a/packages/base/src/stacBrowser/components/StacPanel.tsx b/packages/base/src/stacBrowser/components/StacPanel.tsx index 9a25813f3..92a3d46b7 100644 --- a/packages/base/src/stacBrowser/components/StacPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacPanel.tsx @@ -73,7 +73,7 @@ const StacPanelContent = ({ model }: IStacViewProps) => { // Outer component that provides the context const StacPanel = ({ model }: IStacViewProps) => { return ( - + ); diff --git a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx index 5df229842..1a21abc5d 100644 --- a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx @@ -48,35 +48,24 @@ const StacGeodesFilterPanel = ({ model }: IStacGeodesFilterPanelProps) => { paginationLinks, } = useStacSearch({ model }); - // Track handlers with refs to avoid infinite loops - const handlersRef = useRef({ - handlePaginationClick, - handleResultClick, - formatResult, - }); - - // Update ref when handlers change - useEffect(() => { - handlersRef.current = { - handlePaginationClick, - handleResultClick, - formatResult, - }; - }, [handlePaginationClick, handleResultClick, formatResult]); - // Sync results to context whenever they change useEffect(() => { setResults(results, isLoading, totalPages, currentPage, totalResults); }, [results, isLoading, totalPages, currentPage, totalResults, setResults]); - // Sync handlers separately, only when they actually change + // Sync handlers to context (GEODES has its own handlers) useEffect(() => { setPaginationHandlers( - handlersRef.current.handlePaginationClick, - handlersRef.current.handleResultClick, - handlersRef.current.formatResult, + handlePaginationClick, + handleResultClick, + formatResult, ); - }, [setPaginationHandlers]); + }, [ + handlePaginationClick, + handleResultClick, + formatResult, + setPaginationHandlers, + ]); // Sync pagination links to context whenever they change useEffect(() => { diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index 60e12f563..a4b6cb67e 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -1,9 +1,11 @@ +import { IJupyterGISModel } from '@jupytergis/schema'; import React, { createContext, useContext, useState, useCallback, ReactNode, + useRef, } from 'react'; import { IStacItem, IStacLink } from '../types/types'; @@ -27,14 +29,22 @@ interface IStacResultsContext { currentPage: number, totalResults: number, ) => void; + setPaginationLinks: ( + links: Array }>, + ) => void; + // Register hook-specific functions that handlers need (for generic filter) + registerFetchUsingLink: ( + fetchFn: ( + link: IStacLink & { method?: string; body?: Record }, + ) => Promise, + ) => void; + registerAddToMap: (addFn: (stacData: IStacItem) => void) => void; + // Set handlers from outside (for GEODES search) setPaginationHandlers: ( handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise, handleResultClick: (id: string) => Promise, formatResult: (item: IStacItem) => string, ) => void; - setPaginationLinks: ( - links: Array }>, - ) => void; } const StacResultsContext = createContext( @@ -43,26 +53,35 @@ const StacResultsContext = createContext( interface IStacResultsProviderProps { children: ReactNode; + model?: IJupyterGISModel; } -export function StacResultsProvider({ children }: IStacResultsProviderProps) { +export function StacResultsProvider({ + children, + model, +}: IStacResultsProviderProps) { const [results, setResultsState] = useState([]); const [isLoading, setIsLoading] = useState(false); const [totalPages, setTotalPages] = useState(1); const [currentPage, setCurrentPage] = useState(1); const [totalResults, setTotalResults] = useState(0); - const [handlePaginationClick, setHandlePaginationClick] = useState< - (dir: 'next' | 'previous' | number) => Promise - >(async () => {}); - const [handleResultClick, setHandleResultClick] = useState< - (id: string) => Promise - >(async () => {}); - const [formatResult, setFormatResult] = useState<(item: IStacItem) => string>( - () => (item: IStacItem) => item.id, - ); const [paginationLinks, setPaginationLinksState] = useState< Array }> >([]); + const [externalHandlers, setExternalHandlers] = useState<{ + handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise; + handleResultClick: (id: string) => Promise; + formatResult: (item: IStacItem) => string; + } | null>(null); + + // Store hook-specific functions in refs (these are set by the hooks) + const fetchUsingLinkRef = + useRef< + ( + link: IStacLink & { method?: string; body?: Record }, + ) => Promise + >(); + const addToMapRef = useRef<(stacData: IStacItem) => void>(); const setResults = useCallback( ( @@ -81,6 +100,34 @@ export function StacResultsProvider({ children }: IStacResultsProviderProps) { [], ); + const setPaginationLinks = useCallback( + ( + links: Array }>, + ) => { + setPaginationLinksState(links); + }, + [], + ); + + // Register functions from hooks + const registerFetchUsingLink = useCallback( + ( + fetchFn: ( + link: IStacLink & { method?: string; body?: Record }, + ) => Promise, + ) => { + fetchUsingLinkRef.current = fetchFn; + }, + [], + ); + + const registerAddToMap = useCallback( + (addFn: (stacData: IStacItem) => void) => { + addToMapRef.current = addFn; + }, + [], + ); + const setPaginationHandlers = useCallback( ( newHandlePaginationClick: ( @@ -89,22 +136,91 @@ export function StacResultsProvider({ children }: IStacResultsProviderProps) { newHandleResultClick: (id: string) => Promise, newFormatResult: (item: IStacItem) => string, ) => { - setHandlePaginationClick(() => newHandlePaginationClick); - setHandleResultClick(() => newHandleResultClick); - setFormatResult(() => newFormatResult); + setExternalHandlers({ + handlePaginationClick: newHandlePaginationClick, + handleResultClick: newHandleResultClick, + formatResult: newFormatResult, + }); }, [], ); - const setPaginationLinks = useCallback( - ( - links: Array }>, - ) => { - setPaginationLinksState(links); + // Handlers created in context - always read latest state directly + // Use external handlers if provided, otherwise use context-created ones + const handlePaginationClick = useCallback( + async (dir: 'next' | 'previous' | number): Promise => { + if (!model) { + return; + } + + // Read directly from state - no closure issues! + const currentLinks = paginationLinks; + const currentPageValue = currentPage; + + // If dir is a number, convert it to 'next' or 'previous' based on current page + let rel: 'next' | 'previous'; + if (typeof dir === 'number') { + rel = dir > currentPageValue ? 'next' : 'previous'; + } else { + rel = dir; + } + + // Find the pagination link + const link = currentLinks.find(l => l.rel === rel); + + if (link && link.body && fetchUsingLinkRef.current) { + // Use the registered fetch function + await fetchUsingLinkRef.current(link); + // Update current page after successful fetch if dir was a number + if (typeof dir === 'number') { + setResults(results, isLoading, totalPages, dir, totalResults); + } + } }, - [], + [ + model, + paginationLinks, // Direct dependency - no ref needed! + currentPage, + results, + isLoading, + totalPages, + totalResults, + setResults, + ], + ); + + const handleResultClick = useCallback( + async (id: string): Promise => { + if (!model) { + return; + } + + // Read directly from state - no closure issues! + const currentResults = results; + const result = currentResults.find((r: IStacItem) => r.id === id); + + if (result && addToMapRef.current) { + addToMapRef.current(result); + } + }, + [model, results], // Direct dependency - no ref needed! ); + const formatResult = useCallback((item: IStacItem): string => { + return item.properties?.title ?? item.id; + }, []); + + // Use external handlers if provided, otherwise use context-created ones + const finalHandlePaginationClick = externalHandlers + ? externalHandlers.handlePaginationClick + : handlePaginationClick; + const finalHandleResultClick = externalHandlers + ? externalHandlers.handleResultClick + : handleResultClick; + const finalFormatResult = externalHandlers + ? externalHandlers.formatResult + : formatResult; + return ( {children} diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 22bebf462..9ec44fa08 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -51,11 +51,12 @@ interface IUseStacGenericFilterProps { setPaginationLinks: ( links: Array }>, ) => void; - setPaginationHandlers: ( - handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise, - handleResultClick: (id: string) => Promise, - formatResult: (item: IStacItem) => string, + registerFetchUsingLink: ( + fetchFn: ( + link: IStacLink & { method?: string; body?: Record }, + ) => Promise, ) => void; + registerAddToMap: (addFn: (stacData: IStacItem) => void) => void; } export function useStacGenericFilter({ @@ -69,7 +70,8 @@ export function useStacGenericFilter({ totalResults, paginationLinks, setPaginationLinks, - setPaginationHandlers, + registerFetchUsingLink, + registerAddToMap, }: IUseStacGenericFilterProps) { const { startTime, @@ -80,18 +82,6 @@ export function useStacGenericFilter({ setUseWorldBBox, } = useStacSearch({ model }); - // Use a ref to always access the latest paginationLinks value - const paginationLinksRef = useRef(paginationLinks); - useEffect(() => { - paginationLinksRef.current = paginationLinks; - }, [paginationLinks]); - - // Use a ref to always access the latest results value - const resultsRef = useRef(results); - useEffect(() => { - resultsRef.current = results; - }, [results]); - const [queryableProps, setQueryableProps] = useState<[string, any][]>(); const [collections, setCollections] = useState([]); // ! temp @@ -343,7 +333,7 @@ export function useStacGenericFilter({ calculatedTotalResults, ); - // Store pagination links for use in handlePaginationClick + // Store pagination links if (data.links) { const typedLinks = data.links as Array< IStacLink & { method?: string; body?: Record } @@ -355,27 +345,6 @@ export function useStacGenericFilter({ } }; - /** - * Handles clicking on a result item - * @param id - ID of the clicked result - */ - const handleResultClick = useCallback( - async (id: string): Promise => { - console.log('handle reuslt cliks'); - if (!model) { - return; - } - - // Always use ref to get the latest results value - const currentResults = resultsRef.current; - const result = currentResults.find((r: IStacItem) => r.id === id); - if (result) { - addToMap(result); - } - }, - [model], - ); - /** * Fetches results using a STAC link (for pagination) * @param link - STAC link object with href and optional body @@ -476,8 +445,6 @@ export function useStacGenericFilter({ IStacLink & { method?: string; body?: Record } >; - // Update ref synchronously before updating context - paginationLinksRef.current = typedLinks; setPaginationLinks(typedLinks); } } catch (error) { @@ -496,75 +463,14 @@ export function useStacGenericFilter({ ], ); - /** - * Handles pagination clicks - * @param dir - Direction ('next' | 'previous') or page number (for backward compatibility) - */ - const handlePaginationClick = useCallback( - async (dir: 'next' | 'previous' | number): Promise => { - // Always use ref to get the latest paginationLinks value - const currentLinks = paginationLinksRef.current; - - if (!model) { - return; - } - - // If dir is a number, convert it to 'next' or 'previous' based on current page - let rel: 'next' | 'previous'; - if (typeof dir === 'number') { - rel = dir > currentPage ? 'next' : 'previous'; - } else { - rel = dir; - } - - // Find the pagination link using the ref - const link = currentLinks.find(l => l.rel === rel); - - if (link && link.body) { - // Use the link with its body (contains token) to fetch the page - await fetchUsingLink(link); - // Update current page after successful fetch if dir was a number - if (typeof dir === 'number') { - setResults(results, isLoading, totalPages, dir, totalResults); - } - } - }, - [ - model, - currentPage, - results, - isLoading, - totalPages, - totalResults, - setResults, - fetchUsingLink, - ], - ); - - /** - * Formats a result item for display - * @param item - STAC item to format - * @returns Formatted string representation of the item - */ - const formatResult = useCallback((item: IStacItem): string => { - return item.properties?.title ?? item.id; - }, []); + // Register functions with context so handlers can use them + useEffect(() => { + registerFetchUsingLink(fetchUsingLink); + }, [fetchUsingLink, registerFetchUsingLink]); - // Sync handlers to context whenever they change - // Also sync when paginationLinks change to ensure handler in context has latest links via ref useEffect(() => { - setPaginationHandlers( - handlePaginationClick, - handleResultClick, - formatResult, - ); - }, [ - handlePaginationClick, - handleResultClick, - formatResult, - setPaginationHandlers, - paginationLinks, // Sync when links change so context gets updated handler - ]); + registerAddToMap(addToMap); + }, [addToMap, registerAddToMap]); return { queryableProps, From fa68f73b3ac6d85d6a9289cb439103f3c438de2d Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 5 Dec 2025 14:27:11 +0100 Subject: [PATCH 21/70] Move common stuff to generic hook --- packages/base/src/panelview/leftpanel.tsx | 5 - .../geodes/StacGeodesFilterPanel.tsx | 2 +- .../src/stacBrowser/hooks/useGeodesSearch.ts | 550 +++++++++--------- .../stacBrowser/hooks/useStacGenericFilter.ts | 2 +- .../src/stacBrowser/hooks/useStacSearch.ts | 284 +-------- yarn.lock | 2 +- 6 files changed, 291 insertions(+), 554 deletions(-) diff --git a/packages/base/src/panelview/leftpanel.tsx b/packages/base/src/panelview/leftpanel.tsx index e0ce4431d..9ccedc7d2 100644 --- a/packages/base/src/panelview/leftpanel.tsx +++ b/packages/base/src/panelview/leftpanel.tsx @@ -210,11 +210,6 @@ export const LeftPanel: React.FC = ( > )} - {!settings.stacBrowserDisabled && ( - - - - )} {!settings.stacBrowserDisabled && ( diff --git a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx index 1a21abc5d..d8f13afb9 100644 --- a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx @@ -18,7 +18,7 @@ import { products as productsList, } from '@/src/stacBrowser/constants'; import { useStacResultsContext } from '@/src/stacBrowser/context/StacResultsContext'; -import useStacSearch from '@/src/stacBrowser/hooks/useStacSearch'; +import useStacSearch from '@/src/stacBrowser/hooks/useGeodesSearch'; interface IStacGeodesFilterPanelProps { model?: IJupyterGISModel; diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index fd688cfa3..8c6623c54 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -1,306 +1,306 @@ -// import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; -// import { UUID } from '@lumino/coreutils'; -// import { startOfYesterday } from 'date-fns'; -// import { useCallback, useEffect, useState } from 'react'; +import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; +import { UUID } from '@lumino/coreutils'; +import { startOfYesterday } from 'date-fns'; +import { useCallback, useEffect, useState } from 'react'; -// import useIsFirstRender from '@/src/shared/hooks/useIsFirstRender'; -// import { products } from '@/src/stacBrowser/constants'; -// import { -// IStacItem, -// IStacLink, -// IStacQueryBody, -// IStacSearchResult, -// StacFilterState, -// StacFilterSetters, -// StacFilterStateStateDb, -// } from '@/src/stacBrowser/types/types'; -// import { GlobalStateDbManager } from '@/src/store'; -// import { fetchWithProxies } from '@/src/tools'; -// import { useGeneric } from './useStacSearch'; +import useIsFirstRender from '@/src/shared/hooks/useIsFirstRender'; +import { products } from '@/src/stacBrowser/constants'; +import { + IStacItem, + IStacLink, + IStacQueryBody, + IStacSearchResult, + StacFilterState, + StacFilterSetters, + StacFilterStateStateDb, +} from '@/src/stacBrowser/types/types'; +import { GlobalStateDbManager } from '@/src/store'; +import { fetchWithProxies } from '@/src/tools'; +import { useGeneric } from './useStacSearch'; -// interface IUseStacSearchProps { -// model: IJupyterGISModel | undefined; -// } +interface IUseStacSearchProps { + model: IJupyterGISModel | undefined; +} -// // ! TODO factor out common bits -// interface IUseStacSearchReturn { -// filterState: StacFilterState; -// filterSetters: StacFilterSetters; -// results: IStacItem[]; -// startTime: Date | undefined; -// setStartTime: (date: Date | undefined) => void; -// endTime: Date | undefined; -// setEndTime: (date: Date | undefined) => void; -// totalPages: number; -// currentPage: number; -// totalResults: number; -// handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise; -// handleResultClick: (id: string) => Promise; -// formatResult: (item: IStacItem) => string; -// isLoading: boolean; -// useWorldBBox: boolean; -// setUseWorldBBox: (val: boolean) => void; -// paginationLinks: Array< -// IStacLink & { method?: string; body?: Record } -// >; -// setPaginationLinks: ( -// links: Array }>, -// ) => void; -// } +// ! TODO factor out common bits +interface IUseStacSearchReturn { + filterState: StacFilterState; + filterSetters: StacFilterSetters; + results: IStacItem[]; + startTime: Date | undefined; + setStartTime: (date: Date | undefined) => void; + endTime: Date | undefined; + setEndTime: (date: Date | undefined) => void; + totalPages: number; + currentPage: number; + totalResults: number; + handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise; + handleResultClick: (id: string) => Promise; + formatResult: (item: IStacItem) => string; + isLoading: boolean; + useWorldBBox: boolean; + setUseWorldBBox: (val: boolean) => void; + paginationLinks: Array< + IStacLink & { method?: string; body?: Record } + >; + setPaginationLinks: ( + links: Array }>, + ) => void; +} -// const API_URL = 'https://geodes-portal.cnes.fr/api/stac/search'; -// const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; -// const STAC_FILTERS_KEY = 'jupytergis:stac-filters'; +const API_URL = 'https://geodes-portal.cnes.fr/api/stac/search'; +const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; +const STAC_FILTERS_KEY = 'jupytergis:stac-filters'; -// /** -// * Custom hook for managing STAC search functionality -// * @param props - Configuration object containing datasets, platforms, products, and model -// * @returns Object containing state and handlers for STAC search -// */ -// function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { -// const isFirstRender = useIsFirstRender(); -// const stateDb = GlobalStateDbManager.getInstance().getStateDb(); +/** + * Custom hook for managing STAC search functionality + * @param props - Configuration object containing datasets, platforms, products, and model + * @returns Object containing state and handlers for STAC search + */ +function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { + const isFirstRender = useIsFirstRender(); + const stateDb = GlobalStateDbManager.getInstance().getStateDb(); -// // Get generic state from useGeneric hook -// const { -// startTime, -// setStartTime, -// endTime, -// setEndTime, -// currentBBox, -// useWorldBBox, -// setUseWorldBBox, -// } = useGeneric({ model }); + // Get generic state from useGeneric hook + const { + startTime, + setStartTime, + endTime, + setEndTime, + currentBBox, + useWorldBBox, + setUseWorldBBox, + } = useGeneric({ model }); -// const [results, setResults] = useState([]); -// const [isLoading, setIsLoading] = useState(false); -// const [totalPages, setTotalPages] = useState(1); -// const [currentPage, setCurrentPage] = useState(1); -// const [totalResults, setTotalResults] = useState(0); -// const [paginationLinks, setPaginationLinks] = useState< -// Array }> -// >([]); -// const [filterState, setFilterState] = useState({ -// collections: new Set(), -// datasets: new Set(), -// platforms: new Set(), -// products: new Set(), -// }); + const [results, setResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [totalPages, setTotalPages] = useState(1); + const [currentPage, setCurrentPage] = useState(1); + const [totalResults, setTotalResults] = useState(0); + const [paginationLinks, setPaginationLinks] = useState< + Array }> + >([]); + const [filterState, setFilterState] = useState({ + collections: new Set(), + datasets: new Set(), + platforms: new Set(), + products: new Set(), + }); -// const filterSetters: StacFilterSetters = { -// collections: val => -// setFilterState(s => ({ ...s, collections: new Set(val) })), -// datasets: val => setFilterState(s => ({ ...s, datasets: new Set(val) })), -// platforms: val => setFilterState(s => ({ ...s, platforms: new Set(val) })), -// products: val => setFilterState(s => ({ ...s, products: new Set(val) })), -// }; + const filterSetters: StacFilterSetters = { + collections: val => + setFilterState(s => ({ ...s, collections: new Set(val) })), + datasets: val => setFilterState(s => ({ ...s, datasets: new Set(val) })), + platforms: val => setFilterState(s => ({ ...s, platforms: new Set(val) })), + products: val => setFilterState(s => ({ ...s, products: new Set(val) })), + }; -// // On mount, fetch filterState and times from StateDB (if present) -// useEffect(() => { -// async function loadStacStateFromDb() { -// const savedFilterState = (await stateDb?.fetch( -// STAC_FILTERS_KEY, -// )) as StacFilterStateStateDb; + // On mount, fetch filterState and times from StateDB (if present) + useEffect(() => { + async function loadStacStateFromDb() { + const savedFilterState = (await stateDb?.fetch( + STAC_FILTERS_KEY, + )) as StacFilterStateStateDb; -// setFilterState({ -// collections: new Set((savedFilterState?.collections as string[]) ?? []), -// datasets: new Set((savedFilterState?.datasets as string[]) ?? []), -// platforms: new Set((savedFilterState?.platforms as string[]) ?? []), -// products: new Set((savedFilterState?.products as string[]) ?? []), -// }); -// } + setFilterState({ + collections: new Set((savedFilterState?.collections as string[]) ?? []), + datasets: new Set((savedFilterState?.datasets as string[]) ?? []), + platforms: new Set((savedFilterState?.platforms as string[]) ?? []), + products: new Set((savedFilterState?.products as string[]) ?? []), + }); + } -// loadStacStateFromDb(); -// }, [stateDb]); + loadStacStateFromDb(); + }, [stateDb]); -// // Save filterState to StateDB on change -// useEffect(() => { -// async function saveStacFilterStateToDb() { -// await stateDb?.save(STAC_FILTERS_KEY, { -// collections: Array.from(filterState.collections), -// datasets: Array.from(filterState.datasets), -// platforms: Array.from(filterState.platforms), -// products: Array.from(filterState.products), -// }); -// } + // Save filterState to StateDB on change + useEffect(() => { + async function saveStacFilterStateToDb() { + await stateDb?.save(STAC_FILTERS_KEY, { + collections: Array.from(filterState.collections), + datasets: Array.from(filterState.datasets), + platforms: Array.from(filterState.platforms), + products: Array.from(filterState.products), + }); + } -// saveStacFilterStateToDb(); -// }, [filterState, stateDb]); + saveStacFilterStateToDb(); + }, [filterState, stateDb]); -// // Handle search when filters change -// useEffect(() => { -// if (model && !isFirstRender && filterState.datasets.size > 0) { -// setCurrentPage(1); -// fetchResults(1); -// } -// }, [filterState, startTime, endTime, currentBBox]); + // Handle search when filters change + useEffect(() => { + if (model && !isFirstRender && filterState.datasets.size > 0) { + setCurrentPage(1); + fetchResults(1); + } + }, [filterState, startTime, endTime, currentBBox]); -// const fetchResults = async (page = 1) => { -// const processingLevel = new Set(); -// const productType = new Set(); + const fetchResults = async (page = 1) => { + const processingLevel = new Set(); + const productType = new Set(); -// filterState.products.forEach(productCode => { -// products -// .filter(product => product.productCode === productCode) -// .forEach(product => { -// if (product.processingLevel) { -// processingLevel.add(product.processingLevel); -// } -// if (product.productType) { -// product.productType.forEach(type => productType.add(type)); -// } -// }); -// }); + filterState.products.forEach(productCode => { + products + .filter(product => product.productCode === productCode) + .forEach(product => { + if (product.processingLevel) { + processingLevel.add(product.processingLevel); + } + if (product.productType) { + product.productType.forEach(type => productType.add(type)); + } + }); + }); -// const body: IStacQueryBody = { -// bbox: currentBBox, -// limit: 12, -// page, -// query: { -// latest: { eq: true }, -// dataset: { in: Array.from(filterState.datasets) }, -// end_datetime: { -// gte: startTime -// ? startTime.toISOString() -// : startOfYesterday().toISOString(), -// }, -// ...(endTime && { -// start_datetime: { lte: endTime.toISOString() }, -// }), -// ...(filterState.platforms.size > 0 && { -// platform: { in: Array.from(filterState.platforms) }, -// }), -// ...(processingLevel.size > 0 && { -// 'processing:level': { in: Array.from(processingLevel) }, -// }), -// ...(productType.size > 0 && { -// 'product:type': { in: Array.from(productType) }, -// }), -// }, -// sortBy: [{ direction: 'desc', field: 'start_datetime' }], -// }; + const body: IStacQueryBody = { + bbox: currentBBox, + limit: 12, + page, + query: { + latest: { eq: true }, + dataset: { in: Array.from(filterState.datasets) }, + end_datetime: { + gte: startTime + ? startTime.toISOString() + : startOfYesterday().toISOString(), + }, + ...(endTime && { + start_datetime: { lte: endTime.toISOString() }, + }), + ...(filterState.platforms.size > 0 && { + platform: { in: Array.from(filterState.platforms) }, + }), + ...(processingLevel.size > 0 && { + 'processing:level': { in: Array.from(processingLevel) }, + }), + ...(productType.size > 0 && { + 'product:type': { in: Array.from(productType) }, + }), + }, + sortBy: [{ direction: 'desc', field: 'start_datetime' }], + }; -// try { -// setIsLoading(true); -// const options = { -// method: 'POST', -// headers: { -// 'Content-Type': 'application/json', -// 'X-XSRFToken': XSRF_TOKEN, -// credentials: 'include', -// }, -// body: JSON.stringify(body), -// }; + try { + setIsLoading(true); + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-XSRFToken': XSRF_TOKEN, + credentials: 'include', + }, + body: JSON.stringify(body), + }; -// if (!model) { -// return; -// } + if (!model) { + return; + } -// const data = (await fetchWithProxies( -// API_URL, -// model, -// async response => await response.json(), -// //@ts-expect-error Jupyter requires X-XSRFToken header -// options, -// 'internal', -// )) as IStacSearchResult; + const data = (await fetchWithProxies( + API_URL, + model, + async response => await response.json(), + //@ts-expect-error Jupyter requires X-XSRFToken header + options, + 'internal', + )) as IStacSearchResult; -// if (!data) { -// console.debug('STAC search failed -- no results found'); -// setResults([]); -// setTotalPages(1); -// setTotalResults(0); -// return; -// } + if (!data) { + console.debug('STAC search failed -- no results found'); + setResults([]); + setTotalPages(1); + setTotalResults(0); + return; + } -// setResults(data.features); -// const pages = data.context.matched / data.context.limit; -// setTotalPages(Math.ceil(pages)); -// setTotalResults(data.context.matched); -// } catch (error) { -// console.error('STAC search failed -- error fetching data:', error); -// setResults([]); -// setTotalPages(1); -// setTotalResults(0); -// } finally { -// setIsLoading(false); -// } -// }; + setResults(data.features); + const pages = data.context.matched / data.context.limit; + setTotalPages(Math.ceil(pages)); + setTotalResults(data.context.matched); + } catch (error) { + console.error('STAC search failed -- error fetching data:', error); + setResults([]); + setTotalPages(1); + setTotalResults(0); + } finally { + setIsLoading(false); + } + }; -// /** -// * Handles clicking on a result item -// * @param id - ID of the clicked result -// */ -// const handleResultClick = async (id: string): Promise => { -// if (!results) { -// return; -// } + /** + * Handles clicking on a result item + * @param id - ID of the clicked result + */ + const handleResultClick = async (id: string): Promise => { + if (!results) { + return; + } -// const layerId = UUID.uuid4(); -// const stacData = results.find(item => item.id === id); + const layerId = UUID.uuid4(); + const stacData = results.find(item => item.id === id); -// if (!stacData) { -// console.error('Result not found:', id); -// return; -// } + if (!stacData) { + console.error('Result not found:', id); + return; + } -// const layerModel: IJGISLayer = { -// type: 'StacLayer', -// parameters: { data: stacData }, -// visible: true, -// name: stacData.properties.title ?? stacData.id, -// }; + const layerModel: IJGISLayer = { + type: 'StacLayer', + parameters: { data: stacData }, + visible: true, + name: stacData.properties.title ?? stacData.id, + }; -// model && model.addLayer(layerId, layerModel); -// }; + model && model.addLayer(layerId, layerModel); + }; -// /** -// * Handles pagination clicks -// * @param dir - Direction ('next' | 'previous') or page number to navigate to -// */ -// const handlePaginationClick = async ( -// dir: 'next' | 'previous' | number, -// ): Promise => { -// if (typeof dir === 'number') { -// setCurrentPage(dir); -// model && fetchResults(dir); -// } else { -// // For 'next' or 'previous', calculate the page number -// const newPage = dir === 'next' ? currentPage + 1 : currentPage - 1; -// setCurrentPage(newPage); -// model && fetchResults(newPage); -// } -// }; + /** + * Handles pagination clicks + * @param dir - Direction ('next' | 'previous') or page number to navigate to + */ + const handlePaginationClick = async ( + dir: 'next' | 'previous' | number, + ): Promise => { + if (typeof dir === 'number') { + setCurrentPage(dir); + model && fetchResults(dir); + } else { + // For 'next' or 'previous', calculate the page number + const newPage = dir === 'next' ? currentPage + 1 : currentPage - 1; + setCurrentPage(newPage); + model && fetchResults(newPage); + } + }; -// /** -// * Formats a result item for display -// * @param item - STAC item to format -// * @returns Formatted string representation of the item -// */ -// const formatResult = (item: IStacItem): string => { -// return item.properties.title ?? item.id; -// }; + /** + * Formats a result item for display + * @param item - STAC item to format + * @returns Formatted string representation of the item + */ + const formatResult = (item: IStacItem): string => { + return item.properties.title ?? item.id; + }; -// return { -// filterState, -// filterSetters, -// results, -// startTime, -// setStartTime, -// endTime, -// setEndTime, -// totalPages, -// currentPage, -// totalResults, -// handlePaginationClick, -// handleResultClick, -// formatResult, -// isLoading, -// useWorldBBox, -// setUseWorldBBox, -// paginationLinks, -// setPaginationLinks, -// }; -// } + return { + filterState, + filterSetters, + results, + startTime, + setStartTime, + endTime, + setEndTime, + totalPages, + currentPage, + totalResults, + handlePaginationClick, + handleResultClick, + formatResult, + isLoading, + useWorldBBox, + setUseWorldBBox, + paginationLinks, + setPaginationLinks, + }; +} -// export default useStacSearch; +export default useStacSearch; diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 9ec44fa08..f684f562a 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -4,7 +4,7 @@ import { endOfToday, startOfToday } from 'date-fns'; import { useCallback, useEffect, useRef, useState } from 'react'; import { fetchWithProxies } from '@/src/tools'; -import useStacSearch from './useStacSearch'; +import useStacSearch from './useGeodesSearch'; import { IStacCollection, IStacItem, diff --git a/packages/base/src/stacBrowser/hooks/useStacSearch.ts b/packages/base/src/stacBrowser/hooks/useStacSearch.ts index 9b32262f0..903d3ca36 100644 --- a/packages/base/src/stacBrowser/hooks/useStacSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useStacSearch.ts @@ -1,138 +1,38 @@ -import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; -import { UUID } from '@lumino/coreutils'; -import { startOfYesterday } from 'date-fns'; -import { useCallback, useEffect, useState } from 'react'; +import { IJupyterGISModel } from '@jupytergis/schema'; +import { useEffect, useState } from 'react'; -import useIsFirstRender from '@/src/shared/hooks/useIsFirstRender'; -import { products } from '@/src/stacBrowser/constants'; -import { - IStacItem, - IStacLink, - IStacQueryBody, - IStacSearchResult, - StacFilterState, - StacFilterSetters, - StacFilterStateStateDb, -} from '@/src/stacBrowser/types/types'; -import { GlobalStateDbManager } from '@/src/store'; -import { fetchWithProxies } from '@/src/tools'; - -interface IUseStacSearchProps { +interface IUseGenericProps { model: IJupyterGISModel | undefined; } -// ! TODO factor out common bits -interface IUseStacSearchReturn { - filterState: StacFilterState; - filterSetters: StacFilterSetters; - results: IStacItem[]; +interface IUseGenericReturn { startTime: Date | undefined; setStartTime: (date: Date | undefined) => void; endTime: Date | undefined; setEndTime: (date: Date | undefined) => void; - totalPages: number; - currentPage: number; - totalResults: number; - handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise; - handleResultClick: (id: string) => Promise; - formatResult: (item: IStacItem) => string; - isLoading: boolean; + currentBBox: [number, number, number, number]; + setCurrentBBox: (bbox: [number, number, number, number]) => void; useWorldBBox: boolean; setUseWorldBBox: (val: boolean) => void; - paginationLinks: Array< - IStacLink & { method?: string; body?: Record } - >; - setPaginationLinks: ( - links: Array }>, - ) => void; } -const API_URL = 'https://geodes-portal.cnes.fr/api/stac/search'; -const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; -const STAC_FILTERS_KEY = 'jupytergis:stac-filters'; - /** - * Custom hook for managing STAC search functionality - * @param props - Configuration object containing datasets, platforms, products, and model - * @returns Object containing state and handlers for STAC search + * Custom hook for managing generic STAC search state (temporal and spatial filters) + * @param props - Configuration object containing model + * @returns Object containing state and setters for temporal and spatial filters */ -function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { - const isFirstRender = useIsFirstRender(); - const stateDb = GlobalStateDbManager.getInstance().getStateDb(); - - const [results, setResults] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [totalPages, setTotalPages] = useState(1); - const [currentPage, setCurrentPage] = useState(1); - const [totalResults, setTotalResults] = useState(0); +export function useGeneric({ model }: IUseGenericProps): IUseGenericReturn { const [startTime, setStartTime] = useState(undefined); const [endTime, setEndTime] = useState(undefined); const [currentBBox, setCurrentBBox] = useState< [number, number, number, number] >([-180, -90, 180, 90]); const [useWorldBBox, setUseWorldBBox] = useState(false); - const [paginationLinks, setPaginationLinks] = useState< - Array }> - >([]); - const [filterState, setFilterState] = useState({ - collections: new Set(), - datasets: new Set(), - platforms: new Set(), - products: new Set(), - }); - - const filterSetters: StacFilterSetters = { - collections: val => - setFilterState(s => ({ ...s, collections: new Set(val) })), - datasets: val => setFilterState(s => ({ ...s, datasets: new Set(val) })), - platforms: val => setFilterState(s => ({ ...s, platforms: new Set(val) })), - products: val => setFilterState(s => ({ ...s, products: new Set(val) })), - }; - - // On mount, fetch filterState and times from StateDB (if present) - useEffect(() => { - async function loadStacStateFromDb() { - const savedFilterState = (await stateDb?.fetch( - STAC_FILTERS_KEY, - )) as StacFilterStateStateDb; - - setFilterState({ - collections: new Set((savedFilterState?.collections as string[]) ?? []), - datasets: new Set((savedFilterState?.datasets as string[]) ?? []), - platforms: new Set((savedFilterState?.platforms as string[]) ?? []), - products: new Set((savedFilterState?.products as string[]) ?? []), - }); - } - - loadStacStateFromDb(); - }, [stateDb]); - - // Save filterState to StateDB on change - useEffect(() => { - async function saveStacFilterStateToDb() { - await stateDb?.save(STAC_FILTERS_KEY, { - collections: Array.from(filterState.collections), - datasets: Array.from(filterState.datasets), - platforms: Array.from(filterState.platforms), - products: Array.from(filterState.products), - }); - } - - saveStacFilterStateToDb(); - }, [filterState, stateDb]); - - // Handle search when filters change - useEffect(() => { - if (model && !isFirstRender && filterState.datasets.size > 0) { - setCurrentPage(1); - fetchResults(1); - } - }, [filterState, startTime, endTime, currentBBox]); // Listen for model updates to get current bounding box useEffect(() => { const listenToModel = ( - sender: IJupyterGISModel, + _sender: IJupyterGISModel, bBoxIn4326: [number, number, number, number], ) => { if (useWorldBBox) { @@ -149,172 +49,14 @@ function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { }; }, [model, useWorldBBox]); - const fetchResults = async (page = 1) => { - const processingLevel = new Set(); - const productType = new Set(); - - filterState.products.forEach(productCode => { - products - .filter(product => product.productCode === productCode) - .forEach(product => { - if (product.processingLevel) { - processingLevel.add(product.processingLevel); - } - if (product.productType) { - product.productType.forEach(type => productType.add(type)); - } - }); - }); - - const body: IStacQueryBody = { - bbox: currentBBox, - limit: 12, - page, - query: { - latest: { eq: true }, - dataset: { in: Array.from(filterState.datasets) }, - end_datetime: { - gte: startTime - ? startTime.toISOString() - : startOfYesterday().toISOString(), - }, - ...(endTime && { - start_datetime: { lte: endTime.toISOString() }, - }), - ...(filterState.platforms.size > 0 && { - platform: { in: Array.from(filterState.platforms) }, - }), - ...(processingLevel.size > 0 && { - 'processing:level': { in: Array.from(processingLevel) }, - }), - ...(productType.size > 0 && { - 'product:type': { in: Array.from(productType) }, - }), - }, - sortBy: [{ direction: 'desc', field: 'start_datetime' }], - }; - - try { - setIsLoading(true); - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-XSRFToken': XSRF_TOKEN, - credentials: 'include', - }, - body: JSON.stringify(body), - }; - - if (!model) { - return; - } - - const data = (await fetchWithProxies( - API_URL, - model, - async response => await response.json(), - //@ts-expect-error Jupyter requires X-XSRFToken header - options, - 'internal', - )) as IStacSearchResult; - - if (!data) { - console.debug('STAC search failed -- no results found'); - setResults([]); - setTotalPages(1); - setTotalResults(0); - return; - } - - setResults(data.features); - const pages = data.context.matched / data.context.limit; - setTotalPages(Math.ceil(pages)); - setTotalResults(data.context.matched); - } catch (error) { - console.error('STAC search failed -- error fetching data:', error); - setResults([]); - setTotalPages(1); - setTotalResults(0); - } finally { - setIsLoading(false); - } - }; - - /** - * Handles clicking on a result item - * @param id - ID of the clicked result - */ - const handleResultClick = async (id: string): Promise => { - if (!results) { - return; - } - - const layerId = UUID.uuid4(); - const stacData = results.find(item => item.id === id); - - if (!stacData) { - console.error('Result not found:', id); - return; - } - - const layerModel: IJGISLayer = { - type: 'StacLayer', - parameters: { data: stacData }, - visible: true, - name: stacData.properties.title ?? stacData.id, - }; - - model && model.addLayer(layerId, layerModel); - }; - - /** - * Handles pagination clicks - * @param dir - Direction ('next' | 'previous') or page number to navigate to - */ - const handlePaginationClick = async ( - dir: 'next' | 'previous' | number, - ): Promise => { - if (typeof dir === 'number') { - setCurrentPage(dir); - model && fetchResults(dir); - } else { - // For 'next' or 'previous', calculate the page number - const newPage = dir === 'next' ? currentPage + 1 : currentPage - 1; - setCurrentPage(newPage); - model && fetchResults(newPage); - } - }; - - /** - * Formats a result item for display - * @param item - STAC item to format - * @returns Formatted string representation of the item - */ - const formatResult = (item: IStacItem): string => { - return item.properties.title ?? item.id; - }; - return { - filterState, - filterSetters, - results, startTime, setStartTime, endTime, setEndTime, - totalPages, - currentPage, - totalResults, - handlePaginationClick, - handleResultClick, - formatResult, - isLoading, + currentBBox, + setCurrentBBox, useWorldBBox, setUseWorldBBox, - paginationLinks, - setPaginationLinks, }; } - -export default useStacSearch; diff --git a/yarn.lock b/yarn.lock index 7218c1b5e..280da2c28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3352,7 +3352,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-slot@npm:1.2.4": +"@radix-ui/react-slot@npm:1.2.4, @radix-ui/react-slot@npm:^1.2.3": version: 1.2.4 resolution: "@radix-ui/react-slot@npm:1.2.4" dependencies: From 30c92100a512f5129a20a8d907c4b0754eb1cb19 Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 5 Dec 2025 16:34:59 +0100 Subject: [PATCH 22/70] Big ole refactor --- .../components/StacGenericFilterPanel.tsx | 14 +- .../src/stacBrowser/components/StacPanel.tsx | 29 +- .../components/StacPanelResults.tsx | 49 +- .../geodes/StacGeodesFilterPanel.tsx | 59 +- .../context/StacResultsContext.tsx | 63 +- .../src/stacBrowser/hooks/useFilterSearch.ts | 553 ------------------ .../src/stacBrowser/hooks/useGeodesSearch.ts | 257 ++++---- .../stacBrowser/hooks/useStacGenericFilter.ts | 345 ++--------- .../src/stacBrowser/hooks/useStacSearch.ts | 272 ++++++++- 9 files changed, 516 insertions(+), 1125 deletions(-) delete mode 100644 packages/base/src/stacBrowser/hooks/useFilterSearch.ts diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index 05061a545..251f0593c 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -17,16 +17,11 @@ type FilteredCollection = Pick; // This is a generic UI for apis that support filter extension function StacGenericFilterPanel({ model }: IStacBrowser2Props) { const { - results, setResults, - isLoading, - totalPages, - currentPage, - totalResults, setPaginationLinks, - paginationLinks, registerFetchUsingLink, registerAddToMap, + selectedUrl, } = useStacResultsContext(); const [limit, setLimit] = useState(12); @@ -47,14 +42,9 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { setFilterOperator, } = useStacGenericFilter({ model, + baseUrl: selectedUrl, limit, setResults, - results, - isLoading, - totalPages, - currentPage, - totalResults, - paginationLinks, setPaginationLinks, registerFetchUsingLink, registerAddToMap, diff --git a/packages/base/src/stacBrowser/components/StacPanel.tsx b/packages/base/src/stacBrowser/components/StacPanel.tsx index 92a3d46b7..5bc5c0efb 100644 --- a/packages/base/src/stacBrowser/components/StacPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacPanel.tsx @@ -15,21 +15,32 @@ import StacGenericFilterPanel from './StacGenericFilterPanel'; import StacPanelResults from './StacPanelResults'; import StacGeodesFilterPanel from './geodes/StacGeodesFilterPanel'; +const GEODES_URL = 'https://geodes-portal.cnes.fr/api/stac/search'; + +// URL to panel component mapping for extensibility +// Add new entries here to support additional STAC providers +const URL_TO_PANEL_MAP: Record< + string, + React.ComponentType<{ model?: IJupyterGISModel }> +> = { + [GEODES_URL]: StacGeodesFilterPanel, +}; + interface IStacViewProps { model?: IJupyterGISModel; } // Inner component that uses the context const StacPanelContent = ({ model }: IStacViewProps) => { - const [selectedUrl, setSelectedUrl] = useState( - 'https://stac.dataspace.copernicus.eu/v1/', - ); - const { totalResults } = useStacResultsContext(); + const { totalResults, selectedUrl, setSelectedUrl } = useStacResultsContext(); if (!model) { return null; } + const PanelComponent = + URL_TO_PANEL_MAP[selectedUrl] ?? StacGenericFilterPanel; + return ( @@ -51,17 +62,11 @@ const StacPanelContent = ({ model }: IStacViewProps) => { - +
- {selectedUrl === 'https://geodes-portal.cnes.fr/api/stac/search' ? ( - - ) : ( - - )} + diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index 49d639d5d..462871694 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -46,8 +46,6 @@ function getPageItems( const StacPanelResults = () => { const { results, - currentPage, - totalPages, handlePaginationClick, handleResultClick, formatResult, @@ -100,25 +98,34 @@ const StacPanelResults = () => { {results.length === 0 ? (
No Matches Found
) : ( - getPageItems(currentPage, totalPages).map(item => { - if (item === 'ellipsis') { - return ( - - - - ); - } - return ( - - handlePaginationClick('next')} - > - {item} - - - ); - }) + // getPageItems(currentPage, totalPages).map(item => { + // if (item === 'ellipsis') { + // return ( + // + // + // + // ); + // } + // return ( + // + // handlePaginationClick('next')} + // > + // {item} + // + // + // ); + // }) + + + handlePaginationClick('next')} + > + 1 + + )} { - const { setResults, setPaginationHandlers, setPaginationLinks } = - useStacResultsContext(); + const { + setResults, + setPaginationHandlers, + setPaginationLinks, + registerAddToMap, + registerFetchUsingLink, + selectedUrl, + } = useStacResultsContext(); const { filterState, filterSetters, - results, startTime, setStartTime, endTime, setEndTime, - totalPages, - currentPage, - totalResults, - handlePaginationClick, - handleResultClick, - formatResult, - isLoading, useWorldBBox, setUseWorldBBox, - paginationLinks, - } = useStacSearch({ model }); - - // Sync results to context whenever they change - useEffect(() => { - setResults(results, isLoading, totalPages, currentPage, totalResults); - }, [results, isLoading, totalPages, currentPage, totalResults, setResults]); - - // Sync handlers to context (GEODES has its own handlers) - useEffect(() => { - setPaginationHandlers( - handlePaginationClick, - handleResultClick, - formatResult, - ); - }, [ - handlePaginationClick, - handleResultClick, - formatResult, + } = useGeodesSearch({ + model, + apiUrl: selectedUrl, + setResults, + setPaginationLinks, setPaginationHandlers, - ]); - - // Sync pagination links to context whenever they change - useEffect(() => { - setPaginationLinks(paginationLinks); - }, [paginationLinks, setPaginationLinks]); + registerAddToMap, + registerFetchUsingLink, + }); const handleDatasetSelection = (dataset: string, collection: string) => { const collections = new Set(filterState.collections); diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index a4b6cb67e..303ad31b1 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -13,20 +13,18 @@ import { IStacItem, IStacLink } from '../types/types'; interface IStacResultsContext { results: IStacItem[]; isLoading: boolean; - totalPages: number; - currentPage: number; totalResults: number; - handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise; + handlePaginationClick: (dir: 'next' | 'previous') => Promise; handleResultClick: (id: string) => Promise; formatResult: (item: IStacItem) => string; paginationLinks: Array< IStacLink & { method?: string; body?: Record } >; + selectedUrl: string; + setSelectedUrl: (url: string) => void; setResults: ( results: IStacItem[], isLoading: boolean, - totalPages: number, - currentPage: number, totalResults: number, ) => void; setPaginationLinks: ( @@ -41,7 +39,7 @@ interface IStacResultsContext { registerAddToMap: (addFn: (stacData: IStacItem) => void) => void; // Set handlers from outside (for GEODES search) setPaginationHandlers: ( - handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise, + handlePaginationClick: (dir: 'next' | 'previous') => Promise, handleResultClick: (id: string) => Promise, formatResult: (item: IStacItem) => string, ) => void; @@ -62,14 +60,15 @@ export function StacResultsProvider({ }: IStacResultsProviderProps) { const [results, setResultsState] = useState([]); const [isLoading, setIsLoading] = useState(false); - const [totalPages, setTotalPages] = useState(1); - const [currentPage, setCurrentPage] = useState(1); const [totalResults, setTotalResults] = useState(0); const [paginationLinks, setPaginationLinksState] = useState< Array }> >([]); + const [selectedUrl, setSelectedUrlState] = useState( + 'https://stac.dataspace.copernicus.eu/v1/', + ); const [externalHandlers, setExternalHandlers] = useState<{ - handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise; + handlePaginationClick: (dir: 'next' | 'previous') => Promise; handleResultClick: (id: string) => Promise; formatResult: (item: IStacItem) => string; } | null>(null); @@ -87,14 +86,10 @@ export function StacResultsProvider({ ( newResults: IStacItem[], newIsLoading: boolean, - newTotalPages: number, - newCurrentPage: number, newTotalResults: number, ) => { setResultsState(newResults); setIsLoading(newIsLoading); - setTotalPages(newTotalPages); - setCurrentPage(newCurrentPage); setTotalResults(newTotalResults); }, [], @@ -109,6 +104,10 @@ export function StacResultsProvider({ [], ); + const setSelectedUrl = useCallback((url: string) => { + setSelectedUrlState(url); + }, []); + // Register functions from hooks const registerFetchUsingLink = useCallback( ( @@ -130,9 +129,7 @@ export function StacResultsProvider({ const setPaginationHandlers = useCallback( ( - newHandlePaginationClick: ( - dir: 'next' | 'previous' | number, - ) => Promise, + newHandlePaginationClick: (dir: 'next' | 'previous') => Promise, newHandleResultClick: (id: string) => Promise, newFormatResult: (item: IStacItem) => string, ) => { @@ -148,45 +145,23 @@ export function StacResultsProvider({ // Handlers created in context - always read latest state directly // Use external handlers if provided, otherwise use context-created ones const handlePaginationClick = useCallback( - async (dir: 'next' | 'previous' | number): Promise => { + async (dir: 'next' | 'previous'): Promise => { if (!model) { return; } // Read directly from state - no closure issues! const currentLinks = paginationLinks; - const currentPageValue = currentPage; - - // If dir is a number, convert it to 'next' or 'previous' based on current page - let rel: 'next' | 'previous'; - if (typeof dir === 'number') { - rel = dir > currentPageValue ? 'next' : 'previous'; - } else { - rel = dir; - } - // Find the pagination link - const link = currentLinks.find(l => l.rel === rel); + // Find the pagination link by rel + const link = currentLinks.find(l => l.rel === dir); if (link && link.body && fetchUsingLinkRef.current) { // Use the registered fetch function await fetchUsingLinkRef.current(link); - // Update current page after successful fetch if dir was a number - if (typeof dir === 'number') { - setResults(results, isLoading, totalPages, dir, totalResults); - } } }, - [ - model, - paginationLinks, // Direct dependency - no ref needed! - currentPage, - results, - isLoading, - totalPages, - totalResults, - setResults, - ], + [model, paginationLinks], ); const handleResultClick = useCallback( @@ -226,13 +201,13 @@ export function StacResultsProvider({ value={{ results, isLoading, - totalPages, - currentPage, totalResults, handlePaginationClick: finalHandlePaginationClick, handleResultClick: finalHandleResultClick, formatResult: finalFormatResult, paginationLinks, + selectedUrl, + setSelectedUrl, setResults, setPaginationLinks, registerFetchUsingLink, diff --git a/packages/base/src/stacBrowser/hooks/useFilterSearch.ts b/packages/base/src/stacBrowser/hooks/useFilterSearch.ts deleted file mode 100644 index 40a977507..000000000 --- a/packages/base/src/stacBrowser/hooks/useFilterSearch.ts +++ /dev/null @@ -1,553 +0,0 @@ -// import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; -// import { UUID } from '@lumino/coreutils'; -// import { endOfToday, startOfToday } from 'date-fns'; -// import { useCallback, useEffect, useRef, useState } from 'react'; - -// import { fetchWithProxies } from '@/src/tools'; -// import { useGeneric } from './useStacSearch'; -// import { -// IStacCollection, -// IStacItem, -// IStacLink, -// IStacSearchResult, -// } from '../types/types'; - -// type FilteredCollection = Pick; - -// export type Operator = '=' | '!=' | '<' | '>'; - -// export type FilterOperator = 'and' | 'or'; - -// export interface IQueryableFilter { -// operator: Operator; -// inputValue: string | number | undefined; -// } - -// export type UpdateQueryableFilter = ( -// qKey: string, -// filter: IQueryableFilter, -// ) => void; - -// const API_URL = 'https://stac.dataspace.copernicus.eu/v1/'; - -// interface IUseStacGenericFilterProps { -// model?: IJupyterGISModel; -// limit?: number; -// setResults: ( -// results: IStacItem[], -// isLoading: boolean, -// totalPages: number, -// currentPage: number, -// totalResults: number, -// ) => void; -// results: IStacItem[]; -// isLoading: boolean; -// totalPages: number; -// currentPage: number; -// totalResults: number; -// paginationLinks: Array< -// IStacLink & { method?: string; body?: Record } -// >; -// setPaginationLinks: ( -// links: Array }>, -// ) => void; -// setPaginationHandlers: ( -// handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise, -// handleResultClick: (id: string) => Promise, -// formatResult: (item: IStacItem) => string, -// ) => void; -// } - -// export function useStacGenericFilter({ -// model, -// limit = 12, -// setResults, -// results, -// isLoading, -// totalPages, -// currentPage, -// totalResults, -// paginationLinks, -// setPaginationLinks, -// setPaginationHandlers, -// }: IUseStacGenericFilterProps) { -// const { -// startTime, -// endTime, -// setStartTime, -// setEndTime, -// currentBBox, -// useWorldBBox, -// setUseWorldBBox, -// } = useGeneric({ model }); - -// // Use a ref to always access the latest paginationLinks value -// const paginationLinksRef = useRef(paginationLinks); -// useEffect(() => { -// paginationLinksRef.current = paginationLinks; -// }, [paginationLinks]); - -// const [queryableProps, setQueryableProps] = useState<[string, any][]>(); -// const [collections, setCollections] = useState([]); -// // ! temp -// const [selectedCollection, setSelectedCollection] = -// useState('sentinel-2-l2a'); -// const [queryableFilters, setQueryableFilters] = useState< -// Record -// >({}); -// const [filterOperator, setFilterOperator] = useState('and'); - -// // for collections -// useEffect(() => { -// if (!model) { -// return; -// } - -// const fatch = async () => { -// const data = await fetchWithProxies( -// API_URL + 'collections', -// model, -// async response => await response.json(), -// undefined, -// 'internal', -// ); - -// const collections: FilteredCollection[] = data.collections -// .map((collection: any) => ({ -// title: collection.title ?? collection.id, -// id: collection.id, -// })) -// .sort((a: FilteredCollection, b: FilteredCollection) => { -// const titleA = a.title?.toLowerCase() ?? ''; -// const titleB = b.title?.toLowerCase() ?? ''; -// return titleA.localeCompare(titleB); -// }); - -// setCollections(collections); -// }; - -// fatch(); -// }, [model]); - -// // for queryables -// // should listen for colletion changes and requery -// // need a way to handle querying multiple collections without refetching everything -// // collection id -> queryables map as a basic cache thing?? -// useEffect(() => { -// if (!model) { -// return; -// } - -// const fatch = async () => { -// const data = await fetchWithProxies( -// API_URL + 'queryables', -// model, -// async response => await response.json(), -// undefined, -// 'internal', -// ); - -// setQueryableProps(Object.entries(data.properties)); -// }; - -// fatch(); -// }, [model]); - -// const addToMap = (stacData: any) => { -// console.log('add to amp'); -// if (!model) { -// return; -// } - -// const layerId = UUID.uuid4(); - -// if (!stacData) { -// return; -// } - -// const layerModel: IJGISLayer = { -// type: 'StacLayer', -// parameters: { data: stacData }, -// visible: true, -// name: stacData.properties.title ?? stacData.id, -// }; - -// model.addLayer(layerId, layerModel); -// }; - -// const updateQueryableFilter = useCallback( -// (qKey: string, filter: IQueryableFilter) => { -// setQueryableFilters(prev => ({ -// ...prev, -// [qKey]: filter, -// })); -// }, -// [], -// ); - -// const handleSubmit = async () => { -// if (!model) { -// return; -// } - -// // Reset to page 1 -// setResults(results, isLoading, totalPages, 1, totalResults); - -// const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; - -// const st = startTime -// ? startTime.toISOString() -// : startOfToday().toISOString(); - -// const et = endTime ? endTime.toISOString() : endOfToday().toISOString(); - -// // Build filter object from queryableFilters -// const filterConditions = Object.entries(queryableFilters) -// .filter(([, filter]) => filter.inputValue !== undefined) -// .map(([property, filter]) => { -// return { -// op: filter.operator, -// args: [ -// { -// property, -// }, -// filter.inputValue, -// ], -// }; -// }); - -// const body: Record = { -// bbox: currentBBox, -// collections: [selectedCollection], -// datetime: `${st}/${et}`, -// limit, -// 'filter-lang': 'cql2-json', -// }; - -// // Only add filter if there are any conditions -// if (filterConditions.length > 0) { -// body.filter = { -// op: filterOperator, -// args: filterConditions, -// }; -// } - -// const options = { -// method: 'POST', -// headers: { -// 'Content-Type': 'application/json', -// 'X-XSRFToken': XSRF_TOKEN, -// credentials: 'include', -// }, -// body: JSON.stringify(body), -// }; - -// try { -// // Update context with loading state -// setResults(results, true, totalPages, currentPage, totalResults); -// const data = (await fetchWithProxies( -// 'https://stac.dataspace.copernicus.eu/v1/search', -// model, -// async response => await response.json(), -// //@ts-expect-error Jupyter requires X-XSRFToken header -// options, -// 'internal', -// )) as IStacSearchResult; - -// if (!data) { -// setResults([], false, 1, currentPage, 0); -// return; -// } - -// // Filter assets to only include items with 'overview' or 'thumbnail' roles -// // ? is this a good idea?? -// if (data.features && data.features.length > 0) { -// data.features.forEach(feature => { -// if (feature.assets) { -// const originalAssets = feature.assets; -// const filteredAssets: Record = {}; - -// // Iterate through each asset in the assets object -// for (const [key, asset] of Object.entries(originalAssets)) { -// if ( -// asset && -// typeof asset === 'object' && -// 'roles' in asset && -// Array.isArray(asset.roles) -// ) { -// const roles = asset.roles; - -// if (roles.includes('thumbnail') || roles.includes('overview')) { -// filteredAssets[key] = asset; -// } -// } -// } - -// // Replace assets with filtered version -// feature.assets = filteredAssets; -// } -// }); -// } - -// // Sort features by id before setting results -// const sortedFeatures = [...data.features].sort((a, b) => -// a.id.localeCompare(b.id), -// ); - -// // Handle context if available (STAC API extension) -// let calculatedTotalPages = 1; -// let calculatedTotalResults = data.features.length; -// if (data.context) { -// const pages = data.context.matched / data.context.limit; -// calculatedTotalPages = Math.ceil(pages); -// calculatedTotalResults = data.context.matched; -// } - -// // Update context with results -// setResults( -// sortedFeatures, -// false, -// calculatedTotalPages, -// currentPage, -// calculatedTotalResults, -// ); - -// // Store pagination links for use in handlePaginationClick -// if (data.links) { -// const typedLinks = data.links as Array< -// IStacLink & { method?: string; body?: Record } -// >; -// setPaginationLinks(typedLinks); -// } -// } catch (error) { -// setResults([], false, 1, currentPage, 0); -// } -// }; - -// /** -// * Handles clicking on a result item -// * @param id - ID of the clicked result -// */ -// const handleResultClick = useCallback( -// async (id: string): Promise => { -// console.log('handle reuslt cliks'); -// if (!model) { -// return; -// } - -// const result = results.find((r: IStacItem) => r.id === id); -// if (result) { -// addToMap(result); -// } -// }, -// [results, model], -// ); - -// /** -// * Fetches results using a STAC link (for pagination) -// * @param link - STAC link object with href and optional body -// */ -// const fetchUsingLink = useCallback( -// async ( -// link: IStacLink & { method?: string; body?: Record }, -// ) => { -// if (!model) { -// return; -// } - -// const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; - -// const options = { -// method: (link.method || 'POST').toUpperCase(), -// headers: { -// 'Content-Type': 'application/json', -// 'X-XSRFToken': XSRF_TOKEN, -// credentials: 'include', -// }, -// body: link.body ? JSON.stringify(link.body) : undefined, -// }; - -// try { -// // Update context with loading state -// setResults(results, true, totalPages, currentPage, totalResults); -// const data = (await fetchWithProxies( -// link.href, -// model, -// async response => await response.json(), -// //@ts-expect-error Jupyter requires X-XSRFToken header -// options, -// 'internal', -// )) as IStacSearchResult; - -// if (!data) { -// setResults([], false, 1, currentPage, 0); -// return; -// } - -// // Filter assets to only include items with 'overview' or 'thumbnail' roles -// if (data.features && data.features.length > 0) { -// data.features.forEach(feature => { -// if (feature.assets) { -// const originalAssets = feature.assets; -// const filteredAssets: Record = {}; - -// for (const [key, asset] of Object.entries(originalAssets)) { -// if ( -// asset && -// typeof asset === 'object' && -// 'roles' in asset && -// Array.isArray(asset.roles) -// ) { -// const roles = asset.roles; - -// if ( -// roles.includes('thumbnail') || -// roles.includes('overview') -// ) { -// filteredAssets[key] = asset; -// } -// } -// } - -// feature.assets = filteredAssets; -// } -// }); -// } - -// // Sort features by id before setting results -// const sortedFeatures = [...data.features].sort((a, b) => -// a.id.localeCompare(b.id), -// ); - -// // Handle context if available (STAC API extension) -// let calculatedTotalPages = 1; -// let calculatedTotalResults = data.features.length; -// if (data.context) { -// const pages = data.context.matched / data.context.limit; -// calculatedTotalPages = Math.ceil(pages); -// calculatedTotalResults = data.context.matched; -// } - -// // Update context with results -// setResults( -// sortedFeatures, -// false, -// calculatedTotalPages, -// currentPage, -// calculatedTotalResults, -// ); - -// // Store pagination links for next pagination -// if (data.links) { -// const typedLinks = data.links as Array< -// IStacLink & { method?: string; body?: Record } -// >; - -// // Update ref synchronously before updating context -// paginationLinksRef.current = typedLinks; -// setPaginationLinks(typedLinks); -// } -// } catch (error) { -// setResults([], false, 1, currentPage, 0); -// } -// }, -// [ -// model, -// results, -// isLoading, -// totalPages, -// currentPage, -// totalResults, -// setResults, -// setPaginationLinks, -// ], -// ); - -// /** -// * Handles pagination clicks -// * @param dir - Direction ('next' | 'previous') or page number (for backward compatibility) -// */ -// const handlePaginationClick = useCallback( -// async (dir: 'next' | 'previous' | number): Promise => { -// // Always use ref to get the latest paginationLinks value -// const currentLinks = paginationLinksRef.current; - -// if (!model) { -// return; -// } - -// // If dir is a number, convert it to 'next' or 'previous' based on current page -// let rel: 'next' | 'previous'; -// if (typeof dir === 'number') { -// rel = dir > currentPage ? 'next' : 'previous'; -// } else { -// rel = dir; -// } - -// // Find the pagination link using the ref -// const link = currentLinks.find(l => l.rel === rel); - -// if (link && link.body) { -// // Use the link with its body (contains token) to fetch the page -// await fetchUsingLink(link); -// // Update current page after successful fetch if dir was a number -// if (typeof dir === 'number') { -// setResults(results, isLoading, totalPages, dir, totalResults); -// } -// } -// }, -// [ -// model, -// currentPage, -// results, -// isLoading, -// totalPages, -// totalResults, -// setResults, -// fetchUsingLink, -// ], -// ); - -// /** -// * Formats a result item for display -// * @param item - STAC item to format -// * @returns Formatted string representation of the item -// */ -// const formatResult = useCallback((item: IStacItem): string => { -// return item.properties?.title ?? item.id; -// }, []); - -// // Sync handlers to context whenever they change -// // Also sync when paginationLinks change to ensure handler in context has latest links via ref -// useEffect(() => { -// setPaginationHandlers( -// handlePaginationClick, -// handleResultClick, -// formatResult, -// ); -// }, [ -// handlePaginationClick, -// handleResultClick, -// formatResult, -// setPaginationHandlers, -// paginationLinks, // Sync when links change so context gets updated handler -// ]); - -// return { -// queryableProps, -// collections, -// selectedCollection, -// setSelectedCollection, -// handleSubmit, -// startTime, -// endTime, -// setStartTime, -// setEndTime, -// useWorldBBox, -// setUseWorldBBox, -// queryableFilters, -// updateQueryableFilter, -// filterOperator, -// setFilterOperator, -// }; -// } diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index 8c6623c54..9f1adf29d 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -1,5 +1,4 @@ -import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; -import { UUID } from '@lumino/coreutils'; +import { IJupyterGISModel } from '@jupytergis/schema'; import { startOfYesterday } from 'date-fns'; import { useCallback, useEffect, useState } from 'react'; @@ -9,59 +8,71 @@ import { IStacItem, IStacLink, IStacQueryBody, - IStacSearchResult, StacFilterState, StacFilterSetters, StacFilterStateStateDb, } from '@/src/stacBrowser/types/types'; import { GlobalStateDbManager } from '@/src/store'; -import { fetchWithProxies } from '@/src/tools'; -import { useGeneric } from './useStacSearch'; +import { useStacSearch } from './useStacSearch'; -interface IUseStacSearchProps { +interface IUseGeodesSearchProps { model: IJupyterGISModel | undefined; + apiUrl: string; + setResults: ( + results: IStacItem[], + isLoading: boolean, + totalResults: number, + ) => void; + setPaginationLinks: ( + links: Array }>, + ) => void; + setPaginationHandlers: ( + handlePaginationClick: (dir: 'next' | 'previous') => Promise, + handleResultClick: (id: string) => Promise, + formatResult: (item: IStacItem) => string, + ) => void; + registerAddToMap: (addFn: (stacData: IStacItem) => void) => void; + registerFetchUsingLink: ( + fetchFn: ( + link: IStacLink & { method?: string; body?: Record }, + ) => Promise, + ) => void; } -// ! TODO factor out common bits -interface IUseStacSearchReturn { +interface IUseGeodesSearchReturn { filterState: StacFilterState; filterSetters: StacFilterSetters; - results: IStacItem[]; startTime: Date | undefined; setStartTime: (date: Date | undefined) => void; endTime: Date | undefined; setEndTime: (date: Date | undefined) => void; - totalPages: number; - currentPage: number; - totalResults: number; - handlePaginationClick: (dir: 'next' | 'previous' | number) => Promise; - handleResultClick: (id: string) => Promise; - formatResult: (item: IStacItem) => string; - isLoading: boolean; useWorldBBox: boolean; setUseWorldBBox: (val: boolean) => void; - paginationLinks: Array< - IStacLink & { method?: string; body?: Record } - >; - setPaginationLinks: ( - links: Array }>, - ) => void; + handleSubmit: () => Promise; } -const API_URL = 'https://geodes-portal.cnes.fr/api/stac/search'; -const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; const STAC_FILTERS_KEY = 'jupytergis:stac-filters'; +const GEODES_URL = 'https://geodes-portal.cnes.fr/api/stac/search'; /** - * Custom hook for managing STAC search functionality - * @param props - Configuration object containing datasets, platforms, products, and model - * @returns Object containing state and handlers for STAC search + * Custom hook for managing GEODES-specific STAC search functionality + * Focuses on query building with GEODES-specific filters + * @param props - Configuration object containing model and context setters + * @returns Object containing filter state and temporal/spatial filters */ -function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { +function useGeodesSearch({ + model, + apiUrl, + setResults, + setPaginationLinks, + setPaginationHandlers, + registerAddToMap, + registerFetchUsingLink, +}: IUseGeodesSearchProps): IUseGeodesSearchReturn { const isFirstRender = useIsFirstRender(); const stateDb = GlobalStateDbManager.getInstance().getStateDb(); - // Get generic state from useGeneric hook + // Get temporal/spatial filters and fetch functions from useStacSearch const { startTime, setStartTime, @@ -70,16 +81,15 @@ function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { currentBBox, useWorldBBox, setUseWorldBBox, - } = useGeneric({ model }); + handleSubmit: handleSubmitFromGeneric, + fetchUsingLink, + } = useStacSearch({ + model, + setResults, + setPaginationLinks, + registerAddToMap, + }); - const [results, setResults] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [totalPages, setTotalPages] = useState(1); - const [currentPage, setCurrentPage] = useState(1); - const [totalResults, setTotalResults] = useState(0); - const [paginationLinks, setPaginationLinks] = useState< - Array }> - >([]); const [filterState, setFilterState] = useState({ collections: new Set(), datasets: new Set(), @@ -127,15 +137,10 @@ function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { saveStacFilterStateToDb(); }, [filterState, stateDb]); - // Handle search when filters change - useEffect(() => { - if (model && !isFirstRender && filterState.datasets.size > 0) { - setCurrentPage(1); - fetchResults(1); - } - }, [filterState, startTime, endTime, currentBBox]); - - const fetchResults = async (page = 1) => { + /** + * Builds GEODES-specific query + */ + const buildGeodesQuery = useCallback((): IStacQueryBody => { const processingLevel = new Set(); const productType = new Set(); @@ -152,10 +157,9 @@ function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { }); }); - const body: IStacQueryBody = { + return { bbox: currentBBox, limit: 12, - page, query: { latest: { eq: true }, dataset: { in: Array.from(filterState.datasets) }, @@ -179,128 +183,95 @@ function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn { }, sortBy: [{ direction: 'desc', field: 'start_datetime' }], }; - - try { - setIsLoading(true); - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-XSRFToken': XSRF_TOKEN, - credentials: 'include', - }, - body: JSON.stringify(body), - }; - - if (!model) { - return; - } - - const data = (await fetchWithProxies( - API_URL, - model, - async response => await response.json(), - //@ts-expect-error Jupyter requires X-XSRFToken header - options, - 'internal', - )) as IStacSearchResult; - - if (!data) { - console.debug('STAC search failed -- no results found'); - setResults([]); - setTotalPages(1); - setTotalResults(0); - return; - } - - setResults(data.features); - const pages = data.context.matched / data.context.limit; - setTotalPages(Math.ceil(pages)); - setTotalResults(data.context.matched); - } catch (error) { - console.error('STAC search failed -- error fetching data:', error); - setResults([]); - setTotalPages(1); - setTotalResults(0); - } finally { - setIsLoading(false); - } - }; + }, [filterState, currentBBox, startTime, endTime]); /** - * Handles clicking on a result item - * @param id - ID of the clicked result + * Handles form submission - builds query and fetches results */ - const handleResultClick = async (id: string): Promise => { - if (!results) { + const handleSubmit = useCallback(async () => { + if (!model) { return; } - const layerId = UUID.uuid4(); - const stacData = results.find(item => item.id === id); + // Use handleSubmit from useStacSearch to initiate the query + await handleSubmitFromGeneric(buildGeodesQuery, apiUrl); + }, [model, buildGeodesQuery, handleSubmitFromGeneric, apiUrl]); - if (!stacData) { - console.error('Result not found:', id); - return; + // Handle search when filters change + useEffect(() => { + if (model && !isFirstRender && filterState.datasets.size > 0) { + handleSubmit(); } + }, [ + model, + isFirstRender, + filterState, + startTime, + endTime, + currentBBox, + handleSubmit, + ]); - const layerModel: IJGISLayer = { - type: 'StacLayer', - parameters: { data: stacData }, - visible: true, - name: stacData.properties.title ?? stacData.id, - }; - - model && model.addLayer(layerId, layerModel); - }; + /** + * Handles clicking on a result item + * This will be used by context, which will call addToMap via registerAddToMap + * @param id - ID of the clicked result (not used directly, context handles it) + */ + const handleResultClick = useCallback(async (_id: string): Promise => { + // Context will handle this using addToMapRef + }, []); /** - * Handles pagination clicks - * @param dir - Direction ('next' | 'previous') or page number to navigate to + * Handles pagination clicks (link-only, no page numbers) + * Uses fetchUsingLink from useStacSearch which is registered with context + * @param dir - Direction ('next' | 'previous') */ - const handlePaginationClick = async ( - dir: 'next' | 'previous' | number, - ): Promise => { - if (typeof dir === 'number') { - setCurrentPage(dir); - model && fetchResults(dir); - } else { - // For 'next' or 'previous', calculate the page number - const newPage = dir === 'next' ? currentPage + 1 : currentPage - 1; - setCurrentPage(newPage); - model && fetchResults(newPage); - } - }; + const handlePaginationClick = useCallback( + async (dir: 'next' | 'previous'): Promise => { + // Context will handle this using fetchUsingLinkRef + }, + [], + ); /** * Formats a result item for display * @param item - STAC item to format * @returns Formatted string representation of the item */ - const formatResult = (item: IStacItem): string => { - return item.properties.title ?? item.id; - }; + const formatResult = useCallback((item: IStacItem): string => { + return item.properties?.title ?? item.id; + }, []); + + // Register fetchUsingLink with context + useEffect(() => { + registerFetchUsingLink(fetchUsingLink); + }, [fetchUsingLink, registerFetchUsingLink]); + + // Register handlers with context + useEffect(() => { + setPaginationHandlers( + handlePaginationClick, + handleResultClick, + formatResult, + ); + }, [ + handlePaginationClick, + handleResultClick, + formatResult, + setPaginationHandlers, + ]); return { filterState, filterSetters, - results, startTime, setStartTime, endTime, setEndTime, - totalPages, - currentPage, - totalResults, - handlePaginationClick, - handleResultClick, - formatResult, - isLoading, useWorldBBox, setUseWorldBBox, - paginationLinks, - setPaginationLinks, + handleSubmit, }; } -export default useStacSearch; +export default useGeodesSearch; diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index f684f562a..afb4522a5 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -1,15 +1,14 @@ -import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; -import { UUID } from '@lumino/coreutils'; +import { IJupyterGISModel } from '@jupytergis/schema'; import { endOfToday, startOfToday } from 'date-fns'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { fetchWithProxies } from '@/src/tools'; -import useStacSearch from './useGeodesSearch'; +import { useStacSearch } from './useStacSearch'; import { IStacCollection, IStacItem, IStacLink, - IStacSearchResult, + IStacQueryBody, } from '../types/types'; type FilteredCollection = Pick; @@ -28,26 +27,20 @@ export type UpdateQueryableFilter = ( filter: IQueryableFilter, ) => void; -const API_URL = 'https://stac.dataspace.copernicus.eu/v1/'; +// Helper to get search URL from base URL +const getSearchUrl = (baseUrl: string): string => { + return baseUrl.endsWith('/') ? `${baseUrl}search` : `${baseUrl}/search`; +}; interface IUseStacGenericFilterProps { model?: IJupyterGISModel; + baseUrl: string; limit?: number; setResults: ( results: IStacItem[], isLoading: boolean, - totalPages: number, - currentPage: number, totalResults: number, ) => void; - results: IStacItem[]; - isLoading: boolean; - totalPages: number; - currentPage: number; - totalResults: number; - paginationLinks: Array< - IStacLink & { method?: string; body?: Record } - >; setPaginationLinks: ( links: Array }>, ) => void; @@ -61,35 +54,36 @@ interface IUseStacGenericFilterProps { export function useStacGenericFilter({ model, + baseUrl, limit = 12, setResults, - results, - isLoading, - totalPages, - currentPage, - totalResults, - paginationLinks, setPaginationLinks, registerFetchUsingLink, registerAddToMap, }: IUseStacGenericFilterProps) { + // Get temporal/spatial filters and fetch functions from useStacSearch const { startTime, endTime, setStartTime, setEndTime, + currentBBox, useWorldBBox, setUseWorldBBox, - } = useStacSearch({ model }); + handleSubmit: handleSubmitFromGeneric, + fetchUsingLink, + } = useStacSearch({ + model, + setResults, + setPaginationLinks, + registerAddToMap, + }); const [queryableProps, setQueryableProps] = useState<[string, any][]>(); const [collections, setCollections] = useState([]); // ! temp const [selectedCollection, setSelectedCollection] = useState('sentinel-2-l2a'); - const [currentBBox, setCurrentBBox] = useState< - [number, number, number, number] - >([-180, -90, 180, 90]); const [queryableFilters, setQueryableFilters] = useState< Record >({}); @@ -102,8 +96,11 @@ export function useStacGenericFilter({ } const fatch = async () => { + const collectionsUrl = baseUrl.endsWith('/') + ? `${baseUrl}collections` + : `${baseUrl}/collections`; const data = await fetchWithProxies( - API_URL + 'collections', + collectionsUrl, model, async response => await response.json(), undefined, @@ -137,8 +134,11 @@ export function useStacGenericFilter({ } const fatch = async () => { + const queryablesUrl = baseUrl.endsWith('/') + ? `${baseUrl}queryables` + : `${baseUrl}/queryables`; const data = await fetchWithProxies( - API_URL + 'queryables', + queryablesUrl, model, async response => await response.json(), undefined, @@ -151,51 +151,6 @@ export function useStacGenericFilter({ fatch(); }, [model]); - useEffect(() => { - if (!model) { - return; - } - - const listenToModel = ( - sender: IJupyterGISModel, - bBoxIn4326: [number, number, number, number], - ) => { - if (useWorldBBox) { - setCurrentBBox([-180, -90, 180, 90]); - } else { - setCurrentBBox(bBoxIn4326); - } - }; - - model.updateBboxSignal.connect(listenToModel); - - return () => { - model.updateBboxSignal.disconnect(listenToModel); - }; - }, [model, useWorldBBox]); - - const addToMap = (stacData: any) => { - console.log('add to amp'); - if (!model) { - return; - } - - const layerId = UUID.uuid4(); - - if (!stacData) { - return; - } - - const layerModel: IJGISLayer = { - type: 'StacLayer', - parameters: { data: stacData }, - visible: true, - name: stacData.properties.title ?? stacData.id, - }; - - model.addLayer(layerId, layerModel); - }; - const updateQueryableFilter = useCallback( (qKey: string, filter: IQueryableFilter) => { setQueryableFilters(prev => ({ @@ -206,16 +161,10 @@ export function useStacGenericFilter({ [], ); - const handleSubmit = async () => { - if (!model) { - return; - } - - // Reset to page 1 - setResults(results, isLoading, totalPages, 1, totalResults); - - const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; - + /** + * Builds Copernicus-specific query + */ + const buildCopernicusQuery = useCallback((): IStacQueryBody => { const st = startTime ? startTime.toISOString() : startOfToday().toISOString(); @@ -253,225 +202,35 @@ export function useStacGenericFilter({ }; } - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-XSRFToken': XSRF_TOKEN, - credentials: 'include', - }, - body: JSON.stringify(body), - }; - - try { - // Update context with loading state - setResults(results, true, totalPages, currentPage, totalResults); - const data = (await fetchWithProxies( - 'https://stac.dataspace.copernicus.eu/v1/search', - model, - async response => await response.json(), - //@ts-expect-error Jupyter requires X-XSRFToken header - options, - 'internal', - )) as IStacSearchResult; - - if (!data) { - setResults([], false, 1, currentPage, 0); - return; - } - - // Filter assets to only include items with 'overview' or 'thumbnail' roles - // ? is this a good idea?? - if (data.features && data.features.length > 0) { - data.features.forEach(feature => { - if (feature.assets) { - const originalAssets = feature.assets; - const filteredAssets: Record = {}; - - // Iterate through each asset in the assets object - for (const [key, asset] of Object.entries(originalAssets)) { - if ( - asset && - typeof asset === 'object' && - 'roles' in asset && - Array.isArray(asset.roles) - ) { - const roles = asset.roles; - - if (roles.includes('thumbnail') || roles.includes('overview')) { - filteredAssets[key] = asset; - } - } - } - - // Replace assets with filtered version - feature.assets = filteredAssets; - } - }); - } - - // Sort features by id before setting results - const sortedFeatures = [...data.features].sort((a, b) => - a.id.localeCompare(b.id), - ); - - // Handle context if available (STAC API extension) - let calculatedTotalPages = 1; - let calculatedTotalResults = data.features.length; - if (data.context) { - const pages = data.context.matched / data.context.limit; - calculatedTotalPages = Math.ceil(pages); - calculatedTotalResults = data.context.matched; - } - - // Update context with results - setResults( - sortedFeatures, - false, - calculatedTotalPages, - currentPage, - calculatedTotalResults, - ); - - // Store pagination links - if (data.links) { - const typedLinks = data.links as Array< - IStacLink & { method?: string; body?: Record } - >; - setPaginationLinks(typedLinks); - } - } catch (error) { - setResults([], false, 1, currentPage, 0); - } - }; + return body as IStacQueryBody; + }, [ + startTime, + endTime, + currentBBox, + selectedCollection, + limit, + queryableFilters, + filterOperator, + ]); /** - * Fetches results using a STAC link (for pagination) - * @param link - STAC link object with href and optional body + * Handles form submission - builds query and fetches results */ - const fetchUsingLink = useCallback( - async ( - link: IStacLink & { method?: string; body?: Record }, - ) => { - if (!model) { - return; - } - - const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; - - const options = { - method: (link.method || 'POST').toUpperCase(), - headers: { - 'Content-Type': 'application/json', - 'X-XSRFToken': XSRF_TOKEN, - credentials: 'include', - }, - body: link.body ? JSON.stringify(link.body) : undefined, - }; - - try { - // Update context with loading state - setResults(results, true, totalPages, currentPage, totalResults); - const data = (await fetchWithProxies( - link.href, - model, - async response => await response.json(), - //@ts-expect-error Jupyter requires X-XSRFToken header - options, - 'internal', - )) as IStacSearchResult; - - if (!data) { - setResults([], false, 1, currentPage, 0); - return; - } - - // Filter assets to only include items with 'overview' or 'thumbnail' roles - if (data.features && data.features.length > 0) { - data.features.forEach(feature => { - if (feature.assets) { - const originalAssets = feature.assets; - const filteredAssets: Record = {}; - - for (const [key, asset] of Object.entries(originalAssets)) { - if ( - asset && - typeof asset === 'object' && - 'roles' in asset && - Array.isArray(asset.roles) - ) { - const roles = asset.roles; - - if ( - roles.includes('thumbnail') || - roles.includes('overview') - ) { - filteredAssets[key] = asset; - } - } - } - - feature.assets = filteredAssets; - } - }); - } - - // Sort features by id before setting results - const sortedFeatures = [...data.features].sort((a, b) => - a.id.localeCompare(b.id), - ); - - // Handle context if available (STAC API extension) - let calculatedTotalPages = 1; - let calculatedTotalResults = data.features.length; - if (data.context) { - const pages = data.context.matched / data.context.limit; - calculatedTotalPages = Math.ceil(pages); - calculatedTotalResults = data.context.matched; - } - - // Update context with results - setResults( - sortedFeatures, - false, - calculatedTotalPages, - currentPage, - calculatedTotalResults, - ); + const handleSubmit = useCallback(async () => { + if (!model) { + return; + } - // Store pagination links for next pagination - if (data.links) { - const typedLinks = data.links as Array< - IStacLink & { method?: string; body?: Record } - >; + // Use handleSubmit from useStacSearch to initiate the query + const searchUrl = getSearchUrl(baseUrl); + await handleSubmitFromGeneric(buildCopernicusQuery, searchUrl); + }, [model, buildCopernicusQuery, handleSubmitFromGeneric, baseUrl]); - setPaginationLinks(typedLinks); - } - } catch (error) { - setResults([], false, 1, currentPage, 0); - } - }, - [ - model, - results, - isLoading, - totalPages, - currentPage, - totalResults, - setResults, - setPaginationLinks, - ], - ); - - // Register functions with context so handlers can use them + // Register fetchUsingLink from useStacSearch with context so handlers can use it useEffect(() => { registerFetchUsingLink(fetchUsingLink); }, [fetchUsingLink, registerFetchUsingLink]); - useEffect(() => { - registerAddToMap(addToMap); - }, [addToMap, registerAddToMap]); - return { queryableProps, collections, diff --git a/packages/base/src/stacBrowser/hooks/useStacSearch.ts b/packages/base/src/stacBrowser/hooks/useStacSearch.ts index 903d3ca36..c1e7b53ff 100644 --- a/packages/base/src/stacBrowser/hooks/useStacSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useStacSearch.ts @@ -1,11 +1,30 @@ -import { IJupyterGISModel } from '@jupytergis/schema'; -import { useEffect, useState } from 'react'; +import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; +import { UUID } from '@lumino/coreutils'; +import { useCallback, useEffect, useState } from 'react'; -interface IUseGenericProps { +import { fetchWithProxies } from '@/src/tools'; +import { + IStacItem, + IStacLink, + IStacQueryBody, + IStacSearchResult, +} from '../types/types'; + +interface IUseStacSearchProps { model: IJupyterGISModel | undefined; + setResults: ( + results: IStacItem[], + isLoading: boolean, + totalResults: number, + ) => void; + setPaginationLinks: ( + links: Array }>, + ) => void; + registerAddToMap: (addFn: (stacData: IStacItem) => void) => void; } -interface IUseGenericReturn { +interface IUseStacSearchReturn { + // Temporal and spatial filters startTime: Date | undefined; setStartTime: (date: Date | undefined) => void; endTime: Date | undefined; @@ -14,14 +33,28 @@ interface IUseGenericReturn { setCurrentBBox: (bbox: [number, number, number, number]) => void; useWorldBBox: boolean; setUseWorldBBox: (val: boolean) => void; + // Core fetch functions + handleSubmit: ( + buildQuery: () => IStacQueryBody, + apiUrl: string, + ) => Promise; + fetchUsingLink: ( + link: IStacLink & { method?: string; body?: Record }, + ) => Promise; } /** - * Custom hook for managing generic STAC search state (temporal and spatial filters) - * @param props - Configuration object containing model - * @returns Object containing state and setters for temporal and spatial filters + * Central hook for managing STAC search - handles temporal/spatial filters, + * core fetching, pagination, and context management + * @param props - Configuration object containing model and context setters + * @returns Object containing filter state and core fetch functions */ -export function useGeneric({ model }: IUseGenericProps): IUseGenericReturn { +export function useStacSearch({ + model, + setResults, + setPaginationLinks, + registerAddToMap, +}: IUseStacSearchProps): IUseStacSearchReturn { const [startTime, setStartTime] = useState(undefined); const [endTime, setEndTime] = useState(undefined); const [currentBBox, setCurrentBBox] = useState< @@ -49,6 +82,227 @@ export function useGeneric({ model }: IUseGenericProps): IUseGenericReturn { }; }, [model, useWorldBBox]); + // Core submit function - accepts a query builder function and initiates the query + const handleSubmit = useCallback( + async (buildQuery: () => IStacQueryBody, apiUrl: string) => { + if (!model) { + return; + } + + const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; + const queryBody = buildQuery(); + + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-XSRFToken': XSRF_TOKEN, + credentials: 'include', + }, + body: JSON.stringify(queryBody), + }; + + try { + // Update context with loading state + setResults([], true, 0); + + const data = (await fetchWithProxies( + apiUrl, + model, + async response => await response.json(), + //@ts-expect-error Jupyter requires X-XSRFToken header + options, + 'internal', + )) as IStacSearchResult; + + if (!data) { + setResults([], false, 0); + return; + } + + // Filter assets to only include items with 'overview' or 'thumbnail' roles + if (data.features && data.features.length > 0) { + data.features.forEach(feature => { + if (feature.assets) { + const originalAssets = feature.assets; + const filteredAssets: Record = {}; + + for (const [key, asset] of Object.entries(originalAssets)) { + if ( + asset && + typeof asset === 'object' && + 'roles' in asset && + Array.isArray(asset.roles) + ) { + const roles = asset.roles; + + if ( + roles.includes('thumbnail') || + roles.includes('overview') + ) { + filteredAssets[key] = asset; + } + } + } + + feature.assets = filteredAssets; + } + }); + } + + // Sort features by id before setting results + const sortedFeatures = [...data.features].sort((a, b) => + a.id.localeCompare(b.id), + ); + + // Calculate total results from context if available + let totalResults = data.features.length; + if (data.context) { + totalResults = data.context.matched; + } + + // Update context with results (using 0 for totalPages/currentPage as placeholders) + setResults(sortedFeatures, false, totalResults); + + // Store pagination links + if (data.links) { + const typedLinks = data.links as Array< + IStacLink & { method?: string; body?: Record } + >; + setPaginationLinks(typedLinks); + } + } catch (error) { + setResults([], false, 0); + } + }, + [model, setResults, setPaginationLinks], + ); + + // Fetch using pagination link + const fetchUsingLink = useCallback( + async ( + link: IStacLink & { method?: string; body?: Record }, + ) => { + if (!model) { + return; + } + + const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; + + const options = { + method: (link.method || 'POST').toUpperCase(), + headers: { + 'Content-Type': 'application/json', + 'X-XSRFToken': XSRF_TOKEN, + credentials: 'include', + }, + body: link.body ? JSON.stringify(link.body) : undefined, + }; + + try { + // Update context with loading state + setResults([], true, 0); + + const data = (await fetchWithProxies( + link.href, + model, + async response => await response.json(), + //@ts-expect-error Jupyter requires X-XSRFToken header + options, + 'internal', + )) as IStacSearchResult; + + if (!data) { + setResults([], false, 0); + return; + } + + // Filter assets to only include items with 'overview' or 'thumbnail' roles + if (data.features && data.features.length > 0) { + data.features.forEach(feature => { + if (feature.assets) { + const originalAssets = feature.assets; + const filteredAssets: Record = {}; + + for (const [key, asset] of Object.entries(originalAssets)) { + if ( + asset && + typeof asset === 'object' && + 'roles' in asset && + Array.isArray(asset.roles) + ) { + const roles = asset.roles; + + if ( + roles.includes('thumbnail') || + roles.includes('overview') + ) { + filteredAssets[key] = asset; + } + } + } + + feature.assets = filteredAssets; + } + }); + } + + // Sort features by id before setting results + const sortedFeatures = [...data.features].sort((a, b) => + a.id.localeCompare(b.id), + ); + + // Calculate total results from context if available + let totalResults = data.features.length; + if (data.context) { + totalResults = data.context.matched; + } + + // Update context with results (using 0 for totalPages/currentPage as placeholders) + setResults(sortedFeatures, false, totalResults); + + // Store pagination links + if (data.links) { + const typedLinks = data.links as Array< + IStacLink & { method?: string; body?: Record } + >; + setPaginationLinks(typedLinks); + } + } catch (error) { + setResults([], false, 0); + } + }, + [model, setResults, setPaginationLinks], + ); + + /** + * Adds a STAC item to the map + * @param stacData - STAC item to add + */ + const addToMap = useCallback( + (stacData: IStacItem): void => { + if (!model) { + return; + } + + const layerId = UUID.uuid4(); + const layerModel: IJGISLayer = { + type: 'StacLayer', + parameters: { data: stacData }, + visible: true, + name: stacData.properties?.title ?? stacData.id, + }; + + model.addLayer(layerId, layerModel); + }, + [model], + ); + + // Register addToMap with context + useEffect(() => { + registerAddToMap(addToMap); + }, [addToMap, registerAddToMap]); + return { startTime, setStartTime, @@ -58,5 +312,7 @@ export function useGeneric({ model }: IUseGenericProps): IUseGenericReturn { setCurrentBBox, useWorldBBox, setUseWorldBBox, + handleSubmit, + fetchUsingLink, }; } From 2269589dff73e3debf5bd81045ae137bb292ceab Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 8 Dec 2025 11:16:16 +0100 Subject: [PATCH 23/70] Rename function --- .../components/StacPanelResults.tsx | 31 +++++-------------- .../context/StacResultsContext.tsx | 26 +++++++++++++++- .../src/stacBrowser/hooks/useGeodesSearch.ts | 23 +++++++++++--- .../stacBrowser/hooks/useStacGenericFilter.ts | 8 ++--- .../src/stacBrowser/hooks/useStacSearch.ts | 6 ++-- 5 files changed, 59 insertions(+), 35 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index 462871694..ed3e7bf81 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -51,33 +51,15 @@ const StacPanelResults = () => { formatResult, isLoading, paginationLinks, + currentPage, + setCurrentPage, } = useStacResultsContext(); - // Use a ref to track previous results and detect actual changes - const prevResultsRef = useRef([]); - const resultsIdsRef = useRef(''); - - useEffect(() => { - // Create a string of result IDs for comparison (more reliable than array reference) - const currentResultsIds = results.map(r => r.id).join(','); - - // Only log if results actually changed (by ID comparison) - if (currentResultsIds !== resultsIdsRef.current) { - console.log('[StacPanelResults] Results updated:', { - count: results.length, - resultIds: results.map(r => r.id), - previousCount: prevResultsRef.current.length, - }); - // Update refs - prevResultsRef.current = results; - resultsIdsRef.current = currentResultsIds; - } - }, [results]); useEffect(() => { - console.log('links effect GOOO'); - }, [paginationLinks]); + console.log('current page in results', currentPage); + }, [currentPage]); const isNext = paginationLinks.some(link => link.rel === 'next'); const isPrev = paginationLinks.some(link => link.rel === 'previous'); @@ -129,7 +111,10 @@ const StacPanelResults = () => { )} handlePaginationClick('next')} + onClick={() => { + setCurrentPage(currentPage + 1); + handlePaginationClick('next'); + }} disabled={!isNext} /> diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index 303ad31b1..d08fdc7fd 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -6,6 +6,7 @@ import React, { useCallback, ReactNode, useRef, + useEffect, } from 'react'; import { IStacItem, IStacLink } from '../types/types'; @@ -22,6 +23,9 @@ interface IStacResultsContext { >; selectedUrl: string; setSelectedUrl: (url: string) => void; + currentPage: number; + setCurrentPage: (page: number) => void; + currentPageRef: React.MutableRefObject; setResults: ( results: IStacItem[], isLoading: boolean, @@ -67,6 +71,8 @@ export function StacResultsProvider({ const [selectedUrl, setSelectedUrlState] = useState( 'https://stac.dataspace.copernicus.eu/v1/', ); + const [currentPage, setCurrentPageState] = useState(1); + const currentPageRef = useRef(1); const [externalHandlers, setExternalHandlers] = useState<{ handlePaginationClick: (dir: 'next' | 'previous') => Promise; handleResultClick: (id: string) => Promise; @@ -82,6 +88,14 @@ export function StacResultsProvider({ >(); const addToMapRef = useRef<(stacData: IStacItem) => void>(); + // Keep ref in sync with state + useEffect(() => { + console.log('update curr page ref in context', currentPage) + currentPageRef.current = currentPage; + }, [currentPage]); + + + const setResults = useCallback( ( newResults: IStacItem[], @@ -108,6 +122,11 @@ export function StacResultsProvider({ setSelectedUrlState(url); }, []); + const setCurrentPage = useCallback((page: number) => { + setCurrentPageState(page); + }, []); + + // ! this has got to go // Register functions from hooks const registerFetchUsingLink = useCallback( ( @@ -127,6 +146,7 @@ export function StacResultsProvider({ [], ); + // ! pagination should always be the same const setPaginationHandlers = useCallback( ( newHandlePaginationClick: (dir: 'next' | 'previous') => Promise, @@ -146,6 +166,7 @@ export function StacResultsProvider({ // Use external handlers if provided, otherwise use context-created ones const handlePaginationClick = useCallback( async (dir: 'next' | 'previous'): Promise => { + console.log('context pgination click') if (!model) { return; } @@ -178,7 +199,7 @@ export function StacResultsProvider({ addToMapRef.current(result); } }, - [model, results], // Direct dependency - no ref needed! + [model, results], ); const formatResult = useCallback((item: IStacItem): string => { @@ -208,6 +229,9 @@ export function StacResultsProvider({ paginationLinks, selectedUrl, setSelectedUrl, + currentPage, + setCurrentPage, + currentPageRef, setResults, setPaginationLinks, registerFetchUsingLink, diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index 9f1adf29d..c813a9602 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -14,6 +14,7 @@ import { } from '@/src/stacBrowser/types/types'; import { GlobalStateDbManager } from '@/src/store'; import { useStacSearch } from './useStacSearch'; +import { useStacResultsContext } from '../context/StacResultsContext'; interface IUseGeodesSearchProps { model: IJupyterGISModel | undefined; @@ -71,6 +72,19 @@ function useGeodesSearch({ }: IUseGeodesSearchProps): IUseGeodesSearchReturn { const isFirstRender = useIsFirstRender(); const stateDb = GlobalStateDbManager.getInstance().getStateDb(); + const { + currentPage, currentPageRef + } = useStacResultsContext(); + + useEffect(() => { + console.log('current page', currentPage); + console.log('current page ref i think this one ', currentPageRef.current); + + }, [currentPage]); + + useEffect(() => { + console.log('current page ref', currentPageRef.current); + }, [currentPageRef.current]); // Get temporal/spatial filters and fetch functions from useStacSearch const { @@ -81,7 +95,7 @@ function useGeodesSearch({ currentBBox, useWorldBBox, setUseWorldBBox, - handleSubmit: handleSubmitFromGeneric, + executeQuery: executeQueryFromGeneric, fetchUsingLink, } = useStacSearch({ model, @@ -193,9 +207,9 @@ function useGeodesSearch({ return; } - // Use handleSubmit from useStacSearch to initiate the query - await handleSubmitFromGeneric(buildGeodesQuery, apiUrl); - }, [model, buildGeodesQuery, handleSubmitFromGeneric, apiUrl]); + // Use executeQuery from useStacSearch to initiate the query + await executeQueryFromGeneric(buildGeodesQuery, apiUrl); + }, [model, buildGeodesQuery, executeQueryFromGeneric, apiUrl]); // Handle search when filters change useEffect(() => { @@ -228,6 +242,7 @@ function useGeodesSearch({ */ const handlePaginationClick = useCallback( async (dir: 'next' | 'previous'): Promise => { + console.log('geodes page click', currentPage, currentPageRef.current) // Context will handle this using fetchUsingLinkRef }, [], diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index afb4522a5..032e14850 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -70,7 +70,7 @@ export function useStacGenericFilter({ currentBBox, useWorldBBox, setUseWorldBBox, - handleSubmit: handleSubmitFromGeneric, + executeQuery: executeQueryFromGeneric, fetchUsingLink, } = useStacSearch({ model, @@ -221,10 +221,10 @@ export function useStacGenericFilter({ return; } - // Use handleSubmit from useStacSearch to initiate the query + // Use executeQuery from useStacSearch to initiate the query const searchUrl = getSearchUrl(baseUrl); - await handleSubmitFromGeneric(buildCopernicusQuery, searchUrl); - }, [model, buildCopernicusQuery, handleSubmitFromGeneric, baseUrl]); + await executeQueryFromGeneric(buildCopernicusQuery, searchUrl); + }, [model, buildCopernicusQuery, executeQueryFromGeneric, baseUrl]); // Register fetchUsingLink from useStacSearch with context so handlers can use it useEffect(() => { diff --git a/packages/base/src/stacBrowser/hooks/useStacSearch.ts b/packages/base/src/stacBrowser/hooks/useStacSearch.ts index c1e7b53ff..979ab1340 100644 --- a/packages/base/src/stacBrowser/hooks/useStacSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useStacSearch.ts @@ -34,7 +34,7 @@ interface IUseStacSearchReturn { useWorldBBox: boolean; setUseWorldBBox: (val: boolean) => void; // Core fetch functions - handleSubmit: ( + executeQuery: ( buildQuery: () => IStacQueryBody, apiUrl: string, ) => Promise; @@ -83,7 +83,7 @@ export function useStacSearch({ }, [model, useWorldBBox]); // Core submit function - accepts a query builder function and initiates the query - const handleSubmit = useCallback( + const executeQuery = useCallback( async (buildQuery: () => IStacQueryBody, apiUrl: string) => { if (!model) { return; @@ -312,7 +312,7 @@ export function useStacSearch({ setCurrentBBox, useWorldBBox, setUseWorldBBox, - handleSubmit, + executeQuery, fetchUsingLink, }; } From fd15d9dff23f88af3a106964560b3ac530523ae7 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 8 Dec 2025 16:18:43 +0100 Subject: [PATCH 24/70] Use ref to build query --- .../src/stacBrowser/hooks/useGeodesSearch.ts | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index c813a9602..ba3dfd808 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -153,8 +153,10 @@ function useGeodesSearch({ /** * Builds GEODES-specific query + * @param page - Page number for pagination (defaults to currentPageRef.current) */ - const buildGeodesQuery = useCallback((): IStacQueryBody => { + const buildGeodesQuery = useCallback((page?: number): IStacQueryBody => { + const pageToUse = page ?? currentPageRef.current; const processingLevel = new Set(); const productType = new Set(); @@ -174,6 +176,7 @@ function useGeodesSearch({ return { bbox: currentBBox, limit: 12, + page: pageToUse, query: { latest: { eq: true }, dataset: { in: Array.from(filterState.datasets) }, @@ -197,7 +200,7 @@ function useGeodesSearch({ }, sortBy: [{ direction: 'desc', field: 'start_datetime' }], }; - }, [filterState, currentBBox, startTime, endTime]); + }, [filterState, currentBBox, startTime, endTime, currentPageRef]); /** * Handles form submission - builds query and fetches results @@ -236,16 +239,25 @@ function useGeodesSearch({ }, []); /** - * Handles pagination clicks (link-only, no page numbers) - * Uses fetchUsingLink from useStacSearch which is registered with context + * Handles pagination clicks for GEODES + * Rebuilds query with currentPageRef.current and executes it * @param dir - Direction ('next' | 'previous') */ const handlePaginationClick = useCallback( async (dir: 'next' | 'previous'): Promise => { - console.log('geodes page click', currentPage, currentPageRef.current) - // Context will handle this using fetchUsingLinkRef + if (!model) { + return; + } + + console.log('geodes page click', dir, 'currentPage:', currentPage, 'currentPageRef.current:', currentPageRef.current); + + // Rebuild query with the current page from ref + const queryWithPage = buildGeodesQuery(currentPageRef.current); + + // Execute the query + await executeQueryFromGeneric(() => queryWithPage, apiUrl); }, - [], + [model, buildGeodesQuery, executeQueryFromGeneric, apiUrl, currentPage, currentPageRef], ); /** From 624ad2e1dd5a4f8c26ef49d43be375e47194b4f2 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 8 Dec 2025 16:27:43 +0100 Subject: [PATCH 25/70] Handle prev and use currentpage in UI --- .../src/stacBrowser/components/StacPanelResults.tsx | 10 +++++----- .../src/stacBrowser/context/StacResultsContext.tsx | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index ed3e7bf81..75a89dbaa 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -55,8 +55,6 @@ const StacPanelResults = () => { setCurrentPage, } = useStacResultsContext(); - - useEffect(() => { console.log('current page in results', currentPage); }, [currentPage]); @@ -71,8 +69,10 @@ const StacPanelResults = () => { handlePaginationClick('previous') - // handlePaginationClick(Math.max(1, currentPage - 1)) + () => { + setCurrentPage(Math.max(currentPage - 1, 1)); + handlePaginationClick('previous'); + } } disabled={!isPrev} /> @@ -105,7 +105,7 @@ const StacPanelResults = () => { isActive={true} onClick={() => handlePaginationClick('next')} > - 1 + {currentPage} )} diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index d08fdc7fd..a01dbeb03 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -94,8 +94,6 @@ export function StacResultsProvider({ currentPageRef.current = currentPage; }, [currentPage]); - - const setResults = useCallback( ( newResults: IStacItem[], From 6106816e671905a4557421a2370631255055af85 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 8 Dec 2025 16:35:49 +0100 Subject: [PATCH 26/70] CSS fix --- packages/base/src/stacBrowser/components/StacPanelResults.tsx | 1 - packages/base/style/base.css | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index 75a89dbaa..ea6fc05d4 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -122,7 +122,6 @@ const StacPanelResults = () => {
{isLoading ? ( // TODO: Fancy spinner diff --git a/packages/base/style/base.css b/packages/base/style/base.css index 55ca2a96f..d4b150ed6 100644 --- a/packages/base/style/base.css +++ b/packages/base/style/base.css @@ -11,6 +11,7 @@ @import url('./statusBar.css'); @import url('./temporalSlider.css'); @import url('./tabPanel.css'); +@import url('./stacBrowser.css'); @import url('./storyPanel.css'); @import url('ol/ol.css'); @import url('./shared/button.css'); From 4217bf30c6ec171af944a86c61e22366b29267b2 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 8 Dec 2025 17:00:15 +0100 Subject: [PATCH 27/70] Thank god --- .../geodes/StacGeodesFilterPanel.tsx | 2 - .../context/StacResultsContext.tsx | 60 +++++++------------ .../src/stacBrowser/hooks/useGeodesSearch.ts | 46 +++----------- 3 files changed, 30 insertions(+), 78 deletions(-) diff --git a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx index 46e11bd26..a0fad537f 100644 --- a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx @@ -27,7 +27,6 @@ interface IStacGeodesFilterPanelProps { const StacGeodesFilterPanel = ({ model }: IStacGeodesFilterPanelProps) => { const { setResults, - setPaginationHandlers, setPaginationLinks, registerAddToMap, registerFetchUsingLink, @@ -48,7 +47,6 @@ const StacGeodesFilterPanel = ({ model }: IStacGeodesFilterPanelProps) => { apiUrl: selectedUrl, setResults, setPaginationLinks, - setPaginationHandlers, registerAddToMap, registerFetchUsingLink, }); diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index a01dbeb03..f130542bb 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -41,11 +41,8 @@ interface IStacResultsContext { ) => Promise, ) => void; registerAddToMap: (addFn: (stacData: IStacItem) => void) => void; - // Set handlers from outside (for GEODES search) - setPaginationHandlers: ( - handlePaginationClick: (dir: 'next' | 'previous') => Promise, - handleResultClick: (id: string) => Promise, - formatResult: (item: IStacItem) => string, + registerHandlePaginationClick: ( + handleFn: (dir: 'next' | 'previous') => Promise, ) => void; } @@ -73,11 +70,6 @@ export function StacResultsProvider({ ); const [currentPage, setCurrentPageState] = useState(1); const currentPageRef = useRef(1); - const [externalHandlers, setExternalHandlers] = useState<{ - handlePaginationClick: (dir: 'next' | 'previous') => Promise; - handleResultClick: (id: string) => Promise; - formatResult: (item: IStacItem) => string; - } | null>(null); // Store hook-specific functions in refs (these are set by the hooks) const fetchUsingLinkRef = @@ -87,6 +79,9 @@ export function StacResultsProvider({ ) => Promise >(); const addToMapRef = useRef<(stacData: IStacItem) => void>(); + const handlePaginationClickRef = useRef< + (dir: 'next' | 'previous') => Promise + >(); // Keep ref in sync with state useEffect(() => { @@ -144,27 +139,24 @@ export function StacResultsProvider({ [], ); - // ! pagination should always be the same - const setPaginationHandlers = useCallback( - ( - newHandlePaginationClick: (dir: 'next' | 'previous') => Promise, - newHandleResultClick: (id: string) => Promise, - newFormatResult: (item: IStacItem) => string, - ) => { - setExternalHandlers({ - handlePaginationClick: newHandlePaginationClick, - handleResultClick: newHandleResultClick, - formatResult: newFormatResult, - }); + const registerHandlePaginationClick = useCallback( + (handleFn: (dir: 'next' | 'previous') => Promise) => { + handlePaginationClickRef.current = handleFn; }, [], ); // Handlers created in context - always read latest state directly - // Use external handlers if provided, otherwise use context-created ones + // Use registered handler if provided, otherwise use context-created one const handlePaginationClick = useCallback( async (dir: 'next' | 'previous'): Promise => { - console.log('context pgination click') + // Use registered handler if available (e.g., from GEODES) + if (handlePaginationClickRef.current) { + await handlePaginationClickRef.current(dir); + return; + } + + // Default handler for generic STAC (Copernicus) if (!model) { return; } @@ -193,6 +185,7 @@ export function StacResultsProvider({ const currentResults = results; const result = currentResults.find((r: IStacItem) => r.id === id); + console.log('handler ersult click context') if (result && addToMapRef.current) { addToMapRef.current(result); } @@ -204,26 +197,15 @@ export function StacResultsProvider({ return item.properties?.title ?? item.id; }, []); - // Use external handlers if provided, otherwise use context-created ones - const finalHandlePaginationClick = externalHandlers - ? externalHandlers.handlePaginationClick - : handlePaginationClick; - const finalHandleResultClick = externalHandlers - ? externalHandlers.handleResultClick - : handleResultClick; - const finalFormatResult = externalHandlers - ? externalHandlers.formatResult - : formatResult; - return ( {children} diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index ba3dfd808..6cf18e0d4 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -27,11 +27,6 @@ interface IUseGeodesSearchProps { setPaginationLinks: ( links: Array }>, ) => void; - setPaginationHandlers: ( - handlePaginationClick: (dir: 'next' | 'previous') => Promise, - handleResultClick: (id: string) => Promise, - formatResult: (item: IStacItem) => string, - ) => void; registerAddToMap: (addFn: (stacData: IStacItem) => void) => void; registerFetchUsingLink: ( fetchFn: ( @@ -66,14 +61,15 @@ function useGeodesSearch({ apiUrl, setResults, setPaginationLinks, - setPaginationHandlers, registerAddToMap, registerFetchUsingLink, }: IUseGeodesSearchProps): IUseGeodesSearchReturn { const isFirstRender = useIsFirstRender(); const stateDb = GlobalStateDbManager.getInstance().getStateDb(); const { - currentPage, currentPageRef + currentPage, + currentPageRef, + registerHandlePaginationClick, } = useStacResultsContext(); useEffect(() => { @@ -111,6 +107,8 @@ function useGeodesSearch({ products: new Set(), }); + + const filterSetters: StacFilterSetters = { collections: val => setFilterState(s => ({ ...s, collections: new Set(val) })), @@ -229,14 +227,6 @@ function useGeodesSearch({ handleSubmit, ]); - /** - * Handles clicking on a result item - * This will be used by context, which will call addToMap via registerAddToMap - * @param id - ID of the clicked result (not used directly, context handles it) - */ - const handleResultClick = useCallback(async (_id: string): Promise => { - // Context will handle this using addToMapRef - }, []); /** * Handles pagination clicks for GEODES @@ -253,40 +243,22 @@ function useGeodesSearch({ // Rebuild query with the current page from ref const queryWithPage = buildGeodesQuery(currentPageRef.current); - + // Execute the query await executeQueryFromGeneric(() => queryWithPage, apiUrl); }, [model, buildGeodesQuery, executeQueryFromGeneric, apiUrl, currentPage, currentPageRef], ); - /** - * Formats a result item for display - * @param item - STAC item to format - * @returns Formatted string representation of the item - */ - const formatResult = useCallback((item: IStacItem): string => { - return item.properties?.title ?? item.id; - }, []); - // Register fetchUsingLink with context useEffect(() => { registerFetchUsingLink(fetchUsingLink); }, [fetchUsingLink, registerFetchUsingLink]); - // Register handlers with context + // Register handlePaginationClick with context useEffect(() => { - setPaginationHandlers( - handlePaginationClick, - handleResultClick, - formatResult, - ); - }, [ - handlePaginationClick, - handleResultClick, - formatResult, - setPaginationHandlers, - ]); + registerHandlePaginationClick(handlePaginationClick); + }, [handlePaginationClick, registerHandlePaginationClick]); return { filterState, From eefca665decc02e75b51279e5be929f3fba48b97 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 8 Dec 2025 18:01:45 +0100 Subject: [PATCH 28/70] Get total pages again --- .../components/StacPanelResults.tsx | 58 ++++++++++--------- .../context/StacResultsContext.tsx | 23 ++++++-- .../src/stacBrowser/hooks/useGeodesSearch.ts | 7 +-- .../stacBrowser/hooks/useStacGenericFilter.ts | 1 + .../src/stacBrowser/hooks/useStacSearch.ts | 37 +++++++----- packages/base/src/stacBrowser/types/types.ts | 8 +++ 6 files changed, 80 insertions(+), 54 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index ea6fc05d4..cb4e7689e 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -53,11 +53,13 @@ const StacPanelResults = () => { paginationLinks, currentPage, setCurrentPage, + totalPages } = useStacResultsContext(); useEffect(() => { console.log('current page in results', currentPage); - }, [currentPage]); + console.log('totalPages', totalPages) + }, [currentPage, totalPages]); const isNext = paginationLinks.some(link => link.rel === 'next'); const isPrev = paginationLinks.some(link => link.rel === 'previous'); @@ -80,34 +82,34 @@ const StacPanelResults = () => { {results.length === 0 ? (
No Matches Found
) : ( - // getPageItems(currentPage, totalPages).map(item => { - // if (item === 'ellipsis') { - // return ( - // - // - // - // ); - // } - // return ( - // - // handlePaginationClick('next')} - // > - // {item} - // - // - // ); - // }) + getPageItems(currentPage, totalPages).map(item => { + if (item === 'ellipsis') { + return ( + + + + ); + } + return ( + + handlePaginationClick('next')} + > + {item} + + + ); + }) - - handlePaginationClick('next')} - > - {currentPage} - - + // + // handlePaginationClick('next')} + // > + // {currentPage} + // + // )} Promise; handleResultClick: (id: string) => Promise; formatResult: (item: IStacItem) => string; @@ -26,11 +27,7 @@ interface IStacResultsContext { currentPage: number; setCurrentPage: (page: number) => void; currentPageRef: React.MutableRefObject; - setResults: ( - results: IStacItem[], - isLoading: boolean, - totalResults: number, - ) => void; + setResults: SetResultsFunction; setPaginationLinks: ( links: Array }>, ) => void; @@ -62,6 +59,7 @@ export function StacResultsProvider({ const [results, setResultsState] = useState([]); const [isLoading, setIsLoading] = useState(false); const [totalResults, setTotalResults] = useState(0); + const [totalPages, setTotalPages] = useState(0); const [paginationLinks, setPaginationLinksState] = useState< Array }> >([]); @@ -94,10 +92,12 @@ export function StacResultsProvider({ newResults: IStacItem[], newIsLoading: boolean, newTotalResults: number, + newTotalPages: number, ) => { setResultsState(newResults); setIsLoading(newIsLoading); setTotalResults(newTotalResults); + setTotalPages(newTotalPages); }, [], ); @@ -113,6 +113,16 @@ export function StacResultsProvider({ const setSelectedUrl = useCallback((url: string) => { setSelectedUrlState(url); + // Clear handlers when provider changes to prevent stale handlers + handlePaginationClickRef.current = undefined; + fetchUsingLinkRef.current = undefined; + addToMapRef.current = undefined; + // Reset pagination state + setCurrentPageState(1); + setResultsState([]); + setPaginationLinksState([]); + setTotalResults(0); + setTotalPages(0); }, []); const setCurrentPage = useCallback((page: number) => { @@ -203,6 +213,7 @@ export function StacResultsProvider({ results, isLoading, totalResults, + totalPages, handlePaginationClick, handleResultClick, formatResult, diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index 6cf18e0d4..0c7821fa3 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -8,6 +8,7 @@ import { IStacItem, IStacLink, IStacQueryBody, + SetResultsFunction, StacFilterState, StacFilterSetters, StacFilterStateStateDb, @@ -19,11 +20,7 @@ import { useStacResultsContext } from '../context/StacResultsContext'; interface IUseGeodesSearchProps { model: IJupyterGISModel | undefined; apiUrl: string; - setResults: ( - results: IStacItem[], - isLoading: boolean, - totalResults: number, - ) => void; + setResults: SetResultsFunction; setPaginationLinks: ( links: Array }>, ) => void; diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 032e14850..1b02a44c5 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -40,6 +40,7 @@ interface IUseStacGenericFilterProps { results: IStacItem[], isLoading: boolean, totalResults: number, + totalPages: number, ) => void; setPaginationLinks: ( links: Array }>, diff --git a/packages/base/src/stacBrowser/hooks/useStacSearch.ts b/packages/base/src/stacBrowser/hooks/useStacSearch.ts index 979ab1340..6c362a800 100644 --- a/packages/base/src/stacBrowser/hooks/useStacSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useStacSearch.ts @@ -8,15 +8,12 @@ import { IStacLink, IStacQueryBody, IStacSearchResult, + SetResultsFunction, } from '../types/types'; interface IUseStacSearchProps { model: IJupyterGISModel | undefined; - setResults: ( - results: IStacItem[], - isLoading: boolean, - totalResults: number, - ) => void; + setResults: SetResultsFunction; setPaginationLinks: ( links: Array }>, ) => void; @@ -104,7 +101,7 @@ export function useStacSearch({ try { // Update context with loading state - setResults([], true, 0); + setResults([], true, 0, 0); const data = (await fetchWithProxies( apiUrl, @@ -116,7 +113,7 @@ export function useStacSearch({ )) as IStacSearchResult; if (!data) { - setResults([], false, 0); + setResults([], false, 0, 0); return; } @@ -157,12 +154,17 @@ export function useStacSearch({ // Calculate total results from context if available let totalResults = data.features.length; + let totalPages = 0; if (data.context) { totalResults = data.context.matched; + totalPages = Math.ceil(data.context.matched / data.context.limit); + } else if (sortedFeatures.length > 0) { + // If results found but no context, assume 1 page + totalPages = 1; } - // Update context with results (using 0 for totalPages/currentPage as placeholders) - setResults(sortedFeatures, false, totalResults); + // Update context with results + setResults(sortedFeatures, false, totalResults, totalPages); // Store pagination links if (data.links) { @@ -172,7 +174,7 @@ export function useStacSearch({ setPaginationLinks(typedLinks); } } catch (error) { - setResults([], false, 0); + setResults([], false, 0, 0); } }, [model, setResults, setPaginationLinks], @@ -201,7 +203,7 @@ export function useStacSearch({ try { // Update context with loading state - setResults([], true, 0); + setResults([], true, 0, 0); const data = (await fetchWithProxies( link.href, @@ -213,7 +215,7 @@ export function useStacSearch({ )) as IStacSearchResult; if (!data) { - setResults([], false, 0); + setResults([], false, 0, 0); return; } @@ -254,12 +256,17 @@ export function useStacSearch({ // Calculate total results from context if available let totalResults = data.features.length; + let totalPages = 0; if (data.context) { totalResults = data.context.matched; + totalPages = Math.ceil(data.context.matched / data.context.limit); + } else if (sortedFeatures.length > 0) { + // If results found but no context, assume 1 page + totalPages = 1; } - // Update context with results (using 0 for totalPages/currentPage as placeholders) - setResults(sortedFeatures, false, totalResults); + // Update context with results + setResults(sortedFeatures, false, totalResults, totalPages); // Store pagination links if (data.links) { @@ -269,7 +276,7 @@ export function useStacSearch({ setPaginationLinks(typedLinks); } } catch (error) { - setResults([], false, 0); + setResults([], false, 0, 0); } }, [model, setResults, setPaginationLinks], diff --git a/packages/base/src/stacBrowser/types/types.ts b/packages/base/src/stacBrowser/types/types.ts index cb66a0c6c..0ed7b6394 100644 --- a/packages/base/src/stacBrowser/types/types.ts +++ b/packages/base/src/stacBrowser/types/types.ts @@ -143,3 +143,11 @@ export type StacFilterSetters = Record< StacFilterKey, (val: Set) => void >; + +// Shared type for setResults function signature +export type SetResultsFunction = ( + results: IStacItem[], + isLoading: boolean, + totalResults: number, + totalPages: number, +) => void; From b19941b6e50c9dcc041504be261e86b01a3c774f Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 9 Dec 2025 09:29:59 +0100 Subject: [PATCH 29/70] comments --- packages/base/src/stacBrowser/components/StacPanelResults.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index cb4e7689e..69af59dfa 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -90,6 +90,10 @@ const StacPanelResults = () => { ); } + + // ! todo get this working + // thik of better implementtion to make it simpleer + // stop asking the ai return ( Date: Tue, 9 Dec 2025 11:43:26 +0100 Subject: [PATCH 30/70] disable page numebr links --- packages/base/src/stacBrowser/components/StacPanelResults.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index 69af59dfa..fe0d7112b 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -94,11 +94,13 @@ const StacPanelResults = () => { // ! todo get this working // thik of better implementtion to make it simpleer // stop asking the ai + // also is active is fucked now return ( handlePaginationClick('next')} + disabled={totalPages === 1} > {item} From e0d91d939851e3e19ee3062e73b87287787ff039 Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 9 Dec 2025 12:17:48 +0100 Subject: [PATCH 31/70] pagination works?? --- .../src/stacBrowser/components/StacPanel.tsx | 2 +- .../components/StacPanelResults.tsx | 12 +- .../context/StacResultsContext.tsx | 148 +++++++++++++++++- .../src/stacBrowser/hooks/useGeodesSearch.ts | 30 ++-- .../stacBrowser/hooks/useStacGenericFilter.ts | 21 +-- 5 files changed, 178 insertions(+), 35 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacPanel.tsx b/packages/base/src/stacBrowser/components/StacPanel.tsx index 5bc5c0efb..e653b929d 100644 --- a/packages/base/src/stacBrowser/components/StacPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacPanel.tsx @@ -15,7 +15,7 @@ import StacGenericFilterPanel from './StacGenericFilterPanel'; import StacPanelResults from './StacPanelResults'; import StacGeodesFilterPanel from './geodes/StacGeodesFilterPanel'; -const GEODES_URL = 'https://geodes-portal.cnes.fr/api/stac/search'; +const GEODES_URL = 'https://geodes-portal.cnes.fr/api/stac/'; // URL to panel component mapping for extensibility // Add new entries here to support additional STAC providers diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index fe0d7112b..7a01ef735 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -53,7 +53,8 @@ const StacPanelResults = () => { paginationLinks, currentPage, setCurrentPage, - totalPages + totalPages, + executeQuery, } = useStacResultsContext(); useEffect(() => { @@ -91,15 +92,14 @@ const StacPanelResults = () => { ); } - // ! todo get this working - // thik of better implementtion to make it simpleer - // stop asking the ai - // also is active is fucked now return ( handlePaginationClick('next')} + onClick={async () => { + setCurrentPage(item); + await executeQuery(item); + }} disabled={totalPages === 1} > {item} diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index 68bf65a69..99c1e00c9 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -9,7 +9,14 @@ import React, { useEffect, } from 'react'; -import { IStacItem, IStacLink, SetResultsFunction } from '../types/types'; +import { fetchWithProxies } from '@/src/tools'; +import { + IStacItem, + IStacLink, + IStacQueryBody, + IStacSearchResult, + SetResultsFunction, +} from '../types/types'; interface IStacResultsContext { results: IStacItem[]; @@ -41,6 +48,8 @@ interface IStacResultsContext { registerHandlePaginationClick: ( handleFn: (dir: 'next' | 'previous') => Promise, ) => void; + registerBuildQuery: (buildQueryFn: () => IStacQueryBody) => void; + executeQuery: (pageNumber?: number) => Promise; } const StacResultsContext = createContext( @@ -77,13 +86,13 @@ export function StacResultsProvider({ ) => Promise >(); const addToMapRef = useRef<(stacData: IStacItem) => void>(); - const handlePaginationClickRef = useRef< - (dir: 'next' | 'previous') => Promise - >(); + const handlePaginationClickRef = + useRef<(dir: 'next' | 'previous') => Promise>(); + const buildQueryRef = useRef<() => IStacQueryBody>(); // Keep ref in sync with state useEffect(() => { - console.log('update curr page ref in context', currentPage) + console.log('update curr page ref in context', currentPage); currentPageRef.current = currentPage; }, [currentPage]); @@ -113,12 +122,15 @@ export function StacResultsProvider({ const setSelectedUrl = useCallback((url: string) => { setSelectedUrlState(url); - // Clear handlers when provider changes to prevent stale handlers + // Clear all registered handlers when provider changes to prevent stale handlers handlePaginationClickRef.current = undefined; fetchUsingLinkRef.current = undefined; addToMapRef.current = undefined; - // Reset pagination state + buildQueryRef.current = undefined; + // Reset all state + setIsLoading(false); setCurrentPageState(1); + currentPageRef.current = 1; setResultsState([]); setPaginationLinksState([]); setTotalResults(0); @@ -156,6 +168,124 @@ export function StacResultsProvider({ [], ); + const registerBuildQuery = useCallback( + (buildQueryFn: () => IStacQueryBody) => { + buildQueryRef.current = buildQueryFn; + }, + [], + ); + + // Helper to get search URL from base URL + const getSearchUrl = (baseUrl: string): string => { + return baseUrl.endsWith('/') ? `${baseUrl}search` : `${baseUrl}/search`; + }; + + // Execute query using registered buildQuery function + const executeQuery = useCallback( + async (pageNumber?: number): Promise => { + if (!model || !buildQueryRef.current) { + return; + } + + const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; + let queryBody = buildQueryRef.current(); + + // If pageNumber is provided, inject it into the query (for GEODES) + if (pageNumber !== undefined && queryBody) { + queryBody = { ...queryBody, page: pageNumber }; + } + + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-XSRFToken': XSRF_TOKEN, + credentials: 'include', + }, + body: JSON.stringify(queryBody), + }; + + try { + // Update context with loading state + setResults([], true, 0, 0); + + const data = (await fetchWithProxies( + getSearchUrl(selectedUrl), + model, + async (response: Response) => await response.json(), + //@ts-expect-error Jupyter requires X-XSRFToken header + options, + 'internal', + )) as IStacSearchResult; + + if (!data) { + setResults([], false, 0, 0); + return; + } + + // Filter assets to only include items with 'overview' or 'thumbnail' roles + if (data.features && data.features.length > 0) { + data.features.forEach((feature: IStacItem) => { + if (feature.assets) { + const originalAssets = feature.assets; + const filteredAssets: Record = {}; + + for (const [key, asset] of Object.entries(originalAssets)) { + if ( + asset && + typeof asset === 'object' && + 'roles' in asset && + Array.isArray(asset.roles) + ) { + const roles = asset.roles; + + if ( + roles.includes('thumbnail') || + roles.includes('overview') + ) { + filteredAssets[key] = asset; + } + } + } + + feature.assets = filteredAssets; + } + }); + } + + // Sort features by id before setting results + const sortedFeatures = [...data.features].sort((a, b) => + a.id.localeCompare(b.id), + ); + + // Calculate total results from context if available + let totalResults = data.features.length; + let totalPages = 0; + if (data.context) { + totalResults = data.context.matched; + totalPages = Math.ceil(data.context.matched / data.context.limit); + } else if (sortedFeatures.length > 0) { + // If results found but no context, assume 1 page + totalPages = 1; + } + + // Update context with results + setResults(sortedFeatures, false, totalResults, totalPages); + + // Store pagination links + if (data.links) { + const typedLinks = data.links as Array< + IStacLink & { method?: string; body?: Record } + >; + setPaginationLinks(typedLinks); + } + } catch (error) { + setResults([], false, 0, 0); + } + }, + [model, selectedUrl, setResults, setPaginationLinks], + ); + // Handlers created in context - always read latest state directly // Use registered handler if provided, otherwise use context-created one const handlePaginationClick = useCallback( @@ -195,7 +325,7 @@ export function StacResultsProvider({ const currentResults = results; const result = currentResults.find((r: IStacItem) => r.id === id); - console.log('handler ersult click context') + console.log('handler ersult click context'); if (result && addToMapRef.current) { addToMapRef.current(result); } @@ -228,6 +358,8 @@ export function StacResultsProvider({ registerFetchUsingLink, registerAddToMap, registerHandlePaginationClick, + registerBuildQuery, + executeQuery, }} > {children} diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index 0c7821fa3..b6de60976 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -66,7 +66,10 @@ function useGeodesSearch({ const { currentPage, currentPageRef, + setCurrentPage, registerHandlePaginationClick, + registerBuildQuery, + executeQuery, } = useStacResultsContext(); useEffect(() => { @@ -88,7 +91,6 @@ function useGeodesSearch({ currentBBox, useWorldBBox, setUseWorldBBox, - executeQuery: executeQueryFromGeneric, fetchUsingLink, } = useStacSearch({ model, @@ -197,6 +199,11 @@ function useGeodesSearch({ }; }, [filterState, currentBBox, startTime, endTime, currentPageRef]); + // Register buildQuery with context - always use currentPageRef for latest page + useEffect(() => { + registerBuildQuery(() => buildGeodesQuery(currentPageRef.current)); + }, [registerBuildQuery, buildGeodesQuery, currentPageRef]); + /** * Handles form submission - builds query and fetches results */ @@ -205,9 +212,9 @@ function useGeodesSearch({ return; } - // Use executeQuery from useStacSearch to initiate the query - await executeQueryFromGeneric(buildGeodesQuery, apiUrl); - }, [model, buildGeodesQuery, executeQueryFromGeneric, apiUrl]); + // Use executeQuery from context to initiate the query + await executeQuery(); + }, [model, executeQuery]); // Handle search when filters change useEffect(() => { @@ -227,7 +234,7 @@ function useGeodesSearch({ /** * Handles pagination clicks for GEODES - * Rebuilds query with currentPageRef.current and executes it + * Updates currentPage and executes query with new page number * @param dir - Direction ('next' | 'previous') */ const handlePaginationClick = useCallback( @@ -238,13 +245,16 @@ function useGeodesSearch({ console.log('geodes page click', dir, 'currentPage:', currentPage, 'currentPageRef.current:', currentPageRef.current); - // Rebuild query with the current page from ref - const queryWithPage = buildGeodesQuery(currentPageRef.current); + // Calculate new page number + const newPage = dir === 'next' ? currentPageRef.current + 1 : currentPageRef.current - 1; + + // Update currentPage in context + setCurrentPage(newPage); - // Execute the query - await executeQueryFromGeneric(() => queryWithPage, apiUrl); + // Execute query with new page number + await executeQuery(newPage); }, - [model, buildGeodesQuery, executeQueryFromGeneric, apiUrl, currentPage, currentPageRef], + [model, executeQuery, setCurrentPage, currentPage, currentPageRef], ); // Register fetchUsingLink with context diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 1b02a44c5..53e27917c 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from 'react'; import { fetchWithProxies } from '@/src/tools'; import { useStacSearch } from './useStacSearch'; +import { useStacResultsContext } from '../context/StacResultsContext'; import { IStacCollection, IStacItem, @@ -27,11 +28,6 @@ export type UpdateQueryableFilter = ( filter: IQueryableFilter, ) => void; -// Helper to get search URL from base URL -const getSearchUrl = (baseUrl: string): string => { - return baseUrl.endsWith('/') ? `${baseUrl}search` : `${baseUrl}/search`; -}; - interface IUseStacGenericFilterProps { model?: IJupyterGISModel; baseUrl: string; @@ -71,7 +67,6 @@ export function useStacGenericFilter({ currentBBox, useWorldBBox, setUseWorldBBox, - executeQuery: executeQueryFromGeneric, fetchUsingLink, } = useStacSearch({ model, @@ -80,6 +75,8 @@ export function useStacGenericFilter({ registerAddToMap, }); + const { registerBuildQuery, executeQuery } = useStacResultsContext(); + const [queryableProps, setQueryableProps] = useState<[string, any][]>(); const [collections, setCollections] = useState([]); // ! temp @@ -217,15 +214,19 @@ export function useStacGenericFilter({ /** * Handles form submission - builds query and fetches results */ + // Register buildQuery with context + useEffect(() => { + registerBuildQuery(() => buildCopernicusQuery()); + }, [registerBuildQuery, buildCopernicusQuery, baseUrl]); + const handleSubmit = useCallback(async () => { if (!model) { return; } - // Use executeQuery from useStacSearch to initiate the query - const searchUrl = getSearchUrl(baseUrl); - await executeQueryFromGeneric(buildCopernicusQuery, searchUrl); - }, [model, buildCopernicusQuery, executeQueryFromGeneric, baseUrl]); + // Use executeQuery from context to initiate the query + await executeQuery(); + }, [model, executeQuery]); // Register fetchUsingLink from useStacSearch with context so handlers can use it useEffect(() => { From 70ce296414a91d4f5ccfc3457ab0fb2553c95d8f Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 9 Dec 2025 12:31:36 +0100 Subject: [PATCH 32/70] Accept prev and previous --- .../src/stacBrowser/components/StacPanel.tsx | 3 ++ .../components/StacPanelResults.tsx | 4 +- .../context/StacResultsContext.tsx | 10 +++- .../stacBrowser/hooks/useStacGenericFilter.ts | 52 +++++++++++++++++-- 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacPanel.tsx b/packages/base/src/stacBrowser/components/StacPanel.tsx index e653b929d..8aae88ffb 100644 --- a/packages/base/src/stacBrowser/components/StacPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacPanel.tsx @@ -63,6 +63,9 @@ const StacPanelContent = ({ model }: IStacViewProps) => { Copernicus +
diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index 7a01ef735..44acab35b 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -63,7 +63,9 @@ const StacPanelResults = () => { }, [currentPage, totalPages]); const isNext = paginationLinks.some(link => link.rel === 'next'); - const isPrev = paginationLinks.some(link => link.rel === 'previous'); + const isPrev = paginationLinks.some(link => + ['prev', 'previous'].includes(link.rel), + ); return (
diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index 99c1e00c9..4571a4a80 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -304,8 +304,14 @@ export function StacResultsProvider({ // Read directly from state - no closure issues! const currentLinks = paginationLinks; - // Find the pagination link by rel - const link = currentLinks.find(l => l.rel === dir); + // Find the pagination link by rel (support both 'previous' and 'prev') + const link = currentLinks.find(l => { + if (dir === 'next') { + return l.rel === 'next'; + } + // For 'previous', accept both 'previous' and 'prev' + return ['prev', 'previous'].includes(l.rel); + }); if (link && link.body && fetchUsingLinkRef.current) { // Use the registered fetch function diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 53e27917c..87c9b9c6c 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -1,4 +1,5 @@ -import { IJupyterGISModel } from '@jupytergis/schema'; +import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; +import { UUID } from '@lumino/coreutils'; import { endOfToday, startOfToday } from 'date-fns'; import { useCallback, useEffect, useState } from 'react'; @@ -87,6 +88,19 @@ export function useStacGenericFilter({ >({}); const [filterOperator, setFilterOperator] = useState('and'); + // Reset all state when URL changes + useEffect(() => { + setQueryableProps(undefined); + setCollections([]); + setSelectedCollection('sentinel-2-l2a'); + setQueryableFilters({}); + setFilterOperator('and'); + // Reset temporal/spatial filters + setStartTime(undefined); + setEndTime(undefined); + setUseWorldBBox(false); + }, [baseUrl, setStartTime, setEndTime, setUseWorldBBox]); + // for collections useEffect(() => { if (!model) { @@ -117,10 +131,14 @@ export function useStacGenericFilter({ }); setCollections(collections); + // Set first collection as default if available + if (collections.length > 0) { + setSelectedCollection(collections[0].id); + } }; fatch(); - }, [model]); + }, [model, baseUrl]); // for queryables // should listen for colletion changes and requery @@ -132,6 +150,7 @@ export function useStacGenericFilter({ } const fatch = async () => { + console.log('hittin dem queries boiiii') const queryablesUrl = baseUrl.endsWith('/') ? `${baseUrl}queryables` : `${baseUrl}/queryables`; @@ -147,7 +166,7 @@ export function useStacGenericFilter({ }; fatch(); - }, [model]); + }, [model, baseUrl]); const updateQueryableFilter = useCallback( (qKey: string, filter: IQueryableFilter) => { @@ -233,6 +252,33 @@ export function useStacGenericFilter({ registerFetchUsingLink(fetchUsingLink); }, [fetchUsingLink, registerFetchUsingLink]); + // Register addToMap function - useStacSearch registers it, but we need to ensure + // it's re-registered when URL changes (since addToMapRef gets cleared in setSelectedUrl) + // We create our own addToMap here to ensure it's always registered + const addToMap = useCallback( + (stacData: IStacItem): void => { + if (!model) { + return; + } + + const layerId = UUID.uuid4(); + const layerModel: IJGISLayer = { + type: 'StacLayer', + parameters: { data: stacData }, + visible: true, + name: stacData.properties?.title ?? stacData.id, + }; + + model.addLayer(layerId, layerModel); + }, + [model], + ); + + // Register addToMap with context + useEffect(() => { + registerAddToMap(addToMap); + }, [addToMap, registerAddToMap, baseUrl]); + return { queryableProps, collections, From 4dccfb461a93e6d8d62cfc66bf8ecd0aa51075b4 Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 9 Dec 2025 13:50:07 +0100 Subject: [PATCH 33/70] Move addToMap to context --- .../components/StacGenericFilterPanel.tsx | 2 -- .../geodes/StacGeodesFilterPanel.tsx | 2 -- .../context/StacResultsContext.tsx | 35 ++++++++++++++++--- .../src/stacBrowser/hooks/useGeodesSearch.ts | 3 -- .../stacBrowser/hooks/useStacGenericFilter.ts | 33 +---------------- .../src/stacBrowser/hooks/useStacSearch.ts | 32 +---------------- 6 files changed, 33 insertions(+), 74 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index 251f0593c..e76528f78 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -20,7 +20,6 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { setResults, setPaginationLinks, registerFetchUsingLink, - registerAddToMap, selectedUrl, } = useStacResultsContext(); const [limit, setLimit] = useState(12); @@ -47,7 +46,6 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { setResults, setPaginationLinks, registerFetchUsingLink, - registerAddToMap, }); diff --git a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx index a0fad537f..3be2b943a 100644 --- a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx @@ -28,7 +28,6 @@ const StacGeodesFilterPanel = ({ model }: IStacGeodesFilterPanelProps) => { const { setResults, setPaginationLinks, - registerAddToMap, registerFetchUsingLink, selectedUrl, } = useStacResultsContext(); @@ -47,7 +46,6 @@ const StacGeodesFilterPanel = ({ model }: IStacGeodesFilterPanelProps) => { apiUrl: selectedUrl, setResults, setPaginationLinks, - registerAddToMap, registerFetchUsingLink, }); diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index 4571a4a80..a99f68aa9 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -1,4 +1,5 @@ -import { IJupyterGISModel } from '@jupytergis/schema'; +import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; +import { UUID } from '@lumino/coreutils'; import React, { createContext, useContext, @@ -123,6 +124,7 @@ export function StacResultsProvider({ const setSelectedUrl = useCallback((url: string) => { setSelectedUrlState(url); // Clear all registered handlers when provider changes to prevent stale handlers + // Note: addToMapRef is cleared but defaultAddToMap is always available handlePaginationClickRef.current = undefined; fetchUsingLinkRef.current = undefined; addToMapRef.current = undefined; @@ -321,6 +323,26 @@ export function StacResultsProvider({ [model, paginationLinks], ); + // Default addToMap implementation - always available + const defaultAddToMap = useCallback( + (stacData: IStacItem): void => { + if (!model) { + return; + } + + const layerId = UUID.uuid4(); + const layerModel: IJGISLayer = { + type: 'StacLayer', + parameters: { data: stacData }, + visible: true, + name: stacData.properties?.title ?? stacData.id, + }; + + model.addLayer(layerId, layerModel); + }, + [model], + ); + const handleResultClick = useCallback( async (id: string): Promise => { if (!model) { @@ -332,11 +354,16 @@ export function StacResultsProvider({ const result = currentResults.find((r: IStacItem) => r.id === id); console.log('handler ersult click context'); - if (result && addToMapRef.current) { - addToMapRef.current(result); + if (result) { + // Use registered override if available, otherwise use default + if (addToMapRef.current) { + addToMapRef.current(result); + } else { + defaultAddToMap(result); + } } }, - [model, results], + [model, results, defaultAddToMap], ); const formatResult = useCallback((item: IStacItem): string => { diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index b6de60976..3cfdf4478 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -24,7 +24,6 @@ interface IUseGeodesSearchProps { setPaginationLinks: ( links: Array }>, ) => void; - registerAddToMap: (addFn: (stacData: IStacItem) => void) => void; registerFetchUsingLink: ( fetchFn: ( link: IStacLink & { method?: string; body?: Record }, @@ -58,7 +57,6 @@ function useGeodesSearch({ apiUrl, setResults, setPaginationLinks, - registerAddToMap, registerFetchUsingLink, }: IUseGeodesSearchProps): IUseGeodesSearchReturn { const isFirstRender = useIsFirstRender(); @@ -96,7 +94,6 @@ function useGeodesSearch({ model, setResults, setPaginationLinks, - registerAddToMap, }); const [filterState, setFilterState] = useState({ diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 87c9b9c6c..39c022640 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -1,5 +1,4 @@ -import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; -import { UUID } from '@lumino/coreutils'; +import { IJupyterGISModel } from '@jupytergis/schema'; import { endOfToday, startOfToday } from 'date-fns'; import { useCallback, useEffect, useState } from 'react'; @@ -47,7 +46,6 @@ interface IUseStacGenericFilterProps { link: IStacLink & { method?: string; body?: Record }, ) => Promise, ) => void; - registerAddToMap: (addFn: (stacData: IStacItem) => void) => void; } export function useStacGenericFilter({ @@ -57,7 +55,6 @@ export function useStacGenericFilter({ setResults, setPaginationLinks, registerFetchUsingLink, - registerAddToMap, }: IUseStacGenericFilterProps) { // Get temporal/spatial filters and fetch functions from useStacSearch const { @@ -73,7 +70,6 @@ export function useStacGenericFilter({ model, setResults, setPaginationLinks, - registerAddToMap, }); const { registerBuildQuery, executeQuery } = useStacResultsContext(); @@ -252,33 +248,6 @@ export function useStacGenericFilter({ registerFetchUsingLink(fetchUsingLink); }, [fetchUsingLink, registerFetchUsingLink]); - // Register addToMap function - useStacSearch registers it, but we need to ensure - // it's re-registered when URL changes (since addToMapRef gets cleared in setSelectedUrl) - // We create our own addToMap here to ensure it's always registered - const addToMap = useCallback( - (stacData: IStacItem): void => { - if (!model) { - return; - } - - const layerId = UUID.uuid4(); - const layerModel: IJGISLayer = { - type: 'StacLayer', - parameters: { data: stacData }, - visible: true, - name: stacData.properties?.title ?? stacData.id, - }; - - model.addLayer(layerId, layerModel); - }, - [model], - ); - - // Register addToMap with context - useEffect(() => { - registerAddToMap(addToMap); - }, [addToMap, registerAddToMap, baseUrl]); - return { queryableProps, collections, diff --git a/packages/base/src/stacBrowser/hooks/useStacSearch.ts b/packages/base/src/stacBrowser/hooks/useStacSearch.ts index 6c362a800..09ca46e41 100644 --- a/packages/base/src/stacBrowser/hooks/useStacSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useStacSearch.ts @@ -1,5 +1,4 @@ -import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; -import { UUID } from '@lumino/coreutils'; +import { IJupyterGISModel } from '@jupytergis/schema'; import { useCallback, useEffect, useState } from 'react'; import { fetchWithProxies } from '@/src/tools'; @@ -17,7 +16,6 @@ interface IUseStacSearchProps { setPaginationLinks: ( links: Array }>, ) => void; - registerAddToMap: (addFn: (stacData: IStacItem) => void) => void; } interface IUseStacSearchReturn { @@ -50,7 +48,6 @@ export function useStacSearch({ model, setResults, setPaginationLinks, - registerAddToMap, }: IUseStacSearchProps): IUseStacSearchReturn { const [startTime, setStartTime] = useState(undefined); const [endTime, setEndTime] = useState(undefined); @@ -282,33 +279,6 @@ export function useStacSearch({ [model, setResults, setPaginationLinks], ); - /** - * Adds a STAC item to the map - * @param stacData - STAC item to add - */ - const addToMap = useCallback( - (stacData: IStacItem): void => { - if (!model) { - return; - } - - const layerId = UUID.uuid4(); - const layerModel: IJGISLayer = { - type: 'StacLayer', - parameters: { data: stacData }, - visible: true, - name: stacData.properties?.title ?? stacData.id, - }; - - model.addLayer(layerId, layerModel); - }, - [model], - ); - - // Register addToMap with context - useEffect(() => { - registerAddToMap(addToMap); - }, [addToMap, registerAddToMap]); return { startTime, From 826585b6fd582215b26ef0539b5a639fcb730e19 Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 9 Dec 2025 14:20:32 +0100 Subject: [PATCH 34/70] handler comment for later --- python/jupytergis_core/jupytergis_core/handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/jupytergis_core/jupytergis_core/handler.py b/python/jupytergis_core/jupytergis_core/handler.py index 695bdbe51..af0d92c9c 100644 --- a/python/jupytergis_core/jupytergis_core/handler.py +++ b/python/jupytergis_core/jupytergis_core/handler.py @@ -40,6 +40,7 @@ def load_config() -> ProxyConfig: "https://geodes.cnes.fr", "https://gdh-portal-prod.cnes.fr", "https://geodes-portal.cnes.fr/api/stac/", + # ! add copernicus ?? }, ), ) From 36ab4117052ff177b481532ed7a0513e31980ce7 Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 9 Dec 2025 15:01:03 +0100 Subject: [PATCH 35/70] refactor execute query --- .../components/StacPanelResults.tsx | 4 +- .../context/StacResultsContext.tsx | 39 +++++++++++++------ .../src/stacBrowser/hooks/useGeodesSearch.ts | 19 +++++---- .../stacBrowser/hooks/useStacGenericFilter.ts | 12 ++++-- .../src/stacBrowser/hooks/useStacSearch.ts | 8 ++-- 5 files changed, 54 insertions(+), 28 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index 44acab35b..d403b5a72 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -54,7 +54,7 @@ const StacPanelResults = () => { currentPage, setCurrentPage, totalPages, - executeQuery, + executeQueryWithPage, } = useStacResultsContext(); useEffect(() => { @@ -100,7 +100,7 @@ const StacPanelResults = () => { isActive={item === currentPage} onClick={async () => { setCurrentPage(item); - await executeQuery(item); + await executeQueryWithPage(item); }} disabled={totalPages === 1} > diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index a99f68aa9..cbfb64f5d 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -50,7 +50,8 @@ interface IStacResultsContext { handleFn: (dir: 'next' | 'previous') => Promise, ) => void; registerBuildQuery: (buildQueryFn: () => IStacQueryBody) => void; - executeQuery: (pageNumber?: number) => Promise; + executeQuery: (body: IStacQueryBody, apiUrl?: string) => Promise; + executeQueryWithPage: (pageNumber: number) => Promise; } const StacResultsContext = createContext( @@ -182,20 +183,16 @@ export function StacResultsProvider({ return baseUrl.endsWith('/') ? `${baseUrl}search` : `${baseUrl}/search`; }; - // Execute query using registered buildQuery function + // Execute query using provided body const executeQuery = useCallback( - async (pageNumber?: number): Promise => { - if (!model || !buildQueryRef.current) { + async (body: IStacQueryBody, apiUrl?: string): Promise => { + if (!model) { return; } const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; - let queryBody = buildQueryRef.current(); - - // If pageNumber is provided, inject it into the query (for GEODES) - if (pageNumber !== undefined && queryBody) { - queryBody = { ...queryBody, page: pageNumber }; - } + const queryBody = body; + const urlToUse = apiUrl || getSearchUrl(selectedUrl); const options = { method: 'POST', @@ -212,7 +209,7 @@ export function StacResultsProvider({ setResults([], true, 0, 0); const data = (await fetchWithProxies( - getSearchUrl(selectedUrl), + urlToUse, model, async (response: Response) => await response.json(), //@ts-expect-error Jupyter requires X-XSRFToken header @@ -288,6 +285,25 @@ export function StacResultsProvider({ [model, selectedUrl, setResults, setPaginationLinks], ); + // Wrapper function that takes a page number, builds query, and executes it + const executeQueryWithPage = useCallback( + async (pageNumber: number): Promise => { + if (!model || !buildQueryRef.current) { + return; + } + + // Build query body + let queryBody = buildQueryRef.current(); + + // Inject page number into the query + queryBody = { ...queryBody, page: pageNumber }; + + // Execute query with the modified body + await executeQuery(queryBody); + }, + [model, executeQuery], + ); + // Handlers created in context - always read latest state directly // Use registered handler if provided, otherwise use context-created one const handlePaginationClick = useCallback( @@ -393,6 +409,7 @@ export function StacResultsProvider({ registerHandlePaginationClick, registerBuildQuery, executeQuery, + executeQueryWithPage, }} > {children} diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index 3cfdf4478..6bfa5aba3 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -68,6 +68,7 @@ function useGeodesSearch({ registerHandlePaginationClick, registerBuildQuery, executeQuery, + selectedUrl, } = useStacResultsContext(); useEffect(() => { @@ -209,9 +210,11 @@ function useGeodesSearch({ return; } - // Use executeQuery from context to initiate the query - await executeQuery(); - }, [model, executeQuery]); + const urlToUse = selectedUrl.endsWith('/') ? `${selectedUrl}search` : `${selectedUrl}/search`; + // Build query body and execute query + const queryBody = buildGeodesQuery(); + await executeQuery(queryBody, urlToUse); + }, [model, executeQuery, buildGeodesQuery, selectedUrl]); // Handle search when filters change useEffect(() => { @@ -244,14 +247,16 @@ function useGeodesSearch({ // Calculate new page number const newPage = dir === 'next' ? currentPageRef.current + 1 : currentPageRef.current - 1; - + // Update currentPage in context setCurrentPage(newPage); + const urlToUse = selectedUrl.endsWith('/') ? `${selectedUrl}search` : `${selectedUrl}/search`; - // Execute query with new page number - await executeQuery(newPage); + // Build query body with new page and execute query + const queryBody = buildGeodesQuery(newPage); + await executeQuery(queryBody, urlToUse); }, - [model, executeQuery, setCurrentPage, currentPage, currentPageRef], + [model, executeQuery, setCurrentPage, currentPage, currentPageRef, buildGeodesQuery, selectedUrl], ); // Register fetchUsingLink with context diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 39c022640..d43a79688 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -72,7 +72,7 @@ export function useStacGenericFilter({ setPaginationLinks, }); - const { registerBuildQuery, executeQuery } = useStacResultsContext(); + const { registerBuildQuery, executeQuery, selectedUrl } = useStacResultsContext(); const [queryableProps, setQueryableProps] = useState<[string, any][]>(); const [collections, setCollections] = useState([]); @@ -239,9 +239,13 @@ export function useStacGenericFilter({ return; } - // Use executeQuery from context to initiate the query - await executeQuery(); - }, [model, executeQuery]); + // Build query body and execute query + const queryBody = buildCopernicusQuery(); + const searchUrl = baseUrl.endsWith('/') + ? `${baseUrl}search` + : `${baseUrl}/search`; + await executeQuery(queryBody, searchUrl); + }, [model, executeQuery, buildCopernicusQuery, baseUrl]); // Register fetchUsingLink from useStacSearch with context so handlers can use it useEffect(() => { diff --git a/packages/base/src/stacBrowser/hooks/useStacSearch.ts b/packages/base/src/stacBrowser/hooks/useStacSearch.ts index 09ca46e41..ba4f18201 100644 --- a/packages/base/src/stacBrowser/hooks/useStacSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useStacSearch.ts @@ -30,7 +30,7 @@ interface IUseStacSearchReturn { setUseWorldBBox: (val: boolean) => void; // Core fetch functions executeQuery: ( - buildQuery: () => IStacQueryBody, + body: IStacQueryBody, apiUrl: string, ) => Promise; fetchUsingLink: ( @@ -76,15 +76,15 @@ export function useStacSearch({ }; }, [model, useWorldBBox]); - // Core submit function - accepts a query builder function and initiates the query + // Core submit function - accepts a query body and initiates the query const executeQuery = useCallback( - async (buildQuery: () => IStacQueryBody, apiUrl: string) => { + async (body: IStacQueryBody, apiUrl: string) => { if (!model) { return; } const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; - const queryBody = buildQuery(); + const queryBody = body; const options = { method: 'POST', From 2cf4df1cdd8d3a97d03cc119231e3ae825766c0c Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 9 Dec 2025 15:10:07 +0100 Subject: [PATCH 36/70] paginatio update --- .../context/StacResultsContext.tsx | 27 ++++++++++++++----- .../src/stacBrowser/hooks/useGeodesSearch.ts | 1 + .../src/stacBrowser/hooks/useStacSearch.ts | 6 +++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index cbfb64f5d..b10177ea3 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -50,7 +50,11 @@ interface IStacResultsContext { handleFn: (dir: 'next' | 'previous') => Promise, ) => void; registerBuildQuery: (buildQueryFn: () => IStacQueryBody) => void; - executeQuery: (body: IStacQueryBody, apiUrl?: string) => Promise; + executeQuery: ( + body: IStacQueryBody, + apiUrl?: string, + method?: string, + ) => Promise; executeQueryWithPage: (pageNumber: number) => Promise; } @@ -185,7 +189,11 @@ export function StacResultsProvider({ // Execute query using provided body const executeQuery = useCallback( - async (body: IStacQueryBody, apiUrl?: string): Promise => { + async ( + body: IStacQueryBody, + apiUrl?: string, + method?: string, + ): Promise => { if (!model) { return; } @@ -193,9 +201,10 @@ export function StacResultsProvider({ const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; const queryBody = body; const urlToUse = apiUrl || getSearchUrl(selectedUrl); + const httpMethod = (method || 'POST').toUpperCase(); const options = { - method: 'POST', + method: httpMethod, headers: { 'Content-Type': 'application/json', 'X-XSRFToken': XSRF_TOKEN, @@ -331,9 +340,15 @@ export function StacResultsProvider({ return ['prev', 'previous'].includes(l.rel); }); - if (link && link.body && fetchUsingLinkRef.current) { - // Use the registered fetch function - await fetchUsingLinkRef.current(link); + + // ! this is nice, if no body then link href should have search params - update eventually + if (link && link.body) { + // Use executeQuery with the link's body, href, and method + await executeQuery( + link.body as IStacQueryBody, + link.href, + link.method, + ); } }, [model, paginationLinks], diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index 6bfa5aba3..2ddb48754 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -210,6 +210,7 @@ function useGeodesSearch({ return; } + // ! someosmeosmesse urlTOUse const urlToUse = selectedUrl.endsWith('/') ? `${selectedUrl}search` : `${selectedUrl}/search`; // Build query body and execute query const queryBody = buildGeodesQuery(); diff --git a/packages/base/src/stacBrowser/hooks/useStacSearch.ts b/packages/base/src/stacBrowser/hooks/useStacSearch.ts index ba4f18201..a0dc25174 100644 --- a/packages/base/src/stacBrowser/hooks/useStacSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useStacSearch.ts @@ -32,6 +32,7 @@ interface IUseStacSearchReturn { executeQuery: ( body: IStacQueryBody, apiUrl: string, + method?: string, //! not 100% sure I want this ) => Promise; fetchUsingLink: ( link: IStacLink & { method?: string; body?: Record }, @@ -78,16 +79,17 @@ export function useStacSearch({ // Core submit function - accepts a query body and initiates the query const executeQuery = useCallback( - async (body: IStacQueryBody, apiUrl: string) => { + async (body: IStacQueryBody, apiUrl: string, method?: string) => { if (!model) { return; } const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; const queryBody = body; + const httpMethod = (method || 'POST').toUpperCase(); const options = { - method: 'POST', + method: httpMethod, headers: { 'Content-Type': 'application/json', 'X-XSRFToken': XSRF_TOKEN, From f4f6a4cecb70b23d471fb24237eaabb9fb7d4d4c Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 9 Dec 2025 16:18:58 +0100 Subject: [PATCH 37/70] Remove redundant fetch finally --- .../components/StacGenericFilterPanel.tsx | 2 - .../geodes/StacGeodesFilterPanel.tsx | 2 - .../context/StacResultsContext.tsx | 26 ----- .../src/stacBrowser/hooks/useGeodesSearch.ts | 11 -- .../stacBrowser/hooks/useStacGenericFilter.ts | 11 -- .../src/stacBrowser/hooks/useStacSearch.ts | 107 ------------------ 6 files changed, 159 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index e76528f78..99a3adf09 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -19,7 +19,6 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { const { setResults, setPaginationLinks, - registerFetchUsingLink, selectedUrl, } = useStacResultsContext(); const [limit, setLimit] = useState(12); @@ -45,7 +44,6 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { limit, setResults, setPaginationLinks, - registerFetchUsingLink, }); diff --git a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx index 3be2b943a..acac5f856 100644 --- a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx @@ -28,7 +28,6 @@ const StacGeodesFilterPanel = ({ model }: IStacGeodesFilterPanelProps) => { const { setResults, setPaginationLinks, - registerFetchUsingLink, selectedUrl, } = useStacResultsContext(); @@ -46,7 +45,6 @@ const StacGeodesFilterPanel = ({ model }: IStacGeodesFilterPanelProps) => { apiUrl: selectedUrl, setResults, setPaginationLinks, - registerFetchUsingLink, }); const handleDatasetSelection = (dataset: string, collection: string) => { diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index b10177ea3..4db6b8906 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -40,11 +40,6 @@ interface IStacResultsContext { links: Array }>, ) => void; // Register hook-specific functions that handlers need (for generic filter) - registerFetchUsingLink: ( - fetchFn: ( - link: IStacLink & { method?: string; body?: Record }, - ) => Promise, - ) => void; registerAddToMap: (addFn: (stacData: IStacItem) => void) => void; registerHandlePaginationClick: ( handleFn: (dir: 'next' | 'previous') => Promise, @@ -85,12 +80,6 @@ export function StacResultsProvider({ const currentPageRef = useRef(1); // Store hook-specific functions in refs (these are set by the hooks) - const fetchUsingLinkRef = - useRef< - ( - link: IStacLink & { method?: string; body?: Record }, - ) => Promise - >(); const addToMapRef = useRef<(stacData: IStacItem) => void>(); const handlePaginationClickRef = useRef<(dir: 'next' | 'previous') => Promise>(); @@ -131,7 +120,6 @@ export function StacResultsProvider({ // Clear all registered handlers when provider changes to prevent stale handlers // Note: addToMapRef is cleared but defaultAddToMap is always available handlePaginationClickRef.current = undefined; - fetchUsingLinkRef.current = undefined; addToMapRef.current = undefined; buildQueryRef.current = undefined; // Reset all state @@ -148,19 +136,6 @@ export function StacResultsProvider({ setCurrentPageState(page); }, []); - // ! this has got to go - // Register functions from hooks - const registerFetchUsingLink = useCallback( - ( - fetchFn: ( - link: IStacLink & { method?: string; body?: Record }, - ) => Promise, - ) => { - fetchUsingLinkRef.current = fetchFn; - }, - [], - ); - const registerAddToMap = useCallback( (addFn: (stacData: IStacItem) => void) => { addToMapRef.current = addFn; @@ -419,7 +394,6 @@ export function StacResultsProvider({ currentPageRef, setResults, setPaginationLinks, - registerFetchUsingLink, registerAddToMap, registerHandlePaginationClick, registerBuildQuery, diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index 2ddb48754..758385989 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -24,11 +24,6 @@ interface IUseGeodesSearchProps { setPaginationLinks: ( links: Array }>, ) => void; - registerFetchUsingLink: ( - fetchFn: ( - link: IStacLink & { method?: string; body?: Record }, - ) => Promise, - ) => void; } interface IUseGeodesSearchReturn { @@ -57,7 +52,6 @@ function useGeodesSearch({ apiUrl, setResults, setPaginationLinks, - registerFetchUsingLink, }: IUseGeodesSearchProps): IUseGeodesSearchReturn { const isFirstRender = useIsFirstRender(); const stateDb = GlobalStateDbManager.getInstance().getStateDb(); @@ -90,7 +84,6 @@ function useGeodesSearch({ currentBBox, useWorldBBox, setUseWorldBBox, - fetchUsingLink, } = useStacSearch({ model, setResults, @@ -260,10 +253,6 @@ function useGeodesSearch({ [model, executeQuery, setCurrentPage, currentPage, currentPageRef, buildGeodesQuery, selectedUrl], ); - // Register fetchUsingLink with context - useEffect(() => { - registerFetchUsingLink(fetchUsingLink); - }, [fetchUsingLink, registerFetchUsingLink]); // Register handlePaginationClick with context useEffect(() => { diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index d43a79688..0c900303c 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -41,11 +41,6 @@ interface IUseStacGenericFilterProps { setPaginationLinks: ( links: Array }>, ) => void; - registerFetchUsingLink: ( - fetchFn: ( - link: IStacLink & { method?: string; body?: Record }, - ) => Promise, - ) => void; } export function useStacGenericFilter({ @@ -54,7 +49,6 @@ export function useStacGenericFilter({ limit = 12, setResults, setPaginationLinks, - registerFetchUsingLink, }: IUseStacGenericFilterProps) { // Get temporal/spatial filters and fetch functions from useStacSearch const { @@ -65,7 +59,6 @@ export function useStacGenericFilter({ currentBBox, useWorldBBox, setUseWorldBBox, - fetchUsingLink, } = useStacSearch({ model, setResults, @@ -247,10 +240,6 @@ export function useStacGenericFilter({ await executeQuery(queryBody, searchUrl); }, [model, executeQuery, buildCopernicusQuery, baseUrl]); - // Register fetchUsingLink from useStacSearch with context so handlers can use it - useEffect(() => { - registerFetchUsingLink(fetchUsingLink); - }, [fetchUsingLink, registerFetchUsingLink]); return { queryableProps, diff --git a/packages/base/src/stacBrowser/hooks/useStacSearch.ts b/packages/base/src/stacBrowser/hooks/useStacSearch.ts index a0dc25174..15dd8936c 100644 --- a/packages/base/src/stacBrowser/hooks/useStacSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useStacSearch.ts @@ -34,9 +34,6 @@ interface IUseStacSearchReturn { apiUrl: string, method?: string, //! not 100% sure I want this ) => Promise; - fetchUsingLink: ( - link: IStacLink & { method?: string; body?: Record }, - ) => Promise; } /** @@ -179,109 +176,6 @@ export function useStacSearch({ [model, setResults, setPaginationLinks], ); - // Fetch using pagination link - const fetchUsingLink = useCallback( - async ( - link: IStacLink & { method?: string; body?: Record }, - ) => { - if (!model) { - return; - } - - const XSRF_TOKEN = document.cookie.match(/_xsrf=([^;]+)/)?.[1]; - - const options = { - method: (link.method || 'POST').toUpperCase(), - headers: { - 'Content-Type': 'application/json', - 'X-XSRFToken': XSRF_TOKEN, - credentials: 'include', - }, - body: link.body ? JSON.stringify(link.body) : undefined, - }; - - try { - // Update context with loading state - setResults([], true, 0, 0); - - const data = (await fetchWithProxies( - link.href, - model, - async response => await response.json(), - //@ts-expect-error Jupyter requires X-XSRFToken header - options, - 'internal', - )) as IStacSearchResult; - - if (!data) { - setResults([], false, 0, 0); - return; - } - - // Filter assets to only include items with 'overview' or 'thumbnail' roles - if (data.features && data.features.length > 0) { - data.features.forEach(feature => { - if (feature.assets) { - const originalAssets = feature.assets; - const filteredAssets: Record = {}; - - for (const [key, asset] of Object.entries(originalAssets)) { - if ( - asset && - typeof asset === 'object' && - 'roles' in asset && - Array.isArray(asset.roles) - ) { - const roles = asset.roles; - - if ( - roles.includes('thumbnail') || - roles.includes('overview') - ) { - filteredAssets[key] = asset; - } - } - } - - feature.assets = filteredAssets; - } - }); - } - - // Sort features by id before setting results - const sortedFeatures = [...data.features].sort((a, b) => - a.id.localeCompare(b.id), - ); - - // Calculate total results from context if available - let totalResults = data.features.length; - let totalPages = 0; - if (data.context) { - totalResults = data.context.matched; - totalPages = Math.ceil(data.context.matched / data.context.limit); - } else if (sortedFeatures.length > 0) { - // If results found but no context, assume 1 page - totalPages = 1; - } - - // Update context with results - setResults(sortedFeatures, false, totalResults, totalPages); - - // Store pagination links - if (data.links) { - const typedLinks = data.links as Array< - IStacLink & { method?: string; body?: Record } - >; - setPaginationLinks(typedLinks); - } - } catch (error) { - setResults([], false, 0, 0); - } - }, - [model, setResults, setPaginationLinks], - ); - - return { startTime, setStartTime, @@ -292,6 +186,5 @@ export function useStacSearch({ useWorldBBox, setUseWorldBBox, executeQuery, - fetchUsingLink, }; } From df62591a92f3f279f281969091ccb4356b0ca977 Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 10 Dec 2025 11:54:20 +0100 Subject: [PATCH 38/70] Lint --- .../components/StacGenericFilterPanel.tsx | 8 +- .../src/stacBrowser/components/StacPanel.tsx | 4 +- .../components/StacPanelResults.tsx | 16 +-- .../geodes/StacGeodesFilterPanel.tsx | 7 +- .../context/StacResultsContext.tsx | 7 +- .../src/stacBrowser/hooks/useGeodesSearch.ts | 128 ++++++++++-------- .../stacBrowser/hooks/useStacGenericFilter.ts | 6 +- 7 files changed, 89 insertions(+), 87 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx index 99a3adf09..f774011d8 100644 --- a/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacGenericFilterPanel.tsx @@ -16,11 +16,8 @@ type FilteredCollection = Pick; // This is a generic UI for apis that support filter extension function StacGenericFilterPanel({ model }: IStacBrowser2Props) { - const { - setResults, - setPaginationLinks, - selectedUrl, - } = useStacResultsContext(); + const { setResults, setPaginationLinks, selectedUrl } = + useStacResultsContext(); const [limit, setLimit] = useState(12); const { @@ -46,7 +43,6 @@ function StacGenericFilterPanel({ model }: IStacBrowser2Props) { setPaginationLinks, }); - if (!model) { console.log('no model'); return; diff --git a/packages/base/src/stacBrowser/components/StacPanel.tsx b/packages/base/src/stacBrowser/components/StacPanel.tsx index 8aae88ffb..7950b40f8 100644 --- a/packages/base/src/stacBrowser/components/StacPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacPanel.tsx @@ -63,9 +63,7 @@ const StacPanelContent = ({ model }: IStacViewProps) => { Copernicus - +
diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index d403b5a72..b18aa44d0 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -59,7 +59,7 @@ const StacPanelResults = () => { useEffect(() => { console.log('current page in results', currentPage); - console.log('totalPages', totalPages) + console.log('totalPages', totalPages); }, [currentPage, totalPages]); const isNext = paginationLinks.some(link => link.rel === 'next'); @@ -73,12 +73,10 @@ const StacPanelResults = () => { { - setCurrentPage(Math.max(currentPage - 1, 1)); - handlePaginationClick('previous'); - } - } + onClick={() => { + setCurrentPage(Math.max(currentPage - 1, 1)); + handlePaginationClick('previous'); + }} disabled={!isPrev} /> @@ -130,9 +128,7 @@ const StacPanelResults = () => {
-
+
{isLoading ? ( // TODO: Fancy spinner
Loading results...
diff --git a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx index acac5f856..8cb8ce48e 100644 --- a/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx +++ b/packages/base/src/stacBrowser/components/geodes/StacGeodesFilterPanel.tsx @@ -25,11 +25,8 @@ interface IStacGeodesFilterPanelProps { } const StacGeodesFilterPanel = ({ model }: IStacGeodesFilterPanelProps) => { - const { - setResults, - setPaginationLinks, - selectedUrl, - } = useStacResultsContext(); + const { setResults, setPaginationLinks, selectedUrl } = + useStacResultsContext(); const { filterState, diff --git a/packages/base/src/stacBrowser/context/StacResultsContext.tsx b/packages/base/src/stacBrowser/context/StacResultsContext.tsx index 4db6b8906..8b71ec02a 100644 --- a/packages/base/src/stacBrowser/context/StacResultsContext.tsx +++ b/packages/base/src/stacBrowser/context/StacResultsContext.tsx @@ -315,15 +315,10 @@ export function StacResultsProvider({ return ['prev', 'previous'].includes(l.rel); }); - // ! this is nice, if no body then link href should have search params - update eventually if (link && link.body) { // Use executeQuery with the link's body, href, and method - await executeQuery( - link.body as IStacQueryBody, - link.href, - link.method, - ); + await executeQuery(link.body as IStacQueryBody, link.href, link.method); } }, [model, paginationLinks], diff --git a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts index 758385989..3f3d6e72c 100644 --- a/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts +++ b/packages/base/src/stacBrowser/hooks/useGeodesSearch.ts @@ -68,7 +68,6 @@ function useGeodesSearch({ useEffect(() => { console.log('current page', currentPage); console.log('current page ref i think this one ', currentPageRef.current); - }, [currentPage]); useEffect(() => { @@ -97,8 +96,6 @@ function useGeodesSearch({ products: new Set(), }); - - const filterSetters: StacFilterSetters = { collections: val => setFilterState(s => ({ ...s, collections: new Set(val) })), @@ -143,52 +140,55 @@ function useGeodesSearch({ * Builds GEODES-specific query * @param page - Page number for pagination (defaults to currentPageRef.current) */ - const buildGeodesQuery = useCallback((page?: number): IStacQueryBody => { - const pageToUse = page ?? currentPageRef.current; - const processingLevel = new Set(); - const productType = new Set(); - - filterState.products.forEach(productCode => { - products - .filter(product => product.productCode === productCode) - .forEach(product => { - if (product.processingLevel) { - processingLevel.add(product.processingLevel); - } - if (product.productType) { - product.productType.forEach(type => productType.add(type)); - } - }); - }); + const buildGeodesQuery = useCallback( + (page?: number): IStacQueryBody => { + const pageToUse = page ?? currentPageRef.current; + const processingLevel = new Set(); + const productType = new Set(); + + filterState.products.forEach(productCode => { + products + .filter(product => product.productCode === productCode) + .forEach(product => { + if (product.processingLevel) { + processingLevel.add(product.processingLevel); + } + if (product.productType) { + product.productType.forEach(type => productType.add(type)); + } + }); + }); - return { - bbox: currentBBox, - limit: 12, - page: pageToUse, - query: { - latest: { eq: true }, - dataset: { in: Array.from(filterState.datasets) }, - end_datetime: { - gte: startTime - ? startTime.toISOString() - : startOfYesterday().toISOString(), + return { + bbox: currentBBox, + limit: 12, + page: pageToUse, + query: { + latest: { eq: true }, + dataset: { in: Array.from(filterState.datasets) }, + end_datetime: { + gte: startTime + ? startTime.toISOString() + : startOfYesterday().toISOString(), + }, + ...(endTime && { + start_datetime: { lte: endTime.toISOString() }, + }), + ...(filterState.platforms.size > 0 && { + platform: { in: Array.from(filterState.platforms) }, + }), + ...(processingLevel.size > 0 && { + 'processing:level': { in: Array.from(processingLevel) }, + }), + ...(productType.size > 0 && { + 'product:type': { in: Array.from(productType) }, + }), }, - ...(endTime && { - start_datetime: { lte: endTime.toISOString() }, - }), - ...(filterState.platforms.size > 0 && { - platform: { in: Array.from(filterState.platforms) }, - }), - ...(processingLevel.size > 0 && { - 'processing:level': { in: Array.from(processingLevel) }, - }), - ...(productType.size > 0 && { - 'product:type': { in: Array.from(productType) }, - }), - }, - sortBy: [{ direction: 'desc', field: 'start_datetime' }], - }; - }, [filterState, currentBBox, startTime, endTime, currentPageRef]); + sortBy: [{ direction: 'desc', field: 'start_datetime' }], + }; + }, + [filterState, currentBBox, startTime, endTime, currentPageRef], + ); // Register buildQuery with context - always use currentPageRef for latest page useEffect(() => { @@ -204,7 +204,9 @@ function useGeodesSearch({ } // ! someosmeosmesse urlTOUse - const urlToUse = selectedUrl.endsWith('/') ? `${selectedUrl}search` : `${selectedUrl}/search`; + const urlToUse = selectedUrl.endsWith('/') + ? `${selectedUrl}search` + : `${selectedUrl}/search`; // Build query body and execute query const queryBody = buildGeodesQuery(); await executeQuery(queryBody, urlToUse); @@ -225,7 +227,6 @@ function useGeodesSearch({ handleSubmit, ]); - /** * Handles pagination clicks for GEODES * Updates currentPage and executes query with new page number @@ -237,23 +238,42 @@ function useGeodesSearch({ return; } - console.log('geodes page click', dir, 'currentPage:', currentPage, 'currentPageRef.current:', currentPageRef.current); + console.log( + 'geodes page click', + dir, + 'currentPage:', + currentPage, + 'currentPageRef.current:', + currentPageRef.current, + ); // Calculate new page number - const newPage = dir === 'next' ? currentPageRef.current + 1 : currentPageRef.current - 1; + const newPage = + dir === 'next' + ? currentPageRef.current + 1 + : currentPageRef.current - 1; // Update currentPage in context setCurrentPage(newPage); - const urlToUse = selectedUrl.endsWith('/') ? `${selectedUrl}search` : `${selectedUrl}/search`; + const urlToUse = selectedUrl.endsWith('/') + ? `${selectedUrl}search` + : `${selectedUrl}/search`; // Build query body with new page and execute query const queryBody = buildGeodesQuery(newPage); await executeQuery(queryBody, urlToUse); }, - [model, executeQuery, setCurrentPage, currentPage, currentPageRef, buildGeodesQuery, selectedUrl], + [ + model, + executeQuery, + setCurrentPage, + currentPage, + currentPageRef, + buildGeodesQuery, + selectedUrl, + ], ); - // Register handlePaginationClick with context useEffect(() => { registerHandlePaginationClick(handlePaginationClick); diff --git a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts index 0c900303c..81447358a 100644 --- a/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts +++ b/packages/base/src/stacBrowser/hooks/useStacGenericFilter.ts @@ -65,7 +65,8 @@ export function useStacGenericFilter({ setPaginationLinks, }); - const { registerBuildQuery, executeQuery, selectedUrl } = useStacResultsContext(); + const { registerBuildQuery, executeQuery, selectedUrl } = + useStacResultsContext(); const [queryableProps, setQueryableProps] = useState<[string, any][]>(); const [collections, setCollections] = useState([]); @@ -139,7 +140,7 @@ export function useStacGenericFilter({ } const fatch = async () => { - console.log('hittin dem queries boiiii') + console.log('hittin dem queries boiiii'); const queryablesUrl = baseUrl.endsWith('/') ? `${baseUrl}queryables` : `${baseUrl}/queryables`; @@ -240,7 +241,6 @@ export function useStacGenericFilter({ await executeQuery(queryBody, searchUrl); }, [model, executeQuery, buildCopernicusQuery, baseUrl]); - return { queryableProps, collections, From 6c8656064e54b8547f4af69a880aceb1e267b3bf Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 10 Dec 2025 13:47:35 +0100 Subject: [PATCH 39/70] Pagination stuff --- .../src/stacBrowser/components/StacPanel.tsx | 2 +- .../components/StacPanelResults.tsx | 84 +++++++------ .../context/StacResultsContext.tsx | 16 +-- .../src/stacBrowser/hooks/useStacSearch.ts | 117 +----------------- packages/base/style/stacBrowser.css | 2 + 5 files changed, 56 insertions(+), 165 deletions(-) diff --git a/packages/base/src/stacBrowser/components/StacPanel.tsx b/packages/base/src/stacBrowser/components/StacPanel.tsx index 7950b40f8..b84f509f4 100644 --- a/packages/base/src/stacBrowser/components/StacPanel.tsx +++ b/packages/base/src/stacBrowser/components/StacPanel.tsx @@ -42,7 +42,7 @@ const StacPanelContent = ({ model }: IStacViewProps) => { URL_TO_PANEL_MAP[selectedUrl] ?? StacGenericFilterPanel; return ( - + Filters diff --git a/packages/base/src/stacBrowser/components/StacPanelResults.tsx b/packages/base/src/stacBrowser/components/StacPanelResults.tsx index b18aa44d0..8d259e7d2 100644 --- a/packages/base/src/stacBrowser/components/StacPanelResults.tsx +++ b/packages/base/src/stacBrowser/components/StacPanelResults.tsx @@ -10,6 +10,7 @@ import { PaginationNext, PaginationPrevious, } from '@/src/shared/components/Pagination'; +import { LoadingIcon } from '@/src/shared/components/loading'; import { useStacResultsContext } from '@/src/stacBrowser/context/StacResultsContext'; import { IStacItem } from '@/src/stacBrowser/types/types'; @@ -41,8 +42,6 @@ function getPageItems( ]; } -// ! tues to do -- refactor this, total pages is based on context, which is an extension -// so everythign here needs to be based on link rels instead const StacPanelResults = () => { const { results, @@ -80,43 +79,49 @@ const StacPanelResults = () => { disabled={!isPrev} /> - {results.length === 0 ? ( -
No Matches Found
- ) : ( - getPageItems(currentPage, totalPages).map(item => { - if (item === 'ellipsis') { - return ( - - - - ); - } + { + totalPages === 1 ? ( + // One page, display current page number and keep active + + {currentPage} + + ) : results.length !== 0 || isLoading ? ( + // Multiple pages, display fancy pagination numbers + <> + {getPageItems(currentPage, totalPages).map(item => { + if (item === 'ellipsis') { + return ( + + + + ); + } - return ( - - { - setCurrentPage(item); - await executeQueryWithPage(item); - }} - disabled={totalPages === 1} - > - {item} - - - ); - }) - - // - // handlePaginationClick('next')} - // > - // {currentPage} - // - // - )} + return ( + + { + setCurrentPage(item); + await executeQueryWithPage(item); + }} + disabled={totalPages === 1} + > + {item} + + + ); + })} + + ) : ( + // No results + + + 0 + + + ) + } { @@ -130,8 +135,7 @@ const StacPanelResults = () => {
{isLoading ? ( - // TODO: Fancy spinner -
Loading results...
+ ) : ( results.map(result => (
+ {/* collections */}
- {/* items IDs */} - {/* additional filters - this is where queryables should end up */} + + {/* Queryable filters */} {queryableProps && (
)} {/* sort */} + + {/* ! do i really want this? */} {/* items per page */}