Skip to content

Commit

Permalink
feat: add download functionality
Browse files Browse the repository at this point in the history
Ref #186
  • Loading branch information
stdavis committed Nov 29, 2024
1 parent 585d5a5 commit 69745d8
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 61 deletions.
98 changes: 98 additions & 0 deletions src/components/Download.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { submitJob } from '@arcgis/core/rest/geoprocessor';
import { useQueryClient } from '@tanstack/react-query';
import { Button, Select, SelectItem, Spinner } from '@ugrc/utah-design-system';
import { useState } from 'react';
import config from '../config';

async function download(eventIds: string[], format: string): Promise<string> {
const jobInfo = await submitJob(config.urls.download, {
Event_Ids: eventIds.join(';'),
Format: format,
});

await jobInfo.waitForJobCompletion();

const parameter = await jobInfo.fetchResultData('Zip_File');
const value = parameter.value as __esri.DataFile;

return value.url.replace('http', 'https');
}

type State = {
format: string;
error: string | null;
isBusy: boolean;
};

export default function Download({ eventIds }: { eventIds: string[] }): JSX.Element {
const queryClient = useQueryClient();
const [state, setState] = useState<State>({
format: '',
error: null,
isBusy: false,
});

const updateState = (newState: Partial<State>) => setState((prev) => ({ ...prev, ...newState }));

const onDownloadClick = async () => {
if (eventIds.length === 0) {
console.warn('No data to download');

return;
}

updateState({ isBusy: true, error: null });

let url;
try {
url = await queryClient.fetchQuery({
queryKey: ['download', eventIds],
queryFn: () => download(eventIds as string[], state.format),
});
} catch (error) {
console.error('Error downloading data', error);
updateState({ error: 'There was an error with the download service', isBusy: false });

return;
}

const link = document.createElement('a');
link.href = url;
link.download = 'data.zip';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);

updateState({ isBusy: false });
};

return (
<div className="p-4">
<p>This can take a few minutes to process.</p>
<Select
placeholder="select format"
selectedKey={state.format}
onSelectionChange={(key) => updateState({ format: key as string })}
aria-label="select format"
className="inline-flex py-2"
>
<SelectItem id="csv" aria-label="CSV">
CSV
</SelectItem>
<SelectItem id="fgdb" aria-label="File Geodatabase">
File Geodatabase
</SelectItem>
</Select>
<p>
<Button
isDisabled={!eventIds.length || state.isBusy || state.format === ''}
variant="secondary"
onPress={onDownloadClick}
>
{state.isBusy ? <Spinner /> : 'Download'}
</Button>
</p>
{state.error && <div className="text-sm text-rose-600 forced-colors:text-[Mark]">{state.error}</div>}
</div>
);
}
136 changes: 76 additions & 60 deletions src/components/ResultsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { useQuery } from '@tanstack/react-query';
import { Spinner, useFirebaseAuth } from '@ugrc/utah-design-system';
import { User } from 'firebase/auth';
import ky from 'ky';
import { TableBody } from 'react-aria-components';
import { Tab, TableBody, TabList, TabPanel, Tabs } from 'react-aria-components';
import config from '../config';
import { useFilter } from './contexts/FilterProvider';
import Download from './Download';
import { getGridQuery, removeIrrelevantWhiteSpace } from './queryHelpers';
import { Cell, Column, Row, Table, TableHeader } from './Table';

Expand Down Expand Up @@ -125,67 +126,82 @@ export default function ResultsGrid() {
return <span>{error.message}</span>;
}

const eventIds = data?.length ? (data.map((row) => row[config.fieldNames.EVENT_ID]) as string[]) : ([] as string[]);

