diff --git a/apps/trading/components/ledger-container/ledger-container.tsx b/apps/trading/components/ledger-container/ledger-container.tsx index 36bddc043..5d9f62fde 100644 --- a/apps/trading/components/ledger-container/ledger-container.tsx +++ b/apps/trading/components/ledger-container/ledger-container.tsx @@ -15,15 +15,12 @@ export const LedgerContainer = () => { }); const assets = (data?.party?.accountsConnection?.edges ?? []) - .map( - (item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment) - ) - .reduce((aggr, item) => { - if ('id' in item && 'symbol' in item) { - aggr[item.id as string] = item.symbol as string; - } - return aggr; - }, {} as Record); + .map((item) => item?.node?.asset) + .filter((asset): asset is PartyAssetFieldsFragment => !!asset?.id) + .reduce( + (aggr, item) => Object.assign(aggr, { [item.id]: item.symbol }), + {} as Record + ); if (!pubKey) { return ( diff --git a/libs/ledger/src/lib/ledger-export-form.spec.tsx b/libs/ledger/src/lib/ledger-export-form.spec.tsx index 755b3eade..dedccc6db 100644 --- a/libs/ledger/src/lib/ledger-export-form.spec.tsx +++ b/libs/ledger/src/lib/ledger-export-form.spec.tsx @@ -278,18 +278,4 @@ describe('createDownloadUrl', () => { )}&dateRange.endTimestamp=${toNanoSeconds(dateTo)}` ); }); - - it('should throw if invalid args are provided', () => { - // invalid url - expect(() => { - // @ts-ignore override z.infer type - createDownloadUrl({ ...args, protohost: 'foo' }); - }).toThrow(); - - // invalid partyId - expect(() => { - // @ts-ignore override z.infer type - createDownloadUrl({ ...args, partyId: 'z'.repeat(64) }); - }).toThrow(); - }); }); diff --git a/libs/ledger/src/lib/ledger-export-form.tsx b/libs/ledger/src/lib/ledger-export-form.tsx index 135a63f38..cb585370d 100644 --- a/libs/ledger/src/lib/ledger-export-form.tsx +++ b/libs/ledger/src/lib/ledger-export-form.tsx @@ -1,18 +1,18 @@ -import { useRef, useState } from 'react'; -import { format, subDays } from 'date-fns'; +import { useRef, useCallback } from 'react'; +import { subDays } from 'date-fns'; +import { Controller, useForm } from 'react-hook-form'; import { + InputError, Intent, - Loader, TradingButton, TradingFormGroup, TradingInput, TradingSelect, } from '@vegaprotocol/ui-toolkit'; -import { z } from 'zod'; import { formatForInput, + getDateTimeFormat, toNanoSeconds, - VEGA_ID_REGEX, } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { localLoggerFactory } from '@vegaprotocol/logger'; @@ -35,18 +35,18 @@ const getProtoHost = (vegaurl: string) => { return `${loc.protocol}//${loc.host}`; }; -const downloadSchema = z.object({ - protohost: z.string().url().nonempty(), - partyId: z.string().regex(VEGA_ID_REGEX).nonempty(), - assetId: z.string().regex(VEGA_ID_REGEX).nonempty(), - dateFrom: z.string().nonempty(), - dateTo: z.string().optional(), -}); - -export const createDownloadUrl = (args: z.infer) => { - // check args from form inputs - downloadSchema.parse(args); +type LedgerFormValues = { + assetId: string; + dateFrom: string; + dateTo?: string; +}; +export const createDownloadUrl = ( + args: LedgerFormValues & { + partyId: string; + protohost: string; + } +) => { const params = new URLSearchParams(); params.append('partyId', args.partyId); params.append('assetId', args.assetId); @@ -71,114 +71,101 @@ interface Props { export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => { const now = useRef(new Date()); - const [dateFrom, setDateFrom] = useState(() => { - return formatForInput(subDays(now.current, 7)); + const { control, handleSubmit, watch } = useForm({ + defaultValues: { + dateFrom: formatForInput(subDays(now.current, 7)), + dateTo: '', + assetId: Object.keys(assets)[0], + }, }); - const [dateTo, setDateTo] = useState(''); + const dateTo = watch('dateTo'); const maxFromDate = formatForInput(new Date(dateTo || now.current)); const maxToDate = formatForInput(now.current); - - const [assetId, setAssetId] = useState(Object.keys(assets)[0]); const protohost = getProtoHost(vegaUrl); - const disabled = Boolean(!assetId); const hasItem = useLedgerDownloadFile((store) => store.hasItem); const updateDownloadQueue = useLedgerDownloadFile( (store) => store.updateQueue ); - const assetDropDown = ( - { - setAssetId(e.target.value); - }} - className="w-full" - data-testid="select-ledger-asset" - > - {Object.keys(assets).map((assetKey) => ( - - ))} - - ); + const startDownload = useCallback( + async (formValues: LedgerFormValues) => { + const link = createDownloadUrl({ + protohost, + partyId, + ...formValues, + }); - const link = createDownloadUrl({ - protohost, - partyId, - assetId, - dateFrom, - dateTo, - }); + const dateTimeFormatter = getDateTimeFormat(); + const title = t('Downloading for %s from %s till %s', [ + assets[formValues.assetId], + dateTimeFormatter.format(new Date(formValues.dateFrom)), + dateTimeFormatter.format(new Date(formValues.dateTo || Date.now())), + ]); - const startDownload = async (event: React.FormEvent) => { - event.preventDefault(); - - const title = t('Downloading for %s from %s till %s', [ - assets[assetId], - format(new Date(dateFrom), 'dd MMMM yyyy HH:mm'), - format(new Date(dateTo || Date.now()), 'dd MMMM yyyy HH:mm'), - ]); - - const downloadStoreItem = { - title, - link, - isChanged: true, - }; - if (hasItem(link)) { - updateDownloadQueue(downloadStoreItem); - return; - } - const ts = setTimeout(() => { - updateDownloadQueue({ - ...downloadStoreItem, - intent: Intent.Warning, - isDelayed: true, + const downloadStoreItem = { + title, + link, isChanged: true, - }); - }, 1000 * 30); - - try { - updateDownloadQueue(downloadStoreItem); - const resp = await fetch(link); - if (!resp?.ok) { - if (resp?.status === 429) { - throw new Error('Too many requests. Try again later.'); - } - throw new Error('Download of ledger entries failed'); + }; + if (hasItem(link)) { + updateDownloadQueue(downloadStoreItem); + return; } - const { headers } = resp; - const nameHeader = headers.get('content-disposition'); - const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME; - updateDownloadQueue({ - ...downloadStoreItem, - filename, - }); - const blob = await resp.blob(); - if (blob) { + const ts = setTimeout(() => { updateDownloadQueue({ ...downloadStoreItem, - blob, - isDownloaded: true, + intent: Intent.Warning, + isDelayed: true, isChanged: true, - intent: Intent.Success, }); + }, 1000 * 30); + + try { + updateDownloadQueue(downloadStoreItem); + const resp = await fetch(link); + if (!resp?.ok) { + if (resp?.status === 429) { + throw new Error('Too many requests. Try again later.'); + } + throw new Error('Download of ledger entries failed'); + } + const { headers } = resp; + const nameHeader = headers.get('content-disposition'); + const filename = + nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME; + updateDownloadQueue({ + ...downloadStoreItem, + filename, + }); + const blob = await resp.blob(); + if (blob) { + updateDownloadQueue({ + ...downloadStoreItem, + blob, + isDownloaded: true, + isChanged: true, + intent: Intent.Success, + }); + } + } catch (err) { + localLoggerFactory({ application: 'ledger' }).error( + 'Download file', + err + ); + updateDownloadQueue({ + ...downloadStoreItem, + intent: Intent.Danger, + isError: true, + isChanged: true, + errorMessage: (err as Error).message || undefined, + }); + } finally { + clearTimeout(ts); } - } catch (err) { - localLoggerFactory({ application: 'ledger' }).error('Download file', err); - updateDownloadQueue({ - ...downloadStoreItem, - intent: Intent.Danger, - isError: true, - isChanged: true, - errorMessage: (err as Error).message || undefined, - }); - } finally { - clearTimeout(ts); - } - }; + }, + [assets, hasItem, partyId, protohost, updateDownloadQueue] + ); if (!protohost || Object.keys(assets).length === 0) { return null; @@ -187,49 +174,117 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => { const offset = new Date().getTimezoneOffset(); return ( -
+

{t('Export ledger entries')}

- - {assetDropDown} - - - setDateFrom(e.target.value)} - max={maxFromDate} - /> - - - setDateTo(e.target.value)} - max={maxToDate} - /> - + ( +
+ + + {Object.keys(assets).map((assetKey) => ( + + ))} + + + {fieldState.error && ( + {fieldState.error.message} + )} +
+ )} + /> + ( +
+ + + + {fieldState.error && ( + {fieldState.error.message} + )} +
+ )} + /> + ( +
+ + + + {fieldState.error && ( + {fieldState.error.message} + )} +
+ )} + />
- + {t('Download')}
- {offset && ( + {offset ? (

{t( 'The downloaded file uses the UTC time zone for all listed times. Your time zone is UTC%s.', [toHoursAndMinutes(offset)] )}

- )} + ) : null} ); };