Improvements from the PR

This commit is contained in:
sam-keen 2022-03-10 15:57:48 +00:00
commit 2d635808de
120 changed files with 5872 additions and 3913 deletions

View File

@ -1,7 +1,7 @@
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nrwl/nx"],
"plugins": ["@nrwl/nx", "eslint-plugin-unicorn"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
@ -18,6 +18,13 @@
}
]
}
],
"unicorn/filename-case": [
"error",
{
"case": "kebabCase",
"ignore": ["\\[[a-zA-Z]+\\]\\.page"]
}
]
}
},

55
.github/workflows/cypress.yml vendored Normal file
View File

@ -0,0 +1,55 @@
name: Cypress tests
on:
push:
branches:
- master
- develop
pull_request:
jobs:
master:
name: Run end-to-end tests - main
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' }}
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v2
with:
main-branch-name: master
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16
- name: Install root dependencies
run: yarn install
- name: Run Cypress tests
run: npx nx affected:e2e --parallel=5 --record --key ${{ secrets.CYPRESS_RECORD_KEY }}
pr:
name: Run end-to-end tests - PR
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }}
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v2
with:
main-branch-name: master
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16
- name: Install root dependencies
run: yarn install
- name: Run Cypress tests
run: npx nx affected:e2e --parallel=5 --record --key ${{ secrets.CYPRESS_RECORD_KEY }}

View File

@ -1,11 +1,11 @@
---
name: Publish
"on":
'on':
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+-pre[0-9]+"
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-pre[0-9]+'
jobs:
build:

View File