return (
<>
<div className="px-2 py-2">Results: {data?.length}</div>
<Table aria-label="query results" className="-z-10 w-full border-t dark:border-t-zinc-300">
<TableHeader>
<Column id={config.fieldNames.EVENT_DATE} minWidth={120}>
Event Date
</Column>
<Column id={config.fieldNames.OBSERVERS} minWidth={120}>
Observers
</Column>
<Column id={`${config.fieldNames.WaterName}_Stream`} minWidth={170}>
Stream
</Column>
<Column id={`${config.fieldNames.DWR_WaterID}_Stream`} minWidth={120}>
Stream ID
</Column>
<Column id={`${config.fieldNames.ReachCode}_Stream`} minWidth={200}>
Stream Reach Code
</Column>
<Column id={`${config.fieldNames.WaterName}_Lake`} minWidth={150}>
Lake
</Column>
<Column id={`${config.fieldNames.DWR_WaterID}_Lake`} minWidth={120}>
Lake ID
</Column>
<Column id={`${config.fieldNames.ReachCode}_Lake`} minWidth={200}>
Lake Reach Code
</Column>
<Column id={STATION_NAME} minWidth={180}>
Station Name
</Column>
<Column id={config.fieldNames.SPECIES} minWidth={180}>
Species Codes
</Column>
<Column id={config.fieldNames.TYPES} minWidth={150}>
Equipment
</Column>
<Column id={config.fieldNames.EVENT_ID} isRowHeader minWidth={350}>
Event ID
</Column>
</TableHeader>
<TableBody items={data}>
{(row) => (
<Row>
<Cell>{new Date(row[config.fieldNames.EVENT_DATE] as number).toLocaleDateString()}</Cell>
<Cell>{row[config.fieldNames.OBSERVERS]}</Cell>
<Cell>{row[`${config.fieldNames.WaterName}_Stream`]}</Cell>
<Cell>{row[`${config.fieldNames.DWR_WaterID}_Stream`]}</Cell>
<Cell>{row[`${config.fieldNames.ReachCode}_Stream`]}</Cell>
<Cell>{row[`${config.fieldNames.WaterName}_Lake`]}</Cell>
<Cell>{row[`${config.fieldNames.DWR_WaterID}_Lake`]}</Cell>
<Cell>{row[`${config.fieldNames.ReachCode}_Lake`]}</Cell>
<Cell>{row[STATION_NAME]}</Cell>
<Cell>{row[config.fieldNames.SPECIES]}</Cell>
<Cell>{row[config.fieldNames.TYPES]}</Cell>
<Cell>{row[config.fieldNames.EVENT_ID]}</Cell>
</Row>
)}
</TableBody>
</Table>
<span className="absolute right-12 top-2 z-10 self-center">
Records: <strong>{data?.length}</strong>
</span>
<Tabs aria-label="results panel">
<TabList>
<Tab id="grid">Results</Tab>
<Tab id="download">Download</Tab>
</TabList>
<TabPanel id="grid">
<Table aria-label="query results" className="-z-10 w-full border-t dark:border-t-zinc-300">
<TableHeader>
<Column id={config.fieldNames.EVENT_DATE} minWidth={120}>
Event Date
</Column>
<Column id={config.fieldNames.OBSERVERS} minWidth={120}>
Observers
</Column>
<Column id={`${config.fieldNames.WaterName}_Stream`} minWidth={170}>
Stream
</Column>
<Column id={`${config.fieldNames.DWR_WaterID}_Stream`} minWidth={120}>
Stream ID
</Column>
<Column id={`${config.fieldNames.ReachCode}_Stream`} minWidth={200}>
Stream Reach Code
</Column>
<Column id={`${config.fieldNames.WaterName}_Lake`} minWidth={150}>
Lake
</Column>
<Column id={`${config.fieldNames.DWR_WaterID}_Lake`} minWidth={120}>
Lake ID
</Column>
<Column id={`${config.fieldNames.ReachCode}_Lake`} minWidth={200}>
Lake Reach Code
</Column>
<Column id={STATION_NAME} minWidth={180}>
Station Name
</Column>
<Column id={config.fieldNames.SPECIES} minWidth={180}>
Species Codes
</Column>
<Column id={config.fieldNames.TYPES} minWidth={150}>
Equipment
</Column>
<Column id={config.fieldNames.EVENT_ID} isRowHeader minWidth={350}>
Event ID
</Column>
</TableHeader>
<TableBody items={data}>
{(row) => (
<Row>
<Cell>{new Date(row[config.fieldNames.EVENT_DATE] as number).toLocaleDateString()}</Cell>
<Cell>{row[config.fieldNames.OBSERVERS]}</Cell>
<Cell>{row[`${config.fieldNames.WaterName}_Stream`]}</Cell>
<Cell>{row[`${config.fieldNames.DWR_WaterID}_Stream`]}</Cell>
<Cell>{row[`${config.fieldNames.ReachCode}_Stream`]}</Cell>
<Cell>{row[`${config.fieldNames.WaterName}_Lake`]}</Cell>
<Cell>{row[`${config.fieldNames.DWR_WaterID}_Lake`]}</Cell>
<Cell>{row[`${config.fieldNames.ReachCode}_Lake`]}</Cell>
<Cell>{row[STATION_NAME]}</Cell>
<Cell>{row[config.fieldNames.SPECIES]}</Cell>
<Cell>{row[config.fieldNames.TYPES]}</Cell>
<Cell>{row[config.fieldNames.EVENT_ID]}</Cell>
</Row>
)}
</TableBody>
</Table>
</TabPanel>
<TabPanel id="download">
<Download eventIds={eventIds} />
</TabPanel>
</Tabs>
</>
);
}
2 changes: 1 addition & 1 deletion src/components/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { composeTailwindRenderProps, focusRing } from './utils.ts';

export function Table(props: TableProps) {
return (
<ResizableTableContainer className="relative h-full w-full overflow-auto">
<ResizableTableContainer className="relative overflow-auto">
<AriaTable {...props} />
</ResizableTableContainer>
);
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const config = {
'https://gis.trustlands.utah.gov/hosting/rest/services/Hosted/Land_Ownership_WM_VectorTile/VectorTileServer',
streams: `${referenceMapService}/0`,
lakes: `${referenceMapService}/1`,
download: `${functionsUrl}/toolbox/Download`,
},
};

Expand Down

0 comments on commit 69745d8

Please sign in to comment.