diff --git a/apps/trading/components/ledger-container/ledger-container.tsx b/apps/trading/components/ledger-container/ledger-container.tsx index 8c98ed8cb..ba66dd5d3 100644 --- a/apps/trading/components/ledger-container/ledger-container.tsx +++ b/apps/trading/components/ledger-container/ledger-container.tsx @@ -16,15 +16,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/i18n/src/locales/en/ledger.json b/libs/i18n/src/locales/en/ledger.json index a0c15ba17..9e692a432 100644 --- a/libs/i18n/src/locales/en/ledger.json +++ b/libs/i18n/src/locales/en/ledger.json @@ -1,6 +1,9 @@ { "Date from": "Date from", + "Date from cannot be greater than date to": "Date from cannot be greater than date to", + "Date from cannot be in the future": "Date from cannot be in the future", "Date to": "Date to", + "Date to cannot be in the future": "Date to cannot be in the future", "Download": "Download", "Download all to .csv file": "Download all to .csv file", "Download has been started": "Download has been started", @@ -13,6 +16,8 @@ "Still in progress": "Still in progress", "The downloaded file uses the UTC time zone for all listed times. Your time zone is UTC{{offset}}.": "The downloaded file uses the UTC time zone for all listed times. Your time zone is UTC{{offset}}.", "Try again later": "Try again later", + "You need to provide a date from": "You need to provide a date from", + "You need to select an asset": "You need to select an asset", "You will be notified here when your file is ready.": "You will be notified here when your file is ready.", "Your file is ready": "Your file is ready" } 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 d5e3c886f..da0d345b7 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 { localLoggerFactory } from '@vegaprotocol/logger'; import { useLedgerDownloadFile } from './ledger-download-store'; @@ -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); @@ -72,117 +72,106 @@ interface Props { export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => { const t = useT(); 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 link = createDownloadUrl({ - protohost, - partyId, - assetId, - dateFrom, - dateTo, - }); - - const startDownload = async (event: React.FormEvent) => { - event.preventDefault(); - - const title = t( - 'Downloading for {{asset}} from {{startDate}} till {{endDate}}', - { - asset: assets[assetId], - startDate: format(new Date(dateFrom), 'dd MMMM yyyy HH:mm'), - endDate: 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, - isChanged: true, + const startDownload = useCallback( + async (formValues: LedgerFormValues) => { + const link = createDownloadUrl({ + protohost, + partyId, + ...formValues, }); - }, 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.'); + const dateTimeFormatter = getDateTimeFormat(); + const title = t( + 'Downloading for {{asset}} from {{startDate}} till {{endDate}}', + { + asset: assets[formValues.assetId], + startDate: dateTimeFormatter.format(new Date(formValues.dateFrom)), + endDate: dateTimeFormatter.format( + new Date(formValues.dateTo || Date.now()) + ), } - throw new Error('Download of ledger entries failed'); + ); + + const downloadStoreItem = { + title, + link, + isChanged: true, + }; + 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, t, updateDownloadQueue] + ); if (!protohost || Object.keys(assets).length === 0) { return null; @@ -191,49 +180,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{{offset}}.', { offset: toHoursAndMinutes(offset) } )}

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