feat(explorer): validators page (#2982)
This commit is contained in:
parent
23ce480daa
commit
a3fcd6b7dc
@ -8,7 +8,7 @@ context('Asset page', { tags: '@regression' }, () => {
|
|||||||
|
|
||||||
it('should be able to see full assets list', () => {
|
it('should be able to see full assets list', () => {
|
||||||
cy.getAssets().then((assets) => {
|
cy.getAssets().then((assets) => {
|
||||||
Object.values(assets).forEach((asset) => {
|
assets.forEach((asset) => {
|
||||||
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
|
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -25,7 +25,7 @@ context('Asset page', { tags: '@regression' }, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cy.getAssets().then((assets) => {
|
cy.getAssets().then((assets) => {
|
||||||
Object.values(assets).forEach((asset) => {
|
assets.forEach((asset) => {
|
||||||
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
|
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"', () => {
|
it('should open details page when clicked on "View details"', () => {
|
||||||
cy.getAssets().then((assets) => {
|
cy.getAssets().then((assets) => {
|
||||||
Object.values(assets).forEach((asset) => {
|
assets.forEach((asset) => {
|
||||||
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
||||||
.eq(0)
|
.eq(0)
|
||||||
.should('contain.text', 'View details');
|
.should('contain.text', 'View details');
|
||||||
|
@ -1,303 +1,15 @@
|
|||||||
context('Validator page', { tags: '@smoke' }, function () {
|
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 () {
|
before('Visit validators page and obtain data', function () {
|
||||||
cy.visit('/');
|
cy.visit('/validators');
|
||||||
cy.get(validatorMenuHeading).click();
|
|
||||||
cy.get_validators().as('validators');
|
|
||||||
cy.get_nodes().as('nodes');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Verify elements on page', function () {
|
describe('Verify elements on page', function () {
|
||||||
before('Ensure at least two validators are present', function () {
|
it('should be able to see validator tiles', function () {
|
||||||
assert.isAtLeast(
|
cy.getNodes().then((nodes) => {
|
||||||
this.validators.length,
|
nodes.forEach((node) => {
|
||||||
2,
|
cy.get(`[validator-id="${node.id}"]`).should('be.visible');
|
||||||
'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;
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
1
apps/explorer/src/app/components/page-helpers/index.ts
Normal file
1
apps/explorer/src/app/components/page-helpers/index.ts
Normal file
@ -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>
|
||||||
|
);
|
45
apps/explorer/src/app/components/page-helpers/page-title.tsx
Normal file
45
apps/explorer/src/app/components/page-helpers/page-title.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
55
apps/explorer/src/app/hooks/use-tendermint-validators.ts
Normal file
55
apps/explorer/src/app/hooks/use-tendermint-validators.ts
Normal file
@ -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 { t } from '@vegaprotocol/react-helpers';
|
||||||
import { RouteTitle } from '../../components/route-title';
|
|
||||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||||
@ -8,6 +7,7 @@ import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { PageTitle } from '../../components/page-helpers/page-title';
|
||||||
|
|
||||||
export const AssetPage = () => {
|
export const AssetPage = () => {
|
||||||
useDocumentTitle(['Assets']);
|
useDocumentTitle(['Assets']);
|
||||||
@ -22,18 +22,25 @@ export const AssetPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="relative">
|
<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
|
<AsyncRenderer
|
||||||
noDataMessage={t('Asset not found')}
|
noDataMessage={t('Asset not found')}
|
||||||
data={data}
|
data={data}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
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">
|
<div className="h-full relative">
|
||||||
<AssetDetailsTable asset={data as AssetFieldsFragment} />
|
<AssetDetailsTable asset={data as AssetFieldsFragment} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,12 +3,12 @@ import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { MarketDetails } from '../../components/markets/market-details';
|
import { MarketDetails } from '../../components/markets/market-details';
|
||||||
import { RouteTitle } from '../../components/route-title';
|
|
||||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||||
import compact from 'lodash/compact';
|
import compact from 'lodash/compact';
|
||||||
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||||
import { marketInfoNoCandlesDataProvider } from '@vegaprotocol/market-info';
|
import { marketInfoNoCandlesDataProvider } from '@vegaprotocol/market-info';
|
||||||
|
import { PageTitle } from '../../components/page-helpers/page-title';
|
||||||
|
|
||||||
export const MarketPage = () => {
|
export const MarketPage = () => {
|
||||||
useScrollToLocation();
|
useScrollToLocation();
|
||||||
@ -40,20 +40,25 @@ export const MarketPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="relative">
|
<section className="relative">
|
||||||
<RouteTitle data-testid="markets-heading">
|
<PageTitle
|
||||||
{data?.market?.tradableInstrument.instrument.name}
|
data-testid="markets-heading"
|
||||||
</RouteTitle>
|
title={data?.market?.tradableInstrument.instrument.name || ''}
|
||||||
|
actions={
|
||||||
|
<Button
|
||||||
|
disabled={!data?.market}
|
||||||
|
size="xs"
|
||||||
|
onClick={() => setDialogOpen(true)}
|
||||||
|
>
|
||||||
|
{t('View JSON')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
noDataMessage={t('This chain has no markets')}
|
noDataMessage={t('This chain has no markets')}
|
||||||
data={data}
|
data={data}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 right-0">
|
|
||||||
<Button size="xs" onClick={() => setDialogOpen(true)}>
|
|
||||||
{t('View JSON')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<MarketDetails market={data?.market} />
|
<MarketDetails market={data?.market} />
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
</section>
|
</section>
|
||||||
|
@ -9,7 +9,7 @@ import Party from './parties';
|
|||||||
import { Parties } from './parties/home';
|
import { Parties } from './parties/home';
|
||||||
import { Party as PartySingle } from './parties/id';
|
import { Party as PartySingle } from './parties/id';
|
||||||
import Txs from './txs';
|
import Txs from './txs';
|
||||||
import Validators from './validators';
|
import { ValidatorsPage } from './validators';
|
||||||
import Genesis from './genesis';
|
import Genesis from './genesis';
|
||||||
import { Block } from './blocks/id';
|
import { Block } from './blocks/id';
|
||||||
import { Blocks } from './blocks/home';
|
import { Blocks } from './blocks/home';
|
||||||
@ -125,7 +125,7 @@ const validators: Route[] = flags.validators
|
|||||||
path: Routes.VALIDATORS,
|
path: Routes.VALIDATORS,
|
||||||
name: 'Validators',
|
name: 'Validators',
|
||||||
text: t('Validators'),
|
text: t('Validators'),
|
||||||
element: <Validators />,
|
element: <ValidatorsPage />,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
@ -1,47 +1 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
export * from './validators-page';
|
||||||
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;
|
|
||||||
|
@ -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;
|
|
||||||
}
|
|
340
apps/explorer/src/app/routes/validators/validators-page.tsx
Normal file
340
apps/explorer/src/app/routes/validators/validators-page.tsx
Normal file
@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,9 +1,8 @@
|
|||||||
import { useEtherscanLink } from '@vegaprotocol/environment';
|
import { ContractAddressLink } from '@vegaprotocol/environment';
|
||||||
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
||||||
import type * as Schema from '@vegaprotocol/types';
|
import type * as Schema from '@vegaprotocol/types';
|
||||||
import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit';
|
import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit';
|
||||||
import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit';
|
import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit';
|
||||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import {
|
import {
|
||||||
KeyValueTable,
|
KeyValueTable,
|
||||||
KeyValueTableRow,
|
KeyValueTableRow,
|
||||||
@ -276,16 +275,3 @@ export const AssetDetailsTable = ({
|
|||||||
</KeyValueTable>
|
</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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -18,6 +18,7 @@ import { addMockTransactionResponse } from './lib/commands/mock-transaction-resp
|
|||||||
import { addCreateMarket } from './lib/commands/create-market';
|
import { addCreateMarket } from './lib/commands/create-market';
|
||||||
import { addConnectPublicKey } from './lib/commands/add-connect-public-key';
|
import { addConnectPublicKey } from './lib/commands/add-connect-public-key';
|
||||||
import { addVegaWalletSubmitProposal } from './lib/commands/vega-wallet-submit-proposal';
|
import { addVegaWalletSubmitProposal } from './lib/commands/vega-wallet-submit-proposal';
|
||||||
|
import { addGetNodes } from './lib/commands/get-nodes';
|
||||||
|
|
||||||
addGetTestIdcommand();
|
addGetTestIdcommand();
|
||||||
addSlackCommand();
|
addSlackCommand();
|
||||||
@ -28,6 +29,7 @@ addMockWeb3ProviderCommand();
|
|||||||
addHighlightLog();
|
addHighlightLog();
|
||||||
addVegaWalletReceiveFaucetedAsset();
|
addVegaWalletReceiveFaucetedAsset();
|
||||||
addGetAssets();
|
addGetAssets();
|
||||||
|
addGetNodes();
|
||||||
addContainsExactly();
|
addContainsExactly();
|
||||||
addGetNetworkParameters();
|
addGetNetworkParameters();
|
||||||
addUpdateCapsuleMultiSig();
|
addUpdateCapsuleMultiSig();
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import { print } from 'graphql';
|
import { print } from 'graphql';
|
||||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||||
|
import { edgesToList } from '../utils';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
interface Chainable<Subject> {
|
interface Chainable<Subject> {
|
||||||
getAssets(): Chainable<Record<string, AssetFieldsFragment>>;
|
getAssets(): Chainable<Array<AssetFieldsFragment>>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,12 +73,6 @@ export function addGetAssets() {
|
|||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
})
|
})
|
||||||
.its('body.data.assetsConnection.edges')
|
.its('body.data.assetsConnection.edges')
|
||||||
.then((edges) => {
|
.then(edgesToList);
|
||||||
// @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;
|
|
||||||
}, {});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
55
libs/cypress/src/lib/commands/get-nodes.ts
Normal file
55
libs/cypress/src/lib/commands/get-nodes.ts
Normal file
@ -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
|
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
|
||||||
cy.getAssets().then((assets) => {
|
cy.getAssets().then((assets) => {
|
||||||
console.log(assets);
|
console.log(assets);
|
||||||
const asset = assets[assetName];
|
const asset = assets.find((a) => a.name === assetName);
|
||||||
if (assets[assetName] !== undefined) {
|
if (asset) {
|
||||||
for (let i = 0; i < asset.decimals; i++) amount += '0';
|
for (let i = 0; i < asset.decimals; i++) amount += '0';
|
||||||
cy.exec(
|
cy.exec(
|
||||||
`curl -X POST -d '{"amount": "${amount}", "asset": "${asset.id}", "party": "${vegaWalletPublicKey}"}' http://localhost:1790/api/v1/mint`
|
`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 {
|
} else {
|
||||||
const validAssets = Object.keys(assets)
|
const validAssets = assets.filter((a) => a.name.includes('fake'));
|
||||||
.filter((key) => key.includes('fake'))
|
|
||||||
.reduce((obj, key) => {
|
|
||||||
return Object.assign(obj, {
|
|
||||||
[key]: assets[key],
|
|
||||||
});
|
|
||||||
}, {});
|
|
||||||
assert.exists(
|
assert.exists(
|
||||||
assets[assetName],
|
asset,
|
||||||
`${assetName} is not a faucet-able asset, only the following assets can be faucet-ed: ${validAssets}`
|
`${assetName} is not a faucet-able asset, only the following assets can be faucet-ed: ${validAssets}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
13
libs/environment/src/components/contract-address-link.tsx
Normal file
13
libs/environment/src/components/contract-address-link.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -2,3 +2,4 @@ export * from './network-loader';
|
|||||||
export * from './network-switcher';
|
export * from './network-switcher';
|
||||||
export * from './node-guard';
|
export * from './node-guard';
|
||||||
export * from './node-switcher';
|
export * from './node-switcher';
|
||||||
|
export * from './contract-address-link';
|
||||||
|
@ -94,6 +94,7 @@ export const TOKEN_NEW_NETWORK_PARAM_PROPOSAL =
|
|||||||
export const TOKEN_GOVERNANCE = '/proposals';
|
export const TOKEN_GOVERNANCE = '/proposals';
|
||||||
export const TOKEN_PROPOSALS = '/proposals';
|
export const TOKEN_PROPOSALS = '/proposals';
|
||||||
export const TOKEN_PROPOSAL = '/proposals/:id';
|
export const TOKEN_PROPOSAL = '/proposals/:id';
|
||||||
|
export const TOKEN_VALIDATOR = '/validators/:id';
|
||||||
|
|
||||||
// Explorer pages
|
// Explorer pages
|
||||||
export const EXPLORER_TX = '/txs/:hash';
|
export const EXPLORER_TX = '/txs/:hash';
|
||||||
|
267
libs/react-helpers/src/lib/flag-emoji.spec.ts
Normal file
267
libs/react-helpers/src/lib/flag-emoji.spec.ts
Normal file
@ -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
libs/react-helpers/src/lib/flag-emoji.ts
Normal file
24
libs/react-helpers/src/lib/flag-emoji.ts
Normal 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;
|
||||||
|
};
|
@ -19,3 +19,4 @@ export * from './remove-pagination-wrapper';
|
|||||||
export * from './storage';
|
export * from './storage';
|
||||||
export * from './time';
|
export * from './time';
|
||||||
export * from './validate';
|
export * from './validate';
|
||||||
|
export * from './flag-emoji';
|
||||||
|
@ -7,6 +7,7 @@ export interface KeyValueTableProps
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
numerical?: boolean;
|
numerical?: boolean;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KeyValueTable = ({
|
export const KeyValueTable = ({
|
||||||
@ -14,6 +15,7 @@ export const KeyValueTable = ({
|
|||||||
children,
|
children,
|
||||||
numerical,
|
numerical,
|
||||||
headingLevel,
|
headingLevel,
|
||||||
|
className,
|
||||||
...rest
|
...rest
|
||||||
}: KeyValueTableProps) => {
|
}: KeyValueTableProps) => {
|
||||||
const TitleTag: keyof JSX.IntrinsicElements = headingLevel
|
const TitleTag: keyof JSX.IntrinsicElements = headingLevel
|
||||||
@ -22,7 +24,11 @@ export const KeyValueTable = ({
|
|||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{title && <TitleTag className={`text-xl my-2`}>{title}</TitleTag>}
|
{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={classNames('mb-4', className)}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{children &&
|
{children &&
|
||||||
React.Children.map(
|
React.Children.map(
|
||||||
|
Loading…
Reference in New Issue
Block a user