diff --git a/libs/fills/src/lib/fills-manager.tsx b/libs/fills/src/lib/fills-manager.tsx index bb0f7b0e7..5b2b04f56 100644 --- a/libs/fills/src/lib/fills-manager.tsx +++ b/libs/fills/src/lib/fills-manager.tsx @@ -45,8 +45,9 @@ export const FillsManager = ({ datasource={{ getRows }} onBodyScrollEnd={onBodyScrollEnd} onBodyScroll={onBodyScroll} - noRowsOverlayComponent={() => null} onMarketClick={onMarketClick} + suppressLoadingOverlay + suppressNoRowsOverlay />
{ return responseData?.ledgerEntries?.edges || []; @@ -94,33 +94,29 @@ export const update = ( }); }; -const getPageInfo = (responseData: LedgerEntriesQuery): PageInfo | null => - responseData.ledgerEntries?.pageInfo || null; - const ledgerEntriesOnlyProvider = makeDataProvider({ query: LedgerEntriesDocument, getData, getDelta: getData, update, - pagination: { - getPageInfo, - append, - first: 100, - }, additionalContext: { isEnlargedTimeout: true, }, }); export const ledgerEntriesProvider = makeDerivedDataProvider< - (AggregatedLedgerEntriesEdge | null)[], - AggregatedLedgerEntriesEdge[], + AggregatedLedgerEntriesNode[], + AggregatedLedgerEntriesNode[], LedgerEntriesQueryVariables >( - [ledgerEntriesOnlyProvider, assetsProvider, marketsProvider], + [ + ledgerEntriesOnlyProvider, + (callback, client) => assetsProvider(callback, client), + marketsProvider, + ], ([entries, assets, markets]) => { return entries.map((edge: AggregatedLedgerEntriesEdge) => { - const entry = edge?.node; + const entry = edge.node; const asset = assets.find((asset: Asset) => asset.id === entry.assetId); const marketSender = markets.find( (market: Market) => market.id === entry.fromAccountMarketId @@ -148,21 +144,22 @@ export const useLedgerEntriesDataProvider = ({ filter, gridRef, }: Props) => { - const dataRef = useRef<(AggregatedLedgerEntriesEdge | null)[] | null>(null); + const dataRef = useRef(null); const totalCountRef = useRef(); const variables = useMemo( () => ({ partyId, dateRange: filter?.vegaTime?.value, - fromAccountType: filter?.fromAccountType?.value ?? null, - toAccountType: filter?.toAccountType?.value ?? null, + pagination: { + first: 5000, + }, }), - [partyId, filter] + [partyId, filter?.vegaTime?.value] ); const update = useCallback( - ({ data }: { data: (AggregatedLedgerEntriesEdge | null)[] | null }) => { + ({ data }: { data: AggregatedLedgerEntriesEdge[] | null }) => { return updateGridData(dataRef, data, gridRef); }, [gridRef] @@ -173,7 +170,7 @@ export const useLedgerEntriesDataProvider = ({ data, totalCount, }: { - data: (AggregatedLedgerEntriesEdge | null)[] | null; + data: AggregatedLedgerEntriesEdge[] | null; totalCount?: number; }) => { totalCountRef.current = totalCount; diff --git a/libs/ledger/src/lib/ledger-manager.tsx b/libs/ledger/src/lib/ledger-manager.tsx index e29708511..7ad7883f2 100644 --- a/libs/ledger/src/lib/ledger-manager.tsx +++ b/libs/ledger/src/lib/ledger-manager.tsx @@ -3,7 +3,9 @@ import type * as Schema from '@vegaprotocol/types'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import type { FilterChangedEvent } from 'ag-grid-community'; import type { AgGridReact } from 'ag-grid-react'; -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { subDays, formatRFC3339 } from 'date-fns'; +import type { AggregatedLedgerEntriesNode } from './ledger-entries-data-provider'; import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider'; import { LedgerTable } from './ledger-table'; import type * as Types from '@vegaprotocol/types'; @@ -15,43 +17,43 @@ export interface Filter { fromAccountType?: { value: Types.AccountType[] }; toAccountType?: { value: Types.AccountType[] }; } - -type LedgerManagerProps = { partyId: string }; -export const LedgerManager = ({ partyId }: LedgerManagerProps) => { +const defaultFilter = { + vegaTime: { + value: { start: formatRFC3339(subDays(Date.now(), 7)) }, + }, +}; +export const LedgerManager = ({ partyId }: { partyId: string }) => { const gridRef = useRef(null); - const [filter, setFilter] = useState(); + const [filter, setFilter] = useState(defaultFilter); + const [dataCount, setDataCount] = useState(0); - const { data, error, loading, getRows, reload } = - useLedgerEntriesDataProvider({ - partyId, - filter, - gridRef, - }); + const { data, error, loading, reload } = useLedgerEntriesDataProvider({ + partyId, + filter, + gridRef, + }); - const onFilterChanged = useCallback( - (event: FilterChangedEvent) => { - const updatedFilter = event.api.getFilterModel(); - if (Object.keys(updatedFilter).length) { - setFilter(updatedFilter); - } else if (filter) { - setFilter(undefined); - } - }, - [filter] - ); - const getRowId = useCallback( - ({ data }: { data: Types.AggregatedLedgerEntry }) => - `${data.vegaTime}-${data.fromAccountPartyId}-${data.toAccountPartyId}`, + const onFilterChanged = useCallback((event: FilterChangedEvent) => { + const updatedFilter = { ...defaultFilter, ...event.api.getFilterModel() }; + setFilter(updatedFilter); + }, []); + const extractNodesDecorator = useCallback( + (data: AggregatedLedgerEntriesNode[] | null, loading: boolean) => + data && !loading ? data.map((item) => item.node) : null, [] ); + + const extractedData = extractNodesDecorator(data, loading); + useEffect(() => { + setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0); + }, [extractedData]); + return (
{ error={error} data={data} noDataMessage={t('No entries')} - noDataCondition={(data) => !(data && data.length)} + noDataCondition={() => !dataCount} reload={reload} />
diff --git a/libs/ledger/src/lib/ledger-table.tsx b/libs/ledger/src/lib/ledger-table.tsx index 6eafc6691..564eac2d2 100644 --- a/libs/ledger/src/lib/ledger-table.tsx +++ b/libs/ledger/src/lib/ledger-table.tsx @@ -22,6 +22,7 @@ import { } from '@vegaprotocol/types'; import type { LedgerEntry } from './ledger-entries-data-provider'; import { forwardRef } from 'react'; +import { formatRFC3339, subDays } from 'date-fns'; export const TransferTooltipCellComponent = ({ value, @@ -35,6 +36,11 @@ export const TransferTooltipCellComponent = ({ ); }; +const defaultRangeFilter = { start: formatRFC3339(subDays(Date.now(), 7)) }; +const dateRangeFilterParams = { + maxNextDays: 0, + defaultRangeFilter, +}; type LedgerEntryProps = TypedDataAgGrid; export const LedgerTable = forwardRef( @@ -42,17 +48,20 @@ export const LedgerTable = forwardRef( return ( data.id} tooltipShowDelay={500} defaultColDef={{ flex: 1, resizable: true, sortable: true, tooltipComponent: TransferTooltipCellComponent, - filterParams: { buttons: ['reset'] }, + filterParams: { + ...dateRangeFilterParams, + buttons: ['reset'], + }, }} + suppressLoadingOverlay + suppressNoRowsOverlay {...props} > ( }: VegaValueFormatterParams) => value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-' } + filterParams={dateRangeFilterParams} filter={DateRangeFilter} /> diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx index dfe109505..ef7c0f6fc 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx @@ -152,6 +152,8 @@ export const OrderListManager = ({ isReadOnly={isReadOnly} hasActiveOrder={hasActiveOrder} blockLoadDebounceMillis={100} + suppressLoadingOverlay + suppressNoRowsOverlay />
{ + it('should be properly rendered', async () => { + const defaultRangeFilter = { + start: '2023-02-14T13:53:01+01:00', + end: '2023-02-21T13:53:01+01:00', + }; + const displayStartValue = '2023-02-14T12:53:01.000'; + const displayEndValue = '2023-02-21T12:53:01.000'; + render( + + ); + + expect(screen.getByLabelText('Start')).toHaveValue(displayStartValue); + expect(screen.getByLabelText('End')).toHaveValue(displayEndValue); + + expect(commonProps.filterChangedCallback).toHaveBeenCalled(); + }); +}); diff --git a/libs/react-helpers/src/lib/grid/date-range-filter.tsx b/libs/react-helpers/src/lib/grid/date-range-filter.tsx index ab2f568be..7c389f91e 100644 --- a/libs/react-helpers/src/lib/grid/date-range-filter.tsx +++ b/libs/react-helpers/src/lib/grid/date-range-filter.tsx @@ -1,113 +1,259 @@ import type { ChangeEvent } from 'react'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import type * as Schema from '@vegaprotocol/types'; import { forwardRef, useImperativeHandle, useState } from 'react'; import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community'; -import { formatForInput } from '../format/date'; +import { + isBefore, + subDays, + addDays, + differenceInDays, + formatRFC3339, + min, + max, + isValid, +} from 'date-fns'; import { t } from '../i18n'; +import { formatForInput } from '../format/date'; const defaultFilterValue: Schema.DateRange = {}; +export interface DateRangeFilterProps extends IFilterParams { + defaultRangeFilter?: Schema.DateRange; + maxSubDays?: number; + maxNextDays?: number; + maxDaysRange?: number; +} -export const DateRangeFilter = forwardRef((props: IFilterParams, ref) => { - const [value, setValue] = useState(defaultFilterValue); +export const DateRangeFilter = forwardRef( + (props: DateRangeFilterProps, ref) => { + const defaultDates = props?.defaultRangeFilter || defaultFilterValue; + const [value, setValue] = useState(defaultDates); + const [error, setError] = useState(''); + const [minStartDate, maxStartDate, minEndDate, maxEndDate] = useMemo(() => { + const minStartDate = + props?.maxSubDays !== undefined + ? formatForInput(subDays(Date.now(), props.maxSubDays)) + : ''; + const maxStartDate = + props?.maxNextDays !== undefined + ? formatForInput(addDays(Date.now(), props.maxNextDays)) + : ''; + const minEndDate = + value.start && props?.maxDaysRange !== undefined + ? formatForInput(new Date(value.start)) + : minStartDate || value.start + ? formatForInput(new Date(value.start)) + : ''; + const endDateCandidates = []; + if (props.maxNextDays !== undefined) { + endDateCandidates.push(addDays(new Date(), props.maxNextDays)); + } + if (props.maxDaysRange !== undefined && value.start) { + endDateCandidates.push( + addDays(new Date(value.start), props.maxDaysRange) + ); + } + const maxEndDate = endDateCandidates.length + ? formatForInput(min(endDateCandidates)) + : maxStartDate; + return [minStartDate, maxStartDate, minEndDate, maxEndDate]; + }, [props.maxSubDays, props.maxDaysRange, props.maxNextDays, value.start]); + // expose AG Grid Filter Lifecycle callbacks + useImperativeHandle(ref, () => { + return { + doesFilterPass(params: IDoesFilterPassParams) { + const { api, colDef, column, columnApi, context } = props; + const { node } = params; + const rowValue = props.valueGetter({ + api, + colDef, + column, + columnApi, + context, + data: node.data, + getValue: (field) => node.data[field], + node, + }); + if ( + value.start && + rowValue && + new Date(rowValue) <= new Date(value.start) + ) { + return false; + } + if ( + value.end && + rowValue && + new Date(rowValue) >= new Date(value.end) + ) { + return false; + } + return true; + }, - // expose AG Grid Filter Lifecycle callbacks - useImperativeHandle(ref, () => { - return { - doesFilterPass(params: IDoesFilterPassParams) { - const { api, colDef, column, columnApi, context } = props; - const { node } = params; - const rowValue = props.valueGetter({ - api, - colDef, - column, - columnApi, - context, - data: node.data, - getValue: (field) => node.data[field], - node, - }); - if ( - value.start && - rowValue && - new Date(rowValue) <= new Date(value.start) - ) { - return false; - } - if ( - value.end && - rowValue && - new Date(rowValue) >= new Date(value.end) - ) { - return false; - } - return true; - }, + isFilterActive() { + return value.start || value.end; + }, - isFilterActive() { - return value.start || value.end; - }, + getModel() { + if (!this.isFilterActive()) { + return null; + } - getModel() { - if (!this.isFilterActive()) { - return null; - } + return { value }; + }, - return { value }; - }, - - setModel(model?: { value: Schema.DateRange } | null) { - setValue(model?.value || defaultFilterValue); - }, - }; - }); - - const onChange = (event: ChangeEvent) => { - setValue({ - ...value, - [event.target.name]: - event.target.value && - new Date(event.target.value).toISOString().replace('Z', '000000Z'), + setModel(model?: { value: Schema.DateRange } | null) { + setValue( + model?.value || props?.defaultRangeFilter || defaultFilterValue + ); + }, + }; }); - }; + const validate = ( + name: string, + timeValue: Date, + update?: Schema.DateRange + ) => { + if ( + props.maxSubDays !== undefined && + isBefore(new Date(timeValue), subDays(Date.now(), props.maxSubDays + 1)) + ) { + setError( + t( + 'The earliest data that can be queried is %s days ago.', + String(props.maxSubDays) + ) + ); + return false; + } + if (props?.maxDaysRange !== undefined) { + const contrvalue = + name === 'start' + ? update?.end || value.end + : update?.start || value.start; + if ( + Math.abs( + differenceInDays(new Date(timeValue), new Date(contrvalue)) + ) > props.maxDaysRange + ) { + setError( + t( + 'The maximum time range that can be queried is %s days.', + String(props.maxDaysRange) + ) + ); + return false; + } + } + setError(''); + return true; + }; - useEffect(() => { - props?.filterChangedCallback(); - }, [value, props]); + const checkForEndDate = ( + endDate: Date | undefined, + startDate: Date | undefined + ) => { + const endDateCandidates: Date[] = []; + if (props.maxDaysRange !== undefined && isValid(startDate)) { + endDateCandidates.push(addDays(startDate as Date, props.maxDaysRange)); + } + if (props.maxNextDays !== undefined) { + endDateCandidates.push(addDays(Date.now(), props.maxNextDays)); + } + if (isValid(endDate)) { + endDateCandidates.push(endDate as Date); + } + return endDate && startDate + ? formatRFC3339(max([startDate, min(endDateCandidates)])) + : undefined; + }; + const onChange = (event: ChangeEvent) => { + const { value: dateValue, name } = event.target; + const date = new Date(dateValue || defaultDates[name as 'start' | 'end']); + let update = { [name]: isValid(date) ? formatRFC3339(date) : undefined }; + const startCheckDate = name === 'start' ? date : new Date(value.start); + const endCheckDate = + name === 'start' + ? new Date(value.end) + : isValid(date) + ? date + : new Date(maxEndDate); + const endDate = isValid(endCheckDate) ? endCheckDate : undefined; + const startDate = isValid(startCheckDate) ? startCheckDate : undefined; + update = { ...update, end: checkForEndDate(endDate, startDate) }; - const start = (value.start && formatForInput(new Date(value.start))) || ''; - const end = (value.end && formatForInput(new Date(value.end))) || ''; - return ( -
-
- - -
-
- + {error} +
+ ) : null; + return ( +
{not}
+ ); + }, [error]); + + const start = (value.start && formatForInput(new Date(value.start))) || ''; + const end = (value.end && formatForInput(new Date(value.end))) || ''; + return ( +
+ {notification} +
+
+ +
+
+ +
+
+
+ +
-
- ); -}); + ); + } +); diff --git a/libs/react-helpers/src/lib/grid/set-filter.tsx b/libs/react-helpers/src/lib/grid/set-filter.tsx index 89392a5b6..a37d0dff3 100644 --- a/libs/react-helpers/src/lib/grid/set-filter.tsx +++ b/libs/react-helpers/src/lib/grid/set-filter.tsx @@ -6,6 +6,7 @@ import React, { useState, } from 'react'; import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community'; +import { t } from '../i18n'; export const SetFilter = forwardRef((props: IFilterParams, ref) => { const [value, setValue] = useState([]); @@ -16,18 +17,19 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => { doesFilterPass(params: IDoesFilterPassParams) { const { api, colDef, column, columnApi, context } = props; const { node } = params; - return ( - props.valueGetter({ - api, - colDef, - column, - columnApi, - context, - data: node.data, - getValue: (field) => node.data[field], - node, - }) === value - ); + const getValue = props.valueGetter({ + api, + colDef, + column, + columnApi, + context, + data: node.data, + getValue: (field) => node.data[field], + node, + }); + return Array.isArray(value) + ? value.includes(getValue) + : getValue === value; }, isFilterActive() { @@ -83,7 +85,7 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => { className="ag-standard-button ag-filter-apply-panel-button" onClick={() => setValue([])} > - Reset + {t('Reset')}