feat(explorer): remove infinite tx loader (#4217)
This commit is contained in:
parent
8c8fe6878a
commit
74f2cfa4a5
@ -1,5 +1,4 @@
|
|||||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export interface InfoBlockProps {
|
export interface InfoBlockProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -1,33 +1,98 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import PartyLink from './party-link';
|
import PartyLink from './party-link';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { ExplorerNodeNamesDocument } from '../../../routes/validators/__generated__/NodeNames';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
const zeroes =
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000000';
|
||||||
|
const mocks = [
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
query: ExplorerNodeNamesDocument,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
nodesConnection: {
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '1',
|
||||||
|
name: 'Validator Node',
|
||||||
|
pubkey:
|
||||||
|
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e',
|
||||||
|
tmPubkey: 'tmPubkey1',
|
||||||
|
ethereumAddress: '0x123456789',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '2',
|
||||||
|
name: 'Node 2',
|
||||||
|
pubkey: 'pubkey2',
|
||||||
|
tmPubkey: 'tmPubkey2',
|
||||||
|
ethereumAddress: '0xabcdef123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
describe('PartyLink', () => {
|
describe('PartyLink', () => {
|
||||||
it('renders Network for 000.000 party', () => {
|
it('renders Network for 000.000 party', () => {
|
||||||
const zeroes =
|
const screen = render(
|
||||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
<MockedProvider>
|
||||||
const screen = render(<PartyLink id={zeroes} />);
|
<PartyLink id={zeroes} />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
expect(screen.getByText('Network')).toBeInTheDocument();
|
expect(screen.getByText('Network')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders Network for network party', () => {
|
it('renders Network for network party', () => {
|
||||||
const screen = render(<PartyLink id="network" />);
|
const screen = render(
|
||||||
|
<MockedProvider>
|
||||||
|
<PartyLink id="network" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
expect(screen.getByText('Network')).toBeInTheDocument();
|
expect(screen.getByText('Network')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders ID with no link for invalid party', () => {
|
it('renders ID with no link for invalid party', () => {
|
||||||
const screen = render(<PartyLink id="this-party-is-not-valid" />);
|
const screen = render(
|
||||||
|
<MockedProvider>
|
||||||
|
<PartyLink id="this-party-is-not-valid" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
|
expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('if the key is a validator, render their name instead', async () => {
|
||||||
|
const screen = render(
|
||||||
|
<MockedProvider mocks={mocks}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<PartyLink id="13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e" />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for hook to update with mock data
|
||||||
|
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
||||||
|
await expect(screen.getByText('Validator Node')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('links a valid party to the party page', () => {
|
it('links a valid party to the party page', () => {
|
||||||
const aValidParty =
|
const aValidParty =
|
||||||
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
|
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
|
||||||
|
|
||||||
const screen = render(
|
const screen = render(
|
||||||
<MemoryRouter>
|
<MockedProvider>
|
||||||
<PartyLink id={aValidParty} />
|
<MemoryRouter>
|
||||||
</MemoryRouter>
|
<PartyLink id={aValidParty} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const el = screen.getByText(aValidParty);
|
const el = screen.getByText(aValidParty);
|
||||||
|
@ -1,22 +1,44 @@
|
|||||||
import { Routes } from '../../../routes/route-names';
|
import { Routes } from '../../../routes/route-names';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import { useMemo, type ComponentProps } from 'react';
|
||||||
import Hash from '../hash';
|
import Hash from '../hash';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
|
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
|
||||||
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
import { Icon, truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useExplorerNodeNamesQuery } from '../../../routes/validators/__generated__/NodeNames';
|
||||||
|
import type { ExplorerNodeNamesQuery } from '../../../routes/validators/__generated__/NodeNames';
|
||||||
|
|
||||||
export const SPECIAL_CASE_NETWORK_ID =
|
export const SPECIAL_CASE_NETWORK_ID =
|
||||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
'0000000000000000000000000000000000000000000000000000000000000000';
|
||||||
export const SPECIAL_CASE_NETWORK = 'network';
|
export const SPECIAL_CASE_NETWORK = 'network';
|
||||||
|
|
||||||
|
export function getNameForParty(id: string, data?: ExplorerNodeNamesQuery) {
|
||||||
|
if (!data || data?.nodesConnection?.edges?.length === 0) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validator = data.nodesConnection.edges?.find((e) => {
|
||||||
|
return e?.node.pubkey === id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validator) {
|
||||||
|
return validator.node.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
|
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
id: string;
|
id: string;
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
||||||
|
const { data } = useExplorerNodeNamesQuery();
|
||||||
|
const name = useMemo(() => getNameForParty(id, data), [data, id]);
|
||||||
|
const useName = name !== id;
|
||||||
|
|
||||||
// Some transactions will involve the 'network' party, which is alias for '000...000'
|
// Some transactions will involve the 'network' party, which is alias for '000...000'
|
||||||
// The party page does not handle this nicely, so in this case we render the word 'Network'
|
// The party page does not handle this nicely, so in this case we render the word 'Network'
|
||||||
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
|
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
|
||||||
@ -38,13 +60,20 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<span className="whitespace-nowrap">
|
||||||
className="underline font-mono"
|
{useName && <Icon size={4} name="cube" className="mr-2" />}
|
||||||
{...props}
|
<Link
|
||||||
to={`/${Routes.PARTIES}/${id}`}
|
className="underline font-mono"
|
||||||
>
|
{...props}
|
||||||
<Hash text={truncate ? truncateMiddle(id) : id} />
|
to={`/${Routes.PARTIES}/${id}`}
|
||||||
</Link>
|
>
|
||||||
|
{useName ? (
|
||||||
|
name
|
||||||
|
) : (
|
||||||
|
<Hash text={truncate ? truncateMiddle(id, 4, 4) : id} />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
// https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go
|
// https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go
|
||||||
export const ErrorCodes = new Map([
|
export const ErrorCodes = new Map([
|
||||||
@ -17,6 +17,8 @@ interface ChainResponseCodeProps {
|
|||||||
code: number;
|
code: number;
|
||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
hideIfOk?: boolean;
|
||||||
|
small?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,14 +30,21 @@ export const ChainResponseCode = ({
|
|||||||
code,
|
code,
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
error,
|
error,
|
||||||
|
hideIfOk = false,
|
||||||
|
small = false,
|
||||||
}: ChainResponseCodeProps) => {
|
}: ChainResponseCodeProps) => {
|
||||||
|
if (hideIfOk && code === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const isSuccess = successCodes.has(code);
|
const isSuccess = successCodes.has(code);
|
||||||
|
const size = small ? 3 : 4;
|
||||||
const successColour =
|
const successColour =
|
||||||
code === 71 ? 'fill-vega-orange' : 'fill-vega-green-600';
|
code === 71 ? '!fill-vega-orange' : '!fill-vega-green-600';
|
||||||
const icon = isSuccess ? (
|
const icon = isSuccess ? (
|
||||||
<Icon name="tick-circle" className={successColour} />
|
<Icon size={size} name="tick-circle" className={`${successColour}`} />
|
||||||
) : (
|
) : (
|
||||||
<Icon name="cross" className="fill-vega-pink-600" />
|
<Icon size={size} name="cross" className="!fill-vega-pink-500" />
|
||||||
);
|
);
|
||||||
const label = ErrorCodes.get(code) || 'Unknown response code';
|
const label = ErrorCodes.get(code) || 'Unknown response code';
|
||||||
|
|
||||||
@ -44,18 +53,28 @@ export const ChainResponseCode = ({
|
|||||||
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
|
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div title={`Response code: ${code} - ${label}`} className=" inline-block">
|
<Tooltip
|
||||||
<span
|
description={
|
||||||
className="mr-2"
|
<span>
|
||||||
aria-label={isSuccess ? 'Success' : 'Warning'}
|
Response code: {code} - {label}
|
||||||
role="img"
|
</span>
|
||||||
>
|
}
|
||||||
{icon}
|
>
|
||||||
</span>
|
<div className="mt-[-1px] inline-block">
|
||||||
{hideLabel ? null : <span>{label}</span>}
|
<span
|
||||||
{!hideLabel && !!displayError ? (
|
className="mr-2"
|
||||||
<span className="ml-1 whitespace-pre">— {displayError}</span>
|
aria-label={isSuccess ? 'Success' : 'Warning'}
|
||||||
) : null}
|
role="img"
|
||||||
</div>
|
>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
{hideLabel ? null : <span>{label}</span>}
|
||||||
|
{!hideLabel && !!displayError ? (
|
||||||
|
<span className="ml-1 whitespace-pre">
|
||||||
|
— {displayError}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,13 +10,17 @@ export interface FilterLabelProps {
|
|||||||
*/
|
*/
|
||||||
export function FilterLabel({ filters }: FilterLabelProps) {
|
export function FilterLabel({ filters }: FilterLabelProps) {
|
||||||
if (!filters || filters.size !== 1) {
|
if (!filters || filters.size !== 1) {
|
||||||
return <span className="uppercase">{t('Filter')}</span>;
|
return (
|
||||||
|
<span data-testid="filter-empty" className="uppercase">
|
||||||
|
{t('Filter')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div data-testid="filter-selected">
|
||||||
<span className="uppercase">{t('Filters')}:</span>
|
<span className="uppercase">{t('Filters')}:</span>
|
||||||
<code className="bg-vega-light-150 px-2 rounded-md capitalize">
|
<code className="bg-vega-light-150 dark:bg-vega-light-300 px-2 rounded-md capitalize dark:text-black">
|
||||||
{Array.from(filters)[0]}
|
{Array.from(filters)[0]}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
|
21
apps/explorer/src/app/components/txs/tx-filter.spec.tsx
Normal file
21
apps/explorer/src/app/components/txs/tx-filter.spec.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { TxsFilter } from './tx-filter';
|
||||||
|
import type { FilterOption } from './tx-filter';
|
||||||
|
|
||||||
|
describe('TxsFilter', () => {
|
||||||
|
it('renders holding text when nothing is selected', () => {
|
||||||
|
const filters: Set<FilterOption> = new Set([]);
|
||||||
|
const setFilters = jest.fn();
|
||||||
|
render(<TxsFilter filters={filters} setFilters={setFilters} />);
|
||||||
|
expect(screen.getByTestId('filter-empty')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Filter')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the submit order filter as selected', () => {
|
||||||
|
const filters: Set<FilterOption> = new Set(['Submit Order']);
|
||||||
|
const setFilters = jest.fn();
|
||||||
|
render(<TxsFilter filters={filters} setFilters={setFilters} />);
|
||||||
|
expect(screen.getByTestId('filter-selected')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Submit Order')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -91,7 +91,7 @@ export interface TxFilterProps {
|
|||||||
* types. It allows a user to select one transaction type to view. Later
|
* 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
|
* it will support multiple selection, but until the API supports that it is
|
||||||
* one or all.
|
* one or all.
|
||||||
* @param filters null or Set of tranaction types
|
* @param filters null or Set of transaction types
|
||||||
* @param setFilters A function to update the filters prop
|
* @param setFilters A function to update the filters prop
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
@ -100,15 +100,15 @@ export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
|
|||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
modal={false}
|
modal={false}
|
||||||
trigger={
|
trigger={
|
||||||
<DropdownMenuTrigger className="ml-2">
|
<DropdownMenuTrigger className="ml-0">
|
||||||
<Button size="xs">
|
<Button size="xs" data-testid="filter-trigger">
|
||||||
<FilterLabel filters={filters} />
|
<FilterLabel filters={filters} />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
{filters.size > 1 ? null : (
|
{filters.size > 0 ? null : (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
|
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
|
||||||
|
113
apps/explorer/src/app/components/txs/tx-list-navigation.spec.tsx
Normal file
113
apps/explorer/src/app/components/txs/tx-list-navigation.spec.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { TxsListNavigation } from './tx-list-navigation';
|
||||||
|
|
||||||
|
const NOOP = () => {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
describe('TxsListNavigation', () => {
|
||||||
|
it('renders transaction list navigation', () => {
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={NOOP}
|
||||||
|
previousPage={NOOP}
|
||||||
|
hasMoreTxs={true}
|
||||||
|
hasPreviousPage={true}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Newer')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Older')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls previousPage when "Newer" button is clicked', () => {
|
||||||
|
const previousPageMock = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={NOOP}
|
||||||
|
previousPage={previousPageMock}
|
||||||
|
hasMoreTxs={true}
|
||||||
|
hasPreviousPage={true}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Newer'));
|
||||||
|
|
||||||
|
expect(previousPageMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls nextPage when "Older" button is clicked', () => {
|
||||||
|
const nextPageMock = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={nextPageMock}
|
||||||
|
previousPage={NOOP}
|
||||||
|
hasMoreTxs={true}
|
||||||
|
hasPreviousPage={true}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Older'));
|
||||||
|
|
||||||
|
expect(nextPageMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables "Older" button if hasMoreTxs is false', () => {
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={NOOP}
|
||||||
|
previousPage={NOOP}
|
||||||
|
hasMoreTxs={false}
|
||||||
|
hasPreviousPage={false}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Older')).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables "Newer" button if hasPreviousPage is false', () => {
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={NOOP}
|
||||||
|
previousPage={NOOP}
|
||||||
|
hasMoreTxs={true}
|
||||||
|
hasPreviousPage={false}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Newer')).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables both buttons when more and previous are false', () => {
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={NOOP}
|
||||||
|
previousPage={NOOP}
|
||||||
|
hasMoreTxs={false}
|
||||||
|
hasPreviousPage={false}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Newer')).toBeDisabled();
|
||||||
|
expect(screen.getByText('Older')).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
63
apps/explorer/src/app/components/txs/tx-list-navigation.tsx
Normal file
63
apps/explorer/src/app/components/txs/tx-list-navigation.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { BlocksRefetch } from '../blocks';
|
||||||
|
|
||||||
|
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
export interface TxListNavigationProps {
|
||||||
|
refreshTxs: () => void;
|
||||||
|
nextPage: () => void;
|
||||||
|
previousPage: () => void;
|
||||||
|
loading?: boolean;
|
||||||
|
hasPreviousPage: boolean;
|
||||||
|
hasMoreTxs: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Displays a list of transactions with filters and controls to navigate through the list.
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Transaction List and controls
|
||||||
|
*/
|
||||||
|
export const TxsListNavigation = ({
|
||||||
|
refreshTxs,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
hasMoreTxs,
|
||||||
|
hasPreviousPage,
|
||||||
|
children,
|
||||||
|
loading = false,
|
||||||
|
}: TxListNavigationProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<menu className="mb-2 w-full ">{children}</menu>
|
||||||
|
<menu className="mb-2 w-full">
|
||||||
|
<BlocksRefetch refetch={refreshTxs} />
|
||||||
|
<div className="float-right">
|
||||||
|
<Button
|
||||||
|
className="mr-2"
|
||||||
|
size="xs"
|
||||||
|
disabled={!hasPreviousPage || loading}
|
||||||
|
onClick={() => {
|
||||||
|
previousPage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Newer')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
disabled={!hasMoreTxs || loading}
|
||||||
|
onClick={() => {
|
||||||
|
nextPage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Older')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="float-right mr-2">
|
||||||
|
{loading ? (
|
||||||
|
<span className="text-vega-light-300">{t('Loading...')}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -24,6 +24,7 @@ const displayString: StringMap = {
|
|||||||
LiquidityProvisionSubmission: 'LP order',
|
LiquidityProvisionSubmission: 'LP order',
|
||||||
'Liquidity Provision Order': 'LP order',
|
'Liquidity Provision Order': 'LP order',
|
||||||
LiquidityProvisionCancellation: 'LP cancel',
|
LiquidityProvisionCancellation: 'LP cancel',
|
||||||
|
'Cancel LiquidityProvision Order': 'LP cancel',
|
||||||
LiquidityProvisionAmendment: 'LP update',
|
LiquidityProvisionAmendment: 'LP update',
|
||||||
'Amend LiquidityProvision Order': 'Amend LP',
|
'Amend LiquidityProvision Order': 'Amend LP',
|
||||||
ProposalSubmission: 'Governance Proposal',
|
ProposalSubmission: 'Governance Proposal',
|
||||||
@ -36,9 +37,12 @@ const displayString: StringMap = {
|
|||||||
UndelegateSubmission: 'Undelegation',
|
UndelegateSubmission: 'Undelegation',
|
||||||
KeyRotateSubmission: 'Key Rotation',
|
KeyRotateSubmission: 'Key Rotation',
|
||||||
StateVariableProposal: 'State Variable',
|
StateVariableProposal: 'State Variable',
|
||||||
|
'State Variable Proposal': 'State Variable',
|
||||||
Transfer: 'Transfer',
|
Transfer: 'Transfer',
|
||||||
CancelTransfer: 'Cancel Transfer',
|
CancelTransfer: 'Cancel Transfer',
|
||||||
|
'Cancel Transfer Funds': 'Cancel Transfer',
|
||||||
ValidatorHeartbeat: 'Heartbeat',
|
ValidatorHeartbeat: 'Heartbeat',
|
||||||
|
'Validator Heartbeat': 'Heartbeat',
|
||||||
'Batch Market Instructions': 'Batch',
|
'Batch Market Instructions': 'Batch',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -172,7 +176,7 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="tx-type"
|
data-testid="tx-type"
|
||||||
className={`text-sm rounded-md leading-none px-2 py-2 inline-block ${colours}`}
|
className={`text-sm rounded-md leading-tight px-2 inline-block whitespace-nowrap ${colours}`}
|
||||||
>
|
>
|
||||||
{type}
|
{type}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
@ -83,21 +84,22 @@ describe('Txs infinite list item', () => {
|
|||||||
|
|
||||||
it('renders data correctly', () => {
|
it('renders data correctly', () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MockedProvider>
|
||||||
<TxsInfiniteListItem
|
<MemoryRouter>
|
||||||
type="testType"
|
<TxsInfiniteListItem
|
||||||
submitter="testPubKey"
|
type="testType"
|
||||||
hash="testTxHash"
|
submitter="testPubKey"
|
||||||
block="1"
|
hash="testTxHash"
|
||||||
code={0}
|
block="1"
|
||||||
command={{}}
|
code={0}
|
||||||
/>
|
command={{}}
|
||||||
</MemoryRouter>
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash');
|
expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash');
|
||||||
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
|
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
|
||||||
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
|
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
|
||||||
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
|
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
|
||||||
expect(screen.getByTestId('tx-success')).toHaveTextContent('Success');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import { TruncatedLink } from '../truncate/truncated-link';
|
import { TruncatedLink } from '../truncate/truncated-link';
|
||||||
import { Routes } from '../../routes/route-names';
|
import { Routes } from '../../routes/route-names';
|
||||||
import { TxOrderType } from './tx-order-type';
|
import { TxOrderType } from './tx-order-type';
|
||||||
@ -6,8 +5,25 @@ import type { BlockExplorerTransactionResult } from '../../routes/types/block-ex
|
|||||||
import { toHex } from '../search/detect-search';
|
import { toHex } from '../search/detect-search';
|
||||||
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
|
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
|
||||||
import isNumber from 'lodash/isNumber';
|
import isNumber from 'lodash/isNumber';
|
||||||
|
import { PartyLink } from '../links';
|
||||||
|
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
|
import type { Screen } from '@vegaprotocol/react-helpers';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
const TRUNCATE_LENGTH = 10;
|
const DEFAULT_TRUNCATE_LENGTH = 7;
|
||||||
|
|
||||||
|
export function getIdTruncateLength(screen: Screen): number {
|
||||||
|
if (['xxxl', 'xxl'].includes(screen)) {
|
||||||
|
return 64;
|
||||||
|
} else if (['xl', 'lg', 'md'].includes(screen)) {
|
||||||
|
return 32;
|
||||||
|
}
|
||||||
|
return DEFAULT_TRUNCATE_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldTruncateParty(screen: Screen): boolean {
|
||||||
|
return !['xxxl', 'xxl', 'xl'].includes(screen);
|
||||||
|
}
|
||||||
|
|
||||||
export const TxsInfiniteListItem = ({
|
export const TxsInfiniteListItem = ({
|
||||||
hash,
|
hash,
|
||||||
@ -17,6 +33,12 @@ export const TxsInfiniteListItem = ({
|
|||||||
block,
|
block,
|
||||||
command,
|
command,
|
||||||
}: Partial<BlockExplorerTransactionResult>) => {
|
}: Partial<BlockExplorerTransactionResult>) => {
|
||||||
|
const { screenSize } = useScreenDimensions();
|
||||||
|
const idTruncateLength = useMemo(
|
||||||
|
() => getIdTruncateLength(screenSize),
|
||||||
|
[screenSize]
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!hash ||
|
!hash ||
|
||||||
!submitter ||
|
!submitter ||
|
||||||
@ -29,68 +51,40 @@ export const TxsInfiniteListItem = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<tr
|
||||||
data-testid="transaction-row"
|
data-testid="transaction-row"
|
||||||
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10"
|
className="transaction-row text-left items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item py-[2px]"
|
||||||
>
|
>
|
||||||
<div
|
<td
|
||||||
className="text-sm col-span-10 md:col-span-3 leading-none"
|
className="text-sm leading-none whitespace-nowrap font-mono"
|
||||||
data-testid="tx-hash"
|
data-testid="tx-hash"
|
||||||
>
|
>
|
||||||
<span className="md:hidden uppercase text-vega-dark-300">
|
|
||||||
ID:
|
|
||||||
</span>
|
|
||||||
<TruncatedLink
|
|
||||||
to={`/${Routes.TX}/${toHex(hash)}`}
|
|
||||||
text={hash}
|
|
||||||
startChars={TRUNCATE_LENGTH}
|
|
||||||
endChars={TRUNCATE_LENGTH}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="text-sm col-span-10 md:col-span-3 leading-none"
|
|
||||||
data-testid="pub-key"
|
|
||||||
>
|
|
||||||
<span className="md:hidden uppercase text-vega-dark-300">
|
|
||||||
By:
|
|
||||||
</span>
|
|
||||||
<TruncatedLink
|
|
||||||
to={`/${Routes.PARTIES}/${submitter}`}
|
|
||||||
text={submitter}
|
|
||||||
startChars={TRUNCATE_LENGTH}
|
|
||||||
endChars={TRUNCATE_LENGTH}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm col-span-5 md:col-span-2 leading-none flex items-center">
|
|
||||||
<TxOrderType orderType={type} command={command} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="text-sm col-span-3 md:col-span-1 leading-none flex items-center"
|
|
||||||
data-testid="tx-block"
|
|
||||||
>
|
|
||||||
<span className="md:hidden uppercase text-vega-dark-300">
|
|
||||||
Block:
|
|
||||||
</span>
|
|
||||||
<TruncatedLink
|
|
||||||
to={`/${Routes.BLOCKS}/${block}`}
|
|
||||||
text={block}
|
|
||||||
startChars={TRUNCATE_LENGTH}
|
|
||||||
endChars={TRUNCATE_LENGTH}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="text-sm col-span-2 md:col-span-1 leading-none flex items-center"
|
|
||||||
data-testid="tx-success"
|
|
||||||
>
|
|
||||||
<span className="md:hidden uppercase text-vega-dark-300">
|
|
||||||
Success
|
|
||||||
</span>
|
|
||||||
{isNumber(code) ? (
|
{isNumber(code) ? (
|
||||||
<ChainResponseCode code={code} hideLabel={true} />
|
<ChainResponseCode code={code} hideLabel={true} hideIfOk={true} />
|
||||||
) : (
|
) : (
|
||||||
code
|
code
|
||||||
)}
|
)}
|
||||||
</div>
|
<TruncatedLink
|
||||||
</div>
|
to={`/${Routes.TX}/${toHex(hash)}`}
|
||||||
|
text={hash}
|
||||||
|
startChars={idTruncateLength}
|
||||||
|
endChars={0}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="text-sm leading-none">
|
||||||
|
<TxOrderType orderType={type} command={command} />
|
||||||
|
</td>
|
||||||
|
<td className="text-sm leading-none" data-testid="pub-key">
|
||||||
|
<PartyLink truncate={shouldTruncateParty(screenSize)} id={submitter} />
|
||||||
|
</td>
|
||||||
|
<td className="text-sm items-center font-mono" data-testid="tx-block">
|
||||||
|
<TruncatedLink
|
||||||
|
to={`/${Routes.BLOCKS}/${block}`}
|
||||||
|
text={block}
|
||||||
|
startChars={5}
|
||||||
|
endChars={5}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { TxsInfiniteList } from './txs-infinite-list';
|
import { TxsInfiniteList } from './txs-infinite-list';
|
||||||
import { render, screen, fireEvent, act } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
||||||
import { Side } from '@vegaprotocol/types';
|
import { Side } from '@vegaprotocol/types';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
|
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
|
||||||
return Array.from(Array(number)).map((_) => ({
|
return Array.from(Array(number)).map((_) => ({
|
||||||
@ -40,7 +41,7 @@ describe('Txs infinite list', () => {
|
|||||||
it('should display a "no items" message when no items provided', () => {
|
it('should display a "no items" message when no items provided', () => {
|
||||||
render(
|
render(
|
||||||
<TxsInfiniteList
|
<TxsInfiniteList
|
||||||
txs={undefined}
|
txs={undefined as unknown as BlockExplorerTransactionResult[]}
|
||||||
areTxsLoading={false}
|
areTxsLoading={false}
|
||||||
hasMoreTxs={false}
|
hasMoreTxs={false}
|
||||||
loadMoreTxs={() => null}
|
loadMoreTxs={() => null}
|
||||||
@ -48,23 +49,7 @@ describe('Txs infinite list', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('emptylist')).toBeInTheDocument();
|
expect(screen.getByTestId('emptylist')).toBeInTheDocument();
|
||||||
expect(
|
expect(screen.getByText('No transactions found')).toBeInTheDocument();
|
||||||
screen.getByText('This chain has 0 transactions')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('error is displayed at item level', () => {
|
|
||||||
const txs = generateTxs(1);
|
|
||||||
render(
|
|
||||||
<TxsInfiniteList
|
|
||||||
txs={txs}
|
|
||||||
areTxsLoading={false}
|
|
||||||
hasMoreTxs={false}
|
|
||||||
loadMoreTxs={() => null}
|
|
||||||
error={Error('test error!')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByText('Cannot fetch transaction')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('item renders data of n length into list of n length', () => {
|
it('item renders data of n length into list of n length', () => {
|
||||||
@ -73,85 +58,22 @@ describe('Txs infinite list', () => {
|
|||||||
const txs = generateTxs(7);
|
const txs = generateTxs(7);
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<TxsInfiniteList
|
<MockedProvider>
|
||||||
txs={txs}
|
<TxsInfiniteList
|
||||||
areTxsLoading={false}
|
txs={txs}
|
||||||
hasMoreTxs={false}
|
areTxsLoading={false}
|
||||||
loadMoreTxs={() => null}
|
hasMoreTxs={false}
|
||||||
error={undefined}
|
loadMoreTxs={() => null}
|
||||||
/>
|
error={undefined}
|
||||||
|
/>
|
||||||
|
</MockedProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen
|
screen
|
||||||
.getByTestId('infinite-scroll-wrapper')
|
.getByTestId('transactions-list')
|
||||||
.querySelectorAll('.txs-infinite-list-item')
|
.querySelectorAll('.transaction-row')
|
||||||
).toHaveLength(7);
|
).toHaveLength(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tries to load more items when required to initially fill the list', () => {
|
|
||||||
// For example, if initially rendering 15, the bottom of the list is
|
|
||||||
// in view of the viewport, and the callback should be executed
|
|
||||||
const txs = generateTxs(15);
|
|
||||||
const callback = jest.fn();
|
|
||||||
|
|
||||||
render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<TxsInfiniteList
|
|
||||||
txs={txs}
|
|
||||||
areTxsLoading={false}
|
|
||||||
hasMoreTxs={true}
|
|
||||||
loadMoreTxs={callback}
|
|
||||||
error={undefined}
|
|
||||||
/>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(callback.mock.calls.length).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not try to load more items if there are no more', () => {
|
|
||||||
const txs = generateTxs(3);
|
|
||||||
const callback = jest.fn();
|
|
||||||
|
|
||||||
render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<TxsInfiniteList
|
|
||||||
txs={txs}
|
|
||||||
areTxsLoading={false}
|
|
||||||
hasMoreTxs={false}
|
|
||||||
loadMoreTxs={callback}
|
|
||||||
error={undefined}
|
|
||||||
/>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(callback.mock.calls.length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads more items is called when scrolled', () => {
|
|
||||||
const txs = generateTxs(14);
|
|
||||||
const callback = jest.fn();
|
|
||||||
|
|
||||||
render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<TxsInfiniteList
|
|
||||||
txs={txs}
|
|
||||||
areTxsLoading={false}
|
|
||||||
hasMoreTxs={true}
|
|
||||||
loadMoreTxs={callback}
|
|
||||||
error={undefined}
|
|
||||||
/>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
fireEvent.scroll(screen.getByTestId('infinite-scroll-wrapper'), {
|
|
||||||
target: { scrollY: 2000 },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(callback.mock.calls.length).toEqual(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import { FixedSizeList as List } from 'react-window';
|
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
|
||||||
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||||
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
||||||
import EmptyList from '../empty-list/empty-list';
|
import EmptyList from '../empty-list/empty-list';
|
||||||
@ -11,82 +7,46 @@ import { Loader } from '@vegaprotocol/ui-toolkit';
|
|||||||
interface TxsInfiniteListProps {
|
interface TxsInfiniteListProps {
|
||||||
hasMoreTxs: boolean;
|
hasMoreTxs: boolean;
|
||||||
areTxsLoading: boolean | undefined;
|
areTxsLoading: boolean | undefined;
|
||||||
txs: BlockExplorerTransactionResult[] | undefined;
|
txs: BlockExplorerTransactionResult[];
|
||||||
loadMoreTxs: () => void;
|
loadMoreTxs: () => void;
|
||||||
error: Error | undefined;
|
error: Error | undefined;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
hasFilters?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ItemProps {
|
interface ItemProps {
|
||||||
index: BlockExplorerTransactionResult;
|
tx: BlockExplorerTransactionResult;
|
||||||
style: React.CSSProperties;
|
|
||||||
isLoading: boolean;
|
|
||||||
error: Error | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
const Item = ({ tx }: ItemProps) => {
|
||||||
const NOOP = () => {};
|
const { hash, submitter, type, command, block, code, index: blockIndex } = tx;
|
||||||
|
return (
|
||||||
const Item = ({ index, style, isLoading, error }: ItemProps) => {
|
<TxsInfiniteListItem
|
||||||
let content;
|
type={type}
|
||||||
if (error) {
|
code={code}
|
||||||
content = t(`Cannot fetch transaction`);
|
command={command}
|
||||||
} else if (isLoading) {
|
submitter={submitter}
|
||||||
content = <Loader />;
|
hash={hash}
|
||||||
} else {
|
block={block}
|
||||||
const {
|
index={blockIndex}
|
||||||
hash,
|
/>
|
||||||
submitter,
|
);
|
||||||
type,
|
|
||||||
command,
|
|
||||||
block,
|
|
||||||
code,
|
|
||||||
index: blockIndex,
|
|
||||||
} = index;
|
|
||||||
content = (
|
|
||||||
<TxsInfiniteListItem
|
|
||||||
type={type}
|
|
||||||
code={code}
|
|
||||||
command={command}
|
|
||||||
submitter={submitter}
|
|
||||||
hash={hash}
|
|
||||||
block={block}
|
|
||||||
index={blockIndex}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div style={style}>{content}</div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TxsInfiniteList = ({
|
export const TxsInfiniteList = ({
|
||||||
hasMoreTxs,
|
|
||||||
areTxsLoading,
|
areTxsLoading,
|
||||||
txs,
|
txs,
|
||||||
loadMoreTxs,
|
|
||||||
error,
|
|
||||||
className,
|
className,
|
||||||
|
hasFilters = false,
|
||||||
}: TxsInfiniteListProps) => {
|
}: TxsInfiniteListProps) => {
|
||||||
const { screenSize } = useScreenDimensions();
|
if (!txs || txs.length === 0) {
|
||||||
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 (!areTxsLoading) {
|
if (!areTxsLoading) {
|
||||||
return (
|
return (
|
||||||
<EmptyList
|
<EmptyList
|
||||||
heading={t('This chain has 0 transactions')}
|
heading={t('No transactions found')}
|
||||||
label={t('Check back soon')}
|
label={
|
||||||
|
hasFilters ? t('Try a different filter') : t('Check back soon')
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -94,57 +54,26 @@ export const TxsInfiniteList = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are more items to be loaded then add an extra row to hold a loading indicator.
|
|
||||||
const itemCount = hasMoreTxs ? txs.length + 1 : txs.length;
|
|
||||||
|
|
||||||
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
const loadMoreItems = areTxsLoading ? NOOP : loadMoreTxs;
|
|
||||||
|
|
||||||
// Every row is loaded except for our loading indicator row.
|
|
||||||
const isItemLoaded = (index: number) => !hasMoreTxs || index < txs.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} data-testid="transactions-list">
|
<div className="overflow-scroll">
|
||||||
<div className="lg:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
|
<table className={className} data-testid="transactions-list">
|
||||||
<div className="col-span-3">
|
<thead>
|
||||||
<span className="hidden xl:inline">{t('Transaction')} </span>
|
<tr className="w-full mb-3 text-vega-dark-300 uppercase text-left">
|
||||||
<span>ID</span>
|
<th>
|
||||||
</div>
|
<span className="hidden xl:inline">{t('Txn')} </span>
|
||||||
<div className="col-span-3">{t('Submitted By')}</div>
|
<span>ID</span>
|
||||||
<div className="col-span-2">{t('Type')}</div>
|
</th>
|
||||||
<div className="col-span-1">{t('Block')}</div>
|
<th>{t('Type')}</th>
|
||||||
<div className="col-span-1">{t('Success')}</div>
|
<th className="text-left">{t('From')}</th>
|
||||||
</div>
|
<th>{t('Block')}</th>
|
||||||
<div data-testid="infinite-scroll-wrapper">
|
</tr>
|
||||||
<InfiniteLoader
|
</thead>
|
||||||
isItemLoaded={isItemLoaded}
|
<tbody>
|
||||||
itemCount={itemCount}
|
{txs.map((t) => (
|
||||||
loadMoreItems={loadMoreItems}
|
<Item key={t.hash} tx={t} />
|
||||||
ref={infiniteLoaderRef}
|
))}
|
||||||
>
|
</tbody>
|
||||||
{({ onItemsRendered, ref }) => (
|
</table>
|
||||||
<List
|
|
||||||
className="List"
|
|
||||||
height={995}
|
|
||||||
itemCount={itemCount}
|
|
||||||
itemSize={isStacked ? 134 : 50}
|
|
||||||
onItemsRendered={onItemsRendered}
|
|
||||||
ref={ref}
|
|
||||||
width={'100%'}
|
|
||||||
>
|
|
||||||
{({ index, style }) => (
|
|
||||||
<Item
|
|
||||||
index={txs[index]}
|
|
||||||
style={style}
|
|
||||||
isLoading={!isItemLoaded(index)}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
</InfiniteLoader>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
import { Routes } from '../../routes/route-names';
|
import { Table, TableRow } from '../table';
|
||||||
import { TruncatedLink } from '../truncate/truncated-link';
|
|
||||||
import { TxOrderType } from './tx-order-type';
|
|
||||||
import { Table, TableRow, TableCell } from '../table';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||||
import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response';
|
import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response';
|
||||||
import isNumber from 'lodash/isNumber';
|
|
||||||
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
|
|
||||||
import { getTxsDataUrl } from '../../hooks/use-txs-data';
|
import { getTxsDataUrl } from '../../hooks/use-txs-data';
|
||||||
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
|
||||||
import EmptyList from '../empty-list/empty-list';
|
import EmptyList from '../empty-list/empty-list';
|
||||||
|
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||||
|
|
||||||
interface TxsPerBlockProps {
|
interface TxsPerBlockProps {
|
||||||
blockHeight: string;
|
blockHeight: string;
|
||||||
txCount: number;
|
txCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const truncateLength = 5;
|
|
||||||
|
|
||||||
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
||||||
const filters = `filters[block.height]=${blockHeight}`;
|
const filters = `filters[block.height]=${blockHeight}`;
|
||||||
const url = getTxsDataUrl({ limit: txCount.toString(), filters });
|
const url = getTxsDataUrl({ limit: txCount.toString(), filters });
|
||||||
@ -33,53 +27,23 @@ export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
|||||||
<thead>
|
<thead>
|
||||||
<TableRow modifier="bordered" className="font-mono">
|
<TableRow modifier="bordered" className="font-mono">
|
||||||
<td>{t('Transaction')}</td>
|
<td>{t('Transaction')}</td>
|
||||||
<td>{t('From')}</td>
|
|
||||||
<td>{t('Type')}</td>
|
<td>{t('Type')}</td>
|
||||||
<td>{t('Status')}</td>
|
<td>{t('From')}</td>
|
||||||
|
<td>{t('Block')}</td>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.transactions.map(
|
{data.transactions.map(
|
||||||
({ hash, submitter, type, command, code }) => {
|
({ hash, submitter, type, command, code, block }) => {
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TxsInfiniteListItem
|
||||||
modifier="bordered"
|
block={block}
|
||||||
key={hash}
|
hash={hash}
|
||||||
data-testid="transaction-row"
|
submitter={submitter}
|
||||||
>
|
type={type}
|
||||||
<TableCell
|
command={command}
|
||||||
modifier="bordered"
|
code={code}
|
||||||
className="pr-12 font-mono"
|
/>
|
||||||
>
|
|
||||||
<TruncatedLink
|
|
||||||
to={`/${Routes.TX}/${hash}`}
|
|
||||||
text={hash}
|
|
||||||
startChars={truncateLength}
|
|
||||||
endChars={truncateLength}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
modifier="bordered"
|
|
||||||
className="pr-12 font-mono"
|
|
||||||
>
|
|
||||||
<TruncatedLink
|
|
||||||
to={`/${Routes.PARTIES}/${submitter}`}
|
|
||||||
text={submitter}
|
|
||||||
startChars={truncateLength}
|
|
||||||
endChars={truncateLength}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell modifier="bordered">
|
|
||||||
<TxOrderType orderType={type} command={command} />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell modifier="bordered" className="text">
|
|
||||||
{isNumber(code) ? (
|
|
||||||
<ChainResponseCode code={code} hideLabel={true} />
|
|
||||||
) : (
|
|
||||||
code
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
@ -56,10 +56,14 @@ export function VoteIcon({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`voteicon inline-block my-1 py-1 px-2 py rounded-md text-white leading-one sm align-top ${bg}`}
|
className={`voteicon inline-block py-0 px-2 py rounded-md text-white whitespace-nowrap leading-tight sm align-top ${bg}`}
|
||||||
>
|
>
|
||||||
<Icon name={icon} size={3} className={`mr-2 p-0 fill-${fill}`} />
|
<Icon
|
||||||
<span className={`text-base text-${text}`} data-testid="label">
|
name={icon}
|
||||||
|
size={3}
|
||||||
|
className={`mr-2 p-0 mb-[-1px] fill-${fill}`}
|
||||||
|
/>
|
||||||
|
<span className={`text-${text}`} data-testid="label">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,16 +10,18 @@ import isNumber from 'lodash/isNumber';
|
|||||||
export interface TxsStateProps {
|
export interface TxsStateProps {
|
||||||
txsData: BlockExplorerTransactionResult[];
|
txsData: BlockExplorerTransactionResult[];
|
||||||
hasMoreTxs: boolean;
|
hasMoreTxs: boolean;
|
||||||
lastCursor: string;
|
cursor: string;
|
||||||
|
previousCursors: string[];
|
||||||
|
hasPreviousPage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUseTxsData {
|
export interface IUseTxsData {
|
||||||
limit?: number;
|
limit: number;
|
||||||
filters?: string;
|
filters?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGetTxsDataUrl {
|
interface IGetTxsDataUrl {
|
||||||
limit?: string;
|
limit: string;
|
||||||
filters?: string;
|
filters?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,63 +42,89 @@ export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useTxsData = ({ limit, filters }: IUseTxsData) => {
|
export const useTxsData = ({ limit, filters }: IUseTxsData) => {
|
||||||
const [{ txsData, hasMoreTxs, lastCursor }, setTxsState] =
|
const [
|
||||||
useState<TxsStateProps>({
|
{ txsData, hasMoreTxs, cursor, previousCursors, hasPreviousPage },
|
||||||
txsData: [],
|
setTxsState,
|
||||||
hasMoreTxs: true,
|
] = useState<TxsStateProps>({
|
||||||
lastCursor: '',
|
txsData: [],
|
||||||
});
|
hasMoreTxs: false,
|
||||||
|
previousCursors: [],
|
||||||
|
cursor: '',
|
||||||
|
hasPreviousPage: false,
|
||||||
|
});
|
||||||
|
|
||||||
const url = getTxsDataUrl({ limit: limit?.toString(), filters });
|
const url = getTxsDataUrl({ limit: limit.toString(), filters });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: { data, error, loading },
|
state: { data, error, loading },
|
||||||
refetch,
|
refetch,
|
||||||
} = useFetch<BlockExplorerTransactions>(url, {}, false);
|
} = useFetch<BlockExplorerTransactions>(url, {}, true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && isNumber(data?.transactions?.length)) {
|
if (!loading && data && isNumber(data.transactions.length)) {
|
||||||
setTxsState((prev) => ({
|
setTxsState((prev) => {
|
||||||
txsData: [...prev.txsData, ...data.transactions],
|
return {
|
||||||
hasMoreTxs: data.transactions.length > 0,
|
...prev,
|
||||||
lastCursor:
|
txsData: data.transactions,
|
||||||
data.transactions[data.transactions.length - 1]?.cursor || '',
|
hasMoreTxs: data.transactions.length >= limit,
|
||||||
}));
|
cursor: data?.transactions.at(-1)?.cursor || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [setTxsState, data]);
|
}, [loading, setTxsState, data, limit]);
|
||||||
|
|
||||||
useEffect(() => {
|
const nextPage = useCallback(() => {
|
||||||
setTxsState((prev) => ({
|
const c = data?.transactions.at(0)?.cursor;
|
||||||
txsData: [],
|
const newPreviousCursors = c ? [...previousCursors, c] : previousCursors;
|
||||||
hasMoreTxs: true,
|
|
||||||
lastCursor: '',
|
|
||||||
}));
|
|
||||||
}, [filters]);
|
|
||||||
|
|
||||||
const loadTxs = useCallback(() => {
|
|
||||||
return refetch({
|
|
||||||
limit: limit,
|
|
||||||
before: lastCursor,
|
|
||||||
});
|
|
||||||
}, [lastCursor, limit, refetch]);
|
|
||||||
|
|
||||||
const refreshTxs = useCallback(async () => {
|
|
||||||
setTxsState((prev) => ({
|
setTxsState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
lastCursor: '',
|
hasPreviousPage: true,
|
||||||
hasMoreTxs: true,
|
previousCursors: newPreviousCursors,
|
||||||
txsData: [],
|
|
||||||
}));
|
}));
|
||||||
}, [setTxsState]);
|
|
||||||
|
return refetch({
|
||||||
|
limit,
|
||||||
|
before: cursor,
|
||||||
|
});
|
||||||
|
}, [data, previousCursors, cursor, limit, refetch]);
|
||||||
|
|
||||||
|
const previousPage = useCallback(() => {
|
||||||
|
const previousCursor = [...previousCursors].pop();
|
||||||
|
const newPreviousCursors = previousCursors.slice(0, -1);
|
||||||
|
setTxsState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
hasPreviousPage: newPreviousCursors.length > 0,
|
||||||
|
previousCursors: newPreviousCursors,
|
||||||
|
}));
|
||||||
|
return refetch({
|
||||||
|
limit,
|
||||||
|
before: previousCursor,
|
||||||
|
});
|
||||||
|
}, [previousCursors, limit, refetch]);
|
||||||
|
|
||||||
|
const refreshTxs = useCallback(async () => {
|
||||||
|
setTxsState(() => ({
|
||||||
|
txsData: [],
|
||||||
|
cursor: '',
|
||||||
|
previousCursors: [],
|
||||||
|
hasMoreTxs: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
refetch({ limit });
|
||||||
|
}, [setTxsState, limit, refetch, filters]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
txsData,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
txsData,
|
|
||||||
hasMoreTxs,
|
hasMoreTxs,
|
||||||
lastCursor,
|
hasPreviousPage,
|
||||||
|
previousCursors,
|
||||||
|
cursor,
|
||||||
refreshTxs,
|
refreshTxs,
|
||||||
loadTxs,
|
nextPage,
|
||||||
|
previousPage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
import { Footer } from '../components/footer/footer';
|
import { Footer } from '../components/footer/footer';
|
||||||
import { Header } from '../components/header';
|
import { Header } from '../components/header';
|
||||||
import { Routes } from './route-names';
|
import { Routes } from './route-names';
|
||||||
|
import { useExplorerNodeNamesLazyQuery } from './validators/__generated__/NodeNames';
|
||||||
|
|
||||||
const DialogsContainer = () => {
|
const DialogsContainer = () => {
|
||||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
||||||
@ -39,6 +40,7 @@ export const Layout = () => {
|
|||||||
const isHome = Boolean(useMatch(Routes.HOME));
|
const isHome = Boolean(useMatch(Routes.HOME));
|
||||||
const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
|
const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
|
||||||
const fixedWidthClasses = 'w-full max-w-[1500px] mx-auto';
|
const fixedWidthClasses = 'w-full max-w-[1500px] mx-auto';
|
||||||
|
useExplorerNodeNamesLazyQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -49,7 +51,7 @@ export const Layout = () => {
|
|||||||
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
|
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
|
||||||
'border-vega-light-200 dark:border-vega-dark-200',
|
'border-vega-light-200 dark:border-vega-dark-200',
|
||||||
'antialiased text-black dark:text-white',
|
'antialiased text-black dark:text-white',
|
||||||
'overflow-hidden relative'
|
'relative'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
@ -2,12 +2,15 @@ import { render } from '@testing-library/react';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import type { SourceType } from './oracle';
|
import type { SourceType } from './oracle';
|
||||||
import { OracleSigners } from './oracle-signers';
|
import { OracleSigners } from './oracle-signers';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
function renderComponent(sourceType: SourceType) {
|
function renderComponent(sourceType: SourceType) {
|
||||||
return (
|
return (
|
||||||
<MemoryRouter>
|
<MockedProvider>
|
||||||
<OracleSigners sourceType={sourceType} />
|
<MemoryRouter>
|
||||||
</MemoryRouter>
|
<OracleSigners sourceType={sourceType} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { SubHeading } from '../../../components/sub-heading';
|
import { SubHeading } from '../../../components/sub-heading';
|
||||||
import { toNonHex } from '../../../components/search/detect-search';
|
import { toNonHex } from '../../../components/search/detect-search';
|
||||||
@ -14,8 +14,11 @@ import { PartyBlockStake } from './components/party-block-stake';
|
|||||||
import { PartyBlockAccounts } from './components/party-block-accounts';
|
import { PartyBlockAccounts } from './components/party-block-accounts';
|
||||||
import { isValidPartyId } from './components/party-id-error';
|
import { isValidPartyId } from './components/party-id-error';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
|
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
||||||
|
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
||||||
|
|
||||||
const Party = () => {
|
const Party = () => {
|
||||||
|
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
||||||
const { party } = useParams<{ party: string }>();
|
const { party } = useParams<{ party: string }>();
|
||||||
|
|
||||||
useDocumentTitle(['Public keys', party || '-']);
|
useDocumentTitle(['Public keys', party || '-']);
|
||||||
@ -24,10 +27,24 @@ const Party = () => {
|
|||||||
const partyId = toNonHex(party ? party : '');
|
const partyId = toNonHex(party ? party : '');
|
||||||
const { isMobile } = useScreenDimensions();
|
const { isMobile } = useScreenDimensions();
|
||||||
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
|
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
|
||||||
const filters = `filters[tx.submitter]=${partyId}`;
|
const baseFilters = `filters[tx.submitter]=${partyId}`;
|
||||||
const { hasMoreTxs, loadTxs, error, txsData, loading } = useTxsData({
|
const f =
|
||||||
limit: 10,
|
filters && filters.size === 1
|
||||||
filters,
|
? `${baseFilters}&filters[cmd.type]=${Array.from(filters)[0]}`
|
||||||
|
: baseFilters;
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasMoreTxs,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
error,
|
||||||
|
refreshTxs,
|
||||||
|
loading,
|
||||||
|
txsData,
|
||||||
|
hasPreviousPage,
|
||||||
|
} = useTxsData({
|
||||||
|
limit: 25,
|
||||||
|
filters: f,
|
||||||
});
|
});
|
||||||
|
|
||||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
@ -81,14 +98,24 @@ const Party = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SubHeading>{t('Transactions')}</SubHeading>
|
<SubHeading>{t('Transactions')}</SubHeading>
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={refreshTxs}
|
||||||
|
nextPage={nextPage}
|
||||||
|
previousPage={previousPage}
|
||||||
|
hasPreviousPage={hasPreviousPage}
|
||||||
|
loading={loading}
|
||||||
|
hasMoreTxs={hasMoreTxs}
|
||||||
|
>
|
||||||
|
<TxsFilter filters={filters} setFilters={setFilters} />
|
||||||
|
</TxsListNavigation>
|
||||||
{!error && txsData ? (
|
{!error && txsData ? (
|
||||||
<TxsInfiniteList
|
<TxsInfiniteList
|
||||||
hasMoreTxs={hasMoreTxs}
|
hasMoreTxs={hasMoreTxs}
|
||||||
areTxsLoading={loading}
|
areTxsLoading={loading}
|
||||||
txs={txsData}
|
txs={txsData}
|
||||||
loadMoreTxs={loadTxs}
|
loadMoreTxs={nextPage}
|
||||||
error={error}
|
error={error}
|
||||||
className="mb-28"
|
className="mb-28 w-full"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Splash>
|
<Splash>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { RouteTitle } from '../../../components/route-title';
|
import { RouteTitle } from '../../../components/route-title';
|
||||||
import { BlocksRefetch } from '../../../components/blocks';
|
|
||||||
import { TxsInfiniteList } from '../../../components/txs';
|
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';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
||||||
|
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
||||||
|
|
||||||
const BE_TXS_PER_REQUEST = 15;
|
const BE_TXS_PER_REQUEST = 25;
|
||||||
|
|
||||||
export const TxsList = () => {
|
export const TxsList = () => {
|
||||||
useDocumentTitle(['Transactions']);
|
useDocumentTitle(['Transactions']);
|
||||||
@ -21,6 +21,11 @@ export const TxsList = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a list of transactions with filters and controls to navigate through the list.
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Transaction List and controls
|
||||||
|
*/
|
||||||
export const TxsListFiltered = () => {
|
export const TxsListFiltered = () => {
|
||||||
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
||||||
|
|
||||||
@ -29,26 +34,40 @@ export const TxsListFiltered = () => {
|
|||||||
? `filters[cmd.type]=${Array.from(filters)[0]}`
|
? `filters[cmd.type]=${Array.from(filters)[0]}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
|
const {
|
||||||
useTxsData({
|
hasMoreTxs,
|
||||||
limit: BE_TXS_PER_REQUEST,
|
nextPage,
|
||||||
filters: f,
|
previousPage,
|
||||||
});
|
error,
|
||||||
|
refreshTxs,
|
||||||
|
loading,
|
||||||
|
txsData,
|
||||||
|
hasPreviousPage,
|
||||||
|
} = useTxsData({
|
||||||
|
limit: BE_TXS_PER_REQUEST,
|
||||||
|
filters: f,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<menu className="mb-2">
|
<TxsListNavigation
|
||||||
<BlocksRefetch refetch={refreshTxs} />
|
refreshTxs={refreshTxs}
|
||||||
|
nextPage={nextPage}
|
||||||
|
previousPage={previousPage}
|
||||||
|
hasPreviousPage={hasPreviousPage}
|
||||||
|
loading={loading}
|
||||||
|
hasMoreTxs={hasMoreTxs}
|
||||||
|
>
|
||||||
<TxsFilter filters={filters} setFilters={setFilters} />
|
<TxsFilter filters={filters} setFilters={setFilters} />
|
||||||
</menu>
|
</TxsListNavigation>
|
||||||
|
|
||||||
<TxsInfiniteList
|
<TxsInfiniteList
|
||||||
|
hasFilters={filters.size > 0}
|
||||||
hasMoreTxs={hasMoreTxs}
|
hasMoreTxs={hasMoreTxs}
|
||||||
areTxsLoading={loading}
|
areTxsLoading={loading}
|
||||||
txs={txsData}
|
txs={txsData}
|
||||||
loadMoreTxs={loadTxs}
|
loadMoreTxs={nextPage}
|
||||||
error={error}
|
error={error}
|
||||||
className="mb-28"
|
className="mb-28 w-full min-w-[400px]"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { TxDetails } from './tx-details';
|
import { TxDetails } from './tx-details';
|
||||||
import type {
|
import type {
|
||||||
BlockExplorerTransactionResult,
|
BlockExplorerTransactionResult,
|
||||||
ValidatorHeartbeat,
|
ValidatorHeartbeat,
|
||||||
} from '../../../routes/types/block-explorer-response';
|
} from '../../../routes/types/block-explorer-response';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
// Note: Long enough that there is a truncated output and a full output
|
// Note: Long enough that there is a truncated output and a full output
|
||||||
const pubKey =
|
const pubKey =
|
||||||
@ -27,9 +28,11 @@ const txData: BlockExplorerTransactionResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderComponent = (txData: BlockExplorerTransactionResult) => (
|
const renderComponent = (txData: BlockExplorerTransactionResult) => (
|
||||||
<Router>
|
<MemoryRouter>
|
||||||
<TxDetails txData={txData} pubKey={pubKey} />
|
<MockedProvider>
|
||||||
</Router>
|
<TxDetails txData={txData} pubKey={pubKey} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('Transaction details', () => {
|
describe('Transaction details', () => {
|
||||||
|
13
apps/explorer/src/app/routes/validators/NodeNames.graphql
Normal file
13
apps/explorer/src/app/routes/validators/NodeNames.graphql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
query ExplorerNodeNames {
|
||||||
|
nodesConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
pubkey
|
||||||
|
tmPubkey
|
||||||
|
ethereumAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
apps/explorer/src/app/routes/validators/__generated__/NodeNames.ts
generated
Normal file
53
apps/explorer/src/app/routes/validators/__generated__/NodeNames.ts
generated
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
export type ExplorerNodeNamesQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type ExplorerNodeNamesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, pubkey: string, tmPubkey: string, ethereumAddress: string } } | null> | null } };
|
||||||
|
|
||||||
|
|
||||||
|
export const ExplorerNodeNamesDocument = gql`
|
||||||
|
query ExplorerNodeNames {
|
||||||
|
nodesConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
pubkey
|
||||||
|
tmPubkey
|
||||||
|
ethereumAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useExplorerNodeNamesQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useExplorerNodeNamesQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useExplorerNodeNamesQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useExplorerNodeNamesQuery({
|
||||||
|
* variables: {
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useExplorerNodeNamesQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>(ExplorerNodeNamesDocument, options);
|
||||||
|
}
|
||||||
|
export function useExplorerNodeNamesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>(ExplorerNodeNamesDocument, options);
|
||||||
|
}
|
||||||
|
export type ExplorerNodeNamesQueryHookResult = ReturnType<typeof useExplorerNodeNamesQuery>;
|
||||||
|
export type ExplorerNodeNamesLazyQueryHookResult = ReturnType<typeof useExplorerNodeNamesLazyQuery>;
|
||||||
|
export type ExplorerNodeNamesQueryResult = Apollo.QueryResult<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>;
|
@ -1,12 +1,12 @@
|
|||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|
||||||
export function truncateMiddle(address: string) {
|
export function truncateMiddle(address: string, start = 6, end = 4) {
|
||||||
if (address.length < 11) return address;
|
if (address.length < 11) return address;
|
||||||
return (
|
return (
|
||||||
address.slice(0, 6) +
|
address.slice(0, start) +
|
||||||
'\u2026' +
|
'\u2026' +
|
||||||
address.slice(address.length - 4, address.length)
|
address.slice(address.length - end, address.length)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,8 @@ describe('truncateByChars', () => {
|
|||||||
},
|
},
|
||||||
{ i: '12345678901234567890', s: 0, e: 10, o: `${ELLIPSIS}1234567890` },
|
{ i: '12345678901234567890', s: 0, e: 10, o: `${ELLIPSIS}1234567890` },
|
||||||
{ i: '123', s: 0, e: 4, o: '123' },
|
{ i: '123', s: 0, e: 4, o: '123' },
|
||||||
])('should truncate given string by specific chars', ({ i, s, e, o }) => {
|
{ i: '12345678901234567890', s: 3, e: 0, o: `123${ELLIPSIS}` },
|
||||||
|
])('should truncate given string by specific chars: %s', ({ i, s, e, o }) => {
|
||||||
expect(truncateByChars(i, s, e)).toStrictEqual(o);
|
expect(truncateByChars(i, s, e)).toStrictEqual(o);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -28,7 +29,7 @@ describe('shorten', () => {
|
|||||||
{ i: '12345678901234567890', l: 10, o: `123456789${ELLIPSIS}` },
|
{ i: '12345678901234567890', l: 10, o: `123456789${ELLIPSIS}` },
|
||||||
{ i: '12345678901234567890', l: 20, o: `1234567890123456789${ELLIPSIS}` },
|
{ i: '12345678901234567890', l: 20, o: `1234567890123456789${ELLIPSIS}` },
|
||||||
{ i: '12345678901234567890', l: 30, o: `12345678901234567890` },
|
{ i: '12345678901234567890', l: 30, o: `12345678901234567890` },
|
||||||
])('should shorten given string by specific limit', ({ i, l, o }) => {
|
])('should shorten given string by specific limit: %s', ({ i, l, o }) => {
|
||||||
const output = shorten(i, l);
|
const output = shorten(i, l);
|
||||||
expect(output).toStrictEqual(o);
|
expect(output).toStrictEqual(o);
|
||||||
});
|
});
|
||||||
@ -51,7 +52,7 @@ describe('titlefy', () => {
|
|||||||
words: ['VEGAUSD', '123.22'],
|
words: ['VEGAUSD', '123.22'],
|
||||||
o: 'VEGAUSD - 123.22 - Vega',
|
o: 'VEGAUSD - 123.22 - Vega',
|
||||||
},
|
},
|
||||||
])('should convert to title-like string', ({ words, o }) => {
|
])('should convert to title-like string: %s', ({ words, o }) => {
|
||||||
expect(titlefy(words)).toEqual(o);
|
expect(titlefy(words)).toEqual(o);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,7 +6,10 @@ export function truncateByChars(input: string, start = 6, end = 6) {
|
|||||||
if (input.length <= start + end + 1) {
|
if (input.length <= start + end + 1) {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
return input.slice(0, start) + ELLIPSIS + input.slice(-end);
|
|
||||||
|
const s = input.slice(0, start);
|
||||||
|
const e = end !== 0 ? input.slice(-end) : '';
|
||||||
|
return `${s}${ELLIPSIS}${e}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shorten(input: string, limit?: number) {
|
export function shorten(input: string, limit?: number) {
|
||||||
|
Loading…
Reference in New Issue
Block a user