Skip to content

Commit 99f7af8

Browse files
committed
fix: download search results endpoint
1 parent e0ecd38 commit 99f7af8

1 file changed

Lines changed: 68 additions & 31 deletions

File tree

components/search-page-body.tsx

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { track } from "@/utils/analytics";
1212
import { withTimeout } from "@/utils/api";
1313
import { SERVER_URL } from "@/utils/constants";
1414
import { DB_LABELS, SEARCH_DBS, type SearchDb } from "@/utils/db-colors";
15+
import { downloadCsv } from "@/utils/exportCsv";
1516
import { getProjectShortUrl } from "@/utils/shortUrl";
1617
import { SearchResult } from "@/utils/types";
1718
import {
@@ -246,6 +247,36 @@ function appendFilterParams(url: string, f: SearchFilterParams): string {
246247
return url;
247248
}
248249

250+
// Dataset-level download columns. Must stay in step with _DOWNLOAD_COLUMNS in the
251+
// API (main.py): the text path gets this CSV from the server and the geo path
252+
// builds it in the browser, and the two should be the same file.
253+
const DOWNLOAD_COLUMNS = [
254+
"accession",
255+
"source",
256+
"title",
257+
"summary",
258+
"updated_at",
259+
"organisms",
260+
"countries",
261+
"instrument_models",
262+
"library_strategies",
263+
];
264+
265+
function resultToCsvRow(r: SearchResult): Record<string, unknown> {
266+
const join = (v: string[] | null | undefined) => (v ?? []).join("; ");
267+
return {
268+
accession: r.accession,
269+
source: r.source,
270+
title: r.title,
271+
summary: r.summary,
272+
updated_at: r.updated_at,
273+
organisms: join(r.organisms),
274+
countries: join(r.countries),
275+
instrument_models: join(r.instrument_models),
276+
library_strategies: join(r.library_strategies),
277+
};
278+
}
279+
249280
function buildSearchUrl(
250281
query: string,
251282
db: string | null,
@@ -1884,52 +1915,58 @@ export default function SearchPageBody() {
18841915
}, []);
18851916

18861917
const handleDownloadResults = async () => {
1887-
if (isDownloading || !query) return;
1918+
if (isDownloading) return;
1919+
if (!isGeoSearch && !query) return;
18881920

18891921
setIsDownloading(true);
18901922
setDownloadFailed(false);
18911923

1924+
const timestamp = new Date()
1925+
.toISOString()
1926+
.replace(/[-:]/g, "")
1927+
.replace(/\..+/, "")
1928+
.replace("T", "_");
1929+
18921930
try {
1893-
const params = new URLSearchParams();
1894-
params.set("q", query);
1895-
if (db && (SEARCH_DBS as readonly string[]).includes(db)) {
1896-
params.set("db", db);
1931+
// Geo/map search paginates client-side and eagerly prefetches every page,
1932+
// so the browser already holds the whole result set (filters included) —
1933+
// no server round trip needed, and no /search/structured download endpoint.
1934+
if (isGeoSearch) {
1935+
downloadCsv(
1936+
filteredResults.map((r) => resultToCsvRow(r)),
1937+
DOWNLOAD_COLUMNS,
1938+
`seqout_results_${timestamp}.csv`,
1939+
);
1940+
return;
18971941
}
18981942

1899-
if (timeFilter === "custom") {
1900-
const from = parseInt(customYearRange.from);
1901-
const to = parseInt(customYearRange.to);
1902-
if (from) params.set("updated_year_from", String(from));
1903-
if (to) params.set("updated_year_to", String(to));
1904-
} else if (timeFilter !== "any") {
1905-
const years = parseInt(timeFilter);
1906-
const currentYear = new Date().getFullYear();
1907-
params.set("updated_year_from", String(currentYear - years));
1908-
params.set("updated_year_to", String(currentYear));
1943+
// Text search: the server streams every match. Send the same db + sidebar
1944+
// filters the results list was fetched with, so the CSV is the search, not
1945+
// the page. Year range rides along inside searchFilters.
1946+
let url = `${SERVER_URL}/download/query?q=${encodeURIComponent(
1947+
// The displayed results are the corrected query's when a typo was
1948+
// auto-corrected; download what is on screen, not the typo.
1949+
correction?.corrected_query ?? query!,
1950+
)}`;
1951+
if (db && (SEARCH_DBS as readonly string[]).includes(db)) {
1952+
url += `&db=${encodeURIComponent(db)}`;
19091953
}
1954+
url = appendFilterParams(url, searchFilters);
19101955

1911-
const res = await fetch(
1912-
`${SERVER_URL}/download/query?${params.toString()}`,
1913-
);
1914-
1956+
const res = await fetch(url);
19151957
if (!res.ok) {
19161958
throw new Error("Download failed");
19171959
}
19181960

1919-
const zipBlob = await res.blob();
1920-
const url = URL.createObjectURL(zipBlob);
1961+
const csvBlob = await res.blob();
1962+
const objectUrl = URL.createObjectURL(csvBlob);
19211963
const a = document.createElement("a");
1922-
a.href = url;
1923-
const timestamp = new Date()
1924-
.toISOString()
1925-
.replace(/[-:]/g, "")
1926-
.replace(/\..+/, "")
1927-
.replace("T", "_");
1928-
a.download = `results_${timestamp}.zip`;
1964+
a.href = objectUrl;
1965+
a.download = `seqout_results_${timestamp}.csv`;
19291966
document.body.appendChild(a);
19301967
a.click();
19311968
a.remove();
1932-
URL.revokeObjectURL(url);
1969+
URL.revokeObjectURL(objectUrl);
19331970
} catch (error) {
19341971
console.error(error);
19351972
setDownloadFailed(true);
@@ -2370,7 +2407,7 @@ export default function SearchPageBody() {
23702407
content={
23712408
downloadFailed
23722409
? "Download failed. Please try again."
2373-
: "Download search results as ZIP"
2410+
: "Download every matching dataset as CSV (one row per dataset)"
23742411
}
23752412
>
23762413
<Button
@@ -2379,7 +2416,7 @@ export default function SearchPageBody() {
23792416
aria-busy={isDownloading}
23802417
>
23812418
{isDownloading ? <Spinner /> : <DownloadIcon />}
2382-
{isDownloading ? "Preparing ZIP..." : "Download results"}
2419+
{isDownloading ? "Preparing CSV..." : "Download results"}
23832420
</Button>
23842421
</Tooltip>
23852422
)}

0 commit comments

Comments
 (0)