feat(explorer): transaction list filtering
This commit is contained in:
parent
6be117086a
commit
5a90ebe12e
@ -5,7 +5,7 @@ NX_SENTRY_DSN=https://b3a56b03eda842faad731f3ea9dfd1bc@o286262.ingest.sentry.io/
|
|||||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
|
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
|
||||||
NX_VEGA_URL=https://api.vega.community/graphql
|
NX_VEGA_URL=https://api.vega.community/graphql
|
||||||
NX_VEGA_ENV=MAINNET
|
NX_VEGA_ENV=MAINNET
|
||||||
NX_BLOCK_EXPLORER=https://be.vega.community/rest/
|
NX_BLOCK_EXPLORER=https://be.vega.community/rest
|
||||||
NX_ETHERSCAN_URL=https://etherscan.io
|
NX_ETHERSCAN_URL=https://etherscan.io
|
||||||
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
|
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
|
||||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
"executor": "@nrwl/workspace:run-commands",
|
"executor": "@nrwl/workspace:run-commands",
|
||||||
"options": {
|
"options": {
|
||||||
"commands": [
|
"commands": [
|
||||||
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.67.3/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
|
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.71.4/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,29 +1,13 @@
|
|||||||
import WS from 'jest-websocket-mock';
|
import WS from 'jest-websocket-mock';
|
||||||
import useWebSocket from 'react-use-websocket';
|
import { render, screen } from '@testing-library/react';
|
||||||
import {
|
|
||||||
render,
|
|
||||||
screen,
|
|
||||||
fireEvent,
|
|
||||||
act,
|
|
||||||
waitFor,
|
|
||||||
} from '@testing-library/react';
|
|
||||||
import { TendermintWebsocketContext } from '../../contexts/websocket/tendermint-websocket-context';
|
|
||||||
import { BlocksRefetch } from './blocks-refetch';
|
import { BlocksRefetch } from './blocks-refetch';
|
||||||
|
|
||||||
const BlocksRefetchInWebsocketProvider = ({
|
const BlocksRefetchInWebsocketProvider = ({
|
||||||
callback,
|
callback,
|
||||||
mocketLocation,
|
|
||||||
}: {
|
}: {
|
||||||
callback: () => null;
|
callback: () => null;
|
||||||
mocketLocation: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const contextShape = useWebSocket(mocketLocation);
|
return <BlocksRefetch refetch={callback} />;
|
||||||
|
|
||||||
return (
|
|
||||||
<TendermintWebsocketContext.Provider value={{ ...contextShape }}>
|
|
||||||
<BlocksRefetch refetch={callback} />
|
|
||||||
</TendermintWebsocketContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Blocks refetch', () => {
|
describe('Blocks refetch', () => {
|
||||||
@ -32,111 +16,8 @@ describe('Blocks refetch', () => {
|
|||||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
||||||
new WebSocket(mocketLocation);
|
new WebSocket(mocketLocation);
|
||||||
|
|
||||||
render(
|
render(<BlocksRefetchInWebsocketProvider callback={() => null} />);
|
||||||
<BlocksRefetchInWebsocketProvider
|
|
||||||
callback={() => null}
|
|
||||||
mocketLocation={mocketLocation}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await mocket.connected;
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('new blocks');
|
|
||||||
expect(screen.getByTestId('refresh')).toBeInTheDocument();
|
expect(screen.getByTestId('refresh')).toBeInTheDocument();
|
||||||
mocket.close();
|
mocket.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initiate callback when the button is clicked', async () => {
|
|
||||||
const mocketLocation = 'wss:localhost:3003';
|
|
||||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
|
||||||
new WebSocket(mocketLocation);
|
|
||||||
|
|
||||||
const callback = jest.fn();
|
|
||||||
render(
|
|
||||||
<BlocksRefetchInWebsocketProvider
|
|
||||||
callback={callback}
|
|
||||||
mocketLocation={mocketLocation}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await mocket.connected;
|
|
||||||
const button = screen.getByTestId('refresh');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
fireEvent.click(button);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(callback.mock.calls.length).toEqual(1);
|
|
||||||
mocket.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show new blocks as websocket is correctly updated', async () => {
|
|
||||||
const mocketLocation = 'wss:localhost:3004';
|
|
||||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
|
||||||
new WebSocket(mocketLocation);
|
|
||||||
render(
|
|
||||||
<BlocksRefetchInWebsocketProvider
|
|
||||||
callback={() => null}
|
|
||||||
mocketLocation={mocketLocation}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await mocket.connected;
|
|
||||||
|
|
||||||
// Ensuring we send an ID equal to the one the client subscribed with.
|
|
||||||
await waitFor(() => expect(mocket.messages.length).toEqual(1));
|
|
||||||
// @ts-ignore id on messages
|
|
||||||
const id = mocket.messages[0].id;
|
|
||||||
|
|
||||||
const newBlockMessage = {
|
|
||||||
id,
|
|
||||||
result: {
|
|
||||||
query: "tm.event = 'NewBlock'",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
mocket.send(newBlockMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('1 new blocks');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
mocket.send(newBlockMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('2 new blocks');
|
|
||||||
mocket.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will not show new blocks if websocket has wrong ID', async () => {
|
|
||||||
const mocketLocation = 'wss:localhost:3005';
|
|
||||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
|
||||||
new WebSocket(mocketLocation);
|
|
||||||
|
|
||||||
render(
|
|
||||||
<BlocksRefetchInWebsocketProvider
|
|
||||||
callback={() => null}
|
|
||||||
mocketLocation={mocketLocation}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await mocket.connected;
|
|
||||||
|
|
||||||
// Ensuring we send an ID equal to the one the client subscribed with.
|
|
||||||
await waitFor(() => expect(mocket.messages.length).toEqual(1));
|
|
||||||
|
|
||||||
const newBlockMessageBadId = {
|
|
||||||
id: 'blahblahblah',
|
|
||||||
result: {
|
|
||||||
query: "tm.event = 'NewBlock'",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
mocket.send(newBlockMessageBadId);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
|
|
||||||
mocket.close();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,36 +1,19 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
import { Button, Icon } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
interface BlocksRefetchProps {
|
interface BlocksRefetchProps {
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlocksRefetch = ({ refetch }: BlocksRefetchProps) => {
|
export const BlocksRefetch = ({ refetch }: BlocksRefetchProps) => {
|
||||||
const [blocksToLoad, setBlocksToLoad] = useState<number>(0);
|
|
||||||
|
|
||||||
const { messages } = useTendermintWebsocket({
|
|
||||||
query: "tm.event = 'NewBlock'",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (messages.length > 0) {
|
|
||||||
setBlocksToLoad((prev) => prev + 1);
|
|
||||||
}
|
|
||||||
}, [messages]);
|
|
||||||
|
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
refetch();
|
refetch();
|
||||||
setBlocksToLoad(0);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-4">
|
<Button onClick={refresh} data-testid="refresh" size="xs">
|
||||||
<span data-testid="new-blocks">{blocksToLoad} new blocks - </span>
|
<Icon name="refresh" className="!align-baseline mr-2" size={3} />
|
||||||
<ButtonLink onClick={refresh} data-testid="refresh">
|
{t('Load new')}
|
||||||
{t('refresh to see latest')}
|
</Button>
|
||||||
</ButtonLink>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
24
apps/explorer/src/app/components/txs/tx-filter-label.tsx
Normal file
24
apps/explorer/src/app/components/txs/tx-filter-label.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|
||||||
|
export interface FilterLabelProps {
|
||||||
|
filters: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the list (currently limited to 1) of filters set by the
|
||||||
|
* Transaction Filter
|
||||||
|
*/
|
||||||
|
export function FilterLabel({ filters }: FilterLabelProps) {
|
||||||
|
if (!filters || filters.size !== 1) {
|
||||||
|
return <span className="uppercase">{t('Filter')}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className="uppercase">{t('Filters')}:</span>
|
||||||
|
<code className="bg-vega-light-150 px-2 rounded-md capitalize">
|
||||||
|
{Array.from(filters)[0]}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
164
apps/explorer/src/app/components/txs/tx-filter.tsx
Normal file
164
apps/explorer/src/app/components/txs/tx-filter.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItemIndicator,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
Icon,
|
||||||
|
Button,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { FilterLabel } from './tx-filter-label';
|
||||||
|
|
||||||
|
// All possible transaction types. Should be generated.
|
||||||
|
export type FilterOption =
|
||||||
|
| 'Amend LiquidityProvision Order'
|
||||||
|
| 'Amend Order'
|
||||||
|
| 'Batch Market Instructions'
|
||||||
|
| 'Cancel LiquidityProvision Order'
|
||||||
|
| 'Cancel Order'
|
||||||
|
| 'Cancel Transfer Funds'
|
||||||
|
| 'Chain Event'
|
||||||
|
| 'Delegate'
|
||||||
|
| 'Ethereum Key Rotate Submission'
|
||||||
|
| 'Issue Signatures'
|
||||||
|
| 'Key Rotate Submission'
|
||||||
|
| 'Liquidity Provision Order'
|
||||||
|
| 'Node Signature'
|
||||||
|
| 'Node Vote'
|
||||||
|
| 'Proposal'
|
||||||
|
| 'Protocol Upgrade'
|
||||||
|
| 'Register new Node'
|
||||||
|
| 'State Variable Proposal'
|
||||||
|
| 'Submit Oracle Data'
|
||||||
|
| 'Submit Order'
|
||||||
|
| 'Transfer Funds'
|
||||||
|
| 'Undelegate'
|
||||||
|
| 'Validator Heartbeat'
|
||||||
|
| 'Vote on Proposal'
|
||||||
|
| 'Withdraw';
|
||||||
|
|
||||||
|
// Alphabetised list of transaction types to appear at the top level
|
||||||
|
export const PrimaryFilterOptions: FilterOption[] = [
|
||||||
|
'Amend LiquidityProvision Order',
|
||||||
|
'Amend Order',
|
||||||
|
'Batch Market Instructions',
|
||||||
|
'Cancel LiquidityProvision Order',
|
||||||
|
'Cancel Order',
|
||||||
|
'Cancel Transfer Funds',
|
||||||
|
'Delegate',
|
||||||
|
'Liquidity Provision Order',
|
||||||
|
'Proposal',
|
||||||
|
'Submit Oracle Data',
|
||||||
|
'Submit Order',
|
||||||
|
'Transfer Funds',
|
||||||
|
'Undelegate',
|
||||||
|
'Vote on Proposal',
|
||||||
|
'Withdraw',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Alphabetised list of transaction types to nest under a 'More...' submenu
|
||||||
|
export const SecondaryFilterOptions: FilterOption[] = [
|
||||||
|
'Chain Event',
|
||||||
|
'Ethereum Key Rotate Submission',
|
||||||
|
'Issue Signatures',
|
||||||
|
'Key Rotate Submission',
|
||||||
|
'Node Signature',
|
||||||
|
'Node Vote',
|
||||||
|
'Protocol Upgrade',
|
||||||
|
'Register new Node',
|
||||||
|
'State Variable Proposal',
|
||||||
|
'Validator Heartbeat',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AllFilterOptions: FilterOption[] = [
|
||||||
|
...PrimaryFilterOptions,
|
||||||
|
...SecondaryFilterOptions,
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface TxFilterProps {
|
||||||
|
filters: Set<FilterOption>;
|
||||||
|
setFilters: Dispatch<SetStateAction<Set<FilterOption>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a structured dropdown menu of all of the available transaction
|
||||||
|
* types. It allows a user to select one transaction type to view. Later
|
||||||
|
* it will support multiple selection, but until the API supports that it is
|
||||||
|
* one or all.
|
||||||
|
* @param filters null or Set of tranaction types
|
||||||
|
* @param setFilters A function to update the filters prop
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenu
|
||||||
|
modal={false}
|
||||||
|
trigger={
|
||||||
|
<DropdownMenuTrigger className="ml-2">
|
||||||
|
<Button size="xs">
|
||||||
|
<FilterLabel filters={filters} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
{filters.size > 1 ? null : (
|
||||||
|
<>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
|
||||||
|
>
|
||||||
|
{t('Clear filters')} <Icon name="cross" />
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{PrimaryFilterOptions.map((f) => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={f}
|
||||||
|
checked={filters.has(f)}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
// NOTE: These act like radio buttons until the API supports multiple filters
|
||||||
|
setFilters(new Set([f]));
|
||||||
|
}}
|
||||||
|
id={`radio-${f}`}
|
||||||
|
>
|
||||||
|
{f}
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Icon name="tick-circle" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
))}
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
{t('More Types')}
|
||||||
|
<Icon name="chevron-right" />
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
{SecondaryFilterOptions.map((f) => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={f}
|
||||||
|
checked={filters.has(f)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
// NOTE: These act like radio buttons until the API supports multiple filters
|
||||||
|
setFilters(new Set([f]));
|
||||||
|
}}
|
||||||
|
id={`radio-${f}`}
|
||||||
|
>
|
||||||
|
{f}
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Icon name="tick-circle" className="inline" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { FixedSizeList as List } from 'react-window';
|
import { FixedSizeList as List } from 'react-window';
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import InfiniteLoader from 'react-window-infinite-loader';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
@ -69,6 +69,17 @@ export const TxsInfiniteList = ({
|
|||||||
}: TxsInfiniteListProps) => {
|
}: TxsInfiniteListProps) => {
|
||||||
const { screenSize } = useScreenDimensions();
|
const { screenSize } = useScreenDimensions();
|
||||||
const isStacked = ['xs', 'sm'].includes(screenSize);
|
const isStacked = ['xs', 'sm'].includes(screenSize);
|
||||||
|
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
||||||
|
const hasMountedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasMountedRef.current) {
|
||||||
|
if (infiniteLoaderRef.current) {
|
||||||
|
infiniteLoaderRef.current.resetloadMoreItemsCache(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasMountedRef.current = true;
|
||||||
|
}, [loadMoreTxs]);
|
||||||
|
|
||||||
if (!txs) {
|
if (!txs) {
|
||||||
if (!areTxsLoading) {
|
if (!areTxsLoading) {
|
||||||
@ -110,6 +121,7 @@ export const TxsInfiniteList = ({
|
|||||||
isItemLoaded={isItemLoaded}
|
isItemLoaded={isItemLoaded}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
loadMoreItems={loadMoreItems}
|
loadMoreItems={loadMoreItems}
|
||||||
|
ref={infiniteLoaderRef}
|
||||||
>
|
>
|
||||||
{({ onItemsRendered, ref }) => (
|
{({ onItemsRendered, ref }) => (
|
||||||
<List
|
<List
|
||||||
|
@ -33,7 +33,7 @@ export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
|
|||||||
// Hacky fix for param as array
|
// Hacky fix for param as array
|
||||||
let urlAsString = url.toString();
|
let urlAsString = url.toString();
|
||||||
if (filters) {
|
if (filters) {
|
||||||
urlAsString += '&' + filters;
|
urlAsString += '&' + filters.replace(' ', '%20');
|
||||||
}
|
}
|
||||||
|
|
||||||
return urlAsString;
|
return urlAsString;
|
||||||
@ -65,6 +65,14 @@ export const useTxsData = ({ limit, filters }: IUseTxsData) => {
|
|||||||
}
|
}
|
||||||
}, [setTxsState, data]);
|
}, [setTxsState, data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTxsState((prev) => ({
|
||||||
|
txsData: [],
|
||||||
|
hasMoreTxs: true,
|
||||||
|
lastCursor: '',
|
||||||
|
}));
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
const loadTxs = useCallback(() => {
|
const loadTxs = useCallback(() => {
|
||||||
return refetch({
|
return refetch({
|
||||||
limit: limit,
|
limit: limit,
|
||||||
|
@ -5,17 +5,43 @@ import { TxsInfiniteList } from '../../../components/txs';
|
|||||||
import { useTxsData } from '../../../hooks/use-txs-data';
|
import { useTxsData } from '../../../hooks/use-txs-data';
|
||||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||||
|
|
||||||
const BE_TXS_PER_REQUEST = 20;
|
import { useState } from 'react';
|
||||||
|
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
||||||
|
|
||||||
|
const BE_TXS_PER_REQUEST = 15;
|
||||||
|
|
||||||
export const TxsList = () => {
|
export const TxsList = () => {
|
||||||
useDocumentTitle(['Transactions']);
|
useDocumentTitle(['Transactions']);
|
||||||
|
|
||||||
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
|
|
||||||
useTxsData({ limit: BE_TXS_PER_REQUEST });
|
|
||||||
return (
|
return (
|
||||||
<section className="md:p-2 lg:p-4 xl:p-6">
|
<section className="md:p-2 lg:p-4 xl:p-6 relative">
|
||||||
<RouteTitle>{t('Transactions')}</RouteTitle>
|
<RouteTitle>{t('Transactions')}</RouteTitle>
|
||||||
|
<TxsListFiltered />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TxsListFiltered = () => {
|
||||||
|
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
||||||
|
|
||||||
|
const f =
|
||||||
|
filters && filters.size === 1
|
||||||
|
? `filters[cmd.type]=${Array.from(filters)[0]}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
|
||||||
|
useTxsData({
|
||||||
|
limit: BE_TXS_PER_REQUEST,
|
||||||
|
filters: f,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<menu className="mb-2">
|
||||||
<BlocksRefetch refetch={refreshTxs} />
|
<BlocksRefetch refetch={refreshTxs} />
|
||||||
|
<TxsFilter filters={filters} setFilters={setFilters} />
|
||||||
|
</menu>
|
||||||
|
|
||||||
<TxsInfiniteList
|
<TxsInfiniteList
|
||||||
hasMoreTxs={hasMoreTxs}
|
hasMoreTxs={hasMoreTxs}
|
||||||
areTxsLoading={loading}
|
areTxsLoading={loading}
|
||||||
@ -24,6 +50,6 @@ export const TxsList = () => {
|
|||||||
error={error}
|
error={error}
|
||||||
className="mb-28"
|
className="mb-28"
|
||||||
/>
|
/>
|
||||||
</section>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
849
apps/explorer/src/types/explorer.d.ts
vendored
849
apps/explorer/src/types/explorer.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -161,6 +161,51 @@ export const DropdownMenuSeparator = forwardRef<
|
|||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container element for submenus
|
||||||
|
*/
|
||||||
|
export const DropdownMenuSub = forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Sub>,
|
||||||
|
React.ComponentProps<typeof DropdownMenuPrimitive.Sub>
|
||||||
|
>(({ ...subProps }) => <DropdownMenuPrimitive.Sub {...subProps} />);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container within a DropdownMenuSub specifically for the content
|
||||||
|
*/
|
||||||
|
export const DropdownMenuSubContent = forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...subContentProps }, forwardedRef) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={forwardedRef}
|
||||||
|
className={classNames('bg-vega-light-150 dark:bg-vega-dark-150', className)}
|
||||||
|
{...subContentProps}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equivalent to trigger, but for triggering sub menus
|
||||||
|
*/
|
||||||
|
export const DropdownMenuSubTrigger = forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger>
|
||||||
|
>(({ className, ...subTriggerProps }, forwardedRef) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
className={classNames(className, itemClass)}
|
||||||
|
ref={forwardedRef}
|
||||||
|
{...subTriggerProps}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Portal to ensure menu portions are rendered outwith where they appear in the
|
||||||
|
* DOM.
|
||||||
|
*/
|
||||||
|
export const DropdownMenuPortal = forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Portal>,
|
||||||
|
React.ComponentProps<typeof DropdownMenuPrimitive.Portal>
|
||||||
|
>(({ ...portalProps }) => <DropdownMenuPrimitive.Portal {...portalProps} />);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a regular DropdownMenuItem with copy to clip board functionality
|
* Wraps a regular DropdownMenuItem with copy to clip board functionality
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user