Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac30cdf41d | ||
|
|
8a0c15ac11 | ||
|
|
a4f512f946 | ||
|
|
88f3c278f8 | ||
|
|
34d5f3fd13 | ||
|
|
a060f94146 | ||
|
|
f649d78565 | ||
|
|
b611d30b31 | ||
|
|
545324ccd6 | ||
|
|
8c16759621 | ||
|
|
e724aac2f1 | ||
|
|
e3a6b75130 | ||
|
|
8d8f6b4928 | ||
|
|
6c1c5bf2a9 | ||
|
|
74a3aa8566 | ||
|
|
4cbd7a4928 | ||
|
|
201cf07a01 | ||
|
|
90ceeebf15 | ||
|
|
965f7f6e2d | ||
|
|
18633efec3 | ||
|
|
a0b758334a | ||
|
|
9f3026aa4b | ||
|
|
faa359e2fb | ||
|
|
1d178b940f | ||
|
|
b2dd053621 | ||
|
|
77f5193b7a | ||
|
|
055a0070db | ||
|
|
29b221c86a | ||
|
|
3e619acd4b | ||
|
|
c720719ac5 | ||
|
|
c676a7c1e2 | ||
|
|
ca63a671c8 | ||
|
|
4165f3905f | ||
|
|
a359ccb755 | ||
|
|
e584737005 | ||
|
|
b12c5ed39f | ||
|
|
d3dbd64cf1 | ||
|
|
2df62bca61 | ||
|
|
fd2a434b46 | ||
|
|
4e7a78f988 | ||
|
|
327619652a | ||
|
|
d2339205cc | ||
|
|
63df4cf465 | ||
|
|
5e34178291 | ||
|
|
0c44664aa5 | ||
|
|
7ef1d75529 | ||
|
|
84795b2d6a | ||
|
|
300019f108 | ||
|
|
2d0aebd3fe | ||
|
|
9e86d2aff5 | ||
|
|
0f7e76ae01 | ||
|
|
56732d1503 | ||
|
|
5c19263653 | ||
|
|
0d911b3029 | ||
|
|
9f059e7862 | ||
|
|
92ea93dadb | ||
|
|
6165aefad7 | ||
|
|
c95369d914 | ||
|
|
5dbc5d7997 | ||
|
|
47b3d5b55f | ||
|
|
da7c0b84f7 | ||
|
|
67e602ebb1 | ||
|
|
f407110e95 | ||
|
|
66a85cce2a | ||
|
|
93f8450ecb | ||
|
|
0447a6cf5d | ||
|
|
498be838c7 | ||
|
|
fc3b1bd9ab | ||
|
|
090d850647 | ||
|
|
95280f5b26 | ||
|
|
101ae00a57 | ||
|
|
613cce3683 | ||
|
|
41fa18ed45 | ||
|
|
c941893cdd | ||
|
|
d049b471d8 | ||
|
|
010ffc0f46 | ||
|
|
dd56e4083b | ||
|
|
09d1ba68ba | ||
|
|
3959daa2b7 | ||
|
|
c599b02c89 | ||
|
|
e5ebaf02d2 | ||
|
|
ae54616c1f | ||
|
|
8f3cf390c5 | ||
|
|
64cf0c90a7 | ||
|
|
3edd91eadb | ||
|
|
676aeb07a4 | ||
|
|
52ee998bf0 | ||
|
|
727e2cd2ff | ||
|
|
06b2dd18f9 | ||
|
|
a189f38cdc | ||
|
|
d18681a5f0 | ||
|
|
9e2d03853a | ||
|
|
18c36d5e6b | ||
|
|
dcfb8f9527 | ||
|
|
a64acecf4b | ||
|
|
0998952196 | ||
|
|
2fd4d379cf | ||
|
|
c99d055b24 | ||
|
|
f1fa13e0bd | ||
|
|
ae020a8dca | ||
|
|
fa871a49c7 | ||
|
|
4205dd471a | ||
|
|
1040077c2b | ||
|
|
9d346d7846 | ||
|
|
b68136ba3f | ||
|
|
f5ad2de5a5 | ||
|
|
b3bfb90804 | ||
|
|
327ebf3496 | ||
|
|
1098accb84 | ||
|
|
52183555a9 | ||
|
|
51187029aa | ||
|
|
f7f7be59d3 | ||
|
|
49e3baf094 | ||
|
|
a575b4c502 | ||
|
|
af2e52d59c | ||
|
|
2725bef159 | ||
|
|
070f1905e2 | ||
|
|
6705eb4398 | ||
|
|
9d3fc04597 | ||
|
|
d3929b8d4a | ||
|
|
8a5579b1cc | ||
|
|
197f2e8097 | ||
|
|
8f5a2276de | ||
|
|
46513685c8 | ||
|
|
e66e96f12d | ||
|
|
31c8365812 | ||
|
|
460f534c7a | ||
|
|
24cd080dd4 | ||
|
|
f10c33748f | ||
|
|
ac163d0194 | ||
|
|
e7616e64f8 | ||
|
|
adca4600c2 | ||
|
|
a8eef1cb53 | ||
|
|
58d4bd1459 | ||
|
|
26020b5f71 | ||
|
|
16d345eb4a | ||
|
|
3b3fcab4f4 | ||
|
|
f54a629179 | ||
|
|
56f48671cc | ||
|
|
a65ee358ff | ||
|
|
7b8d814724 | ||
|
|
cd237dc491 | ||
|
|
9ad00adc5a | ||
|
|
fab055bb34 | ||
|
|
d66956d080 | ||
|
|
9848c8ffff | ||
|
|
b0e8ed62d2 | ||
|
|
289ff3b5b3 | ||
|
|
1d965187dc | ||
|
|
8bf3615001 | ||
|
|
1b77800b62 | ||
|
|
979dc2904b | ||
|
|
0fec3bcf3c | ||
|
|
4542e25ec9 | ||
|
|
acd96fbf21 | ||
|
|
96bf12d4dd | ||
|
|
47b81188d4 | ||
|
|
efada06256 | ||
|
|
311ef41138 | ||
|
|
5dd0cafeca | ||
|
|
f3d6aa1129 | ||
|
|
93dbc32f22 | ||
|
|
fd1827ef67 | ||
|
|
9c9948e4c1 | ||
|
|
79b0868ff7 | ||
|
|
a8401d4c6f | ||
|
|
c030264211 | ||
|
|
068f7b3fc3 | ||
|
|
4430f0c5f6 | ||
|
|
0a14000708 | ||
|
|
9ca6fe3cbf | ||
|
|
4f96472375 | ||
|
|
cddf2ba00b | ||
|
|
d01d6f5815 | ||
|
|
2998e165f6 | ||
|
|
886b0cb6d3 | ||
|
|
6799a06acd | ||
|
|
47a6bcff78 | ||
|
|
06dcd00b53 | ||
|
|
fd7aa5bd78 | ||
|
|
384cfbc70b | ||
|
|
9d14f603b3 | ||
|
|
8fab8dad6c | ||
|
|
76bf45b461 | ||
|
|
f128f41ea0 | ||
|
|
fcb992b64e | ||
|
|
1251c8b89b | ||
|
|
a796a38200 | ||
|
|
cf83fe9469 | ||
|
|
43c48741e2 | ||
|
|
cc6ea6176a | ||
|
|
6b280d8a70 | ||
|
|
891b441427 | ||
|
|
86739dfaad | ||
|
|
d87adfd7d5 | ||
|
|
7364abbbf2 | ||
|
|
1bd56c9bdb | ||
|
|
a985bc7317 | ||
|
|
1cab9abfd5 | ||
|
|
26a82ab2ab | ||
|
|
69a0830e80 | ||
|
|
4eb6e09bd4 | ||
|
|
a02100b079 | ||
|
|
4bb78e9891 | ||
|
|
79f360dc24 | ||
|
|
a3fcd6b7dc | ||
|
|
23ce480daa | ||
|
|
da209d96b0 | ||
|
|
9166a85a55 | ||
|
|
a5ec1f708f | ||
|
|
3afdc0c8da | ||
|
|
757d545d97 | ||
|
|
fc4d40f3cf | ||
|
|
ff47eda692 | ||
|
|
7483d9ce1d | ||
|
|
f9209cf327 | ||
|
|
420a29680d | ||
|
|
19c9e79647 | ||
|
|
4cd856fcb8 | ||
|
|
649cc61280 | ||
|
|
35f47b75ac | ||
|
|
0697acdac9 | ||
|
|
92ec7166bc | ||
|
|
56b5214dbf | ||
|
|
94509a29c5 | ||
|
|
4e2b6b2d04 | ||
|
|
c29087cc96 | ||
|
|
5bde096977 | ||
|
|
923dc09eb6 | ||
|
|
4fbdb337dd | ||
|
|
6f4a5b9097 | ||
|
|
0c26d99ce2 | ||
|
|
5aedeba4ff | ||
|
|
5789d3496b | ||
|
|
d95fdca0d4 | ||
|
|
e4bf61c2e2 | ||
|
|
0ba54cd8a4 | ||
|
|
eee85c231b | ||
|
|
afdb387742 | ||
|
|
63698fbb80 | ||
|
|
58d8f7857e | ||
|
|
0931730582 | ||
|
|
3aefa10a4f | ||
|
|
efeccc7972 | ||
|
|
95f4e489b2 | ||
|
|
0033f3c5f5 | ||
|
|
abf69786ba | ||
|
|
0a5fb9d917 | ||
|
|
8e82bf43fb | ||
|
|
4bb57e9c47 | ||
|
|
ce6d4cb35d | ||
|
|
647f04656f | ||
|
|
67186bf4c0 | ||
|
|
96007bd230 | ||
|
|
554d9ecb06 | ||
|
|
a12a5b3a15 | ||
|
|
a79e2f85bc | ||
|
|
a2190fdde0 | ||
|
|
849415a78e | ||
|
|
d33f5428b6 | ||
|
|
c73b01d549 | ||
|
|
4286bc37b3 | ||
|
|
d4ecb88fb4 | ||
|
|
57a8955795 | ||
|
|
ac53b1f97a | ||
|
|
4b83a10475 | ||
|
|
7e957a2841 | ||
|
|
38c0e70bbc | ||
|
|
94c40cce7d | ||
|
|
0059050440 | ||
|
|
f7be890eb9 | ||
|
|
59ae971a07 | ||
|
|
cfcd5efa2c | ||
|
|
fb1e85893c | ||
|
|
084972577e | ||
|
|
94df5ca048 | ||
|
|
6142a7afbb | ||
|
|
d2e0c57676 | ||
|
|
e48d8d8715 | ||
|
|
7de333e1ea | ||
|
|
2123afc4b0 | ||
|
|
ea91fa1723 | ||
|
|
4dcdec7a35 | ||
|
|
196f1914a3 | ||
|
|
ea4c7a4a37 | ||
|
|
c22fec97b4 | ||
|
|
989a0456c0 | ||
|
|
8c79a94a8c | ||
|
|
a7a6a820c0 | ||
|
|
1fa09a690e | ||
|
|
b0a84970f2 | ||
|
|
b2a115f935 | ||
|
|
9dfed0a211 | ||
|
|
5283676be7 | ||
|
|
a82509f0e0 | ||
|
|
64c92ce91d | ||
|
|
b7426e73d8 | ||
|
|
969bfd6945 | ||
|
|
200487a6fc | ||
|
|
3f330e78c5 | ||
|
|
05726c6a5a | ||
|
|
fcace22393 | ||
|
|
b95462ea38 | ||
|
|
42cd32f376 | ||
|
|
55d6dd4dce | ||
|
|
7017e24adf | ||
|
|
a4d597d426 | ||
|
|
b51376ba0d | ||
|
|
c72b5d7912 | ||
|
|
462a5bc84b | ||
|
|
3edbc71787 | ||
|
|
ee67736ac5 | ||
|
|
8bb6b7a942 | ||
|
|
c1c0d960eb | ||
|
|
a25e2ad101 | ||
|
|
7ee8bd47c8 | ||
|
|
b72ed66bb3 | ||
|
|
589bd8d29f | ||
|
|
7f7c425e34 | ||
|
|
062ea5e968 | ||
|
|
87e187c225 | ||
|
|
c0f15d8212 | ||
|
|
499de6e836 | ||
|
|
e72d117a62 | ||
|
|
abede79a4c | ||
|
|
fa415318ff | ||
|
|
718959b920 | ||
|
|
6d0ebc3a1b | ||
|
|
5f5cb965e3 | ||
|
|
86090c295c | ||
|
|
6983587f28 | ||
|
|
5e87baf174 | ||
|
|
fc93bbd7c5 | ||
|
|
ce832ad6f4 | ||
|
|
a5d53eee77 | ||
|
|
4dd63da62b | ||
|
|
2e8dd294de | ||
|
|
8917ceb08a | ||
|
|
87f116daee | ||
|
|
9e91746488 | ||
|
|
5b4ed1a0c3 | ||
|
|
56731e34cc | ||
|
|
026e5f5679 | ||
|
|
e75c160579 | ||
|
|
5d613396e1 | ||
|
|
da551f9d3c | ||
|
|
47e4861fdb | ||
|
|
1a85a89440 | ||
|
|
7ab412a8f7 | ||
|
|
5a14174a81 | ||
|
|
5e00b93783 | ||
|
|
31d9b023e6 | ||
|
|
103e503a47 | ||
|
|
1952cb0e78 | ||
|
|
69340f4ddf | ||
|
|
c52bf2200e | ||
|
|
8dccee69f2 | ||
|
|
c7a6fdd879 | ||
|
|
8bcdaf4cda | ||
|
|
3eb1359504 | ||
|
|
39cf8ad4d6 | ||
|
|
faef98f0ae | ||
|
|
710e2daa27 | ||
|
|
06ea3924fa | ||
|
|
965e2d8972 | ||
|
|
73de2fed43 | ||
|
|
536859e067 | ||
|
|
54bfaef473 | ||
|
|
e2cad707a7 | ||
|
|
ec8d9798ab | ||
|
|
e629c1e81a | ||
|
|
25feed793c | ||
|
|
2867367e16 | ||
|
|
50b408e74d | ||
|
|
8f531c7c05 | ||
|
|
350d16fee1 | ||
|
|
c052c66266 | ||
|
|
e2032df236 | ||
|
|
9ece19869d | ||
|
|
22abc8160c | ||
|
|
b1a7a22bf9 | ||
|
|
c4b38cc0dc | ||
|
|
44d6d931d5 | ||
|
|
7f1e47d7fd | ||
|
|
0eddfa4073 | ||
|
|
83a034ce91 | ||
|
|
5ab5aa01a2 | ||
|
|
b22f9156b5 | ||
|
|
bab5e2018b | ||
|
|
3f7692ce75 | ||
|
|
b69fb3b0e5 | ||
|
|
83af6e74f6 | ||
|
|
b9908003ba | ||
|
|
1f9af79215 | ||
|
|
e31e40a7ef | ||
|
|
81f6b3cc15 | ||
|
|
8adb208a91 | ||
|
|
545c3a17d2 | ||
|
|
ad76988f63 | ||
|
|
d2bbd7e1fc | ||
|
|
fb1ebb3bdf | ||
|
|
b2489537ef | ||
|
|
382a7ea411 | ||
|
|
1ae2b4c153 | ||
|
|
e8875e817e | ||
|
|
4a01c880f3 | ||
|
|
f7f00dc7bb | ||
|
|
d151b6e923 | ||
|
|
8f8e9c1061 | ||
|
|
b40358a723 | ||
|
|
01f0934da3 | ||
|
|
df7755dbc4 | ||
|
|
9073234040 | ||
|
|
053cd0fa27 | ||
|
|
961dc88dc7 | ||
|
|
b16607a844 | ||
|
|
d0d804631c | ||
|
|
3e778a88dc | ||
|
|
dbb09c9c79 | ||
|
|
06f44b67de | ||
|
|
1861852852 | ||
|
|
3ee0eee80d | ||
|
|
69ce870a81 | ||
|
|
d8467c3206 | ||
|
|
de87f40a01 |
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Release
|
||||||
|
about:
|
||||||
|
A template to outline the steps needed to for a successful release of our frontend apps
|
||||||
|
title: 'Release [add dapp version]-core-[add core version]'
|
||||||
|
labels:
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
- [ ] Review [link to core release](xxx)
|
||||||
|
- [ ] Tag frontend-monorepo
|
||||||
|
- [ ] Create release and generate release notes
|
||||||
|
- [ ] Run `@smoke` tests
|
||||||
|
- [ ] Run `@regression` tests
|
||||||
|
- [ ] Run `@slow` tests
|
||||||
|
- [ ] Explorative testing of key flows
|
||||||
|
- [ ] Set `release/[network]` to tagged commit
|
||||||
|
- [ ] Verify builds (on Netlify and Fleek) are successful
|
||||||
|
- [ ] Verify build has been deployed
|
||||||
|
- [ ] Smoke testing on deployed app
|
||||||
@@ -18,6 +18,12 @@ 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
|
||||||
- token-e2e
|
- governance-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","token-e2e","trading-e2e"]'
|
projects: '["explorer-e2e","governance-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 == *"token"* ]]; then projects+='"token-e2e" '; fi
|
if [[ $affected == *"governance"* ]]; then projects+='"governance-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+='"token-e2e" "trading-e2e" "explorer-e2e" '; fi
|
if [[ -z "$projects" ]]; then projects+='"governance-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,12 +64,11 @@ 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
|
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}"
|
||||||
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_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
|
||||||
@@ -83,8 +82,12 @@ 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)
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- name: Print logs files
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
|
run: ls -alsh /home/runner/.vegacapsule/testnet/logs/
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ failure() }}
|
||||||
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.14.0
|
node-version: 16.15.1
|
||||||
- name: Install root dependencies
|
- name: Install root dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
- name: Generate queries
|
- name: Generate queries
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ 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.14.0
|
node-version: 16.15.1
|
||||||
- name: Install commitlint cli and config
|
- name: Install root dependencies
|
||||||
run: npm install @commitlint/cli @commitlint/config-conventional
|
run: yarn install
|
||||||
- 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
|
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
id: Node
|
id: Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 16.14.0
|
node-version: 16.15.1
|
||||||
- name: Install root dependencies
|
- name: Install root dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
- name: Generate queries
|
- name: Generate queries
|
||||||
|
|||||||
@@ -1,54 +1,20 @@
|
|||||||
name: Publish docker containers
|
name: Publish docker containers
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
push:
|
pull_request:
|
||||||
tags:
|
|
||||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
|
||||||
- 'v[0-9]+.[0-9]+.[0-9]+-*'
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
publish:
|
|
||||||
description: 'Publish tag to Docker Hub & GitHub Registry'
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
tag:
|
|
||||||
description: 'Git Tag to build and publish'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
apps:
|
|
||||||
description: 'Applications to build and publish'
|
|
||||||
required: false
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- '["explorer", "token", "trading"]'
|
|
||||||
- '["explorer"]'
|
|
||||||
- '["token"]'
|
|
||||||
- '["trading"]'
|
|
||||||
archs:
|
|
||||||
description: 'Architecture to build and publish'
|
|
||||||
required: false
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- linux/amd64, linux/arm64
|
|
||||||
- linux/amd64
|
|
||||||
- linux/arm64
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
master:
|
master:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
# to be replaced by nix: https://github.com/vegaprotocol/frontend-monorepo/blob/develop/tools/ipfs-deploy.js#L106-L108
|
||||||
matrix:
|
matrix:
|
||||||
app: ${{ fromJson(inputs.apps || '["explorer", "token", "trading"]') }}
|
app: ${{ fromJson('["explorer"]') }}
|
||||||
name: Build the ${{ matrix.app }} image
|
name: Build the ${{ matrix.app }} image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
ref: ${{ inputs.tag }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
id: quemu
|
id: quemu
|
||||||
@@ -60,67 +26,64 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to DockerHub
|
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||||
if: ${{ inputs.publish || startsWith(github.ref, 'refs/tags/') }}
|
# https://github.com/actions/checkout#Checkout-pull-request-HEAD-commit-instead-of-merge-commit
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Determine Docker Image tag
|
- name: Determine Docker Image tag
|
||||||
id: tags
|
id: tags
|
||||||
run: |
|
run: |
|
||||||
hash=$(git rev-parse HEAD|cut -b1-8)
|
npmVersion=$(cat .nvmrc | head -n 1)
|
||||||
versionTag=${{ inputs.tag || startsWith(github.ref, 'refs/tags/') && github.ref_name || '${hash}' }}
|
versionTag=${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.event.pull_request.head.sha }}
|
||||||
|
echo ::set-output name=npmVersion::${npmVersion}
|
||||||
echo ::set-output name=version::${versionTag}
|
echo ::set-output name=version::${versionTag}
|
||||||
echo ::set-output name=npmVersion::$(cat dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }} | grep FROM | head -n 1 | awk '{print $2}' | cut -d ':' -f 2 | cut -d '-' -f 1 )
|
|
||||||
|
|
||||||
- name: Print config
|
- name: Print config
|
||||||
run: |
|
run: |
|
||||||
git rev-parse --verify HEAD
|
git rev-parse --verify HEAD
|
||||||
git status
|
git status
|
||||||
echo "inputs.tag=${{ inputs.tag }}"
|
|
||||||
echo "inputs.publish=${{ inputs.publish }}"
|
|
||||||
echo "inputs.apps=${{ inputs.apps }}"
|
|
||||||
echo "inputs.archs=${{ inputs.archs }}"
|
|
||||||
echo "steps.tags.outputs.version=${{ steps.tags.outputs.version }}"
|
echo "steps.tags.outputs.version=${{ steps.tags.outputs.version }}"
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: ${{ steps.tags.outputs.npmVersion }}
|
|
||||||
|
|
||||||
- name: Build frontend dists
|
|
||||||
run: |
|
|
||||||
yarn --verbose --pure-lockfile
|
|
||||||
yarn nx ${{ matrix.app =='trading' && 'export' || 'build' }} ${{ matrix.app }} --pure-lockfile
|
|
||||||
|
|
||||||
- name: Build and export to local Docker
|
- name: Build and export to local Docker
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
|
file: dockerfiles/Dockerfile
|
||||||
build-args: APP=${{ matrix.app }}
|
build-args: |
|
||||||
|
APP=${{ matrix.app }}
|
||||||
|
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
|
||||||
load: true
|
load: true
|
||||||
tags: vegaprotocol/${{ matrix.app }}:local
|
tags: |
|
||||||
|
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
|
||||||
|
|
||||||
- name: Sanity check docker image
|
- name: Sanity check docker image
|
||||||
run: |
|
run: |
|
||||||
docker run --rm vegaprotocol/${{ matrix.app }}:local cat .env
|
echo "Check .env file"
|
||||||
docker run --rm vegaprotocol/${{ matrix.app }}:local ls -lah
|
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat .env
|
||||||
|
echo "Check ipfs-hash"
|
||||||
|
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat ipfs-hash
|
||||||
|
echo "List html directory"
|
||||||
|
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ls -lah
|
||||||
|
|
||||||
- name: Build and push to DockerHub
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ inputs.publish || startsWith(github.ref, 'refs/tags/') }}
|
push: true
|
||||||
file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
|
file: dockerfiles/Dockerfile
|
||||||
build-args: APP=${{ matrix.app }}
|
build-args: |
|
||||||
platforms: ${{ inputs.archs || 'linux/amd64, linux/arm64' }}
|
APP=${{ matrix.app }}
|
||||||
|
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
|
||||||
tags: |
|
tags: |
|
||||||
vegaprotocol/${{ matrix.app }}:latest
|
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:latest
|
||||||
vegaprotocol/${{ matrix.app }}:${{ steps.tags.outputs.version }}
|
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ steps.tags.outputs.version }}
|
||||||
|
|
||||||
- name: Image digest
|
- name: Image digest
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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.14.0
|
node-version: 16.15.1
|
||||||
- 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/token-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/governance-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:
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ NX_TENDERMINT_URL=http://localhost:26617
|
|||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
||||||
NX_VEGA_URL=http://localhost:3028/query
|
NX_VEGA_URL=http://localhost:3028/query
|
||||||
NX_VEGA_ENV=CUSTOM
|
NX_VEGA_ENV=CUSTOM
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/capsule-network.json
|
NX_VEGA_CONFIG_URL=
|
||||||
|
|
||||||
CYPRESS_VEGA_TENDERMINT_URL=http://localhost:26617
|
CYPRESS_VEGA_TENDERMINT_URL=http://localhost:26617
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
"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",
|
||||||
@@ -77,6 +76,7 @@
|
|||||||
"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,150 +1,48 @@
|
|||||||
context('Asset page', { tags: '@regression' }, function () {
|
context('Asset page', { tags: '@regression' }, () => {
|
||||||
before('gather system asset information', function () {
|
const columns = ['symbol', 'name', 'id', 'type', 'status', 'actions'];
|
||||||
cy.get_asset_information().as('assetsInfo');
|
const hiddenOnMobile = ['id', 'type', 'status'];
|
||||||
});
|
describe('Verify elements on page', () => {
|
||||||
|
before('Navigate to assets page', () => {
|
||||||
describe('Verify elements on page', function () {
|
cy.visit('/assets');
|
||||||
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 assets page sections', function () {
|
it('should be able to see full assets list', () => {
|
||||||
const assetNames = Object.keys(this.assetsInfo);
|
cy.getAssets().then((assets) => {
|
||||||
assetNames.forEach((assetName) => {
|
assets.forEach((asset) => {
|
||||||
cy.get(assetHeader)
|
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
|
||||||
.contains(assetName)
|
|
||||||
.should('be.visible')
|
|
||||||
.next()
|
|
||||||
.within(() => {
|
|
||||||
cy.get(jsonSection).should('not.be.empty');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be able to see all asset details displayed in JSON', function () {
|
|
||||||
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];
|
|
||||||
|
|
||||||
assert.equal(assetsListedInJson.name, assetInfo.node.name);
|
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
if (assetInfo.node.source.__typename == 'ERC20') {
|
|
||||||
assert.equal(
|
|
||||||
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 be able to switch assets 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
|
columns.forEach((col) => {
|
||||||
cy.get(themeSwitcher).click();
|
cy.get(`[col-id="${col}"]`).should('be.visible');
|
||||||
|
});
|
||||||
// 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 () {
|
it('should be able to see assets page displayed in mobile', () => {
|
||||||
cy.common_switch_to_mobile_and_click_toggle();
|
cy.switchToMobile();
|
||||||
cy.get(assetsNavigation).click();
|
|
||||||
|
|
||||||
const assetNames = Object.keys(this.assetsInfo);
|
hiddenOnMobile.forEach((col) => {
|
||||||
assetNames.forEach((assetName) => {
|
cy.get(`[col-id="${col}"]`).should('have.length', 0);
|
||||||
cy.get(assetHeader)
|
});
|
||||||
.contains(assetName)
|
|
||||||
.should('be.visible')
|
cy.getAssets().then((assets) => {
|
||||||
.next()
|
assets.forEach((asset) => {
|
||||||
.within(() => {
|
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
|
||||||
cy.get(jsonSection).should('not.be.empty');
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open details page when clicked on "View details"', () => {
|
||||||
|
cy.getAssets().then((assets) => {
|
||||||
|
assets.forEach((asset) => {
|
||||||
|
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
||||||
|
.eq(0)
|
||||||
|
.should('contain.text', 'View details');
|
||||||
|
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
||||||
|
.eq(0)
|
||||||
|
.click();
|
||||||
|
cy.getByTestId('asset-header').should('have.text', asset.name);
|
||||||
|
cy.go('back');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,33 +1,27 @@
|
|||||||
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 () {
|
||||||
const blockNavigation = 'a[href="/blocks"]';
|
beforeEach(() => {
|
||||||
|
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.common_switch_to_mobile_and_click_toggle();
|
cy.switchToMobile();
|
||||||
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).click();
|
cy.get(blockHeight).eq(0).find('a').click({ force: true });
|
||||||
|
|
||||||
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
|
||||||
@@ -35,7 +29,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).click();
|
cy.get(blockHeight).eq(0).find('a').click({ force: true });
|
||||||
cy.get(blockHeader)
|
cy.get(blockHeader)
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
.then(($blockHeaderTxt) => {
|
.then(($blockHeaderTxt) => {
|
||||||
|
|||||||
@@ -6,18 +6,10 @@ context('Home Page', function () {
|
|||||||
describe('Stats page', { tags: '@smoke' }, function () {
|
describe('Stats page', { tags: '@smoke' }, function () {
|
||||||
const statsValue = '[data-testid="stats-value"]';
|
const statsValue = '[data-testid="stats-value"]';
|
||||||
|
|
||||||
it('Should show connected environment', function () {
|
|
||||||
const deployedEnv = Cypress.env('environment').toUpperCase();
|
|
||||||
cy.get('[data-testid="stats-environment"]').should(
|
|
||||||
'have.text',
|
|
||||||
`/ ${deployedEnv}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show connected environment stats', function () {
|
it('should show connected environment stats', function () {
|
||||||
const statTitles = {
|
const statTitles = {
|
||||||
0: 'Status',
|
0: 'Status',
|
||||||
1: 'Height',
|
1: 'Block height',
|
||||||
2: 'Uptime',
|
2: 'Uptime',
|
||||||
3: 'Total nodes',
|
3: 'Total nodes',
|
||||||
4: 'Total staked',
|
4: 'Total staked',
|
||||||
@@ -36,27 +28,27 @@ context('Home Page', function () {
|
|||||||
|
|
||||||
cy.get('[data-testid="stats-title"]')
|
cy.get('[data-testid="stats-title"]')
|
||||||
.each(($list, index) => {
|
.each(($list, index) => {
|
||||||
cy.wrap($list).should('have.text', statTitles[index]);
|
cy.wrap($list).should('contain.text', statTitles[index]);
|
||||||
})
|
})
|
||||||
.then(($list) => {
|
.then(($list) => {
|
||||||
cy.wrap($list).should('have.length', 16);
|
cy.wrap($list).should('have.length', 16);
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(statsValue).eq(0).should('have.text', 'CONNECTED');
|
cy.get(statsValue).eq(0).should('contain.text', 'CONNECTED');
|
||||||
cy.get(statsValue).eq(1).should('not.be.empty');
|
cy.get(statsValue).eq(1).should('not.be.empty');
|
||||||
cy.get(statsValue)
|
cy.get(statsValue)
|
||||||
.eq(2)
|
.eq(2)
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
.should('match', /\d+d \d+h \d+m \d+s/i);
|
.should('match', /\d+d \d+h \d+m \d+s/i);
|
||||||
cy.get(statsValue).eq(3).should('have.text', '2');
|
cy.get(statsValue).eq(3).should('contain.text', '2');
|
||||||
cy.get(statsValue)
|
cy.get(statsValue)
|
||||||
.eq(4)
|
.eq(4)
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
.should('match', /\d+\.\d\d(?!\d)/i);
|
.should('match', /\d+\.\d\d(?!\d)/i);
|
||||||
cy.get(statsValue).eq(5).should('have.text', '0');
|
cy.get(statsValue).eq(5).should('contain.text', '0');
|
||||||
cy.get(statsValue).eq(6).should('have.text', '0');
|
cy.get(statsValue).eq(6).should('contain.text', '0');
|
||||||
cy.get(statsValue).eq(7).should('have.text', '0');
|
cy.get(statsValue).eq(7).should('contain.text', '0');
|
||||||
cy.get(statsValue).eq(8).should('have.text', '0');
|
cy.get(statsValue).eq(8).should('contain.text', '0');
|
||||||
cy.get(statsValue).eq(9).should('not.be.empty');
|
cy.get(statsValue).eq(9).should('not.be.empty');
|
||||||
cy.get(statsValue).eq(10).should('not.be.empty');
|
cy.get(statsValue).eq(10).should('not.be.empty');
|
||||||
cy.get(statsValue).eq(11).should('not.be.empty');
|
cy.get(statsValue).eq(11).should('not.be.empty');
|
||||||
@@ -86,75 +78,4 @@ context('Home Page', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Git info', function () {
|
|
||||||
it('git info is rendered on the footer of the page', function () {
|
|
||||||
cy.getByTestId('git-info').within(() => {
|
|
||||||
cy.getByTestId('git-network-data').within(() => {
|
|
||||||
cy.contains('Reading network data from').should('be.visible');
|
|
||||||
cy.get('span').should('have.text', Cypress.env('networkQueryUrl'));
|
|
||||||
cy.getByTestId('link').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.getByTestId('git-eth-data').within(() => {
|
|
||||||
cy.contains('Reading Ethereum data from').should('be.visible');
|
|
||||||
cy.get('span').should('have.text', Cypress.env('ethUrl'));
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.getByTestId('git-commit-hash').within(() => {
|
|
||||||
cy.contains('Version/commit hash:').should('be.visible');
|
|
||||||
cy.getByTestId('link').should('have.text', Cypress.env('commitHash'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Search bar', function () {
|
|
||||||
it('Successful search for specific id by block id', function () {
|
|
||||||
const blockId = '973624';
|
|
||||||
search(blockId);
|
|
||||||
cy.url().should('include', blockId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Successful search for specific id by tx hash', function () {
|
|
||||||
const txHash =
|
|
||||||
'9ED3718AA8308E7E08EC588EE7AADAF49711D2138860D8914B4D81A2054D9FB8';
|
|
||||||
search(txHash);
|
|
||||||
cy.url().should('include', txHash);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Successful search for specific id by tx id', function () {
|
|
||||||
const txId =
|
|
||||||
'0x61DCCEBB955087F50D0B85382DAE138EDA9631BF1A4F92E563D528904AA38898';
|
|
||||||
search(txId);
|
|
||||||
cy.url().should('include', txId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Error message displayed when invalid search by wrong string length', function () {
|
|
||||||
search('9ED3718AA8308E7E08EC588EE7AADAF497D2138860D8914B4D81A2054D9FB8');
|
|
||||||
validateSearchError("Something doesn't look right");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Error message displayed when invalid search by invalid hash', function () {
|
|
||||||
search(
|
|
||||||
'9ED3718AA8308E7E08ECht8EE753DAF49711D2138860D8914B4D81A2054D9FB8'
|
|
||||||
);
|
|
||||||
validateSearchError('Transaction is not hexadecimal');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Error message displayed when searching empty field', function () {
|
|
||||||
cy.get('[data-testid="search"]').clear();
|
|
||||||
cy.get('[data-testid="search-button"]').click();
|
|
||||||
validateSearchError('Search required');
|
|
||||||
});
|
|
||||||
|
|
||||||
function search(searchTxt) {
|
|
||||||
cy.get('[data-testid="search"]').clear().type(searchTxt);
|
|
||||||
cy.get('[data-testid="search-button"]').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateSearchError(expectedError) {
|
|
||||||
cy.get('[data-testid="search-error"]').should('have.text', expectedError);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ 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 () {
|
describe('Verify elements on page', function () {
|
||||||
const networkParametersNavigation = 'a[href="/network-parameters"]';
|
beforeEach(() => {
|
||||||
|
cy.visit('/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')
|
||||||
@@ -201,55 +198,8 @@ 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.common_switch_to_mobile_and_click_toggle();
|
cy.switchToMobile();
|
||||||
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.get_asset_information().then((assetsInfo) => {
|
cy.getAssets().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.get_asset_information().then((assetsInfo) => {
|
cy.getAssets().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,303 +1,15 @@
|
|||||||
context('Validator page', { tags: '@smoke' }, function () {
|
context('Validator page', { tags: '@smoke' }, function () {
|
||||||
const validatorMenuHeading = 'a[href="/validators"]';
|
|
||||||
const tendermintDataHeader = '[data-testid="tendermint-header"]';
|
|
||||||
const vegaDataHeader = '[data-testid="vega-header"]';
|
|
||||||
const jsonSection = '.language-json';
|
|
||||||
|
|
||||||
before('Visit validators page and obtain data', function () {
|
before('Visit validators page and obtain data', function () {
|
||||||
cy.visit('/');
|
cy.visit('/validators');
|
||||||
cy.get(validatorMenuHeading).click();
|
|
||||||
cy.get_validators().as('validators');
|
|
||||||
cy.get_nodes().as('nodes');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Verify elements on page', function () {
|
describe('Verify elements on page', function () {
|
||||||
before('Ensure at least two validators are present', function () {
|
it('should be able to see validator tiles', function () {
|
||||||
assert.isAtLeast(
|
cy.getNodes().then((nodes) => {
|
||||||
this.validators.length,
|
nodes.forEach((node) => {
|
||||||
2,
|
cy.get(`[validator-id="${node.id}"]`).should('be.visible');
|
||||||
'Ensuring at least two validators exist'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be able to see validator page sections', function () {
|
|
||||||
cy.get(vegaDataHeader)
|
|
||||||
.contains('Vega data')
|
|
||||||
.and('is.visible')
|
|
||||||
.next()
|
|
||||||
.within(() => {
|
|
||||||
cy.get(jsonSection).should('not.be.empty');
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(tendermintDataHeader)
|
|
||||||
.contains('Tendermint data')
|
|
||||||
.and('is.visible')
|
|
||||||
.next()
|
|
||||||
.within(() => {
|
|
||||||
cy.get(jsonSection).should('not.be.empty');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be able to see relevant validator information in tendermint section', function () {
|
|
||||||
cy.get(tendermintDataHeader)
|
|
||||||
.contains('Tendermint data')
|
|
||||||
.next()
|
|
||||||
.within(() => {
|
|
||||||
cy.get(jsonSection)
|
|
||||||
.invoke('text')
|
|
||||||
.convert_string_json_to_js_object()
|
|
||||||
.then((validatorsInJson) => {
|
|
||||||
this.validators.forEach((validator, index) => {
|
|
||||||
const validatorInJson =
|
|
||||||
validatorsInJson.result.validators[index];
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
validatorInJson.address,
|
|
||||||
validator.address,
|
|
||||||
`Checking that validator address shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(validator.address).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
validatorInJson.pub_key.type,
|
|
||||||
validator.pub_key.type,
|
|
||||||
`Checking that validator public key type shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(validator.pub_key.type).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
validatorInJson.pub_key.value,
|
|
||||||
validator.pub_key.value,
|
|
||||||
`Checking that validator public key value shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(validator.pub_key.value).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
validatorInJson.voting_power,
|
|
||||||
validator.voting_power,
|
|
||||||
`Checking that validator voting power in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(validator.voting_power).should('be.visible');
|
|
||||||
|
|
||||||
// Proposer priority can change frequently mid test
|
|
||||||
// Therefore only checking the field name is present.
|
|
||||||
cy.contains('proposer_priority').should('be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test disabled 2022/11/15 during the 0.62.1 upgrade. The JSON structure changed, and
|
|
||||||
// this test failed. Rather than fix it, it will be replaced when the validator view displays
|
|
||||||
// something useful rather than just dumping out the JSON.
|
|
||||||
xit('should be able to see relevant node information in vega data section', function () {
|
|
||||||
cy.get(vegaDataHeader)
|
|
||||||
.contains('Vega data')
|
|
||||||
.next()
|
|
||||||
.within(() => {
|
|
||||||
cy.get(jsonSection)
|
|
||||||
.invoke('text')
|
|
||||||
.convert_string_json_to_js_object()
|
|
||||||
.then((nodesInJson) => {
|
|
||||||
this.nodes.forEach((node, index) => {
|
|
||||||
const nodeInJson = nodesInJson.edges[index].node;
|
|
||||||
|
|
||||||
// Vegacapsule shows no info or null for following fields:
|
|
||||||
// name, infoURL, avatarUrl, location, epoch data
|
|
||||||
// Therefore, these values remain unchecked.
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
nodeInJson.__typename,
|
|
||||||
node.__typename,
|
|
||||||
`Checking that node __typename shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(node.__typename).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
nodeInJson.id,
|
|
||||||
node.id,
|
|
||||||
`Checking that node id shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(node.id).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
nodeInJson.pubkey,
|
|
||||||
node.pubkey,
|
|
||||||
`Checking that node pubkey shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(node.pubkey).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
nodeInJson.tmPubkey,
|
|
||||||
node.tmPubkey,
|
|
||||||
`Checking that node tmPubkey shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(node.tmPubkey).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
nodeInJson.ethereumAddress,
|
|
||||||
node.ethereumAddress,
|
|
||||||
`Checking that node ethereumAddress shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(node.ethereumAddress).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
nodeInJson.stakedByOperator,
|
|
||||||
node.stakedByOperator,
|
|
||||||
`Checking that node stakedByOperator value shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(node.stakedByOperator).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
nodeInJson.stakedByDelegates,
|
|
||||||
node.stakedByDelegates,
|
|
||||||
`Checking that node stakedByDelegates value shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(node.stakedByDelegates).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
nodeInJson.stakedTotal,
|
|
||||||
node.stakedTotal,
|
|
||||||
`Checking that node stakedTotal shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(node.stakedTotal).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
nodeInJson.pendingStake,
|
|
||||||
node.pendingStake,
|
|
||||||
`Checking that node pendingStake shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(node.pendingStake).should('be.visible');
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
nodeInJson.status,
|
|
||||||
node.status,
|
|
||||||
`Checking that node status shown in json matches system data`
|
|
||||||
);
|
|
||||||
cy.contains(node.status).should('be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be able to see validator page displayed on mobile', function () {
|
|
||||||
cy.common_switch_to_mobile_and_click_toggle();
|
|
||||||
cy.get(validatorMenuHeading).click();
|
|
||||||
cy.get(vegaDataHeader)
|
|
||||||
.contains('Vega data')
|
|
||||||
.and('is.visible')
|
|
||||||
.next()
|
|
||||||
.within(() => {
|
|
||||||
cy.get(jsonSection).should('not.be.empty');
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(tendermintDataHeader)
|
|
||||||
.contains('Tendermint data')
|
|
||||||
.and('is.visible')
|
|
||||||
.next()
|
|
||||||
.within(() => {
|
|
||||||
cy.get(jsonSection).should('not.be.empty');
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(tendermintDataHeader)
|
|
||||||
.contains('Tendermint data')
|
|
||||||
.next()
|
|
||||||
.within(() => {
|
|
||||||
this.validators.forEach((validator) => {
|
|
||||||
cy.contains(validator.address).should('be.visible');
|
|
||||||
cy.contains(validator.pub_key.type).should('be.visible');
|
|
||||||
cy.contains(validator.pub_key.value).should('be.visible');
|
|
||||||
cy.contains(validator.voting_power).should('be.visible');
|
|
||||||
// Proposer priority can change frequently mid test
|
|
||||||
// Therefore only checking the field name is present.
|
|
||||||
cy.contains('proposer_priority').should('be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be able to switch validator page between light and dark mode', function () {
|
|
||||||
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
|
|
||||||
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
|
|
||||||
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
|
|
||||||
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
|
|
||||||
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
|
|
||||||
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
|
|
||||||
const themeSwitcher = '[data-testid="theme-switcher"]';
|
|
||||||
const jsonFields = '.hljs';
|
|
||||||
const sideMenuBackground = '.absolute';
|
|
||||||
|
|
||||||
// Engage dark mode if not allready set
|
|
||||||
cy.get(sideMenuBackground)
|
|
||||||
.should('have.css', 'background-color')
|
|
||||||
.then((background_color) => {
|
|
||||||
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
|
|
||||||
cy.get(themeSwitcher).click();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Engage white mode
|
|
||||||
cy.get(themeSwitcher).click();
|
|
||||||
|
|
||||||
// White Mode
|
|
||||||
cy.get(validatorMenuHeading)
|
|
||||||
.should('have.css', 'background-color')
|
|
||||||
.and('include', whiteThemeSelectedMenuOptionColor);
|
|
||||||
cy.get(jsonFields)
|
|
||||||
.should('have.css', 'background-color')
|
|
||||||
.and('include', whiteThemeJsonFieldBackColor);
|
|
||||||
cy.get(sideMenuBackground)
|
|
||||||
.should('have.css', 'background-color')
|
|
||||||
.and('include', whiteThemeSideMenuBackgroundColor);
|
|
||||||
|
|
||||||
// Dark Mode
|
|
||||||
cy.get(themeSwitcher).click();
|
|
||||||
cy.get(validatorMenuHeading)
|
|
||||||
.should('have.css', 'background-color')
|
|
||||||
.and('include', darkThemeSelectedMenuOptionColor);
|
|
||||||
cy.get(jsonFields)
|
|
||||||
.should('have.css', 'background-color')
|
|
||||||
.and('include', darkThemeJsonFieldBackColor);
|
|
||||||
cy.get(sideMenuBackground)
|
|
||||||
.should('have.css', 'background-color')
|
|
||||||
.and('include', darkThemeSideMenuBackgroundColor);
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('get_validators', () => {
|
|
||||||
cy.request({
|
|
||||||
method: 'GET',
|
|
||||||
url: `http://localhost:26617/validators`,
|
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
})
|
|
||||||
.its(`body.result.validators`)
|
|
||||||
.then(function (response) {
|
|
||||||
let validators = [];
|
|
||||||
response.forEach((account, index) => {
|
|
||||||
validators[index] = account;
|
|
||||||
});
|
|
||||||
return validators;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('get_nodes', () => {
|
|
||||||
const mutation =
|
|
||||||
'{nodesConnection { edges { node { id name infoUrl avatarUrl pubkey tmPubkey ethereumAddress \
|
|
||||||
location stakedByOperator stakedByDelegates stakedTotal pendingStake \
|
|
||||||
epochData { total offline online __typename } status name __typename}}}}';
|
|
||||||
cy.request({
|
|
||||||
method: 'POST',
|
|
||||||
url: `http://localhost:3028/query`,
|
|
||||||
body: {
|
|
||||||
query: mutation,
|
|
||||||
},
|
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
})
|
|
||||||
.its(`body.data.nodesConnection.edges`)
|
|
||||||
.then(function (response) {
|
|
||||||
let nodes = [];
|
|
||||||
response.forEach((node) => {
|
|
||||||
nodes.push(node);
|
|
||||||
});
|
|
||||||
return nodes;
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ 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('/');
|
||||||
|
|||||||
+2
-2
@@ -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://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml
|
||||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
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_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,9 +1,7 @@
|
|||||||
# 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,10 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm
|
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
|
||||||
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_GOVERNANCE_URL=https://dev.token.vega.xyz
|
||||||
|
NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql
|
||||||
|
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# 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
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz/websocket
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
|
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
|
||||||
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
|
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# 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=https://tm.be.mainnet-mirror.vega.xyz
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.xyz/websocket
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mirror-network.json
|
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
|
||||||
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
|
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_VEGA_ENV=SANDBOX
|
NX_VEGA_ENV=SANDBOX
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/sandbox-network.json
|
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
|
||||||
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
|
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_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
|
||||||
NX_TENDERMINT_URL=https://tm.n01.sandbox.vega.xyz
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.sandbox.vega.xyz/websocket
|
||||||
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
|
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
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_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
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://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
|
||||||
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
|
||||||
|
|||||||
@@ -1,8 +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://be.stagnet3.vega.xyz/websocket
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml
|
||||||
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_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
|
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
|
||||||
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
|
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
|
||||||
|
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases/
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
# 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://tm.be.testnet.vega.xyz/websocket/
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
|
||||||
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
|
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# App configuration variables
|
||||||
|
NX_VEGA_ENV=VALIDATOR_TESTNET
|
||||||
|
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/testnet2/testnet2.toml
|
||||||
|
NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql
|
||||||
|
NX_VEGA_REST=https://api-validators-testnet.vega.rocks/
|
||||||
|
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.rocks
|
||||||
|
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/
|
||||||
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
|
||||||
@@ -2,6 +2,5 @@
|
|||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
function ReactMarkdown({ children }) {
|
||||||
|
return <div>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReactMarkdown;
|
||||||
@@ -1,62 +1,29 @@
|
|||||||
import classnames from 'classnames';
|
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
||||||
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 { 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 { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
import { Footer } from './components/footer/footer';
|
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
|
||||||
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
import { router } from './routes/router-config';
|
||||||
|
|
||||||
|
const splashLoading = (
|
||||||
|
<Splash>
|
||||||
|
<Loader />
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
|
||||||
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={cacheConfig}>
|
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
||||||
<div className={layoutClasses}>
|
<RouterProvider router={router} fallbackElement={splashLoading} />
|
||||||
<Header menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
|
|
||||||
<Nav menuOpen={menuOpen} />
|
|
||||||
<Main />
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
</NetworkLoader>
|
</NetworkLoader>
|
||||||
</TendermintWebsocketProvider>
|
</TendermintWebsocketProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
return (
|
useInitializeEnv();
|
||||||
<EnvironmentProvider>
|
return <App />;
|
||||||
<App />
|
|
||||||
</EnvironmentProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Wrapper;
|
export default Wrapper;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
|
import { useAssetDataProvider } from '@vegaprotocol/assets';
|
||||||
|
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,21 +17,17 @@ const AssetBalance = ({
|
|||||||
price,
|
price,
|
||||||
showAssetLink = true,
|
showAssetLink = true,
|
||||||
}: AssetBalanceProps) => {
|
}: AssetBalanceProps) => {
|
||||||
const { data } = useExplorerAssetQuery({
|
const { data: asset } = useAssetDataProvider(assetId);
|
||||||
variables: { id: assetId },
|
|
||||||
});
|
|
||||||
|
|
||||||
const label =
|
const label =
|
||||||
data && data.asset?.decimals
|
asset && asset.decimals
|
||||||
? addDecimalsFormatNumber(price, data.asset.decimals)
|
? addDecimalsFormatNumber(price, asset.decimals)
|
||||||
: price;
|
: price;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-block">
|
<div className="inline-block">
|
||||||
<span>{label}</span>{' '}
|
<span>{label}</span>{' '}
|
||||||
{showAssetLink && data?.asset?.id ? (
|
{showAssetLink && asset?.id ? <AssetLink assetId={assetId} /> : null}
|
||||||
<AssetLink id={data.asset.id} />
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
import { useExplorerGovernanceAssetQuery } from './__generated__/Governance-asset';
|
import { useExplorerGovernanceAssetQuery } from './__generated__/Governance-asset';
|
||||||
import AssetBalance from './asset-balance';
|
import AssetBalance from './asset-balance';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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/react-helpers';
|
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||||
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/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|
||||||
interface BlockProps {
|
interface BlockProps {
|
||||||
block: BlockMeta;
|
block: BlockMeta;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
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/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
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/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
interface BlocksRefetchProps {
|
interface BlocksRefetchProps {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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 text-xl uppercase text-center leading-relaxed">
|
<h1 className="font-alpha calt text-xl uppercase text-center leading-relaxed">
|
||||||
{heading}
|
{heading}
|
||||||
</h1>
|
</h1>
|
||||||
) : null}
|
) : null}
|
||||||
{label ? (
|
{label ? (
|
||||||
<p className="font-alpha text-gray-500 text-center">{label}</p>
|
<p className="font-alpha calt text-gray-500 text-center">{label}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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>;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
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,39 +1,60 @@
|
|||||||
import { useEnvironment } from '@vegaprotocol/environment';
|
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
|
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, setNodeSwitcherOpen } =
|
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
|
||||||
useEnvironment();
|
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
||||||
return (
|
const { screenSize } = useScreenDimensions();
|
||||||
<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">
|
const showFullFeedbackLabel = useMemo(
|
||||||
<div className="flex justify-between gap-2 align-middle">
|
() => ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
|
||||||
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
[screenSize]
|
||||||
{GIT_COMMIT_HASH && (
|
);
|
||||||
<p data-testid="git-commit-hash">
|
|
||||||
{t('Version')}:{' '}
|
|
||||||
<Link
|
|
||||||
href={
|
|
||||||
GIT_ORIGIN_URL
|
|
||||||
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
target={GIT_ORIGIN_URL ? '_blank' : undefined}
|
|
||||||
>
|
|
||||||
{GIT_COMMIT_HASH}
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex pl-2 content-center">
|
return (
|
||||||
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
|
<>
|
||||||
<Link className="ml-2" onClick={setNodeSwitcherOpen}>
|
<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">
|
||||||
{t('Change')}
|
<div className="flex justify-between gap-2 align-middle">
|
||||||
</Link>
|
{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">
|
||||||
|
{t('Version')}:{' '}
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
GIT_ORIGIN_URL
|
||||||
|
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
target={GIT_ORIGIN_URL ? '_blank' : undefined}
|
||||||
|
>
|
||||||
|
{GIT_COMMIT_HASH}
|
||||||
|
</Link>
|
||||||
|
</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>
|
||||||
</div>
|
</footer>
|
||||||
</footer>
|
<NodeSwitcherDialog
|
||||||
|
open={nodeSwitcherOpen}
|
||||||
|
setOpen={setNodeSwitcherOpen}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,19 +13,18 @@ jest.mock('../search', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const renderComponent = () => (
|
const renderComponent = () => (
|
||||||
<MemoryRouter>
|
<MemoryRouter initialEntries={['/txs']}>
|
||||||
<Header menuOpen={false} setMenuOpen={jest.fn()} />
|
<Header />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('Header', () => {
|
describe('Header', () => {
|
||||||
it('should render heading', () => {
|
it('should render navigation', () => {
|
||||||
render(renderComponent());
|
render(renderComponent());
|
||||||
|
|
||||||
expect(screen.getByTestId('explorer-header')).toHaveTextContent(
|
expect(screen.getByTestId('navigation')).toHaveTextContent('Explorer');
|
||||||
'Vega Explorer'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render search', () => {
|
it('should render search', () => {
|
||||||
render(renderComponent());
|
render(renderComponent());
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,111 @@
|
|||||||
import classnames from 'classnames';
|
import { matchPath, useLocation, useMatch } from 'react-router-dom';
|
||||||
import { Link } from 'react-router-dom';
|
import {
|
||||||
import { ThemeSwitcher, Icon } from '@vegaprotocol/ui-toolkit';
|
ThemeSwitcher,
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
Navigation,
|
||||||
import { Search } from '../search';
|
NavigationList,
|
||||||
|
NavigationItem,
|
||||||
|
NavigationLink,
|
||||||
|
NavigationBreakpoint,
|
||||||
|
NavigationTrigger,
|
||||||
|
NavigationContent,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { Routes } from '../../routes/route-names';
|
import { Routes } from '../../routes/route-names';
|
||||||
import type { Dispatch, SetStateAction } from 'react';
|
|
||||||
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
||||||
|
import type { Navigable } from '../../routes/router-config';
|
||||||
|
import { isNavigable } from '../../routes/router-config';
|
||||||
|
import { routerConfig } from '../../routes/router-config';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import compact from 'lodash/compact';
|
||||||
|
import { Search } from '../search';
|
||||||
|
|
||||||
interface ThemeToggleProps {
|
const routeToNavigationItem = (r: Navigable) => (
|
||||||
menuOpen: boolean;
|
<NavigationItem key={r.handle.name}>
|
||||||
setMenuOpen: Dispatch<SetStateAction<boolean>>;
|
<NavigationLink to={r.path}>{r.handle.text}</NavigationLink>
|
||||||
}
|
</NavigationItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Header = () => {
|
||||||
|
const isHome = Boolean(useMatch(Routes.HOME));
|
||||||
|
const pages = routerConfig[0].children || [];
|
||||||
|
const mainItems = compact(
|
||||||
|
[Routes.TX, Routes.BLOCKS, Routes.ORACLES, Routes.VALIDATORS].map((n) =>
|
||||||
|
pages.find((r) => r.path === n)
|
||||||
|
)
|
||||||
|
).filter(isNavigable);
|
||||||
|
|
||||||
|
const groupedItems = compact(
|
||||||
|
[
|
||||||
|
Routes.PARTIES,
|
||||||
|
Routes.ASSETS,
|
||||||
|
Routes.MARKETS,
|
||||||
|
Routes.GOVERNANCE,
|
||||||
|
Routes.NETWORK_PARAMETERS,
|
||||||
|
Routes.GENESIS,
|
||||||
|
].map((n) => pages.find((r) => r.path === n))
|
||||||
|
).filter(isNavigable);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
export const Header = ({ menuOpen, setMenuOpen }: ThemeToggleProps) => {
|
|
||||||
const headerClasses = classnames(
|
|
||||||
'md:col-span-2',
|
|
||||||
'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'
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<header className={headerClasses}>
|
<Navigation
|
||||||
<div className="flex h-full items-center sm:items-stretch gap-4">
|
appName="Explorer"
|
||||||
<Link to={Routes.HOME}>
|
theme="system"
|
||||||
<h1
|
breakpoints={[490, 900]}
|
||||||
className="text-white text-3xl font-alpha uppercase calt mb-0"
|
actions={
|
||||||
data-testid="explorer-header"
|
<>
|
||||||
>
|
<ThemeSwitcher />
|
||||||
{t('Vega Explorer')}
|
{!isHome && <Search />}
|
||||||
</h1>
|
</>
|
||||||
</Link>
|
}
|
||||||
<NetworkSwitcher />
|
onResize={(width, el) => {
|
||||||
</div>
|
if (width < 1157) {
|
||||||
<button
|
// switch to magnifying glass trigger when width < 1157
|
||||||
data-testid="open-menu"
|
el.classList.remove('nav-search-full');
|
||||||
className="md:hidden text-white"
|
el.classList.add('nav-search-compact');
|
||||||
onClick={() => setMenuOpen(!menuOpen)}
|
} else {
|
||||||
|
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]}
|
||||||
>
|
>
|
||||||
<Icon name={menuOpen ? 'cross' : 'menu'} />
|
{mainItems.map(routeToNavigationItem)}
|
||||||
</button>
|
{groupedItems && groupedItems.length > 0 && (
|
||||||
<Search />
|
<NavigationItem>
|
||||||
<ThemeSwitcher className="-my-4" />
|
<NavigationTrigger isActive={Boolean(isOnOther)}>
|
||||||
</header>
|
{t('Other')}
|
||||||
|
</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-zinc-800 dark:text-zinc-300">
|
<p className="text-vega-dark-100 dark:text-vega-light-200">
|
||||||
{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-zinc-400 dark:text-zinc-600"
|
className="ml-2 text-vega-light-300 dark:text-vega-dark-300"
|
||||||
/>
|
/>
|
||||||
</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-black dark:text-zinc-200"
|
className="text-vega-dark-100 dark:text-vega-light-200"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p
|
<p
|
||||||
title={id}
|
title={id}
|
||||||
className="text-black dark:text-zinc-200 truncate ..."
|
className="text-vega-dark-100 dark:text-vega-light-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-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
|
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
|
||||||
<Icon name="duplicate" />
|
<Icon name="duplicate" />
|
||||||
</button>
|
</button>
|
||||||
</CopyWithTooltip>
|
</CopyWithTooltip>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
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/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|
||||||
interface JumpToProps {
|
interface JumpToProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
query ExplorerAsset($id: ID!) {
|
|
||||||
asset(id: $id) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
status
|
|
||||||
decimals
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +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 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,63 +1,37 @@
|
|||||||
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 } from '@testing-library/react';
|
import { render, waitFor } from '@testing-library/react';
|
||||||
import AssetLink from './asset-link';
|
import { AssetLink } from './asset-link';
|
||||||
import { ExplorerAssetDocument } from './__generated__/Asset';
|
import { mockAssetA1 } from '../../../mocks/assets';
|
||||||
|
|
||||||
function renderComponent(id: string, mock: MockedResponse[]) {
|
function renderComponent(id: string, mock: MockedResponse[]) {
|
||||||
return (
|
return (
|
||||||
<MockedProvider mocks={mock}>
|
<MockedProvider mocks={mock} addTypename={false}>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<AssetLink id={id} />
|
<AssetLink assetId={id} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Asset link component', () => {
|
describe('AssetLink', () => {
|
||||||
it('Renders the ID at first', () => {
|
it('renders the asset id when not found and makes the button disabled', async () => {
|
||||||
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 the query returns a result', async () => {
|
it('renders the asset name when found and make the button enabled', async () => {
|
||||||
const mock = {
|
const res = render(renderComponent('123', [mockAssetA1]));
|
||||||
request: {
|
|
||||||
query: ExplorerAssetDocument,
|
|
||||||
variables: {
|
|
||||||
id: '123',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
asset: {
|
|
||||||
id: '123',
|
|
||||||
name: 'test-label',
|
|
||||||
status: 'irrelevant-test-data',
|
|
||||||
decimals: 18,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = render(renderComponent('123', [mock]));
|
|
||||||
expect(res.getByText('123')).toBeInTheDocument();
|
expect(res.getByText('123')).toBeInTheDocument();
|
||||||
expect(await res.findByText('test-label')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Leaves the asset id when the asset is not found', async () => {
|
await waitFor(async () => {
|
||||||
const mock = {
|
expect(await res.findByText('A ONE')).toBeInTheDocument();
|
||||||
request: {
|
expect(await res.findByTestId('asset-link')).not.toBeDisabled();
|
||||||
query: ExplorerAssetDocument,
|
});
|
||||||
variables: {
|
|
||||||
id: '123',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
error: new Error('No such asset'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = render(renderComponent('123', [mock]));
|
|
||||||
expect(await res.findByText('123')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,36 +1,54 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Routes } from '../../../routes/route-names';
|
|
||||||
import { useExplorerAssetQuery } from './__generated__/Asset';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import Hash from '../hash';
|
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';
|
||||||
|
|
||||||
export type AssetLinkProps = Partial<ComponentProps<typeof Link>> & {
|
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
|
||||||
id: string;
|
assetId: string;
|
||||||
|
asDialog?: boolean;
|
||||||
|
showAssetSymbol?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 list. If the name does not come back
|
* with a link to the assets modal. If the name does not come back
|
||||||
* it will use the ID instead.
|
* it will use the ID instead.
|
||||||
*/
|
*/
|
||||||
const AssetLink = ({ id, ...props }: AssetLinkProps) => {
|
export const AssetLink = ({
|
||||||
const { data } = useExplorerAssetQuery({
|
assetId,
|
||||||
variables: { id },
|
asDialog,
|
||||||
});
|
showAssetSymbol = false,
|
||||||
|
...props
|
||||||
|
}: AssetLinkProps) => {
|
||||||
|
const { data: asset } = useAssetDataProvider(assetId);
|
||||||
|
|
||||||
let label: string = id;
|
const open = useAssetDetailsDialogStore((state) => state.open);
|
||||||
|
const navigate = useNavigate();
|
||||||
if (data?.asset?.name) {
|
const label = asset
|
||||||
label = data.asset.name;
|
? showAssetSymbol
|
||||||
}
|
? asset?.symbol
|
||||||
|
: asset?.name
|
||||||
|
: assetId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link className="underline" {...props} to={`/${Routes.ASSETS}#${id}`}>
|
<ButtonLink
|
||||||
|
data-testid="asset-link"
|
||||||
|
disabled={!asset}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (asDialog) {
|
||||||
|
open(assetId, e.target as HTMLElement);
|
||||||
|
} else {
|
||||||
|
navigate(`${Routes.ASSETS}/${asset?.id}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<Hash text={label} />
|
<Hash text={label} />
|
||||||
</Link>
|
</ButtonLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AssetLink;
|
|
||||||
|
|||||||
@@ -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 { default as AssetLink } from './asset-link/asset-link';
|
export * from './asset-link/asset-link';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ query ExplorerMarket($id: ID!) {
|
|||||||
market(id: $id) {
|
market(id: $id) {
|
||||||
id
|
id
|
||||||
decimalPlaces
|
decimalPlaces
|
||||||
|
positionDecimalPlaces
|
||||||
tradableInstrument {
|
tradableInstrument {
|
||||||
instrument {
|
instrument {
|
||||||
name
|
name
|
||||||
|
|||||||
+2
-1
@@ -8,7 +8,7 @@ export type ExplorerMarketQueryVariables = Types.Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
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 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 const ExplorerMarketDocument = gql`
|
export const ExplorerMarketDocument = gql`
|
||||||
@@ -16,6 +16,7 @@ export const ExplorerMarketDocument = gql`
|
|||||||
market(id: $id) {
|
market(id: $id) {
|
||||||
id
|
id
|
||||||
decimalPlaces
|
decimalPlaces
|
||||||
|
positionDecimalPlaces
|
||||||
tradableInstrument {
|
tradableInstrument {
|
||||||
instrument {
|
instrument {
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ 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: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import Hash from '../hash';
|
import Hash from '../hash';
|
||||||
|
|
||||||
export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
|
export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
@@ -48,7 +48,7 @@ const MarketLink = ({
|
|||||||
<Link
|
<Link
|
||||||
className="underline"
|
className="underline"
|
||||||
{...props}
|
{...props}
|
||||||
to={`/${Routes.MARKETS}#${id}`}
|
to={`/${Routes.MARKETS}/${id}`}
|
||||||
title={id}
|
title={id}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@@ -56,7 +56,7 @@ const MarketLink = ({
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
|
<Link className="underline" {...props} to={`/${Routes.MARKETS}/${id}`}>
|
||||||
<Hash text={id} />
|
<Hash text={id} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,19 +3,47 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import Hash from '../hash';
|
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, ...props }: PartyLinkProps) => {
|
const PartyLink = ({ id, truncate = false, ...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={id} />
|
<Hash text={truncate ? truncateMiddle(id) : id} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { ENV } from '../../../config/env';
|
|||||||
import Hash from '../hash';
|
import Hash from '../hash';
|
||||||
export type ProposalLinkProps = {
|
export type ProposalLinkProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
text?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a proposal ID, generates an external link over to
|
* Given a proposal ID, generates an external link over to
|
||||||
* the Governance page for more information
|
* the Governance page for more information
|
||||||
*/
|
*/
|
||||||
const ProposalLink = ({ id }: ProposalLinkProps) => {
|
const ProposalLink = ({ id, text }: ProposalLinkProps) => {
|
||||||
const { data } = useExplorerProposalQuery({
|
const { data } = useExplorerProposalQuery({
|
||||||
variables: { id },
|
variables: { id },
|
||||||
});
|
});
|
||||||
@@ -20,7 +21,7 @@ const ProposalLink = ({ id }: ProposalLinkProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ExternalLink href={`${base}/proposals/${id}`}>
|
<ExternalLink href={`${base}/proposals/${id}`}>
|
||||||
<Hash text={label} />
|
{text ? text : <Hash text={label} />}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { AppRouter } from '../../routes';
|
|
||||||
|
|
||||||
export const Main = () => {
|
|
||||||
return (
|
|
||||||
<main className="p-4 overflow-scroll">
|
|
||||||
<AppRouter />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import type { MarketInfoWithData } from '@vegaprotocol/market-info';
|
||||||
|
import { LiquidityInfoPanel } from '@vegaprotocol/market-info';
|
||||||
|
import { LiquidityMonitoringParametersInfoPanel } from '@vegaprotocol/market-info';
|
||||||
|
import {
|
||||||
|
InstrumentInfoPanel,
|
||||||
|
KeyDetailsInfoPanel,
|
||||||
|
LiquidityPriceRangeInfoPanel,
|
||||||
|
MetadataInfoPanel,
|
||||||
|
OracleInfoPanel,
|
||||||
|
RiskFactorsInfoPanel,
|
||||||
|
RiskModelInfoPanel,
|
||||||
|
RiskParametersInfoPanel,
|
||||||
|
SettlementAssetInfoPanel,
|
||||||
|
} from '@vegaprotocol/market-info';
|
||||||
|
import { MarketInfoTable } from '@vegaprotocol/market-info';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||||
|
if (!market) return null;
|
||||||
|
|
||||||
|
const panels = [
|
||||||
|
{
|
||||||
|
title: t('Key details'),
|
||||||
|
content: <KeyDetailsInfoPanel noBorder={false} market={market} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Instrument'),
|
||||||
|
content: <InstrumentInfoPanel noBorder={false} market={market} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Settlement asset'),
|
||||||
|
content: <SettlementAssetInfoPanel market={market} noBorder={false} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Metadata'),
|
||||||
|
content: <MetadataInfoPanel noBorder={false} market={market} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Risk model'),
|
||||||
|
content: <RiskModelInfoPanel noBorder={false} market={market} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Risk parameters'),
|
||||||
|
content: <RiskParametersInfoPanel noBorder={false} market={market} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Risk factors'),
|
||||||
|
content: <RiskFactorsInfoPanel noBorder={false} market={market} />,
|
||||||
|
},
|
||||||
|
...(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={
|
||||||
|
market.tradableInstrument.instrument.product.settlementAsset
|
||||||
|
.decimals
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
title: t('Liquidity monitoring parameters'),
|
||||||
|
content: (
|
||||||
|
<LiquidityMonitoringParametersInfoPanel
|
||||||
|
noBorder={false}
|
||||||
|
market={market}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Liquidity'),
|
||||||
|
content: <LiquidityInfoPanel market={market} noBorder={false} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Liquidity price range'),
|
||||||
|
content: (
|
||||||
|
<LiquidityPriceRangeInfoPanel market={market} noBorder={false} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Oracle'),
|
||||||
|
content: (
|
||||||
|
<OracleInfoPanel noBorder={false} market={market}>
|
||||||
|
<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>
|
||||||
|
</OracleInfoPanel>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{panels.map((p) => (
|
||||||
|
<div key={p.title} className="mb-3">
|
||||||
|
<h2 className="font-alpha calt text-xl">{p.title}</h2>
|
||||||
|
{p.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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': hasChildren,
|
'text-xl pl-4 border-l-4 font-alpha calt': 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-400 mb-2 last:mb-0 dark:text-vega-dark-400 break-all">
|
<code className="text-vega-light-100 mb-2 last:mb-0 dark:text-vega-dark-100 break-all">
|
||||||
{JSON.stringify(value, null, ' ')}
|
{JSON.stringify(value, null, ' ')}
|
||||||
</code>
|
</code>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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, 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, positionDecimalPlaces: 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, 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, positionDecimalPlaces: 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,6 +35,7 @@ export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
|
|||||||
market {
|
market {
|
||||||
id
|
id
|
||||||
decimalPlaces
|
decimalPlaces
|
||||||
|
positionDecimalPlaces
|
||||||
tradableInstrument {
|
tradableInstrument {
|
||||||
instrument {
|
instrument {
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ 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: {
|
||||||
@@ -88,6 +89,7 @@ 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/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
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,9 +1,10 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
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;
|
||||||
@@ -12,7 +13,7 @@ export interface DeterministicOrderDetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const wrapperClasses =
|
export const wrapperClasses =
|
||||||
'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';
|
'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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the *current* details for an order
|
* This component renders the *current* details for an order
|
||||||
@@ -41,7 +42,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-gray-500 mb-12">
|
<p className="text-vega-light-400 mb-12">
|
||||||
{t('No order created from this transaction')}
|
{t('No order created from this transaction')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,7 +91,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">
|
||||||
{o.size}
|
<SizeInMarket size={o.size} marketId={o.market.id} />
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -100,7 +101,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">
|
||||||
{o.remaining}
|
<SizeInMarket size={o.remaining} marketId={o.market.id} />
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
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';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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,6 +38,7 @@ 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,6 +1,7 @@
|
|||||||
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';
|
||||||
@@ -41,7 +42,8 @@ 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>
|
||||||
<span>{order.size}</span> <i>@</i>
|
<SizeInMarket marketId={order.market.id} size={order.size} />
|
||||||
|
<i>@</i>
|
||||||
<PriceInMarket marketId={order.market.id} price={order.price} />
|
<PriceInMarket marketId={order.market.id} price={order.price} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ 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: {
|
||||||
@@ -111,13 +112,14 @@ 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();
|
||||||
expect(res.getByText('10')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Initially renders price alone
|
// Initially renders price and size unformatted
|
||||||
expect(res.getByText('333')).toBeInTheDocument();
|
expect(res.getByText('333')).toBeInTheDocument();
|
||||||
|
expect(res.getByText('10')).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,6 +2,7 @@ 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'];
|
||||||
@@ -29,7 +30,12 @@ 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>
|
||||||
<span>{order.size}</span> <i className="text-xs">@</i>
|
{order.size ? (
|
||||||
|
<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';
|
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha calt';
|
||||||
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-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
|
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
|
||||||
<Icon name="duplicate" className="" />
|
<Icon name="duplicate" className="" />
|
||||||
</button>
|
</button>
|
||||||
</CopyWithTooltip>
|
</CopyWithTooltip>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './page-actions';
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
export const PageActions = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className="flex flex-row items-start gap-1" {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import type { HTMLAttributes, ReactNode } from 'react';
|
||||||
|
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||||
|
import { RouteTitle } from '../route-title';
|
||||||
|
import { PageActions } from './page-actions';
|
||||||
|
|
||||||
|
type PageTitleProps = {
|
||||||
|
/**
|
||||||
|
* The page title
|
||||||
|
* (also sets the document title unless overwritten by
|
||||||
|
* `documentTitle` property)
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* The react node that consists of CTA buttons, links, etc.
|
||||||
|
*/
|
||||||
|
actions?: ReactNode;
|
||||||
|
/**
|
||||||
|
* Overwrites the document title
|
||||||
|
*/
|
||||||
|
documentTitle?: Parameters<typeof useDocumentTitle>[0];
|
||||||
|
} & Omit<HTMLAttributes<HTMLHeadingElement>, 'children'>;
|
||||||
|
|
||||||
|
export const PageTitle = ({
|
||||||
|
title,
|
||||||
|
actions,
|
||||||
|
documentTitle,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PageTitleProps) => {
|
||||||
|
useDocumentTitle(
|
||||||
|
documentTitle && documentTitle.length > 0 ? documentTitle : [title]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex flex-col md:flex-row gap-1 justify-between content-start mb-8"
|
||||||
|
data-testid="page-title"
|
||||||
|
>
|
||||||
|
<RouteTitle className={classNames('mb-1', className)} {...props}>
|
||||||
|
{title}
|
||||||
|
</RouteTitle>
|
||||||
|
{actions && <PageActions>{actions}</PageActions>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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-zinc-200 dark:border-zinc-800 rounded-md p-5 mb-5',
|
'border border-vega-light-150 dark:border-vega-dark-150 rounded-md p-5 mb-5',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ 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,4 +1,5 @@
|
|||||||
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
|
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';
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import React from 'react';
|
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||||
import { StatusMessage } from '../status-message';
|
import { StatusMessage } from '../status-message';
|
||||||
|
|
||||||
interface RenderFetchedProps {
|
interface RenderFetchedProps {
|
||||||
@@ -8,6 +8,7 @@ interface RenderFetchedProps {
|
|||||||
loading: boolean | undefined;
|
loading: boolean | undefined;
|
||||||
className?: string;
|
className?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
refetch?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RenderFetched = ({
|
export const RenderFetched = ({
|
||||||
@@ -16,6 +17,7 @@ export const RenderFetched = ({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
errorMessage = t('Error retrieving data'),
|
errorMessage = t('Error retrieving data'),
|
||||||
|
refetch,
|
||||||
}: RenderFetchedProps) => {
|
}: RenderFetchedProps) => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -24,7 +26,20 @@ export const RenderFetched = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <StatusMessage className={className}>{errorMessage}</StatusMessage>;
|
return (
|
||||||
|
<>
|
||||||
|
<StatusMessage className={className}>{errorMessage}</StatusMessage>
|
||||||
|
{refetch && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Try again')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const RouteTitle = ({
|
|||||||
...props
|
...props
|
||||||
}: RouteTitleProps) => {
|
}: RouteTitleProps) => {
|
||||||
const classes = classnames(
|
const classes = classnames(
|
||||||
'font-alpha',
|
'font-alpha calt',
|
||||||
'text-4xl',
|
'text-4xl',
|
||||||
'uppercase',
|
'uppercase',
|
||||||
'mb-8',
|
'mb-8',
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface RouteErrorBoundaryProps {
|
|
||||||
children: React.ReactElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RouteErrorBoundary extends React.Component<
|
|
||||||
RouteErrorBoundaryProps,
|
|
||||||
{ hasError: boolean }
|
|
||||||
> {
|
|
||||||
constructor(props: RouteErrorBoundaryProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = { hasError: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError() {
|
|
||||||
return { hasError: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
override componentDidCatch(error: Error) {
|
|
||||||
console.log(`Error caught in App error boundary ${error.message}`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
override render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
return <h1>{t('Something went wrong')}</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,189 +1,70 @@
|
|||||||
import {
|
import {
|
||||||
detectTypeByFetching,
|
determineType,
|
||||||
detectTypeFromQuery,
|
|
||||||
getSearchType,
|
|
||||||
isBlock,
|
isBlock,
|
||||||
isHexadecimal,
|
|
||||||
isNetworkParty,
|
isNetworkParty,
|
||||||
isNonHex,
|
|
||||||
SearchTypes,
|
SearchTypes,
|
||||||
toHex,
|
isHash,
|
||||||
toNonHex,
|
|
||||||
} from './detect-search';
|
} from './detect-search';
|
||||||
import { DATA_SOURCES } from '../../config';
|
|
||||||
|
|
||||||
global.fetch = jest.fn();
|
global.fetch = jest.fn();
|
||||||
|
|
||||||
describe('Detect Search', () => {
|
describe('Detect Search', () => {
|
||||||
it("should detect that it's a hexadecimal", () => {
|
it.each([
|
||||||
const expected = true;
|
['0000000000000000000000000000000000000000000000000000000000000000', true],
|
||||||
const testString =
|
['0000000000000000000000000000000000000000000000000000000000000001', true],
|
||||||
'0x073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
|
[
|
||||||
const actual = isHexadecimal(testString);
|
'LOOONG0000000000000000000000000000000000000000000000000000000000000000',
|
||||||
expect(actual).toBe(expected);
|
false,
|
||||||
});
|
],
|
||||||
|
['xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', false],
|
||||||
it("should detect that it's not hexadecimal", () => {
|
['something else', false],
|
||||||
const expected = true;
|
])("should detect that it's a hash", (input, expected) => {
|
||||||
const testString =
|
expect(isHash(input)).toBe(expected);
|
||||||
'073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
|
|
||||||
const actual = isNonHex(testString);
|
|
||||||
expect(actual).toBe(expected);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect that it's a network party", () => {
|
it("should detect that it's a network party", () => {
|
||||||
const expected = true;
|
expect(isNetworkParty('network')).toBe(true);
|
||||||
const testString = 'network';
|
expect(isNetworkParty('web')).toBe(false);
|
||||||
const actual = isNetworkParty(testString);
|
|
||||||
expect(actual).toBe(expected);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect that it's a block", () => {
|
it("should detect that it's a block", () => {
|
||||||
const expected = true;
|
expect(isBlock('123')).toBe(true);
|
||||||
const testString = '3188';
|
expect(isBlock('x123')).toBe(false);
|
||||||
const actual = isBlock(testString);
|
|
||||||
expect(actual).toBe(expected);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should convert from non-hex to hex', () => {
|
it.each([
|
||||||
const expected = '0x123';
|
[
|
||||||
const testString = '123';
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||||
const actual = toHex(testString);
|
SearchTypes.Transaction,
|
||||||
expect(actual).toBe(expected);
|
],
|
||||||
});
|
[
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||||
it('should convert from hex to non-hex', () => {
|
SearchTypes.Party,
|
||||||
const expected = '123';
|
],
|
||||||
const testString = '0x123';
|
['123', SearchTypes.Block],
|
||||||
const actual = toNonHex(testString);
|
['network', SearchTypes.Party],
|
||||||
expect(actual).toBe(expected);
|
['something else', SearchTypes.Unknown],
|
||||||
});
|
])(
|
||||||
|
"detectTypeByFetching should call fetch with non-hex query it's a transaction",
|
||||||
it("should detect type client side from query if it's a hexadecimal", () => {
|
async (input, type) => {
|
||||||
const expected = [SearchTypes.Party, SearchTypes.Transaction];
|
// @ts-ignore issue related to polyfill
|
||||||
const testString =
|
fetch.mockImplementation(
|
||||||
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
jest.fn(() =>
|
||||||
const actual = detectTypeFromQuery(testString);
|
Promise.resolve({
|
||||||
expect(actual).toStrictEqual(expected);
|
ok:
|
||||||
});
|
input ===
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||||
it("should detect type client side from query if it's a non hex", () => {
|
json: () =>
|
||||||
const expected = [SearchTypes.Party, SearchTypes.Transaction];
|
Promise.resolve({
|
||||||
const testString =
|
transaction: {
|
||||||
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
hash: input,
|
||||||
const actual = detectTypeFromQuery(testString);
|
},
|
||||||
expect(actual).toStrictEqual(expected);
|
}),
|
||||||
});
|
})
|
||||||
|
)
|
||||||
it("should detect type client side from query if it's a network party", () => {
|
);
|
||||||
const expected = [SearchTypes.Party];
|
const result = await determineType(input);
|
||||||
const testString = 'network';
|
expect(result).toBe(type);
|
||||||
const actual = detectTypeFromQuery(testString);
|
}
|
||||||
expect(actual).toStrictEqual(expected);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
it("should detect type client side from query if it's a block (number)", () => {
|
|
||||||
const expected = [SearchTypes.Block];
|
|
||||||
const testString = '23432';
|
|
||||||
const actual = detectTypeFromQuery(testString);
|
|
||||||
expect(actual).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detectTypeByFetching should call fetch with non-hex query it's a transaction", async () => {
|
|
||||||
const query = '0xabc';
|
|
||||||
const type = SearchTypes.Transaction;
|
|
||||||
// @ts-ignore issue related to polyfill
|
|
||||||
fetch.mockImplementation(
|
|
||||||
jest.fn(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
ok: true,
|
|
||||||
json: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
transaction: {
|
|
||||||
hash: query,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const result = await detectTypeByFetching(query);
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
|
||||||
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(query)}`
|
|
||||||
);
|
|
||||||
expect(result).toBe(type);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detectTypeByFetching should call fetch with non-hex query it's a party", async () => {
|
|
||||||
const query = 'abc';
|
|
||||||
const type = SearchTypes.Party;
|
|
||||||
// @ts-ignore issue related to polyfill
|
|
||||||
fetch.mockImplementation(
|
|
||||||
jest.fn(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
ok: false,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const result = await detectTypeByFetching(query);
|
|
||||||
expect(result).toBe(type);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getSearchType should return party from fetch response', async () => {
|
|
||||||
const query =
|
|
||||||
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
|
||||||
const expected = SearchTypes.Party;
|
|
||||||
// @ts-ignore issue related to polyfill
|
|
||||||
fetch.mockImplementation(
|
|
||||||
jest.fn(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
ok: false,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const result = await getSearchType(query);
|
|
||||||
expect(result).toBe(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getSearchType should return transaction from fetch response', async () => {
|
|
||||||
const query =
|
|
||||||
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
|
||||||
const expected = SearchTypes.Transaction;
|
|
||||||
// @ts-ignore issue related to polyfill
|
|
||||||
fetch.mockImplementation(
|
|
||||||
jest.fn(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
ok: true,
|
|
||||||
json: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
transaction: {
|
|
||||||
hash: query,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const result = await getSearchType(query);
|
|
||||||
expect(result).toBe(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getSearchType should return undefined from transaction response', async () => {
|
|
||||||
const query = 'u';
|
|
||||||
const expected = undefined;
|
|
||||||
const result = await getSearchType(query);
|
|
||||||
expect(result).toBe(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getSearchType should return block if query is number', async () => {
|
|
||||||
const query = '123';
|
|
||||||
const expected = SearchTypes.Block;
|
|
||||||
const result = await getSearchType(query);
|
|
||||||
expect(result).toBe(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getSearchType should return party if query is network', async () => {
|
|
||||||
const query = 'network';
|
|
||||||
const expected = SearchTypes.Party;
|
|
||||||
const result = await getSearchType(query);
|
|
||||||
expect(result).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { remove0x } from '@vegaprotocol/utils';
|
||||||
import { DATA_SOURCES } from '../../config';
|
import { DATA_SOURCES } from '../../config';
|
||||||
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
|
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
|
||||||
|
|
||||||
@@ -6,15 +7,20 @@ export enum SearchTypes {
|
|||||||
Party = 'party',
|
Party = 'party',
|
||||||
Block = 'block',
|
Block = 'block',
|
||||||
Order = 'order',
|
Order = 'order',
|
||||||
|
Unknown = 'unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TX_LENGTH = 64;
|
export const HASH_LENGTH = 64;
|
||||||
|
|
||||||
|
export const isHash = (value: string) =>
|
||||||
|
/[0-9a-fA-F]+/.test(remove0x(value)) &&
|
||||||
|
remove0x(value).length === HASH_LENGTH;
|
||||||
|
|
||||||
export const isHexadecimal = (search: string) =>
|
export const isHexadecimal = (search: string) =>
|
||||||
search.startsWith('0x') && search.length === 2 + TX_LENGTH;
|
search.startsWith('0x') && search.length === 2 + HASH_LENGTH;
|
||||||
|
|
||||||
export const isNonHex = (search: string) =>
|
export const isNonHex = (search: string) =>
|
||||||
!search.startsWith('0x') && search.length === TX_LENGTH;
|
!search.startsWith('0x') && search.length === HASH_LENGTH;
|
||||||
|
|
||||||
export const isBlock = (search: string) => !Number.isNaN(Number(search));
|
export const isBlock = (search: string) => !Number.isNaN(Number(search));
|
||||||
|
|
||||||
@@ -23,121 +29,44 @@ export const isNetworkParty = (search: string) => search === 'network';
|
|||||||
export const toHex = (query: string) =>
|
export const toHex = (query: string) =>
|
||||||
isHexadecimal(query) ? query : `0x${query}`;
|
isHexadecimal(query) ? query : `0x${query}`;
|
||||||
|
|
||||||
export const toNonHex = (query: string) =>
|
export const toNonHex = remove0x;
|
||||||
isNonHex(query) ? query : `${query.replace('0x', '')}`;
|
|
||||||
|
|
||||||
export const detectTypeFromQuery = (
|
/**
|
||||||
query: string
|
* Determine the type of the given query
|
||||||
): SearchTypes[] | undefined => {
|
*/
|
||||||
const i = query.toLowerCase();
|
export const determineType = async (query: string): Promise<SearchTypes> => {
|
||||||
|
const value = query.toLowerCase();
|
||||||
if (isHexadecimal(i) || isNonHex(i)) {
|
if (isHash(value)) {
|
||||||
return [SearchTypes.Party, SearchTypes.Transaction];
|
// it can be either `SearchTypes.Party` or `SearchTypes.Transaction`
|
||||||
} else if (isNetworkParty(i)) {
|
if (await isTransactionHash(value)) {
|
||||||
return [SearchTypes.Party];
|
return SearchTypes.Transaction;
|
||||||
} else if (isBlock(i)) {
|
} else {
|
||||||
return [SearchTypes.Block];
|
return SearchTypes.Party;
|
||||||
|
}
|
||||||
|
} else if (isNetworkParty(value)) {
|
||||||
|
return SearchTypes.Party;
|
||||||
|
} else if (isBlock(value)) {
|
||||||
|
return SearchTypes.Block;
|
||||||
}
|
}
|
||||||
|
return SearchTypes.Unknown;
|
||||||
return undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const detectTypeByFetching = async (
|
/**
|
||||||
query: string
|
* Checks if given input is a transaction hash by querying the transactions
|
||||||
): Promise<SearchTypes | undefined> => {
|
* endpoint
|
||||||
const hash = toNonHex(query);
|
*/
|
||||||
|
export const isTransactionHash = async (input: string): Promise<boolean> => {
|
||||||
|
const hash = remove0x(input);
|
||||||
const request = await fetch(
|
const request = await fetch(
|
||||||
`${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
|
`${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (request?.ok) {
|
if (request?.ok) {
|
||||||
const body: BlockExplorerTransaction = await request.json();
|
const body: BlockExplorerTransaction = await request.json();
|
||||||
|
|
||||||
if (body?.transaction) {
|
if (body?.transaction) {
|
||||||
return SearchTypes.Transaction;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SearchTypes.Party;
|
return false;
|
||||||
};
|
|
||||||
|
|
||||||
// Code commented out because the current solution to detect a hex is temporary (by process of elimination)
|
|
||||||
// export const detectTypeByFetching = async (
|
|
||||||
// query: string,
|
|
||||||
// type: SearchTypes
|
|
||||||
// ): Promise<SearchTypes | undefined> => {
|
|
||||||
// const TYPES = [SearchTypes.Party, SearchTypes.Transaction];
|
|
||||||
//
|
|
||||||
// if (!TYPES.includes(type)) {
|
|
||||||
// throw new Error('Search type provided not recognised');
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (type === SearchTypes.Transaction) {
|
|
||||||
// const hash = toNonHex(query);
|
|
||||||
// const request = await fetch(
|
|
||||||
// `${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// if (request?.ok) {
|
|
||||||
// const body: BlockExplorerTransaction = await request.json();
|
|
||||||
//
|
|
||||||
// if (body?.transaction) {
|
|
||||||
// return SearchTypes.Transaction;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } else if (type === SearchTypes.Party) {
|
|
||||||
// const party = toNonHex(query);
|
|
||||||
//
|
|
||||||
// const request = await fetch(
|
|
||||||
// `${DATA_SOURCES.blockExplorerUrl}/transactions?limit=1&filters[tx.submitter]=${party}`
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// if (request.ok) {
|
|
||||||
// const body: BlockExplorerTransactions = await request.json();
|
|
||||||
//
|
|
||||||
// if (body?.transactions?.length) {
|
|
||||||
// return SearchTypes.Party;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return undefined;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export const getSearchType = async (
|
|
||||||
// query: string
|
|
||||||
// ): Promise<SearchTypes | undefined> => {
|
|
||||||
// const searchTypes = detectTypeFromQuery(query);
|
|
||||||
// const hasResults = searchTypes?.length;
|
|
||||||
//
|
|
||||||
// if (hasResults) {
|
|
||||||
// if (hasResults > 1) {
|
|
||||||
// const promises = searchTypes.map((type) =>
|
|
||||||
// detectTypeByFetching(query, type)
|
|
||||||
// );
|
|
||||||
// const results = await Promise.all(promises);
|
|
||||||
// return results.find((result) => result !== undefined);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return searchTypes[0];
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return undefined;
|
|
||||||
// };
|
|
||||||
|
|
||||||
export const getSearchType = async (
|
|
||||||
query: string
|
|
||||||
): Promise<SearchTypes | undefined> => {
|
|
||||||
const searchTypes = detectTypeFromQuery(query);
|
|
||||||
const hasResults = searchTypes?.length;
|
|
||||||
|
|
||||||
if (hasResults) {
|
|
||||||
if (hasResults > 1) {
|
|
||||||
return await detectTypeByFetching(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchTypes[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user