@ -1,6 +1,6 @@
module.exports = {
stories: [],
addons: ['@storybook/addon-essentials'],
addons: ['@storybook/addon-essentials', '@storybook/addon-a11y'],
// uncomment the property below if you want to apply some webpack config globally
// webpackFinal: async (config, { configType }) => {
// // Make whatever fine-grained changes you need that should apply to all storybook configs

View File

@ -2,3 +2,11 @@ NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-
NX_TENDERMINT_URL = "https://lb.testnet.vega.xyz/tm"
NX_TENDERMINT_WEBSOCKET_URL = "wss://lb.testnet.vega.xyz/tm/websocket"
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

View File

@ -0,0 +1,34 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
NX_VERSION=$npm_package_version
NX_REPOSITORY_URL=$REPOSITORY_URL
NX_BRANCH=$BRANCH
NX_PULL_REQUEST=$PULL_REQUEST
NX_HEAD=$HEAD
NX_COMMIT_REF=$COMMIT_REF
NX_CONTEXT=$CONTEXT
NX_REVIEW_ID=$REVIEW_ID
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
NX_TENDERMINT_URL = "https://n04.d.vega.xyz/tm"
NX_TENDERMINT_WEBSOCKET_URL = "wss://n04.d.vega.xyz/tm/websocket"
NX_VEGA_URL = "https://n04.d.vega.xyz/query"
# App flags
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

View File

@ -0,0 +1,34 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
NX_VERSION=$npm_package_version
NX_REPOSITORY_URL=$REPOSITORY_URL
NX_BRANCH=$BRANCH
NX_PULL_REQUEST=$PULL_REQUEST
NX_HEAD=$HEAD
NX_COMMIT_REF=$COMMIT_REF
NX_CONTEXT=$CONTEXT
NX_REVIEW_ID=$REVIEW_ID
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
NX_TENDERMINT_URL = "https://mainnet-observer-proxy01.ops.vega.xyz/"
NX_TENDERMINT_WEBSOCKET_URL = "wss://mainnet-observer-proxy01.ops.vega.xyz/websocket"
NX_VEGA_URL = "https://api.token.vega.xyz/query"
# App flags
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

View File

@ -0,0 +1,35 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
NX_VERSION=$npm_package_version
NX_REPOSITORY_URL=$REPOSITORY_URL
NX_BRANCH=$BRANCH
NX_PULL_REQUEST=$PULL_REQUEST
NX_HEAD=$HEAD
NX_COMMIT_REF=$COMMIT_REF
NX_CONTEXT=$CONTEXT
NX_REVIEW_ID=$REVIEW_ID
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
NX_TENDERMINT_URL = "https://n03.s.vega.xyz/tm"
NX_TENDERMINT_WEBSOCKET_URL = "wss://n03.s.vega.xyz/tm/websocket"
NX_VEGA_URL = "https://n03.s.vega.xyz/query"
# App flags
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

View File

@ -0,0 +1,34 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
NX_VERSION=$npm_package_version
NX_REPOSITORY_URL=$REPOSITORY_URL
NX_BRANCH=$BRANCH
NX_PULL_REQUEST=$PULL_REQUEST
NX_HEAD=$HEAD
NX_COMMIT_REF=$COMMIT_REF
NX_CONTEXT=$CONTEXT
NX_REVIEW_ID=$REVIEW_ID
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
NX_TENDERMINT_URL = "https://n03.stagnet2.vega.xyz/tm"
NX_TENDERMINT_WEBSOCKET_URL = "wss://n03.stagnet2.vega.xyz/tm/websocket"
NX_VEGA_URL = "https://n03.stagnet2.vega.xyz/query"
# App flags
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

View File

@ -0,0 +1,34 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
NX_VERSION=$npm_package_version
NX_REPOSITORY_URL=$REPOSITORY_URL
NX_BRANCH=$BRANCH
NX_PULL_REQUEST=$PULL_REQUEST
NX_HEAD=$HEAD
NX_COMMIT_REF=$COMMIT_REF
NX_CONTEXT=$CONTEXT
NX_REVIEW_ID=$REVIEW_ID
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
NX_TENDERMINT_URL = "https://lb.testnet.vega.xyz/tm"
NX_TENDERMINT_WEBSOCKET_URL = "wss://lb.testnet.vega.xyz/tm/websocket"
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
# App flags
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

View File

@ -1,5 +1,6 @@
{
"baseUrl": "http://localhost:4200",
"projectId": "et4snf",
"fileServerFolder": ".",
"fixturesFolder": false,
"pluginsFile": "./src/plugins/index.js",

View File

@ -0,0 +1,6 @@
Feature: Asset Page
Scenario: Navigate to Asset Page
Given I am on the homepage
When I navigate to the asset page
Then asset page is correctly displayed

View File

@ -0,0 +1,6 @@
Feature: Blocks Page
Scenario: Navigate to blocks page
Given I am on the homepage
When I navigate to the blocks page
Then blocks page is correctly displayed

View File

@ -1,4 +1,4 @@
Feature: Home page
Scenario: Visit Home page
Given I go to the homepage
Given I am on the homepage

View File

@ -0,0 +1,6 @@
Feature: Markets Page
Scenario: Navigate to markets page
Given I am on the homepage
When I navigate to the markets page
Then markets page is correctly displayed

View File

@ -0,0 +1,6 @@
Feature: Network parameters Page
Scenario: Navigate to network parameters page
Given I am on the homepage
When I navigate to the transactions page
Then transactions page is correctly displayed

View File

@ -0,0 +1,6 @@
Feature: Transactions Page
Scenario: Navigate to transactions page
Given I am on the homepage
When I navigate to the transactions page
Then transactions page is correctly displayed

View File

@ -0,0 +1,6 @@
Feature: Validators Page
Scenario: Navigate to validators page
Given I am on the homepage
When I navigate to the validators page
Then validators page is correctly displayed

View File

@ -13,6 +13,7 @@ declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
getByTestId(selector: string): Chainable<JQuery<HTMLElement>>;
}
}
//
@ -31,3 +32,6 @@ Cypress.Commands.add('login', (email, password) => {
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add('getByTestId', (selector, ...args) => {
return cy.get(`[data-testid=${selector}]`, ...args);
});

View File

@ -0,0 +1,9 @@
import BasePage from './base-page';
export default class AssetsPage extends BasePage {
assetHeader = 'asset-header';
validateAssetsDisplayed() {
this.validateBlockDataDisplayed(this.assetHeader);
}
}

View File

@ -0,0 +1,79 @@
export default class BasePage {
transactionsUrl = '/txs';
blocksUrl = '/blocks';
partiesUrl = '/parties';
assetsUrl = '/assets';
genesisUrl = '/genesis';
governanceUrl = '/governance';
marketsUrl = '/markets';
networkParametersUrl = '/network-parameters';
validatorsUrl = '/validators';
blockExplorerHeader = 'explorer-header';
searchField = 'search-input';
navigateToTxs() {
cy.get(`a[href='${this.transactionsUrl}']`).click();
}
navigateToBlocks() {
cy.get(`a[href='${this.blocksUrl}']`).click();
}
navigateToParties() {
cy.get(`a[href='${this.partiesUrl}']`).click();
}
navigateToAssets() {
cy.get(`a[href*='${this.assetsUrl}']`).click();
}
navigateToGenesis() {
cy.get(`a[href='${this.genesisUrl}']`).click();
}
navigateToGovernance() {
cy.get(`a[href='${this.governanceUrl}']`).click();
}
navigateToMarkets() {
cy.get(`a[href='${this.marketsUrl}']`).click();
}
navigateToNetworkParameters() {
cy.get(`a[href='${this.networkParametersUrl}']`).click();
}
navigateToValidators() {
cy.get(`a[href='${this.validatorsUrl}']`).click();
}
search(searchText) {
cy.getByTestId(this.searchField).type(searchText);
}
validateSearchDisplayed() {
cy.getByTestId(this.blockExplorerHeader).should(
'have.text',
'Vega Block Explorer'
);
cy.getByTestId(this.searchField).should('be.visible');
}
validateBlockDataDisplayed(headerTestId) {
cy.getByTestId(headerTestId).then(($assetHeaders) => {
const headersAmount = parseInt($assetHeaders.length);
cy.wrap($assetHeaders).each(($header) => {
expect($header).to.not.be.empty;
});
cy.get('.language-json')
.each(($asset, index, $list) => {
expect($asset).to.not.be.empty;
})
.then(($list) => {
expect($list).to.have.length(headersAmount);
});
});
}
}

View File

@ -0,0 +1,9 @@
import BasePage from './base-page';
export default class GenesisPage extends BasePage {
GenesisHeader = 'genesis-header';
genesisFieldsDisplayed() {
this.validateBlockDataDisplayed(this.GenesisHeader);
}
}

View File

@ -0,0 +1,3 @@
import BasePage from './base-page';
export default class HomePage extends BasePage {}

View File

@ -0,0 +1,9 @@
import BasePage from './base-page';
export default class MarketsPage extends BasePage {
marketHeaders = 'markets-header';
validateMarketDataDisplayed() {
this.validateBlockDataDisplayed(this.marketHeaders);
}
}

View File

@ -0,0 +1,14 @@
import BasePage from './base-page';
export default class NetworkParametersPage extends BasePage {
networkParametersHeader = 'network-param-header';
parameters = 'parameters';
verifyNetworkParametersDisplayed() {
cy.getByTestId(this.networkParametersHeader).should(
'have.text',
'NetworkParameters'
);
cy.getByTestId(this.parameters).should('not.be.empty');
}
}

View File

@ -0,0 +1,37 @@
import BasePage from './base-page';
export default class TransactionsPage extends BasePage {
blockRow = 'block-row';
blockHeight = 'block-height';
numberOfTransactions = 'num-txs';
validatorLink = 'validator-link';
blockTime = 'block-time';
refreshBtn = 'refresh';
validateTransactionsPagedisplayed() {
cy.getByTestId(this.blockRow).should('have.length.above', 1);
cy.getByTestId(this.blockHeight).first().should('not.be.empty');
cy.getByTestId(this.numberOfTransactions).first().should('not.be.empty');
cy.getByTestId(this.validatorLink).first().should('not.be.empty');
cy.getByTestId(this.blockTime).first().should('not.be.empty');
}
validateRefreshBtn() {
cy.getByTestId(this.blockHeight)
.first()
.invoke('text')
.then((blockHeightTxt) => {
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
//Wait needed to allow blocks to change
cy.getByTestId(this.refreshBtn).click();
cy.getByTestId(this.blockHeight)
.first()
.invoke('text')
.should((newBlockHeightText) => {
expect(blockHeightTxt).not.to.eq(newBlockHeightText);
});
});
cy.getByTestId(this.blockTime).first();
}
}

View File

@ -0,0 +1,18 @@
import BasePage from './base-page';
export default class ValidatorPage extends BasePage {
tendermintHeader = 'tendermint-header';
vegaHeader = 'vega-header';
tendermintData = 'tendermint-data';
vegaData = 'vega-data';
validateValidatorsDisplayed() {
cy.getByTestId(this.tendermintHeader).should(
'have.text',
'Tendermint data'
);
cy.getByTestId(this.tendermintData).should('not.be.empty');
cy.getByTestId(this.vegaHeader).should('have.text', 'Vega data');
cy.getByTestId(this.vegaData).should('not.be.empty');
}
}

View File

@ -0,0 +1,12 @@
import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
import AssetsPage from '../pages/assets-page';
const assetPage = new AssetsPage();
When('I navigate to the asset page', () => {
assetPage.navigateToAssets();
});
Then('asset page is correctly displayed', () => {
assetPage.validateAssetsDisplayed();
});

View File

@ -0,0 +1,5 @@
import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
Given('I am on the homepage', () => {
cy.visit('/');
});

View File

@ -1,5 +1 @@
import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
Given('I go to the homepage', () => {
cy.visit('/');
});

View File

@ -0,0 +1,12 @@
import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
import MarketsPage from '../pages/markets-page';
const marketsPage = new MarketsPage();
When('I navigate to the markets page', () => {
marketsPage.navigateToMarkets();
});
Then('markets page is correctly displayed', () => {
marketsPage.validateMarketDataDisplayed();
});

View File

@ -0,0 +1,22 @@
import { Then, When } from 'cypress-cucumber-preprocessor/steps';
import TransactionsPage from '../pages/transactions-page';
const transactionsPage = new TransactionsPage();
When('I navigate to the transactions page', () => {
transactionsPage.navigateToTxs();
});
When('I navigate to the blocks page', () => {
transactionsPage.navigateToBlocks();
});
Then('transactions page is correctly displayed', () => {
transactionsPage.validateTransactionsPagedisplayed();
transactionsPage.validateRefreshBtn();
});
Then('blocks page is correctly displayed', () => {
transactionsPage.validateTransactionsPagedisplayed();
transactionsPage.validateRefreshBtn();
});

View File

@ -0,0 +1,12 @@
import { Then, When } from 'cypress-cucumber-preprocessor/steps';
import ValidatorPage from '../pages/validators-page';
const validatorsPage = new ValidatorPage();
When('I navigate to the validators page', () => {
validatorsPage.navigateToValidators();
});
Then('validators page is correctly displayed', () => {
validatorsPage.validateValidatorsDisplayed();
});

View File

@ -20,6 +20,15 @@ NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
NX_TENDERMINT_URL = "https://mainnet-observer-proxy01.ops.vega.xyz"
NX_TENDERMINT_WEBSOCKET_URL = "wss://mainnet-observer-proxy01.ops.vega.xyz/websocket"
NX_VEGA_URL = "http://mainnet-observer.ops.vega.xyz:3008/query"
NX_TENDERMINT_URL = "https://lb.testnet.vega.xyz/tm"
NX_TENDERMINT_WEBSOCKET_URL = "wss://lb.testnet.vega.xyz/tm/websocket"
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
# App flags
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

34
apps/explorer/.env.devent Normal file
View File

@ -0,0 +1,34 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
NX_VERSION=$npm_package_version
NX_REPOSITORY_URL=$REPOSITORY_URL
NX_BRANCH=$BRANCH
NX_PULL_REQUEST=$PULL_REQUEST
NX_HEAD=$HEAD
NX_COMMIT_REF=$COMMIT_REF
NX_CONTEXT=$CONTEXT
NX_REVIEW_ID=$REVIEW_ID
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
NX_TENDERMINT_URL = "https://n04.d.vega.xyz/tm"
NX_TENDERMINT_WEBSOCKET_URL = "wss://n04.d.vega.xyz/tm/websocket"
NX_VEGA_URL = "https://n04.d.vega.xyz/query"
# App flags
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

View File

@ -0,0 +1,34 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
NX_VERSION=$npm_package_version
NX_REPOSITORY_URL=$REPOSITORY_URL
NX_BRANCH=$BRANCH
NX_PULL_REQUEST=$PULL_REQUEST
NX_HEAD=$HEAD
NX_COMMIT_REF=$COMMIT_REF
NX_CONTEXT=$CONTEXT
NX_REVIEW_ID=$REVIEW_ID
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
NX_TENDERMINT_URL = "https://mainnet-observer-proxy01.ops.vega.xyz/"
NX_TENDERMINT_WEBSOCKET_URL = "wss://mainnet-observer-proxy01.ops.vega.xyz/websocket"
NX_VEGA_URL = "https://api.token.vega.xyz/query"
# App flags
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

View File

@ -0,0 +1,35 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
NX_VERSION=$npm_package_version
NX_REPOSITORY_URL=$REPOSITORY_URL
NX_BRANCH=$BRANCH
NX_PULL_REQUEST=$PULL_REQUEST
NX_HEAD=$HEAD
NX_COMMIT_REF=$COMMIT_REF
NX_CONTEXT=$CONTEXT
NX_REVIEW_ID=$REVIEW_ID
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
NX_TENDERMINT_URL = "https://n03.s.vega.xyz/tm"
NX_TENDERMINT_WEBSOCKET_URL = "wss://n03.s.vega.xyz/tm/websocket"
NX_VEGA_URL = "https://n03.s.vega.xyz/query"
# App flags
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

View File

@ -0,0 +1,34 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
NX_VERSION=$npm_package_version
NX_REPOSITORY_URL=$REPOSITORY_URL
NX_BRANCH=$BRANCH
NX_PULL_REQUEST=$PULL_REQUEST
NX_HEAD=$HEAD
NX_COMMIT_REF=$COMMIT_REF
NX_CONTEXT=$CONTEXT
NX_REVIEW_ID=$REVIEW_ID
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
NX_TENDERMINT_URL = "https://n03.stagnet2.vega.xyz/tm"
NX_TENDERMINT_WEBSOCKET_URL = "wss://n03.stagnet2.vega.xyz/tm/websocket"
NX_VEGA_URL = "https://n03.stagnet2.vega.xyz/query"
# App flags
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

View File

@ -0,0 +1,34 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
NX_VERSION=$npm_package_version
NX_REPOSITORY_URL=$REPOSITORY_URL
NX_BRANCH=$BRANCH
NX_PULL_REQUEST=$PULL_REQUEST
NX_HEAD=$HEAD
NX_COMMIT_REF=$COMMIT_REF
NX_CONTEXT=$CONTEXT
NX_REVIEW_ID=$REVIEW_ID
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
NX_TENDERMINT_URL = "https://lb.testnet.vega.xyz/tm"
NX_TENDERMINT_WEBSOCKET_URL = "wss://lb.testnet.vega.xyz/tm/websocket"
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
# App flags
NX_EXPLORER_ASSETS = 1
NX_EXPLORER_GENESIS = 1
NX_EXPLORER_GOVERNANCE = 1
NX_EXPLORER_MARKETS = 1
NX_EXPLORER_NETWORK_PARAMETERS = 1
NX_EXPLORER_PARTIES = 1
NX_EXPLORER_VALIDATORS = 1

View File

@ -74,6 +74,19 @@
}
]
}
},
"build-netlify": {
"builder": "@nrwl/workspace:run-commands",
"options": {
"commands": [
{
"command": "cp apps/explorer/netlify.toml netlify.toml"
},
{
"command": "nx build explorer"
}
]
}
}
},
"tags": []

