Skip to content

Commit 5be42fd

Browse files
authored
fix: save per-collection searches (#302)
fix: attach upload button to input fix: don't get SAS for public Planetary Computer assets fix: sign Planetary Computer thumbnails fix: don't break when there's no collections in a catalog
1 parent a26401a commit 5be42fd

File tree

10 files changed

+134
-69
lines changed

10 files changed

+134
-69
lines changed

src/components/footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default function Footer() {
1212
fontWeight={"lighter"}
1313
fontSize={"small"}
1414
>
15-
v{version} | Created with <LuHeart /> by{" "}
15+
v{version} | Crafted with <LuHeart /> by{" "}
1616
<Link href="https://developmentseed.org/">
1717
Development Seed <CollecticonBrandDevelopmentSeed2 />
1818
</Link>

src/components/header.tsx

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,14 @@
1-
import { Button, FileUpload, HStack, IconButton } from "@chakra-ui/react";
2-
import { useDuckDb } from "duckdb-wasm-kit";
3-
import { LuUpload } from "react-icons/lu";
4-
import { useStore } from "../store";
5-
import { uploadFile } from "../utils/upload";
1+
import { Button, HStack } from "@chakra-ui/react";
62
import { Examples } from "./examples";
73
import HrefInput from "./href-input";
84
import { ColorModeButton } from "./ui/color-mode";
95
import { ProjectionButton } from "./ui/projection";
106
import { SettingsButton } from "./ui/settings";
117

128
export default function Header() {
13-
const setUploadedFile = useStore((store) => store.setUploadedFile);
14-
const { db } = useDuckDb();
15-
169
return (
1710
<HStack pointerEvents={"auto"}>
1811
<HrefInput />
19-
<FileUpload.Root
20-
flex={0}
21-
onFileAccept={(details) =>
22-
uploadFile({
23-
file: details.files[0],
24-
setUploadedFile,
25-
db,
26-
})
27-
}
28-
>
29-
<FileUpload.HiddenInput />
30-
<FileUpload.Trigger asChild>
31-
<IconButton bg={"bg.muted/90"} variant={"outline"} disabled={!db}>
32-
<LuUpload />
33-
</IconButton>
34-
</FileUpload.Trigger>
35-
</FileUpload.Root>
3612
<Examples>
3713
<Button bg={"bg.muted/90"} variant={"outline"}>
3814
Examples

src/components/href-input.tsx

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
import { Box, Input } from "@chakra-ui/react";
1+
import {
2+
Box,
3+
FileUpload,
4+
IconButton,
5+
Input,
6+
InputGroup,
7+
} from "@chakra-ui/react";
8+
import { useDuckDb } from "duckdb-wasm-kit";
9+
import { LuUpload } from "react-icons/lu";
210
import { useStore } from "../store";
11+
import { uploadFile } from "../utils/upload";
312

413
export default function HrefInput() {
514
const setHref = useStore((state) => state.setHref);
615
const input = useStore((state) => state.input);
716
const setInput = useStore((state) => state.setInput);
17+
const setUploadedFile = useStore((store) => store.setUploadedFile);
18+
const { db } = useDuckDb();
819

920
return (
1021
<Box
@@ -15,12 +26,33 @@ export default function HrefInput() {
1526
}}
1627
flex="1"
1728
>
18-
<Input
19-
bg={"bg.muted/90"}
20-
placeholder="Enter a url to a STAC API, JSON, or GeoParquet"
21-
value={input}
22-
onChange={(e) => setInput(e.target.value)}
23-
></Input>
29+
<InputGroup
30+
endElement={
31+
<FileUpload.Root
32+
onFileAccept={(details) =>
33+
uploadFile({
34+
file: details.files[0],
35+
setUploadedFile,
36+
db,
37+
})
38+
}
39+
>
40+
<FileUpload.HiddenInput />
41+
<FileUpload.Trigger asChild>
42+
<IconButton variant={"plain"} size={"sm"} disabled={!db}>
43+
<LuUpload />
44+
</IconButton>
45+
</FileUpload.Trigger>
46+
</FileUpload.Root>
47+
}
48+
>
49+
<Input
50+
bg={"bg.muted/90"}
51+
placeholder="Enter a url to a STAC API, JSON, or GeoParquet"
52+
value={input}
53+
onChange={(e) => setInput(e.target.value)}
54+
/>
55+
</InputGroup>
2456
</Box>
2557
);
2658
}

src/components/ui/thumbnail.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,45 @@
1+
import { usePlanetaryComputerToken } from "@/hooks/planetary-computer";
2+
import type { AzureBlobStorageContainer } from "@/types/planetary-computer";
3+
import {
4+
parsePlanetaryComputerContainer,
5+
signPlanetaryComputerHref,
6+
} from "@/utils/planetary-computer";
17
import { Center, Image } from "@chakra-ui/react";
8+
import { useMemo } from "react";
29
import type { StacAsset } from "stac-ts";
310

411
export default function Thumbnail({ asset }: { asset: StacAsset }) {
12+
const planetaryComputerContainer = parsePlanetaryComputerContainer(
13+
asset.href
14+
);
15+
if (planetaryComputerContainer)
16+
return (
17+
<PlanetaryComputerThumbnail
18+
href={asset.href}
19+
container={planetaryComputerContainer}
20+
/>
21+
);
22+
else return <HrefThumbnail href={asset.href} />;
23+
}
24+
25+
function PlanetaryComputerThumbnail({
26+
href,
27+
container,
28+
}: {
29+
href: string;
30+
container: AzureBlobStorageContainer;
31+
}) {
32+
const { data: token } = usePlanetaryComputerToken({ container });
33+
const signedHref = useMemo(() => {
34+
if (token) return signPlanetaryComputerHref(href, token);
35+
}, [token, href]);
36+
if (signedHref) return <HrefThumbnail href={signedHref} />;
37+
}
38+
39+
function HrefThumbnail({ href }: { href: string }) {
540
return (
641
<Center>
7-
<Image src={asset.href} maxH={"250px"} />
42+
<Image src={href} maxH={"250px"} />
843
</Center>
944
);
1045
}

src/components/value/search.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Section } from "@/components/section";
22
import { useStacSearch } from "@/hooks/stac";
33
import { useItems } from "@/hooks/store";
44
import { useStore } from "@/store/index.ts";
5+
import { toSearchKey } from "@/store/items";
56
import type { StacSearch } from "@/types/stac";
67
import { paddedBbox } from "@/utils/bbox";
78
import { getCollectionDatetimes } from "@/utils/stac";
@@ -47,23 +48,22 @@ interface SetSearchParams {
4748
}
4849

4950
export default function Search({ href, collection }: Props) {
50-
const search = useStore((store) => store.search);
51-
const setSearch = useStore((store) => store.setSearch);
51+
const searches = useStore((store) => store.searches);
52+
const setSearchState = useStore((store) => store.setSearch);
5253
const setSearchedItems = useStore((store) => store.setSearchedItems);
53-
const result = useStacSearch({ href, search });
5454
const setDatetimeBounds = useStore((store) => store.setDatetimeBounds);
55-
5655
const [fetchAll, setFetchAll] = useState(false);
5756

57+
const searchKey = { href, collection };
58+
const searchKeyString = toSearchKey(searchKey);
59+
const search = searches[searchKeyString] || { collections: [collection.id] };
60+
const setSearch = (s: StacSearch) => setSearchState(searchKey, s);
61+
const result = useStacSearch({ href, search });
62+
5863
const numberMatched = useMemo(() => {
5964
if (result.data) return result.data.pages.at(0)?.numberMatched;
6065
}, [result.data]);
6166

62-
useEffect(() => {
63-
if (search.collections.at(0) !== collection.id)
64-
setSearch({ collections: [collection.id] });
65-
}, [collection, setSearch, search]);
66-
6767
useEffect(() => {
6868
if (result.data)
6969
setSearchedItems(result.data.pages.map((page) => page?.features || []));

src/store/href.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const createHrefSlice: StateCreator<State, [], [], HrefState> = (
2828
pickedItem: null,
2929
staticItems: null,
3030
searchedItems: null,
31+
searches: {},
3132
datetimeBounds: null,
3233
datetimeFilter: null,
3334
stacGeoparquetTable: null,

src/store/items.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import type { StacItem } from "stac-ts";
1+
import type { StacCollection, StacItem } from "stac-ts";
22
import type { StateCreator } from "zustand";
33
import type { State } from ".";
44
import type { StacSearch } from "../types/stac";
55

66
export type ItemSource = "static" | "searched";
77

8+
export type SearchKey = { href: string; collection: StacCollection };
9+
810
export interface ItemsState {
9-
search: StacSearch;
10-
setSearch: (search: StacSearch) => void;
11+
searches: Record<string, StacSearch>;
12+
setSearch: (key: SearchKey, search: StacSearch) => void;
1113
staticItems: StacItem[] | null;
1214
setStaticItems: (items: StacItem[] | null) => void;
1315
addItem: (item: StacItem) => void;
@@ -26,15 +28,22 @@ export interface ItemsState {
2628
setVisualizeItemBounds: (visualize: boolean) => void;
2729
}
2830

31+
export function toSearchKey({ href, collection }: SearchKey): string {
32+
return `${href}:${collection.id}`;
33+
}
34+
2935
export const createItemsSlice: StateCreator<State, [], [], ItemsState> = (
3036
set,
3137
get
3238
) => ({
33-
search: {
34-
collections: [],
35-
},
36-
setSearch: (search) => {
37-
set({ search });
39+
searches: {},
40+
setSearch: (key, search) => {
41+
set({
42+
searches: {
43+
...get().searches,
44+
[toSearchKey(key)]: search,
45+
},
46+
});
3847
},
3948
staticItems: null,
4049
setStaticItems: (items) => {

src/utils/planetary-computer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ export function parsePlanetaryComputerContainer(
99
): AzureBlobStorageContainer | null {
1010
try {
1111
const url = new URL(href);
12-
if (url.host.endsWith("blob.core.windows.net"))
12+
if (
13+
url.host.endsWith("blob.core.windows.net") &&
14+
!url.host.startsWith("ai4edatasetspublicassets")
15+
)
1316
return {
1417
storageAccount: url.hostname.split(".")[0],
1518
container: url.pathname.split("/")[1],

src/utils/stac.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -359,17 +359,19 @@ export function getBbox(
359359
}
360360

361361
function getCollectionsBbox(collections: StacCollection[]) {
362-
return sanitizeBbox(
363-
collections
364-
.map((collection) => getCollectionExtents(collection))
365-
.filter((extents) => !!extents)
366-
.reduce((accumulator, currentValue) => {
367-
return [
368-
Math.min(accumulator[0], currentValue[0]),
369-
Math.min(accumulator[1], currentValue[1]),
370-
Math.max(accumulator[2], currentValue[2]),
371-
Math.max(accumulator[3], currentValue[3]),
372-
];
373-
})
374-
);
362+
if (collections.length > 1)
363+
return sanitizeBbox(
364+
collections
365+
.map((collection) => getCollectionExtents(collection))
366+
.filter((extents) => !!extents)
367+
.reduce((accumulator, currentValue) => {
368+
return [
369+
Math.min(accumulator[0], currentValue[0]),
370+
Math.min(accumulator[1], currentValue[1]),
371+
Math.max(accumulator[2], currentValue[2]),
372+
Math.max(accumulator[3], currentValue[3]),
373+
];
374+
})
375+
);
376+
else return null;
375377
}

tests/utils/planetary-computer.spec.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ describe("parsePlanetaryComputerContainer", () => {
1717
});
1818
});
1919

20+
test("parses public storage to null", () => {
21+
const result = parsePlanetaryComputerContainer(
22+
"https://ai4edatasetspublicassets.blob.core.windows.net/assets/path/to/file.tiff"
23+
);
24+
expect(result).toBeNull();
25+
});
26+
2027
test("returns null for non-Azure URL", () => {
2128
expect(
2229
parsePlanetaryComputerContainer("https://example.com/file.tiff")
@@ -42,7 +49,7 @@ describe("signPlanetaryComputerHref", () => {
4249
test("adds token to URL query string", () => {
4350
const token: PlanetaryComputerToken = {
4451
token: "sv=2019-12-12&st=2021-01-01&se=2021-01-02&sr=c&sp=rl&sig=abc123",
45-
msft_request_id: "request-id",
52+
"msft:expiry": "2026-02-05T12:00:00Z",
4653
};
4754
const result = signPlanetaryComputerHref(
4855
"https://myaccount.blob.core.windows.net/container/file.tiff",
@@ -56,7 +63,7 @@ describe("signPlanetaryComputerHref", () => {
5663
test("replaces existing query string", () => {
5764
const token: PlanetaryComputerToken = {
5865
token: "newsig=xyz",
59-
msft_request_id: "request-id",
66+
"msft:expiry": "2026-02-05T12:00:00Z",
6067
};
6168
const result = signPlanetaryComputerHref(
6269
"https://myaccount.blob.core.windows.net/container/file.tiff?oldsig=abc",
@@ -74,7 +81,7 @@ describe("signPlanetaryComputerHrefFromTokens", () => {
7481
myaccount: {
7582
mycontainer: {
7683
token: "sig=abc123",
77-
msft_request_id: "request-id",
84+
"msft:expiry": "2026-02-05T12:00:00Z",
7885
},
7986
},
8087
};
@@ -101,7 +108,7 @@ describe("signPlanetaryComputerHrefFromTokens", () => {
101108
otheraccount: {
102109
othercontainer: {
103110
token: "sig=abc123",
104-
msft_request_id: "request-id",
111+
"msft:expiry": "2026-02-05T12:00:00Z",
105112
},
106113
},
107114
};

0 commit comments

Comments
 (0)