Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b589ca1405 |
@@ -18,12 +18,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Use Node.js 16
|
|
||||||
id: Node
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 16.15.1
|
|
||||||
|
|
||||||
- name: Run Cypress tests
|
- name: Run Cypress tests
|
||||||
uses: cypress-io/github-action@v4
|
uses: cypress-io/github-action@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- explorer-e2e
|
- explorer-e2e
|
||||||
- governance-e2e
|
- token-e2e
|
||||||
- trading-e2e
|
- trading-e2e
|
||||||
tags:
|
tags:
|
||||||
description: 'Test tags to run'
|
description: 'Test tags to run'
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ jobs:
|
|||||||
uses: ./.github/workflows/cypress-run.yml
|
uses: ./.github/workflows/cypress-run.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
projects: '["explorer-e2e","governance-e2e","trading-e2e"]'
|
projects: '["explorer-e2e","token-e2e","trading-e2e"]'
|
||||||
tags: '@smoke @regression @slow'
|
tags: '@smoke @regression @slow'
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ jobs:
|
|||||||
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
|
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
|
||||||
echo -n "Affected projects: $affected"
|
echo -n "Affected projects: $affected"
|
||||||
projects=""
|
projects=""
|
||||||
if [[ $affected == *"governance"* ]]; then projects+='"governance-e2e" '; fi
|
if [[ $affected == *"token"* ]]; then projects+='"token-e2e" '; fi
|
||||||
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
|
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
|
||||||
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
|
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
|
||||||
if [[ -z "$projects" ]]; then projects+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
|
if [[ -z "$projects" ]]; then projects+='"token-e2e" "trading-e2e" "explorer-e2e" '; fi
|
||||||
projects=${projects%?}
|
projects=${projects%?}
|
||||||
projects=[${projects// /,}]
|
projects=[${projects// /,}]
|
||||||
echo PROJECTS=$projects >> $GITHUB_ENV
|
echo PROJECTS=$projects >> $GITHUB_ENV
|
||||||
|
|||||||
@@ -64,11 +64,13 @@ jobs:
|
|||||||
######
|
######
|
||||||
|
|
||||||
- name: Run Cypress tests
|
- name: Run Cypress tests
|
||||||
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}"
|
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome
|
||||||
working-directory: frontend-monorepo
|
working-directory: frontend-monorepo
|
||||||
env:
|
env:
|
||||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||||
|
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPRESS_ETH_WALLET_MNEMONIC }}
|
||||||
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
|
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
|
||||||
|
CYPRESS_grepTags: ${{ inputs.tags }}
|
||||||
|
|
||||||
######
|
######
|
||||||
## Upload logs
|
## Upload logs
|
||||||
@@ -82,12 +84,8 @@ jobs:
|
|||||||
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
|
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
|
||||||
done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
|
done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
|
||||||
|
|
||||||
- name: Print logs files
|
|
||||||
if: ${{ always() }}
|
|
||||||
run: ls -alsh /home/runner/.vegacapsule/testnet/logs/
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
if: ${{ failure() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
name: logs-${{ matrix.project }}
|
name: logs-${{ matrix.project }}
|
||||||
path: /home/runner/.vegacapsule/testnet/logs
|
path: /home/runner/.vegacapsule/testnet/logs
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
id: Node
|
id: Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 16.15.1
|
node-version: 16.14.0
|
||||||
- name: Install root dependencies
|
- name: Install root dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
- name: Generate queries
|
- name: Generate queries
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ jobs:
|
|||||||
lint_pr:
|
lint_pr:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Use Node.js 16
|
- name: Use Node.js 16
|
||||||
id: Node
|
id: Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 16.15.1
|
node-version: 16.14.0
|
||||||
- name: Install root dependencies
|
- name: Install commitlint cli and config
|
||||||
run: yarn install
|
run: npm install @commitlint/cli @commitlint/config-conventional
|
||||||
|
- name: Create config
|
||||||
|
run: echo "module.exports = {extends:['@commitlint/config-conventional']};" > commitlint.config.js
|
||||||
- name: Check PR title
|
- name: Check PR title
|
||||||
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
|
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
id: Node
|
id: Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 16.15.1
|
node-version: 16.14.0
|
||||||
- name: Install root dependencies
|
- name: Install root dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
- name: Generate queries
|
- name: Generate queries
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
name: Publish libs to npm
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
project:
|
|
||||||
description: 'Monorepo project to publish'
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- ui-toolkit
|
|
||||||
- react-helpers
|
|
||||||
- tailwindcss-config
|
|
||||||
- types
|
|
||||||
- utils
|
|
||||||
- i18n
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
name: Build & Publish - Tag
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: 'read'
|
|
||||||
actions: 'read'
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: User Node.js 16
|
|
||||||
id: Node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 16.15.1
|
|
||||||
- name: Restore node_modules from cache
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: '**/node_modules'
|
|
||||||
key: node_modules-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
- name: Install root dependencies
|
|
||||||
run: yarn install --frozen-lockfile
|
|
||||||
- name: Build project
|
|
||||||
run: yarn nx build ${{inputs.project}}
|
|
||||||
- name: Publish project to @vegaprotocol
|
|
||||||
uses: JS-DevTools/npm-publish@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.NPM_TOKEN }}
|
|
||||||
package: dist/libs/${{inputs.project}}/package.json
|
|
||||||
access: public
|
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
id: Node
|
id: Node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.15.1
|
node-version: 16.14.0
|
||||||
- name: Restore node_modules from cache
|
- name: Restore node_modules from cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ In order to setup and run vegawallet for e2e capsule tests, in a separate termin
|
|||||||
bash setup-vegawallet.sh
|
bash setup-vegawallet.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
3. copy generated `api-token` and paste the token into `CYPRESS_VEGA_WALLET_API_TOKEN` environment variable in either `apps/governance-e2e/.env` or `apps/explorer-e2e/.env` depending on which project needs testing.
|
3. copy generated `api-token` and paste the token into `CYPRESS_VEGA_WALLET_API_TOKEN` environment variable in either `apps/token-e2e/.env` or `apps/explorer-e2e/.env` depending on which project needs testing.
|
||||||
|
|
||||||
Note: The script is only needed if capsule was built for first time or fresh. To run existing wallet service for capsule:
|
Note: The script is only needed if capsule was built for first time or fresh. To run existing wallet service for capsule:
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"governance.proposal.updateAsset.minProposerBalance",
|
"governance.proposal.updateAsset.minProposerBalance",
|
||||||
"governance.proposal.updateAsset.minVoterBalance",
|
"governance.proposal.updateAsset.minVoterBalance",
|
||||||
"governance.proposal.updateAsset.requiredParticipation",
|
"governance.proposal.updateAsset.requiredParticipation",
|
||||||
|
"governance.proposal.updateMarket.minProposerEquityLikeShare",
|
||||||
"market.fee.factors.infrastructureFee",
|
"market.fee.factors.infrastructureFee",
|
||||||
"market.fee.factors.makerFee",
|
"market.fee.factors.makerFee",
|
||||||
"market.liquidity.bondPenaltyParameter",
|
"market.liquidity.bondPenaltyParameter",
|
||||||
@@ -76,7 +77,6 @@
|
|||||||
"governance.proposal.updateMarket.requiredParticipationLP",
|
"governance.proposal.updateMarket.requiredParticipationLP",
|
||||||
"governance.proposal.updateNetParam.requiredMajority",
|
"governance.proposal.updateNetParam.requiredMajority",
|
||||||
"governance.proposal.updateNetParam.requiredParticipation",
|
"governance.proposal.updateNetParam.requiredParticipation",
|
||||||
"governance.proposal.updateMarket.minProposerEquityLikeShare",
|
|
||||||
"validators.vote.required"
|
"validators.vote.required"
|
||||||
],
|
],
|
||||||
"duration": [
|
"duration": [
|
||||||
|
|||||||
@@ -1,48 +1,150 @@
|
|||||||
context('Asset page', { tags: '@regression' }, () => {
|
context('Asset page', { tags: '@regression' }, function () {
|
||||||
const columns = ['symbol', 'name', 'id', 'type', 'status', 'actions'];
|
before('gather system asset information', function () {
|
||||||
const hiddenOnMobile = ['id', 'type', 'status'];
|
cy.get_asset_information().as('assetsInfo');
|
||||||
describe('Verify elements on page', () => {
|
});
|
||||||
before('Navigate to assets page', () => {
|
|
||||||
cy.visit('/assets');
|
describe('Verify elements on page', function () {
|
||||||
|
const assetsNavigation = 'a[href="/assets"]';
|
||||||
|
const assetHeader = '[data-testid="asset-header"]';
|
||||||
|
const jsonSection = '.language-json';
|
||||||
|
|
||||||
|
before('Navigate to assets page', function () {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get(assetsNavigation).click();
|
||||||
|
|
||||||
|
// Check we have enough enough assets
|
||||||
|
const assetNames = Object.keys(this.assetsInfo);
|
||||||
|
assert.isAtLeast(
|
||||||
|
assetNames.length,
|
||||||
|
5,
|
||||||
|
'Ensuring we have at least 5 assets to test'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to see full assets list', () => {
|
it('should be able to see assets page sections', function () {
|
||||||
cy.getAssets().then((assets) => {
|
const assetNames = Object.keys(this.assetsInfo);
|
||||||
assets.forEach((asset) => {
|
assetNames.forEach((assetName) => {
|
||||||
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
|
cy.get(assetHeader)
|
||||||
});
|
.contains(assetName)
|
||||||
});
|
.should('be.visible')
|
||||||
columns.forEach((col) => {
|
.next()
|
||||||
cy.get(`[col-id="${col}"]`).should('be.visible');
|
.within(() => {
|
||||||
|
cy.get(jsonSection).should('not.be.empty');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to see assets page displayed in mobile', () => {
|
it('should be able to see all asset details displayed in JSON', function () {
|
||||||
cy.switchToMobile();
|
const assetNames = Object.keys(this.assetsInfo);
|
||||||
|
assetNames.forEach((assetName) => {
|
||||||
|
cy.get(assetHeader)
|
||||||
|
.contains(assetName)
|
||||||
|
.next()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(jsonSection)
|
||||||
|
.invoke('text')
|
||||||
|
.convert_string_json_to_js_object()
|
||||||
|
.then((assetsListedInJson) => {
|
||||||
|
const assetInfo = this.assetsInfo[assetName];
|
||||||
|
|
||||||
hiddenOnMobile.forEach((col) => {
|
assert.equal(assetsListedInJson.name, assetInfo.node.name);
|
||||||
cy.get(`[col-id="${col}"]`).should('have.length', 0);
|
assert.equal(assetsListedInJson.id, assetInfo.node.id);
|
||||||
});
|
assert.equal(
|
||||||
|
assetsListedInJson.decimals,
|
||||||
|
assetInfo.node.decimals
|
||||||
|
);
|
||||||
|
assert.equal(assetsListedInJson.symbol, assetInfo.node.symbol);
|
||||||
|
assert.equal(
|
||||||
|
assetsListedInJson.source.__typename,
|
||||||
|
assetInfo.node.source.__typename
|
||||||
|
);
|
||||||
|
|
||||||
cy.getAssets().then((assets) => {
|
if (assetInfo.node.source.__typename == 'ERC20') {
|
||||||
assets.forEach((asset) => {
|
assert.equal(
|
||||||
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
|
assetsListedInJson.source.contractAddress,
|
||||||
});
|
assetInfo.node.source.contractAddress
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assetInfo.node.source.__typename == 'BuiltinAsset') {
|
||||||
|
assert.equal(
|
||||||
|
assetsListedInJson.source.maxFaucetAmountMint,
|
||||||
|
assetInfo.node.source.maxFaucetAmountMint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let knownAssetTypes = ['BuiltinAsset', 'ERC20'];
|
||||||
|
assert.include(
|
||||||
|
knownAssetTypes,
|
||||||
|
assetInfo.node.source.__typename,
|
||||||
|
`Checking that current asset type of ${assetInfo.node.source.__typename} /
|
||||||
|
is one of: ${knownAssetTypes}: /
|
||||||
|
If fail then we need to add extra tests for un-encountered asset types`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open details page when clicked on "View details"', () => {
|
it('should be able to switch assets between light and dark mode', function () {
|
||||||
cy.getAssets().then((assets) => {
|
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
|
||||||
assets.forEach((asset) => {
|
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
|
||||||
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
|
||||||
.eq(0)
|
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
|
||||||
.should('contain.text', 'View details');
|
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
|
||||||
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
|
||||||
.eq(0)
|
const themeSwitcher = '[data-testid="theme-switcher"]';
|
||||||
.click();
|
const jsonFields = '.hljs';
|
||||||
cy.getByTestId('asset-header').should('have.text', asset.name);
|
const sideMenuBackground = '.absolute';
|
||||||
cy.go('back');
|
|
||||||
|
// 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(assetsNavigation)
|
||||||
|
.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(assetsNavigation)
|
||||||
|
.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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to see assets page displayed in mobile', function () {
|
||||||
|
cy.common_switch_to_mobile_and_click_toggle();
|
||||||
|
cy.get(assetsNavigation).click();
|
||||||
|
|
||||||
|
const assetNames = Object.keys(this.assetsInfo);
|
||||||
|
assetNames.forEach((assetName) => {
|
||||||
|
cy.get(assetHeader)
|
||||||
|
.contains(assetName)
|
||||||
|
.should('be.visible')
|
||||||
|
.next()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(jsonSection).should('not.be.empty');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,27 +1,33 @@
|
|||||||
context('Blocks page', { tags: '@regression' }, function () {
|
context('Blocks page', { tags: '@regression' }, function () {
|
||||||
|
before('visit token home page', function () {
|
||||||
|
cy.visit('/');
|
||||||
|
});
|
||||||
|
|
||||||
describe('Verify elements on page', function () {
|
describe('Verify elements on page', function () {
|
||||||
beforeEach(() => {
|
const blockNavigation = 'a[href="/blocks"]';
|
||||||
cy.visit('/blocks');
|
|
||||||
});
|
|
||||||
const blockHeight = '[data-testid="block-height"]';
|
const blockHeight = '[data-testid="block-height"]';
|
||||||
const blockTime = '[data-testid="block-time"]';
|
const blockTime = '[data-testid="block-time"]';
|
||||||
const blockHeader = '[data-testid="block-header"]';
|
const blockHeader = '[data-testid="block-header"]';
|
||||||
const previousBlockBtn = '[data-testid="previous-block"]';
|
const previousBlockBtn = '[data-testid="previous-block"]';
|
||||||
const infiniteScrollWrapper = '[data-testid="infinite-scroll-wrapper"]';
|
const infiniteScrollWrapper = '[data-testid="infinite-scroll-wrapper"]';
|
||||||
|
|
||||||
|
beforeEach('navigate to blocks page', function () {
|
||||||
|
cy.get(blockNavigation).click();
|
||||||
|
});
|
||||||
|
|
||||||
it('Blocks page is displayed', function () {
|
it('Blocks page is displayed', function () {
|
||||||
validateBlocksDisplayed();
|
validateBlocksDisplayed();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Blocks page is displayed on mobile', function () {
|
it('Blocks page is displayed on mobile', function () {
|
||||||
cy.switchToMobile();
|
cy.common_switch_to_mobile_and_click_toggle();
|
||||||
|
cy.get(blockNavigation).click();
|
||||||
validateBlocksDisplayed();
|
validateBlocksDisplayed();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Block validator page is displayed', function () {
|
it('Block validator page is displayed', function () {
|
||||||
waitForBlocksResponse();
|
waitForBlocksResponse();
|
||||||
cy.get(blockHeight).eq(0).find('a').click({ force: true });
|
cy.get(blockHeight).eq(0).click();
|
||||||
|
|
||||||
cy.get('[data-testid="block-validator"]').should('not.be.empty');
|
cy.get('[data-testid="block-validator"]').should('not.be.empty');
|
||||||
cy.get(blockTime).should('not.be.empty');
|
cy.get(blockTime).should('not.be.empty');
|
||||||
//TODO: Add assertion for transactions when txs are added
|
//TODO: Add assertion for transactions when txs are added
|
||||||
@@ -29,7 +35,7 @@ context('Blocks page', { tags: '@regression' }, function () {
|
|||||||
|
|
||||||
it('Navigate to previous block', function () {
|
it('Navigate to previous block', function () {
|
||||||
waitForBlocksResponse();
|
waitForBlocksResponse();
|
||||||
cy.get(blockHeight).eq(0).find('a').click({ force: true });
|
cy.get(blockHeight).eq(0).click();
|
||||||
cy.get(blockHeader)
|
cy.get(blockHeader)
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
.then(($blockHeaderTxt) => {
|
.then(($blockHeaderTxt) => {
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ context('Network parameters page', { tags: '@smoke' }, function () {
|
|||||||
before('navigate to network parameter page', function () {
|
before('navigate to network parameter page', function () {
|
||||||
cy.fixture('net_parameter_format_lookup').as('networkParameterFormat');
|
cy.fixture('net_parameter_format_lookup').as('networkParameterFormat');
|
||||||
});
|
});
|
||||||
describe('Verify elements on page', function () {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('/network-parameters');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
describe('Verify elements on page', function () {
|
||||||
|
const networkParametersNavigation = 'a[href="/network-parameters"]';
|
||||||
const networkParametersHeader = '[data-testid="network-param-header"]';
|
const networkParametersHeader = '[data-testid="network-param-header"]';
|
||||||
const tableRows = '[data-testid="key-value-table-row"]';
|
const tableRows = '[data-testid="key-value-table-row"]';
|
||||||
|
|
||||||
|
before('navigate to network parameter page', function () {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get(networkParametersNavigation).click();
|
||||||
|
});
|
||||||
|
|
||||||
it('should show network parameter heading at top of page', function () {
|
it('should show network parameter heading at top of page', function () {
|
||||||
cy.get(networkParametersHeader)
|
cy.get(networkParametersHeader)
|
||||||
.should('have.text', 'Network Parameters')
|
.should('have.text', 'Network Parameters')
|
||||||
@@ -198,8 +201,55 @@ context('Network parameters page', { tags: '@smoke' }, function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be able to switch network parameter 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 already 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(networkParametersNavigation)
|
||||||
|
.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(networkParametersNavigation)
|
||||||
|
.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);
|
||||||
|
});
|
||||||
|
|
||||||
it('should be able to see network parameters - on mobile', function () {
|
it('should be able to see network parameters - on mobile', function () {
|
||||||
cy.switchToMobile();
|
cy.common_switch_to_mobile_and_click_toggle();
|
||||||
|
cy.get(networkParametersNavigation).click();
|
||||||
cy.get_network_parameters().then((network_parameters) => {
|
cy.get_network_parameters().then((network_parameters) => {
|
||||||
network_parameters = Object.entries(network_parameters);
|
network_parameters = Object.entries(network_parameters);
|
||||||
network_parameters.forEach((network_parameter) => {
|
network_parameters.forEach((network_parameter) => {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ context.skip('Parties page', { tags: '@regression' }, function () {
|
|||||||
.convert_string_json_to_js_object()
|
.convert_string_json_to_js_object()
|
||||||
.get_party_accounts_data_from_js_object()
|
.get_party_accounts_data_from_js_object()
|
||||||
.then((accountsListedInJson) => {
|
.then((accountsListedInJson) => {
|
||||||
cy.getAssets().then((assetsInfo) => {
|
cy.get_asset_information().then((assetsInfo) => {
|
||||||
const assetInfo =
|
const assetInfo =
|
||||||
assetsInfo[accountsListedInJson[assetInTest].asset.name];
|
assetsInfo[accountsListedInJson[assetInTest].asset.name];
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ context.skip('Parties page', { tags: '@regression' }, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('get_asset_decimals', (assetID) => {
|
Cypress.Commands.add('get_asset_decimals', (assetID) => {
|
||||||
cy.getAssets().then((assetsInfo) => {
|
cy.get_asset_information().then((assetsInfo) => {
|
||||||
const assetDecimals = assetsInfo[assetData[assetID].name].decimals;
|
const assetDecimals = assetsInfo[assetData[assetID].name].decimals;
|
||||||
let decimals = '';
|
let decimals = '';
|
||||||
for (let i = 0; i < assetDecimals; i++) decimals += '0';
|
for (let i = 0; i < assetDecimals; i++) decimals += '0';
|
||||||
|
|||||||
@@ -1,15 +1,303 @@
|
|||||||
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('/validators');
|
cy.visit('/');
|
||||||
|
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 () {
|
||||||
it('should be able to see validator tiles', function () {
|
before('Ensure at least two validators are present', function () {
|
||||||
cy.getNodes().then((nodes) => {
|
assert.isAtLeast(
|
||||||
nodes.forEach((node) => {
|
this.validators.length,
|
||||||
cy.get(`[validator-id="${node.id}"]`).should('be.visible');
|
2,
|
||||||
|
'Ensuring at least two validators exist'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to see validator page sections', function () {
|
||||||
|
cy.get(vegaDataHeader)
|
||||||
|
.contains('Vega data')
|
||||||
|
.and('is.visible')
|
||||||
|
.next()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(jsonSection).should('not.be.empty');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(tendermintDataHeader)
|
||||||
|
.contains('Tendermint data')
|
||||||
|
.and('is.visible')
|
||||||
|
.next()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(jsonSection).should('not.be.empty');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to see relevant validator information in tendermint section', function () {
|
||||||
|
cy.get(tendermintDataHeader)
|
||||||
|
.contains('Tendermint data')
|
||||||
|
.next()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(jsonSection)
|
||||||
|
.invoke('text')
|
||||||
|
.convert_string_json_to_js_object()
|
||||||
|
.then((validatorsInJson) => {
|
||||||
|
this.validators.forEach((validator, index) => {
|
||||||
|
const validatorInJson =
|
||||||
|
validatorsInJson.result.validators[index];
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
validatorInJson.address,
|
||||||
|
validator.address,
|
||||||
|
`Checking that validator address shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(validator.address).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
validatorInJson.pub_key.type,
|
||||||
|
validator.pub_key.type,
|
||||||
|
`Checking that validator public key type shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(validator.pub_key.type).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
validatorInJson.pub_key.value,
|
||||||
|
validator.pub_key.value,
|
||||||
|
`Checking that validator public key value shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(validator.pub_key.value).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
validatorInJson.voting_power,
|
||||||
|
validator.voting_power,
|
||||||
|
`Checking that validator voting power in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(validator.voting_power).should('be.visible');
|
||||||
|
|
||||||
|
// Proposer priority can change frequently mid test
|
||||||
|
// Therefore only checking the field name is present.
|
||||||
|
cy.contains('proposer_priority').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test disabled 2022/11/15 during the 0.62.1 upgrade. The JSON structure changed, and
|
||||||
|
// this test failed. Rather than fix it, it will be replaced when the validator view displays
|
||||||
|
// something useful rather than just dumping out the JSON.
|
||||||
|
xit('should be able to see relevant node information in vega data section', function () {
|
||||||
|
cy.get(vegaDataHeader)
|
||||||
|
.contains('Vega data')
|
||||||
|
.next()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(jsonSection)
|
||||||
|
.invoke('text')
|
||||||
|
.convert_string_json_to_js_object()
|
||||||
|
.then((nodesInJson) => {
|
||||||
|
this.nodes.forEach((node, index) => {
|
||||||
|
const nodeInJson = nodesInJson.edges[index].node;
|
||||||
|
|
||||||
|
// Vegacapsule shows no info or null for following fields:
|
||||||
|
// name, infoURL, avatarUrl, location, epoch data
|
||||||
|
// Therefore, these values remain unchecked.
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
nodeInJson.__typename,
|
||||||
|
node.__typename,
|
||||||
|
`Checking that node __typename shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(node.__typename).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
nodeInJson.id,
|
||||||
|
node.id,
|
||||||
|
`Checking that node id shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(node.id).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
nodeInJson.pubkey,
|
||||||
|
node.pubkey,
|
||||||
|
`Checking that node pubkey shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(node.pubkey).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
nodeInJson.tmPubkey,
|
||||||
|
node.tmPubkey,
|
||||||
|
`Checking that node tmPubkey shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(node.tmPubkey).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
nodeInJson.ethereumAddress,
|
||||||
|
node.ethereumAddress,
|
||||||
|
`Checking that node ethereumAddress shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(node.ethereumAddress).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
nodeInJson.stakedByOperator,
|
||||||
|
node.stakedByOperator,
|
||||||
|
`Checking that node stakedByOperator value shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(node.stakedByOperator).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
nodeInJson.stakedByDelegates,
|
||||||
|
node.stakedByDelegates,
|
||||||
|
`Checking that node stakedByDelegates value shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(node.stakedByDelegates).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
nodeInJson.stakedTotal,
|
||||||
|
node.stakedTotal,
|
||||||
|
`Checking that node stakedTotal shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(node.stakedTotal).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
nodeInJson.pendingStake,
|
||||||
|
node.pendingStake,
|
||||||
|
`Checking that node pendingStake shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(node.pendingStake).should('be.visible');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
nodeInJson.status,
|
||||||
|
node.status,
|
||||||
|
`Checking that node status shown in json matches system data`
|
||||||
|
);
|
||||||
|
cy.contains(node.status).should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to see validator page displayed on mobile', function () {
|
||||||
|
cy.common_switch_to_mobile_and_click_toggle();
|
||||||
|
cy.get(validatorMenuHeading).click();
|
||||||
|
cy.get(vegaDataHeader)
|
||||||
|
.contains('Vega data')
|
||||||
|
.and('is.visible')
|
||||||
|
.next()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(jsonSection).should('not.be.empty');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(tendermintDataHeader)
|
||||||
|
.contains('Tendermint data')
|
||||||
|
.and('is.visible')
|
||||||
|
.next()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(jsonSection).should('not.be.empty');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(tendermintDataHeader)
|
||||||
|
.contains('Tendermint data')
|
||||||
|
.next()
|
||||||
|
.within(() => {
|
||||||
|
this.validators.forEach((validator) => {
|
||||||
|
cy.contains(validator.address).should('be.visible');
|
||||||
|
cy.contains(validator.pub_key.type).should('be.visible');
|
||||||
|
cy.contains(validator.pub_key.value).should('be.visible');
|
||||||
|
cy.contains(validator.voting_power).should('be.visible');
|
||||||
|
// Proposer priority can change frequently mid test
|
||||||
|
// Therefore only checking the field name is present.
|
||||||
|
cy.contains('proposer_priority').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to switch validator page between light and dark mode', function () {
|
||||||
|
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
|
||||||
|
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
|
||||||
|
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
|
||||||
|
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
|
||||||
|
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
|
||||||
|
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
|
||||||
|
const themeSwitcher = '[data-testid="theme-switcher"]';
|
||||||
|
const jsonFields = '.hljs';
|
||||||
|
const sideMenuBackground = '.absolute';
|
||||||
|
|
||||||
|
// Engage dark mode if not allready set
|
||||||
|
cy.get(sideMenuBackground)
|
||||||
|
.should('have.css', 'background-color')
|
||||||
|
.then((background_color) => {
|
||||||
|
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
|
||||||
|
cy.get(themeSwitcher).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Engage white mode
|
||||||
|
cy.get(themeSwitcher).click();
|
||||||
|
|
||||||
|
// White Mode
|
||||||
|
cy.get(validatorMenuHeading)
|
||||||
|
.should('have.css', 'background-color')
|
||||||
|
.and('include', whiteThemeSelectedMenuOptionColor);
|
||||||
|
cy.get(jsonFields)
|
||||||
|
.should('have.css', 'background-color')
|
||||||
|
.and('include', whiteThemeJsonFieldBackColor);
|
||||||
|
cy.get(sideMenuBackground)
|
||||||
|
.should('have.css', 'background-color')
|
||||||
|
.and('include', whiteThemeSideMenuBackgroundColor);
|
||||||
|
|
||||||
|
// Dark Mode
|
||||||
|
cy.get(themeSwitcher).click();
|
||||||
|
cy.get(validatorMenuHeading)
|
||||||
|
.should('have.css', 'background-color')
|
||||||
|
.and('include', darkThemeSelectedMenuOptionColor);
|
||||||
|
cy.get(jsonFields)
|
||||||
|
.should('have.css', 'background-color')
|
||||||
|
.and('include', darkThemeJsonFieldBackColor);
|
||||||
|
cy.get(sideMenuBackground)
|
||||||
|
.should('have.css', 'background-color')
|
||||||
|
.and('include', darkThemeSideMenuBackgroundColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('get_validators', () => {
|
||||||
|
cy.request({
|
||||||
|
method: 'GET',
|
||||||
|
url: `http://localhost:26617/validators`,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
})
|
||||||
|
.its(`body.result.validators`)
|
||||||
|
.then(function (response) {
|
||||||
|
let validators = [];
|
||||||
|
response.forEach((account, index) => {
|
||||||
|
validators[index] = account;
|
||||||
|
});
|
||||||
|
return validators;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('get_nodes', () => {
|
||||||
|
const mutation =
|
||||||
|
'{nodesConnection { edges { node { id name infoUrl avatarUrl pubkey tmPubkey ethereumAddress \
|
||||||
|
location stakedByOperator stakedByDelegates stakedTotal pendingStake \
|
||||||
|
epochData { total offline online __typename } status name __typename}}}}';
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: `http://localhost:3028/query`,
|
||||||
|
body: {
|
||||||
|
query: mutation,
|
||||||
|
},
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
})
|
||||||
|
.its(`body.data.nodesConnection.edges`)
|
||||||
|
.then(function (response) {
|
||||||
|
let nodes = [];
|
||||||
|
response.forEach((node) => {
|
||||||
|
nodes.push(node);
|
||||||
|
});
|
||||||
|
return nodes;
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ Cypress.Commands.add(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
Cypress.Commands.add('switchToMobile', () => {
|
|
||||||
cy.viewport('iphone-x');
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('common_switch_to_mobile_and_click_toggle', function () {
|
Cypress.Commands.add('common_switch_to_mobile_and_click_toggle', function () {
|
||||||
cy.viewport('iphone-x');
|
cy.viewport('iphone-x');
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
|
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
||||||
NX_VEGA_NETWORKS='{"SANDBOX":"https://sandbox.explorer.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=STAGNET3
|
NX_VEGA_ENV=STAGNET3
|
||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_TENDERMINT_URL=http://localhost:26617
|
NX_TENDERMINT_URL=http://localhost:26617
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
||||||
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=CUSTOM
|
NX_VEGA_ENV=CUSTOM
|
||||||
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
|
||||||
# App flags
|
# App flags
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm
|
NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
||||||
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=DEVNET
|
NX_VEGA_ENV=DEVNET
|
||||||
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
|
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
|
|
||||||
NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql
|
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet1-network.json
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_TENDERMINT_URL=https://be.explorer.vega.xyz
|
NX_TENDERMINT_URL=https://be.explorer.vega.xyz
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
|
||||||
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=MAINNET
|
NX_VEGA_ENV=MAINNET
|
||||||
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
|
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
|
||||||
NX_ETHERSCAN_URL=https://etherscan.io
|
NX_ETHERSCAN_URL=https://etherscan.io
|
||||||
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
|
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=https://tm.be.mainnet-mirror.vega.xyz
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mirror-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mirror-network.json
|
||||||
|
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||||
NX_VEGA_ENV=MIRROR
|
NX_VEGA_ENV=MIRROR
|
||||||
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
|
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
|
|
||||||
|
|
||||||
# App flags
|
# App flags
|
||||||
NX_EXPLORER_ASSETS=1
|
NX_EXPLORER_ASSETS=1
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
|
|||||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.sandbox.vega.xyz/websocket
|
NX_TENDERMINT_URL=https://tm.n01.sandbox.vega.xyz
|
||||||
|
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.sandbox.vega.xyz/websocket
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
|||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
|
||||||
NX_VEGA_ENV=STAGNET1
|
NX_VEGA_ENV=STAGNET1
|
||||||
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
|
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
|
||||||
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
|
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
|
||||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||||
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
|
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||||
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
|
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
NX_VEGA_GOVERNANCE_URL=https://stagnet1.token.vega.xyz
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
|
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.stagnet3.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
||||||
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=STAGNET3
|
NX_VEGA_ENV=STAGNET3
|
||||||
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
|
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
|
||||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases/
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||||
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
|
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.testnet.vega.xyz/websocket/
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
||||||
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=TESTNET
|
NX_VEGA_ENV=TESTNET
|
||||||
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
|
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
|
||||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||||
|
NX_VEGA_NETWORKS={}
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# App configuration variables
|
|
||||||
NX_VEGA_ENV=VALIDATOR_TESTNET
|
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/validator-testnet-network.json
|
|
||||||
NX_VEGA_URL=https://api.validators-testnet.vega.xyz/graphql
|
|
||||||
NX_VEGA_REST=https://api.validators-testnet.vega.xyz/
|
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
|
||||||
|
|
||||||
NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
|
|
||||||
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.xyz
|
|
||||||
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.xyz/rest
|
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
|
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
NX_TENDERMINT_URL=http://localhost:26607/
|
NX_TENDERMINT_URL=http://localhost:26607/
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
|
||||||
NX_VEGA_ENV=CUSTOM
|
NX_VEGA_ENV=CUSTOM
|
||||||
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_BLOCK_EXPLORER=
|
NX_BLOCK_EXPLORER=
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
"executor": "@nrwl/workspace:run-commands",
|
"executor": "@nrwl/workspace:run-commands",
|
||||||
"options": {
|
"options": {
|
||||||
"commands": [
|
"commands": [
|
||||||
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.67.3/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
|
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.66.1/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
function ReactMarkdown({ children }) {
|
|
||||||
return <div>{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ReactMarkdown;
|
|
||||||
@@ -1,95 +1,62 @@
|
|||||||
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
import classnames from 'classnames';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
|
||||||
|
import { Nav } from './components/nav';
|
||||||
import { Header } from './components/header';
|
import { Header } from './components/header';
|
||||||
import { Main } from './components/main';
|
import { Main } from './components/main';
|
||||||
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
||||||
|
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||||
import { Footer } from './components/footer/footer';
|
import { Footer } from './components/footer/footer';
|
||||||
import {
|
|
||||||
AnnouncementBanner,
|
|
||||||
ExternalLink,
|
|
||||||
Icon,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import {
|
|
||||||
AssetDetailsDialog,
|
|
||||||
useAssetDetailsDialogStore,
|
|
||||||
} from '@vegaprotocol/assets';
|
|
||||||
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const DialogsContainer = () => {
|
|
||||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
|
||||||
return (
|
|
||||||
<AssetDetailsDialog
|
|
||||||
assetId={id}
|
|
||||||
trigger={trigger || null}
|
|
||||||
asJson={asJson}
|
|
||||||
open={isOpen}
|
|
||||||
onChange={setOpen}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MainnetSimAd = () => {
|
|
||||||
const [shouldDisplayBanner, setShouldDisplayBanner] = useState<boolean>(true);
|
|
||||||
|
|
||||||
// Return an empty div so that the grid layout in _app.page.ts
|
|
||||||
// renders correctly
|
|
||||||
if (!shouldDisplayBanner) {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnnouncementBanner>
|
|
||||||
<div className="grid grid-cols-[auto_1fr] gap-4 font-alpha calt uppercase text-center text-lg text-white">
|
|
||||||
<button
|
|
||||||
className="flex items-center"
|
|
||||||
onClick={() => setShouldDisplayBanner(false)}
|
|
||||||
>
|
|
||||||
<Icon name="cross" className="w-6 h-6" ariaLabel="dismiss" />
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
<span className="pr-4">Mainnet sim 3 is coming soon!</span>
|
|
||||||
<ExternalLink href="https://fairground.wtf/">Learn more</ExternalLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AnnouncementBanner>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMenuOpen(false);
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
const cacheConfig: InMemoryCacheConfig = {
|
||||||
|
typePolicies: {
|
||||||
|
statistics: {
|
||||||
|
keyFields: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const layoutClasses = classnames(
|
||||||
|
'grid grid-rows-[auto_1fr_auto] grid-cols-[1fr] md:grid-rows-[auto_minmax(700px,_1fr)_auto] md:grid-cols-[300px_1fr]',
|
||||||
|
'min-h-[100vh] mx-auto my-0',
|
||||||
|
'border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r',
|
||||||
|
'bg-white dark:bg-black',
|
||||||
|
'antialiased text-black dark:text-white',
|
||||||
|
{
|
||||||
|
'h-[100vh] min-h-auto overflow-hidden': menuOpen,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TendermintWebsocketProvider>
|
<TendermintWebsocketProvider>
|
||||||
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
<NetworkLoader cache={cacheConfig}>
|
||||||
<div
|
<div className={layoutClasses}>
|
||||||
className={classNames(
|
<Header menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
|
||||||
'max-w-[1500px] min-h-[100vh]',
|
<Nav menuOpen={menuOpen} />
|
||||||
'mx-auto my-0',
|
<Main />
|
||||||
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
|
<Footer />
|
||||||
'border-vega-light-200 dark:border-vega-dark-200 lg:border-l lg:border-r',
|
|
||||||
'antialiased text-black dark:text-white',
|
|
||||||
'overflow-hidden relative'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Header />
|
|
||||||
<MainnetSimAd />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Main />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<DialogsContainer />
|
|
||||||
</NetworkLoader>
|
</NetworkLoader>
|
||||||
</TendermintWebsocketProvider>
|
</TendermintWebsocketProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
useInitializeEnv();
|
return (
|
||||||
return <App />;
|
<EnvironmentProvider>
|
||||||
|
<App />
|
||||||
|
</EnvironmentProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Wrapper;
|
export default Wrapper;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAssetDataProvider } from '@vegaprotocol/assets';
|
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
|
||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
|
||||||
import { AssetLink } from '../links';
|
import { AssetLink } from '../links';
|
||||||
|
import { useExplorerAssetQuery } from '../links/asset-link/__generated__/Asset';
|
||||||
|
|
||||||
export type AssetBalanceProps = {
|
export type AssetBalanceProps = {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
@@ -17,17 +17,21 @@ const AssetBalance = ({
|
|||||||
price,
|
price,
|
||||||
showAssetLink = true,
|
showAssetLink = true,
|
||||||
}: AssetBalanceProps) => {
|
}: AssetBalanceProps) => {
|
||||||
const { data: asset } = useAssetDataProvider(assetId);
|
const { data } = useExplorerAssetQuery({
|
||||||
|
variables: { id: assetId },
|
||||||
|
});
|
||||||
|
|
||||||
const label =
|
const label =
|
||||||
asset && asset.decimals
|
data && data.asset?.decimals
|
||||||
? addDecimalsFormatNumber(price, asset.decimals)
|
? addDecimalsFormatNumber(price, data.asset.decimals)
|
||||||
: price;
|
: price;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-block">
|
<div className="inline-block">
|
||||||
<span>{label}</span>{' '}
|
<span>{label}</span>{' '}
|
||||||
{showAssetLink && asset?.id ? <AssetLink assetId={assetId} /> : null}
|
{showAssetLink && data?.asset?.id ? (
|
||||||
|
<AssetLink id={data.asset.id} />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
|
||||||
import { useExplorerGovernanceAssetQuery } from './__generated__/Governance-asset';
|
import { useExplorerGovernanceAssetQuery } from './__generated__/Governance-asset';
|
||||||
import AssetBalance from './asset-balance';
|
import AssetBalance from './asset-balance';
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { render, waitFor } from '@testing-library/react';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { assetsList } from '../../mocks/assets';
|
|
||||||
import { AssetsTable } from './assets-table';
|
|
||||||
|
|
||||||
describe('AssetsTable', () => {
|
|
||||||
it('shows loading message on first render', async () => {
|
|
||||||
const res = render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<AssetsTable data={null} />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
expect(await res.findByText('Loading...')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows no data message if no assets found', async () => {
|
|
||||||
const res = render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<AssetsTable data={[]} />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await res.findByText('This chain has no assets')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows a table/list with all the assets', async () => {
|
|
||||||
const res = render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<AssetsTable data={assetsList} />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
await waitFor(() => {
|
|
||||||
const rowA1 = res.container.querySelector('[row-id="123"]');
|
|
||||||
expect(rowA1).toBeInTheDocument();
|
|
||||||
|
|
||||||
const rowA2 = res.container.querySelector('[row-id="456"]');
|
|
||||||
expect(rowA2).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
|
||||||
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
|
||||||
import { AgGridColumn } from 'ag-grid-react';
|
|
||||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
|
||||||
import type { VegaICellRendererParams } from '@vegaprotocol/datagrid';
|
|
||||||
import { useRef, useLayoutEffect } from 'react';
|
|
||||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import type { RowClickedEvent } from 'ag-grid-community';
|
|
||||||
|
|
||||||
type AssetsTableProps = {
|
|
||||||
data: AssetFieldsFragment[] | null;
|
|
||||||
};
|
|
||||||
export const AssetsTable = ({ data }: AssetsTableProps) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const ref = useRef<AgGridReact>(null);
|
|
||||||
const showColumnsOnDesktop = () => {
|
|
||||||
ref.current?.columnApi.setColumnsVisible(
|
|
||||||
['id', 'type', 'status'],
|
|
||||||
window.innerWidth > BREAKPOINT_MD
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
window.addEventListener('resize', showColumnsOnDesktop);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', showColumnsOnDesktop);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AgGrid
|
|
||||||
ref={ref}
|
|
||||||
rowData={data}
|
|
||||||
getRowId={({ data }: { data: AssetFieldsFragment }) => data.id}
|
|
||||||
overlayNoRowsTemplate={t('This chain has no assets')}
|
|
||||||
domLayout="autoHeight"
|
|
||||||
defaultColDef={{
|
|
||||||
flex: 1,
|
|
||||||
resizable: true,
|
|
||||||
sortable: true,
|
|
||||||
filter: true,
|
|
||||||
filterParams: { buttons: ['reset'] },
|
|
||||||
autoHeight: true,
|
|
||||||
}}
|
|
||||||
suppressCellFocus={true}
|
|
||||||
onRowClicked={({ data }: RowClickedEvent) => {
|
|
||||||
navigate(data.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AgGridColumn headerName={t('Symbol')} field="symbol" />
|
|
||||||
<AgGridColumn headerName={t('Name')} field="name" />
|
|
||||||
<AgGridColumn
|
|
||||||
flex="2"
|
|
||||||
headerName={t('ID')}
|
|
||||||
field="id"
|
|
||||||
hide={window.innerWidth < BREAKPOINT_MD}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="type"
|
|
||||||
headerName={t('Type')}
|
|
||||||
field="source.__typename"
|
|
||||||
hide={window.innerWidth < BREAKPOINT_MD}
|
|
||||||
valueFormatter={({ value }: { value?: string }) =>
|
|
||||||
value && AssetTypeMapping[value].value
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
headerName={t('Status')}
|
|
||||||
field="status"
|
|
||||||
hide={window.innerWidth < BREAKPOINT_MD}
|
|
||||||
valueFormatter={({ value }: { value?: string }) =>
|
|
||||||
value && AssetStatusMapping[value].value
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="actions"
|
|
||||||
headerName=""
|
|
||||||
sortable={false}
|
|
||||||
filter={false}
|
|
||||||
resizable={false}
|
|
||||||
wrapText={true}
|
|
||||||
field="id"
|
|
||||||
cellRenderer={({
|
|
||||||
value,
|
|
||||||
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
|
|
||||||
value ? (
|
|
||||||
<ButtonLink
|
|
||||||
onClick={(e) => {
|
|
||||||
navigate(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('View details')}
|
|
||||||
</ButtonLink>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</AgGrid>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -2,11 +2,11 @@ import React from 'react';
|
|||||||
import type { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response';
|
import type { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response';
|
||||||
import { Routes } from '../../routes/route-names';
|
import { Routes } from '../../routes/route-names';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
|
||||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import { TimeAgo } from '../time-ago';
|
import { TimeAgo } from '../time-ago';
|
||||||
import { TableWithTbody, TableRow, TableCell } from '../table';
|
import { TableWithTbody, TableRow, TableCell } from '../table';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
interface BlockProps {
|
interface BlockProps {
|
||||||
block: BlockMeta;
|
block: BlockMeta;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { TendermintBlockchainResponse } from '../../routes/blocks/tendermint-blockchain-response';
|
import type { TendermintBlockchainResponse } from '../../routes/blocks/tendermint-blockchain-response';
|
||||||
import { BlockData } from './block-data';
|
import { BlockData } from './block-data';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FixedSizeList as List } from 'react-window';
|
import { FixedSizeList as List } from 'react-window';
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import InfiniteLoader from 'react-window-infinite-loader';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import type { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response';
|
import type { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response';
|
||||||
import { BlockData } from './block-data';
|
import { BlockData } from './block-data';
|
||||||
import EmptyList from '../empty-list/empty-list';
|
import EmptyList from '../empty-list/empty-list';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket';
|
import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
interface BlocksRefetchProps {
|
interface BlocksRefetchProps {
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
Icon,
|
|
||||||
SyntaxHighlighter,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
type JsonViewerDialogProps = {
|
|
||||||
title: string;
|
|
||||||
content: unknown;
|
|
||||||
open: boolean;
|
|
||||||
onChange: (isOpen: boolean) => void;
|
|
||||||
trigger?: HTMLElement;
|
|
||||||
};
|
|
||||||
export const JsonViewerDialog = ({
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
open,
|
|
||||||
onChange,
|
|
||||||
trigger,
|
|
||||||
}: JsonViewerDialogProps) => {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
size="medium"
|
|
||||||
title={title}
|
|
||||||
icon={<Icon name="info-sign"></Icon>}
|
|
||||||
open={open}
|
|
||||||
onChange={(isOpen) => onChange(isOpen)}
|
|
||||||
onCloseAutoFocus={(e) => {
|
|
||||||
/**
|
|
||||||
* This mimics radix's default behaviour that focuses the dialog's
|
|
||||||
* trigger after closing itself
|
|
||||||
*/
|
|
||||||
if (trigger) {
|
|
||||||
e.preventDefault();
|
|
||||||
trigger.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="pr-8 mb-8 max-h-[70vh] overflow-y-scroll">
|
|
||||||
<SyntaxHighlighter size="smaller" data={content} />
|
|
||||||
</div>
|
|
||||||
<div className="w-1/4">
|
|
||||||
<Button
|
|
||||||
data-testid="close-asset-details-dialog"
|
|
||||||
fill={true}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onChange(false)}
|
|
||||||
>
|
|
||||||
{t('Close')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -19,12 +19,12 @@ const EmptyList = ({ heading, label }: EmptyListProps) => {
|
|||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{heading ? (
|
{heading ? (
|
||||||
<h1 className="font-alpha calt text-xl uppercase text-center leading-relaxed">
|
<h1 className="font-alpha text-xl uppercase text-center leading-relaxed">
|
||||||
{heading}
|
{heading}
|
||||||
</h1>
|
</h1>
|
||||||
) : null}
|
) : null}
|
||||||
{label ? (
|
{label ? (
|
||||||
<p className="font-alpha calt text-gray-500 text-center">{label}</p>
|
<p className="font-alpha text-gray-500 text-center">{label}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
query ExplorerEpoch($id: ID!) {
|
|
||||||
epoch(id: $id) {
|
|
||||||
id
|
|
||||||
timestamps {
|
|
||||||
start
|
|
||||||
end
|
|
||||||
firstBlock
|
|
||||||
lastBlock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query ExplorerFutureEpoch {
|
|
||||||
networkParameter(key: "validators.epoch.length") {
|
|
||||||
value
|
|
||||||
}
|
|
||||||
|
|
||||||
epoch {
|
|
||||||
id
|
|
||||||
timestamps {
|
|
||||||
start
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import * as Types from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
import { gql } from '@apollo/client';
|
|
||||||
import * as Apollo from '@apollo/client';
|
|
||||||
const defaultOptions = {} as const;
|
|
||||||
export type ExplorerEpochQueryVariables = Types.Exact<{
|
|
||||||
id: Types.Scalars['ID'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
|
|
||||||
export type ExplorerEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, firstBlock: string, lastBlock?: string | null } } };
|
|
||||||
|
|
||||||
export type ExplorerFutureEpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
|
||||||
|
|
||||||
|
|
||||||
export type ExplorerFutureEpochQuery = { __typename?: 'Query', networkParameter?: { __typename?: 'NetworkParameter', value: string } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null } } };
|
|
||||||
|
|
||||||
|
|
||||||
export const ExplorerEpochDocument = gql`
|
|
||||||
query ExplorerEpoch($id: ID!) {
|
|
||||||
epoch(id: $id) {
|
|
||||||
id
|
|
||||||
timestamps {
|
|
||||||
start
|
|
||||||
end
|
|
||||||
firstBlock
|
|
||||||
lastBlock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* __useExplorerEpochQuery__
|
|
||||||
*
|
|
||||||
* To run a query within a React component, call `useExplorerEpochQuery` and pass it any options that fit your needs.
|
|
||||||
* When your component renders, `useExplorerEpochQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
||||||
* you can use to render your UI.
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { data, loading, error } = useExplorerEpochQuery({
|
|
||||||
* variables: {
|
|
||||||
* id: // value for 'id'
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useExplorerEpochQuery(baseOptions: Apollo.QueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
|
|
||||||
}
|
|
||||||
export function useExplorerEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useLazyQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
|
|
||||||
}
|
|
||||||
export type ExplorerEpochQueryHookResult = ReturnType<typeof useExplorerEpochQuery>;
|
|
||||||
export type ExplorerEpochLazyQueryHookResult = ReturnType<typeof useExplorerEpochLazyQuery>;
|
|
||||||
export type ExplorerEpochQueryResult = Apollo.QueryResult<ExplorerEpochQuery, ExplorerEpochQueryVariables>;
|
|
||||||
export const ExplorerFutureEpochDocument = gql`
|
|
||||||
query ExplorerFutureEpoch {
|
|
||||||
networkParameter(key: "validators.epoch.length") {
|
|
||||||
value
|
|
||||||
}
|
|
||||||
epoch {
|
|
||||||
id
|
|
||||||
timestamps {
|
|
||||||
start
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* __useExplorerFutureEpochQuery__
|
|
||||||
*
|
|
||||||
* To run a query within a React component, call `useExplorerFutureEpochQuery` and pass it any options that fit your needs.
|
|
||||||
* When your component renders, `useExplorerFutureEpochQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
||||||
* you can use to render your UI.
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { data, loading, error } = useExplorerFutureEpochQuery({
|
|
||||||
* variables: {
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useExplorerFutureEpochQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
|
|
||||||
}
|
|
||||||
export function useExplorerFutureEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useLazyQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
|
|
||||||
}
|
|
||||||
export type ExplorerFutureEpochQueryHookResult = ReturnType<typeof useExplorerFutureEpochQuery>;
|
|
||||||
export type ExplorerFutureEpochLazyQueryHookResult = ReturnType<typeof useExplorerFutureEpochLazyQuery>;
|
|
||||||
export type ExplorerFutureEpochQueryResult = Apollo.QueryResult<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>;
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { render } from '@testing-library/react';
|
|
||||||
import { IconForEpoch } from './epoch';
|
|
||||||
|
|
||||||
const THE_PAST = 'Monday, 17 February 2022 11:44:09';
|
|
||||||
const THE_FUTURE = 'Monday, 17 February 3023 11:44:09';
|
|
||||||
|
|
||||||
describe('IconForEpoch', () => {
|
|
||||||
it('Handles malformed dates', () => {
|
|
||||||
const start = 'This is n0t a d4te';
|
|
||||||
const end = '📅';
|
|
||||||
const screen = render(<IconForEpoch start={start} end={end} />);
|
|
||||||
|
|
||||||
expect(screen.getByRole('img')).toHaveAttribute(
|
|
||||||
'aria-label',
|
|
||||||
'calendar icon'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defaults to a calendar icon', () => {
|
|
||||||
const start = null as unknown as string;
|
|
||||||
const end = null as unknown as string;
|
|
||||||
const screen = render(<IconForEpoch start={start} end={end} />);
|
|
||||||
|
|
||||||
expect(screen.getByRole('img')).toHaveAttribute(
|
|
||||||
'aria-label',
|
|
||||||
'calendar icon'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if start and end are both in the future, stick with calendar', () => {
|
|
||||||
const screen = render(<IconForEpoch start={THE_FUTURE} end={THE_FUTURE} />);
|
|
||||||
|
|
||||||
expect(screen.getByRole('img')).toHaveAttribute(
|
|
||||||
'aria-label',
|
|
||||||
'calendar icon'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if start is in the past and end is in the future, this is currently active', () => {
|
|
||||||
const screen = render(<IconForEpoch start={THE_PAST} end={THE_FUTURE} />);
|
|
||||||
|
|
||||||
expect(screen.getByRole('img')).toHaveAttribute(
|
|
||||||
'aria-label',
|
|
||||||
'circle icon'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if start and end are in the paste, this is done', () => {
|
|
||||||
const screen = render(<IconForEpoch start={THE_PAST} end={THE_PAST} />);
|
|
||||||
|
|
||||||
expect(screen.getByRole('img')).toHaveAttribute(
|
|
||||||
'aria-label',
|
|
||||||
'tick-circle icon'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import EpochMissingOverview, { calculateEpochData } from './epoch-missing';
|
|
||||||
import { getSecondsFromInterval } from '@vegaprotocol/utils';
|
|
||||||
const START_DATE_PAST = 'Monday, 17 February 2022 11:44:09';
|
|
||||||
|
|
||||||
describe('getSecondsFromInterval', () => {
|
|
||||||
it('returns 0 for bad data', () => {
|
|
||||||
expect(getSecondsFromInterval(null as unknown as string)).toEqual(0);
|
|
||||||
expect(getSecondsFromInterval('')).toEqual(0);
|
|
||||||
expect(getSecondsFromInterval('🧙')).toEqual(0);
|
|
||||||
expect(getSecondsFromInterval(2 as unknown as string)).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses out months from a capital M', () => {
|
|
||||||
expect(getSecondsFromInterval('2M')).toEqual(5184000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses out days from a capital D', () => {
|
|
||||||
expect(getSecondsFromInterval('1D')).toEqual(86400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses out hours from a lower case h', () => {
|
|
||||||
expect(getSecondsFromInterval('11h')).toEqual(39600);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses out minutes from a lower case m', () => {
|
|
||||||
expect(getSecondsFromInterval('10m')).toEqual(600);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses out seconds from a lower case s', () => {
|
|
||||||
expect(getSecondsFromInterval('99s')).toEqual(99);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses complex examples', () => {
|
|
||||||
expect(getSecondsFromInterval('24h')).toEqual(86400);
|
|
||||||
expect(getSecondsFromInterval('1h30m')).toEqual(5400);
|
|
||||||
expect(getSecondsFromInterval('1D1h30m1s')).toEqual(91801);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('calculateEpochData', () => {
|
|
||||||
it('Handles bad data', () => {
|
|
||||||
const currentEpochId = null as unknown as string;
|
|
||||||
const missingEpochId = null as unknown as string;
|
|
||||||
const epochStart = null as unknown as string;
|
|
||||||
const epochLength = null as unknown as string;
|
|
||||||
const res = calculateEpochData(
|
|
||||||
currentEpochId,
|
|
||||||
missingEpochId,
|
|
||||||
epochStart,
|
|
||||||
epochLength
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(res).toHaveProperty('label', 'Missing data');
|
|
||||||
expect(res).toHaveProperty('isInFuture', false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Calculates that a bigger epoch number is in the future from basic data', () => {
|
|
||||||
const currentEpochId = '10';
|
|
||||||
const missingEpochId = '20';
|
|
||||||
const epochStart = '';
|
|
||||||
const epochLength = '';
|
|
||||||
const res = calculateEpochData(
|
|
||||||
currentEpochId,
|
|
||||||
missingEpochId,
|
|
||||||
epochStart,
|
|
||||||
epochLength
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(res).toHaveProperty('isInFuture', true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('If it has an epoch length and a start time, it provides an estimate', () => {
|
|
||||||
const currentEpochId = '10';
|
|
||||||
const missingEpochId = '20';
|
|
||||||
const epochStart = START_DATE_PAST;
|
|
||||||
const epochLength = '1s';
|
|
||||||
const res = calculateEpochData(
|
|
||||||
currentEpochId,
|
|
||||||
missingEpochId,
|
|
||||||
epochStart,
|
|
||||||
epochLength
|
|
||||||
);
|
|
||||||
|
|
||||||
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
|
|
||||||
expect(res).toHaveProperty('label');
|
|
||||||
expect(res.label).toMatch(/^Estimate/);
|
|
||||||
expect(res.label).toMatch(/in less than a minute$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Provide decent string for past', () => {
|
|
||||||
const currentEpochId = '20';
|
|
||||||
const missingEpochId = '10';
|
|
||||||
const epochStart = START_DATE_PAST;
|
|
||||||
const epochLength = '1s';
|
|
||||||
const res = calculateEpochData(
|
|
||||||
currentEpochId,
|
|
||||||
missingEpochId,
|
|
||||||
epochStart,
|
|
||||||
epochLength
|
|
||||||
);
|
|
||||||
|
|
||||||
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
|
|
||||||
expect(res).toHaveProperty('label');
|
|
||||||
expect(res.label).toMatch(/^Estimate/);
|
|
||||||
expect(res.label).toMatch(/less than a minute ago$/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('EpochMissingOverview', () => {
|
|
||||||
function renderComponent(missingEpochId: string) {
|
|
||||||
return render(
|
|
||||||
<MockedProvider>
|
|
||||||
<EpochMissingOverview missingEpochId={missingEpochId} />
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
it('renders a - if no id is provided', () => {
|
|
||||||
const n = null as unknown as string;
|
|
||||||
const screen = renderComponent(n);
|
|
||||||
expect(screen.getByTestId('empty')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import { useExplorerFutureEpochQuery } from './__generated__/Epoch';
|
|
||||||
|
|
||||||
import addSeconds from 'date-fns/addSeconds';
|
|
||||||
import formatDistance from 'date-fns/formatDistance';
|
|
||||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import isFuture from 'date-fns/isFuture';
|
|
||||||
import { getSecondsFromInterval, isValidDate } from '@vegaprotocol/utils';
|
|
||||||
|
|
||||||
export type EpochMissingOverviewProps = {
|
|
||||||
missingEpochId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a set of details for an epoch that has no representation in the
|
|
||||||
* data node. This is primarily for one of two reasons:
|
|
||||||
*
|
|
||||||
* 1. The epoch hasn't happened yet
|
|
||||||
* 2. The epoch happened before a snapshot, and thus the details don't exist
|
|
||||||
*
|
|
||||||
* This component is used when the API has responded with no data for an epoch
|
|
||||||
* by ID, so we already know that we can't display start time/block etc.
|
|
||||||
*
|
|
||||||
* We can detect 1 if the epoch is a higher number than the current epoch
|
|
||||||
* We can detect 2 if the epoch is in the past, but we still get no response.
|
|
||||||
*/
|
|
||||||
const EpochMissingOverview = ({
|
|
||||||
missingEpochId,
|
|
||||||
}: EpochMissingOverviewProps) => {
|
|
||||||
const { data, error, loading } = useExplorerFutureEpochQuery();
|
|
||||||
|
|
||||||
// This should not happen, but it's easily handled
|
|
||||||
if (!missingEpochId) {
|
|
||||||
return <span data-testid="empty">-</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No data should also not happen - we've requested the current epoch. This
|
|
||||||
// could happen at chain restart, but shouldn't. If it does, fallback.
|
|
||||||
if (!data || loading || error) {
|
|
||||||
return <span data-testid="empty">{missingEpochId}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have enough information to predict a future or past block time, let's do it
|
|
||||||
if (
|
|
||||||
!missingEpochId ||
|
|
||||||
!data.epoch.id ||
|
|
||||||
!data.epoch.timestamps.start ||
|
|
||||||
!data?.networkParameter?.value
|
|
||||||
) {
|
|
||||||
return <span data-testid="empty">{missingEpochId}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { label, isInFuture } = calculateEpochData(
|
|
||||||
data.epoch.id,
|
|
||||||
missingEpochId,
|
|
||||||
data.epoch.timestamps.start,
|
|
||||||
data.networkParameter.value
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip description={<p className="text-xs m-2">{label}</p>}>
|
|
||||||
<p>
|
|
||||||
{isInFuture ? (
|
|
||||||
<Icon name="calendar" className="mr-1" />
|
|
||||||
) : (
|
|
||||||
<Icon name="outdated" className="mr-1" />
|
|
||||||
)}
|
|
||||||
{missingEpochId}
|
|
||||||
</p>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function calculateEpochData(
|
|
||||||
currentEpochId: string,
|
|
||||||
missingEpochId: string,
|
|
||||||
epochStart: string,
|
|
||||||
epochLength: string
|
|
||||||
) {
|
|
||||||
// Blank string will be return 0 seconds from getSecondsFromInterval
|
|
||||||
const epochLengthInSeconds = getSecondsFromInterval(epochLength);
|
|
||||||
|
|
||||||
if (!epochStart || !epochLength) {
|
|
||||||
// Let's just take a guess
|
|
||||||
return {
|
|
||||||
label: 'Missing data',
|
|
||||||
isInFuture: parseInt(missingEpochId) > parseInt(currentEpochId),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const startFrom = new Date(epochStart);
|
|
||||||
|
|
||||||
const diff = parseInt(missingEpochId) - parseInt(currentEpochId);
|
|
||||||
const futureDate = addSeconds(startFrom, diff * epochLengthInSeconds);
|
|
||||||
|
|
||||||
const label =
|
|
||||||
isValidDate(futureDate) && isValidDate(startFrom)
|
|
||||||
? `Estimate: ${futureDate.toLocaleString()} - ${formatDistance(
|
|
||||||
futureDate,
|
|
||||||
startFrom,
|
|
||||||
{ addSuffix: true }
|
|
||||||
)}`
|
|
||||||
: 'Missing data';
|
|
||||||
|
|
||||||
return {
|
|
||||||
label,
|
|
||||||
isInFuture: isFuture(futureDate),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EpochMissingOverview;
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { useExplorerEpochQuery } from './__generated__/Epoch';
|
|
||||||
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { BlockLink } from '../links';
|
|
||||||
import { Time } from '../time';
|
|
||||||
import { TimeAgo } from '../time-ago';
|
|
||||||
import EpochMissingOverview from './epoch-missing';
|
|
||||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import type { IconProps } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import isPast from 'date-fns/isPast';
|
|
||||||
|
|
||||||
const borderClass =
|
|
||||||
'border-solid border-2 border-vega-dark-200 border-collapse';
|
|
||||||
|
|
||||||
export type EpochOverviewProps = {
|
|
||||||
id?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays detailed information about an epoch, given an ID. This
|
|
||||||
* works for past epochs and current epochs - future epochs, and a
|
|
||||||
* few other situations (see epoch-missing) will not return us
|
|
||||||
* enough information to render this.
|
|
||||||
*
|
|
||||||
* The details are hidden in a tooltip, behind the epoch number
|
|
||||||
*/
|
|
||||||
const EpochOverview = ({ id }: EpochOverviewProps) => {
|
|
||||||
const { data, error, loading } = useExplorerEpochQuery({
|
|
||||||
variables: { id: id || '' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const ti = data?.epoch.timestamps;
|
|
||||||
if (
|
|
||||||
error?.message &&
|
|
||||||
error.message.includes('no resource corresponding to this id')
|
|
||||||
) {
|
|
||||||
return <EpochMissingOverview missingEpochId={id} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ti || loading || error) {
|
|
||||||
return <span>{id}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const description = (
|
|
||||||
<table className="text-xs m-2">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th className={`text-center ${borderClass}`}>{t('Block')}</th>
|
|
||||||
<th className={`text-center ${borderClass}`}>{t('Time')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th className={`px-2 ${borderClass}`}>{t('Start')}</th>
|
|
||||||
<td className={`px-2 ${borderClass}`}>
|
|
||||||
{ti.firstBlock ? <BlockLink height={ti.firstBlock} /> : '-'}
|
|
||||||
</td>
|
|
||||||
<td className={`px-2 ${borderClass}`}>
|
|
||||||
<Time date={ti.start} />
|
|
||||||
<br />
|
|
||||||
<TimeAgo date={ti.start} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th className={`px-2 ${borderClass}`}>{t('End')}</th>
|
|
||||||
<td className={`px-2 ${borderClass}`}>
|
|
||||||
{ti.lastBlock ? (
|
|
||||||
<BlockLink height={ti.lastBlock} />
|
|
||||||
) : (
|
|
||||||
t('In progress')
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className={`px-2 ${borderClass}`}>
|
|
||||||
{ti.end ? (
|
|
||||||
<>
|
|
||||||
<Time date={ti.end} />
|
|
||||||
<br />
|
|
||||||
<TimeAgo date={ti.end} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span>{t('-')}</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip description={description}>
|
|
||||||
<p>
|
|
||||||
<IconForEpoch start={ti.start} end={ti.end} />
|
|
||||||
{id}
|
|
||||||
</p>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IconForEpochProps = {
|
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chooses an icon to display next to the epoch number, representing
|
|
||||||
* when the epoch is relative to now (i.e. not yet started, started,
|
|
||||||
* finished)
|
|
||||||
*/
|
|
||||||
export function IconForEpoch({ start, end }: IconForEpochProps) {
|
|
||||||
const startHasPassed = start ? isPast(new Date(start)) : false;
|
|
||||||
const endHasPassed = end ? isPast(new Date(end)) : false;
|
|
||||||
|
|
||||||
let i: IconProps['name'] = 'calendar';
|
|
||||||
|
|
||||||
if (!startHasPassed && !endHasPassed) {
|
|
||||||
i = 'calendar';
|
|
||||||
} else if (startHasPassed && !endHasPassed) {
|
|
||||||
i = 'circle';
|
|
||||||
} else if (startHasPassed && endHasPassed) {
|
|
||||||
i = 'tick-circle';
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Icon name={i} className="mr-2" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EpochOverview;
|
|
||||||
@@ -1,60 +1,39 @@
|
|||||||
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||||
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { useMemo, useState } from 'react';
|
|
||||||
import { ENV } from '../../config/env';
|
|
||||||
|
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
|
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL, setNodeSwitcherOpen } =
|
||||||
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
useEnvironment();
|
||||||
const { screenSize } = useScreenDimensions();
|
|
||||||
const showFullFeedbackLabel = useMemo(
|
|
||||||
() => ['lg', 'xl'].includes(screenSize),
|
|
||||||
[screenSize]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-neutral-700 dark:border-neutral-300">
|
||||||
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
|
<div className="flex justify-between gap-2 align-middle">
|
||||||
<div className="flex justify-between gap-2 align-middle">
|
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
||||||
{GIT_COMMIT_HASH && (
|
{GIT_COMMIT_HASH && (
|
||||||
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
<p data-testid="git-commit-hash">
|
||||||
<p data-testid="git-commit-hash">
|
{t('Version')}:{' '}
|
||||||
{t('Version')}:{' '}
|
<Link
|
||||||
<Link
|
href={
|
||||||
href={
|
GIT_ORIGIN_URL
|
||||||
GIT_ORIGIN_URL
|
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
|
||||||
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
|
: undefined
|
||||||
: undefined
|
}
|
||||||
}
|
target={GIT_ORIGIN_URL ? '_blank' : undefined}
|
||||||
target={GIT_ORIGIN_URL ? '_blank' : undefined}
|
>
|
||||||
>
|
{GIT_COMMIT_HASH}
|
||||||
{GIT_COMMIT_HASH}
|
</Link>
|
||||||
</Link>
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
|
||||||
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
|
|
||||||
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
|
|
||||||
{t('Change')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex pl-2 content-center">
|
|
||||||
<ExternalLink href={ENV.addresses.feedback}>
|
|
||||||
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
|
|
||||||
</ExternalLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
|
||||||
<NodeSwitcherDialog
|
<div className="flex pl-2 content-center">
|
||||||
open={nodeSwitcherOpen}
|
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
|
||||||
setOpen={setNodeSwitcherOpen}
|
<Link className="ml-2" onClick={setNodeSwitcherOpen}>
|
||||||
/>
|
{t('Change')}
|
||||||
</>
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,17 @@ jest.mock('../search', () => ({
|
|||||||
|
|
||||||
const renderComponent = () => (
|
const renderComponent = () => (
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Header />
|
<Header menuOpen={false} setMenuOpen={jest.fn()} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('Header', () => {
|
describe('Header', () => {
|
||||||
it('should render navigation', () => {
|
it('should render heading', () => {
|
||||||
render(renderComponent());
|
render(renderComponent());
|
||||||
|
|
||||||
expect(screen.getByTestId('navigation')).toHaveTextContent('Explorer');
|
expect(screen.getByTestId('explorer-header')).toHaveTextContent(
|
||||||
|
'Vega Explorer'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
it('should render search', () => {
|
it('should render search', () => {
|
||||||
render(renderComponent());
|
render(renderComponent());
|
||||||
|
|||||||
@@ -1,108 +1,47 @@
|
|||||||
import { matchPath, useLocation } from 'react-router-dom';
|
import classnames from 'classnames';
|
||||||
import {
|
import { Link } from 'react-router-dom';
|
||||||
ThemeSwitcher,
|
import { ThemeSwitcher, Icon } from '@vegaprotocol/ui-toolkit';
|
||||||
Navigation,
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
NavigationList,
|
|
||||||
NavigationItem,
|
|
||||||
NavigationLink,
|
|
||||||
NavigationBreakpoint,
|
|
||||||
NavigationTrigger,
|
|
||||||
NavigationContent,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { Routes } from '../../routes/route-names';
|
|
||||||
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
|
||||||
import type { Navigable } from '../../routes/router-config';
|
|
||||||
import routerConfig from '../../routes/router-config';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import { Search } from '../search';
|
import { Search } from '../search';
|
||||||
|
import { Routes } from '../../routes/route-names';
|
||||||
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
const routeToNavigationItem = (r: Navigable) => (
|
interface ThemeToggleProps {
|
||||||
<NavigationItem key={r.name}>
|
menuOpen: boolean;
|
||||||
<NavigationLink to={r.path}>{r.text}</NavigationLink>
|
setMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
</NavigationItem>
|
}
|
||||||
);
|
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = ({ menuOpen, setMenuOpen }: ThemeToggleProps) => {
|
||||||
const mainItems = compact(
|
const headerClasses = classnames(
|
||||||
[Routes.TX, Routes.BLOCKS, Routes.ORACLES, Routes.VALIDATORS].map((n) =>
|
'md:col-span-2',
|
||||||
routerConfig.find((r) => r.path === n)
|
'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center',
|
||||||
)
|
'p-4 gap-2 md:gap-4',
|
||||||
|
'border-b border-neutral-700 dark:border-neutral-300 bg-black',
|
||||||
|
'dark text-white'
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupedItems = compact(
|
|
||||||
[
|
|
||||||
Routes.PARTIES,
|
|
||||||
Routes.ASSETS,
|
|
||||||
Routes.MARKETS,
|
|
||||||
Routes.GOVERNANCE,
|
|
||||||
Routes.NETWORK_PARAMETERS,
|
|
||||||
Routes.GENESIS,
|
|
||||||
].map((n) => routerConfig.find((r) => r.path === n))
|
|
||||||
);
|
|
||||||
|
|
||||||
const { pathname } = useLocation();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Because the grouped items are displayed in a sub menu under an "Other" item
|
|
||||||
* we need to determine whether any underlying item is active to highlight the
|
|
||||||
* trigger in the same fashion as any other top-level `NavigationLink`.
|
|
||||||
* This function checks whether the current location pathname is one of the
|
|
||||||
* underlying NavigationLinks.
|
|
||||||
*/
|
|
||||||
const isOnOther = useMemo(() => {
|
|
||||||
for (const path of groupedItems.map((r) => r.path)) {
|
|
||||||
const matched = matchPath(`${path}/*`, pathname);
|
|
||||||
if (matched) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}, [groupedItems, pathname]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navigation
|
<header className={headerClasses}>
|
||||||
appName="Explorer"
|
<div className="flex h-full items-center sm:items-stretch gap-4">
|
||||||
theme="system"
|
<Link to={Routes.HOME}>
|
||||||
breakpoints={[490, 900]}
|
<h1
|
||||||
actions={
|
className="text-white text-3xl font-alpha uppercase calt mb-0"
|
||||||
<>
|
data-testid="explorer-header"
|
||||||
<ThemeSwitcher />
|
>
|
||||||
<Search />
|
{t('Vega Explorer')}
|
||||||
</>
|
</h1>
|
||||||
}
|
</Link>
|
||||||
onResize={(width, el) => {
|
<NetworkSwitcher />
|
||||||
if (width < 1157) {
|
</div>
|
||||||
// switch to magnifying glass trigger when width < 1157
|
<button
|
||||||
el.classList.remove('nav-search-full');
|
data-testid="open-menu"
|
||||||
el.classList.add('nav-search-compact');
|
className="md:hidden text-white"
|
||||||
} else {
|
onClick={() => setMenuOpen(!menuOpen)}
|
||||||
el.classList.remove('nav-search-compact');
|
|
||||||
el.classList.add('nav-search-full');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NavigationList hide={[NavigationBreakpoint.Small]}>
|
|
||||||
<NavigationItem>
|
|
||||||
<NetworkSwitcher />
|
|
||||||
</NavigationItem>
|
|
||||||
</NavigationList>
|
|
||||||
<NavigationList
|
|
||||||
hide={[NavigationBreakpoint.Small, NavigationBreakpoint.Narrow]}
|
|
||||||
>
|
>
|
||||||
{mainItems.map(routeToNavigationItem)}
|
<Icon name={menuOpen ? 'cross' : 'menu'} />
|
||||||
{groupedItems && (
|
</button>
|
||||||
<NavigationItem>
|
<Search />
|
||||||
<NavigationTrigger isActive={Boolean(isOnOther)}>
|
<ThemeSwitcher className="-my-4" />
|
||||||
{t('Other')}
|
</header>
|
||||||
</NavigationTrigger>
|
|
||||||
<NavigationContent>
|
|
||||||
<NavigationList>
|
|
||||||
{groupedItems.map(routeToNavigationItem)}
|
|
||||||
</NavigationList>
|
|
||||||
</NavigationContent>
|
|
||||||
</NavigationItem>
|
|
||||||
)}
|
|
||||||
</NavigationList>
|
|
||||||
</Navigation>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ export const InfoBlock = ({ title, subtitle, tooltipInfo }: InfoBlockProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col text-center ">
|
<div className="flex flex-col text-center ">
|
||||||
<h3 className="text-4xl">{title}</h3>
|
<h3 className="text-4xl">{title}</h3>
|
||||||
<p className="text-vega-dark-100 dark:text-vega-light-200">
|
<p className="text-zinc-800 dark:text-zinc-300">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
{tooltipInfo ? (
|
{tooltipInfo ? (
|
||||||
<Tooltip description={tooltipInfo} align="center">
|
<Tooltip description={tooltipInfo} align="center">
|
||||||
<span>
|
<span>
|
||||||
<Icon
|
<Icon
|
||||||
name="info-sign"
|
name="info-sign"
|
||||||
className="ml-2 text-vega-light-300 dark:text-vega-dark-300"
|
className="ml-2 text-zinc-400 dark:text-zinc-600"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ export const InfoPanel = ({
|
|||||||
text={id}
|
text={id}
|
||||||
startChars={visibleChars}
|
startChars={visibleChars}
|
||||||
endChars={visibleChars}
|
endChars={visibleChars}
|
||||||
className="text-vega-dark-100 dark:text-vega-light-200"
|
className="text-black dark:text-zinc-200"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p
|
<p
|
||||||
title={id}
|
title={id}
|
||||||
className="text-vega-dark-100 dark:text-vega-light-200 truncate ..."
|
className="text-black dark:text-zinc-200 truncate ..."
|
||||||
>
|
>
|
||||||
{id}
|
{id}
|
||||||
</p>
|
</p>
|
||||||
@@ -70,7 +70,7 @@ export const InfoPanel = ({
|
|||||||
</div>
|
</div>
|
||||||
{copy && (
|
{copy && (
|
||||||
<CopyWithTooltip text={id}>
|
<CopyWithTooltip text={id}>
|
||||||
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
|
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
|
||||||
<Icon name="duplicate" />
|
<Icon name="duplicate" />
|
||||||
</button>
|
</button>
|
||||||
</CopyWithTooltip>
|
</CopyWithTooltip>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Routes } from '../../routes/route-names';
|
import { Routes } from '../../routes/route-names';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { HTMLInputTypeAttribute, SyntheticEvent } from 'react';
|
import type { HTMLInputTypeAttribute, SyntheticEvent } from 'react';
|
||||||
import { Input, Button } from '@vegaprotocol/ui-toolkit';
|
import { Input, Button } from '@vegaprotocol/ui-toolkit';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
interface JumpToProps {
|
interface JumpToProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
query ExplorerAsset($id: ID!) {
|
||||||
|
asset(id: $id) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
status
|
||||||
|
decimals
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
export type ExplorerAssetQueryVariables = Types.Exact<{
|
||||||
|
id: Types.Scalars['ID'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type ExplorerAssetQuery = { __typename?: 'Query', asset?: { __typename?: 'Asset', id: string, name: string, status: Types.AssetStatus, decimals: number } | null };
|
||||||
|
|
||||||
|
|
||||||
|
export const ExplorerAssetDocument = gql`
|
||||||
|
query ExplorerAsset($id: ID!) {
|
||||||
|
asset(id: $id) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
status
|
||||||
|
decimals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useExplorerAssetQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useExplorerAssetQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useExplorerAssetQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useExplorerAssetQuery({
|
||||||
|
* variables: {
|
||||||
|
* id: // value for 'id'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useExplorerAssetQuery(baseOptions: Apollo.QueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
|
||||||
|
}
|
||||||
|
export function useExplorerAssetLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
|
||||||
|
}
|
||||||
|
export type ExplorerAssetQueryHookResult = ReturnType<typeof useExplorerAssetQuery>;
|
||||||
|
export type ExplorerAssetLazyQueryHookResult = ReturnType<typeof useExplorerAssetLazyQuery>;
|
||||||
|
export type ExplorerAssetQueryResult = Apollo.QueryResult<ExplorerAssetQuery, ExplorerAssetQueryVariables>;
|
||||||
@@ -1,37 +1,63 @@
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
import { render, waitFor } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { AssetLink } from './asset-link';
|
import AssetLink from './asset-link';
|
||||||
import { mockAssetA1 } from '../../../mocks/assets';
|
import { ExplorerAssetDocument } from './__generated__/Asset';
|
||||||
|
|
||||||
function renderComponent(id: string, mock: MockedResponse[]) {
|
function renderComponent(id: string, mock: MockedResponse[]) {
|
||||||
return (
|
return (
|
||||||
<MockedProvider mocks={mock} addTypename={false}>
|
<MockedProvider mocks={mock}>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<AssetLink assetId={id} />
|
<AssetLink id={id} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('AssetLink', () => {
|
describe('Asset link component', () => {
|
||||||
it('renders the asset id when not found and makes the button disabled', async () => {
|
it('Renders the ID at first', () => {
|
||||||
const res = render(renderComponent('123', []));
|
const res = render(renderComponent('123', []));
|
||||||
expect(res.getByText('123')).toBeInTheDocument();
|
expect(res.getByText('123')).toBeInTheDocument();
|
||||||
expect(await res.findByTestId('asset-link')).toBeDisabled();
|
|
||||||
await waitFor(async () => {
|
|
||||||
expect(await res.queryByText('A ONE')).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the asset name when found and make the button enabled', async () => {
|
it('Renders the asset name when the query returns a result', async () => {
|
||||||
const res = render(renderComponent('123', [mockAssetA1]));
|
const mock = {
|
||||||
expect(res.getByText('123')).toBeInTheDocument();
|
request: {
|
||||||
|
query: ExplorerAssetDocument,
|
||||||
|
variables: {
|
||||||
|
id: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
asset: {
|
||||||
|
id: '123',
|
||||||
|
name: 'test-label',
|
||||||
|
status: 'irrelevant-test-data',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
await waitFor(async () => {
|
const res = render(renderComponent('123', [mock]));
|
||||||
expect(await res.findByText('A ONE')).toBeInTheDocument();
|
expect(res.getByText('123')).toBeInTheDocument();
|
||||||
expect(await res.findByTestId('asset-link')).not.toBeDisabled();
|
expect(await res.findByText('test-label')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Leaves the asset id when the asset is not found', async () => {
|
||||||
|
const mock = {
|
||||||
|
request: {
|
||||||
|
query: ExplorerAssetDocument,
|
||||||
|
variables: {
|
||||||
|
id: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: new Error('No such asset'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = render(renderComponent('123', [mock]));
|
||||||
|
expect(await res.findByText('123')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,54 +1,35 @@
|
|||||||
import type { ComponentProps } from 'react';
|
import React from 'react';
|
||||||
import Hash from '../hash';
|
|
||||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import {
|
|
||||||
useAssetDataProvider,
|
|
||||||
useAssetDetailsDialogStore,
|
|
||||||
} from '@vegaprotocol/assets';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { Routes } from '../../../routes/route-names';
|
import { Routes } from '../../../routes/route-names';
|
||||||
|
import { useExplorerAssetQuery } from './__generated__/Asset';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
|
import type { ComponentProps } from 'react';
|
||||||
assetId: string;
|
|
||||||
asDialog?: boolean;
|
export type AssetLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
showAssetSymbol?: boolean;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an asset ID, it will fetch the asset name and show that,
|
* Given an asset ID, it will fetch the asset name and show that,
|
||||||
* with a link to the assets modal. If the name does not come back
|
* with a link to the assets list. If the name does not come back
|
||||||
* it will use the ID instead.
|
* it will use the ID instead.
|
||||||
*/
|
*/
|
||||||
export const AssetLink = ({
|
const AssetLink = ({ id, ...props }: AssetLinkProps) => {
|
||||||
assetId,
|
const { data } = useExplorerAssetQuery({
|
||||||
asDialog,
|
variables: { id },
|
||||||
showAssetSymbol = false,
|
});
|
||||||
...props
|
|
||||||
}: AssetLinkProps) => {
|
|
||||||
const { data: asset } = useAssetDataProvider(assetId);
|
|
||||||
|
|
||||||
const open = useAssetDetailsDialogStore((state) => state.open);
|
let label: string = id;
|
||||||
const navigate = useNavigate();
|
|
||||||
const label = asset
|
if (data?.asset?.name) {
|
||||||
? showAssetSymbol
|
label = data.asset.name;
|
||||||
? asset?.symbol
|
}
|
||||||
: asset?.name
|
|
||||||
: assetId;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonLink
|
<Link className="underline" {...props} to={`/${Routes.ASSETS}#${id}`}>
|
||||||
data-testid="asset-link"
|
{label}
|
||||||
disabled={!asset}
|
</Link>
|
||||||
onClick={(e) => {
|
|
||||||
if (asDialog) {
|
|
||||||
open(assetId, e.target as HTMLElement);
|
|
||||||
} else {
|
|
||||||
navigate(`${Routes.ASSETS}/${asset?.id}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Hash text={label} />
|
|
||||||
</ButtonLink>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default AssetLink;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Routes } from '../../../routes/route-names';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import Hash from '../hash';
|
|
||||||
|
|
||||||
export type BlockLinkProps = Partial<ComponentProps<typeof Link>> & {
|
export type BlockLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
height: string;
|
height: string;
|
||||||
@@ -12,7 +11,7 @@ export type BlockLinkProps = Partial<ComponentProps<typeof Link>> & {
|
|||||||
const BlockLink = ({ height, ...props }: BlockLinkProps) => {
|
const BlockLink = ({ height, ...props }: BlockLinkProps) => {
|
||||||
return (
|
return (
|
||||||
<Link className="underline" {...props} to={`/${Routes.BLOCKS}/${height}`}>
|
<Link className="underline" {...props} to={`/${Routes.BLOCKS}/${height}`}>
|
||||||
<Hash text={height} />
|
{height}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DATA_SOURCES } from '../../../config';
|
import { DATA_SOURCES } from '../../../config';
|
||||||
import Hash from '../hash';
|
|
||||||
|
|
||||||
export enum EthExplorerLinkTypes {
|
export enum EthExplorerLinkTypes {
|
||||||
block = 'block',
|
block = 'block',
|
||||||
@@ -28,7 +27,7 @@ export const EthExplorerLink = ({
|
|||||||
{...props}
|
{...props}
|
||||||
href={link}
|
href={link}
|
||||||
>
|
>
|
||||||
<Hash text={id} />
|
{id}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
export type HashProps = {
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple component that ensures long text things like hashes
|
|
||||||
* are broken when they need to wrap. This will remove the need
|
|
||||||
* for a lot of the overflow scrolling that currently exists.
|
|
||||||
*/
|
|
||||||
const Hash = ({ text }: HashProps) => {
|
|
||||||
return (
|
|
||||||
<code className="break-all font-mono" style={{ wordWrap: 'break-word' }}>
|
|
||||||
{text}
|
|
||||||
</code>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Hash;
|
|
||||||
@@ -2,4 +2,4 @@ export { default as BlockLink } from './block-link/block-link';
|
|||||||
export { default as PartyLink } from './party-link/party-link';
|
export { default as PartyLink } from './party-link/party-link';
|
||||||
export { default as NodeLink } from './node-link/node-link';
|
export { default as NodeLink } from './node-link/node-link';
|
||||||
export { default as MarketLink } from './market-link/market-link';
|
export { default as MarketLink } from './market-link/market-link';
|
||||||
export * from './asset-link/asset-link';
|
export { default as AssetLink } from './asset-link/asset-link';
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ query ExplorerMarket($id: ID!) {
|
|||||||
market(id: $id) {
|
market(id: $id) {
|
||||||
id
|
id
|
||||||
decimalPlaces
|
decimalPlaces
|
||||||
positionDecimalPlaces
|
|
||||||
tradableInstrument {
|
tradableInstrument {
|
||||||
instrument {
|
instrument {
|
||||||
name
|
name
|
||||||
|
|||||||
+1
-2
@@ -8,7 +8,7 @@ export type ExplorerMarketQueryVariables = Types.Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', decimals: number } } } } } | null };
|
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', decimals: number } } } } } | null };
|
||||||
|
|
||||||
|
|
||||||
export const ExplorerMarketDocument = gql`
|
export const ExplorerMarketDocument = gql`
|
||||||
@@ -16,7 +16,6 @@ export const ExplorerMarketDocument = gql`
|
|||||||
market(id: $id) {
|
market(id: $id) {
|
||||||
id
|
id
|
||||||
decimalPlaces
|
decimalPlaces
|
||||||
positionDecimalPlaces
|
|
||||||
tradableInstrument {
|
tradableInstrument {
|
||||||
instrument {
|
instrument {
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ describe('Market link component', () => {
|
|||||||
market: {
|
market: {
|
||||||
id: '123',
|
id: '123',
|
||||||
decimalPlaces: 5,
|
decimalPlaces: 5,
|
||||||
positionDecimalPlaces: 2,
|
|
||||||
state: 'irrelevant-test-data',
|
state: 'irrelevant-test-data',
|
||||||
tradableInstrument: {
|
tradableInstrument: {
|
||||||
instrument: {
|
instrument: {
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
import { Routes } from '../../../routes/route-names';
|
import { Routes } from '../../../routes/route-names';
|
||||||
import { useExplorerMarketQuery } from './__generated__/Market';
|
import { useExplorerMarketQuery } from './__generated__/Market';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import Hash from '../hash';
|
|
||||||
|
|
||||||
export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
|
export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
id: string;
|
id: string;
|
||||||
showMarketName?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,11 +15,7 @@ export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
|
|||||||
* with a link to the markets list. If the name does not come back
|
* with a link to the markets list. If the name does not come back
|
||||||
* it will use the ID instead
|
* it will use the ID instead
|
||||||
*/
|
*/
|
||||||
const MarketLink = ({
|
const MarketLink = ({ id, ...props }: MarketLinkProps) => {
|
||||||
id,
|
|
||||||
showMarketName = true,
|
|
||||||
...props
|
|
||||||
}: MarketLinkProps) => {
|
|
||||||
const { data, error, loading } = useExplorerMarketQuery({
|
const { data, error, loading } = useExplorerMarketQuery({
|
||||||
variables: { id },
|
variables: { id },
|
||||||
});
|
});
|
||||||
@@ -36,31 +31,17 @@ const MarketLink = ({
|
|||||||
<span role="img" aria-label="Unknown market" className="img">
|
<span role="img" aria-label="Unknown market" className="img">
|
||||||
⚠️ {t('Invalid market')}
|
⚠️ {t('Invalid market')}
|
||||||
</span>
|
</span>
|
||||||
|
{id}
|
||||||
<Hash text={id} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showMarketName) {
|
return (
|
||||||
return (
|
<Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
|
||||||
<Link
|
{label}
|
||||||
className="underline"
|
</Link>
|
||||||
{...props}
|
);
|
||||||
to={`/${Routes.MARKETS}/${id}`}
|
|
||||||
title={id}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Link className="underline" {...props} to={`/${Routes.MARKETS}/${id}`}>
|
|
||||||
<Hash text={id} />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MarketLink;
|
export default MarketLink;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useExplorerNodeQuery } from './__generated__/Node';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import Hash from '../hash';
|
|
||||||
|
|
||||||
export type NodeLinkProps = Partial<ComponentProps<typeof Link>> & {
|
export type NodeLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -23,7 +22,7 @@ const NodeLink = ({ id, ...props }: NodeLinkProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link className="underline" {...props} to={`/${Routes.VALIDATORS}#${id}`}>
|
<Link className="underline" {...props} to={`/${Routes.VALIDATORS}#${id}`}>
|
||||||
<Hash text={label} />
|
<code>{label}</code>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Routes } from '../../../routes/route-names';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
|
||||||
import Hash from '../hash';
|
|
||||||
|
|
||||||
export type OracleLinkProps = Partial<ComponentProps<typeof Link>> & {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const OracleLink = ({ id, ...props }: OracleLinkProps) => {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className="underline font-mono"
|
|
||||||
{...props}
|
|
||||||
to={`/${Routes.ORACLES}/${id}`}
|
|
||||||
>
|
|
||||||
<Hash text={id} />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OracleLink;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { render } from '@testing-library/react';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import PartyLink from './party-link';
|
|
||||||
|
|
||||||
describe('PartyLink', () => {
|
|
||||||
it('renders Network for 000.000 party', () => {
|
|
||||||
const zeroes =
|
|
||||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
|
||||||
const screen = render(<PartyLink id={zeroes} />);
|
|
||||||
expect(screen.getByText('Network')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders Network for network party', () => {
|
|
||||||
const screen = render(<PartyLink id="network" />);
|
|
||||||
expect(screen.getByText('Network')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders ID with no link for invalid party', () => {
|
|
||||||
const screen = render(<PartyLink id="this-party-is-not-valid" />);
|
|
||||||
expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('links a valid party to the party page', () => {
|
|
||||||
const aValidParty =
|
|
||||||
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
|
|
||||||
|
|
||||||
const screen = render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<PartyLink id={aValidParty} />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
const el = screen.getByText(aValidParty);
|
|
||||||
expect(el).toBeInTheDocument();
|
|
||||||
// The text should be a link that points to the party's page
|
|
||||||
expect(el.parentElement?.tagName).toEqual('A');
|
|
||||||
expect(el.parentElement?.getAttribute('href')).toContain(aValidParty);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -2,48 +2,19 @@ import { Routes } from '../../../routes/route-names';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import Hash from '../hash';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
|
|
||||||
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
export const SPECIAL_CASE_NETWORK_ID =
|
|
||||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
|
||||||
export const SPECIAL_CASE_NETWORK = 'network';
|
|
||||||
|
|
||||||
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
|
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
id: string;
|
id: string;
|
||||||
truncate?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
const PartyLink = ({ id, ...props }: PartyLinkProps) => {
|
||||||
// Some transactions will involve the 'network' party, which is alias for '000...000'
|
|
||||||
// The party page does not handle this nicely, so in this case we render the word 'Network'
|
|
||||||
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
|
|
||||||
return (
|
|
||||||
<span className="font-mono" data-testid="network">
|
|
||||||
{t('Network')}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the party doesn't look correct, there's no point in linking to id. Just render
|
|
||||||
// the ID as it was given to us
|
|
||||||
if (!isValidPartyId(id)) {
|
|
||||||
return (
|
|
||||||
<span className="font-mono" data-testid="invalid-party">
|
|
||||||
{id}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className="underline font-mono"
|
className="underline font-mono"
|
||||||
{...props}
|
{...props}
|
||||||
to={`/${Routes.PARTIES}/${id}`}
|
to={`/${Routes.PARTIES}/${id}`}
|
||||||
>
|
>
|
||||||
<Hash text={truncate ? truncateMiddle(id) : id} />
|
{id}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
query ExplorerProposal($id: ID!) {
|
|
||||||
proposal(id: $id) {
|
|
||||||
id
|
|
||||||
rationale {
|
|
||||||
title
|
|
||||||
description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
import * as Types from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
import { gql } from '@apollo/client';
|
|
||||||
import * as Apollo from '@apollo/client';
|
|
||||||
const defaultOptions = {} as const;
|
|
||||||
export type ExplorerProposalQueryVariables = Types.Exact<{
|
|
||||||
id: Types.Scalars['ID'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
|
|
||||||
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
|
|
||||||
|
|
||||||
|
|
||||||
export const ExplorerProposalDocument = gql`
|
|
||||||
query ExplorerProposal($id: ID!) {
|
|
||||||
proposal(id: $id) {
|
|
||||||
id
|
|
||||||
rationale {
|
|
||||||
title
|
|
||||||
description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* __useExplorerProposalQuery__
|
|
||||||
*
|
|
||||||
* To run a query within a React component, call `useExplorerProposalQuery` and pass it any options that fit your needs.
|
|
||||||
* When your component renders, `useExplorerProposalQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
||||||
* you can use to render your UI.
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { data, loading, error } = useExplorerProposalQuery({
|
|
||||||
* variables: {
|
|
||||||
* id: // value for 'id'
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useExplorerProposalQuery(baseOptions: Apollo.QueryHookOptions<ExplorerProposalQuery, ExplorerProposalQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useQuery<ExplorerProposalQuery, ExplorerProposalQueryVariables>(ExplorerProposalDocument, options);
|
|
||||||
}
|
|
||||||
export function useExplorerProposalLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerProposalQuery, ExplorerProposalQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useLazyQuery<ExplorerProposalQuery, ExplorerProposalQueryVariables>(ExplorerProposalDocument, options);
|
|
||||||
}
|
|
||||||
export type ExplorerProposalQueryHookResult = ReturnType<typeof useExplorerProposalQuery>;
|
|
||||||
export type ExplorerProposalLazyQueryHookResult = ReturnType<typeof useExplorerProposalLazyQuery>;
|
|
||||||
export type ExplorerProposalQueryResult = Apollo.QueryResult<ExplorerProposalQuery, ExplorerProposalQueryVariables>;
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import ProposalLink from './proposal-link';
|
|
||||||
import { ExplorerProposalDocument } from './__generated__/Proposal';
|
|
||||||
import { GraphQLError } from 'graphql';
|
|
||||||
|
|
||||||
function renderComponent(id: string, mocks: MockedResponse[]) {
|
|
||||||
return (
|
|
||||||
<MockedProvider mocks={mocks} addTypename={false}>
|
|
||||||
<MemoryRouter>
|
|
||||||
<ProposalLink id={id} />
|
|
||||||
</MemoryRouter>
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Proposal link component', () => {
|
|
||||||
it('Renders the ID at first', () => {
|
|
||||||
const res = render(renderComponent('123', []));
|
|
||||||
expect(res.getByText('123')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Renders the ID on error', async () => {
|
|
||||||
const mock = {
|
|
||||||
request: {
|
|
||||||
query: ExplorerProposalDocument,
|
|
||||||
variables: {
|
|
||||||
id: '456',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
errors: [new GraphQLError('No such proposal')],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = render(renderComponent('456', [mock]));
|
|
||||||
// The ID
|
|
||||||
expect(res.getByText('456')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Renders the proposal title when the query returns a result', async () => {
|
|
||||||
const mock = {
|
|
||||||
request: {
|
|
||||||
query: ExplorerProposalDocument,
|
|
||||||
variables: {
|
|
||||||
id: '123',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
proposal: {
|
|
||||||
id: '123',
|
|
||||||
rationale: {
|
|
||||||
title: 'test-title',
|
|
||||||
description: 'test description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = render(renderComponent('123', [mock]));
|
|
||||||
expect(res.getByText('123')).toBeInTheDocument();
|
|
||||||
expect(await res.findByText('test-title')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Leaves the proposal id when the market is not found', async () => {
|
|
||||||
const mock = {
|
|
||||||
request: {
|
|
||||||
query: ExplorerProposalDocument,
|
|
||||||
variables: {
|
|
||||||
id: '123',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
error: new Error('No such asset'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = render(renderComponent('123', [mock]));
|
|
||||||
expect(await res.findByText('123')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { useExplorerProposalQuery } from './__generated__/Proposal';
|
|
||||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { ENV } from '../../../config/env';
|
|
||||||
import Hash from '../hash';
|
|
||||||
export type ProposalLinkProps = {
|
|
||||||
id: string;
|
|
||||||
text?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a proposal ID, generates an external link over to
|
|
||||||
* the Governance page for more information
|
|
||||||
*/
|
|
||||||
const ProposalLink = ({ id, text }: ProposalLinkProps) => {
|
|
||||||
const { data } = useExplorerProposalQuery({
|
|
||||||
variables: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
const base = ENV.dataSources.governanceUrl;
|
|
||||||
const label = data?.proposal?.rationale.title || id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ExternalLink href={`${base}/proposals/${id}`}>
|
|
||||||
{text ? text : <Hash text={label} />}
|
|
||||||
</ExternalLink>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProposalLink;
|
|
||||||
@@ -2,7 +2,7 @@ import { AppRouter } from '../../routes';
|
|||||||
|
|
||||||
export const Main = () => {
|
export const Main = () => {
|
||||||
return (
|
return (
|
||||||
<main className="p-4">
|
<main className="p-4 overflow-scroll">
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
import {
|
|
||||||
addDecimalsFormatNumber,
|
|
||||||
formatNumberPercentage,
|
|
||||||
getMarketExpiryDateFormatted,
|
|
||||||
} from '@vegaprotocol/utils';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import type { MarketInfoWithData } from '@vegaprotocol/market-info';
|
|
||||||
import { MarketInfoTable } from '@vegaprotocol/market-info';
|
|
||||||
import {
|
|
||||||
MarketStateMapping,
|
|
||||||
MarketTradingModeMapping,
|
|
||||||
} from '@vegaprotocol/types';
|
|
||||||
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
|
||||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
|
||||||
const quoteUnit = market?.tradableInstrument.instrument.product.quoteName;
|
|
||||||
const assetId = useMemo(
|
|
||||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
|
||||||
[market]
|
|
||||||
);
|
|
||||||
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
|
||||||
|
|
||||||
if (!market) return null;
|
|
||||||
|
|
||||||
const keyDetails = {
|
|
||||||
decimalPlaces: market.decimalPlaces,
|
|
||||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
|
||||||
tradingMode: market.tradingMode,
|
|
||||||
state: MarketStateMapping[market.state],
|
|
||||||
};
|
|
||||||
const assetDecimals =
|
|
||||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
|
||||||
|
|
||||||
const liquidityPriceRange = formatNumberPercentage(
|
|
||||||
new BigNumber(market.lpPriceRange).times(100)
|
|
||||||
);
|
|
||||||
|
|
||||||
const panels = [
|
|
||||||
{
|
|
||||||
title: t('Key details'),
|
|
||||||
content: (
|
|
||||||
<MarketInfoTable
|
|
||||||
noBorder={false}
|
|
||||||
data={{
|
|
||||||
name: market.tradableInstrument.instrument.name,
|
|
||||||
marketID: market.id,
|
|
||||||
tradingMode:
|
|
||||||
keyDetails.tradingMode &&
|
|
||||||
MarketTradingModeMapping[keyDetails.tradingMode],
|
|
||||||
marketDecimalPlaces: market.decimalPlaces,
|
|
||||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
|
||||||
settlementAssetDecimalPlaces: assetDecimals,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('Instrument'),
|
|
||||||
content: (
|
|
||||||
<MarketInfoTable
|
|
||||||
noBorder={false}
|
|
||||||
data={{
|
|
||||||
marketName: market.tradableInstrument.instrument.name,
|
|
||||||
code: market.tradableInstrument.instrument.code,
|
|
||||||
productType:
|
|
||||||
market.tradableInstrument.instrument.product.__typename,
|
|
||||||
...market.tradableInstrument.instrument.product,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('Settlement asset'),
|
|
||||||
content: asset ? (
|
|
||||||
<AssetDetailsTable
|
|
||||||
asset={asset}
|
|
||||||
inline={true}
|
|
||||||
noBorder={false}
|
|
||||||
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
|
|
||||||
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Splash>{t('No data')}</Splash>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('Metadata'),
|
|
||||||
content: (
|
|
||||||
<MarketInfoTable
|
|
||||||
noBorder={false}
|
|
||||||
data={{
|
|
||||||
expiryDate: getMarketExpiryDateFormatted(
|
|
||||||
market.tradableInstrument.instrument.metadata.tags
|
|
||||||
),
|
|
||||||
...market.tradableInstrument.instrument.metadata.tags
|
|
||||||
?.map((tag) => {
|
|
||||||
const [key, value] = tag.split(':');
|
|
||||||
return { [key]: value };
|
|
||||||
})
|
|
||||||
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('Risk model'),
|
|
||||||
content: (
|
|
||||||
<MarketInfoTable
|
|
||||||
noBorder={false}
|
|
||||||
data={market.tradableInstrument.riskModel}
|
|
||||||
unformatted={true}
|
|
||||||
omits={[]}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('Risk parameters'),
|
|
||||||
content: (
|
|
||||||
<MarketInfoTable
|
|
||||||
noBorder={false}
|
|
||||||
data={market.tradableInstrument.riskModel.params}
|
|
||||||
unformatted={true}
|
|
||||||
omits={[]}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('Risk factors'),
|
|
||||||
content: (
|
|
||||||
<MarketInfoTable
|
|
||||||
noBorder={false}
|
|
||||||
data={market.riskFactors}
|
|
||||||
unformatted={true}
|
|
||||||
omits={['market', '__typename']}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
...(market.priceMonitoringSettings?.parameters?.triggers || []).map(
|
|
||||||
(trigger, i) => ({
|
|
||||||
title: t(`Price monitoring trigger ${i + 1}`),
|
|
||||||
content: <MarketInfoTable noBorder={false} data={trigger} />,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
...(market.data?.priceMonitoringBounds || []).map((trigger, i) => ({
|
|
||||||
title: t(`Price monitoring bound ${i + 1}`),
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<MarketInfoTable
|
|
||||||
noBorder={false}
|
|
||||||
data={trigger}
|
|
||||||
decimalPlaces={market.decimalPlaces}
|
|
||||||
omits={['referencePrice', '__typename']}
|
|
||||||
/>
|
|
||||||
<MarketInfoTable
|
|
||||||
noBorder={false}
|
|
||||||
data={{ referencePrice: trigger.referencePrice }}
|
|
||||||
decimalPlaces={assetDecimals}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
title: t('Liquidity monitoring parameters'),
|
|
||||||
content: (
|
|
||||||
<MarketInfoTable
|
|
||||||
noBorder={false}
|
|
||||||
data={{
|
|
||||||
triggeringRatio:
|
|
||||||
market.liquidityMonitoringParameters.triggeringRatio,
|
|
||||||
...market.liquidityMonitoringParameters.targetStakeParameters,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('Liquidity price range'),
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<p className="text-xs mb-4">
|
|
||||||
{`For liquidity orders to count towards a commitment, they must be
|
|
||||||
within the liquidity monitoring bounds.`}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs mb-4">
|
|
||||||
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
|
|
||||||
price.`}
|
|
||||||
</p>
|
|
||||||
<MarketInfoTable
|
|
||||||
noBorder={false}
|
|
||||||
data={{
|
|
||||||
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
|
|
||||||
lowestPrice:
|
|
||||||
market.data?.midPrice &&
|
|
||||||
`${addDecimalsFormatNumber(
|
|
||||||
new BigNumber(1)
|
|
||||||
.minus(market.lpPriceRange)
|
|
||||||
.times(market.data.midPrice)
|
|
||||||
.toString(),
|
|
||||||
market.decimalPlaces
|
|
||||||
)} ${quoteUnit}`,
|
|
||||||
highestPrice:
|
|
||||||
market.data?.midPrice &&
|
|
||||||
`${addDecimalsFormatNumber(
|
|
||||||
new BigNumber(1)
|
|
||||||
.plus(market.lpPriceRange)
|
|
||||||
.times(market.data.midPrice)
|
|
||||||
.toString(),
|
|
||||||
market.decimalPlaces
|
|
||||||
)} ${quoteUnit}`,
|
|
||||||
}}
|
|
||||||
></MarketInfoTable>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('Oracle'),
|
|
||||||
content: (
|
|
||||||
<MarketInfoTable
|
|
||||||
noBorder={false}
|
|
||||||
data={
|
|
||||||
market.tradableInstrument.instrument.product.dataSourceSpecBinding
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
className="text-xs hover:underline"
|
|
||||||
to={`/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData.id}`}
|
|
||||||
>
|
|
||||||
{t('View settlement data oracle specification')}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
className="text-xs hover:underline"
|
|
||||||
to={`/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForTradingTermination.id}`}
|
|
||||||
>
|
|
||||||
{t('View termination oracle specification')}
|
|
||||||
</Link>
|
|
||||||
</MarketInfoTable>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{panels.map((p) => (
|
|
||||||
<div className="mb-3">
|
|
||||||
<h2 className="font-alpha calt text-xl">{p.title}</h2>
|
|
||||||
{p.content}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import type { MarketFieldsFragment } from '@vegaprotocol/market-list';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
|
||||||
import { AgGridColumn } from 'ag-grid-react';
|
|
||||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
|
||||||
import type {
|
|
||||||
VegaICellRendererParams,
|
|
||||||
VegaValueGetterParams,
|
|
||||||
} from '@vegaprotocol/datagrid';
|
|
||||||
import { useRef, useLayoutEffect } from 'react';
|
|
||||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
|
||||||
import { MarketStateMapping } from '@vegaprotocol/types';
|
|
||||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
|
||||||
import type { RowClickedEvent } from 'ag-grid-community';
|
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
type MarketsTableProps = {
|
|
||||||
data: MarketFieldsFragment[] | null;
|
|
||||||
};
|
|
||||||
export const MarketsTable = ({ data }: MarketsTableProps) => {
|
|
||||||
const openAssetDetailsDialog = useAssetDetailsDialogStore(
|
|
||||||
(state) => state.open
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const gridRef = useRef<AgGridReact>(null);
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const showColumnsOnDesktop = () => {
|
|
||||||
gridRef.current?.columnApi.setColumnsVisible(
|
|
||||||
['id', 'state', 'asset'],
|
|
||||||
window.innerWidth > BREAKPOINT_MD
|
|
||||||
);
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', showColumnsOnDesktop);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', showColumnsOnDesktop);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AgGrid
|
|
||||||
ref={gridRef}
|
|
||||||
rowData={data}
|
|
||||||
getRowId={({ data }: { data: MarketFieldsFragment }) => data.id}
|
|
||||||
overlayNoRowsTemplate={t('This chain has no markets')}
|
|
||||||
domLayout="autoHeight"
|
|
||||||
defaultColDef={{
|
|
||||||
flex: 1,
|
|
||||||
resizable: true,
|
|
||||||
sortable: true,
|
|
||||||
filter: true,
|
|
||||||
filterParams: { buttons: ['reset'] },
|
|
||||||
autoHeight: true,
|
|
||||||
}}
|
|
||||||
suppressCellFocus={true}
|
|
||||||
onRowClicked={({ data, event }: RowClickedEvent) => {
|
|
||||||
if ((event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON') {
|
|
||||||
navigate(data.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="code"
|
|
||||||
headerName={t('Code')}
|
|
||||||
field="tradableInstrument.instrument.code"
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="name"
|
|
||||||
headerName={t('Name')}
|
|
||||||
field="tradableInstrument.instrument.name"
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
headerName={t('Status')}
|
|
||||||
field="state"
|
|
||||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
|
||||||
valueGetter={({
|
|
||||||
data,
|
|
||||||
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
|
|
||||||
return data?.state ? MarketStateMapping[data?.state] : '-';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="asset"
|
|
||||||
headerName={t('Settlement asset')}
|
|
||||||
field="tradableInstrument.instrument.product.settlementAsset"
|
|
||||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
|
||||||
cellRenderer={({
|
|
||||||
value,
|
|
||||||
}: VegaICellRendererParams<
|
|
||||||
MarketFieldsFragment,
|
|
||||||
'tradableInstrument.instrument.product.settlementAsset'
|
|
||||||
>) =>
|
|
||||||
value ? (
|
|
||||||
<ButtonLink
|
|
||||||
onClick={(e) => {
|
|
||||||
openAssetDetailsDialog(value.id, e.target as HTMLElement);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{value.symbol}
|
|
||||||
</ButtonLink>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
flex={2}
|
|
||||||
headerName={t('Market ID')}
|
|
||||||
field="id"
|
|
||||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="actions"
|
|
||||||
headerName=""
|
|
||||||
field="id"
|
|
||||||
cellRenderer={({
|
|
||||||
value,
|
|
||||||
}: VegaICellRendererParams<MarketFieldsFragment, 'id'>) =>
|
|
||||||
value ? (
|
|
||||||
<Link className="underline" to={value}>
|
|
||||||
{t('View details')}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</AgGrid>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import routerConfig from '../../routes/router-config';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
interface NavProps {
|
||||||
|
menuOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Nav = ({ menuOpen }: NavProps) => {
|
||||||
|
return (
|
||||||
|
<nav className="relative">
|
||||||
|
<div
|
||||||
|
className={classnames(
|
||||||
|
'absolute top-0 z-50 md:static',
|
||||||
|
'w-full p-4 md:border-r border-neutral-700 dark:border-neutral-300',
|
||||||
|
'bg-white dark:bg-black',
|
||||||
|
'transition-[right]',
|
||||||
|
{
|
||||||
|
'right-0 h-[100vh]': menuOpen,
|
||||||
|
'right-[200vw] h-full': !menuOpen,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{routerConfig.map((r) => (
|
||||||
|
<NavLink
|
||||||
|
key={r.name}
|
||||||
|
to={r.path}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
classnames(
|
||||||
|
'block mb-2 px-2',
|
||||||
|
'text-lg hover:bg-vega-pink dark:hover:bg-vega-yellow hover:text-white dark:hover:text-black',
|
||||||
|
{
|
||||||
|
'bg-vega-pink text-white dark:bg-vega-yellow dark:text-black':
|
||||||
|
isActive,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{r.text}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -83,7 +83,7 @@ const NestedDataListItem = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const titleClasses = classNames({
|
const titleClasses = classNames({
|
||||||
'text-xl pl-4 border-l-4 font-alpha calt': hasChildren,
|
'text-xl pl-4 border-l-4 font-alpha': hasChildren,
|
||||||
'text-base font-medium whitespace-nowrap': !hasChildren,
|
'text-base font-medium whitespace-nowrap': !hasChildren,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ const NestedDataListItem = ({
|
|||||||
)}
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
{!hasChildren && (
|
{!hasChildren && (
|
||||||
<code className="text-vega-light-100 mb-2 last:mb-0 dark:text-vega-dark-100 break-all">
|
<code className="text-vega-light-400 mb-2 last:mb-0 dark:text-vega-dark-400 break-all">
|
||||||
{JSON.stringify(value, null, ' ')}
|
{JSON.stringify(value, null, ' ')}
|
||||||
</code>
|
</code>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ fragment ExplorerDeterministicOrderFields on Order {
|
|||||||
market {
|
market {
|
||||||
id
|
id
|
||||||
decimalPlaces
|
decimalPlaces
|
||||||
positionDecimalPlaces
|
|
||||||
tradableInstrument {
|
tradableInstrument {
|
||||||
instrument {
|
instrument {
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as Types from '@vegaprotocol/types';
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
const defaultOptions = {} as const;
|
const defaultOptions = {} as const;
|
||||||
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
|
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
|
||||||
|
|
||||||
export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
|
export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
|
||||||
orderId: Types.Scalars['ID'];
|
orderId: Types.Scalars['ID'];
|
||||||
@@ -11,7 +11,7 @@ export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
|
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
|
||||||
|
|
||||||
export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
|
export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
|
||||||
fragment ExplorerDeterministicOrderFields on Order {
|
fragment ExplorerDeterministicOrderFields on Order {
|
||||||
@@ -35,7 +35,6 @@ export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
|
|||||||
market {
|
market {
|
||||||
id
|
id
|
||||||
decimalPlaces
|
decimalPlaces
|
||||||
positionDecimalPlaces
|
|
||||||
tradableInstrument {
|
tradableInstrument {
|
||||||
instrument {
|
instrument {
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
|
|||||||
__typename: 'Market',
|
__typename: 'Market',
|
||||||
id: '789',
|
id: '789',
|
||||||
state: 'STATUS_ACTIVE',
|
state: 'STATUS_ACTIVE',
|
||||||
positionDecimalPlaces: 2,
|
|
||||||
decimalPlaces: '5',
|
decimalPlaces: '5',
|
||||||
tradableInstrument: {
|
tradableInstrument: {
|
||||||
instrument: {
|
instrument: {
|
||||||
@@ -89,7 +88,6 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
|
|||||||
market: {
|
market: {
|
||||||
id: '789',
|
id: '789',
|
||||||
decimalPlaces: 5,
|
decimalPlaces: 5,
|
||||||
positionDecimalPlaces: 2,
|
|
||||||
state: 'irrelevant-test-data',
|
state: 'irrelevant-test-data',
|
||||||
tradableInstrument: {
|
tradableInstrument: {
|
||||||
instrument: {
|
instrument: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { useExplorerDeterministicOrderQuery } from './__generated__/Order';
|
import { useExplorerDeterministicOrderQuery } from './__generated__/Order';
|
||||||
import { MarketLink } from '../links';
|
import { MarketLink } from '../links';
|
||||||
import PriceInMarket from '../price-in-market/price-in-market';
|
import PriceInMarket from '../price-in-market/price-in-market';
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { useExplorerDeterministicOrderQuery } from './__generated__/Order';
|
import { useExplorerDeterministicOrderQuery } from './__generated__/Order';
|
||||||
import { MarketLink } from '../links';
|
import { MarketLink } from '../links';
|
||||||
import PriceInMarket from '../price-in-market/price-in-market';
|
import PriceInMarket from '../price-in-market/price-in-market';
|
||||||
import { Time } from '../time';
|
import { Time } from '../time';
|
||||||
import { sideText, statusText, tifFull, tifShort } from './lib/order-labels';
|
import { sideText, statusText, tifFull, tifShort } from './lib/order-labels';
|
||||||
import SizeInMarket from '../size-in-market/size-in-market';
|
|
||||||
|
|
||||||
export interface DeterministicOrderDetailsProps {
|
export interface DeterministicOrderDetailsProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,7 +12,7 @@ export interface DeterministicOrderDetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const wrapperClasses =
|
export const wrapperClasses =
|
||||||
'grid lg:grid-cols-1 flex items-center max-w-xl border border-vega-light-200 dark:border-vega-dark-150 rounded-md pv-2 ph-5 mb-5';
|
'grid lg:grid-cols-1 flex items-center max-w-xl border border-zinc-200 dark:border-zinc-800 rounded-md pv-2 ph-5 mb-5';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the *current* details for an order
|
* This component renders the *current* details for an order
|
||||||
@@ -42,7 +41,7 @@ const DeterministicOrderDetails = ({
|
|||||||
<h2 className="text-3xl font-bold mb-4 display-5">
|
<h2 className="text-3xl font-bold mb-4 display-5">
|
||||||
{t('Order not found')}
|
{t('Order not found')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-vega-light-400 mb-12">
|
<p className="text-gray-500 mb-12">
|
||||||
{t('No order created from this transaction')}
|
{t('No order created from this transaction')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +90,7 @@ const DeterministicOrderDetails = ({
|
|||||||
<div className="mb-12 md:mb-0">
|
<div className="mb-12 md:mb-0">
|
||||||
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
|
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
|
||||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||||
<SizeInMarket size={o.size} marketId={o.market.id} />
|
{o.size}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -101,7 +100,7 @@ const DeterministicOrderDetails = ({
|
|||||||
{t('Remaining')}
|
{t('Remaining')}
|
||||||
</h2>
|
</h2>
|
||||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||||
<SizeInMarket size={o.remaining} marketId={o.market.id} />
|
{o.remaining}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import type * as Schema from '@vegaprotocol/types';
|
import type * as Schema from '@vegaprotocol/types';
|
||||||
import type { components } from '../../../../types/explorer';
|
import type { components } from '../../../../types/explorer';
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
|
|
||||||
interface CancelSummaryProps {
|
|
||||||
orderId?: string;
|
|
||||||
marketId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple component for rendering a reasonable string from an order cancellation
|
|
||||||
*/
|
|
||||||
export const CancelSummary = ({ orderId, marketId }: CancelSummaryProps) => {
|
|
||||||
return <span className="font-bold">{getLabel(orderId, marketId)}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getLabel(
|
|
||||||
orderId: string | undefined,
|
|
||||||
marketId: string | undefined
|
|
||||||
): string {
|
|
||||||
if (!orderId && !marketId) {
|
|
||||||
return t('All orders');
|
|
||||||
} else if (marketId && !orderId) {
|
|
||||||
return t('All in market');
|
|
||||||
}
|
|
||||||
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,6 @@ const mock = {
|
|||||||
__typename: 'Market',
|
__typename: 'Market',
|
||||||
id: '789',
|
id: '789',
|
||||||
state: 'STATE_ACTIVE',
|
state: 'STATE_ACTIVE',
|
||||||
positionDecimalPlaces: 2,
|
|
||||||
decimalPlaces: 2,
|
decimalPlaces: 2,
|
||||||
tradableInstrument: {
|
tradableInstrument: {
|
||||||
instrument: {
|
instrument: {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useExplorerDeterministicOrderQuery } from '../order-details/__generated__/Order';
|
import { useExplorerDeterministicOrderQuery } from '../order-details/__generated__/Order';
|
||||||
import PriceInMarket from '../price-in-market/price-in-market';
|
import PriceInMarket from '../price-in-market/price-in-market';
|
||||||
import { sideText } from '../order-details/lib/order-labels';
|
import { sideText } from '../order-details/lib/order-labels';
|
||||||
import SizeInMarket from '../size-in-market/size-in-market';
|
|
||||||
|
|
||||||
// Note: Edited has no style currently
|
// Note: Edited has no style currently
|
||||||
export type OrderSummaryModifier = 'cancelled' | 'edited';
|
export type OrderSummaryModifier = 'cancelled' | 'edited';
|
||||||
@@ -42,8 +41,7 @@ const OrderSummary = ({ id, modifier }: OrderSummaryProps) => {
|
|||||||
return (
|
return (
|
||||||
<div data-testid="order-summary" className={getClassName(modifier)}>
|
<div data-testid="order-summary" className={getClassName(modifier)}>
|
||||||
<span>{sideText[order.side]}</span>
|
<span>{sideText[order.side]}</span>
|
||||||
<SizeInMarket marketId={order.market.id} size={order.size} />
|
<span>{order.size}</span> <i>@</i>
|
||||||
<i>@</i>
|
|
||||||
<PriceInMarket marketId={order.market.id} price={order.price} />
|
<PriceInMarket marketId={order.market.id} price={order.price} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ describe('Order TX Summary component', () => {
|
|||||||
market: {
|
market: {
|
||||||
id: '123',
|
id: '123',
|
||||||
decimalPlaces: 2,
|
decimalPlaces: 2,
|
||||||
positionDecimalPlaces: 2,
|
|
||||||
state: 'irrelevant-test-data',
|
state: 'irrelevant-test-data',
|
||||||
tradableInstrument: {
|
tradableInstrument: {
|
||||||
instrument: {
|
instrument: {
|
||||||
@@ -112,14 +111,13 @@ describe('Order TX Summary component', () => {
|
|||||||
const res = renderComponent(o, [mock]);
|
const res = renderComponent(o, [mock]);
|
||||||
expect(res.queryByTestId('order-summary')).toBeInTheDocument();
|
expect(res.queryByTestId('order-summary')).toBeInTheDocument();
|
||||||
expect(res.getByText('Buy')).toBeInTheDocument();
|
expect(res.getByText('Buy')).toBeInTheDocument();
|
||||||
|
|
||||||
// Initially renders price and size unformatted
|
|
||||||
expect(res.getByText('333')).toBeInTheDocument();
|
|
||||||
expect(res.getByText('10')).toBeInTheDocument();
|
expect(res.getByText('10')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Initially renders price alone
|
||||||
|
expect(res.getByText('333')).toBeInTheDocument();
|
||||||
|
|
||||||
// After fetch renders formatted price and asset quotename
|
// After fetch renders formatted price and asset quotename
|
||||||
expect(await res.findByText('3.33')).toBeInTheDocument();
|
expect(await res.findByText('3.33')).toBeInTheDocument();
|
||||||
expect(await res.findByText('TEST')).toBeInTheDocument();
|
expect(await res.findByText('TEST')).toBeInTheDocument();
|
||||||
expect(await res.getByText('0.10')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { components } from '../../../types/explorer';
|
|||||||
|
|
||||||
import PriceInMarket from '../price-in-market/price-in-market';
|
import PriceInMarket from '../price-in-market/price-in-market';
|
||||||
import { sideText } from '../order-details/lib/order-labels';
|
import { sideText } from '../order-details/lib/order-labels';
|
||||||
import SizeInMarket from '../size-in-market/size-in-market';
|
|
||||||
|
|
||||||
export type OrderSummaryProps = {
|
export type OrderSummaryProps = {
|
||||||
order: components['schemas']['v1OrderSubmission'];
|
order: components['schemas']['v1OrderSubmission'];
|
||||||
@@ -30,12 +29,7 @@ const OrderTxSummary = ({ order }: OrderSummaryProps) => {
|
|||||||
return (
|
return (
|
||||||
<div data-testid="order-summary">
|
<div data-testid="order-summary">
|
||||||
<span>{sideText[order.side]}</span>
|
<span>{sideText[order.side]}</span>
|
||||||
{order.size ? (
|
<span>{order.size}</span> <i className="text-xs">@</i>
|
||||||
<SizeInMarket size={order.size} marketId={order.marketId} />
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)}
|
|
||||||
<i className="text-xs">@</i>
|
|
||||||
<PriceInMarket
|
<PriceInMarket
|
||||||
marketId={order.marketId}
|
marketId={order.marketId}
|
||||||
price={order.price}
|
price={order.price}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const PageHeader = ({
|
|||||||
copy = false,
|
copy = false,
|
||||||
className,
|
className,
|
||||||
}: PageHeaderProps) => {
|
}: PageHeaderProps) => {
|
||||||
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha calt';
|
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha';
|
||||||
return (
|
return (
|
||||||
<header className={className}>
|
<header className={className}>
|
||||||
<span className={`${titleClasses} block`}>{prefix}</span>
|
<span className={`${titleClasses} block`}>{prefix}</span>
|
||||||
@@ -38,7 +38,7 @@ export const PageHeader = ({
|
|||||||
</h2>
|
</h2>
|
||||||
{copy && (
|
{copy && (
|
||||||
<CopyWithTooltip data-testid="copy-to-clipboard" text={title}>
|
<CopyWithTooltip data-testid="copy-to-clipboard" text={title}>
|
||||||
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
|
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
|
||||||
<Icon name="duplicate" className="" />
|
<Icon name="duplicate" className="" />
|
||||||
</button>
|
</button>
|
||||||
</CopyWithTooltip>
|
</CopyWithTooltip>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './page-actions';
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
|
||||||
|
|
||||||
export const PageActions = ({
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div className="flex flex-row items-start gap-1" {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,7 @@ interface PanelProps {
|
|||||||
export const Panel = ({ children, className }: PanelProps) => (
|
export const Panel = ({ children, className }: PanelProps) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'border border-vega-light-150 dark:border-vega-dark-150 rounded-md p-5 mb-5',
|
'border border-zinc-200 dark:border-zinc-800 rounded-md p-5 mb-5',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ const fullMock = {
|
|||||||
market: {
|
market: {
|
||||||
id: '123',
|
id: '123',
|
||||||
decimalPlaces: 2,
|
decimalPlaces: 2,
|
||||||
positionDecimalPlaces: 2,
|
|
||||||
state: 'irrelevant-test-data',
|
state: 'irrelevant-test-data',
|
||||||
tradableInstrument: {
|
tradableInstrument: {
|
||||||
instrument: {
|
instrument: {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import isUndefined from 'lodash/isUndefined';
|
import isUndefined from 'lodash/isUndefined';
|
||||||
import { useExplorerMarketQuery } from '../links/market-link/__generated__/Market';
|
import { useExplorerMarketQuery } from '../links/market-link/__generated__/Market';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals';
|
|
||||||
import { VoteProgress } from '@vegaprotocol/proposals';
|
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
|
||||||
import { AgGridColumn } from 'ag-grid-react';
|
|
||||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
|
||||||
import type {
|
|
||||||
VegaICellRendererParams,
|
|
||||||
VegaValueFormatterParams,
|
|
||||||
} from '@vegaprotocol/datagrid';
|
|
||||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import type { RowClickedEvent } from 'ag-grid-community';
|
|
||||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
|
|
||||||
import { ProposalStateMapping } from '@vegaprotocol/types';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
|
||||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
|
||||||
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
|
|
||||||
|
|
||||||
type ProposalTermsDialog = {
|
|
||||||
open: boolean;
|
|
||||||
title: string;
|
|
||||||
content: unknown;
|
|
||||||
};
|
|
||||||
type ProposalsTableProps = {
|
|
||||||
data: ProposalListFieldsFragment[] | null;
|
|
||||||
};
|
|
||||||
export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
|
||||||
const { params } = useNetworkParams([
|
|
||||||
NetworkParams.governance_proposal_market_requiredMajority,
|
|
||||||
]);
|
|
||||||
const tokenLink = useLinks(DApp.Token);
|
|
||||||
const requiredMajorityPercentage = useMemo(() => {
|
|
||||||
const requiredMajority =
|
|
||||||
params?.governance_proposal_market_requiredMajority ?? 1;
|
|
||||||
return new BigNumber(requiredMajority).times(100);
|
|
||||||
}, [params?.governance_proposal_market_requiredMajority]);
|
|
||||||
|
|
||||||
const gridRef = useRef<AgGridReact>(null);
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const showColumnsOnDesktop = () => {
|
|
||||||
gridRef.current?.columnApi.setColumnsVisible(
|
|
||||||
['voting', 'cDate', 'eDate', 'type'],
|
|
||||||
window.innerWidth > BREAKPOINT_MD
|
|
||||||
);
|
|
||||||
gridRef.current?.columnApi.setColumnWidth(
|
|
||||||
'actions',
|
|
||||||
window.innerWidth > BREAKPOINT_MD ? 221 : 80
|
|
||||||
);
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', showColumnsOnDesktop);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', showColumnsOnDesktop);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [dialog, setDialog] = useState<ProposalTermsDialog>({
|
|
||||||
open: false,
|
|
||||||
title: '',
|
|
||||||
content: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AgGrid
|
|
||||||
ref={gridRef}
|
|
||||||
rowData={data}
|
|
||||||
getRowId={({ data }: { data: ProposalListFieldsFragment }) =>
|
|
||||||
data.id || data.rationale.title
|
|
||||||
}
|
|
||||||
overlayNoRowsTemplate={t('This chain has no markets')}
|
|
||||||
domLayout="autoHeight"
|
|
||||||
defaultColDef={{
|
|
||||||
flex: 1,
|
|
||||||
resizable: true,
|
|
||||||
sortable: true,
|
|
||||||
filter: true,
|
|
||||||
filterParams: { buttons: ['reset'] },
|
|
||||||
autoHeight: true,
|
|
||||||
}}
|
|
||||||
suppressCellFocus={true}
|
|
||||||
onRowClicked={({ data, event }: RowClickedEvent) => {
|
|
||||||
if (
|
|
||||||
(event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON'
|
|
||||||
) {
|
|
||||||
const proposalPage = tokenLink(
|
|
||||||
TOKEN_PROPOSAL.replace(':id', data.id)
|
|
||||||
);
|
|
||||||
window.open(proposalPage, '_blank');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="title"
|
|
||||||
headerName={t('Title')}
|
|
||||||
field="rationale.title"
|
|
||||||
flex={2}
|
|
||||||
wrapText={true}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="type"
|
|
||||||
maxWidth={180}
|
|
||||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
|
||||||
headerName={t('Type')}
|
|
||||||
field="terms.change.__typename"
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
maxWidth={100}
|
|
||||||
headerName={t('State')}
|
|
||||||
field="state"
|
|
||||||
valueFormatter={({
|
|
||||||
value,
|
|
||||||
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) => {
|
|
||||||
return value ? ProposalStateMapping[value] : '-';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="voting"
|
|
||||||
maxWidth={100}
|
|
||||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
|
||||||
headerName={t('Voting')}
|
|
||||||
cellRenderer={({
|
|
||||||
data,
|
|
||||||
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
|
||||||
if (data) {
|
|
||||||
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
|
|
||||||
const noTokens = new BigNumber(data.votes.no.totalTokens);
|
|
||||||
const totalTokensVoted = yesTokens.plus(noTokens);
|
|
||||||
const yesPercentage = totalTokensVoted.isZero()
|
|
||||||
? new BigNumber(0)
|
|
||||||
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
|
||||||
return (
|
|
||||||
<div className="uppercase flex h-full items-center justify-center pt-2">
|
|
||||||
<VoteProgress
|
|
||||||
threshold={requiredMajorityPercentage}
|
|
||||||
progress={yesPercentage}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return '-';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="cDate"
|
|
||||||
maxWidth={150}
|
|
||||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
|
||||||
headerName={t('Closing date')}
|
|
||||||
field="terms.closingDatetime"
|
|
||||||
valueFormatter={({
|
|
||||||
value,
|
|
||||||
}: VegaValueFormatterParams<
|
|
||||||
ProposalListFieldsFragment,
|
|
||||||
'terms.closingDatetime'
|
|
||||||
>) => {
|
|
||||||
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="eDate"
|
|
||||||
maxWidth={150}
|
|
||||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
|
||||||
headerName={t('Enactment date')}
|
|
||||||
field="terms.enactmentDatetime"
|
|
||||||
valueFormatter={({
|
|
||||||
value,
|
|
||||||
}: VegaValueFormatterParams<
|
|
||||||
ProposalListFieldsFragment,
|
|
||||||
'terms.enactmentDatetime'
|
|
||||||
>) => {
|
|
||||||
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
colId="actions"
|
|
||||||
minWidth={window.innerWidth > BREAKPOINT_MD ? 221 : 80}
|
|
||||||
maxWidth={221}
|
|
||||||
sortable={false}
|
|
||||||
filter={false}
|
|
||||||
resizable={false}
|
|
||||||
cellRenderer={({
|
|
||||||
data,
|
|
||||||
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
|
||||||
const proposalPage = tokenLink(
|
|
||||||
TOKEN_PROPOSAL.replace(':id', data?.id || '')
|
|
||||||
);
|
|
||||||
const openDialog = () => {
|
|
||||||
if (!data) return;
|
|
||||||
setDialog({
|
|
||||||
open: true,
|
|
||||||
title: data.rationale.title,
|
|
||||||
content: data.terms,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="pb-1">
|
|
||||||
<button
|
|
||||||
className="underline max-md:hidden"
|
|
||||||
onClick={openDialog}
|
|
||||||
>
|
|
||||||
{t('View terms')}
|
|
||||||
</button>{' '}
|
|
||||||
<ExternalLink className="max-md:hidden" href={proposalPage}>
|
|
||||||
{t('Open in Governance')}
|
|
||||||
</ExternalLink>
|
|
||||||
<ExternalLink className="md:hidden" href={proposalPage}>
|
|
||||||
{t('Open')}
|
|
||||||
</ExternalLink>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</AgGrid>
|
|
||||||
<JsonViewerDialog
|
|
||||||
open={dialog.open}
|
|
||||||
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
|
|
||||||
title={dialog.title}
|
|
||||||
content={dialog.content}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user