feat(trading): show block height (#3010)
This commit is contained in:
parent
da209d96b0
commit
23ce480daa
@ -190,6 +190,24 @@ describe('capsule', { tags: '@slow' }, () => {
|
|||||||
cy.setVegaWallet();
|
cy.setVegaWallet();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows node health', function () {
|
||||||
|
const market = this.market;
|
||||||
|
cy.visit(`/#/markets/${market.id}`);
|
||||||
|
cy.getByTestId('node-health')
|
||||||
|
.children()
|
||||||
|
.first()
|
||||||
|
.should('contain.text', 'Operational')
|
||||||
|
.next()
|
||||||
|
.should('contain.text', new URL(Cypress.env('VEGA_URL')).origin)
|
||||||
|
.next()
|
||||||
|
.then(($el) => {
|
||||||
|
const blockHeight = parseInt($el.text());
|
||||||
|
// block height will increase over the course of the test run so best
|
||||||
|
// we can do here is check that its showing something sensible
|
||||||
|
expect(blockHeight).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('can place and receive an order', function () {
|
it('can place and receive an order', function () {
|
||||||
const market = this.market;
|
const market = this.market;
|
||||||
cy.visit(`/#/markets/${market.id}`);
|
cy.visit(`/#/markets/${market.id}`);
|
||||||
|
@ -304,4 +304,18 @@ describe('home', { tags: '@regression' }, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('footer', () => {
|
||||||
|
it('shows current block height', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.getByTestId('node-health')
|
||||||
|
.children()
|
||||||
|
.first()
|
||||||
|
.should('contain.text', 'Operational')
|
||||||
|
.next()
|
||||||
|
.should('contain.text', new URL(Cypress.env('VEGA_URL')).origin)
|
||||||
|
.next()
|
||||||
|
.should('contain.text', '100'); // all mocked queries have x-block-height header set to 100
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,20 +1,35 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { NodeUrl, NodeHealth } from './footer';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { NodeHealth, NodeUrl, HealthIndicator } from './footer';
|
||||||
|
|
||||||
describe('NodeUrl', () => {
|
describe('NodeHealth', () => {
|
||||||
it('can open node switcher by clicking the node url', () => {
|
it('controls the node switcher dialog', async () => {
|
||||||
const mockOpenNodeSwitcher = jest.fn();
|
const mockOnClick = jest.fn();
|
||||||
const node = 'https://api.n99.somenetwork.vega.xyz';
|
render(
|
||||||
|
<NodeHealth
|
||||||
render(<NodeUrl url={node} openNodeSwitcher={mockOpenNodeSwitcher} />);
|
onClick={mockOnClick}
|
||||||
|
url={'https://api.n99.somenetwork.vega.xyz'}
|
||||||
fireEvent.click(screen.getByText(/n99/));
|
blockHeight={100}
|
||||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
blockDiff={0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await userEvent.click(screen.getByRole('button'));
|
||||||
|
expect(mockOnClick).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('NodeHealth', () => {
|
describe('NodeUrl', () => {
|
||||||
const mockOpenNodeSwitcher = jest.fn();
|
it('renders correct part of node url', () => {
|
||||||
|
const node = 'https://api.n99.somenetwork.vega.xyz';
|
||||||
|
const expectedText = node.split('.').slice(1).join('.');
|
||||||
|
|
||||||
|
render(<NodeUrl url={node} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(expectedText)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('HealthIndicator', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{ diff: 0, classname: 'bg-vega-green-550', text: 'Operational' },
|
{ diff: 0, classname: 'bg-vega-green-550', text: 'Operational' },
|
||||||
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
|
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
|
||||||
@ -23,16 +38,9 @@ describe('NodeHealth', () => {
|
|||||||
it.each(cases)(
|
it.each(cases)(
|
||||||
'renders correct text and indicator color for $diff block difference',
|
'renders correct text and indicator color for $diff block difference',
|
||||||
(elem) => {
|
(elem) => {
|
||||||
render(
|
render(<HealthIndicator blockDiff={elem.diff} />);
|
||||||
<NodeHealth
|
|
||||||
blockDiff={elem.diff}
|
|
||||||
openNodeSwitcher={mockOpenNodeSwitcher}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
||||||
expect(screen.getByText(elem.text)).toBeInTheDocument();
|
expect(screen.getByText(elem.text)).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByText(elem.text));
|
|
||||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
|
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
|
||||||
import { t, useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
import { t, useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
||||||
import { ButtonLink, Indicator, Intent } from '@vegaprotocol/ui-toolkit';
|
import { Indicator, Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||||
import { useGlobalStore } from '../../stores';
|
import { useGlobalStore } from '../../stores';
|
||||||
|
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
@ -8,45 +10,64 @@ export const Footer = () => {
|
|||||||
const setNodeSwitcher = useGlobalStore(
|
const setNodeSwitcher = useGlobalStore(
|
||||||
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
||||||
);
|
);
|
||||||
const { blockDiff } = useNodeHealth();
|
const { blockDiff, datanodeBlockHeight } = useNodeHealth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="px-4 py-1 text-xs border-t border-default">
|
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300">
|
||||||
<div className="flex justify-between">
|
{/* Pull left to align with top nav, due to button padding */}
|
||||||
<div className="flex gap-2">
|
<div className="-ml-2">
|
||||||
{VEGA_URL && (
|
{VEGA_URL && (
|
||||||
<>
|
|
||||||
<NodeHealth
|
<NodeHealth
|
||||||
blockDiff={blockDiff}
|
|
||||||
openNodeSwitcher={() => setNodeSwitcher(true)}
|
|
||||||
/>
|
|
||||||
{' | '}
|
|
||||||
<NodeUrl
|
|
||||||
url={VEGA_URL}
|
url={VEGA_URL}
|
||||||
openNodeSwitcher={() => setNodeSwitcher(true)}
|
blockHeight={datanodeBlockHeight}
|
||||||
|
blockDiff={blockDiff}
|
||||||
|
onClick={() => setNodeSwitcher(true)}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
interface NodeHealthProps {
|
||||||
|
url: string;
|
||||||
|
blockHeight: number | undefined;
|
||||||
|
blockDiff: number | null;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeHealth = ({
|
||||||
|
url,
|
||||||
|
blockHeight,
|
||||||
|
blockDiff,
|
||||||
|
onClick,
|
||||||
|
}: NodeHealthProps) => {
|
||||||
|
return (
|
||||||
|
<FooterButton onClick={onClick} data-testid="node-health">
|
||||||
|
<FooterButtonPart>
|
||||||
|
<HealthIndicator blockDiff={blockDiff} />
|
||||||
|
</FooterButtonPart>
|
||||||
|
<FooterButtonPart>
|
||||||
|
<NodeUrl url={url} />
|
||||||
|
</FooterButtonPart>
|
||||||
|
<FooterButtonPart>
|
||||||
|
<span title={t('Block height')}>{blockHeight}</span>
|
||||||
|
</FooterButtonPart>
|
||||||
|
</FooterButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface NodeUrlProps {
|
interface NodeUrlProps {
|
||||||
url: string;
|
url: string;
|
||||||
openNodeSwitcher: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NodeUrl = ({ url, openNodeSwitcher }: NodeUrlProps) => {
|
export const NodeUrl = ({ url }: NodeUrlProps) => {
|
||||||
// get base url from api url, api sub domain
|
// get base url from api url, api sub domain
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, '');
|
const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, '');
|
||||||
return <ButtonLink onClick={openNodeSwitcher}>{nodeUrl}</ButtonLink>;
|
return <span title={t('Connected node')}>{nodeUrl}</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface NodeHealthProps {
|
interface HealthIndicatorProps {
|
||||||
openNodeSwitcher: () => void;
|
|
||||||
blockDiff: number | null;
|
blockDiff: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,10 +75,7 @@ interface NodeHealthProps {
|
|||||||
// deemed acceptable for "Good" status
|
// deemed acceptable for "Good" status
|
||||||
const BLOCK_THRESHOLD = 3;
|
const BLOCK_THRESHOLD = 3;
|
||||||
|
|
||||||
export const NodeHealth = ({
|
export const HealthIndicator = ({ blockDiff }: HealthIndicatorProps) => {
|
||||||
blockDiff,
|
|
||||||
openNodeSwitcher,
|
|
||||||
}: NodeHealthProps) => {
|
|
||||||
const online = useNavigatorOnline();
|
const online = useNavigatorOnline();
|
||||||
|
|
||||||
let intent = Intent.Success;
|
let intent = Intent.Success;
|
||||||
@ -76,9 +94,36 @@ export const NodeHealth = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<span title={t('Node health')}>
|
||||||
<Indicator variant={intent} />
|
<Indicator variant={intent} />
|
||||||
<ButtonLink onClick={openNodeSwitcher}>{text}</ButtonLink>
|
{text}
|
||||||
</>
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type FooterButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
|
|
||||||
|
const FooterButton = (props: FooterButtonProps) => {
|
||||||
|
const buttonClasses = classNames(
|
||||||
|
'px-2 py-0.5 rounded-md',
|
||||||
|
'enabled:hover:bg-vega-light-150',
|
||||||
|
'dark:enabled:hover:bg-vega-dark-150'
|
||||||
|
);
|
||||||
|
return <button {...props} className={buttonClasses} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FooterButtonPart = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'relative inline-block mr-2 last:mr-0 pr-2 last:pr-0',
|
||||||
|
'last:after:hidden',
|
||||||
|
'after:content after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2',
|
||||||
|
'after:h-3 after:w-1 after:border-r',
|
||||||
|
'after:border-vega-light-300 dark:after:border-vega-dark-300'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -32,7 +32,8 @@ export const aliasGQLQuery = (
|
|||||||
operationName: string,
|
operationName: string,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
data?: any,
|
data?: any,
|
||||||
errors?: Partial<GraphQLError>[]
|
errors?: Partial<GraphQLError>[],
|
||||||
|
headers?: Record<string, string>
|
||||||
) => {
|
) => {
|
||||||
if (hasOperationName(req, operationName)) {
|
if (hasOperationName(req, operationName)) {
|
||||||
req.alias = operationName;
|
req.alias = operationName;
|
||||||
@ -40,6 +41,13 @@ export const aliasGQLQuery = (
|
|||||||
req.reply({
|
req.reply({
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
body: { ...(data && { data }), ...(errors && { errors }) },
|
body: { ...(data && { data }), ...(errors && { errors }) },
|
||||||
|
headers: {
|
||||||
|
...req.headers,
|
||||||
|
// basic default block height header response
|
||||||
|
'x-block-height': '100',
|
||||||
|
'x-block-timestamp': Date.now().toString() + '0'.repeat(6),
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,14 @@ import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
|||||||
import { useEnvironment } from './use-environment';
|
import { useEnvironment } from './use-environment';
|
||||||
import { fromNanoSeconds } from '@vegaprotocol/react-helpers';
|
import { fromNanoSeconds } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
const POLL_INTERVAL = 1000;
|
||||||
|
|
||||||
export const useNodeHealth = () => {
|
export const useNodeHealth = () => {
|
||||||
const url = useEnvironment((store) => store.VEGA_URL);
|
const url = useEnvironment((store) => store.VEGA_URL);
|
||||||
const headerStore = useHeaderStore();
|
const headerStore = useHeaderStore();
|
||||||
const headers = url ? headerStore[url] : undefined;
|
const headers = url ? headerStore[url] : undefined;
|
||||||
const { data, error, loading, stopPolling } = useStatisticsQuery({
|
const { data, error, loading, startPolling, stopPolling } =
|
||||||
pollInterval: 1000,
|
useStatisticsQuery({
|
||||||
fetchPolicy: 'no-cache',
|
fetchPolicy: 'no-cache',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -28,8 +30,13 @@ export const useNodeHealth = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [error, stopPolling]);
|
|
||||||
|
if (!('Cypress' in window)) {
|
||||||
|
startPolling(POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
}, [error, startPolling, stopPolling]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error,
|
error,
|
||||||
|
Loading…
Reference in New Issue
Block a user