View File

@ -4,7 +4,11 @@ interface BlocksRefetchProps {
export const BlocksRefetch = ({ refetch }: BlocksRefetchProps) => {
return (
<button onClick={() => refetch()} className="underline mb-28">
<button
onClick={() => refetch()}
className="underline mb-28"
data-testid="refresh"
>
Refresh to see latest blocks
</button>
);

View File

@ -3,7 +3,7 @@ import { TendermintBlockchainResponse } from '../../../routes/blocks/tendermint-
import { Link } from 'react-router-dom';
import { SecondsAgo } from '../../seconds-ago';
import { TxsPerBlock } from '../../txs/txs-per-block';
import { Table } from '../../table';
import { Table, TableRow, TableCell } from '../../table';
interface BlocksProps {
data: TendermintBlockchainResponse | undefined;
@ -20,33 +20,39 @@ export const BlocksTable = ({ data, showTransactions }: BlocksProps) => {
{data.result?.block_metas?.map((block, index) => {
return (
<React.Fragment key={index}>
<tr className="bg-neutral-850 border-b-4 border-b-black">
<td className="pl-4 py-2">
<TableRow dataTestId="block-row" modifier="background">
<TableCell dataTestId="block-height" className="pl-4 py-2">
<Link
to={`/blocks/${block.header?.height}`}
className="text-vega-yellow"
>
{block.header?.height}
</Link>
</td>
<td className="px-8 text-center">
</TableCell>
<TableCell dataTestId="num-txs" className="px-8 text-center">
{block.num_txs === '1'
? '1 transaction'
: `${block.num_txs} transactions`}
</td>
<td className="px-8 text-center">
</TableCell>
<TableCell
dataTestId="validator-link"
className="px-8 text-center"
>
<Link to={`/validators/${block.header?.proposer_address}`}>
{block.header.proposer_address}
</Link>
</td>
<td className="text-center pr-28 text-neutral-300">
</TableCell>
<TableCell
dataTestId="block-time"
className="text-center pr-28 text-neutral-300"
>
<SecondsAgo date={block.header?.time} />
</td>
</tr>
</TableCell>
</TableRow>
{showTransactions && (
<tr>
<TableRow>
<TxsPerBlock blockHeight={block.header?.height} />
</tr>
</TableRow>
)}
</React.Fragment>
);

View File

@ -28,12 +28,16 @@ export const JumpToBlock = () => {
</label>
<input
id="block-input"
type='tel'
type="tel"
name={'blockNumber'}
placeholder={'Block number'}
className="form-input"
className="bg-white-25 border-white border px-8 py-4 placeholder-white-60"
/>
<input
className="border-white border px-28 py-4 cursor-pointer"
type={'submit'}
value={'Go'}
/>
<input className="form-submit" type={'submit'} value={'Go'} />
</form>
);
};

View File

@ -0,0 +1,19 @@
import classnames from 'classnames';
import React from 'react';
interface RouteTitleProps {
children: React.ReactNode;
className?: string;
}
export const RouteTitle = ({ children, className }: RouteTitleProps) => {
const classes = classnames(
'font-alpha',
'text-h3',
'uppercase',
'mt-12',
'mb-28',
className
);
return <h1 className={classes}>{children}</h1>;
};

View File

@ -91,10 +91,15 @@ const Search = () => {
const { search, onChange } = useGuess();
return (
<section>
<h1>Vega Block Explorer</h1>
<h1 data-testid="explorer-header">Vega Block Explorer</h1>
<fieldset>
<label htmlFor="search">Search: </label>
<input name="search" value={search} onChange={(e) => onChange(e)} />
<input
data-testid="search-field"
name="search"
value={search}
onChange={(e) => onChange(e)}
/>
</fieldset>
</section>
);

View File

@ -1 +1,76 @@
export { Table } from './table';
import React, { ThHTMLAttributes } from 'react';
import classnames from 'classnames';
interface TableProps {
children: React.ReactNode;
}
interface TableHeaderProps extends ThHTMLAttributes<HTMLTableRowElement> {
children: React.ReactNode;
className?: string;
}
interface TableChildProps {
children: React.ReactNode;
className?: string;
dataTestId?: string;
modifier?: 'bordered' | 'background';
}
export const Table = ({ children }: TableProps) => {
return (
<div className="overflow-x-auto whitespace-nowrap mb-28">
<table className="w-full">
<tbody>{children}</tbody>
</table>
</div>
);
};
export const TableRow = ({
children,
className,
dataTestId,
modifier,
}: TableChildProps) => {
const cellClasses = classnames(className, {
'border-b border-white-40': modifier === 'bordered',
'bg-white-25 border-b-4 border-b-black': modifier === 'background',
});
return (
<tr className={cellClasses} data-testid={dataTestId || null}>
{children}
</tr>
);
};
export const TableHeader = ({
children,
className,
...props
}: TableHeaderProps) => {
const cellClasses = classnames(className, {
'text-left, font-normal': props?.scope === 'row',
});
return (
<tr className={cellClasses} {...props}>
{children}
</tr>
);
};
export const TableCell = ({
children,
className,
dataTestId,
modifier,
}: TableChildProps) => {
const cellClasses = classnames(className, {
'py-4': modifier === 'bordered',
});
return (
<td className={cellClasses} data-testid={dataTestId || null}>
{children}
</td>
);
};

View File

@ -1,15 +0,0 @@
import React from 'react';
interface TableProps {
children: React.ReactNode;
}
export const Table = ({ children }: TableProps) => {
return (
<div className="overflow-x-auto whitespace-nowrap mb-28">
<table className="w-full">
<tbody>{children}</tbody>
</table>
</div>
);
};

View File

@ -5,7 +5,6 @@ const ELLIPSIS = '\u2026';
interface TruncateInlineProps {
text: string | null;
className?: string;
style?: React.CSSProperties;
children?: (truncatedText: string) => React.ReactElement;
startChars?: number; // number chars to show before ellipsis
endChars?: number; // number of chars to show after ellipsis
@ -20,7 +19,6 @@ interface TruncateInlineProps {
export function TruncateInline({
text,
className,
style,
children,
startChars,
endChars,
@ -32,7 +30,6 @@ export function TruncateInline({
const wrapperProps = {
title: text,
style,
className,
};

View File

@ -1,7 +1,7 @@
import { Link } from 'react-router-dom';
import { Routes } from '../../../routes/router-config';
import { Result } from '../../../routes/txs/tendermint-transaction-response.d';
import { Table } from '../../table';
import { Table, TableRow, TableCell } from '../../table';
interface TxDetailsProps {
txData: Result | undefined;
@ -15,35 +15,35 @@ export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
return (
<Table>
<tr>
<td>Hash</td>
<td>{txData.hash}</td>
</tr>
<TableRow>
<TableCell>Hash</TableCell>
<TableCell dataTestId="hash">{txData.hash}</TableCell>
</TableRow>
{pubKey ? (
<tr>
<TableRow>
<td>Submitted by</td>
<td>
<td data-testid="submitted-by">
<Link to={`/${Routes.PARTIES}/${pubKey}`}>{pubKey}</Link>
</td>
</tr>
</TableRow>
) : (
<tr>
<TableRow>
<td>Submitted by</td>
<td>Awaiting decoded transaction data</td>
</tr>
</TableRow>
)}
{txData.height ? (
<tr>
<TableRow>
<td>Block</td>
<td>
<td data-testid="block">
<Link to={`/blocks/${txData.height}`}>{txData.height}</Link>
</td>
</tr>
</TableRow>
) : null}
<tr>
<TableRow>
<td>Encoded tnx</td>
<td>{txData.tx}</td>
</tr>
<td data-testid="encoded-tnx">{txData.tx}</td>
</TableRow>
</Table>
);
};

View File

@ -1,5 +1,5 @@
import React from "react";
import { WebSocketHook } from "react-use-websocket/dist/lib/types";
import React from 'react';
import { WebSocketHook } from 'react-use-websocket/dist/lib/types';
export type WebsocketContextShape = WebSocketHook;
@ -9,7 +9,7 @@ export const TendermintWebsocketContext =
export function useTendermintWebsocketContext() {
const context = React.useContext(TendermintWebsocketContext);
if (context === null) {
throw new Error("useWebsocket must be used within WebsocketContext");
throw new Error('useWebsocket must be used within WebsocketContext');
}
return context;
}

View File

@ -0,0 +1,13 @@
const truthy = ['1', 'true'];
export default {
assets: truthy.includes(process.env['NX_EXPLORER_ASSETS'] || ''),
genesis: truthy.includes(process.env['NX_EXPLORER_GENESIS'] || ''),
governance: truthy.includes(process.env['NX_EXPLORER_GOVERNANCE'] || ''),
markets: truthy.includes(process.env['NX_EXPLORER_MARKETS'] || ''),
networkParameters: truthy.includes(
process.env['NX_EXPLORER_NETWORK_PARAMETERS'] || ''
),
parties: truthy.includes(process.env['NX_EXPLORER_PARTIES'] || ''),
validators: truthy.includes(process.env['NX_EXPLORER_VALIDATORS'] || ''),
};

View File

@ -39,7 +39,7 @@ const Assets = () => {
<h1>Assets</h1>
{data?.assets.map((a) => (
<React.Fragment key={a.id}>
<h2>
<h2 data-testid="asset-header">
{a.name} ({a.symbol})
</h2>
<SyntaxHighlighter data={a} />

View File

@ -1,6 +1,7 @@
import { DATA_SOURCES } from '../../../config';
import useFetch from '../../../hooks/use-fetch';
import { TendermintBlockchainResponse } from '../tendermint-blockchain-response';
import { RouteTitle } from '../../../components/route-title';
import { BlocksTable, BlocksRefetch } from '../../../components/blocks';
import { JumpToBlock } from '../../../components/jump-to-block';
@ -15,7 +16,7 @@ const Blocks = () => {
return (
<>
<section>
<h1 className="route-header">Blocks</h1>
<RouteTitle>Blocks</RouteTitle>
<BlocksRefetch refetch={refetch} />
<BlocksTable data={data} />
</section>

View File

@ -3,9 +3,15 @@ import { Link, useParams } from 'react-router-dom';
import { DATA_SOURCES } from '../../../config';
import useFetch from '../../../hooks/use-fetch';
import { TendermintBlocksResponse } from '../tendermint-blocks-response';
import { RouteTitle } from '../../../components/route-title';
import { TxsPerBlock } from '../../../components/txs/txs-per-block';
import { SecondsAgo } from '../../../components/seconds-ago';
import { Table } from '../../../components/table';
import {
Table,
TableRow,
TableHeader,
TableCell,
} from '../../../components/table';
const Block = () => {
const { block } = useParams<{ block: string }>();
@ -23,25 +29,25 @@ const Block = () => {
return (
<section>
<h1 className="route-header">BLOCK {block}</h1>
<RouteTitle>BLOCK {block}</RouteTitle>
<Table>
<tr className="table-bordered-tr">
<td className="table-bordered-td">Mined by</td>
<td className="table-bordered-td">
<TableRow modifier="bordered">
<TableHeader scope="row">Mined by</TableHeader>
<TableCell modifier="bordered">
<Link
className="text-vega-yellow"
to={`/validators/${header.proposer_address}`}
>
{header.proposer_address}
</Link>
</td>
</tr>
<tr className="table-bordered-tr">
<td className="table-bordered-td">Time</td>
<td className="table-bordered-td">
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableHeader scope="row">Time</TableHeader>
<TableCell modifier="bordered">
<SecondsAgo date={header.time} />
</td>
</tr>
</TableCell>
</TableRow>
</Table>
{blockData?.result.block.data.txs.length > 0 && (
<TxsPerBlock blockHeight={block} />

View File

@ -12,7 +12,7 @@ const Genesis = () => {
if (!genesis?.result.genesis) return null;
return (
<section>
<h1>Genesis</h1>
<h1 data-testid="genesis-header">Genesis</h1>
<SyntaxHighlighter data={genesis?.result.genesis} />
</section>
);

View File

@ -1,7 +1,24 @@
import { gql, useQuery } from '@apollo/client';
import React from 'react';
import { SyntaxHighlighter } from '../../components/syntax-highlighter';
import { ProposalsQuery } from './__generated__/ProposalsQuery';
import {
ProposalsQuery,
ProposalsQuery_proposals_terms_change,
} from './__generated__/ProposalsQuery';
export function getProposalName(change: ProposalsQuery_proposals_terms_change) {
if (change.__typename === 'NewAsset') {
return `New asset: ${change.symbol}`;
} else if (change.__typename === 'NewMarket') {
return `New market: ${change.instrument.name}`;
} else if (change.__typename === 'UpdateMarket') {
return `Update market: ${change.marketId}`;
} else if (change.__typename === 'UpdateNetworkParameter') {
return `Update network: ${change.networkParameter.key}`;
}
return 'Unknown proposal';
}
const PROPOSAL_QUERY = gql`
query ProposalsQuery {
@ -90,7 +107,7 @@ const Governance = () => {
{data.proposals.map((p) => (
<React.Fragment key={p.id}>
{/* TODO get proposal name generator from console */}
<h2>{p.id}</h2>
<h2>{getProposalName(p.terms.change)}</h2>
<SyntaxHighlighter data={p} />
</React.Fragment>
))}

View File

@ -153,7 +153,7 @@ const Markets = () => {
<h1>Markets</h1>
{data.markets.map((m) => (
<React.Fragment key={m.id}>
<h2>{m.name}</h2>
<h2 data-testid="markets-header">{m.name}</h2>
<SyntaxHighlighter data={m} />
</React.Fragment>
))}

View File

@ -1,5 +1,5 @@
import { gql, useQuery } from "@apollo/client";
import { NetworkParametersQuery } from "./__generated__/NetworkParametersQuery";
import { gql, useQuery } from '@apollo/client';
import { NetworkParametersQuery } from './__generated__/NetworkParametersQuery';
export const NETWORK_PARAMETERS_QUERY = gql`
query NetworkParametersQuery {
@ -14,8 +14,8 @@ const NetworkParameters = () => {
const { data } = useQuery<NetworkParametersQuery>(NETWORK_PARAMETERS_QUERY);
return (
<section>
<h1>NetworkParameters</h1>
<pre>{JSON.stringify(data, null, " ")}</pre>
<h1 data-testid="network-param-header">NetworkParameters</h1>
<pre data-testid="parameters">{JSON.stringify(data, null, ' ')}</pre>
</section>
);
};

View File

@ -15,7 +15,7 @@ import { Blocks } from './blocks/home';
import { Tx } from './txs/id';
import { Txs as TxHome } from './txs/home';
import { PendingTxs } from './pending';
import flags from '../lib/flags';
export const Routes = {
HOME: '/',
TX: 'txs',
@ -29,6 +29,85 @@ export const Routes = {
NETWORK_PARAMETERS: 'network-parameters',
};
const partiesRoutes = flags.parties
? [
{
path: Routes.PARTIES,
name: 'Parties',
element: <Party />,
children: [
{
index: true,
element: <Parties />,
},
{
path: ':party',
element: <PartySingle />,
},
],
},
]
: [];
const assetsRoutes = flags.assets
? [
{
path: Routes.ASSETS,
name: 'Assets',
element: <Assets />,
},
]
: [];
const genesisRoutes = flags.genesis
? [
{
path: Routes.GENESIS,
name: 'Genesis',
element: <Genesis />,
},
]
: [];
const governanceRoutes = flags.governance
? [
{
path: Routes.GOVERNANCE,
name: 'Governance',
element: <Governance />,
},
]
: [];
const marketsRoutes = flags.markets
? [
{
path: Routes.MARKETS,
name: 'Markets',
element: <Markets />,
},
]
: [];
const networkParametersRoutes = flags.networkParameters
? [
{
path: Routes.NETWORK_PARAMETERS,
name: 'NetworkParameters',
element: <NetworkParameters />,
},
]
: [];
const validators = flags.validators
? [
{
path: Routes.VALIDATORS,
name: 'Validators',
element: <Validators />,
},
]
: [];
const routerConfig = [
{
path: Routes.HOME,
@ -70,51 +149,13 @@ const routerConfig = [
},
],
},
{
path: Routes.PARTIES,
name: 'Parties',
element: <Party />,
children: [
{
index: true,
element: <Parties />,
},
{
path: ':party',
element: <PartySingle />,
},
],
},
{
path: Routes.ASSETS,
name: 'Assets',
element: <Assets />,
},
{
path: Routes.GENESIS,
name: 'Genesis',
element: <Genesis />,
},
{
path: Routes.GOVERNANCE,
name: 'Governance',
element: <Governance />,
},
{
path: Routes.MARKETS,
name: 'Markets',
element: <Markets />,
},
{
path: Routes.NETWORK_PARAMETERS,
name: 'NetworkParameters',
element: <NetworkParameters />,
},
{
path: Routes.VALIDATORS,
name: 'Validators',
element: <Validators />,
},
...partiesRoutes,
...assetsRoutes,
...genesisRoutes,
...governanceRoutes,
...marketsRoutes,
...networkParametersRoutes,
...validators,
];
export default routerConfig;

View File

@ -1,6 +1,7 @@
import useFetch from '../../../hooks/use-fetch';
import { TendermintBlockchainResponse } from '../../blocks/tendermint-blockchain-response';
import { DATA_SOURCES } from '../../../config';
import { RouteTitle } from '../../../components/route-title';
import { BlocksTable, BlocksRefetch } from '../../../components/blocks';
import { JumpToBlock } from '../../../components/jump-to-block';
@ -15,7 +16,7 @@ const Txs = () => {
return (
<>
<section>
<h1>Transactions</h1>
<RouteTitle>Transactions</RouteTitle>
<BlocksRefetch refetch={refetch} />
<BlocksTable data={data} showTransactions={true} />
</section>

View File

@ -44,10 +44,12 @@ const Validators = () => {
return (
<section>
<h1>Validators</h1>
<h2>Tendermint data</h2>
<pre>{JSON.stringify(validators, null, ' ')}</pre>
<h2>Vega data</h2>
<pre>{JSON.stringify(data, null, ' ')}</pre>
<h2 data-testid="tendermint-header">Tendermint data</h2>
<pre data-testid="tendermint-data">
{JSON.stringify(validators, null, ' ')}
</pre>
<h2 data-testid="vega-header">Vega data</h2>
<pre data-testid="vega-data">{JSON.stringify(data, null, ' ')}</pre>
</section>
);
};

View File

@ -1,3 +1,5 @@
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
@ -5,6 +7,18 @@ import './styles.css';
import App from './app/app';
const dsn = process.env['NX_SENTRY_DSN'];
/* istanbul ignore next */
if (dsn) {
Sentry.init({
dsn,
integrations: [new BrowserTracing()],
tracesSampleRate: 0.1,
environment: process.env['NODE_ENV'],
});
}
ReactDOM.render(
<StrictMode>
<BrowserRouter>

View File

@ -1,26 +1,4 @@
/* You can add global styles to this file, and also import other style files */
@tailwind base;
@tailwind components;
.route-header {
@apply font-alpha text-h3 uppercase mt-12 mb-28;
}
.table-bordered-tr {
@apply border-b border-neutral-500;
}
.table-bordered-td {
@apply py-4;
}
/* Used for text, tel input */
.form-input {
@apply bg-neutral-800 border-white border px-8 py-4 placeholder-neutral-300;
}
.form-submit {
@apply border-white border px-28 py-4;
}
@tailwind utilities;

View File

@ -5,7 +5,7 @@
"next",
"next/core-web-vitals"
],
"ignorePatterns": ["!**/*"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],

View File

@ -1,13 +1,14 @@
export function Vega({ className }: { className?: string }) {
return (
<svg className="w-[76px] h-[16px]" viewBox="0 0 283.5 61.3">
<path d="M26.6,53.1L44,0h8.8L31.2,61.3h-9.5L0,0h8.9L26.6,53.1z" />
<path d="M89.6,33.3v21.1h34.3v6.9H81.3V0h41.7V7H89.6v19.3h29.8v6.9H89.6z" />
<path
d="M156.8,7.5h7.4V54h-7.4V7.5z M193.7,0v7.5h-29.5V0H193.7z M164.2,61.3V54h29.5v7.4H164.2z M201.1,54h-7.4V39.2H179v-7.4
h22.1V54z M201.1,7.5v7.4h-7.4V7.5H201.1z"
/>
<path d="M283.5,61.3h-8.6L270.4,49h-30.8L235,61.3h-8.4L250.4,0h9.3L283.5,61.3z M254.8,7.4L242.2,42h25.6L255,7.4H254.8z" />
<svg
width="86"
height="19"
viewBox="0 0 86 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path d="M8.05624 16.4619L13.3478 0.0158333H16.0045L9.45138 18.9881H6.58031L0 0.0158333H2.70328L8.05624 16.4619ZM27.1835 10.3067V16.8372H37.5787V18.9873H24.6884V0.0150417H37.3503V2.16442H27.1835V8.16288H36.2395V10.3067H27.1835ZM47.5793 2.31167H49.8165V16.716H47.5793V2.31167V2.31167ZM58.7676 0V2.31167H49.8157V0H58.7683H58.7676ZM49.8157 19V16.716H58.7683V19H49.8157ZM61.0055 16.716H58.7683V12.1481H54.2924V9.86021H61.0055V16.716ZM61.0055 2.31167V4.59483H58.7683V2.31167H61.0055V2.31167ZM86 18.9873H83.3946L82.0251 15.1668H72.6801L71.3106 18.9873H68.7635L75.9769 0.0150417H78.7936L86 18.9873V18.9873ZM77.3138 2.299L73.4694 13.0213H81.239L77.3674 2.29821H77.313L77.3138 2.299Z" />
</svg>
);
}

View File

@ -1,21 +1,18 @@
import { useRouter } from 'next/router';
import classNames from 'classnames';
import { Vega } from '../icons/vega';
import Link from 'next/link';
import { AnchorButton } from '@vegaprotocol/ui-toolkit';
export const Navbar = () => {
const navClasses = classNames(
'flex items-center',
'border-neutral-200 border-b'
);
return (
<nav className={navClasses}>
<nav className="flex items-center">
<Link href="/" passHref={true}>
<a className="px-8">
<Vega />
<a className="px-[26px]">
<Vega className="fill-black dark:fill-white" />
</a>
</Link>
{[
{ name: 'Trading', path: '/', exact: true },
{ name: 'Portfolio', path: '/portfolio' },
{ name: 'Markets', path: '/markets' },
].map((route) => (
@ -28,18 +25,17 @@ export const Navbar = () => {
interface NavLinkProps {
name: string;
path: string;
exact?: boolean;
}
const NavLink = ({ name, path }: NavLinkProps) => {
const NavLink = ({ name, path, exact }: NavLinkProps) => {
const router = useRouter();
const className = classNames('inline-block', 'p-8', {
// Handle direct math and child page matches
'text-vega-pink': router.asPath === path || router.asPath.startsWith(path),
});
const isActive =
router.asPath === path || (!exact && router.asPath.startsWith(path));
return (
<a
className={className}
<AnchorButton
variant={isActive ? 'accent' : 'inline'}
className="px-16 h-[38px] text-h4 uppercase border-0"
href={path}
onClick={(e) => {
e.preventDefault();
@ -47,6 +43,6 @@ const NavLink = ({ name, path }: NavLinkProps) => {
}}
>
{name}
</a>
</AnchorButton>
);
};

View File

@ -1,20 +1,45 @@
import { ApolloProvider } from '@apollo/client';
import { AppProps } from 'next/app';
import Head from 'next/head';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { Navbar } from '../components/navbar';
import { createClient } from '../lib/apollo-client';
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
import './styles.css';
function VegaTradingApp({ Component, pageProps }: AppProps) {
const client = useMemo(() => createClient(process.env['NX_VEGA_URL']), []);
useCallback(() => {
if (
localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, []);
const setTheme = () => {
localStorage.theme = document.documentElement.classList.toggle('dark')
? 'dark'
: undefined;
};
return (
<ApolloProvider client={client}>
<Head>
<title>Welcome to trading!</title>
<link
rel="icon"
href="https://vega.xyz/favicon-32x32.png"
type="image/png"
/>
</Head>
<div className="h-full grid grid-rows-[min-content,_1fr]">
<Navbar />
<div className="h-full dark:bg-black dark:text-white-60 bg-white text-black-60">
<div className="flex items-center border-b-[7px] border-vega-yellow">
<Navbar />
<ThemeSwitcher onToggle={setTheme} className="ml-auto mr-8 -my-2" />
</div>
<main>
<Component {...pageProps} />
</main>

View File

@ -1,6 +1,4 @@
import { EtherscanLink } from '@vegaprotocol/ui-toolkit';
import { Callout } from '@vegaprotocol/ui-toolkit';
import { ReactHelpers } from '@vegaprotocol/react-helpers';
import { Callout, Button } from '@vegaprotocol/ui-toolkit';
export function Index() {
/*
@ -9,12 +7,20 @@ export function Index() {
* Note: The corresponding styles are in the ./index.scss file.
*/
return (
<div>
<Callout title="Hello there" headingLevel={1}>
Welcome trading 👋
<div className="m-24 ">
<Callout
intent="help"
title="This is what this thing does"
iconName="endorsed"
headingLevel={1}
>
<div className="flex flex-col">
<div>With a longer explaination</div>
<Button className="block mt-8" variant="secondary">
Action
</Button>
</div>
</Callout>
<EtherscanLink chainId={null} address="address" />
<ReactHelpers />
</div>
);
}

View File

@ -44,26 +44,26 @@ export const GridTabs = ({ children, group }: GridTabsProps) => {
className="h-full grid grid-rows-[min-content_1fr]"
onValueChange={(value) => setActiveTab(value)}
>
<Tabs.List className="flex gap-[2px] bg-neutral-200" role="tablist">
<Tabs.List
className="flex flex-nowrap gap-4 overflow-x-auto my-4"
role="tablist"
>
{Children.map(children, (child) => {
if (!isValidElement(child)) return null;
const isActive = child.props.name === activeTab;
const triggerClass = classNames(
'py-4',
'px-12',
'border-t border-neutral-200',
'capitalize',
{
'text-vega-pink': isActive,
'bg-white': isActive,
}
);
const triggerClass = classNames('py-4', 'px-12', 'capitalize', {
'text-black dark:text-vega-yellow': isActive,
'bg-white dark:bg-black': isActive,
'text-black dark:text-white': !isActive,
'bg-black-10 dark:bg-white-10': !isActive,
});
return (
<Tabs.Trigger value={child.props.name} className={triggerClass}>
{child.props.name}
</Tabs.Trigger>
);
})}
<div className="bg-black-10 dark:bg-white-10 grow"></div>
</Tabs.List>
<div className="h-full overflow-auto">
{Children.map(children, (child) => {

View File

@ -18,7 +18,7 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
);
return (
<div className={wrapperClasses}>
<header className="col-start-1 col-end-2 row-start-1 row-end-1 bg-white p-8">
<header className="col-start-1 col-end-2 row-start-1 row-end-1 p-8">
<h1>Market: {market.name}</h1>
</header>
<TradeGridChild className="col-start-1 col-end-2">
@ -93,7 +93,7 @@ export const TradePanels = ({ market }: TradePanelsProps) => {
return (
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
<header className="bg-white p-8">
<header className="p-8">
<h1>Market: {market.name}</h1>
</header>
<div className="h-full">
@ -103,18 +103,15 @@ export const TradePanels = ({ market }: TradePanelsProps) => {
)}
</AutoSizer>
</div>
<div className="flex flex-nowrap gap-2 bg-neutral-200 border-neutral-200 border-t overflow-x-auto">
<div className="flex flex-nowrap gap-4 overflow-x-auto my-4">
{Object.keys(TradingViews).map((key: TradingView) => {
const className = classNames(
'p-8',
'border-t',
'border-neutral-200',
'capitalize',
{
'text-vega-pink': view === key,
'bg-white': view === key,
}
);
const isActive = view === key;
const className = classNames('py-4', 'px-12', 'capitalize', {
'text-black dark:text-vega-yellow': isActive,
'bg-white dark:bg-black': isActive,
'text-black dark:text-white': !isActive,
'bg-black-10 dark:bg-white-10': !isActive,
});
return (
<button
onClick={() => setView(key)}
@ -125,6 +122,7 @@ export const TradePanels = ({ market }: TradePanelsProps) => {
</button>
);
})}
<div className="bg-black-10 dark:bg-white-10 grow"></div>
</div>
</div>
);

View File

@ -8,72 +8,48 @@ module.exports = {
colors: {
transparent: 'transparent',
current: 'currentColor',
black: '#000',
white: '#FFF',
neutral: {
// 250 - 23 = 227; (900-50) / 227 = 850 / 227 = 3.74449339207
50: '#fafafa', // FA = 250
100: '#ebebeb',
150: '#dcdcdc',
200: '#cdcdcd',
250: '#bebebe',
300: '#afafaf',
350: '#a1a1a1',
400: '#939393',
450: '#858585',
500: '#787878',
550: '#6a6a6a',
593: '#696969', // dark muted
600: '#5d5d5d',
650: '#515151',
700: '#444444',
753: '#3E3E3E', // dark -> 3F is muted
750: '#383838',
800: '#2d2d2d', // breakdown-background was 2C
850: '#222222',
900: '#171717', // 17 = 23
white: {
DEFAULT: '#FFF',
'02': 'rgba(255, 255, 255, 0.02)',
'05': 'rgba(255, 255, 255, 0.05)',
10: 'rgba(255, 255, 255, 0.10)',
25: 'rgba(255, 255, 255, 0.25)',
40: 'rgba(255, 255, 255, 0.40)',
60: 'rgba(255, 255, 255, 0.60)',
80: 'rgba(255, 255, 255, 0.80)',
95: 'rgba(255, 255, 255, 0.95)',
100: 'rgba(255, 255, 255, 1.00)',
},
'light-gray-50': '#F5F8FA', //off-white - https://blueprintjs.com/docs/#core/colors
'gray-50': '#BFCCD6', // muted - https://blueprintjs.com/docs/#core/colors
black: {
DEFAULT: '#000',
'02': 'rgba(0, 0, 0, 0.02)',
'05': 'rgba(0, 0, 0, 0.05)',
10: 'rgba(0, 0, 0, 0.10)',
25: 'rgba(0, 0, 0, 0.25)',
40: 'rgba(0, 0, 0, 0.40)',
60: 'rgba(0, 0, 0, 0.60)',
80: 'rgba(0, 0, 0, 0.80)',
95: 'rgba(0, 0, 0, 0.95)',
100: 'rgba(0, 0, 0, 1)',
},
blue: '#1DA2FB',
coral: '#FF6057',
// below colors are not defined as atoms
vega: {
yellow: '#EDFF22',
pink: '#FF2D5E',
green: '#00F780',
},
'vega-yellow-dark': '#474B0A', // yellow 0.3 opacity on black
intent: {
danger: '#FF261A',
warning: '#FF7A1A',
prompt: '#EDFF22',
progress: '#FFF',
success: '#26FF8A',
help: '#494949',
background: {
danger: '#9E0025', // for white text
},
} /*,
data: {
red: {
white: {
50: '#FFFFFF',
220: '#FF6057', // overlay FFF 80%
390: '#FF6057', // overlay FFF 60%
560: '#FF6057', // overlay FFF 40%
730: '#FF6057', // overlay FFF 20%
900: '#FF6057',
},
green: {
50: '#30F68B',
220: '#89DC50',
475: '#F2BD09',
730: '#FF8501',
900: '#FF6057',
},
},
},*/,
},
'intent-background': {
danger: '#9E0025', // for white text
},
},
spacing: {
0: '0px',
@ -81,18 +57,37 @@ module.exports = {
4: '0.25rem',
8: '0.5rem',
12: '0.75rem',
16: '1rem',
20: '1.25rem',
24: '1.5rem',
28: '1.75rem',
32: '2rem',
44: '2.75rem',
},
backgroundColor: ({ theme }) => ({
transparent: 'transparent',
neutral: theme('colors.neutral'),
dark: theme('colors.dark'),
black: '#000',
white: theme('colors.white'),
danger: theme('colors.intent.background.danger'),
'neutral-200': theme('colors.neutral.200'),
}),
opacity: {
0: '0',
2: '0.02',
5: '0.05',
10: '0.1',
15: '0.15',
20: '0.2',
25: '0.25',
30: '0.3',
35: '0.35',
40: '0.4',
45: '0.45',
50: '0.5',
55: '0.55',
60: '0.6',
65: '0.65',
70: '0.7',
75: '0.75',
80: '0.8',
85: '0.85',
90: '0.9',
95: '0.95',
100: '1',
},
borderWidth: {
DEFAULT: '1px',
1: '1px',
@ -132,12 +127,12 @@ module.exports = {
],
},
fontSize: {
h1: ['72px', { lineHeight: '92px', letterSpacing: '-1%' }],
h2: ['48px', { lineHeight: '64px', letterSpacing: '-1%' }],
h3: ['32px', { lineHeight: '40px', letterSpacing: '-1%' }],
h1: ['72px', { lineHeight: '92px', letterSpacing: '-0.01em' }],
h2: ['48px', { lineHeight: '64px', letterSpacing: '-0.01em' }],
h3: ['32px', { lineHeight: '40px', letterSpacing: '-0.01em' }],
h4: ['24px', { lineHeight: '36px', letterSpacing: '-1%' }],
h5: ['18px', { lineHeight: '28px', letterSpacing: '-1%' }],
h4: ['24px', { lineHeight: '36px', letterSpacing: '-0.01em' }],
h5: ['18px', { lineHeight: '28px', letterSpacing: '-0.01em' }],
'body-large': ['16px', '24px'],
body: ['14px', '20px'],
@ -148,7 +143,9 @@ module.exports = {
extend: {
boxShadow: {
callout: '5px 5px 0 1px rgba(0, 0, 0, 0.05)',
callout: '5px 5px 0 1px rgba(255, 255, 255, 0.05)',
focus: '0px 0px 0px 1px #FFFFFF, 0px 0px 3px 2px #FFE600',
'focus-dark': '0px 0px 0px 1px #000000, 0px 0px 3px 2px #FFE600',
},
},
};

View File

@ -1,19 +1,29 @@
import '../src/styles.scss';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
themes: {
/*themes: {
default: 'dark',
list: [
{ name: 'dark', class: ['dark', 'bg-black'], color: '#000' },
{ name: 'light', class: '', color: '#FFF' },
],
},
},*/
};
export const decorators = [
(Story) => (
<div className="dark bg-black">
<Story />
</div>
),
(Story, context) =>
context.parameters.themes === false ? (
<div className="text-body">
<Story />
</div>
) : (
<div className="text-body">
<div className="dark bg-black p-16">
<Story />
</div>
<div className="p-16">
<Story />
</div>
</div>
),
];

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { Button } from './button';
describe('Button', () => {
it('should render successfully', () => {
const { baseElement } = render(<Button>Label</Button>);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,101 @@
import { Story, Meta } from '@storybook/react';
import { Button } from './button';
export default {
component: Button,
title: 'Button',
} as Meta;
const Template: Story = (args) => (
<>
<div className="mb-8">
<Button {...args} />
</div>
{args['variant'] !== 'inline' && <Button {...args} disabled />}
</>
);
export const Primary = Template.bind({});
Primary.args = {
children: 'Primary',
};
export const Secondary = Template.bind({});
Secondary.args = {
children: 'Secondary',
variant: 'secondary',
};
export const Accent = Template.bind({});
Accent.args = {
children: 'Accent',
variant: 'accent',
};
export const Inline = Template.bind({});
Inline.args = {
children: 'Inline',
variant: 'inline',
};
export const NavAccent: Story = (args) => (
<>
<div className="mb-8">
<Button variant="accent" className="px-4">
Background
</Button>
</div>
<div className="mb-8">
<Button variant="accent" className="px-4" prependIconName="menu-open">
Background
</Button>
</div>
<div className="mb-8">
<Button variant="accent" className="px-4" appendIconName="menu-closed">
Background
</Button>
</div>
</>
);
export const NavInline: Story = (args) => (
<>
<div className="mb-8">
<Button variant="inline" className="uppercase">
Background
</Button>
</div>
<div className="mb-8">
<Button
variant="inline"
className="uppercase"
prependIconName="menu-open"
>
Background
</Button>
</div>
<div className="mb-8">
<Button
variant="inline"
className="uppercase"
appendIconName="menu-closed"
>
Background
</Button>
</div>
</>
);
export const IconPrepend = Template.bind({});
IconPrepend.args = {
children: 'Icon prepend',
prependIconName: 'search',
variant: 'accent',
};
export const IconAppend = Template.bind({});
IconAppend.args = {
children: 'Icon append',
appendIconName: 'search',
variant: 'accent',
};

View File

@ -0,0 +1,165 @@
import { AnchorHTMLAttributes, ButtonHTMLAttributes, forwardRef } from 'react';
import classNames from 'classnames';
import { Icon, IconName } from '../icon';
import {
includesLeftPadding,
includesRightPadding,
includesBorderWidth,
} from '../../utils/class-names';
interface CommonProps {
children?: React.ReactNode;
variant?: 'primary' | 'secondary' | 'accent' | 'inline';
className?: string;
prependIconName?: IconName;
appendIconName?: IconName;
}
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
CommonProps {}
export interface AnchorButtonProps
extends AnchorHTMLAttributes<HTMLAnchorElement>,
CommonProps {}
const getClassName = (
className: CommonProps['className'],
variant: CommonProps['variant']
) => {
const paddingLeftProvided = includesLeftPadding(className);
const paddingRightProvided = includesRightPadding(className);
const borderWidthProvided = includesBorderWidth(className);
return classNames(
[
'inline-flex',
'items-center',
'justify-center',
'box-border',
'h-28',
'text-ui',
'no-underline',
'hover:underline',
'disabled:no-underline',
'transition-all',
],
{
'pl-28': !(paddingLeftProvided || variant === 'inline'),
'pr-28': !(paddingRightProvided || variant === 'inline'),
border: !borderWidthProvided,
'hover:border-black dark:hover:border-white': variant !== 'inline',
'active:border-black dark:active:border-white': true,
'bg-black dark:bg-white': variant === 'primary',
'border-black-60 dark:border-white-60':
variant === 'primary' || variant === 'secondary',
'text-white dark:text-black': variant === 'primary',
'hover:bg-black-80 dark:hover:bg-white-80': variant === 'primary',
'active:bg-white dark:active:bg-black':
variant === 'primary' || variant === 'accent',
'active:text-black dark:active:text-white':
variant === 'primary' || variant === 'accent',
'bg-white dark:bg-black': variant === 'secondary',
'text-black dark:text-white': variant === 'secondary',
'hover:bg-black-25 dark:hover:bg-white-25': variant === 'secondary',
'hover:text-black dark:hover:text-white':
variant === 'secondary' || variant === 'accent',
'active:bg-black dark:active:bg-white': variant === 'secondary',
'active:text-white dark:active:text-black': variant === 'secondary',
uppercase: variant === 'accent',
'bg-vega-yellow dark:bg-vega-yellow': variant === 'accent',
'border-transparent dark:border-transparent':
variant === 'accent' || variant === 'inline',
'hover:bg-vega-yellow-dark dark:hover:bg-vega-yellow/30':
variant === 'accent',
'text-black dark:text-black': variant === 'accent',
'hover:text-white dark:hover:text-white': variant === 'accent',
'pl-4': variant === 'inline' && !paddingLeftProvided,
'pr-4': variant === 'inline' && !paddingRightProvided,
'border-0': variant === 'inline' && !borderWidthProvided,
underline: variant === 'inline',
'hover:no-underline': variant === 'inline',
'hover:border-transparent dark:hover:border-transparent':
variant === 'inline',
'active:border-transparent dark:active:border-transparent':
variant === 'inline',
'active:text-black dark:active:text-vega-yellow': variant === 'inline',
'text-black-95 dark:text-white-95': variant === 'inline',
'hover:text-black hover:dark:text-white': variant === 'inline',
'disabled:bg-black-10 dark:disabled:bg-white-10': variant !== 'inline',
'disabled:text-black-60 dark:disabled:text-white-60':
variant !== 'inline',
'disabled:border-black-25 dark:disabled:border-white-25':
variant !== 'inline',
},
className
);
};
const getContent = (
children: React.ReactNode,
prependIconName?: IconName,
appendIconName?: IconName
) => {
const iconName = prependIconName || appendIconName;
if (iconName === undefined) {
return children;
}
const iconClassName = classNames(['fill-current'], {
'mr-8': prependIconName,
'ml-8': appendIconName,
});
const icon = <Icon name={iconName} className={iconClassName} size={16} />;
return (
<>
{prependIconName && icon}
{children}
{appendIconName && icon}
</>
);
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = 'primary',
children,
className,
prependIconName,
appendIconName,
...props
},
ref
) => {
return (
<button ref={ref} className={getClassName(className, variant)} {...props}>
{getContent(children, prependIconName, appendIconName)}
</button>
);
}
);
export const AnchorButton = forwardRef<HTMLAnchorElement, AnchorButtonProps>(
(
{
variant = 'primary',
children,
className,
prependIconName,
appendIconName,
...prosp
},
ref
) => {
return (
<a ref={ref} className={getClassName(className, variant)} {...prosp}>
{getContent(children, prependIconName, appendIconName)}
</a>
);
}
);

View File

@ -0,0 +1 @@
export * from './button';

View File

@ -1,58 +0,0 @@
@import '../../styles/colors';
.callout {
display: flex;
padding: 14px;
border: 1px solid $white;
box-shadow: 3px 3px 0px $white;
margin: 12px 0;
p {
margin: 0 0 10px 0;
&:last-child {
margin-bottom: 0;
}
}
// VARIATIONS
&--error {
box-shadow: 5px 5px 0px $vega-red3;
}
&--success {
box-shadow: 5px 5px 0px $vega-green3;
}
&--warn {
box-shadow: 5px 5px 0px $vega-orange3;
}
&--action {
box-shadow: 5px 5px 0px $vega-yellow3;
}
&__content {
width: 100%;
> :last-child {
margin-bottom: 0;
}
}
&__title,
h4,
h5,
h6 {
margin-bottom: 15px;
}
&__title {
margin-top: 0;
}
&__icon {
margin-right: 10px;
color: $white;
}
}

View File

@ -1,7 +1,8 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { Callout } from '.';
import { Callout } from './callout';
import { Button } from '../button';
export default {
title: 'Callout',
@ -9,37 +10,61 @@ export default {
} as ComponentMeta<typeof Callout>;
const Template: ComponentStory<typeof Callout> = (args) => (
<Callout {...args}>Content</Callout>
<Callout {...args} />
);
export const Default = Template.bind({});
Default.args = {
children: 'Content',
};
export const Danger = Template.bind({});
Danger.args = {
intent: 'danger',
children: 'Content',
};
export const Warning = Template.bind({});
Warning.args = {
intent: 'warning',
children: 'Content',
};
export const Prompt = Template.bind({});
Prompt.args = {
intent: 'prompt',
children: 'Content',
};
export const Progress = Template.bind({});
Progress.args = {
intent: 'progress',
children: 'Content',
};
export const Success = Template.bind({});
Success.args = {
intent: 'success',
children: 'Content',
};
export const Help = Template.bind({});
Help.args = {
intent: 'help',
children: 'Content',
};
export const IconAndContent = Template.bind({});
IconAndContent.args = {
intent: 'help',
title: 'This is what this thing does',
iconName: 'endorsed',
children: (
<div className="flex flex-col">
<div>With a longer explaination</div>
<Button className="block mt-8" variant="secondary">
Action
</Button>
</div>
),
};

View File

@ -8,8 +8,10 @@ test('It renders content within callout', () => {
});
test('It renders title and icon', () => {
render(<Callout icon={<div data-testid="icon" />} title="title" />);
expect(screen.getByTestId('icon')).toBeInTheDocument();
render(<Callout iconName="endorsed" title="title" />);
expect(
screen.getByTestId('callout').querySelector('svg')
).toBeInTheDocument();
expect(screen.getByText('title')).toBeInTheDocument();
});
@ -33,7 +35,7 @@ intents.map((intent) =>
test(`Applies class for progress`, () => {
render(<Callout intent="progress" />);
expect(screen.getByTestId('callout')).toHaveClass(
'shadow-intent-black',
'dark:shadow-intent-progress'
'shadow-black',
'dark:shadow-white'
);
});

View File

@ -1,48 +1,55 @@
import React from 'react';
import classNames from 'classnames';
import { Icon, IconName } from '../icon';
export const Callout = ({
children,
title,
intent = 'help',
icon,
headingLevel,
}: {
export interface CalloutProps {
children?: React.ReactNode;
title?: React.ReactElement | string;
intent?: 'danger' | 'warning' | 'prompt' | 'progress' | 'success' | 'help';
icon?: React.ReactNode;
iconName?: IconName;
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
}) => {
}
export function Callout({
children,
title,
intent = 'help',
iconName,
headingLevel,
}: CalloutProps) {
const className = classNames(
'shadow-callout',
'border',
'border-black',
'dark:border-white',
'text-body-large',
'dark:text-white',
'p-8',
{
'shadow-intent-danger': intent === 'danger',
'shadow-intent-warning': intent === 'warning',
'shadow-intent-prompt': intent === 'prompt',
'shadow-intent-black dark:shadow-intent-progress': intent === 'progress',
'shadow-black dark:shadow-white': intent === 'progress',
'shadow-intent-success': intent === 'success',
'shadow-intent-help': intent === 'help',
flex: icon,
flex: !!iconName,
}
);
const TitleTag: keyof JSX.IntrinsicElements = headingLevel
? `h${headingLevel}`
: 'div';
const icon = iconName && (
<Icon name={iconName} className="fill-current ml-8 mr-16 mt-8" size={20} />
);
const body = (
<div className="body-large dark:text-white">
<>
{title && <TitleTag className="text-h5">{title}</TitleTag>}
{children}
</div>
</>
);
return (
<div data-testid="callout" className={className}>
{icon && <div className="">{icon}</div>}
{icon}
{icon ? <div className="grow">{body}</div> : body}
</div>
);
};
}

View File

@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import * as React from 'react';
import { EtherscanLink } from '.';
import { EthereumChainIds } from '../../utils/web3';

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { Icon } from './icon';
describe('Icon', () => {
it('should render successfully', () => {
const { baseElement } = render(<Icon name="add" />);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,12 @@
import { Story, Meta } from '@storybook/react';
import { Icon } from './icon';
export default {
component: Icon,
title: 'Icon',
} as Meta;
const Template: Story = (args) => <Icon {...args} name="warning-sign" />;
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,30 @@
import { IconSvgPaths20, IconSvgPaths16, IconName } from '@blueprintjs/icons';
import classNames from 'classnames';
export type { IconName } from '@blueprintjs/icons';
interface IconProps {
name: IconName;
className?: string;
size?: 16 | 20 | 24 | 32 | 48 | 64;
}
export const Icon = ({ size = 16, name, className }: IconProps) => {
const effectiveClassName = classNames(
{
'w-20': size === 20,
'h-20': size === 20,
'w-16': size === 16,
'h-16': size === 16,
},
className
);
const viewbox = size <= 16 ? '0 0 16 16' : '0 0 20 20';
return (
<svg className={effectiveClassName} viewBox={viewbox}>
{(size <= 16 ? IconSvgPaths16 : IconSvgPaths20)[name].map((d, key) => (
<path fillRule="evenodd" clipRule="evenodd" d={d} key={key} />
))}
</svg>
);
};

View File

@ -0,0 +1 @@
export * from './icon';

View File

@ -0,0 +1 @@
export * from './input-error';

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { InputError } from './input-error';
describe('InputError', () => {
it('should render successfully', () => {
const { baseElement } = render(<InputError />);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { Story, Meta } from '@storybook/react';
import { InputError } from './input-error';
export default {
component: InputError,
title: 'InputError',
} as Meta;
const Template: Story = (args) => <InputError {...args} />;
export const Danger = Template.bind({});
Danger.args = {
children: 'An error that might have happened',
};
export const Warning = Template.bind({});
Warning.args = {
intent: 'warning',
children: 'Something that might be an issue',
};

View File

@ -0,0 +1,41 @@
import classNames from 'classnames';
import { Icon } from '../icon';
interface InputErrorProps {
children?: React.ReactNode;
className?: string;
intent?: 'danger' | 'warning';
}
export const InputError = ({
intent = 'danger',
className,
children,
}: InputErrorProps) => {
const effectiveClassName = classNames(
[
'inline-flex',
'items-center',
'box-border',
'h-28',
'border-l-4',
'text-black-95 dark:text-white-95',
'text-ui',
],
{
'border-intent-danger': intent === 'danger',
'border-intent-warning': intent === 'warning',
},
className
);
const iconClassName = classNames(['mx-8'], {
'fill-intent-danger': intent === 'danger',
'fill-intent-warning': intent === 'warning',
});
return (
<div className={effectiveClassName}>
<Icon name="warning-sign" className={iconClassName} />
{children}
</div>
);
};

View File

@ -0,0 +1 @@
export * from './input';

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { Input } from './input';
describe('Input', () => {
it('should render successfully', () => {
const { baseElement } = render(<Input />);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,29 @@
import { Story, Meta } from '@storybook/react';
import { Input } from './input';
export default {
component: Input,
title: 'Input',
} as Meta;
const Template: Story = (args) => <Input {...args} value="I type words" />;
export const Default = Template.bind({});
Default.args = {};
export const WithError = Template.bind({});
WithError.args = {
hasError: true,
};
export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
};
export const IconPrepend: Story = () => (
<Input value="I type words" prependIconName="search" />
);
export const IconAppend: Story = () => (
<Input value="I type words and even more words" appendIconName="search" />
);

View File

@ -0,0 +1,98 @@
import { InputHTMLAttributes, forwardRef } from 'react';
import classNames from 'classnames';
import { Icon, IconName } from '../icon';
import {
includesLeftPadding,
includesRightPadding,
} from '../../utils/class-names';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
hasError?: boolean;
disabled?: boolean;
className?: string;
prependIconName?: IconName;
appendIconName?: IconName;
}
export const inputClassNames = ({
hasError,
className,
}: {
hasError?: boolean;
className?: string;
}) => {
return classNames(
[
'inline-flex',
'items-center',
'box-border',
'border',
'bg-clip-padding',
'border-black-60 dark:border-white-60',
'bg-black-25 dark:bg-white-25',
'text-black-60 dark:text-white-60',
'text-ui',
'focus-visible:shadow-focus dark:focus-visible:shadow-focus-dark',
'focus-visible:outline-0',
'disabled:bg-black-10 disabled:dark:bg-white-10',
],
{
'pl-8': !includesLeftPadding(className),
'pr-8': !includesRightPadding(className),
'border-vega-pink dark:border-vega-pink': hasError,
},
className
);
};
export const inputStyle = ({
style,
disabled,
}: {
style?: React.CSSProperties;
disabled?: boolean;
}) =>
disabled
? {
...style,
backgroundImage:
'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAAXNSR0IArs4c6QAAACNJREFUGFdjtLS0/M8ABcePH2eEsRlJl4BpBdHIuuFmEi0BABqjEQVjx/LTAAAAAElFTkSuQmCC)',
}
: style;
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ prependIconName, appendIconName, className, ...props }, ref) => {
className = `${className} h-28`;
if (prependIconName) {
className += ' pl-28';
}
if (appendIconName) {
className += ' pr-28';
}
const input = (
<input
{...props}
ref={ref}
className={classNames(inputClassNames({ className, ...props }))}
/>
);
const iconName = prependIconName || appendIconName;
if (iconName !== undefined) {
const iconClassName = classNames(
['fill-black-60 dark:fill-white-60', 'absolute', 'z-10'],
{
'left-8': prependIconName,
'right-8': appendIconName,
}
);
const icon = <Icon name={iconName} className={iconClassName} size={16} />;
return (
<div className="inline-flex items-center relative">
{prependIconName && icon}
{input}
{appendIconName && icon}
</div>
);
}
return input;
}
);

View File

@ -0,0 +1 @@
export * from './select';

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { Select } from './select';
describe('Select', () => {
it('should render successfully', () => {
const { baseElement } = render(<Select />);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import { Story, Meta } from '@storybook/react';
import { Select } from './select';
export default {
component: Select,
title: 'Select',
} as Meta;
const Template: Story = (args) => (
<Select {...args}>
<option value="Only option">Only option</option>
</Select>
);
export const Default = Template.bind({});
Default.args = {};
export const WithError = Template.bind({});
WithError.args = {
hasError: true,
};
export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
};

View File

@ -0,0 +1,20 @@
import { SelectHTMLAttributes, forwardRef } from 'react';
import classNames from 'classnames';
import { inputClassNames } from '../input/input';
export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
hasError?: boolean;
className?: string;
value?: string | number;
children?: React.ReactNode;
}
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
(props, ref) => (
<select
ref={ref}
{...props}
className={classNames(inputClassNames(props), 'h-28')}
/>
)
);

View File

@ -0,0 +1 @@
export * from './text-area';

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { TextArea } from './text-area';
describe('TextArea', () => {
it('should render successfully', () => {
const { baseElement } = render(<TextArea />);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import { Story, Meta } from '@storybook/react';
import { TextArea } from './text-area';
export default {
component: TextArea,
title: 'TextArea',
} as Meta;
const Template: Story = (args) => (
<TextArea {...args} className="h-48">
I type words
</TextArea>
);
export const Default = Template.bind({});
Default.args = {};
export const WithError = Template.bind({});
WithError.args = {
hasError: true,
};
export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
};

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