Compare commits

...
Author SHA1 Message Date
Madalina Raicu 8b17606d7b fix(trading): fix formatting on price bounds 2023-02-28 16:24:22 +00:00
Madalina Raicu 99515f2904 fix(trading): update market-info 2023-02-28 15:33:51 +00:00
Madalina Raicu 24844a976d fix(trading): fix layout for fees in fills table 2023-02-28 15:07:18 +00:00
Madalina Raicu 825534dc82 fix(trading): update banners 2023-02-28 14:56:05 +00:00
Madalina Raicu 3b250e7c8b fix(trading): dropdown menu invisible 2023-02-28 14:54:45 +00:00
Madalina Raicu afefc1a9f4 fix(trading): price monitoring bounds update 2023-02-28 14:47:09 +00:00
Madalina Raicu ba90e063a7 fix(trading): remove cancel all buttons 2023-02-28 14:44:07 +00:00
Madalina Raicu 2861720455 Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into fix/show-breakdown-in-view-as-user 2023-02-28 14:33:22 +00:00
Maciek 86739dfaad chore(trading): loader component css animation only, preloader tweaks (#3027) 2023-02-28 15:31:23 +01:00
Madalina Raicu 7c110287c0 fix(trading): price monitoring bounds fix test 2023-02-28 14:18:42 +00:00
Madalina Raicu 0a4ab2bc6a fix(trading): update mainnet sim 2 banners 2023-02-28 14:05:21 +00:00
Sam Keen d87adfd7d5 fix(governance): vote state update fix (#3023) 2023-02-28 12:43:20 +00:00
dexturr 7364abbbf2 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-28 12:10:21 +00:00
Madalina Raicu 8d856fc697 fix(trading): fix market info and merge with develop 2023-02-28 10:57:55 +00:00
Ciaran McGhie 1bd56c9bdb chore(ci): bump ui-toolkit version to 0.7.0 (#2961) 2023-02-28 10:13:51 +00:00
Sam Keen a985bc7317 fix(governance): some fixes and tweaks for the rewards tables (#3009) 2023-02-28 10:07:51 +00:00
dexturr 1cab9abfd5 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-28 06:08:01 +00:00
dexturr 26a82ab2ab chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-28 00:17:25 +00:00
Matthew Russell 69a0830e80 fix(ui-toolkit): remove calt from nav and toasts (#3020) 2023-02-27 15:21:17 -08:00
Madalina Raicu 46a5527d27 fix: show breakdown in view as user mode 2023-02-27 21:36:30 +00:00
m.ray 4eb6e09bd4 fix(trading): condense deal ticket & market info price range snags (#3012) 2023-02-27 18:04:13 +00:00
Maciek a02100b079 chore(ui-toolkit): revert: loader no rerender version only 2023-02-27 07:51:50 -08:00
Maciek 4bb78e9891 chore(ui-toolkit): loader no rerender version only (#3011) 2023-02-27 16:44:48 +01:00
dexturr 79f360dc24 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-27 12:10:43 +00:00
Art a3fcd6b7dc feat(explorer): validators page (#2982) 2023-02-27 09:17:23 +00:00
Matthew Russell 23ce480daa feat(trading): show block height (#3010) 2023-02-27 09:16:19 +00:00
Matthew Russell da209d96b0 fix(tailwindcss-config): fix calt for firefox, make calt opt in when using font-alpha (#3007) 2023-02-27 09:13:19 +00:00
Matthew Russell 9166a85a55 chore(trading): update tooltip text for 24h volume (#3005) 2023-02-27 09:12:50 +00:00
dexturr a5ec1f708f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-27 06:09:54 +00:00
dexturr 3afdc0c8da chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-27 00:17:20 +00:00
dexturr 757d545d97 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-26 18:09:19 +00:00
dexturr fc4d40f3cf chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-26 12:07:31 +00:00
dexturr ff47eda692 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-26 06:08:17 +00:00
dexturr 7483d9ce1d chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-26 00:15:56 +00:00
dexturr f9209cf327 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-25 18:07:08 +00:00
dexturr 420a29680d chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-25 12:09:08 +00:00
dexturr 19c9e79647 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-25 06:07:29 +00:00
dexturr 4cd856fcb8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-25 00:16:11 +00:00
Matthew Russell 649cc61280 chore(trading): fix formatting for volume cells in book (#3006) 2023-02-24 16:31:42 -05:00
dexturr 35f47b75ac chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-24 18:07:20 +00:00
106 changed files with 139675 additions and 138546 deletions
@@ -8,7 +8,7 @@ context('Asset page', { tags: '@regression' }, () => {
it('should be able to see full assets list', () => {
cy.getAssets().then((assets) => {
Object.values(assets).forEach((asset) => {
assets.forEach((asset) => {
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
});
});
@@ -25,7 +25,7 @@ context('Asset page', { tags: '@regression' }, () => {
});
cy.getAssets().then((assets) => {
Object.values(assets).forEach((asset) => {
assets.forEach((asset) => {
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
});
});
@@ -33,7 +33,7 @@ context('Asset page', { tags: '@regression' }, () => {
it('should open details page when clicked on "View details"', () => {
cy.getAssets().then((assets) => {
Object.values(assets).forEach((asset) => {
assets.forEach((asset) => {
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
.eq(0)
.should('contain.text', 'View details');
@@ -1,303 +1,15 @@
context('Validator page', { tags: '@smoke' }, function () {
const validatorMenuHeading = 'a[href="/validators"]';
const tendermintDataHeader = '[data-testid="tendermint-header"]';
const vegaDataHeader = '[data-testid="vega-header"]';
const jsonSection = '.language-json';
before('Visit validators page and obtain data', function () {
cy.visit('/');
cy.get(validatorMenuHeading).click();
cy.get_validators().as('validators');
cy.get_nodes().as('nodes');
cy.visit('/validators');
});
describe('Verify elements on page', function () {
before('Ensure at least two validators are present', function () {
assert.isAtLeast(
this.validators.length,
2,
'Ensuring at least two validators exist'
);
});
it('should be able to see validator page sections', function () {
cy.get(vegaDataHeader)
.contains('Vega data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
});
it('should be able to see relevant validator information in tendermint section', function () {
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.next()
.within(() => {
cy.get(jsonSection)
.invoke('text')
.convert_string_json_to_js_object()
.then((validatorsInJson) => {
this.validators.forEach((validator, index) => {
const validatorInJson =
validatorsInJson.result.validators[index];
assert.equal(
validatorInJson.address,
validator.address,
`Checking that validator address shown in json matches system data`
);
cy.contains(validator.address).should('be.visible');
assert.equal(
validatorInJson.pub_key.type,
validator.pub_key.type,
`Checking that validator public key type shown in json matches system data`
);
cy.contains(validator.pub_key.type).should('be.visible');
assert.equal(
validatorInJson.pub_key.value,
validator.pub_key.value,
`Checking that validator public key value shown in json matches system data`
);
cy.contains(validator.pub_key.value).should('be.visible');
assert.equal(
validatorInJson.voting_power,
validator.voting_power,
`Checking that validator voting power in json matches system data`
);
cy.contains(validator.voting_power).should('be.visible');
// Proposer priority can change frequently mid test
// Therefore only checking the field name is present.
cy.contains('proposer_priority').should('be.visible');
});
});
});
});
// Test disabled 2022/11/15 during the 0.62.1 upgrade. The JSON structure changed, and
// this test failed. Rather than fix it, it will be replaced when the validator view displays
// something useful rather than just dumping out the JSON.
xit('should be able to see relevant node information in vega data section', function () {
cy.get(vegaDataHeader)
.contains('Vega data')
.next()
.within(() => {
cy.get(jsonSection)
.invoke('text')
.convert_string_json_to_js_object()
.then((nodesInJson) => {
this.nodes.forEach((node, index) => {
const nodeInJson = nodesInJson.edges[index].node;
// Vegacapsule shows no info or null for following fields:
// name, infoURL, avatarUrl, location, epoch data
// Therefore, these values remain unchecked.
assert.equal(
nodeInJson.__typename,
node.__typename,
`Checking that node __typename shown in json matches system data`
);
cy.contains(node.__typename).should('be.visible');
assert.equal(
nodeInJson.id,
node.id,
`Checking that node id shown in json matches system data`
);
cy.contains(node.id).should('be.visible');
assert.equal(
nodeInJson.pubkey,
node.pubkey,
`Checking that node pubkey shown in json matches system data`
);
cy.contains(node.pubkey).should('be.visible');
assert.equal(
nodeInJson.tmPubkey,
node.tmPubkey,
`Checking that node tmPubkey shown in json matches system data`
);
cy.contains(node.tmPubkey).should('be.visible');
assert.equal(
nodeInJson.ethereumAddress,
node.ethereumAddress,
`Checking that node ethereumAddress shown in json matches system data`
);
cy.contains(node.ethereumAddress).should('be.visible');
assert.equal(
nodeInJson.stakedByOperator,
node.stakedByOperator,
`Checking that node stakedByOperator value shown in json matches system data`
);
cy.contains(node.stakedByOperator).should('be.visible');
assert.equal(
nodeInJson.stakedByDelegates,
node.stakedByDelegates,
`Checking that node stakedByDelegates value shown in json matches system data`
);
cy.contains(node.stakedByDelegates).should('be.visible');
assert.equal(
nodeInJson.stakedTotal,
node.stakedTotal,
`Checking that node stakedTotal shown in json matches system data`
);
cy.contains(node.stakedTotal).should('be.visible');
assert.equal(
nodeInJson.pendingStake,
node.pendingStake,
`Checking that node pendingStake shown in json matches system data`
);
cy.contains(node.pendingStake).should('be.visible');
assert.equal(
nodeInJson.status,
node.status,
`Checking that node status shown in json matches system data`
);
cy.contains(node.status).should('be.visible');
});
});
});
});
it('should be able to see validator page displayed on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(validatorMenuHeading).click();
cy.get(vegaDataHeader)
.contains('Vega data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.next()
.within(() => {
this.validators.forEach((validator) => {
cy.contains(validator.address).should('be.visible');
cy.contains(validator.pub_key.type).should('be.visible');
cy.contains(validator.pub_key.value).should('be.visible');
cy.contains(validator.voting_power).should('be.visible');
// Proposer priority can change frequently mid test
// Therefore only checking the field name is present.
cy.contains('proposer_priority').should('be.visible');
});
});
});
it('should be able to switch validator page between light and dark mode', function () {
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
const themeSwitcher = '[data-testid="theme-switcher"]';
const jsonFields = '.hljs';
const sideMenuBackground = '.absolute';
// Engage dark mode if not allready set
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.then((background_color) => {
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
cy.get(themeSwitcher).click();
});
// Engage white mode
cy.get(themeSwitcher).click();
// White Mode
cy.get(validatorMenuHeading)
.should('have.css', 'background-color')
.and('include', whiteThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', whiteThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', whiteThemeSideMenuBackgroundColor);
// Dark Mode
cy.get(themeSwitcher).click();
cy.get(validatorMenuHeading)
.should('have.css', 'background-color')
.and('include', darkThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', darkThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', darkThemeSideMenuBackgroundColor);
});
Cypress.Commands.add('get_validators', () => {
cy.request({
method: 'GET',
url: `http://localhost:26617/validators`,
headers: { 'content-type': 'application/json' },
})
.its(`body.result.validators`)
.then(function (response) {
let validators = [];
response.forEach((account, index) => {
validators[index] = account;
});
return validators;
});
});
Cypress.Commands.add('get_nodes', () => {
const mutation =
'{nodesConnection { edges { node { id name infoUrl avatarUrl pubkey tmPubkey ethereumAddress \
location stakedByOperator stakedByDelegates stakedTotal pendingStake \
epochData { total offline online __typename } status name __typename}}}}';
cy.request({
method: 'POST',
url: `http://localhost:3028/query`,
body: {
query: mutation,
},
headers: { 'content-type': 'application/json' },
})
.its(`body.data.nodesConnection.edges`)
.then(function (response) {
let nodes = [];
response.forEach((node) => {
nodes.push(node);
});
return nodes;
it('should be able to see validator tiles', function () {
cy.getNodes().then((nodes) => {
nodes.forEach((node) => {
cy.get(`[validator-id="${node.id}"]`).should('be.visible');
});
});
});
});
});
+2 -2
View File
@@ -40,9 +40,9 @@ function App() {
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<AnnouncementBanner>
<div className="font-alpha calt uppercase text-center text-lg text-white">
<span className="pr-4">Mainnet sim 2 coming in March!</span>
<span className="pr-4">Mainnet sim 2 is live!</span>
<ExternalLink href="https://fairground.wtf/">
Learn more
Come help stress test the network
</ExternalLink>
</div>
</AnnouncementBanner>
@@ -19,12 +19,12 @@ const EmptyList = ({ heading, label }: EmptyListProps) => {
<div className="mt-4">
{heading ? (
<h1 className="font-alpha text-xl uppercase text-center leading-relaxed">
<h1 className="font-alpha calt text-xl uppercase text-center leading-relaxed">
{heading}
</h1>
) : null}
{label ? (
<p className="font-alpha text-gray-500 text-center">{label}</p>
<p className="font-alpha calt text-gray-500 text-center">{label}</p>
) : null}
</div>
</div>
@@ -184,9 +184,8 @@ export const MarketDetails = ({
content: (
<>
<p className="text-xs mb-4">
{`For liquidity orders count towards a commitment they have to be
within either the liquidity or price monitoring bounds (whichever is
tighter).`}
{`For liquidity orders to count towards a commitment, they must be
within the liquidity monitoring bounds.`}
</p>
<p className="text-xs mb-4">
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
@@ -249,7 +248,7 @@ export const MarketDetails = ({
<>
{panels.map((p) => (
<div className="mb-3">
<h2 className="font-alpha text-xl">{p.title}</h2>
<h2 className="font-alpha calt text-xl">{p.title}</h2>
{p.content}
</div>
))}
@@ -83,7 +83,7 @@ const NestedDataListItem = ({
});
const titleClasses = classNames({
'text-xl pl-4 border-l-4 font-alpha': hasChildren,
'text-xl pl-4 border-l-4 font-alpha calt': hasChildren,
'text-base font-medium whitespace-nowrap': !hasChildren,
});
@@ -20,7 +20,7 @@ export const PageHeader = ({
copy = false,
className,
}: PageHeaderProps) => {
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha';
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha calt';
return (
<header className={className}>
<span className={`${titleClasses} block`}>{prefix}</span>
@@ -0,0 +1 @@
export * from './page-actions';
@@ -0,0 +1,10 @@
import type { HTMLAttributes } from 'react';
export const PageActions = ({
children,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div className="flex flex-row items-start gap-1" {...props}>
{children}
</div>
);
@@ -0,0 +1,45 @@
import classNames from 'classnames';
import type { HTMLAttributes, ReactNode } from 'react';
import { useDocumentTitle } from '../../hooks/use-document-title';
import { RouteTitle } from '../route-title';
import { PageActions } from './page-actions';
type PageTitleProps = {
/**
* The page title
* (also sets the document title unless overwritten by
* `documentTitle` property)
*/
title: string;
/**
* The react node that consists of CTA buttons, links, etc.
*/
actions?: ReactNode;
/**
* Overwrites the document title
*/
documentTitle?: Parameters<typeof useDocumentTitle>[0];
} & Omit<HTMLAttributes<HTMLHeadingElement>, 'children'>;
export const PageTitle = ({
title,
actions,
documentTitle,
className,
...props
}: PageTitleProps) => {
useDocumentTitle(
documentTitle && documentTitle.length > 0 ? documentTitle : [title]
);
return (
<div
className="flex flex-col md:flex-row gap-1 justify-between content-start mb-8"
data-testid="page-title"
>
<RouteTitle className={classNames('mb-1', className)} {...props}>
{title}
</RouteTitle>
{actions && <PageActions>{actions}</PageActions>}
</div>
);
};
@@ -13,7 +13,7 @@ export const RouteTitle = ({
...props
}: RouteTitleProps) => {
const classes = classnames(
'font-alpha',
'font-alpha calt',
'text-4xl',
'uppercase',
'mb-8',
@@ -11,7 +11,7 @@ export const StatusMessage = ({
className,
...props
}: StatusMessageProps) => {
const classes = classnames('font-alpha text-2xl mb-28', className);
const classes = classnames('font-alpha calt text-2xl mb-28', className);
return (
<h3 className={classes} {...props}>
{children}
@@ -12,7 +12,7 @@ export const SubHeading = ({
...props
}: SubHeadingProps) => {
const classes = classnames(
'font-alpha',
'font-alpha calt',
'text-2xl',
'uppercase',
'mt-8 mb-2',
@@ -9,7 +9,7 @@ export type Metric = components['schemas']['vegaDispatchMetric'];
export const wrapperClasses =
'border border-vega-light-150 dark:border-vega-dark-200 rounded-md pv-2 mb-5 w-full sm:w-1/4 min-w-[200px] ';
export const headerClasses =
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha';
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha calt';
export type Transfer = components['schemas']['commandsv1Transfer'];
@@ -0,0 +1,55 @@
import { useFetch } from '@vegaprotocol/react-helpers';
import { useEffect, useRef } from 'react';
import { DATA_SOURCES } from '../config';
type PubKey = {
type: string;
value: string;
};
type Validator = {
address: string;
pub_key: PubKey;
voting_power: string;
proposer_priority: string;
};
type Result = {
block_height: string;
validators: Validator[];
count: string;
total: string;
};
type TendermintValidatorsResponse = {
jsonrpc: string;
id: number;
result: Result;
};
export const useTendermintValidators = (pollInterval?: number) => {
const {
state: { data, loading, error },
refetch,
} = useFetch<TendermintValidatorsResponse>(
`${DATA_SOURCES.tendermintUrl}/validators`
);
const ref = useRef<TendermintValidatorsResponse | undefined>(undefined);
useEffect(() => {
if (data) ref.current = data;
}, [data]);
useEffect(() => {
const interval =
pollInterval &&
setInterval(() => {
refetch();
}, pollInterval);
return () => {
clearInterval(interval);
};
}, [pollInterval, refetch]);
return { data: ref.current, loading, error, refetch };
};
@@ -1,5 +1,4 @@
import { t } from '@vegaprotocol/react-helpers';
import { RouteTitle } from '../../components/route-title';
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
@@ -8,6 +7,7 @@ import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
import { useParams } from 'react-router-dom';
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
import { useState } from 'react';
import { PageTitle } from '../../components/page-helpers/page-title';
export const AssetPage = () => {
useDocumentTitle(['Assets']);
@@ -22,18 +22,25 @@ export const AssetPage = () => {
return (
<>
<section className="relative">
<RouteTitle data-testid="asset-header">{title}</RouteTitle>
<PageTitle
data-testid="asset-header"
title={title}
actions={
<Button
disabled={!data}
size="xs"
onClick={() => setDialogOpen(true)}
>
{t('View JSON')}
</Button>
}
/>
<AsyncRenderer
noDataMessage={t('Asset not found')}
data={data}
loading={loading}
error={error}
>
<div className="absolute top-0 right-0">
<Button size="xs" onClick={() => setDialogOpen(true)}>
{t('View JSON')}
</Button>
</div>
<div className="h-full relative">
<AssetDetailsTable asset={data as AssetFieldsFragment} />
</div>
@@ -3,12 +3,12 @@ import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { MarketDetails } from '../../components/markets/market-details';
import { RouteTitle } from '../../components/route-title';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
import compact from 'lodash/compact';
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
import { marketInfoNoCandlesDataProvider } from '@vegaprotocol/market-info';
import { PageTitle } from '../../components/page-helpers/page-title';
export const MarketPage = () => {
useScrollToLocation();
@@ -40,20 +40,25 @@ export const MarketPage = () => {
return (
<>
<section className="relative">
<RouteTitle data-testid="markets-heading">
{data?.market?.tradableInstrument.instrument.name}
</RouteTitle>
<PageTitle
data-testid="markets-heading"
title={data?.market?.tradableInstrument.instrument.name || ''}
actions={
<Button
disabled={!data?.market}
size="xs"
onClick={() => setDialogOpen(true)}
>
{t('View JSON')}
</Button>
}
/>
<AsyncRenderer
noDataMessage={t('This chain has no markets')}
data={data}
loading={loading}
error={error}
>
<div className="absolute top-0 right-0">
<Button size="xs" onClick={() => setDialogOpen(true)}>
{t('View JSON')}
</Button>
</div>
<MarketDetails market={data?.market} />
</AsyncRenderer>
</section>
@@ -68,7 +68,7 @@ const Party = () => {
return (
<section>
<h1
className="font-alpha uppercase font-xl mb-4 text-vega-dark-100 dark:text-vega-light-100"
className="font-alpha calt uppercase font-xl mb-4 text-vega-dark-100 dark:text-vega-light-100"
data-testid="parties-header"
>
{t('Public key')}
@@ -9,7 +9,7 @@ import Party from './parties';
import { Parties } from './parties/home';
import { Party as PartySingle } from './parties/id';
import Txs from './txs';
import Validators from './validators';
import { ValidatorsPage } from './validators';
import Genesis from './genesis';
import { Block } from './blocks/id';
import { Blocks } from './blocks/home';
@@ -125,7 +125,7 @@ const validators: Route[] = flags.validators
path: Routes.VALIDATORS,
name: 'Validators',
text: t('Validators'),
element: <Validators />,
element: <ValidatorsPage />,
},
]
: [];
@@ -1,47 +1 @@
import { t } from '@vegaprotocol/react-helpers';
import { RouteTitle } from '../../components/route-title';
import { SubHeading } from '../../components/sub-heading';
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { DATA_SOURCES } from '../../config';
import { useFetch } from '@vegaprotocol/react-helpers';
import type { TendermintValidatorsResponse } from './tendermint-validator-response';
import { useExplorerNodesQuery } from './__generated__/Nodes';
import { useDocumentTitle } from '../../hooks/use-document-title';
const Validators = () => {
const {
state: { data: validators },
} = useFetch<TendermintValidatorsResponse>(
`${DATA_SOURCES.tendermintUrl}/validators`
);
useDocumentTitle(['Validators']);
const { data } = useExplorerNodesQuery();
return (
<section>
<RouteTitle data-testid="validators-header">{t('Validators')}</RouteTitle>
{data ? (
<>
<SubHeading data-testid="vega-header">{t('Vega data')}</SubHeading>
<SyntaxHighlighter data-testid="vega-data" data={data} />
</>
) : (
<Loader />
)}
{validators ? (
<>
<SubHeading data-testid="tendermint-header">
{t('Tendermint data')}
</SubHeading>
<SyntaxHighlighter data-testid="tendermint-data" data={validators} />
</>
) : (
<Loader />
)}
</section>
);
};
export default Validators;
export * from './validators-page';
@@ -1,24 +0,0 @@
export interface PubKey {
type: string;
value: string;
}
export interface Validator {
address: string;
pub_key: PubKey;
voting_power: string;
proposer_priority: string;
}
export interface Result {
block_height: string;
validators: Validator[];
count: string;
total: string;
}
export interface TendermintValidatorsResponse {
jsonrpc: string;
id: number;
result: Result;
}
@@ -0,0 +1,340 @@
import { countryCodeToFlagEmoji, t } from '@vegaprotocol/react-helpers';
import {
AsyncRenderer,
Button,
CopyWithTooltip,
ExternalLink,
Icon,
KeyValueTable,
KeyValueTableRow,
Tooltip,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import { useExplorerNodesQuery } from './__generated__/Nodes';
import { useDocumentTitle } from '../../hooks/use-document-title';
import compact from 'lodash/compact';
import { useTendermintValidators } from '../../hooks/use-tendermint-validators';
import { useMemo, useState } from 'react';
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
import { PageTitle } from '../../components/page-helpers/page-title';
import BigNumber from 'bignumber.js';
import {
ContractAddressLink,
DApp,
TOKEN_VALIDATOR,
useLinks,
} from '@vegaprotocol/environment';
import classNames from 'classnames';
import { NodeStatus, NodeStatusMapping } from '@vegaprotocol/types';
type RateProps = {
value: BigNumber | number | undefined;
className?: string;
colour?: 'green' | 'blue' | 'pink' | 'orange';
asPoint?: boolean;
zero?: boolean;
};
const Rate = ({
value,
className,
colour = 'blue',
asPoint = false,
}: RateProps) => {
const val =
typeof value === 'undefined'
? new BigNumber(0)
: typeof value === 'number'
? new BigNumber(value)
: value;
const bar = asPoint
? {
right: `${val.times(100).toFixed(2)}%`,
}
: { width: `${val.times(100).toFixed(2)}%` };
return (
<div
className={classNames(
'overflow-hidden rounded h-[9px] flex w-full bg-vega-light-100 dark:bg-vega-dark-150',
{ 'pl-[9px]': asPoint },
{
'bg-gradient-to-l to-vega-orange-500 dark:to-vega-orange-550 from-vega-light-100 dark:from-vega-dark-150':
asPoint && colour === 'orange',
}
)}
>
<div className="relative w-full">
<div
className={classNames(
'w-[9px] h-[9px] absolute top-0 right-0 transition-all rounded',
{
'bg-vega-green-550 dark:bg-vega-green-500': colour === 'green',
'bg-vega-blue-550 dark:bg-vega-blue-500': colour === 'blue',
'bg-vega-pink-550 dark:bg-vega-pink-500': colour === 'pink',
'bg-vega-orange-550 dark:bg-vega-orange-500': colour === 'orange',
},
'bg-vega',
className
)}
style={bar}
></div>
</div>
</div>
);
};
export const ValidatorsPage = () => {
useDocumentTitle(['Validators']);
const { data: tmData } = useTendermintValidators(5000);
const { data, loading, error, refetch } = useExplorerNodesQuery();
const validators = compact(data?.nodesConnection.edges?.map((e) => e?.node));
// voting power
const powers = compact(tmData?.result.validators).map(
(v) => new BigNumber(v.voting_power)
);
const totalVotingPower = BigNumber.sum(...powers);
// proposer priority
const priorities = compact(tmData?.result.validators).map(
(v) => new BigNumber(v.proposer_priority)
);
const absoluteProposerPriority = BigNumber.max(
...priorities.map((p) => p.abs())
);
const tmValidators = useMemo(() => {
return tmData?.result.validators.map((v) => {
const data = {
key: v.pub_key.value,
votingPower: new BigNumber(v.voting_power),
proposerPriority: new BigNumber(v.proposer_priority),
};
return {
...data,
votingPowerRatio: data.votingPower.dividedBy(totalVotingPower),
proposerPriorityRatio: absoluteProposerPriority
.plus(data.proposerPriority)
.dividedBy(absoluteProposerPriority.times(2)),
};
});
}, [tmData?.result.validators, totalVotingPower, absoluteProposerPriority]);
const totalStaked = BigNumber.sum(
...validators.map((v) => new BigNumber(v.stakedTotal))
);
const [vegaDialog, setVegaDialog] = useState<boolean>(false);
const [tmDialog, setTmDialog] = useState<boolean>(false);
const tokenLink = useLinks(DApp.Token);
return (
<>
<section>
<PageTitle
title={t('Validators')}
actions={
<>
<Button
disabled={Boolean(!data)}
size="xs"
onClick={() => setVegaDialog(true)}
>
{t('View JSON')}
</Button>
{
<Button
disabled={Boolean(!tmData)}
size="xs"
onClick={() => setTmDialog(true)}
>
{t('View tendermint as JSON')}
</Button>
}
</>
}
/>
<AsyncRenderer
data={validators}
loading={loading}
error={error}
reload={refetch}
>
<ul className="md:columns-2">
{validators.map((v) => {
const tm = tmValidators?.find((tmv) => tmv.key === v.tmPubkey);
const stakedRatio = new BigNumber(v.stakedTotal).dividedBy(
totalStaked
);
const validatorPage = tokenLink(
TOKEN_VALIDATOR.replace(':id', v.id)
);
const validatorName =
v.name && v.name.length > 0 ? v.name : truncateMiddle(v.id);
return (
<li className="mb-5" key={v.id}>
<div
data-testid="validator-tile"
validator-id={v.id}
className="border border-vega-light-200 dark:border-vega-dark-200 rounded p-2 overflow-hidden relative flex gap-2 items-start justify-between"
>
{v.avatarUrl && (
<div className="w-20">
<ExternalLink href={validatorPage}>
<img
className="w-full"
src={v.avatarUrl}
alt={validatorName}
/>
</ExternalLink>
</div>
)}
<div className="w-full">
<h2 className="font-alpha text-2xl">
<ExternalLink href={validatorPage}>
{validatorName}
</ExternalLink>
</h2>
<KeyValueTable>
<KeyValueTableRow>
<div>{t('ID')}</div>
<div className="break-all text-xs">{v.id}</div>
</KeyValueTableRow>
<KeyValueTableRow>
<div>{t('Status')}</div>
<div className="break-all text-xs">
<span
className={classNames('mr-1', {
'text-vega-green-550 dark:vega-green-500':
v.status === NodeStatus.NODE_STATUS_VALIDATOR,
'text-vega-pink-550 dark:vega-pink-500':
v.status ===
NodeStatus.NODE_STATUS_NON_VALIDATOR,
})}
>
<Icon name="tick-circle" size={3} />
</span>
{NodeStatusMapping[v.status]}
</div>
</KeyValueTableRow>
<KeyValueTableRow>
<div>{t('Location')}</div>
<div>
{countryCodeToFlagEmoji(v.location)}{' '}
<span className="text-[10px]">{v.location}</span>
</div>
</KeyValueTableRow>
<KeyValueTableRow>
<div>{t('Public key')}</div>
<div className="break-all text-xs">{v.pubkey}</div>
</KeyValueTableRow>
<KeyValueTableRow>
<div>{t('Ethereum address')}</div>
<div className="break-all text-xs">
<ContractAddressLink address={v.ethereumAddress} />{' '}
<CopyWithTooltip text={v.ethereumAddress}>
<button title={t('Copy address to clipboard')}>
<Icon size={3} name="duplicate" />
</button>
</CopyWithTooltip>
</div>
</KeyValueTableRow>
<KeyValueTableRow>
<div>{t('Tendermint public key')}</div>
<div className="break-all text-xs">{v.tmPubkey}</div>
</KeyValueTableRow>
<KeyValueTableRow>
<div>{t('Voting power')}</div>
<div className="w-44 text-right">
<Rate value={tm?.votingPowerRatio} />
<div className="text-[10px] leading-3">
{tm?.votingPowerRatio.times(100).toFixed(2)}
{'% '}({tm?.votingPower.toString()})
</div>
</div>
</KeyValueTableRow>
<KeyValueTableRow>
<div>{t('Proposer priority')}</div>
<div className="w-44 text-right">
<Rate
value={tm?.proposerPriorityRatio}
colour="orange"
asPoint={true}
zero={true}
/>
<div className="text-[10px] leading-3">
{tm?.proposerPriority.toString()}
</div>
</div>
</KeyValueTableRow>
<KeyValueTableRow>
<div>{t('Stake share')}</div>
<div className="w-44 text-right">
<Rate value={stakedRatio} colour="green" />
<div className="text-[10px] leading-3">
<Tooltip
description={
<KeyValueTable
numerical={true}
className="mb-1"
>
<KeyValueTableRow
className="text-xs"
noBorder={true}
>
<div>{t('Staked by operator')}</div>
<div>{v.stakedByOperator}</div>
</KeyValueTableRow>
<KeyValueTableRow
className="text-xs"
noBorder={true}
>
<div>{t('Staked by delegates')}</div>
<div>{v.stakedByDelegates}</div>
</KeyValueTableRow>
<KeyValueTableRow
className="text-xs"
noBorder={true}
>
<div>{t('Staked (total)')}</div>
<div>{v.stakedTotal}</div>
</KeyValueTableRow>
</KeyValueTable>
}
>
<span>
{stakedRatio.times(100).toFixed(2)}%
</span>
</Tooltip>
</div>
</div>
</KeyValueTableRow>
</KeyValueTable>
</div>
</div>
</li>
);
})}
</ul>
</AsyncRenderer>
</section>
<JsonViewerDialog
open={vegaDialog}
onChange={(isOpen) => setVegaDialog(isOpen)}
title={t('Vega Validators')}
content={data}
/>
<JsonViewerDialog
open={tmDialog}
onChange={(isOpen) => setTmDialog(isOpen)}
title={t('Tendermint Validators')}
content={tmData}
/>
</>
);
};
@@ -8,7 +8,7 @@ export function Dashboard() {
<>
<div className="px-16 pt-20 pb-12 bg-greys-light-100">
<div className="max-w-screen-xl mx-auto">
<h1 className="font-alpha uppercase text-5xl mb-8">
<h1 className="font-alpha calt uppercase text-5xl mb-8">
{t('Top liquidity opportunities')}
</h1>
@@ -30,7 +30,7 @@ type Network = 'testnet' | 'mainnet';
export const Intro = ({ network = 'testnet' }: { network?: Network }) => {
return (
<div>
<p className="font-alpha text-2xl font-medium mb-2">
<p className="font-alpha calt text-2xl font-medium mb-2">
{t(
'Become a liquidity provider and earn a cut of the fees paid during trading.'
)}
@@ -92,7 +92,7 @@ export const Detail = () => {
)}
</div>
<div>
<h2 className="font-alpha text-2xl mb-4">
<h2 className="font-alpha calt text-2xl mb-4">
{t('Current Liquidity Provision')}
</h2>
<LPProvidersGrid
@@ -14,13 +14,13 @@ export const Header = ({
<div className="mb-6">
<Link to="/">
<Icon name="chevron-left" className="mr-2" />
<span className="underline font-alpha text-lg font-medium">
<span className="underline font-alpha calt text-lg font-medium">
{t('Liquidity opportunities')}
</span>
</Link>
</div>
<h1 className="font-alpha text-5xl mb-6">{name}</h1>
<p className="font-alpha text-4xl">{symbol}</p>
<h1 className="font-alpha calt text-5xl mb-6">{name}</h1>
<p className="font-alpha calt text-4xl">{symbol}</p>
</div>
);
};
@@ -44,7 +44,7 @@ export const Market = ({
<table className="w-full">
<thead>
<tr
className="text-sm text-greys-light-400 text-left font-alpha"
className="text-sm text-greys-light-400 text-left font-alpha calt"
style={{ fontFeatureSettings: "'liga' off, 'calt' off" }}
>
<th className="font-medium px-4">{t('Volume (24h)')}</th>
@@ -36,7 +36,7 @@ export const Grid = ({ isRowClickable, children, ...props }: Props) => {
return (
<AgGridReact
className={classNames('ag-theme-alpine h-full font-alpha', {
className={classNames('ag-theme-alpine h-full font-alpha calt', {
'row-hover': isRowClickable,
})}
rowHeight={92}
@@ -13,7 +13,7 @@ const Remainder = () => (
);
const COPY_CLASS =
'text-sm font-medium whitespace-nowrap text-white font-alpha';
'text-sm font-medium whitespace-nowrap text-white font-alpha calt';
const Tooltip = ({
children,
@@ -59,10 +59,10 @@ const ROWS = [
export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => {
return (
<Dialog size="medium" open={isOpen} onChange={onChange}>
<h1 className="text-2xl mb-5 pr-2 font-medium font-alpha uppercase liga-0-calt-0">
<h1 className="text-2xl mb-5 pr-2 font-medium font-alpha uppercase">
{t('Health')}
</h1>
<p className="text-lg font-medium font-alpha mb-8 liga-0-calt-0">
<p className="text-lg font-medium font-alpha mb-8">
{t(
'Market health is a representation of market and liquidity status and how close that market is to moving from one fee level to another.'
)}
@@ -70,10 +70,10 @@ export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => {
<table className="table-fixed">
<thead className="border-b border-greys-light-300">
<th className="w-1/2 text-left font-medium font-alpha text-base pb-4 uppercase liga-0-calt-0">
<th className="w-1/2 text-left font-medium font-alpha text-base pb-4 uppercase">
{t('Market status')}
</th>
<th className="w-1/2 text-lef font-medium font-alpha text-base pb-4 uppercase liga-0-calt-0">
<th className="w-1/2 text-lef font-medium font-alpha text-base pb-4 uppercase">
{t('Liquidity status')}
</th>
</thead>
@@ -85,12 +85,10 @@ export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => {
<td
className={classNames('pr-4 pb-10', { 'pt-8': isFirstRow })}
>
<h2 className="font-medium font-alpha uppercase text-base liga-0-calt-0">
<h2 className="font-medium font-alpha uppercase text-base">
{t(r.title)}
</h2>
<p className="font-medium font-alpha text-lg liga-0-calt-0">
{t(r.copy)}
</p>
<p className="font-medium font-alpha text-lg">{t(r.copy)}</p>
</td>
<td
className={classNames('pl-4 pb-10', { 'pt-8': isFirstRow })}
File diff suppressed because it is too large Load Diff
+111
View File
@@ -0,0 +1,111 @@
.pre-loader {
display: flex;
width: 100%;
min-height: 100vh;
justify-content: center;
align-items: center;
}
.pre-loader .loader-item {
width: 10px;
height: 10px;
background: #000;
}
.pre-loader .pre-loader-center {
align-items: center;
display: flex;
flex-direction: column;
}
.pre-loader .pre-loader-wrapper {
width: 50px;
height: 50px;
display: flex;
flex-wrap: wrap;
}
.pre-loader .loader-item:nth-child(0) {
animation-delay: 0ms;
animation-direction: reverse;
}
.pre-loader .loader-item:first-child {
animation-delay: -50ms;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(2) {
animation-delay: 0.2s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(3) {
animation-delay: -0.6s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(4) {
animation-delay: 0.4s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(5) {
animation-delay: -0.5s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(6) {
animation-delay: 0.3s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(7) {
animation-delay: -1.4s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(8) {
animation-delay: 2s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(9) {
animation-delay: -0.9s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(10) {
animation-delay: 1.5s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(11) {
animation-delay: -0.55s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(12) {
animation-delay: 1.2s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(13) {
animation-delay: -0.65s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(14) {
animation-delay: 0.7s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(15) {
animation-delay: -3.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(16) {
animation-delay: 1.6s;
animation-direction: reverse;
}
.pre-loader .loader-item {
animation: flickering 0.4s linear infinite alternate;
}
@keyframes flickering {
0% {
opacity: 1;
}
25% {
opacity: 1;
}
26% {
opacity: 0;
}
to {
opacity: 0;
}
}
html.dark .pre-loader .loader-item {
background: #fff;
}
+9
View File
@@ -0,0 +1,9 @@
(function () {
var storedTheme = window.localStorage.getItem('theme');
if (
storedTheme === 'dark' ||
(!storedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
}
})();
@@ -21,8 +21,10 @@ export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
<>
<AnnouncementBanner>
<div className="font-alpha calt uppercase text-center text-lg text-white">
<span className="pr-4">Mainnet sim 2 coming in March!</span>
<ExternalLink href="https://fairground.wtf/">Learn more</ExternalLink>
<span className="pr-4">Mainnet sim 2 is live!</span>
<ExternalLink href="https://fairground.wtf/">
Come help stress test the network
</ExternalLink>
</div>
</AnnouncementBanner>
<Nav navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
@@ -5,6 +5,7 @@ import { VoteValue } from '@vegaprotocol/types';
import { useEffect, useState } from 'react';
import { useUserVoteQuery } from './__generated__/Vote';
import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
import type { FinalizedVote } from '@vegaprotocol/governance';
export enum VoteState {
NotCast = 'NotCast',
@@ -21,25 +22,14 @@ export type Vote = {
party: { id: string };
};
export type Votes = Array<Vote | null>;
export function getUserVote(pubkey: string, yesVotes?: Votes, noVotes?: Votes) {
const yesVote = yesVotes?.find((v) => v && v.party.id === pubkey);
const noVote = noVotes?.find((v) => v && v.party.id === pubkey);
if (yesVote) {
return yesVote;
} else if (noVote) {
return noVote;
} else {
return null;
}
}
/**
* Finds the status of a users given vote in a given proposal and provides
* a function to send a vote transaction to your wallet
*/
export function useUserVote(proposalId: string | null | undefined) {
export function useUserVote(
proposalId: string | null | undefined,
finalizedVote?: FinalizedVote | null
) {
const { pubKey } = useVegaWallet();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [timeout, setTimeoutValue] = useState<any>(null);
@@ -50,10 +40,27 @@ export function useUserVote(proposalId: string | null | undefined) {
variables: { partyId: pubKey || '' },
skip: !pubKey || !proposalId,
});
const [userVote, setUserVote] = useState<FinalizedVote | undefined>(
undefined
);
const userVote = removePaginationWrapper(
data?.party?.votesConnection?.edges
).find(({ proposalId: pId }) => proposalId === pId);
useEffect(() => {
if (finalizedVote?.vote.value) {
setUserVote(finalizedVote);
} else if (data?.party?.votesConnection?.edges) {
// This sets the vote (if any) when the user first loads the page
setUserVote(
removePaginationWrapper(data?.party?.votesConnection?.edges).find(
({ proposalId: pId }) => proposalId === pId
)
);
}
}, [
finalizedVote?.vote.value,
data?.party?.votesConnection?.edges,
finalizedVote,
proposalId,
]);
// If user vote changes update the vote state
useEffect(() => {
@@ -22,6 +22,8 @@ describe('Vote buttons', () => {
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -43,6 +45,8 @@ describe('Vote buttons', () => {
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -75,6 +79,8 @@ describe('Vote buttons', () => {
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -97,6 +103,8 @@ describe('Vote buttons', () => {
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(0)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -120,6 +128,8 @@ describe('Vote buttons', () => {
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -145,6 +155,8 @@ describe('Vote buttons', () => {
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -167,6 +179,8 @@ describe('Vote buttons', () => {
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { AsyncRenderer, Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
import { addDecimal, toBigNum } from '@vegaprotocol/react-helpers';
import { useVoteSubmit } from '@vegaprotocol/governance';
import { ProposalState, VoteValue } from '@vegaprotocol/types';
import {
AppStateActionType,
@@ -16,6 +15,7 @@ import { VoteState } from './use-user-vote';
import { ProposalMinRequirements, ProposalUserAction } from '../shared';
import { VoteTransactionDialog } from './vote-transaction-dialog';
import { useVoteButtonsQuery } from './__generated__/Stake';
import type { DialogProps } from '@vegaprotocol/wallet';
interface VoteButtonsContainerProps {
voteState: VoteState | null;
@@ -24,6 +24,8 @@ interface VoteButtonsContainerProps {
proposalState: ProposalState;
minVoterBalance: string | null;
spamProtectionMinTokens: string | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element;
className?: string;
}
@@ -62,11 +64,12 @@ export const VoteButtons = ({
currentStakeAvailable,
minVoterBalance,
spamProtectionMinTokens,
submit,
dialog: Dialog,
}: VoteButtonsProps) => {
const { t } = useTranslation();
const { appDispatch } = useAppState();
const { pubKey } = useVegaWallet();
const { submit, Dialog } = useVoteSubmit();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
import { formatDistanceToNow } from 'date-fns';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ProposalState } from '@vegaprotocol/types';
import { VoteProgress } from '@vegaprotocol/governance';
import { useVoteSubmit, VoteProgress } from '@vegaprotocol/governance';
import { formatNumber } from '../../../../lib/format-number';
import { ConnectToVega } from '../../../../components/connect-to-vega';
import { useVoteInformation } from '../../hooks';
@@ -44,7 +44,8 @@ export const VoteDetails = ({
} = useVoteInformation({ proposal });
const { t } = useTranslation();
const { voteState, voteDatetime } = useUserVote(proposal?.id);
const { submit, Dialog, finalizedVote } = useVoteSubmit();
const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote);
const defaultDecimals = 2;
const daysLeft = t('daysLeft', {
daysLeft: formatDistanceToNow(new Date(proposal?.terms.closingDatetime)),
@@ -188,6 +189,8 @@ export const VoteDetails = ({
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={spamProtectionMinTokens}
className="flex"
submit={submit}
dialog={Dialog}
/>
)}
</section>
@@ -50,10 +50,11 @@ const DisplayReward = ({
>
<button>
<div className="flex flex-col items-start">
<span>{formatNumber(toBigNum(reward, decimals))}</span>
<span>{formatNumber(toBigNum(reward, decimals), 4)}</span>
{percentageOfTotal && (
<span className="text-vega-dark-300">
({formatNumber(toBigNum(percentageOfTotal, 4)).toString()}%)
({formatNumber(percentageOfTotal, 4).toString()}
%)
</span>
)}
</div>
@@ -8,7 +8,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD' },
asset: { id: 'usd', symbol: 'USD', name: 'USD' },
party: { id: 'blah' },
epoch: { id: '1' },
};
@@ -18,7 +18,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '50',
percentageOfTotal: '0.05',
receivedAt: new Date(),
asset: { id: 'eur', symbol: 'EUR' },
asset: { id: 'eur', symbol: 'EUR', name: 'EUR' },
party: { id: 'blah' },
epoch: { id: '2' },
};
@@ -28,7 +28,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
asset: { id: 'gbp', symbol: 'GBP' },
asset: { id: 'gbp', symbol: 'GBP', name: 'GBP' },
party: { id: 'blah' },
epoch: { id: '2' },
};
@@ -38,7 +38,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD' },
asset: { id: 'usd', symbol: 'USD', name: 'USD' },
party: { id: 'blah' },
epoch: { id: '1' },
};
@@ -48,7 +48,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '50',
percentageOfTotal: '0.05',
receivedAt: new Date(),
asset: { id: 'eur', symbol: 'EUR' },
asset: { id: 'eur', symbol: 'EUR', name: 'EUR' },
party: { id: 'blah' },
epoch: { id: '2' },
};
@@ -134,20 +134,20 @@ describe('generateEpochIndividualRewardsList', () => {
epoch: '2',
rewards: [
{
asset: 'EUR',
totalAmount: '50',
asset: 'GBP',
totalAmount: '200',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '0',
percentageOfTotal: '0',
amount: '200',
percentageOfTotal: '0.2',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '50',
percentageOfTotal: '0.05',
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
@@ -164,21 +164,21 @@ describe('generateEpochIndividualRewardsList', () => {
},
},
{
asset: 'GBP',
totalAmount: '200',
asset: 'EUR',
totalAmount: '50',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '200',
percentageOfTotal: '0.2',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '50',
percentageOfTotal: '0.05',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
@@ -33,7 +33,7 @@ export const generateEpochIndividualRewardsList = (
// We take the rewards and aggregate them by epoch and asset.
const epochIndividualRewards = rewards.reduce((map, reward) => {
const epochId = reward.epoch.id;
const assetName = reward.asset.symbol;
const assetName = reward.asset.name;
const rewardType = reward.rewardType;
const amount = reward.amount;
const percentageOfTotal = reward.percentageOfTotal;
@@ -70,6 +70,12 @@ export const generateEpochIndividualRewardsList = (
'0'
);
if (epoch) {
epoch.rewards = epoch.rewards.sort((a, b) => {
return new BigNumber(b.totalAmount).comparedTo(a.totalAmount);
});
}
return map;
}, new Map<string, EpochIndividualReward>());
@@ -5,6 +5,7 @@ import type {
import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { AccountType } from '@vegaprotocol/types';
import { BigNumber } from '../../../lib/bignumber';
interface EpochSummaryWithNamedReward extends EpochRewardSummaryFieldsFragment {
name: string;
@@ -120,7 +121,9 @@ export const generateEpochTotalRewardsList = (
return {
epoch: epochSummaries[0].epoch,
assetRewards,
assetRewards: assetRewards.sort((a, b) => {
return new BigNumber(b.totalAmount).comparedTo(a.totalAmount);
}),
};
});
@@ -3,6 +3,7 @@ fragment RewardFields on Reward {
asset {
id
symbol
name
}
party {
id
+3 -2
View File
@@ -3,7 +3,7 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type RewardFieldsFragment = { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } };
export type RewardFieldsFragment = { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } };
export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: string, epoch: number };
@@ -13,7 +13,7 @@ export type RewardsQueryVariables = Types.Exact<{
}>;
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } } };
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } } };
export type EpochRewardSummaryFieldsFragment = { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType };
@@ -37,6 +37,7 @@ export const RewardFieldsFragmentDoc = gql`
asset {
id
symbol
name
}
party {
id
+1 -1
View File
@@ -13,7 +13,7 @@ export const NoRewards = () => {
return (
<div className={classes}>
<SubHeading title={t('noRewardsHaveBeenDistributedYet')} />
<p className="font-alpha text-xl">{t('checkBackSoon')}</p>
<p className="font-alpha calt text-xl">{t('checkBackSoon')}</p>
</div>
);
};
@@ -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
});
});
});
@@ -125,19 +125,12 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(1, 'Long', '0.008508132993273576');
});
it('price monitoring trigger displayed', () => {
cy.getByTestId(marketTitle).contains('Price monitoring trigger 1').click();
validateMarketDataRow(0, 'Horizon Secs', '43,200');
validateMarketDataRow(1, 'Probability', '1');
validateMarketDataRow(2, 'Auction Extension Secs', '600');
});
it('price monitoring bound displayed', () => {
cy.getByTestId(marketTitle).contains('Price monitoring bound 1').click();
validateMarketDataRow(0, 'Min Valid Price', '6.54701 ');
validateMarketDataRow(1, 'Max Valid Price', '7.97323 ');
it('price monitoring bounds displayed', () => {
cy.getByTestId(marketTitle).contains('Price monitoring bounds 1').click();
cy.get('p.col-span-1').contains('99.99999% probability price bounds');
cy.get('p.col-span-1').contains('Within 43,200 seconds');
validateMarketDataRow(0, 'Highest Price', '7.97323 ');
validateMarketDataRow(1, 'Lowest Price', '6.54701 ');
validateMarketDataRow(2, 'Reference Price', '7.22625 ');
});
@@ -84,7 +84,7 @@ export const TradeMarketHeader = ({
heading={t('Volume (24h)')}
testId="market-volume"
description={t(
'The total amount of assets traded in the last 24 hours.'
'The total number of contracts traded in the last 24 hours.'
)}
>
<Last24hVolume
+1 -1
View File
@@ -29,7 +29,7 @@ export const Banner = () => {
<Icon name="cross" className="w-6 h-6" ariaLabel="dismiss" />
</button>
<div>
<span className="pr-4">Mainnet sim 2 coming in March!</span>
<span className="pr-4">Mainnet sim 2 is live!</span>
<ExternalLink href="https://fairground.wtf/">Learn more</ExternalLink>
</div>
</div>
+29 -21
View File
@@ -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();
}
);
});
+76 -31
View File
@@ -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>
);
};
@@ -9,11 +9,15 @@ export const Preloader = () => {
display: block;
width: 100%;
height: 100%;
margin: 0;
}
html.dark body{
background: #000;
}
`}
</style>
<div className="pre-loader">
<Loader forceTheme="light" preloader />
<Loader />
</div>
</>
);
-1
View File
@@ -22,7 +22,6 @@ import {
useInitializeEnv,
} from '@vegaprotocol/environment';
import './styles.css';
import './gen-styles.scss';
import { useGlobalStore, usePageTitleStore } from '../stores';
import { Footer } from '../components/footer';
import { useMemo, useState } from 'react';
+2 -1
View File
@@ -19,12 +19,13 @@ export default function Document() {
type="image/x-icon"
href="https://static.vega.xyz/favicon.ico"
/>
<script src="/theme-setter.js" type="text/javascript" async />
{['1', 'true'].includes(process.env['NX_USE_ENV_OVERRIDES'] || '') ? (
/* eslint-disable-next-line @next/next/no-sync-scripts */
<script src="/assets/env-config.js" type="text/javascript" />
) : null}
</Head>
<body className="font-alpha liga-0-calt-0">
<body className="font-alpha dark:bg-black dark:text-white">
<Main />
<NextScript />
</body>
+35 -27
View File
@@ -4,17 +4,10 @@
min-height: 100vh;
justify-content: center;
align-items: center;
@for $i from 0 through 16 {
.loader-item:nth-child(#{$i}) {
@if $i % 2 == 0 {
animation-delay: #{$i * 50 * random(5)}ms;
animation-direction: reverse;
} @else {
animation-delay: #{$i * -50 * random(5)}ms;
animation-direction: alternate;
}
}
.loader-item {
width: 10px;
height: 10px;
background: black;
}
.pre-loader-center {
align-items: center;
@@ -27,24 +20,39 @@
display: flex;
flex-wrap: wrap;
}
@for $i from 0 through 16 {
.loader-item:nth-child(#{$i}) {
@if $i % 2 == 0 {
animation-delay: #{$i * 50 * random(5)}ms;
animation-direction: reverse;
} @else {
animation-delay: #{$i * -50 * random(5)}ms;
animation-direction: alternate;
}
}
}
.loader-item {
width: 10px;
height: 10px;
background: black;
animation: flickering 0.4s steps(2, jump-none) alternate infinite;
animation: flickering 0.4s linear alternate infinite;
}
@keyframes flickering {
0% {
opacity: 1;
}
25% {
opacity: 1;
}
26% {
opacity: 0;
}
100% {
opacity: 0;
}
}
}
@keyframes flickering {
0% {
opacity: 1;
}
25% {
opacity: 1;
}
26% {
opacity: 0;
}
100% {
opacity: 0;
html.dark {
.pre-loader {
.loader-item {
background: white;
}
}
}
+81 -71
View File
@@ -5,73 +5,10 @@
justify-content: center;
align-items: center;
}
.pre-loader .loader-item:nth-child(0) {
animation-delay: 0ms;
animation-direction: reverse;
}
.pre-loader .loader-item:first-child {
animation-delay: -0.2s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(2) {
animation-delay: 0.1s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(3) {
animation-delay: -0.15s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(4) {
animation-delay: 1s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(5) {
animation-delay: -0.25s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(6) {
animation-delay: 0.3s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(7) {
animation-delay: -1.05s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(8) {
animation-delay: 0.8s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(9) {
animation-delay: -0.9s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(10) {
animation-delay: 0.5s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(11) {
animation-delay: -2.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(12) {
animation-delay: 2.4s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(13) {
animation-delay: -0.65s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(14) {
animation-delay: 0.7s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(15) {
animation-delay: -3s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(16) {
animation-delay: 2.4s;
animation-direction: reverse;
.pre-loader .loader-item {
width: 10px;
height: 10px;
background: #000;
}
.pre-loader .pre-loader-center {
align-items: center;
@@ -84,11 +21,81 @@
display: flex;
flex-wrap: wrap;
}
@media (prefers-color-scheme: light) {
.pre-loader .loader-item {
background: #000;
}
}
.pre-loader .loader-item:nth-child(0) {
animation-delay: 0ms;
animation-direction: reverse;
}
.pre-loader .loader-item:first-child {
animation-delay: -50ms;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(2) {
animation-delay: 0.2s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(3) {
animation-delay: -0.6s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(4) {
animation-delay: 0.4s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(5) {
animation-delay: -0.5s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(6) {
animation-delay: 0.3s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(7) {
animation-delay: -1.4s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(8) {
animation-delay: 2s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(9) {
animation-delay: -0.9s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(10) {
animation-delay: 1.5s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(11) {
animation-delay: -0.55s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(12) {
animation-delay: 1.2s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(13) {
animation-delay: -0.65s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(14) {
animation-delay: 0.7s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(15) {
animation-delay: -3.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(16) {
animation-delay: 1.6s;
animation-direction: reverse;
}
.pre-loader .loader-item {
width: 10px;
height: 10px;
background: #000;
animation: flickering 0.4s steps(2, jump-none) infinite alternate;
animation: flickering 0.4s linear infinite alternate;
}
@keyframes flickering {
0% {
@@ -104,3 +111,6 @@
opacity: 0;
}
}
html.dark .pre-loader .loader-item {
background: #fff;
}
+9
View File
@@ -0,0 +1,9 @@
(function () {
var storedTheme = window.localStorage.getItem('theme');
if (
storedTheme === 'dark' ||
(!storedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
}
})();
@@ -68,6 +68,32 @@ describe('AccountsTable', () => {
'1,256.00',
'Breakdown',
'Deposit',
'Withdraw',
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
const rows = await screen.findAllByRole('row');
expect(rows.length).toBe(6);
});
it('should apply correct formatting in view as user mode', async () => {
await act(async () => {
render(
<AccountTable
rowData={singleRowData}
onClickAsset={() => null}
isReadOnly={true}
/>
);
});
const cells = await screen.findAllByRole('gridcell');
const expectedValues = [
'tBTC',
'1,256.00',
'1,256.00',
'1,256.00',
'Breakdown',
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
+22 -18
View File
@@ -154,7 +154,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
}
maxWidth={300}
/>
{!props.isReadOnly && (
{
<AgGridColumn
colId="breakdown"
headerName=""
@@ -193,29 +193,33 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
{t('Breakdown')}
</ButtonLink>
<span className="mx-1" />
<ButtonLink
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(data.asset.id);
}}
>
{t('Deposit')}
</ButtonLink>
{!props.isReadOnly && (
<ButtonLink
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(data.asset.id);
}}
>
{t('Deposit')}
</ButtonLink>
)}
<span className="mx-1" />
<ButtonLink
data-testid="withdraw"
onClick={() =>
onClickWithdraw && onClickWithdraw(data.asset.id)
}
>
{t('Withdraw')}
</ButtonLink>
{!props.isReadOnly && (
<ButtonLink
data-testid="withdraw"
onClick={() =>
onClickWithdraw && onClickWithdraw(data.asset.id)
}
>
{t('Withdraw')}
</ButtonLink>
)}
</>
);
}
}}
/>
)}
}
</AgGrid>
<Dialog size="medium" open={openBreakdown} onChange={setOpenBreakdown}>
<div className="h-[35vh] w-full m-auto flex flex-col">
+1 -1
View File
@@ -98,7 +98,7 @@ export const AssetDetailsDialog = ({
}}
>
{content}
<p className="text-sm mb-4">
<p className="text-sm my-4">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit.',
[assetSymbol]
+1 -15
View File
@@ -1,9 +1,8 @@
import { useEtherscanLink } from '@vegaprotocol/environment';
import { ContractAddressLink } from '@vegaprotocol/environment';
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import type * as Schema from '@vegaprotocol/types';
import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit';
import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit';
import { Link } from '@vegaprotocol/ui-toolkit';
import {
KeyValueTable,
KeyValueTableRow,
@@ -276,16 +275,3 @@ export const AssetDetailsTable = ({
</KeyValueTable>
);
};
// Separate component for the link as otherwise eslint will complain
// about useEnvironment being used in a component
// named with a lowercase 'value'
const ContractAddressLink = ({ address }: { address: string }) => {
const etherscanLink = useEtherscanLink();
const href = etherscanLink(`/address/${address}`);
return (
<Link href={href} target="_blank" title={t('View on etherscan')}>
{address}
</Link>
);
};
+2
View File
@@ -18,6 +18,7 @@ import { addMockTransactionResponse } from './lib/commands/mock-transaction-resp
import { addCreateMarket } from './lib/commands/create-market';
import { addConnectPublicKey } from './lib/commands/add-connect-public-key';
import { addVegaWalletSubmitProposal } from './lib/commands/vega-wallet-submit-proposal';
import { addGetNodes } from './lib/commands/get-nodes';
addGetTestIdcommand();
addSlackCommand();
@@ -28,6 +29,7 @@ addMockWeb3ProviderCommand();
addHighlightLog();
addVegaWalletReceiveFaucetedAsset();
addGetAssets();
addGetNodes();
addContainsExactly();
addGetNetworkParameters();
addUpdateCapsuleMultiSig();
+3 -8
View File
@@ -1,13 +1,14 @@
import { gql } from '@apollo/client';
import { print } from 'graphql';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { edgesToList } from '../utils';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
getAssets(): Chainable<Record<string, AssetFieldsFragment>>;
getAssets(): Chainable<Array<AssetFieldsFragment>>;
}
}
}
@@ -72,12 +73,6 @@ export function addGetAssets() {
headers: { 'content-type': 'application/json' },
})
.its('body.data.assetsConnection.edges')
.then((edges) => {
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
return edges.reduce((list, edge) => {
list[edge.node.name] = edge.node;
return list;
}, {});
});
.then(edgesToList);
});
}
@@ -0,0 +1,55 @@
import { gql } from '@apollo/client';
import type { Node } from '@vegaprotocol/types';
import { print } from 'graphql';
import { edgesToList } from '../utils';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
getNodes(): Chainable<Array<Partial<Node>>>;
}
}
}
export function addGetNodes() {
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
Cypress.Commands.add('getNodes', () => {
const query = gql`
query Nodes {
nodesConnection {
edges {
node {
__typename
avatarUrl
ethereumAddress
id
infoUrl
location
name
pendingStake
pubkey
stakedByDelegates
stakedByOperator
stakedTotal
status
tmPubkey
}
}
}
}
`;
cy.request({
method: 'POST',
url: `http://localhost:3028/query`,
body: {
query: print(query),
},
headers: { 'content-type': 'application/json' },
})
.its(`body.data.nodesConnection.edges`)
.then(edgesToList);
});
}
@@ -23,8 +23,8 @@ export function addVegaWalletReceiveFaucetedAsset() {
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
cy.getAssets().then((assets) => {
console.log(assets);
const asset = assets[assetName];
if (assets[assetName] !== undefined) {
const asset = assets.find((a) => a.name === assetName);
if (asset) {
for (let i = 0; i < asset.decimals; i++) amount += '0';
cy.exec(
`curl -X POST -d '{"amount": "${amount}", "asset": "${asset.id}", "party": "${vegaWalletPublicKey}"}' http://localhost:1790/api/v1/mint`
@@ -38,15 +38,9 @@ export function addVegaWalletReceiveFaucetedAsset() {
);
});
} else {
const validAssets = Object.keys(assets)
.filter((key) => key.includes('fake'))
.reduce((obj, key) => {
return Object.assign(obj, {
[key]: assets[key],
});
}, {});
const validAssets = assets.filter((a) => a.name.includes('fake'));
assert.exists(
assets[assetName],
asset,
`${assetName} is not a faucet-able asset, only the following assets can be faucet-ed: ${validAssets}`
);
}
+9 -1
View File
@@ -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,
},
});
}
}
+8
View File
@@ -62,3 +62,11 @@ const checkSortChange = (tabsArr: string[], column: string) => {
});
});
};
type Edges = { node: unknown }[];
export function edgesToList(edges: Edges) {
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
return edges.map((edge) => {
return edge.node;
});
}
@@ -12,7 +12,7 @@ export const DealTicketButton = ({ disabled, variant }: Props) => {
const { pubKey, isReadOnly } = useVegaWallet();
const isDisabled = !pubKey || isReadOnly || disabled;
return (
<div className="mb-4">
<div className="mb-2">
<Button
variant={variant}
fill
@@ -31,7 +31,7 @@ export const DealTicketFeeDetails = ({
<div>
{details.map(({ label, value, labelDescription, symbol }) => (
<div
key={label}
key={typeof label === 'string' ? label : 'value-dropdown'}
className="text-xs mt-2 flex justify-between items-center gap-4 flex-wrap"
>
<div>
@@ -38,7 +38,7 @@ export const DealTicketLimitAmount = ({
};
return (
<div className="mb-6">
<div className="mb-2">
<div className="flex items-center gap-4">
<div className="flex-1">
<FormGroup
@@ -29,9 +29,9 @@ export const DealTicketMarketAmount = ({
: undefined;
return (
<div className="mb-6">
<div className="mb-2">
<div className="flex items-end gap-4 mb-2">
<div className="flex-1 text-sm">Size</div>
<div className="flex-1 text-sm">{t('Size')}</div>
<div />
<div className="flex-2 text-sm text-right">
{isMarketInAuction(marketData.marketTradingMode) && (
@@ -307,7 +307,7 @@ const SummaryMessage = memo(
);
if (isReadOnly) {
return (
<div className="mb-4">
<div className="mb-2">
<InputError testId="dealticket-error-message-summary">
{
'You need to connect your own wallet to start trading on this market'
@@ -318,7 +318,7 @@ const SummaryMessage = memo(
}
if (!pubKey) {
return (
<div className="mb-4">
<div className="mb-2">
<Notification
testId={'deal-ticket-connect-wallet'}
intent={Intent.Warning}
@@ -343,7 +343,7 @@ const SummaryMessage = memo(
}
if (errorMessage === SummaryValidationType.NoCollateral) {
return (
<div className="mb-4">
<div className="mb-2">
<ZeroBalanceError
asset={market.tradableInstrument.instrument.product.settlementAsset}
onClickCollateral={onClickCollateral}
@@ -356,7 +356,7 @@ const SummaryMessage = memo(
// submission render that first
if (errorMessage) {
return (
<div className="mb-4">
<div className="mb-2">
<InputError testId="dealticket-error-message-summary">
{errorMessage}
</InputError>
@@ -368,7 +368,7 @@ const SummaryMessage = memo(
// balance render the margin warning, but still allow submission
if (balanceError) {
return (
<div className="mb-4">
<div className="mb-2">
<MarginWarning balance={balance} margin={margin} asset={asset} />;
</div>
);
@@ -383,7 +383,7 @@ const SummaryMessage = memo(
].includes(marketData.marketTradingMode)
) {
return (
<div className="mb-4">
<div className="mb-2">
<Notification
intent={Intent.Warning}
testId={'dealticket-warning-auction'}
@@ -0,0 +1,13 @@
import { t } from '@vegaprotocol/react-helpers';
import { Link } from '@vegaprotocol/ui-toolkit';
import { useEtherscanLink } from '../hooks';
export const ContractAddressLink = ({ address }: { address: string }) => {
const etherscanLink = useEtherscanLink();
const href = etherscanLink(`/address/${address}`);
return (
<Link href={href} target="_blank" title={t('View on etherscan')}>
{address}
</Link>
);
};
+1
View File
@@ -2,3 +2,4 @@ export * from './network-loader';
export * from './network-switcher';
export * from './node-guard';
export * from './node-switcher';
export * from './contract-address-link';
+1
View File
@@ -94,6 +94,7 @@ export const TOKEN_NEW_NETWORK_PARAM_PROPOSAL =
export const TOKEN_GOVERNANCE = '/proposals';
export const TOKEN_PROPOSALS = '/proposals';
export const TOKEN_PROPOSAL = '/proposals/:id';
export const TOKEN_VALIDATOR = '/validators/:id';
// Explorer pages
export const EXPLORER_TX = '/txs/:hash';
+12 -5
View File
@@ -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,
+9 -9
View File
@@ -267,24 +267,24 @@ const FeesBreakdownTooltip = ({
data-testid="fee-breakdown-tooltip"
className="max-w-sm border border-neutral-600 bg-neutral-100 dark:bg-neutral-800 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white"
>
<dl className="grid grid-cols-2 gap-x-2">
<dt>{t('Infrastructure fee')}</dt>
<dd className="text-right">
<dl className="grid grid-cols-3 gap-x-1">
<dt className="col-span-1">{t('Infrastructure fee')}</dt>
<dd className="text-right col-span-2">
{addDecimalsFormatNumber(feesObj.infrastructureFee, asset.decimals)}{' '}
{asset.symbol}
</dd>
<dt>{t('Liquidity fee')}</dt>
<dd className="text-right">
<dt className="col-span-1">{t('Liquidity fee')}</dt>
<dd className="text-right col-span-2">
{addDecimalsFormatNumber(feesObj.liquidityFee, asset.decimals)}{' '}
{asset.symbol}
</dd>
<dt>{t('Maker fee')}</dt>
<dd className="text-right">
<dt className="col-span-1">{t('Maker fee')}</dt>
<dd className="text-right col-span-2">
{addDecimalsFormatNumber(feesObj.makerFee, asset.decimals)}{' '}
{asset.symbol}
</dd>
<dt>{t('Total fees')}</dt>
<dd className="text-right">{valueFormatted}</dd>
<dt className="col-span-1">{t('Total fees')}</dt>
<dd className="text-right col-span-2">{valueFormatted}</dd>
</dl>
</div>
);
@@ -5,6 +5,8 @@ import { useVoteEvent } from './use-vote-event';
import type { VoteValue } from '@vegaprotocol/types';
import type { VoteEventFieldsFragment } from './__generated__/VoteSubsciption';
export type FinalizedVote = VoteEventFieldsFragment;
export const useVoteSubmit = () => {
const { pubKey } = useVegaWallet();
const { send, transaction, setComplete, Dialog } = useVegaTransaction();
+5 -6
View File
@@ -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}
/>
@@ -145,7 +145,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
}}
decimalPlaces={market.decimalPlaces}
/>
<p className="text-xs">
<p className="text-xs mt-4">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
[assetSymbol, quoteUnit]
@@ -249,7 +249,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
/>
<p className="text-xs">
<p className="text-xs mt-4">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
[assetSymbol, quoteUnit]
@@ -309,27 +309,47 @@ export const Info = ({ market, onSelect }: InfoProps) => {
),
},
...(market.priceMonitoringSettings?.parameters?.triggers || []).map(
(trigger, i) => ({
title: t(`Price monitoring trigger ${i + 1}`),
content: <MarketInfoTable data={trigger} />,
})
(trigger, i) => {
const bounds = market.data?.priceMonitoringBounds?.[i];
return {
title: t(`Price monitoring bounds ${i + 1}`),
content: (
<div className="text-xs">
<div className="grid grid-cols-2 text-xs mb-4">
<p className="col-span-1">
{t('%s probability price bounds', [
formatNumberPercentage(
new BigNumber(trigger.probability).times(100)
),
])}
</p>
<p className="col-span-1 text-right">
{t('Within %s seconds', [formatNumber(trigger.horizonSecs)])}
</p>
</div>
<div className="pl-2 pb-0 text-xs border-l-2">
{bounds && (
<MarketInfoTable
data={{
highestPrice: bounds.maxValidPrice,
lowestPrice: bounds.minValidPrice,
referencePrice: bounds.referencePrice,
}}
decimalPlaces={assetDecimals}
assetSymbol={quoteUnit}
/>
)}
</div>
<p className="mt-4">
{t('Results in %s seconds auction if breached', [
trigger.auctionExtensionSecs.toString(),
])}
</p>
</div>
),
};
}
),
...(market.data?.priceMonitoringBounds || []).map((trigger, i) => ({
title: t(`Price monitoring bound ${i + 1}`),
content: (
<>
<MarketInfoTable
data={trigger}
decimalPlaces={market.decimalPlaces}
omits={['referencePrice', '__typename']}
/>
<MarketInfoTable
data={{ referencePrice: trigger.referencePrice }}
decimalPlaces={assetDecimals}
/>
</>
),
})),
{
title: t('Liquidity monitoring parameters'),
content: (
@@ -369,37 +389,38 @@ export const Info = ({ market, onSelect }: InfoProps) => {
content: (
<>
<p className="text-xs mb-4">
{`For liquidity orders count towards a commitment they have to be
within either the liquidity or price monitoring bounds (whichever is
tighter).`}
{`For liquidity orders to count towards a commitment, they must be
within the liquidity monitoring bounds.`}
</p>
<p className="text-xs mb-4">
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
price.`}
</p>
<MarketInfoTable
data={{
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
lowestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
highestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
}}
></MarketInfoTable>
<div className="pl-2 pb-0 text-xs border-l-2">
<MarketInfoTable
data={{
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
lowestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
highestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
}}
></MarketInfoTable>
</div>
</>
),
},
@@ -63,7 +63,7 @@ export const StatsManager = ({ className }: StatsManagerProps) => {
<div className={classes}>
<h3
data-testid="stats-environment"
className="font-alpha uppercase text-2xl pb-8 col-span-full"
className="font-alpha calt uppercase text-2xl pb-8 col-span-full"
>
{(error && `/ ${error}`) ||
(data ? `/ ${VEGA_ENV}` : '/ Connecting...')}
@@ -166,7 +166,7 @@ export const OrderListManager = ({
/>
</div>
</div>
{hasActiveOrder && (
{!isReadOnly && hasActiveOrder && (
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
<Button
size="sm"
@@ -0,0 +1,267 @@
import { countryCodeToFlagEmoji, FALLBACK_FLAG } from './flag-emoji';
// REGIONAL INDICATOR SYMBOLS:
// 🇦
// 🇧
// 🇨
// 🇩
// 🇪
// 🇫
// 🇬
// 🇭
// 🇮
// 🇯
// 🇰
// 🇱
// 🇲
// 🇳
// 🇴
// 🇵
// 🇶
// 🇷
// 🇸
// 🇹
// 🇺
// 🇻
// 🇼
// 🇽
// 🇾
// 🇿
const cases = [
['AC', '🇦🇨'],
['AD', '🇦🇩'],
['AE', '🇦🇪'],
['AF', '🇦🇫'],
['AG', '🇦🇬'],
['AI', '🇦🇮'],
['AL', '🇦🇱'],
['AM', '🇦🇲'],
['AO', '🇦🇴'],
['AR', '🇦🇷'],
['AS', '🇦🇸'],
['AT', '🇦🇹'],
['AQ', '🇦🇶'],
['AW', '🇦🇼'],
['AZ', '🇦🇿'],
['BA', '🇧🇦'],
['BB', '🇧🇧'],
['BD', '🇧🇩'],
['BE', '🇧🇪'],
['BF', '🇧🇫'],
['BG', '🇧🇬'],
['BH', '🇧🇭'],
['BI', '🇧🇮'],
['BJ', '🇧🇯'],
['BL', '🇧🇱'],
['BM', '🇧🇲'],
['BN', '🇧🇳'],
['BO', '🇧🇴'],
['BR', '🇧🇷'],
['BS', '🇧🇸'],
['BT', '🇧🇹'],
['BQ', '🇧🇶'],
['BW', '🇧🇼'],
['BY', '🇧🇾'],
['BZ', '🇧🇿'],
['CA', '🇨🇦'],
['CC', '🇨🇨'],
['CD', '🇨🇩'],
['CF', '🇨🇫'],
['CG', '🇨🇬'],
['CH', '🇨🇭'],
['CI', '🇨🇮'],
['CK', '🇨🇰'],
['CL', '🇨🇱'],
['CM', '🇨🇲'],
['CN', '🇨🇳'],
['CO', '🇨🇴'],
['CP', '🇨🇵'],
['CR', '🇨🇷'],
['CW', '🇨🇼'],
['CY', '🇨🇾'],
['CZ', '🇨🇿'],
['DE', '🇩🇪'],
['DG', '🇩🇬'],
['DJ', '🇩🇯'],
['DK', '🇩🇰'],
['DM', '🇩🇲'],
['DO', '🇩🇴'],
['DZ', '🇩🇿'],
['EA', '🇪🇦'],
['EC', '🇪🇨'],
['EE', '🇪🇪'],
['EG', '🇪🇬'],
['EH', '🇪🇭'],
['ER', '🇪🇷'],
['ES', '🇪🇸'],
['ET', '🇪🇹'],
['FI', '🇫🇮'],
['FJ', '🇫🇯'],
['FK', '🇫🇰'],
['FM', '🇫🇲'],
['FO', '🇫🇴'],
['FR', '🇫🇷'],
['GA', '🇬🇦'],
['GB', '🇬🇧'],
['GD', '🇬🇩'],
['GE', '🇬🇪'],
['GF', '🇬🇫'],
['GG', '🇬🇬'],
['GH', '🇬🇭'],
['GI', '🇬🇮'],
['GL', '🇬🇱'],
['GM', '🇬🇲'],
['GN', '🇬🇳'],
['GP', '🇬🇵'],
['GR', '🇬🇷'],
['GS', '🇬🇸'],
['GT', '🇬🇹'],
['GQ', '🇬🇶'],
['GW', '🇬🇼'],
['GY', '🇬🇾'],
['HK', '🇭🇰'],
['HM', '🇭🇲'],
['HN', '🇭🇳'],
['HR', '🇭🇷'],
['HT', '🇭🇹'],
['IC', '🇮🇨'],
['ID', '🇮🇩'],
['IE', '🇮🇪'],
['IL', '🇮🇱'],
['IM', '🇮🇲'],
['IN', '🇮🇳'],
['IO', '🇮🇴'],
['IR', '🇮🇷'],
['IS', '🇮🇸'],
['IT', '🇮🇹'],
['IQ', '🇮🇶'],
['JE', '🇯🇪'],
['JM', '🇯🇲'],
['JO', '🇯🇴'],
['JP', '🇯🇵'],
['KE', '🇰🇪'],
['KG', '🇰🇬'],
['KH', '🇰🇭'],
['KI', '🇰🇮'],
['KM', '🇰🇲'],
['KN', '🇰🇳'],
['KP', '🇰🇵'],
['KR', '🇰🇷'],
['KW', '🇰🇼'],
['KY', '🇰🇾'],
['KZ', '🇰🇿'],
['LA', '🇱🇦'],
['LB', '🇱🇧'],
['LC', '🇱🇨'],
['LI', '🇱🇮'],
['LK', '🇱🇰'],
['LR', '🇱🇷'],
['LS', '🇱🇸'],
['LT', '🇱🇹'],
['LY', '🇱🇾'],
['MA', '🇲🇦'],
['MC', '🇲🇨'],
['MD', '🇲🇩'],
['ME', '🇲🇪'],
['MF', '🇲🇫'],
['MG', '🇲🇬'],
['MH', '🇲🇭'],
['MK', '🇲🇰'],
['ML', '🇲🇱'],
['MM', '🇲🇲'],
['MN', '🇲🇳'],
['MO', '🇲🇴'],
['MP', '🇲🇵'],
['MR', '🇲🇷'],
['MS', '🇲🇸'],
['MT', '🇲🇹'],
['MQ', '🇲🇶'],
['MW', '🇲🇼'],
['MY', '🇲🇾'],
['MZ', '🇲🇿'],
['NA', '🇳🇦'],
['NC', '🇳🇨'],
['NE', '🇳🇪'],
['NF', '🇳🇫'],
['NG', '🇳🇬'],
['NI', '🇳🇮'],
['NL', '🇳🇱'],
['NO', '🇳🇴'],
['NP', '🇳🇵'],
['NR', '🇳🇷'],
['NZ', '🇳🇿'],
['OM', '🇴🇲'],
['PA', '🇵🇦'],
['PE', '🇵🇪'],
['PF', '🇵🇫'],
['PG', '🇵🇬'],
['PH', '🇵🇭'],
['PK', '🇵🇰'],
['PL', '🇵🇱'],
['PM', '🇵🇲'],
['PN', '🇵🇳'],
['PR', '🇵🇷'],
['PS', '🇵🇸'],
['PT', '🇵🇹'],
['PW', '🇵🇼'],
['PY', '🇵🇾'],
['RE', '🇷🇪'],
['RO', '🇷🇴'],
['RS', '🇷🇸'],
['RW', '🇷🇼'],
['SA', '🇸🇦'],
['SB', '🇸🇧'],
['SC', '🇸🇨'],
['SD', '🇸🇩'],
['SE', '🇸🇪'],
['SG', '🇸🇬'],
['SH', '🇸🇭'],
['SI', '🇸🇮'],
['SJ', '🇸🇯'],
['SK', '🇸🇰'],
['SL', '🇸🇱'],
['SM', '🇸🇲'],
['SN', '🇸🇳'],
['SO', '🇸🇴'],
['SR', '🇸🇷'],
['SS', '🇸🇸'],
['ST', '🇸🇹'],
['SY', '🇸🇾'],
['SZ', '🇸🇿'],
['TA', '🇹🇦'],
['TC', '🇹🇨'],
['TD', '🇹🇩'],
['TF', '🇹🇫'],
['TG', '🇹🇬'],
['TH', '🇹🇭'],
['TJ', '🇹🇯'],
['TK', '🇹🇰'],
['TL', '🇹🇱'],
['TM', '🇹🇲'],
['TN', '🇹🇳'],
['TO', '🇹🇴'],
['TR', '🇹🇷'],
['TT', '🇹🇹'],
['TW', '🇹🇼'],
['TZ', '🇹🇿'],
['QA', '🇶🇦'],
['VG', '🇻🇬'],
['WF', '🇼🇫'],
['WS', '🇼🇸'],
['YE', '🇾🇪'],
['YT', '🇾🇹'],
['ZA', '🇿🇦'],
['ZM', '🇿🇲'],
['ZW', '🇿🇼'],
// unknown
['AA', FALLBACK_FLAG],
['XX', FALLBACK_FLAG],
['AAA', FALLBACK_FLAG],
];
describe('countryCodeToFlagEmoji', () => {
it.each(cases)('converts %s to %s', (countryCode, flag) => {
expect(countryCodeToFlagEmoji(countryCode)).toEqual(flag);
});
});
+24
View File
@@ -0,0 +1,24 @@
import compact from 'lodash/compact';
export const FALLBACK_FLAG = '🏳';
const KNOWN_CODES = `AC AD AE AF AG AI AL AM AO AR AS AT AQ AW AZ BA BB BD BE BF
BG BH BI BJ BL BM BN BO BR BS BT BQ BW BY BZ CA CC CD CF CG CH CI CK CL CM CN
CO CP CR CW CY CZ DE DG DJ DK DM DO DZ EA EC EE EG EH ER ES ET FI FJ FK FM FO
FR GA GB GD GE GF GG GH GI GL GM GN GP GR GS GT GQ GW GY HK HM HN HR HT IC ID
IE IL IM IN IO IR IS IT IQ JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB
LC LI LK LR LS LT LY MA MC MD ME MF MG MH MK ML MM MN MO MP MR MS MT MQ MW MY
MZ NA NC NE NF NG NI NL NO NP NR NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW
PY RE RO RS RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SY SZ TA TC
TD TF TG TH TJ TK TL TM TN TO TR TT TW TZ QA VG WF WS YE YT ZA ZM ZW`;
export const countryCodeToFlagEmoji = (countryCode: string) => {
const code = countryCode.trim().toUpperCase();
const known = compact(KNOWN_CODES.split(' ').map((ch) => ch.trim()));
if (known.includes(code)) {
return code.replace(/./g, (char) =>
String.fromCodePoint(0x1f1a5 + char.toUpperCase().charCodeAt(0))
);
}
return FALLBACK_FLAG;
};
@@ -6,6 +6,7 @@ import {
formatNumber,
formatNumberPercentage,
isNumeric,
toDecimal,
toNumberParts,
} from './number';
@@ -197,3 +198,20 @@ describe('compactNumber', () => {
}
);
});
describe('toDecimal', () => {
it.each([
{ v: 0, o: '1' },
{ v: 1, o: '0.1' },
{ v: 2, o: '0.01' },
{ v: 3, o: '0.001' },
{ v: 4, o: '0.0001' },
{ v: 5, o: '0.00001' },
{ v: 6, o: '0.000001' },
{ v: 7, o: '0.0000001' },
{ v: 8, o: '0.00000001' },
{ v: 9, o: '0.000000001' },
])('formats with toNumber given number correctly', ({ v, o }) => {
expect(toDecimal(v)).toStrictEqual(o);
});
});
@@ -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,
});
});
});
+7 -6
View File
@@ -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';
+1
View File
@@ -19,3 +19,4 @@ export * from './remove-pagination-wrapper';
export * from './storage';
export * from './time';
export * from './validate';
export * from './flag-emoji';
+3 -25
View File
@@ -122,33 +122,11 @@ module.exports = {
fontFamily: {
mono: ['Roboto Mono', 'monospace'],
sans: [
'"Helvetica Neue"',
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'Arial',
'"Noto Sans"',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
'"Noto Color Emoji"',
'"Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
],
alpha: [
'AlphaLyrae',
'"Helvetica Neue"',
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'Arial',
'"Noto Sans"',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
'"Noto Color Emoji"',
'AlphaLyrae, "Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
{ fontFeatureSettings: '"calt" 0, "liga" 0' },
],
},
keyframes: {
@@ -7,12 +7,13 @@ const vegaCustomClasses = plugin(function ({ addUtilities }) {
'.calt': {
fontFeatureSettings: "'calt'",
},
'.liga-0-calt-0': {
fontFeatureSettings: "'liga' 0, 'calt' 0",
},
'.liga': {
fontFeatureSettings: "'liga'",
},
// Fix for Firefox to make it inherit font-feature-settings from the default theme
'button, input, optgroup, select, textarea': {
fontFeatureSettings: 'inherit',
},
'.syntax-highlighter-wrapper .hljs': {
fontSize: '1rem',
fontFamily: "'Roboto Mono', monospace",
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/ui-toolkit",
"version": "0.0.1"
"version": "0.7.0"
}
@@ -73,7 +73,7 @@ export const DropdownMenuContent = forwardRef<
<DropdownMenuPrimitive.Content
{...contentProps}
ref={forwardedRef}
className="min-w-[290px] bg-neutral-200 dark:bg-white p-2 rounded"
className="min-w-[290px] bg-neutral-200 dark:bg-white p-2 rounded text-black"
align="start"
sideOffset={10}
/>
@@ -20,7 +20,7 @@ export const FormGroup = ({
labelAlign = 'left',
hideLabel = false,
}: FormGroupProps) => {
const wrapperClasses = classNames('relative mb-6', className);
const wrapperClasses = classNames('relative mb-2', className);
const labelClasses = classNames('block mb-2 text-sm', {
'text-right': labelAlign === 'right',
'sr-only': hideLabel,
@@ -7,6 +7,7 @@ export interface KeyValueTableProps
children: React.ReactNode;
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
numerical?: boolean;
className?: string;
}
export const KeyValueTable = ({
@@ -14,6 +15,7 @@ export const KeyValueTable = ({
children,
numerical,
headingLevel,
className,
...rest
}: KeyValueTableProps) => {
const TitleTag: keyof JSX.IntrinsicElements = headingLevel
@@ -22,7 +24,7 @@ export const KeyValueTable = ({
return (
<React.Fragment>
{title && <TitleTag className={`text-xl my-2`}>{title}</TitleTag>}
<div data-testid="key-value-table" {...rest} className="mb-4">
<div data-testid="key-value-table" {...rest} className={className}>
<div>
{children &&
React.Children.map(
@@ -0,0 +1,28 @@
@for $i from 0 through 16 {
.loader-item:nth-child(#{$i}) {
@if $i % 2 == 0 {
animation-delay: #{$i * 50 * random(5)}ms;
animation-direction: reverse;
} @else {
animation-delay: #{$i * -50 * random(5)}ms;
animation-direction: alternate;
}
}
}
.loader-item {
animation: flickering 0.4s linear alternate infinite;
}
@keyframes flickering {
0% {
opacity: 1;
}
25% {
opacity: 1;
}
26% {
opacity: 0;
}
100% {
opacity: 0;
}
}
@@ -1,5 +1,6 @@
import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import { useMemo } from 'react';
import styles from './loader.module.scss';
const pseudoRandom = (seed: number) => {
let value = seed;
@@ -12,27 +13,10 @@ const pseudoRandom = (seed: number) => {
export interface LoaderProps {
size?: 'small' | 'large';
forceTheme?: 'dark' | 'light';
preloader?: boolean;
}
export const Loader = ({
size = 'large',
forceTheme,
preloader,
}: LoaderProps) => {
const [, forceRender] = useState(false);
useEffect(() => {
const interval = preloader
? undefined
: setInterval(() => {
forceRender((x) => !x);
}, 100);
return () => clearInterval(interval);
}, [preloader]);
const itemClasses = classNames('loader-item', {
export const Loader = ({ size = 'large', forceTheme }: LoaderProps) => {
const itemClasses = classNames('loader-item', styles['loader-item'], {
'dark:bg-white bg-black': !forceTheme,
'bg-white': forceTheme === 'dark',
'bg-black': forceTheme === 'light',
@@ -209,7 +209,7 @@ export const MaintenancePage = () => {
</div>
</div>
<div className="flex mb-2">
<span className="font-alpha uppercase text-xl text-center">
<span className="font-alpha calt uppercase text-xl text-center">
{t("We're doing some maintenance right now, check back later")}
</span>
</div>

Some files were not shown because too many files have changed in this diff Show More