Commit 6db2fd57 authored by isstuev's avatar isstuev

csv export filters

parent 46e795fe
...@@ -9,7 +9,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources' ...@@ -9,7 +9,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources'
export default function buildUrl<R extends ResourceName>( export default function buildUrl<R extends ResourceName>(
resourceName: R, resourceName: R,
pathParams?: ResourcePathParams<R>, pathParams?: ResourcePathParams<R>,
queryParams?: Record<string, string | Array<string> | number | undefined>, queryParams?: Record<string, string | Array<string> | number | null | undefined>,
): string { ): string {
const resource: ApiResource = RESOURCES[resourceName]; const resource: ApiResource = RESOURCES[resourceName];
const baseUrl = isNeedProxy() ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint); const baseUrl = isNeedProxy() ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
......
export type CsvExportType = 'transactions' | 'internal-transactions' | 'token-transfers' | 'logs'; import type { AddressFromToFilter } from 'types/api/address';
export type CsvExportParams = {
type: 'transactions' | 'internal-transactions' | 'token-transfers';
filterType?: 'address';
filterValue?: AddressFromToFilter;
} | {
type: 'logs';
filterType?: 'topic';
filterValue?: string;
}
...@@ -2,7 +2,7 @@ import { chakra, Icon, Tooltip, Hide, Skeleton, Flex } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { chakra, Icon, Tooltip, Hide, Skeleton, Flex } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { CsvExportType } from 'types/client/address'; import type { CsvExportParams } from 'types/client/address';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import svgFileIcon from 'icons/files/csv.svg'; import svgFileIcon from 'icons/files/csv.svg';
...@@ -12,12 +12,12 @@ import LinkInternal from 'ui/shared/LinkInternal'; ...@@ -12,12 +12,12 @@ import LinkInternal from 'ui/shared/LinkInternal';
interface Props { interface Props {
address: string; address: string;
type: CsvExportType; params: CsvExportParams;
className?: string; className?: string;
isLoading?: boolean; isLoading?: boolean;
} }
const AddressCsvExportLink = ({ className, address, type, isLoading }: Props) => { const AddressCsvExportLink = ({ className, address, params, isLoading }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isInitialLoading = useIsInitialLoading(isLoading); const isInitialLoading = useIsInitialLoading(isLoading);
...@@ -43,7 +43,7 @@ const AddressCsvExportLink = ({ className, address, type, isLoading }: Props) => ...@@ -43,7 +43,7 @@ const AddressCsvExportLink = ({ className, address, type, isLoading }: Props) =>
display="inline-flex" display="inline-flex"
alignItems="center" alignItems="center"
whiteSpace="nowrap" whiteSpace="nowrap"
href={ route({ pathname: '/csv-export', query: { type, address } }) } href={ route({ pathname: '/csv-export', query: { ...params, address } }) }
flexShrink={ 0 } flexShrink={ 0 }
> >
<Icon as={ svgFileIcon } boxSize={{ base: '30px', lg: 6 }}/> <Icon as={ svgFileIcon } boxSize={{ base: '30px', lg: 6 }}/>
......
...@@ -74,7 +74,12 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -74,7 +74,12 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
isActive={ Boolean(filterValue) } isActive={ Boolean(filterValue) }
isLoading={ pagination.isLoading } isLoading={ pagination.isLoading }
/> />
<AddressCsvExportLink address={ hash } isLoading={ pagination.isLoading } type="internal-transactions" ml={{ base: 2, lg: 'auto' }}/> <AddressCsvExportLink
address={ hash }
isLoading={ pagination.isLoading }
params={{ type: 'internal-transactions', filterType: 'address', filterValue }}
ml={{ base: 2, lg: 'auto' }}
/>
<Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/>
</ActionBar> </ActionBar>
); );
......
...@@ -32,7 +32,11 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement> ...@@ -32,7 +32,11 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
const actionBar = ( const actionBar = (
<ActionBar mt={ -6 } showShadow justifyContent={{ base: 'space-between', lg: 'end' }}> <ActionBar mt={ -6 } showShadow justifyContent={{ base: 'space-between', lg: 'end' }}>
<AddressCsvExportLink address={ hash } isLoading={ pagination.isLoading } type="logs"/> <AddressCsvExportLink
address={ hash }
isLoading={ pagination.isLoading }
params={{ type: 'logs' }}
/>
<Pagination ml={{ base: 0, lg: 8 }} { ...pagination }/> <Pagination ml={{ base: 0, lg: 8 }} { ...pagination }/>
</ActionBar> </ActionBar>
); );
......
...@@ -272,7 +272,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr ...@@ -272,7 +272,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
{ currentAddress && ( { currentAddress && (
<AddressCsvExportLink <AddressCsvExportLink
address={ currentAddress } address={ currentAddress }
type="token-transfers" params={{ type: 'token-transfers', filterType: 'address', filterValue: filters.filter }}
ml={{ base: 2, lg: 'auto' }} ml={{ base: 2, lg: 'auto' }}
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
......
...@@ -159,24 +159,27 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { ...@@ -159,24 +159,27 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
/> />
); );
const csvExportLink = (
<AddressCsvExportLink
address={ currentAddress }
params={{ type: 'transactions', filterType: 'address', filterValue }}
ml="auto"
isLoading={ addressTxsQuery.pagination.isLoading }
/>
);
return ( return (
<> <>
{ !isMobile && ( { !isMobile && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
{ filter } { filter }
{ currentAddress && ( { currentAddress && csvExportLink }
<AddressCsvExportLink
address={ currentAddress }
type="transactions"
ml="auto"
isLoading={ addressTxsQuery.pagination.isLoading }
/>
) }
<Pagination { ...addressTxsQuery.pagination } ml={ 8 }/> <Pagination { ...addressTxsQuery.pagination } ml={ 8 }/>
</ActionBar> </ActionBar>
) } ) }
<TxsContent <TxsContent
filter={ filter } filter={ filter }
filterValue={ filterValue }
query={ addressTxsQuery } query={ addressTxsQuery }
currentAddress={ typeof currentAddress === 'string' ? currentAddress : undefined } currentAddress={ typeof currentAddress === 'string' ? currentAddress : undefined }
enableTimeIncrement enableTimeIncrement
......
...@@ -4,6 +4,7 @@ import type { SubmitHandler } from 'react-hook-form'; ...@@ -4,6 +4,7 @@ import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { FormFields } from './types'; import type { FormFields } from './types';
import type { CsvExportParams } from 'types/client/address';
import buildUrl from 'lib/api/buildUrl'; import buildUrl from 'lib/api/buildUrl';
import type { ResourceName } from 'lib/api/resources'; import type { ResourceName } from 'lib/api/resources';
...@@ -17,10 +18,12 @@ import CsvExportFormReCaptcha from './CsvExportFormReCaptcha'; ...@@ -17,10 +18,12 @@ import CsvExportFormReCaptcha from './CsvExportFormReCaptcha';
interface Props { interface Props {
hash: string; hash: string;
resource: ResourceName; resource: ResourceName;
filterType?: CsvExportParams['filterType'] | null;
filterValue?: CsvExportParams['filterValue'] | null;
fileNameTemplate: string; fileNameTemplate: string;
} }
const CsvExportForm = ({ hash, resource, fileNameTemplate }: Props) => { const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTemplate }: Props) => {
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: { defaultValues: {
...@@ -37,6 +40,8 @@ const CsvExportForm = ({ hash, resource, fileNameTemplate }: Props) => { ...@@ -37,6 +40,8 @@ const CsvExportForm = ({ hash, resource, fileNameTemplate }: Props) => {
address_id: hash, address_id: hash,
from_period: data.from, from_period: data.from,
to_period: data.to, to_period: data.to,
filter_type: filterType,
filter_value: filterValue,
recaptcha_response: data.reCaptcha, recaptcha_response: data.reCaptcha,
}); });
...@@ -51,7 +56,11 @@ const CsvExportForm = ({ hash, resource, fileNameTemplate }: Props) => { ...@@ -51,7 +56,11 @@ const CsvExportForm = ({ hash, resource, fileNameTemplate }: Props) => {
} }
const blob = await response.blob(); const blob = await response.blob();
downloadBlob(blob, `${ fileNameTemplate }_${ hash }_${ data.from }_${ data.to }.csv`); downloadBlob(
blob,
`${ fileNameTemplate }_${ hash }_${ data.from }_${ data.to }
${ filterType && filterValue ? '_with_filter_type_' + filterType + '_value_' + filterValue : '' }.csv`,
);
} catch (error) { } catch (error) {
toast({ toast({
...@@ -64,7 +73,7 @@ const CsvExportForm = ({ hash, resource, fileNameTemplate }: Props) => { ...@@ -64,7 +73,7 @@ const CsvExportForm = ({ hash, resource, fileNameTemplate }: Props) => {
}); });
} }
}, [ fileNameTemplate, hash, resource, toast ]); }, [ fileNameTemplate, hash, resource, filterType, filterValue, toast ]);
return ( return (
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as addressMock from 'mocks/address/address';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import CsvExport from './CsvExport';
const ADDRESS_API_URL = buildApiUrl('address', { hash: addressMock.hash });
const hooksConfig = {
router: {
query: { address: addressMock.hash, type: 'transactions', filterType: 'address', filterValue: 'from' },
isReady: true,
},
};
test.beforeEach(async({ page }) => {
await page.route(ADDRESS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.withName),
}));
});
test('base view +@mobile +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<CsvExport/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
...@@ -2,7 +2,8 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,7 +2,8 @@ import { Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { CsvExportType } from 'types/client/address'; import { AddressFromToFilterValues } from 'types/api/address';
import type { CsvExportParams } from 'types/client/address';
import type { ResourceName } from 'lib/api/resources'; import type { ResourceName } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
...@@ -21,32 +22,41 @@ interface ExportTypeEntity { ...@@ -21,32 +22,41 @@ interface ExportTypeEntity {
text: string; text: string;
resource: ResourceName; resource: ResourceName;
fileNameTemplate: string; fileNameTemplate: string;
filterType?: CsvExportParams['filterType'];
filterValues?: Readonly<Array<CsvExportParams['filterValue']>>;
} }
const EXPORT_TYPES: Record<CsvExportType, ExportTypeEntity> = { const EXPORT_TYPES: Record<CsvExportParams['type'], ExportTypeEntity> = {
transactions: { transactions: {
text: 'transactions', text: 'transactions',
resource: 'csv_export_txs', resource: 'csv_export_txs',
fileNameTemplate: 'transactions', fileNameTemplate: 'transactions',
filterType: 'address',
filterValues: AddressFromToFilterValues,
}, },
'internal-transactions': { 'internal-transactions': {
text: 'internal transactions', text: 'internal transactions',
resource: 'csv_export_internal_txs', resource: 'csv_export_internal_txs',
fileNameTemplate: 'internal_transactions', fileNameTemplate: 'internal_transactions',
filterType: 'address',
filterValues: AddressFromToFilterValues,
}, },
'token-transfers': { 'token-transfers': {
text: 'token transfers', text: 'token transfers',
resource: 'csv_export_token_transfers', resource: 'csv_export_token_transfers',
fileNameTemplate: 'token_transfers', fileNameTemplate: 'token_transfers',
filterType: 'address',
filterValues: AddressFromToFilterValues,
}, },
logs: { logs: {
text: 'logs', text: 'logs',
resource: 'csv_export_logs', resource: 'csv_export_logs',
fileNameTemplate: 'logs', fileNameTemplate: 'logs',
filterType: 'topic',
}, },
}; };
const isCorrectExportType = (type: string): type is CsvExportType => Object.keys(EXPORT_TYPES).includes(type); const isCorrectExportType = (type: string): type is CsvExportParams['type'] => Object.keys(EXPORT_TYPES).includes(type);
const CsvExport = () => { const CsvExport = () => {
const router = useRouter(); const router = useRouter();
...@@ -55,6 +65,8 @@ const CsvExport = () => { ...@@ -55,6 +65,8 @@ const CsvExport = () => {
const addressHash = router.query.address?.toString() || ''; const addressHash = router.query.address?.toString() || '';
const exportType = router.query.type?.toString() || ''; const exportType = router.query.type?.toString() || '';
const filterTypeFromQuery = router.query.filterType?.toString() || null;
const filterValueFromQuery = router.query.filterValue?.toString();
const addressQuery = useApiQuery('address', { const addressQuery = useApiQuery('address', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
...@@ -80,6 +92,19 @@ const CsvExport = () => { ...@@ -80,6 +92,19 @@ const CsvExport = () => {
throw Error('Not found', { cause: { status: 404 } }); throw Error('Not found', { cause: { status: 404 } });
} }
const filterType = filterTypeFromQuery === EXPORT_TYPES[exportType].filterType ? filterTypeFromQuery : null;
const filterValue = (() => {
if (!filterType || !filterValueFromQuery) {
return null;
}
if (EXPORT_TYPES[exportType].filterValues && !EXPORT_TYPES[exportType].filterValues?.includes(filterValueFromQuery)) {
return null;
}
return filterValueFromQuery;
})();
const content = (() => { const content = (() => {
if (addressQuery.isError) { if (addressQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
...@@ -89,7 +114,15 @@ const CsvExport = () => { ...@@ -89,7 +114,15 @@ const CsvExport = () => {
return <ContentLoader/>; return <ContentLoader/>;
} }
return <CsvExportForm hash={ addressHash } resource={ EXPORT_TYPES[exportType].resource } fileNameTemplate={ EXPORT_TYPES[exportType].fileNameTemplate }/>; return (
<CsvExportForm
hash={ addressHash }
resource={ EXPORT_TYPES[exportType].resource }
filterType={ filterType }
filterValue={ filterValue }
fileNameTemplate={ EXPORT_TYPES[exportType].fileNameTemplate }
/>
);
})(); })();
return ( return (
...@@ -104,6 +137,7 @@ const CsvExport = () => { ...@@ -104,6 +137,7 @@ const CsvExport = () => {
<AddressIcon address={{ hash: addressHash, is_contract: true, implementation_name: null }}/> <AddressIcon address={{ hash: addressHash, is_contract: true, implementation_name: null }}/>
<AddressLink hash={ addressHash } type="address" ml={ 2 } truncation={ isMobile ? 'constant' : 'dynamic' }/> <AddressLink hash={ addressHash } type="address" ml={ 2 } truncation={ isMobile ? 'constant' : 'dynamic' }/>
</Address> </Address>
{ filterType && filterValue && <span> with applied filter by { filterType } ({ filterValue })</span> }
<span> to CSV file</span> <span> to CSV file</span>
</Flex> </Flex>
{ content } { content }
......
import { Box, Show, Hide } from '@chakra-ui/react'; import { Box, Show, Hide } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AddressCsvExportLink from 'ui/address/AddressCsvExportLink'; import AddressCsvExportLink from 'ui/address/AddressCsvExportLink';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
...@@ -20,12 +22,14 @@ type Props = { ...@@ -20,12 +22,14 @@ type Props = {
socketInfoNum?: number; socketInfoNum?: number;
currentAddress?: string; currentAddress?: string;
filter?: React.ReactNode; filter?: React.ReactNode;
filterValue?: AddressFromToFilter;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
top?: number; top?: number;
} }
const TxsContent = ({ const TxsContent = ({
filter, filter,
filterValue,
query, query,
showBlockInfo = true, showBlockInfo = true,
showSocketInfo = true, showSocketInfo = true,
...@@ -88,8 +92,14 @@ const TxsContent = ({ ...@@ -88,8 +92,14 @@ const TxsContent = ({
paginationProps={ query.pagination } paginationProps={ query.pagination }
showPagination={ query.pagination.isVisible } showPagination={ query.pagination.isVisible }
filterComponent={ filter } filterComponent={ filter }
linkSlot={ currentAddress ? linkSlot={ currentAddress ? (
<AddressCsvExportLink address={ currentAddress } type="transactions" ml={ 2 } isLoading={ query.pagination.isLoading }/> : null <AddressCsvExportLink
address={ currentAddress }
params={{ type: 'transactions', filterType: 'address', filterValue }}
ml={ 2 }
isLoading={ query.pagination.isLoading }
/>
) : null
} }
/> />
) : null; ) : null;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment