Compare commits

...

10 Commits

Author SHA1 Message Date
Matthew Russell
85f7e791ba
chore: update mock query result 2023-02-07 12:37:44 -08:00
Matthew Russell
f3a369a146
chore: mock nodeswitcher, remove block diff from tests 2023-02-07 12:37:44 -08:00
Matthew Russell
a65941a5d3
chore: update tests to accomodate useNodeHealth changes 2023-02-07 12:37:43 -08:00
Matthew Russell
8cf627aed4
chore: update footer tests to account for changes to useNodeHealth 2023-02-07 12:37:43 -08:00
Matthew Russell
4cae1c7677
fix: layout row 2023-02-07 12:37:43 -08:00
Matthew Russell
ab9574761c
chore: lift block height state for easier testing 2023-02-07 12:37:43 -08:00
Matthew Russell
58d0174934
chore: rename fromISONano to be more explicit 2023-02-07 12:37:43 -08:00
Matthew Russell
eef562762a
chore: only show node block height in node switcher table 2023-02-07 12:37:43 -08:00
Matthew Russell
4102c5f653
fix: update header store to contain header results for all nodes 2023-02-07 12:37:43 -08:00
Matthew Russell
bb4555c553
fix: get block height from header and compare with core block height 2023-02-07 12:37:42 -08:00
18 changed files with 240 additions and 216 deletions

View File

