Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
737b796227 | ||
|
|
8379d139ef | ||
|
|
0fd6ff6eec | ||
|
|
b9d0ce6485 | ||
|
|
420a29680d | ||
|
|
19c9e79647 | ||
|
|
4cd856fcb8 | ||
|
|
649cc61280 | ||
|
|
35f47b75ac |
+137784
-137707
File diff suppressed because it is too large
Load Diff
@@ -190,6 +190,24 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
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 () {
|
||||
const market = this.market;
|
||||
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 { NodeUrl, NodeHealth } from './footer';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { NodeHealth, NodeUrl, HealthIndicator } from './footer';
|
||||
|
||||
describe('NodeUrl', () => {
|
||||
it('can open node switcher by clicking the node url', () => {
|
||||
const mockOpenNodeSwitcher = jest.fn();
|
||||
const node = 'https://api.n99.somenetwork.vega.xyz';
|
||||
|
||||
render(<NodeUrl url={node} openNodeSwitcher={mockOpenNodeSwitcher} />);
|
||||
|
||||
fireEvent.click(screen.getByText(/n99/));
|
||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
||||
describe('NodeHealth', () => {
|
||||
it('controls the node switcher dialog', async () => {
|
||||
const mockOnClick = jest.fn();
|
||||
render(
|
||||
<NodeHealth
|
||||
onClick={mockOnClick}
|
||||
url={'https://api.n99.somenetwork.vega.xyz'}
|
||||
blockHeight={100}
|
||||
blockDiff={0}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(mockOnClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('NodeHealth', () => {
|
||||
const mockOpenNodeSwitcher = jest.fn();
|
||||
describe('NodeUrl', () => {
|
||||
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 = [
|
||||
{ diff: 0, classname: 'bg-vega-green-550', text: 'Operational' },
|
||||
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
|
||||
@@ -23,16 +38,9 @@ describe('NodeHealth', () => {
|
||||
it.each(cases)(
|
||||
'renders correct text and indicator color for $diff block difference',
|
||||
(elem) => {
|
||||
render(
|
||||
<NodeHealth
|
||||
blockDiff={elem.diff}
|
||||
openNodeSwitcher={mockOpenNodeSwitcher}
|
||||
/>
|
||||
);
|
||||
render(<HealthIndicator blockDiff={elem.diff} />);
|
||||
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
||||
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 { 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';
|
||||
|
||||
export const Footer = () => {
|
||||
@@ -8,45 +10,64 @@ export const Footer = () => {
|
||||
const setNodeSwitcher = useGlobalStore(
|
||||
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
||||
);
|
||||
const { blockDiff } = useNodeHealth();
|
||||
const { blockDiff, datanodeBlockHeight } = useNodeHealth();
|
||||
|
||||
return (
|
||||
<footer className="px-4 py-1 text-xs border-t border-default">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
{VEGA_URL && (
|
||||
<>
|
||||
<NodeHealth
|
||||
blockDiff={blockDiff}
|
||||
openNodeSwitcher={() => setNodeSwitcher(true)}
|
||||
/>
|
||||
{' | '}
|
||||
<NodeUrl
|
||||
url={VEGA_URL}
|
||||
openNodeSwitcher={() => setNodeSwitcher(true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300">
|
||||
{/* Pull left to align with top nav, due to button padding */}
|
||||
<div className="-ml-2">
|
||||
{VEGA_URL && (
|
||||
<NodeHealth
|
||||
url={VEGA_URL}
|
||||
blockHeight={datanodeBlockHeight}
|
||||
blockDiff={blockDiff}
|
||||
onClick={() => setNodeSwitcher(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</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 {
|
||||
url: string;
|
||||
openNodeSwitcher: () => void;
|
||||
}
|
||||
|
||||
export const NodeUrl = ({ url, openNodeSwitcher }: NodeUrlProps) => {
|
||||
export const NodeUrl = ({ url }: NodeUrlProps) => {
|
||||
// get base url from api url, api sub domain
|
||||
const urlObj = new URL(url);
|
||||
const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, '');
|
||||
return <ButtonLink onClick={openNodeSwitcher}>{nodeUrl}</ButtonLink>;
|
||||
return <span title={t('Connected node')}>{nodeUrl}</span>;
|
||||
};
|
||||
|
||||
interface NodeHealthProps {
|
||||
openNodeSwitcher: () => void;
|
||||
interface HealthIndicatorProps {
|
||||
blockDiff: number | null;
|
||||
}
|
||||
|
||||
@@ -54,10 +75,7 @@ interface NodeHealthProps {
|
||||
// deemed acceptable for "Good" status
|
||||
const BLOCK_THRESHOLD = 3;
|
||||
|
||||
export const NodeHealth = ({
|
||||
blockDiff,
|
||||
openNodeSwitcher,
|
||||
}: NodeHealthProps) => {
|
||||
export const HealthIndicator = ({ blockDiff }: HealthIndicatorProps) => {
|
||||
const online = useNavigatorOnline();
|
||||
|
||||
let intent = Intent.Success;
|
||||
@@ -76,9 +94,36 @@ export const NodeHealth = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span title={t('Node health')}>
|
||||
<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,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data?: any,
|
||||
errors?: Partial<GraphQLError>[]
|
||||
errors?: Partial<GraphQLError>[],
|
||||
headers?: Record<string, string>
|
||||
) => {
|
||||
if (hasOperationName(req, operationName)) {
|
||||
req.alias = operationName;
|
||||
@@ -40,6 +41,13 @@ export const aliasGQLQuery = (
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
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,14 +4,16 @@ import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
||||
import { useEnvironment } from './use-environment';
|
||||
import { fromNanoSeconds } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const POLL_INTERVAL = 1000;
|
||||
|
||||
export const useNodeHealth = () => {
|
||||
const url = useEnvironment((store) => store.VEGA_URL);
|
||||
const headerStore = useHeaderStore();
|
||||
const headers = url ? headerStore[url] : undefined;
|
||||
const { data, error, loading, stopPolling } = useStatisticsQuery({
|
||||
pollInterval: 1000,
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
const { data, error, loading, startPolling, stopPolling } =
|
||||
useStatisticsQuery({
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
const blockDiff = useMemo(() => {
|
||||
if (!data?.statistics.blockHeight) {
|
||||
@@ -28,8 +30,13 @@ export const useNodeHealth = () => {
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
}, [error, stopPolling]);
|
||||
|
||||
if (!('Cypress' in window)) {
|
||||
startPolling(POLL_INTERVAL);
|
||||
}
|
||||
}, [error, startPolling, stopPolling]);
|
||||
|
||||
return {
|
||||
error,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
PriceCell,
|
||||
Vol,
|
||||
VolCell,
|
||||
CumulativeVol,
|
||||
addDecimalsFormatNumber,
|
||||
VolumeType,
|
||||
addDecimal,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface OrderbookRowProps {
|
||||
@@ -42,17 +41,17 @@ export const OrderbookRow = React.memo(
|
||||
}: OrderbookRowProps) => {
|
||||
return (
|
||||
<>
|
||||
<Vol
|
||||
<VolCell
|
||||
testId={`bid-vol-${price}`}
|
||||
value={bid}
|
||||
valueFormatted={addDecimal(bid, positionDecimalPlaces)}
|
||||
valueFormatted={addDecimalsFormatNumber(bid, positionDecimalPlaces)}
|
||||
relativeValue={relativeBid}
|
||||
type={VolumeType.bid}
|
||||
/>
|
||||
<Vol
|
||||
<VolCell
|
||||
testId={`ask-vol-${price}`}
|
||||
value={ask}
|
||||
valueFormatted={addDecimal(ask, positionDecimalPlaces)}
|
||||
valueFormatted={addDecimalsFormatNumber(ask, positionDecimalPlaces)}
|
||||
relativeValue={relativeAsk}
|
||||
type={VolumeType.ask}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
import { NumericCell } from './numeric-cell';
|
||||
|
||||
describe('NumericCell', () => {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
import { PriceFlashCell } from './price-flash-cell';
|
||||
|
||||
describe('<PriceFlashCell />', () => {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { VolCell, VolumeType } from './vol-cell';
|
||||
import * as tailwind from '@vegaprotocol/tailwindcss-config';
|
||||
|
||||
describe('VolCell', () => {
|
||||
const significantPart = '12,345';
|
||||
const decimalPart = '67';
|
||||
const props = {
|
||||
value: 1234567,
|
||||
valueFormatted: `${significantPart}.${decimalPart}`,
|
||||
type: VolumeType.ask,
|
||||
testId: 'cell',
|
||||
};
|
||||
|
||||
it('Displays formatted value', () => {
|
||||
render(<VolCell {...props} />);
|
||||
expect(screen.getByTestId(props.testId)).toHaveTextContent(
|
||||
props.valueFormatted
|
||||
);
|
||||
expect(screen.getByText(decimalPart)).toBeInTheDocument();
|
||||
expect(screen.getByText(decimalPart)).toHaveClass('opacity-60');
|
||||
});
|
||||
|
||||
it('Displays 0', () => {
|
||||
render(<VolCell {...props} value={0} valueFormatted="0.00" />);
|
||||
expect(screen.getByTestId(props.testId)).toHaveTextContent('0.00');
|
||||
});
|
||||
|
||||
it('Displays - if value is not a number', () => {
|
||||
render(<VolCell {...props} value={null} valueFormatted="" />);
|
||||
expect(screen.getByTestId(props.testId)).toHaveTextContent('-');
|
||||
});
|
||||
|
||||
it('renders bid volume bar', () => {
|
||||
render(<VolCell {...props} type={VolumeType.bid} />);
|
||||
expect(screen.getByTestId('vol-bar')).toHaveClass('left-0'); // renders bid bars from the left
|
||||
expect(screen.getByTestId('vol-bar')).toHaveStyle({
|
||||
backgroundColor: tailwind.theme.colors.vega.green.DEFAULT,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders ask volume bar', () => {
|
||||
render(<VolCell {...props} type={VolumeType.ask} />);
|
||||
expect(screen.getByTestId('vol-bar')).toHaveClass('right-0'); // renders ask bars from the right
|
||||
expect(screen.getByTestId('vol-bar')).toHaveStyle({
|
||||
backgroundColor: tailwind.theme.colors.vega.pink.DEFAULT,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ export enum VolumeType {
|
||||
bid,
|
||||
ask,
|
||||
}
|
||||
export interface VolProps {
|
||||
export interface VolCellProps {
|
||||
value: number | bigint | null | undefined;
|
||||
valueFormatted: string;
|
||||
relativeValue?: number;
|
||||
@@ -17,20 +17,21 @@ export interface VolProps {
|
||||
}
|
||||
export interface IVolCellProps extends ICellRendererParams {
|
||||
value: number | bigint | null | undefined;
|
||||
valueFormatted: Omit<VolProps, 'value'>;
|
||||
valueFormatted: Omit<VolCellProps, 'value'>;
|
||||
}
|
||||
|
||||
export const BID_COLOR = tailwind.theme.colors.vega.green.DEFAULT;
|
||||
export const ASK_COLOR = tailwind.theme.colors.vega.pink.DEFAULT;
|
||||
|
||||
export const Vol = React.memo(
|
||||
({ value, valueFormatted, relativeValue, type, testId }: VolProps) => {
|
||||
export const VolCell = React.memo(
|
||||
({ value, valueFormatted, relativeValue, type, testId }: VolCellProps) => {
|
||||
if ((!value && value !== 0) || isNaN(Number(value))) {
|
||||
return <div data-testid="vol">-</div>;
|
||||
return <div data-testid={testId || 'vol'}>-</div>;
|
||||
}
|
||||
return (
|
||||
<div className="relative" data-testid={testId || 'vol'}>
|
||||
<div
|
||||
data-testid="vol-bar"
|
||||
className={classNames(
|
||||
'h-full absolute top-0 opacity-40 dark:opacity-100',
|
||||
{
|
||||
@@ -50,4 +51,4 @@ export const Vol = React.memo(
|
||||
}
|
||||
);
|
||||
|
||||
Vol.displayName = 'Vol';
|
||||
VolCell.displayName = 'VolCell';
|
||||
|
||||
Reference in New Issue
Block a user