@ -1,6 +1,6 @@
import {
addDecimal,
fromNanoSeconds,
fromISONano,
t,
useThemeSwitcher,
} from '@vegaprotocol/react-helpers';
@ -228,7 +228,7 @@ export const AccountHistoryChart = ({
.reduce((acc, edge) => {
if (edge.node.accountType === accountType) {
acc?.push({
datetime: fromNanoSeconds(edge.node.timestamp),
datetime: fromISONano(edge.node.timestamp),
balance: Number(addDecimal(edge.node.balance, asset.decimals)),
});
}

View File

@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { Footer, NodeHealth } from './footer';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
jest.mock('@vegaprotocol/environment');
@ -12,10 +12,14 @@ describe('Footer', () => {
// @ts-ignore mock env hook
useEnvironment.mockImplementation(() => ({
VEGA_URL: `https://api.${node}/graphql`,
blockDifference: 0,
setNodeSwitcherOpen: mockOpenNodeSwitcher,
}));
// @ts-ignore mock env hook
useNodeHealth.mockImplementation(() => ({
blockDiff: 0,
}));
render(<Footer />);
fireEvent.click(screen.getByText(node));
@ -29,10 +33,14 @@ describe('Footer', () => {
// @ts-ignore mock env hook
useEnvironment.mockImplementation(() => ({
VEGA_URL: `https://api.${node}/graphql`,
blockDifference: 0,
setNodeSwitcherOpen: mockOpenNodeSwitcher,
}));
// @ts-ignore mock env hook
useNodeHealth.mockImplementation(() => ({
blockDiff: 0,
}));
render(<Footer />);
fireEvent.click(screen.getByText('Operational'));
@ -43,8 +51,9 @@ describe('Footer', () => {
describe('NodeHealth', () => {
const cases = [
{ diff: 0, classname: 'bg-success', text: 'Operational' },
{ diff: -1, classname: 'bg-success', text: 'Operational' },
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
{ diff: -1, classname: 'bg-danger', text: 'Non operational' },
{ diff: null, classname: 'bg-danger', text: 'Non operational' },
];
it.each(cases)(
'renders correct text and indicator color for $diff block difference',

View File

@ -1,9 +1,10 @@
import { useEnvironment } from '@vegaprotocol/environment';
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
import { t, useNavigatorOnline } from '@vegaprotocol/react-helpers';
import { ButtonLink, Indicator, Intent } from '@vegaprotocol/ui-toolkit';
export const Footer = () => {
const { VEGA_URL, blockDifference, setNodeSwitcherOpen } = useEnvironment();
const { VEGA_URL, setNodeSwitcherOpen } = useEnvironment();
const { blockDiff } = useNodeHealth();
return (
<footer className="px-4 py-1 text-xs border-t border-default">
<div className="flex justify-between">
@ -11,10 +12,9 @@ export const Footer = () => {
{VEGA_URL && (
<>
<NodeHealth
blockDiff={blockDifference}
blockDiff={blockDiff}
openNodeSwitcher={setNodeSwitcherOpen}
/>
{' | '}
<NodeUrl url={VEGA_URL} openNodeSwitcher={setNodeSwitcherOpen} />
</>
)}
@ -37,8 +37,8 @@ const NodeUrl = ({ url, openNodeSwitcher }: NodeUrlProps) => {
};
interface NodeHealthProps {
blockDiff: number | null;
openNodeSwitcher: () => void;
blockDiff: number;
}
// How many blocks behind the most advanced block that is
@ -57,7 +57,7 @@ export const NodeHealth = ({
if (!online) {
text = t('Offline');
intent = Intent.Danger;
} else if (blockDiff < 0) {
} else if (blockDiff === null) {
// Block height query failed and null was returned
text = t('Non operational');
intent = Intent.Danger;
@ -67,9 +67,9 @@ export const NodeHealth = ({
}
return (
<span>
<>
<Indicator variant={intent} />
<ButtonLink onClick={openNodeSwitcher}>{text}</ButtonLink>
</span>
</>
);
};

View File

@ -1 +1,2 @@
export * from './lib/apollo-client';
export * from './lib/header-store';

View File

@ -13,7 +13,11 @@ import { createClient as createWSClient } from 'graphql-ws';
import { onError } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
import ApolloLinkTimeout from 'apollo-link-timeout';
import { localLoggerFactory } from '@vegaprotocol/react-helpers';
import {
fromNanoSeconds,
localLoggerFactory,
} from '@vegaprotocol/react-helpers';
import { useHeaderStore } from './header-store';
const isBrowser = typeof window !== 'undefined';
@ -24,6 +28,7 @@ export type ClientOptions = {
cacheConfig?: InMemoryCacheConfig;
retry?: boolean;
connectToDevTools?: boolean;
connectToHeaderStore?: boolean;
};
export function createClient({
@ -31,6 +36,7 @@ export function createClient({
cacheConfig,
retry = true,
connectToDevTools = true,
connectToHeaderStore = true,
}: ClientOptions) {
if (!url) {
throw new Error('url must be passed into createClient!');
@ -44,6 +50,28 @@ export function createClient({
return forward(operation);
});
const headerLink = connectToHeaderStore
? new ApolloLink((operation, forward) => {
return forward(operation).map((response) => {
const context = operation.getContext();
const headers = context['response'].headers;
const blockHeight = headers.get('x-block-height');
const timestamp = headers.get('x-block-timestamp');
if (blockHeight && timestamp) {
const state = useHeaderStore.getState();
useHeaderStore.setState({
...state,
[context['response'].url]: {
blockHeight: Number(blockHeight),
timestamp: fromNanoSeconds(timestamp),
},
});
}
return response;
});
})
: noOpLink;
const timeoutLink = new ApolloLinkTimeout(10000);
const enlargedTimeoutLink = new ApolloLinkTimeout(100000);
@ -104,7 +132,13 @@ export function createClient({
);
return new ApolloClient({
link: from([errorLink, composedTimeoutLink, retryLink, splitLink]),
link: from([
errorLink,
composedTimeoutLink,
headerLink,
retryLink,
splitLink,
]),
cache: new InMemoryCache(cacheConfig),
connectToDevTools,
});

View File

@ -0,0 +1,10 @@
import { create } from 'zustand';
type HeaderStore = {
[url: string]: {
blockHeight: number;
timestamp: Date;
};
};
export const useHeaderStore = create<HeaderStore>(() => ({}));

View File

@ -13,6 +13,10 @@ type NodeStatsContentProps = {
setBlock: (value: number) => void;
children?: ReactNode;
dataTestId?: string;
headers?: {
blockHeight: number;
timestamp: Date;
};
};
const getResponseTimeDisplayValue = (
@ -28,13 +32,13 @@ const getResponseTimeDisplayValue = (
};
const getBlockDisplayValue = (
block: NodeData['block'] | undefined,
block: number | undefined,
setBlock: (block: number) => void
) => {
if (block?.value) {
return <NodeBlockHeight value={block?.value} setValue={setBlock} />;
if (block) {
return <NodeBlockHeight value={block} setValue={setBlock} />;
}
if (block?.hasError) {
if (!block) {
return t('n/a');
}
return '-';
@ -59,6 +63,7 @@ const NodeStatsContent = ({
setBlock,
children,
dataTestId,
headers,
}: NodeStatsContentProps) => {
return (
<LayoutRow dataTestId={dataTestId}>
@ -72,15 +77,12 @@ const NodeStatsContent = ({
{getResponseTimeDisplayValue(data.responseTime)}
</LayoutCell>
<LayoutCell
label={t('Block')}
isLoading={data.block?.isLoading}
hasError={
data.block?.hasError ||
(!!data.block?.value && highestBlock > data.block.value)
}
dataTestId="block-cell"
label={t('Header block')}
isLoading={false}
hasError={headers ? highestBlock - 3 > headers.blockHeight : false}
dataTestId="header-block-cell"
>
{getBlockDisplayValue(data.block, setBlock)}
{getBlockDisplayValue(headers?.blockHeight, setBlock)}
</LayoutCell>
<LayoutCell
label={t('Subscription')}
@ -113,6 +115,10 @@ export type NodeStatsProps = {
highestBlock: number;
setBlock: (value: number) => void;
children?: ReactNode;
headers: {
blockHeight: number;
timestamp: Date;
};
};
export const NodeStats = ({
@ -121,6 +127,7 @@ export const NodeStats = ({
highestBlock,
children,
setBlock,
headers,
}: NodeStatsProps) => {
return (
<Wrapper client={client}>
@ -129,6 +136,7 @@ export const NodeStats = ({
highestBlock={highestBlock}
setBlock={setBlock}
dataTestId="node-row"
headers={headers}
>
{children}
</NodeStatsContent>

View File

@ -21,6 +21,7 @@ import type { Configuration, NodeData, ErrorType } from '../../types';
import { LayoutRow } from './layout-row';
import { NodeError } from './node-error';
import { NodeStats } from './node-stats';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
type NodeSwitcherProps = {
error?: string;
@ -46,6 +47,7 @@ export const NodeSwitcher = ({
onConnect,
}: NodeSwitcherProps) => {
const { VEGA_ENV, VEGA_URL } = useEnvironment();
const headerStore = useHeaderStore();
const [networkError, setNetworkError] = useState(
getErrorByType(initialErrorType, VEGA_ENV, VEGA_URL)
);
@ -96,7 +98,7 @@ export const NodeSwitcher = ({
<LayoutRow>
<div />
<span className="text-right">{t('Response time')}</span>
<span className="text-right">{t('Block')}</span>
<span className="text-right">{t('Block height')}</span>
<span className="text-right">{t('Subscription')}</span>
</LayoutRow>
</div>
@ -115,6 +117,7 @@ export const NodeSwitcher = ({
client={clients[node]}
highestBlock={highestBlock}
setBlock={(block) => updateNodeBlock(node, block)}
headers={headerStore[node]}
>
<div className="break-all" data-testid="node">
<Radio
@ -131,6 +134,7 @@ export const NodeSwitcher = ({
client={customUrl ? clients[customUrl] : undefined}
highestBlock={highestBlock}
setBlock={(block) => updateNodeBlock(CUSTOM_NODE_KEY, block)}
headers={headerStore[CUSTOM_NODE_KEY]}
>
<div className="flex w-full mb-2">
<Radio

View File

@ -37,6 +37,7 @@ export const getMockStatisticsResult = (
__typename: 'Statistics',
chainId: `${env.toLowerCase()}-0123`,
blockHeight: '11',
vegaTime: new Date().toISOString(),
},
});
@ -45,6 +46,7 @@ export const getMockQueryResult = (env: Networks): StatisticsQuery => ({
__typename: 'Statistics',
chainId: `${env.toLowerCase()}-0123`,
blockHeight: '11',
vegaTime: new Date().toISOString(),
},
});

View File

@ -17,6 +17,10 @@ jest.mock('react-dom', () => ({
createPortal: (node: ReactNode) => node,
}));
jest.mock('../components/node-switcher', () => ({
NodeSwitcher: () => <div />,
}));
global.fetch = jest.fn();
const MockWrapper = (props: ComponentProps<typeof EnvironmentProvider>) => {
@ -46,7 +50,6 @@ const mockEnvironmentState = {
GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
MAINTENANCE_PAGE: false,
configLoading: false,
blockDifference: 0,
nodeSwitcherOpen: false,
setNodeSwitcherOpen: noop,
networkError: undefined,

View File

@ -25,7 +25,6 @@ import type {
NodeData,
Configuration,
} from '../types';
import { useNodeHealth } from './use-node-health';
type EnvironmentProviderProps = {
config?: Configuration;
@ -36,7 +35,6 @@ type EnvironmentProviderProps = {
export type EnvironmentState = Environment & {
configLoading: boolean;
networkError?: ErrorType;
blockDifference: number;
nodeSwitcherOpen: boolean;
setNodeSwitcherOpen: () => void;
};
@ -86,8 +84,6 @@ export const EnvironmentProvider = ({
environment.MAINTENANCE_PAGE
);
const blockDifference = useNodeHealth(clients, environment.VEGA_URL);
const nodeKeys = Object.keys(nodes);
useEffect(() => {
@ -150,7 +146,6 @@ export const EnvironmentProvider = ({
...environment,
configLoading: loading,
networkError,
blockDifference,
nodeSwitcherOpen: isNodeSwitcherOpen,
setNodeSwitcherOpen: () => setNodeSwitcherOpen(true),
}}

View File

@ -1,117 +1,118 @@
import { act, renderHook } from '@testing-library/react';
import {
useNodeHealth,
NODE_SUBSET_COUNT,
INTERVAL_TIME,
} from './use-node-health';
import type { createClient } from '@vegaprotocol/apollo-client';
import type { ClientCollection } from './use-nodes';
import { renderHook, waitFor } from '@testing-library/react';
import { useNodeHealth } from './use-node-health';
import type { MockedResponse } from '@apollo/react-testing';
import { MockedProvider } from '@apollo/react-testing';
import type { StatisticsQuery } from '../utils/__generated__/Node';
import { StatisticsDocument } from '../utils/__generated__/Node';
import { useEnvironment } from './use-environment';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
function setup(...args: Parameters<typeof useNodeHealth>) {
return renderHook(() => useNodeHealth(...args));
}
const vegaUrl = 'https://foo.bar.com';
function createMockClient(blockHeight: number) {
jest.mock('./use-environment');
jest.mock('@vegaprotocol/apollo-client');
// @ts-ignore ignore mock implementation
useEnvironment.mockImplementation(() => ({
VEGA_URL: vegaUrl,
}));
const createStatsMock = (
blockHeight: number
): MockedResponse<StatisticsQuery> => {
return {
query: jest.fn().mockResolvedValue({
request: {
query: StatisticsDocument,
},
result: {
data: {
statistics: {
chainId: 'chain-id',
blockHeight: blockHeight.toString(),
vegaTime: '12345',
},
},
}),
} as unknown as ReturnType<typeof createClient>;
}
},
};
};
function createRejectingClient() {
return {
query: () => Promise.reject(new Error('request failed')),
} as unknown as ReturnType<typeof createClient>;
}
function setup(
mock: MockedResponse<StatisticsQuery>,
headers:
| {
blockHeight: number;
timestamp: Date;
}
| undefined
) {
// @ts-ignore ignore mock implementation
useHeaderStore.mockImplementation(() => ({
[vegaUrl]: headers,
}));
function createErroringClient() {
return {
query: () =>
Promise.resolve({
error: new Error('failed'),
}),
} as unknown as ReturnType<typeof createClient>;
return renderHook(() => useNodeHealth(), {
wrapper: ({ children }) => (
<MockedProvider mocks={[mock]}>{children}</MockedProvider>
),
});
}
const CURRENT_URL = 'https://current.test.com';
describe('useNodeHealth', () => {
beforeAll(() => {
jest.useFakeTimers();
});
it.each([
{ core: 1, node: 1, expected: 0 },
{ core: 1, node: 5, expected: -4 },
{ core: 10, node: 5, expected: 5 },
])(
'provides difference core block $core and node block $node',
async (cases) => {
const { result } = setup(createStatsMock(cases.core), {
blockHeight: cases.node,
timestamp: new Date(),
});
expect(result.current.blockDiff).toEqual(null);
expect(result.current.coreBlockHeight).toEqual(undefined);
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
await waitFor(() => {
expect(result.current.blockDiff).toEqual(cases.expected);
expect(result.current.coreBlockHeight).toEqual(cases.core);
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
});
}
);
it('provides difference between the highest block and the current block', async () => {
const highest = 100;
const curr = 97;
const clientCollection: ClientCollection = {
[CURRENT_URL]: createMockClient(curr),
'https://n02.test.com': createMockClient(98),
'https://n03.test.com': createMockClient(highest),
it('block diff is null if query fails indicating non operational', async () => {
const failedQuery: MockedResponse<StatisticsQuery> = {
request: {
query: StatisticsDocument,
},
result: {
// @ts-ignore failed query with no result
data: {},
},
};
const { result } = setup(clientCollection, CURRENT_URL);
await act(async () => {
jest.advanceTimersByTime(INTERVAL_TIME);
const { result } = setup(failedQuery, {
blockHeight: 1,
timestamp: new Date(),
});
expect(result.current.blockDiff).toEqual(null);
expect(result.current.coreBlockHeight).toEqual(undefined);
expect(result.current.datanodeBlockHeight).toEqual(1);
await waitFor(() => {
expect(result.current.blockDiff).toEqual(null);
expect(result.current.coreBlockHeight).toEqual(undefined);
expect(result.current.datanodeBlockHeight).toEqual(1);
});
expect(result.current).toBe(highest - curr);
});
it('returns -1 if the current node query fails', async () => {
const clientCollection: ClientCollection = {
[CURRENT_URL]: createRejectingClient(),
'https://n02.test.com': createMockClient(200),
'https://n03.test.com': createMockClient(102),
};
const { result } = setup(clientCollection, CURRENT_URL);
await act(async () => {
jest.advanceTimersByTime(INTERVAL_TIME);
it('returns 0 if no headers are found (wait until stats query resolves)', async () => {
const { result } = setup(createStatsMock(1), undefined);
expect(result.current.blockDiff).toEqual(null);
expect(result.current.coreBlockHeight).toEqual(undefined);
expect(result.current.datanodeBlockHeight).toEqual(undefined);
await waitFor(() => {
expect(result.current.blockDiff).toEqual(0);
expect(result.current.coreBlockHeight).toEqual(1);
expect(result.current.datanodeBlockHeight).toEqual(undefined);
});
expect(result.current).toBe(-1);
});
it('returns -1 if the current node query returns an error', async () => {
const clientCollection: ClientCollection = {
[CURRENT_URL]: createErroringClient(),
'https://n02.test.com': createMockClient(200),
'https://n03.test.com': createMockClient(102),
};
const { result } = setup(clientCollection, CURRENT_URL);
await act(async () => {
jest.advanceTimersByTime(INTERVAL_TIME);
});
expect(result.current).toBe(-1);
});
it('queries against 5 random nodes along with the current url', async () => {
const clientCollection: ClientCollection = new Array(20)
.fill(null)
.reduce((obj, x, i) => {
obj[`https://n${i}.test.com`] = createMockClient(100);
return obj;
}, {} as ClientCollection);
clientCollection[CURRENT_URL] = createMockClient(100);
const spyOnCurrent = jest.spyOn(clientCollection[CURRENT_URL], 'query');
const { result } = setup(clientCollection, CURRENT_URL);
await act(async () => {
jest.advanceTimersByTime(INTERVAL_TIME);
});
let count = 0;
Object.values(clientCollection).forEach((client) => {
// @ts-ignore jest.fn() in client setup means mock will be present
if (client?.query.mock.calls.length) {
count++;
}
});
expect(count).toBe(NODE_SUBSET_COUNT + 1);
expect(spyOnCurrent).toHaveBeenCalledTimes(1);
expect(result.current).toBe(0);
});
});

View File

@ -1,88 +1,37 @@
import compact from 'lodash/compact';
import shuffle from 'lodash/shuffle';
import type { createClient } from '@vegaprotocol/apollo-client';
import { useEffect, useState } from 'react';
import type { StatisticsQuery } from '../utils/__generated__/Node';
import { StatisticsDocument } from '../utils/__generated__/Node';
import type { ClientCollection } from './use-nodes';
import { useMemo } from 'react';
import { useStatisticsQuery } from '../utils/__generated__/Node';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
import { fromISONano } from '@vegaprotocol/react-helpers';
import { useEnvironment } from './use-environment';
// How often to query other nodes
export const INTERVAL_TIME = 30 * 1000;
// Number of nodes to query against
export const NODE_SUBSET_COUNT = 5;
export const useNodeHealth = () => {
const { VEGA_URL } = useEnvironment();
const headerStore = useHeaderStore();
const headers = VEGA_URL ? headerStore[VEGA_URL] : undefined;
const { data } = useStatisticsQuery({
pollInterval: 1000,
fetchPolicy: 'no-cache',
});
// Queries all nodes from the environment provider via an interval
// to calculate and return the difference between the most advanced block
// and the block height of the current node
export const useNodeHealth = (clients: ClientCollection, vegaUrl?: string) => {
const [blockDiff, setBlockDiff] = useState(0);
const blockDiff = useMemo(() => {
if (!data?.statistics.blockHeight) {
return null;
}
useEffect(() => {
if (!clients || !vegaUrl) return;
if (!headers) {
return 0;
}
const fetchBlockHeight = async (
client?: ReturnType<typeof createClient>
) => {
try {
const result = await client?.query<StatisticsQuery>({
query: StatisticsDocument,
fetchPolicy: 'no-cache', // always fetch and never cache
});
return Number(data.statistics.blockHeight) - headers.blockHeight;
}, [data, headers]);
if (!result) return null;
if (result.error) return null;
return result;
} catch {
return null;
}
};
const getBlockHeights = async () => {
const nodes = Object.keys(clients).filter((key) => key !== vegaUrl);
// make sure that your current vega url is always included
// so we can compare later
const testNodes = [vegaUrl, ...randomSubset(nodes, NODE_SUBSET_COUNT)];
const result = await Promise.all(
testNodes.map((node) => fetchBlockHeight(clients[node]))
);
const blockHeights: { [node: string]: number | null } = {};
testNodes.forEach((node, i) => {
const data = result[i];
const blockHeight = data
? Number(data?.data.statistics.blockHeight)
: null;
blockHeights[node] = blockHeight;
});
return blockHeights;
};
// Every INTERVAL_TIME get block heights of a random subset
// of nodes and determine if your current node is falling behind
const interval = setInterval(async () => {
const blockHeights = await getBlockHeights();
const highestBlock = Math.max.apply(
null,
compact(Object.values(blockHeights))
);
const currNodeBlock = blockHeights[vegaUrl];
if (!currNodeBlock) {
// Block height query failed and null was returned
setBlockDiff(-1);
} else {
setBlockDiff(highestBlock - currNodeBlock);
}
}, INTERVAL_TIME);
return () => {
clearInterval(interval);
};
}, [clients, vegaUrl]);
return blockDiff;
};
const randomSubset = (arr: string[], size: number) => {
const shuffled = shuffle(arr);
return shuffled.slice(0, size);
return {
coreBlockHeight: data?.statistics.blockHeight
? Number(data?.statistics.blockHeight)
: undefined,
coreVegaTime: fromISONano(data?.statistics.vegaTime),
datanodeBlockHeight: headers?.blockHeight,
datanodeVegaTime: headers?.timestamp,
blockDiff,
};
};

View File

@ -2,6 +2,7 @@ query Statistics {
statistics {
chainId
blockHeight
vegaTime
}
}

View File

@ -6,7 +6,7 @@ const defaultOptions = {} as const;
export type StatisticsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type StatisticsQuery = { __typename?: 'Query', statistics: { __typename?: 'Statistics', chainId: string, blockHeight: string } };
export type StatisticsQuery = { __typename?: 'Query', statistics: { __typename?: 'Statistics', chainId: string, blockHeight: string, vegaTime: any } };
export type BlockTimeSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
@ -19,6 +19,7 @@ export const StatisticsDocument = gql`
statistics {
chainId
blockHeight
vegaTime
}
}
`;

View File

@ -10,6 +10,7 @@ export const statisticsQuery = (
__typename: 'Statistics',
chainId: 'chain-id',
blockHeight: '11',
vegaTime: new Date().toISOString(),
},
};

View File

@ -1,7 +1,7 @@
import {
addDecimalsFormatNumber,
DateRangeFilter,
fromNanoSeconds,
fromISONano,
getDateTimeFormat,
SetFilter,
t,
@ -199,7 +199,7 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
valueFormatter={({
value,
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-'
value ? getDateTimeFormat().format(fromISONano(value)) : '-'
}
filter={DateRangeFilter}
/>

View File

@ -4,7 +4,12 @@ export const toNanoSeconds = (date: Date | string) => {
return new Date(date).getTime().toString() + '000000';
};
export const fromNanoSeconds = (ts: string) => {
export const fromISONano = (ts: string) => {
const val = parseISO(ts);
return new Date(isValid(val) ? val : 0);
};
export const fromNanoSeconds = (ts: string | number) => {
const ms = Number(String(ts).slice(0, -6));
return new Date(ms);
};