Compare commits
364
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b800943a09 | ||
|
|
a0187564cc | ||
|
|
369a7fbf6d | ||
|
|
3c71a86b48 | ||
|
|
b381f16ace | ||
|
|
fc6ce9e99b | ||
|
|
f6fc4df1c5 | ||
|
|
fc8f12d6fc | ||
|
|
26f4c1c983 | ||
|
|
efd632f5c6 | ||
|
|
ce3da1762b | ||
|
|
5040fbfd07 | ||
|
|
74f2cfa4a5 | ||
|
|
8c8fe6878a | ||
|
|
6e0577aee4 | ||
|
|
87b41a30d8 | ||
|
|
0850f31855 | ||
|
|
43aff8e359 | ||
|
|
6e9e7c2a5c | ||
|
|
f054f4c516 | ||
|
|
f382078ee6 | ||
|
|
ebc058bcbe | ||
|
|
d3df339696 | ||
|
|
a31008ea26 | ||
|
|
5e93e98f07 | ||
|
|
bf3ff8fb6f | ||
|
|
16538ca3a3 | ||
|
|
2fa00dacaa | ||
|
|
45b7c2ad4d | ||
|
|
ed2c82487d | ||
|
|
55143331f1 | ||
|
|
cafb3b1c57 | ||
|
|
019b2d7d89 | ||
|
|
de0dc4af8e | ||
|
|
6765f4d03a | ||
|
|
7bea90e4d1 | ||
|
|
ac4762f9d7 | ||
|
|
1195309b61 | ||
|
|
8ce77e23ba | ||
|
|
5cae2ce400 | ||
|
|
b8c9f9772e | ||
|
|
c3e39b6e15 | ||
|
|
77a391448b | ||
|
|
484d7888cf | ||
|
|
187f1929fe | ||
|
|
0a4333645b | ||
|
|
310506e5af | ||
|
|
0b33ed4299 | ||
|
|
13f2e51798 | ||
|
|
fc047feeee | ||
|
|
005455c870 | ||
|
|
2c2bc391e8 | ||
|
|
445a085190 | ||
|
|
bb1b236cdf | ||
|
|
da66b7b20d | ||
|
|
ec12811f72 | ||
|
|
180de8cf25 | ||
|
|
df88e77cdf | ||
|
|
9441aee8cf | ||
|
|
2353812834 | ||
|
|
bba2b3c177 | ||
|
|
ae57bd92f4 | ||
|
|
115b642140 | ||
|
|
ce3da97a8a | ||
|
|
cabd99d3ef | ||
|
|
597e07608f | ||
|
|
e451dc54b3 | ||
|
|
9703c3b7a6 | ||
|
|
c22b6f3ce9 | ||
|
|
c740c11eaa | ||
|
|
d1fabb31a3 | ||
|
|
94f045a60b | ||
|
|
1af375bab8 | ||
|
|
5a48ba4a33 | ||
|
|
d1fd9184ce | ||
|
|
8f29824f9e | ||
|
|
fc3c73ad24 | ||
|
|
1027827576 | ||
|
|
0089920d4c | ||
|
|
35896a9cf5 | ||
|
|
5a90ebe12e | ||
|
|
6be117086a | ||
|
|
9c6dd9f254 | ||
|
|
a675576a52 | ||
|
|
98d248e02e | ||
|
|
c8d55c6293 | ||
|
|
3467feb3e5 | ||
|
|
bdf1663709 | ||
|
|
d775573543 | ||
|
|
c0fdc8d570 | ||
|
|
a9ebb36b9b | ||
|
|
f6c0082f2d | ||
|
|
931ad75cc9 | ||
|
|
f25a58a3c3 | ||
|
|
37340b4dc3 | ||
|
|
17ba3eb351 | ||
|
|
8f966a4ba6 | ||
|
|
cc7f403718 | ||
|
|
9184c2374c | ||
|
|
37247a504a | ||
|
|
2e7a6a6458 | ||
|
|
bf6c13f523 | ||
|
|
a6f3a08d2f | ||
|
|
f13e456a29 | ||
|
|
5cab040189 | ||
|
|
e47b868c53 | ||
|
|
b6286cd0f3 | ||
|
|
f66e976b66 | ||
|
|
866e7232db | ||
|
|
5a8ff90890 | ||
|
|
7b8f654906 | ||
|
|
d6f39049bd | ||
|
|
1d06be8f4e | ||
|
|
5eba8fe28f | ||
|
|
2ba0e9a1b2 | ||
|
|
43d3754c64 | ||
|
|
aae5c44fa4 | ||
|
|
878bed9c7a | ||
|
|
6f9f432c90 | ||
|
|
3f01b93159 | ||
|
|
473c244d7b | ||
|
|
d6a32f5090 | ||
|
|
71540a90fb | ||
|
|
cefdbe6a3c | ||
|
|
3928dd5c0e | ||
|
|
1521bab4c4 | ||
|
|
b3036d520f | ||
|
|
429a5a23d3 | ||
|
|
5b02fd5d54 | ||
|
|
b8309a76e7 | ||
|
|
e8ae085c06 | ||
|
|
fdcd24847c | ||
|
|
e054db39b5 | ||
|
|
a01e48d508 | ||
|
|
0d96c487d9 | ||
|
|
011e4e4cec | ||
|
|
d6e2432955 | ||
|
|
18c034b910 | ||
|
|
849cbb43c4 | ||
|
|
cd34cb181f | ||
|
|
97b3f4decf | ||
|
|
0465fb8ca9 | ||
|
|
22af89e78c | ||
|
|
adfd2f41b6 | ||
|
|
d9b1aa6da3 | ||
|
|
205f369cdc | ||
|
|
9f63fde123 | ||
|
|
f2c70e6da4 | ||
|
|
e8ff1b0b60 | ||
|
|
f5989b0022 | ||
|
|
653ca034f2 | ||
|
|
ef25552311 | ||
|
|
0a8439d125 | ||
|
|
702ea0c6b4 | ||
|
|
0f2b86770a | ||
|
|
cd6d350047 | ||
|
|
c5a06c57ac | ||
|
|
5bb31fa38d | ||
|
|
fd8f38803c | ||
|
|
a22f8f0ef1 | ||
|
|
c2d5c71f35 | ||
|
|
f88c35648e | ||
|
|
348c61530e | ||
|
|
f364dabe2f | ||
|
|
1ae1fdff5e | ||
|
|
3ed615c36e | ||
|
|
6b50816234 | ||
|
|
f5e67a0c2d | ||
|
|
e4d4646878 | ||
|
|
bbd1536c18 | ||
|
|
f8444c0c88 | ||
|
|
b1adf776c0 | ||
|
|
ad898ab0e8 | ||
|
|
5b0fffc5cb | ||
|
|
a9dbc0f6eb | ||
|
|
a71786817f | ||
|
|
9ee0cb7b65 | ||
|
|
8c4aa41240 | ||
|
|
91ac977704 | ||
|
|
1190033776 | ||
|
|
b99e2235b4 | ||
|
|
002bc4f188 | ||
|
|
0c2af8c75f | ||
|
|
a305c5b8a9 | ||
|
|
4c3ceb94f3 | ||
|
|
2215864d0c | ||
|
|
589785ce09 | ||
|
|
babd99088d | ||
|
|
d155909fe4 | ||
|
|
1d3298cdc9 | ||
|
|
c2baaa2dd9 | ||
|
|
e2c4c4a841 | ||
|
|
7a89dc984d | ||
|
|
19580fb9ce | ||
|
|
0721099d7d | ||
|
|
9ee2810c51 | ||
|
|
9f8f99b50b | ||
|
|
3208f762fd | ||
|
|
7a06c946d1 | ||
|
|
70e172045d | ||
|
|
06c540666e | ||
|
|
f81503b8f7 | ||
|
|
1699e33cbd | ||
|
|
5280b79927 | ||
|
|
34526d527e | ||
|
|
7b9aae700d | ||
|
|
e9cb2ad217 | ||
|
|
6dd4536fdd | ||
|
|
522eb521b9 | ||
|
|
3aa0e974c2 | ||
|
|
a0eb381ab1 | ||
|
|
a51ab4d98d | ||
|
|
67643afd74 | ||
|
|
145539e0c0 | ||
|
|
48109b584c | ||
|
|
a4512e6064 | ||
|
|
45adac2932 | ||
|
|
fff8fdd78b | ||
|
|
079d8bc48d | ||
|
|
9658363b56 | ||
|
|
e24289330e | ||
|
|
0fbd2bfa72 | ||
|
|
43ef4fad6f | ||
|
|
828dcf6e93 | ||
|
|
4a583ccf09 | ||
|
|
8bcad28f50 | ||
|
|
b375681076 | ||
|
|
8a2db407cf | ||
|
|
491844d3fd | ||
|
|
f1f51c5ecd | ||
|
|
9bc78f8ccd | ||
|
|
a03031a283 | ||
|
|
b499c293e3 | ||
|
|
65747e8d26 | ||
|
|
9a84eda21b | ||
|
|
fba98f2fd0 | ||
|
|
3e32d0f396 | ||
|
|
b75d189054 | ||
|
|
afa627864c | ||
|
|
b13a866b2a | ||
|
|
c65f07eee6 | ||
|
|
5bd18cc127 | ||
|
|
ff3519279d | ||
|
|
fd338c7400 | ||
|
|
0db5d0ce87 | ||
|
|
42c316c8e1 | ||
|
|
9e2474d39a | ||
|
|
2533e5ec44 | ||
|
|
22a43249ea | ||
|
|
b40ee0caf5 | ||
|
|
95aca70434 | ||
|
|
5e13266250 | ||
|
|
5c0588887c | ||
|
|
089a7daac7 | ||
|
|
02b6251b1a | ||
|
|
9ce8907861 | ||
|
|
3be9126906 | ||
|
|
77316092d1 | ||
|
|
fca229e62a | ||
|
|
162b167860 | ||
|
|
a4149cc55e | ||
|
|
4161a74eaf | ||
|
|
5008e57a56 | ||
|
|
89031bf826 | ||
|
|
61501d4d14 | ||
|
|
f8f3ce7df9 | ||
|
|
f7aab5a78a | ||
|
|
2904975fd3 | ||
|
|
4f55c946f0 | ||
|
|
de637fee13 | ||
|
|
69c6db8ec2 | ||
|
|
8dc465c1d1 | ||
|
|
0be934edca | ||
|
|
653688c1dd | ||
|
|
34400be18a | ||
|
|
d4a0a4a42c | ||
|
|
fabf643ce7 | ||
|
|
45e210092c | ||
|
|
1dc3b3fd05 | ||
|
|
0a80e3e39d | ||
|
|
7b8d9dc94b | ||
|
|
c0320c2468 | ||
|
|
11173d6614 | ||
|
|
e3324331e2 | ||
|
|
0a6e3cb6f5 | ||
|
|
68ede90609 | ||
|
|
09778a5d3f | ||
|
|
59993e3ea6 | ||
|
|
54161a6dba | ||
|
|
26f524ea07 | ||
|
|
db13e6f21d | ||
|
|
4a679813ba | ||
|
|
a4279d5b5e | ||
|
|
1a274a67c3 | ||
|
|
1e9ff31fd7 | ||
|
|
aa2b755b93 | ||
|
|
4a7296312a | ||
|
|
1418a2259f | ||
|
|
7e5ef0157a | ||
|
|
0cf764f11e | ||
|
|
1c626aeeb2 | ||
|
|
c4cec47b0b | ||
|
|
1a67c41bb4 | ||
|
|
fc66240390 | ||
|
|
fc0da98683 | ||
|
|
4d0dafb113 | ||
|
|
eda431be9e | ||
|
|
4ff1749e54 | ||
|
|
e7a108fd7a | ||
|
|
5d3f2ea7d8 | ||
|
|
1ab27574ca | ||
|
|
0ea3c43e78 | ||
|
|
9e183568b0 | ||
|
|
69fd5b26b6 | ||
|
|
2d5b926609 | ||
|
|
98a2ba79a8 | ||
|
|
0e6e986937 | ||
|
|
15d6ea0c05 | ||
|
|
0358ce6293 | ||
|
|
0791be4636 | ||
|
|
07442848ee | ||
|
|
d5f0bd419e | ||
|
|
2c7934a8c7 | ||
|
|
fae61c28ca | ||
|
|
fcd4541625 | ||
|
|
8e0a92fdec | ||
|
|
6e7109ed87 | ||
|
|
f2c698ce06 | ||
|
|
ad9a3a3400 | ||
|
|
efeac49d58 | ||
|
|
2d430710ab | ||
|
|
bca1a98985 | ||
|
|
8a080d3279 | ||
|
|
f121836b4e | ||
|
|
b641c82ad8 | ||
|
|
bf73559c30 | ||
|
|
653cec2592 | ||
|
|
579c884a5a | ||
|
|
b2279c7e47 | ||
|
|
a8c17b6807 | ||
|
|
b4c7dc6f59 | ||
|
|
d44392bebf | ||
|
|
bd679957e2 | ||
|
|
e30d48555e | ||
|
|
b4b2416780 | ||
|
|
dc7832ac81 | ||
|
|
91207d31ee | ||
|
|
58efb460a6 | ||
|
|
044f98777b | ||
|
|
00fdc5e81a | ||
|
|
adfad3bafb | ||
|
|
e6ae88905c | ||
|
|
3953ed9953 | ||
|
|
b48ab58e6a | ||
|
|
e44cf1ff53 | ||
|
|
bded1d32ba | ||
|
|
76ddf45f4c | ||
|
|
6a2c9004d8 | ||
|
|
77ee88b43e | ||
|
|
e4958ce3c0 | ||
|
|
ecedb879f8 | ||
|
|
89700803d6 | ||
|
|
65c9fa5df2 | ||
|
|
02399c40fc |
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
+11
-6
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"ignorePatterns": ["**/*"],
|
"ignorePatterns": ["**/*"],
|
||||||
"plugins": ["@nrwl/nx", "eslint-plugin-unicorn", "jsx-a11y", "jest"],
|
"plugins": ["@nx", "eslint-plugin-unicorn", "jsx-a11y", "jest"],
|
||||||
"settings": {
|
"settings": {
|
||||||
"jsx-a11y": {
|
"jsx-a11y": {
|
||||||
"components": {
|
"components": {
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
"extends": ["plugin:jsx-a11y/strict"],
|
"extends": ["plugin:jsx-a11y/strict"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@nrwl/nx/enforce-module-boundaries": [
|
"@nx/enforce-module-boundaries": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
"enforceBuildableLibDependency": true,
|
"enforceBuildableLibDependency": true,
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx"],
|
"files": ["*.ts", "*.tsx"],
|
||||||
"extends": ["plugin:@nrwl/nx/typescript"],
|
"extends": ["plugin:@nx/typescript"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/ban-ts-comment": [
|
"@typescript-eslint/ban-ts-comment": [
|
||||||
"error",
|
"error",
|
||||||
@@ -80,14 +80,19 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["*.spec.ts", "*.spec.tsx"],
|
"files": ["*.spec.ts", "*.spec.tsx"],
|
||||||
"extends": ["plugin:@nrwl/nx/typescript", "plugin:jest/recommended"],
|
"extends": ["plugin:@nx/typescript", "plugin:jest/recommended"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"jest/consistent-test-it": ["error", { "fn": "it" }]
|
"jest/consistent-test-it": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"fn": "it"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["*.js", "*.jsx"],
|
"files": ["*.js", "*.jsx"],
|
||||||
"extends": ["plugin:@nrwl/nx/javascript"],
|
"extends": ["plugin:@nx/javascript"],
|
||||||
"rules": {}
|
"rules": {}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ What we need to achieve and who for
|
|||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
- [ ] What do we need to do first
|
- [ ]
|
||||||
- [ ] and then what?
|
- [ ]
|
||||||
- [ ] Etc.
|
|
||||||
|
|
||||||
## Additional details / background info
|
## Additional details / background info
|
||||||
|
|||||||
@@ -22,11 +22,14 @@ So that
|
|||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
- [ ] Explore and sketch
|
- [ ] UX (if needed)
|
||||||
|
- [ ] Design (if needed)
|
||||||
- [ ] Team and stakeholder review
|
- [ ] Team and stakeholder review
|
||||||
- [ ] Visual Design
|
- [ ] Specs reviewed and created or adjusted
|
||||||
- [ ] Team review
|
- [ ] Implementation
|
||||||
- [ ] Etc.
|
- [ ] Testing (unit and/or e2e)
|
||||||
|
- [ ] Code review
|
||||||
|
- [ ] QA review
|
||||||
|
|
||||||
## Sketch
|
## Sketch
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
name: After Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
after-release:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Log in to the Container registry (ghcr)
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to the Container registry (docker hub)
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: resolve ipfs hashes for release
|
||||||
|
run: |
|
||||||
|
echo "Name: ${{ github.event.release.name }}"
|
||||||
|
echo "Description: ${{ github.event.release.body }}"
|
||||||
|
echo "Tag: ${{ github.event.release.tag_name }}"
|
||||||
|
commit="$(git rev-list -n 1 ${{ github.event.release.tag_name }})"
|
||||||
|
echo "Commit: $commit"
|
||||||
|
until docker pull vegaprotocol/trading:$commit; do
|
||||||
|
echo "Image not pushed yet, waiting 60 seconds"
|
||||||
|
sleep 60
|
||||||
|
done
|
||||||
|
docker run --rm vegaprotocol/trading:$commit cat /ipfs-hash > ipfs-hash
|
||||||
|
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
|
||||||
|
tar -xzf kubo.tgz
|
||||||
|
export PATH="$PATH:$PWD/kubo"
|
||||||
|
which ipfs
|
||||||
|
echo IPFS_V0=$(cat ipfs-hash) >> $GITHUB_ENV
|
||||||
|
echo IPFS_V1=$(ipfs cid format -v 1 -b base32 $(cat ipfs-hash)) >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Edit Release
|
||||||
|
uses: irongut/EditRelease@v1.2.0
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
id: ${{ github.event.release.id }}
|
||||||
|
body: |
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
# Deployments
|
||||||
|
* https://explorer.vega.xyz
|
||||||
|
* https://governance.vega.xyz
|
||||||
|
|
||||||
|
# IPFS releases
|
||||||
|
The IPFS hash of this release of the Trading app is:
|
||||||
|
|
||||||
|
CIDv0: ${{ env.IPFS_V0 }}
|
||||||
|
CIDv1: ${{ env.IPFS_V1 }}
|
||||||
|
|
||||||
|
You can always access the latest IPFS release by visiting [console.vega.xyz](https://console.vega.xyz).
|
||||||
|
|
||||||
|
You can also access Trading directly from an IPFS gateway.
|
||||||
|
BEWARE: The Trading interface uses [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. You should always use an IPFS gateway that enforces [origin separation](https://ipfs.github.io/public-gateway-checker/).
|
||||||
|
|
||||||
|
Your settings are not remembered across different URLs.
|
||||||
|
|
||||||
|
IPFS gateways:
|
||||||
|
|
||||||
|
https://${{ env.IPFS_V1 }}.ipfs.dweb.link/
|
||||||
|
https://${{ env.IPFS_V1 }}.ipfs.cf-ipfs.com/
|
||||||
|
ipfs://${{ env.IPFS_V0 }}/
|
||||||
@@ -5,7 +5,10 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- release/*
|
- release/*
|
||||||
- develop
|
- develop
|
||||||
pull_request:
|
- main
|
||||||
|
# uncomment pull_request and comment pull_request_target to test CI changes against feature branch not target branch (develop)
|
||||||
|
# pull_request:
|
||||||
|
pull_request_target:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
- ready_for_review
|
- ready_for_review
|
||||||
@@ -19,6 +22,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
id: cache
|
id: cache
|
||||||
@@ -44,7 +49,7 @@ jobs:
|
|||||||
|
|
||||||
lint-pr-title:
|
lint-pr-title:
|
||||||
needs: node-modules
|
needs: node-modules
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}
|
||||||
name: Verify PR title
|
name: Verify PR title
|
||||||
uses: ./.github/workflows/lint-pr.yml
|
uses: ./.github/workflows/lint-pr.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
@@ -59,6 +64,7 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
@@ -96,46 +102,96 @@ jobs:
|
|||||||
# See affected apps
|
# See affected apps
|
||||||
- name: See affected apps
|
- name: See affected apps
|
||||||
run: |
|
run: |
|
||||||
|
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
|
||||||
|
branch_slug="$(echo '${{ github.head_ref || github.ref_name }}' | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
|
||||||
|
|
||||||
echo ">>>> debug"
|
echo ">>>> debug"
|
||||||
echo "NX Version: $nx_version"
|
|
||||||
echo "NX_BASE: ${{ env.NX_BASE }}"
|
echo "NX_BASE: ${{ env.NX_BASE }}"
|
||||||
echo "NX_HEAD: ${{ env.NX_HEAD }}"
|
echo "NX_HEAD: ${{ env.NX_HEAD }}"
|
||||||
|
echo "Affected: ${affected}"
|
||||||
|
echo "Branch slug: ${branch_slug}"
|
||||||
echo ">>>> eof debug"
|
echo ">>>> eof debug"
|
||||||
|
|
||||||
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
|
projects_array=()
|
||||||
echo -n "Affected projects: $affected"
|
|
||||||
|
|
||||||
branch_slug="$(echo ${{ github.head_ref || github.ref_name }} | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
|
|
||||||
projects_e2e=""
|
|
||||||
preview_governance="not deployed"
|
preview_governance="not deployed"
|
||||||
preview_trading="not deployed"
|
preview_trading="not deployed"
|
||||||
preview_explorer="not deployed"
|
preview_explorer="not deployed"
|
||||||
if [[ -z "$projects_e2e" ]]; then
|
preview_tools="not deployed"
|
||||||
projects_e2e+='"governance-e2e" "trading-e2e" "explorer-e2e" '
|
|
||||||
|
# parse if affected is any of three main applications, if none - use all of them
|
||||||
|
if echo "$affected" | grep -q governance; then
|
||||||
|
echo "Governance is affected"
|
||||||
|
projects_array+=("governance")
|
||||||
|
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
|
||||||
|
fi
|
||||||
|
if echo "$affected" | grep -q trading; then
|
||||||
|
echo "Trading is affected"
|
||||||
|
projects_array+=("trading")
|
||||||
|
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
|
||||||
|
fi
|
||||||
|
if echo "$affected" | grep -q explorer; then
|
||||||
|
echo "Explorer is affected"
|
||||||
|
projects_array+=("explorer")
|
||||||
|
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
|
||||||
|
fi
|
||||||
|
if [[ ${#projects_array[@]} -eq 0 ]]; then
|
||||||
|
projects_array=("governance" "trading" "explorer")
|
||||||
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
|
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
|
||||||
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
|
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
|
||||||
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
|
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
|
||||||
else
|
fi
|
||||||
if [[ $affected == *"governance"* ]]; then
|
|
||||||
projects_e2e+='"governance-e2e" '
|
# applications parsed before this loop are applicable for running e2e-tests
|
||||||
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
|
projects_e2e_array=()
|
||||||
|
for project in "${projects_array[@]}"; do
|
||||||
|
projects_e2e_array+=("${project}-e2e")
|
||||||
|
done
|
||||||
|
# all applications below this loop are not applicable for running e2e-test
|
||||||
|
|
||||||
|
# check if pull request event to deploy tools
|
||||||
|
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
|
||||||
|
if echo "$affected" | grep -q multisig-signer; then
|
||||||
|
echo "Tools are affected"
|
||||||
|
echo "Deploying tools on preview"
|
||||||
|
preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug")
|
||||||
|
|
||||||
|
projects_array+=("multisig-signer")
|
||||||
fi
|
fi
|
||||||
if [[ $affected == *"trading"* ]]; then
|
# those apps deploy only from develop to mainnet
|
||||||
projects_e2e+='"trading-e2e" '
|
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
||||||
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
|
if echo "$affected" | grep -q multisig-signer; then
|
||||||
|
echo "Tools are affected"
|
||||||
|
echo "Deploying tools on s3"
|
||||||
|
|
||||||
|
projects_array+=("multisig-signer")
|
||||||
fi
|
fi
|
||||||
if [[ $affected == *"explorer"* ]]; then
|
if echo "$affected" | grep -q static; then
|
||||||
projects_e2e+='"explorer-e2e" '
|
echo "static is affected"
|
||||||
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
|
echo "Deploying static on s3"
|
||||||
|
|
||||||
|
projects_array+=("static")
|
||||||
|
fi
|
||||||
|
if echo "$affected" | grep -q ui-toolkit; then
|
||||||
|
echo "ui-toolkit is affected"
|
||||||
|
echo "Deploying ui-toolkit on s3"
|
||||||
|
|
||||||
|
projects_array+=("ui-toolkit")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
projects_e2e=${projects_e2e%?}
|
|
||||||
projects_e2e=[${projects_e2e// /,}]
|
echo "Projects: ${projects_array[@]}"
|
||||||
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
|
echo "Projects E2E: ${projects_e2e_array[@]}"
|
||||||
echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV
|
projects_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_array[@]}")
|
||||||
|
projects_e2e_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_e2e_array[@]}")
|
||||||
|
|
||||||
|
echo PROJECTS_E2E=$projects_e2e_json >> $GITHUB_ENV
|
||||||
|
echo PROJECTS=$projects_json >> $GITHUB_ENV
|
||||||
|
|
||||||
echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV
|
echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV
|
||||||
echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV
|
echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV
|
||||||
echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV
|
echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV
|
||||||
|
echo PREVIEW_TOOLS=$preview_tools >> $GITHUB_ENV
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
projects: ${{ env.PROJECTS }}
|
projects: ${{ env.PROJECTS }}
|
||||||
@@ -143,11 +199,12 @@ jobs:
|
|||||||
preview_governance: ${{ env.PREVIEW_GOVERNANCE }}
|
preview_governance: ${{ env.PREVIEW_GOVERNANCE }}
|
||||||
preview_trading: ${{ env.PREVIEW_TRADING }}
|
preview_trading: ${{ env.PREVIEW_TRADING }}
|
||||||
preview_explorer: ${{ env.PREVIEW_EXPLORER }}
|
preview_explorer: ${{ env.PREVIEW_EXPLORER }}
|
||||||
|
preview_tools: ${{ env.PREVIEW_TOOLS }}
|
||||||
|
|
||||||
cypress:
|
cypress:
|
||||||
needs: lint-test-build
|
needs: lint-test-build
|
||||||
name: '(CI) cypress'
|
name: '(CI) cypress'
|
||||||
if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
|
# if: ${{ needs.lint-test-build.outputs.projects-e2e != '[]' }}
|
||||||
uses: ./.github/workflows/cypress-run.yml
|
uses: ./.github/workflows/cypress-run.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
@@ -157,7 +214,7 @@ jobs:
|
|||||||
publish-dist:
|
publish-dist:
|
||||||
needs: lint-test-build
|
needs: lint-test-build
|
||||||
name: '(CD) publish dist'
|
name: '(CD) publish dist'
|
||||||
if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
|
# if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
|
||||||
uses: ./.github/workflows/publish-dist.yml
|
uses: ./.github/workflows/publish-dist.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
@@ -168,7 +225,8 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- publish-dist
|
- publish-dist
|
||||||
- lint-test-build
|
- lint-test-build
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}
|
||||||
|
timeout-minutes: 60
|
||||||
name: '(CD) comment preview links'
|
name: '(CD) comment preview links'
|
||||||
steps:
|
steps:
|
||||||
- name: Find Comment
|
- name: Find Comment
|
||||||
@@ -178,6 +236,35 @@ jobs:
|
|||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
body-includes: Previews
|
body-includes: Previews
|
||||||
|
|
||||||
|
- name: Wait for deployments
|
||||||
|
run: |
|
||||||
|
# https://stackoverflow.com/questions/3183444/check-for-valid-link-url
|
||||||
|
regex='(https?|ftp|file)://[-[:alnum:]\+&@#/%?=~_|!:,.;]*[-[:alnum:]\+&@#/%=~_|]'
|
||||||
|
if [[ "${{ needs.lint-test-build.outputs.preview_governance }}" =~ $regex ]]; then
|
||||||
|
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_governance }}"; do
|
||||||
|
echo "waiting for governance preview"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [[ "${{ needs.lint-test-build.outputs.preview_explorer }}" =~ $regex ]]; then
|
||||||
|
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_explorer }}"; do
|
||||||
|
echo "waiting for explorer preview"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [[ "${{ needs.lint-test-build.outputs.preview_trading }}" =~ $regex ]]; then
|
||||||
|
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_trading }}"; do
|
||||||
|
echo "waiting for trading preview"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [[ "${{ needs.lint-test-build.outputs.preview_tools }}" =~ $regex ]]; then
|
||||||
|
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_tools }}"; do
|
||||||
|
echo "waiting for tools preview"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Create comment
|
- name: Create comment
|
||||||
uses: peter-evans/create-or-update-comment@v3
|
uses: peter-evans/create-or-update-comment@v3
|
||||||
if: ${{ steps.fc.outputs.comment-id == 0 }}
|
if: ${{ steps.fc.outputs.comment-id == 0 }}
|
||||||
@@ -188,16 +275,11 @@ jobs:
|
|||||||
* governance: ${{ needs.lint-test-build.outputs.preview_governance }}
|
* governance: ${{ needs.lint-test-build.outputs.preview_governance }}
|
||||||
* explorer: ${{ needs.lint-test-build.outputs.preview_explorer }}
|
* explorer: ${{ needs.lint-test-build.outputs.preview_explorer }}
|
||||||
* trading: ${{ needs.lint-test-build.outputs.preview_trading }}
|
* trading: ${{ needs.lint-test-build.outputs.preview_trading }}
|
||||||
|
* tools: ${{ needs.lint-test-build.outputs.preview_tools }}
|
||||||
cypress-check:
|
|
||||||
name: '(CI) cypress - check'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: cypress
|
|
||||||
steps:
|
|
||||||
- run: echo Done!
|
|
||||||
|
|
||||||
# Report single result at the end, to avoid mess with required checks in PR
|
# Report single result at the end, to avoid mess with required checks in PR
|
||||||
cypress-result:
|
cypress-check:
|
||||||
|
name: '(CI) cypress - check'
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
needs: cypress
|
needs: cypress
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|||||||
@@ -33,5 +33,4 @@ jobs:
|
|||||||
config: baseUrl=${{ github.event.inputs.url }}
|
config: baseUrl=${{ github.event.inputs.url }}
|
||||||
env: grepTags=@live
|
env: grepTags=@live
|
||||||
env:
|
env:
|
||||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
project: ${{ fromJSON(inputs.projects) }}
|
project: ${{ fromJSON(inputs.projects) }}
|
||||||
name: ${{ matrix.project }}
|
name: ${{ matrix.project }}
|
||||||
runs-on: self-hosted-runner
|
runs-on: self-hosted-runner
|
||||||
timeout-minutes: 40
|
timeout-minutes: 100
|
||||||
steps:
|
steps:
|
||||||
# Checks if skip cache was requested
|
# Checks if skip cache was requested
|
||||||
- name: Set skip-nx-cache flag
|
- name: Set skip-nx-cache flag
|
||||||
@@ -33,6 +33,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
path: './frontend-monorepo'
|
path: './frontend-monorepo'
|
||||||
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
# Restore node_modules from cache if possible
|
# Restore node_modules from cache if possible
|
||||||
- name: Restore node_modules from cache
|
- name: Restore node_modules from cache
|
||||||
@@ -66,7 +67,7 @@ jobs:
|
|||||||
######
|
######
|
||||||
|
|
||||||
- name: Run Cypress tests
|
- name: Run Cypress tests
|
||||||
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}"
|
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --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 }}
|
||||||
@@ -98,4 +99,4 @@ jobs:
|
|||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
with:
|
with:
|
||||||
name: test-report-${{ matrix.project }}
|
name: test-report-${{ matrix.project }}
|
||||||
path: frontend-monorepo/apps/trading-e2e/cypress/reports
|
path: frontend-monorepo/apps/${{ matrix.project }}/cypress/reports
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
name: Generate tranches
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 */6 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
master:
|
|
||||||
name: Generate Queries
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Use Node.js 16
|
|
||||||
id: Node
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 16.15.1
|
|
||||||
- name: Install root dependencies
|
|
||||||
run: yarn install
|
|
||||||
- name: Generate queries
|
|
||||||
run: node ./scripts/get-tranches.js
|
|
||||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
|
||||||
with:
|
|
||||||
commit_message: 'chore: update tranches'
|
|
||||||
commit_options: '--no-verify --signoff'
|
|
||||||
skip_fetch: true
|
|
||||||
skip_checkout: true
|
|
||||||
@@ -19,6 +19,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
id: quemu
|
id: quemu
|
||||||
@@ -30,13 +32,22 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry (ghcr)
|
||||||
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to the Container registry (docker hub)
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
|
||||||
|
with:
|
||||||
|
# registry: registry.hub.docker.com
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
@@ -51,46 +62,70 @@ jobs:
|
|||||||
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
|
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
|
||||||
|
|
||||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||||
- name: Define variables
|
- name: Define dist variables
|
||||||
|
if: ${{ github.event_name == 'push' }}
|
||||||
run: |
|
run: |
|
||||||
envName=''
|
envName=''
|
||||||
dockerfile="dist.Dockerfile"
|
domain="vega.rocks"
|
||||||
if [[ "${{ github.event_name }}" = "push" ]]; then
|
bucketName=''
|
||||||
domain="vega.rocks"
|
|
||||||
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
|
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
|
||||||
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
|
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
|
||||||
if [[ "${{ github.ref }}" =~ .*mainnet.* ]]; then
|
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
||||||
domain="vega.community"
|
envName="stagnet1"
|
||||||
if [[ "${{ matrix.app }}" = "trading" ]]; then
|
if [[ "${{ matrix.app }}" = "multisig-signer" ]]; then
|
||||||
dockerfile="ipfs.Dockerfile"
|
envName="mainnet"
|
||||||
fi
|
bucketName="tools.vega.xyz"
|
||||||
fi
|
|
||||||
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
|
||||||
envName="stagnet1"
|
|
||||||
fi
|
fi
|
||||||
bucketName="${{ matrix.app }}.${envName}.${domain}"
|
if [[ "${{ matrix.app }}" = "static" ]]; then
|
||||||
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
|
envName="mainnet"
|
||||||
|
bucketName="static.vega.xyz"
|
||||||
|
fi
|
||||||
|
if [[ "${{ matrix.app }}" = "ui-toolkit" ]]; then
|
||||||
|
envName="mainnet"
|
||||||
|
bucketName="ui.vega.rocks"
|
||||||
|
fi
|
||||||
|
elif [[ "${{ github.ref }}" =~ .*main$ ]]; then
|
||||||
|
envName="mainnet"
|
||||||
fi
|
fi
|
||||||
nodeVersion=$(cat .nvmrc | head -n 1)
|
|
||||||
|
if [[ "${envName}" = "mainnet" ]]; then
|
||||||
|
domain="vega.xyz"
|
||||||
|
if [[ -z "${bucketName}" ]]; then
|
||||||
|
bucketName="${{ matrix.app }}.${domain}"
|
||||||
|
fi
|
||||||
|
elif [[ "${envName}" = "testnet" ]]; then
|
||||||
|
domain="fairground.wtf"
|
||||||
|
if [[ -z "${bucketName}" ]]; then
|
||||||
|
bucketName="${{ matrix.app }}.${domain}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${bucketName}" ]]; then
|
||||||
|
bucketName="${{ matrix.app }}.${envName}.${domain}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "bucket name: ${bucketName}"
|
||||||
|
echo "env name: ${envName}"
|
||||||
|
|
||||||
|
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
|
||||||
echo ENV_NAME=${envName} >> $GITHUB_ENV
|
echo ENV_NAME=${envName} >> $GITHUB_ENV
|
||||||
echo NODE_VERSION=${nodeVersion} >> $GITHUB_ENV
|
|
||||||
echo DOCKERFILE=docker/${dockerfile} >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Build local dist
|
- name: Build local dist
|
||||||
if: ${{ env.DOCKERFILE != 'docker/ipfs.Dockerfile' }}
|
|
||||||
run: |
|
run: |
|
||||||
flags=""
|
envCmd=""
|
||||||
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
|
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
|
||||||
if [[ "${{ env.ENV_NAME }}" != "ops-vega" ]]; then
|
envCmd="yarn env-cmd -f ./apps/${{ matrix.app }}/.env.${{ env.ENV_NAME }}"
|
||||||
flags="--env=${{ env.ENV_NAME }}"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${{ matrix.app }}" = "trading" ]; then
|
if [ "${{ matrix.app }}" = "trading" ]; then
|
||||||
yarn nx export trading $flags || (yarn install && yarn nx export trading $flags)
|
$envCmd yarn nx export trading || (yarn install && $envCmd yarn nx export trading)
|
||||||
DIST_LOCATION=dist/apps/trading/exported
|
DIST_LOCATION=dist/apps/trading/exported
|
||||||
|
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
|
||||||
|
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
|
||||||
|
DIST_LOCATION=dist/storybook/ui-toolkit
|
||||||
else
|
else
|
||||||
yarn nx build ${{ matrix.app }} $flags || (yarn install && yarn nx build ${{ matrix.app }} $flags)
|
$envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }})
|
||||||
DIST_LOCATION=dist/apps/${{ matrix.app }}
|
DIST_LOCATION=dist/apps/${{ matrix.app }}
|
||||||
fi
|
fi
|
||||||
mv $DIST_LOCATION dist-result
|
mv $DIST_LOCATION dist-result
|
||||||
@@ -98,62 +133,92 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and export to local Docker
|
- name: Build and export to local Docker
|
||||||
id: docker_build
|
id: docker_build
|
||||||
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
|
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ${{ env.DOCKERFILE }}
|
file: docker/node-outside-docker.Dockerfile
|
||||||
load: true
|
load: true
|
||||||
build-args: |
|
build-args: |
|
||||||
APP=${{ matrix.app }}
|
APP=${{ matrix.app }}
|
||||||
NODE_VERSION=${{ env.NODE_VERSION }}
|
|
||||||
ENV_NAME=${{ env.ENV_NAME }}
|
ENV_NAME=${{ env.ENV_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
|
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
|
||||||
|
|
||||||
- name: Image digest
|
- name: Image digest
|
||||||
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
|
||||||
- name: Sanity check docker image
|
- name: Sanity check docker image
|
||||||
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
|
|
||||||
run: |
|
run: |
|
||||||
echo "Check ipfs-hash"
|
echo "Check ipfs-hash"
|
||||||
if [[ "${{ env.DOCKERFILE }}" = "docker/ipfs.Dockerfile" ]]; then
|
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
|
||||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
|
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash > ${{ matrix.app }}-ipfs-hash
|
||||||
fi
|
|
||||||
|
|
||||||
echo "List html directory"
|
echo "List html directory"
|
||||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree .'
|
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree /usr/share/nginx/html'
|
||||||
|
|
||||||
- name: Copy dist to local filesystem
|
- name: Publish dist as docker image (ghcr)
|
||||||
if: ${{ env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' }}
|
|
||||||
run: |
|
|
||||||
docker create --name=dist ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
|
|
||||||
docker cp dist:/usr/share/nginx/html dist
|
|
||||||
|
|
||||||
echo "check local dist files"
|
|
||||||
tree dist/html
|
|
||||||
mv dist/html dist-result
|
|
||||||
|
|
||||||
- name: Publish dist as docker image
|
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
continue-on-error: true
|
||||||
|
id: ghcr-push
|
||||||
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ${{ env.DOCKERFILE }}
|
file: docker/node-outside-docker.Dockerfile
|
||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
APP=${{ matrix.app }}
|
APP=${{ matrix.app }}
|
||||||
NODE_VERSION=${{ env.NODE_VERSION }}
|
|
||||||
ENV_NAME=${{ env.ENV_NAME }}
|
ENV_NAME=${{ env.ENV_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
|
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
|
- name: Publish dist as docker image (docker hub)
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
continue-on-error: true
|
||||||
|
id: dockerhub-push
|
||||||
|
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/node-outside-docker.Dockerfile
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
APP=${{ matrix.app }}
|
||||||
|
ENV_NAME=${{ env.ENV_NAME }}
|
||||||
|
tags: |
|
||||||
|
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
|
||||||
|
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }}
|
||||||
|
|
||||||
|
- name: Publish dist as docker image (ghcr - retry)
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
if: ${{ steps.ghcr-push.outcome == 'failure' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/node-outside-docker.Dockerfile
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
APP=${{ matrix.app }}
|
||||||
|
ENV_NAME=${{ env.ENV_NAME }}
|
||||||
|
tags: |
|
||||||
|
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
|
- name: Publish dist as docker image (docker hub - retry)
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
if: ${{ steps.dockerhub-push.outcome == 'failure' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/node-outside-docker.Dockerfile
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
APP=${{ matrix.app }}
|
||||||
|
ENV_NAME=${{ env.ENV_NAME }}
|
||||||
|
tags: |
|
||||||
|
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
|
||||||
|
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }}
|
||||||
|
|
||||||
# bucket creation in github.com/vegaprotocol/terraform//frontend
|
# bucket creation in github.com/vegaprotocol/terraform//frontend
|
||||||
- name: Publish dist to s3
|
- name: Publish dist to s3
|
||||||
uses: jakejarvis/s3-sync-action@master
|
uses: jakejarvis/s3-sync-action@master
|
||||||
if: ${{ github.event_name == 'push' }}
|
# s3 releases are not happening for trading on mainnet - it's IPFS
|
||||||
|
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }}
|
||||||
with:
|
with:
|
||||||
args: --acl private --follow-symlinks --delete
|
args: --acl private --follow-symlinks --delete
|
||||||
env:
|
env:
|
||||||
@@ -163,9 +228,104 @@ jobs:
|
|||||||
AWS_REGION: 'eu-west-1'
|
AWS_REGION: 'eu-west-1'
|
||||||
SOURCE_DIR: 'dist-result'
|
SOURCE_DIR: 'dist-result'
|
||||||
|
|
||||||
|
- name: Install aws CLI
|
||||||
|
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }}
|
||||||
|
uses: unfor19/install-aws-cli-action@master
|
||||||
|
|
||||||
|
- name: Perform cache invalidation
|
||||||
|
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }}
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_REGION: 'eu-west-1'
|
||||||
|
run: |
|
||||||
|
echo "Looking for distribution for bucket: ${{ env.BUCKET_NAME }}"
|
||||||
|
id=$(aws cloudfront list-distributions | jq -Mrc '.DistributionList.Items | .[] | select(.DefaultCacheBehavior.TargetOriginId == "${{ env.BUCKET_NAME }}") | .Id')
|
||||||
|
echo "Found id is: ${id}"
|
||||||
|
aws cloudfront create-invalidation --distribution-id $id --paths "/*"
|
||||||
|
|
||||||
- name: Add preview label
|
- name: Add preview label
|
||||||
uses: actions-ecosystem/action-add-labels@v1
|
uses: actions-ecosystem/action-add-labels@v1
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}
|
||||||
with:
|
with:
|
||||||
labels: ${{ matrix.app }}-preview
|
labels: ${{ matrix.app }}-preview
|
||||||
number: ${{ github.event.number }}
|
number: ${{ github.event.number }}
|
||||||
|
|
||||||
|
- name: Trigger fleek deployment
|
||||||
|
# release to ipfs happens only on mainnet (represented by main branch) for trading
|
||||||
|
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
|
||||||
|
run: |
|
||||||
|
if echo ${{ github.ref }} | grep -q main; then
|
||||||
|
# display info about app
|
||||||
|
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
|
||||||
|
https://api.fleek.co/graphql
|
||||||
|
|
||||||
|
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
|
||||||
|
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
|
||||||
|
https://api.fleek.co/graphql
|
||||||
|
|
||||||
|
elif echo ${{ github.ref }} | grep -q release/testnet; then
|
||||||
|
# display info about app
|
||||||
|
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"query": "query{getSiteById(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id latestDeploy{id status}}}"}' \
|
||||||
|
https://api.fleek.co/graphql
|
||||||
|
|
||||||
|
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
|
||||||
|
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"query": "mutation{triggerDeploy(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id status}}"}' \
|
||||||
|
https://api.fleek.co/graphql
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check out ipfs-redirect
|
||||||
|
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: 'vegaprotocol/ipfs-redirect'
|
||||||
|
path: 'ipfs-redirect'
|
||||||
|
fetch-depth: '0'
|
||||||
|
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Update interstitial page to point to the new console
|
||||||
|
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
# set CID
|
||||||
|
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
|
||||||
|
tar -xzf kubo.tgz
|
||||||
|
export PATH="$PATH:$PWD/kubo"
|
||||||
|
which ipfs
|
||||||
|
new_hash=$(cat ${{ matrix.app }}-ipfs-hash)
|
||||||
|
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
|
||||||
|
|
||||||
|
(
|
||||||
|
cd ipfs-redirect
|
||||||
|
|
||||||
|
# configure git
|
||||||
|
git status
|
||||||
|
cat .git/config
|
||||||
|
git config --global user.email "vega-ci-bot@vega.xyz"
|
||||||
|
git config --global user.name "vega-ci-bot"
|
||||||
|
|
||||||
|
# update CID files
|
||||||
|
if echo ${{ github.ref }} | grep -q main; then
|
||||||
|
echo $new_hash > cidv0-mainnet.txt
|
||||||
|
echo $new_cid > cidv1-mainnet.txt
|
||||||
|
git add cidv0-mainnet.txt cidv1-mainnet.txt
|
||||||
|
elif echo ${{ github.ref }} | grep -q release/testnet; then
|
||||||
|
echo $new_hash > cidv0-fairground.txt
|
||||||
|
echo $new_cid > cidv1-fairground.txt
|
||||||
|
git add cidv0-fairground.txt cidv1-fairground.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
# create commit
|
||||||
|
commit_msg="Automated hash update from ${{ github.ref }}"
|
||||||
|
git commit -m "$commit_msg"
|
||||||
|
git push -u origin "main"
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
name: 'Rollback console'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version that should be set on rollback'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rollback:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Log in to the Container registry (docker hub)
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Retag mainnet
|
||||||
|
run: |
|
||||||
|
docker pull vegaprotocol/trading:${{ inputs.version }}
|
||||||
|
docker tag vegaprotocol/trading:${{ inputs.version }} vegaprotocol/trading:mainnet
|
||||||
|
docker push vegaprotocol/trading:mainnet
|
||||||
|
docker run --rm vegaprotocol/trading:mainnet cat /ipfs-hash > ipfs-hash
|
||||||
|
|
||||||
|
- name: Trigger fleek deployment
|
||||||
|
run: |
|
||||||
|
# display info about app
|
||||||
|
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
|
||||||
|
https://api.fleek.co/graphql
|
||||||
|
|
||||||
|
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
|
||||||
|
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
|
||||||
|
https://api.fleek.co/graphql
|
||||||
|
|
||||||
|
- name: Check out ipfs-redirect
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: 'vegaprotocol/ipfs-redirect'
|
||||||
|
path: 'ipfs-redirect'
|
||||||
|
fetch-depth: '0'
|
||||||
|
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Update console.vega.xyz DNS to redirect to the new console
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
|
||||||
|
tar -xzf kubo.tgz
|
||||||
|
export PATH="$PATH:$PWD/kubo"
|
||||||
|
which ipfs
|
||||||
|
new_hash=$(cat ipfs-hash)
|
||||||
|
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
|
||||||
|
git config --global user.email "vega-ci-bot@vega.xyz"
|
||||||
|
git config --global user.name "vega-ci-bot"
|
||||||
|
echo $new_hash > ipfs-redirect/cidv0.txt
|
||||||
|
echo $new_cid > ipfs-redirect/cidv1.txt
|
||||||
|
|
||||||
|
(
|
||||||
|
cd ipfs-redirect
|
||||||
|
|
||||||
|
git status
|
||||||
|
branch_name="rollback-to-$new_hash"
|
||||||
|
git checkout -b "$branch_name"
|
||||||
|
commit_msg="hash rollback to $new_hash"
|
||||||
|
git add cidv0.txt cidv1.txt
|
||||||
|
git commit -m "$commit_msg"
|
||||||
|
git push -u origin "$branch_name" --force-with-lease
|
||||||
|
pr_url="$(gh pr create --title "${commit_msg}" --body 'automated pull request to update CIDs')"
|
||||||
|
echo $pr_url
|
||||||
|
# once auto merge get's enabled on documentation repo let's do follow up
|
||||||
|
sleep 5
|
||||||
|
gh pr merge "${pr_url}" --delete-branch --squash --admin
|
||||||
|
)
|
||||||
+4
-1
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
# compiled output
|
# compiled output
|
||||||
/dist
|
/dist
|
||||||
|
/dist-result
|
||||||
/tmp
|
/tmp
|
||||||
/out-tsc
|
/out-tsc
|
||||||
/tools/executors/**/*.js
|
/tools/executors/**/*.js
|
||||||
@@ -48,4 +49,6 @@ cypress.env.json
|
|||||||
.next
|
.next
|
||||||
|
|
||||||
#cypress
|
#cypress
|
||||||
/apps/trading-e2e/cypress/reports/
|
/apps/**/cypress/reports/
|
||||||
|
/apps/**/cypress/downloads/
|
||||||
|
/apps/**/fixtures/wallet/node**
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
stories: [],
|
|
||||||
addons: [
|
|
||||||
'@storybook/addon-actions',
|
|
||||||
'@storybook/addon-viewport',
|
|
||||||
{
|
|
||||||
name: '@storybook/addon-docs',
|
|
||||||
options: {
|
|
||||||
configureJSX: true,
|
|
||||||
babelOptions: {},
|
|
||||||
sourceLoaderOptions: null,
|
|
||||||
transcludeMarkdown: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'@storybook/addon-controls',
|
|
||||||
'@storybook/addon-backgrounds',
|
|
||||||
'@storybook/addon-toolbars',
|
|
||||||
'@storybook/addon-measure',
|
|
||||||
'@storybook/addon-outline',
|
|
||||||
'@storybook/addon-a11y',
|
|
||||||
],
|
|
||||||
// uncomment the property below if you want to apply some webpack config globally
|
|
||||||
// webpackFinal: async (config, { configType }) => {
|
|
||||||
// // Make whatever fine-grained changes you need that should apply to all storybook configs
|
|
||||||
|
|
||||||
// // Return the altered config
|
|
||||||
// return config;
|
|
||||||
// },
|
|
||||||
};
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.base.json",
|
|
||||||
"exclude": [
|
|
||||||
"../**/*.spec.js",
|
|
||||||
"../**/*.test.js",
|
|
||||||
"../**/*.spec.ts",
|
|
||||||
"../**/*.test.ts",
|
|
||||||
"../**/*.spec.tsx",
|
|
||||||
"../**/*.test.tsx",
|
|
||||||
"../**/*.spec.jsx",
|
|
||||||
"../**/*.test.jsx"
|
|
||||||
],
|
|
||||||
"include": ["../**/*"]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.PHONY: latest-release
|
||||||
|
latest-release:
|
||||||
|
gh release list | head -n 1 | awk '{print $1}'
|
||||||
|
|
||||||
|
.PHONY: show-latest-release
|
||||||
|
show-latest-release:
|
||||||
|
gh release view `gh release list | head -n 1 | awk '{print $1}'`
|
||||||
|
|
||||||
|
.PHONY: recalculate-ipfs
|
||||||
|
recalculate-ipfs:
|
||||||
|
echo "ipfs hash inside the image"
|
||||||
|
docker run --rm ${TAG} cat /ipfs-hash
|
||||||
|
echo "recalculating ipfs hash"
|
||||||
|
docker run --rm ${TAG} ipfs add -r /usr/share/nginx/html
|
||||||
|
|
||||||
|
.PHONY: eject-ipfs-hash
|
||||||
|
unpack:
|
||||||
|
docker create --name=dist ${TAG}
|
||||||
|
docker cp dist:/usr/share/nginx/html dist
|
||||||
|
docker rm dist
|
||||||
@@ -103,25 +103,68 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
|
|||||||
|
|
||||||
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
|
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
|
||||||
|
|
||||||
# Docker & Vegacapsule
|
# 🐋 Hosting a console
|
||||||
|
|
||||||
## Docker
|
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
|
||||||
|
|
||||||
The [Dockerfile](./dockerfiles) for running the frontends is pretty basic, merely building the application with the APP arg that is passed in and serving the application from [nginx](./nginx/nginx.conf). The only complexity that exists is that there is a script which allows the passing of run time environment variables to the containers. See configuration below for how to do this.
|
In order to run a container on port 3000:
|
||||||
|
|
||||||
You can build any of the containers locally with the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build --dockerfile dockerfiles/Dockerfile.cra . --build-arg APP=[YOUR APP] --tag=[TAG]
|
|
||||||
```
|
|
||||||
|
|
||||||
In order to run a container:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 3000:80 [TAG]
|
docker run -p 3000:80 [TAG]
|
||||||
```
|
```
|
||||||
|
|
||||||
Images ending with `.dist` are to pack locally created transpiled HTML files into nginx container for non-compatible with yarn architectures like M1 Mac
|
## Build instructions
|
||||||
|
|
||||||
|
The [`docker`](./docker) subfolder has some docker configurations for easily setting up your own hosted version of console either for the web, or ready for pinning on IPFS
|
||||||
|
|
||||||
|
### nx build outside the docker
|
||||||
|
|
||||||
|
Packaging prepared dist into [`nginx`](https://hub.docker.com/_/nginx)([server configuration](./nginx/nginx.conf)) docker image involves building the application on docker host machine from source.
|
||||||
|
|
||||||
|
As a prerequisite you need to perform build of `dist` directory and move its content for specific application to `dist-result` directory. Use following script to do it with a single command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./docker/prepare-dist.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
You can build any of the containers locally with the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --dockerfile docker/node-outside-docker.Dockerfile . --tag=[TAG]
|
||||||
|
```
|
||||||
|
|
||||||
|
### nx build inside the docker
|
||||||
|
|
||||||
|
Using multistage dockerfile dist is compiled using [node](https://hub.docker.com/_/node) image and later packed to nginx as in [dist build](#dist-build) example.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --build-arg APP=[YOUR APP] --build-arg NODE_VERSION=$(cat .nvmrc) --build-arg ENV_NAME=mainnet -t [TAG] -f docker/node-inside-docker.Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Computing ipfs-hash of the build
|
||||||
|
|
||||||
|
At the moment this feature is important only for `trading` (console) releases.
|
||||||
|
|
||||||
|
Each docker build finishes with hash calculation for dist directory. Resulting hash is added to file named as `/ipfs-hash`. Once docker image is produced you can run following commad to display ipfs-hash:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make recalculate-ipfs TAG=vegaprotocol/trading:{YOUR_VERSION}
|
||||||
|
```
|
||||||
|
|
||||||
|
**updating hash:** recompiling dist directory (even if there are no changed to source code) results in different hash computed by ipfs command.
|
||||||
|
|
||||||
|
### Verifying ipfs-hash of existing current application version
|
||||||
|
|
||||||
|
An IPFS CID will be attached to every [release](https://github.com/vegaprotocol/frontend-monorepo/releases). If you are intending to pin an application on IPFS, you can check that your build matches by running the following steps:
|
||||||
|
|
||||||
|
1. Show latest release by runnning: `make latest-release`. You need to configure [`gh`](https://cli.github.com/) for this step to work, otherwise please provide release manually from [github](https://github.com/vegaprotocol/frontend-monorepo/releases) or [dockerhub](https://hub.docker.com/r/vegaprotocol/trading)
|
||||||
|
2. Set RELEASE environment variable to value that you want to validate: `export RELEASE=$(make latest-release)` or `export RELEASE=vXX.XX.XX`
|
||||||
|
3. Set TAG environment variable to image that you want to validate: `export TAG=vegaprotocol/trading:$RELEASE`
|
||||||
|
4. Download docker image with the desired release `docker pull $TAG`.
|
||||||
|
5. Recalculate hash: `make recalculate-ipfs`
|
||||||
|
6. You should see exactly same hash produced by ipfs command as one placed with the release notes: `make show-latest-release`
|
||||||
|
7. If you want to extract dist from docker image to your local filesystem you can run following command: `make unpack`
|
||||||
|
8. Now `dist` directory contains valid application build. **it is not possible to calculate same ipfs hash on files that are result of copy operation**
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
|
||||||
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_URL=http://localhost:3008/graphql
|
NX_VEGA_URL=http://localhost:3008/graphql
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
|
||||||
NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm
|
NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket
|
||||||
NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql
|
NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
|
||||||
NX_TENDERMINT_URL=https://mainnet-observer-proxy01.ops.vega.xyz/
|
NX_TENDERMINT_URL=https://mainnet-observer-proxy01.ops.vega.xyz/
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://mainnet-observer-proxy01.ops.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://mainnet-observer-proxy01.ops.vega.xyz/websocket
|
||||||
NX_VEGA_URL=https://api.vega.community/graphql
|
NX_VEGA_URL=https://api.vega.community/graphql
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
|
||||||
NX_TENDERMINT_URL=https://tm.n07.testnet.vega.xyz/tm
|
NX_TENDERMINT_URL=https://tm.n07.testnet.vega.xyz/tm
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://lb.testnet.vega.xyz/tm/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://lb.testnet.vega.xyz/tm/websocket
|
||||||
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
|
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
const { defineConfig } = require('cypress');
|
const { defineConfig } = require('cypress');
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
projectId: 'et4snf',
|
reporter: '../../node_modules/cypress-mochawesome-reporter',
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
setupNodeEvents(on, config) {
|
setupNodeEvents(on, config) {
|
||||||
|
require('cypress-mochawesome-reporter/plugin')(on);
|
||||||
require('@cypress/grep/src/plugin')(config);
|
require('@cypress/grep/src/plugin')(config);
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"name": "explorer-e2e",
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
"sourceRoot": "apps/explorer-e2e/src",
|
"sourceRoot": "apps/explorer-e2e/src",
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"targets": {
|
"targets": {
|
||||||
"e2e": {
|
"e2e": {
|
||||||
"executor": "@nrwl/cypress:cypress",
|
"executor": "@nx/cypress:cypress",
|
||||||
"options": {
|
"options": {
|
||||||
"cypressConfig": "apps/explorer-e2e/cypress.config.js",
|
"cypressConfig": "apps/explorer-e2e/cypress.config.js",
|
||||||
"devServerTarget": "explorer:serve"
|
"devServerTarget": "explorer:serve"
|
||||||
@@ -16,14 +17,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"executor": "@nrwl/linter:eslint",
|
"executor": "@nx/linter:eslint",
|
||||||
"outputs": ["{options.outputFile}"],
|
"outputs": ["{options.outputFile}"],
|
||||||
"options": {
|
"options": {
|
||||||
"lintFilePatterns": ["apps/explorer-e2e/**/*.{js,ts}"]
|
"lintFilePatterns": ["apps/explorer-e2e/**/*.{js,ts}"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"executor": "@nrwl/workspace:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"options": {
|
"options": {
|
||||||
"command": "yarn tsc --project ./apps/explorer-e2e/"
|
"command": "yarn tsc --project ./apps/explorer-e2e/"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ context('Asset page', { tags: '@regression' }, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open details page when clicked on "View details"', () => {
|
it.skip('should open details page when clicked on "View details"', () => {
|
||||||
cy.getAssets().then((assets) => {
|
cy.getAssets().then((assets) => {
|
||||||
assets.forEach((asset) => {
|
assets.forEach((asset) => {
|
||||||
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ context('Home Page', function () {
|
|||||||
16: 'Chain ID',
|
16: 'Chain ID',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
|
||||||
|
* You should use .then() to chain commands instead.
|
||||||
|
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
|
||||||
|
**/
|
||||||
cy.get('[data-testid="stats-title"]')
|
cy.get('[data-testid="stats-title"]')
|
||||||
.each(($list, index) => {
|
.each(($list, index) => {
|
||||||
cy.wrap($list).should('contain.text', statTitles[index]);
|
cy.wrap($list).should('contain.text', statTitles[index]);
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ context('Network parameters page', { tags: '@smoke' }, function () {
|
|||||||
const parameterName = network_parameter[0];
|
const parameterName = network_parameter[0];
|
||||||
const parameterValue = network_parameter[1];
|
const parameterValue = network_parameter[1];
|
||||||
if (this.networkParameterFormat.json.includes(parameterName)) {
|
if (this.networkParameterFormat.json.includes(parameterName)) {
|
||||||
|
/**
|
||||||
|
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
|
||||||
|
* You should use .then() to chain commands instead.
|
||||||
|
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
|
||||||
|
**/
|
||||||
cy.get(tableRows)
|
cy.get(tableRows)
|
||||||
.contains(parameterName)
|
.contains(parameterName)
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
@@ -65,6 +70,11 @@ context('Network parameters page', { tags: '@smoke' }, function () {
|
|||||||
if (this.networkParameterFormat.percentage.includes(parameterName)) {
|
if (this.networkParameterFormat.percentage.includes(parameterName)) {
|
||||||
const formattedPercentageParameter =
|
const formattedPercentageParameter =
|
||||||
(parseFloat(parameterValue) * 100).toFixed(0) + '%';
|
(parseFloat(parameterValue) * 100).toFixed(0) + '%';
|
||||||
|
/**
|
||||||
|
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
|
||||||
|
* You should use .then() to chain commands instead.
|
||||||
|
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
|
||||||
|
**/
|
||||||
cy.get(tableRows)
|
cy.get(tableRows)
|
||||||
.contains(parameterName)
|
.contains(parameterName)
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
@@ -148,6 +158,11 @@ context('Network parameters page', { tags: '@smoke' }, function () {
|
|||||||
cy.convert_number_to_max_four_decimal(parameterValue)
|
cy.convert_number_to_max_four_decimal(parameterValue)
|
||||||
.add_commas_to_number_if_large_enough()
|
.add_commas_to_number_if_large_enough()
|
||||||
.then((parameterValueFormatted) => {
|
.then((parameterValueFormatted) => {
|
||||||
|
/**
|
||||||
|
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
|
||||||
|
* You should use .then() to chain commands instead.
|
||||||
|
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
|
||||||
|
**/
|
||||||
cy.get(tableRows)
|
cy.get(tableRows)
|
||||||
.contains(parameterName)
|
.contains(parameterName)
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
@@ -179,6 +194,11 @@ context('Network parameters page', { tags: '@smoke' }, function () {
|
|||||||
cy.convert_number_to_max_eighteen_decimal(parameterValue)
|
cy.convert_number_to_max_eighteen_decimal(parameterValue)
|
||||||
.add_commas_to_number_if_large_enough()
|
.add_commas_to_number_if_large_enough()
|
||||||
.then((parameterValueFormatted) => {
|
.then((parameterValueFormatted) => {
|
||||||
|
/**
|
||||||
|
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
|
||||||
|
* You should use .then() to chain commands instead.
|
||||||
|
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
|
||||||
|
**/
|
||||||
cy.get(tableRows)
|
cy.get(tableRows)
|
||||||
.contains(parameterName)
|
.contains(parameterName)
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
|
|||||||
@@ -170,6 +170,11 @@ context.skip('Parties page', { tags: '@regression' }, function () {
|
|||||||
const sideMenuBackground = '.absolute';
|
const sideMenuBackground = '.absolute';
|
||||||
|
|
||||||
// Engage dark mode if not allready set
|
// Engage dark mode if not allready set
|
||||||
|
/**
|
||||||
|
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
|
||||||
|
* You should use .then() to chain commands instead.
|
||||||
|
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
|
||||||
|
**/
|
||||||
cy.get(sideMenuBackground)
|
cy.get(sideMenuBackground)
|
||||||
.should('have.css', 'background-color')
|
.should('have.css', 'background-color')
|
||||||
.then((background_color) => {
|
.then((background_color) => {
|
||||||
|
|||||||
@@ -60,16 +60,31 @@ context.skip('Transactions page', function () {
|
|||||||
});
|
});
|
||||||
cy.get('block').should('not.be.empty');
|
cy.get('block').should('not.be.empty');
|
||||||
cy.get('encoded-tnx').should('not.be.empty');
|
cy.get('encoded-tnx').should('not.be.empty');
|
||||||
|
/**
|
||||||
|
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
|
||||||
|
* You should use .then() to chain commands instead.
|
||||||
|
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
|
||||||
|
**/
|
||||||
cy.get('tx-type')
|
cy.get('tx-type')
|
||||||
.should('not.be.empty')
|
.should('not.be.empty')
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
.then((txTypeTxt) => {
|
.then((txTypeTxt) => {
|
||||||
if (txTypeTxt == 'Order Submission') {
|
if (txTypeTxt == 'Order Submission') {
|
||||||
|
/**
|
||||||
|
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
|
||||||
|
* You should use .then() to chain commands instead.
|
||||||
|
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
|
||||||
|
**/
|
||||||
cy.get('.hljs-attr')
|
cy.get('.hljs-attr')
|
||||||
.should('have.length.at.least', 8)
|
.should('have.length.at.least', 8)
|
||||||
.each(($propertyName) => {
|
.each(($propertyName) => {
|
||||||
cy.wrap($propertyName).should('not.be.empty');
|
cy.wrap($propertyName).should('not.be.empty');
|
||||||
});
|
});
|
||||||
|
/**
|
||||||
|
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
|
||||||
|
* You should use .then() to chain commands instead.
|
||||||
|
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
|
||||||
|
**/
|
||||||
cy.get('.hljs-string')
|
cy.get('.hljs-string')
|
||||||
.should('have.length.at.least', 8)
|
.should('have.length.at.least', 8)
|
||||||
.each(($propertyValue) => {
|
.each(($propertyValue) => {
|
||||||
|
|||||||
@@ -15,5 +15,6 @@
|
|||||||
|
|
||||||
import '@vegaprotocol/cypress';
|
import '@vegaprotocol/cypress';
|
||||||
import './common.functions.js';
|
import './common.functions.js';
|
||||||
|
import 'cypress-mochawesome-reporter/register';
|
||||||
import registerCypressGrep from '@cypress/grep';
|
import registerCypressGrep from '@cypress/grep';
|
||||||
registerCypressGrep();
|
registerCypressGrep();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"presets": [
|
"presets": [
|
||||||
[
|
[
|
||||||
"@nrwl/react/babel",
|
"@nx/react/babel",
|
||||||
{
|
{
|
||||||
"runtime": "automatic"
|
"runtime": "automatic"
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-7
@@ -1,18 +1,21 @@
|
|||||||
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.rocks
|
||||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
|
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://explorer.stagnet1.vega.rocks
|
||||||
NX_VEGA_TOKEN_URL=https://stagnet1.governance.vega.xyz
|
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
|
||||||
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.rocks
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||||
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
|
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.rocks/rest
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
|
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
|
||||||
NX_VEGA_GOVERNANCE_URL=https://stagnet1.governance.vega.xyz
|
NX_VEGA_GOVERNANCE_URL=https://governance.stagnet1.vega.rocks
|
||||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.jsoo
|
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||||
|
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
|
||||||
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
|
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases/tag/
|
||||||
|
|
||||||
# App flags
|
# App flags
|
||||||
NX_EXPLORER_ASSETS=1
|
NX_EXPLORER_ASSETS=1
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_TENDERMINT_URL=https://be.vega.community
|
NX_TENDERMINT_URL=https://be.vega.community
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||||
|
NX_SENTRY_DSN=https://b3a56b03eda842faad731f3ea9dfd1bc@o286262.ingest.sentry.io/6242427
|
||||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
|
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
|
||||||
|
NX_VEGA_URL=https://api.vega.community/graphql
|
||||||
NX_VEGA_ENV=MAINNET
|
NX_VEGA_ENV=MAINNET
|
||||||
NX_BLOCK_EXPLORER=https://be.vega.community/rest/
|
NX_BLOCK_EXPLORER=https://be.vega.community/rest
|
||||||
NX_ETHERSCAN_URL=https://etherscan.io
|
NX_ETHERSCAN_URL=https://etherscan.io
|
||||||
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
|
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
|
||||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||||
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz/
|
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz/
|
||||||
|
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# App configuration variables
|
||||||
|
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
|
||||||
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
|
||||||
|
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
|
||||||
|
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
|
||||||
|
NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql
|
||||||
|
NX_VEGA_ENV=MAINNET-MIRROR
|
||||||
|
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.rocks/rest
|
||||||
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
NX_VEGA_GOVERNANCE_URL=https://governance.mainnet-mirror.vega.rocks
|
||||||
|
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||||
|
NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks/
|
||||||
|
NX_VEGA_CONSOLE_URL=https://console.mainnet-mirror.vega.rocks
|
||||||
@@ -1 +1,2 @@
|
|||||||
# .env is stagnet1, so there are no overrides required
|
# .env is stagnet1, so there are no overrides required
|
||||||
|
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz","STAGNET1":"https://stagnet1.explorer.vega.xyz"}'
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
|||||||
NX_VEGA_GOVERNANCE_URL=https://governance.fairground.wtf
|
NX_VEGA_GOVERNANCE_URL=https://governance.fairground.wtf
|
||||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||||
|
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9
|
|||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||||
|
|
||||||
NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
|
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
|
||||||
NX_TENDERMINT_URL=https://tm-be.validators-testnet.vega.rocks
|
|
||||||
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest
|
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
|
||||||
NX_VEGA_EXPLORER_URL=https://validator-testnet.explorer.vega.xyz/
|
NX_VEGA_GOVERNANCE_URL=https://governance.validators-testnet.vega.rocks
|
||||||
|
NX_VEGA_EXPLORER_URL=https://explorer.validators-testnet.vega.rocks/
|
||||||
|
NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks/
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
||||||
"ignorePatterns": ["!**/*", "__generated__", "__generated___"],
|
"ignorePatterns": ["!**/*", "__generated__"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ yarn nx serve explorer
|
|||||||
Example configurations are provided here:
|
Example configurations are provided here:
|
||||||
|
|
||||||
- [Mainnet](./.env.mainnet)
|
- [Mainnet](./.env.mainnet)
|
||||||
|
- [Mainnet-mirror](./.env.mainnet-mirror)
|
||||||
- [Devnet](./.env.devnet)
|
- [Devnet](./.env.devnet)
|
||||||
- [Capsule](./.env.capsule)
|
- [Capsule](./.env.capsule)
|
||||||
- [Testnet](./.env.testnet)
|
- [Testnet](./.env.testnet)
|
||||||
@@ -39,7 +40,7 @@ Example configurations are provided here:
|
|||||||
For convenience, you can boot the app injecting one of the configurations above by running:
|
For convenience, you can boot the app injecting one of the configurations above by running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn nx run explorer:serve --env={env} # e.g. stagnet1
|
yarn env-cmd -f .\apps\explorer\.env.{env} yarn nx run explorer:serve # e.g. stagnet1
|
||||||
```
|
```
|
||||||
|
|
||||||
There are a few different configuration options offered for this app:
|
There are a few different configuration options offered for this app:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ export default {
|
|||||||
displayName: 'explorer',
|
displayName: 'explorer',
|
||||||
preset: '../../jest.preset.js',
|
preset: '../../jest.preset.js',
|
||||||
transform: {
|
transform: {
|
||||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
|
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
||||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
|
||||||
},
|
},
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||||
coverageDirectory: '../../coverage/apps/explorer',
|
coverageDirectory: '../../coverage/apps/explorer',
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"name": "explorer",
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
"sourceRoot": "apps/explorer/src",
|
"sourceRoot": "apps/explorer/src",
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"targets": {
|
"targets": {
|
||||||
"build": {
|
"build": {
|
||||||
"executor": "./tools/executors/webpack:build",
|
"executor": "@nx/webpack:webpack",
|
||||||
"outputs": ["{options.outputPath}"],
|
"outputs": ["{options.outputPath}"],
|
||||||
"defaultConfiguration": "production",
|
"defaultConfiguration": "production",
|
||||||
"options": {
|
"options": {
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"executor": "./tools/executors/webpack:serve",
|
"executor": "@nx/webpack:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"port": 3000,
|
"port": 3000,
|
||||||
"buildTarget": "explorer:build:development",
|
"buildTarget": "explorer:build:development",
|
||||||
@@ -52,30 +53,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"executor": "@nrwl/linter:eslint",
|
"executor": "@nx/linter:eslint",
|
||||||
"outputs": ["{options.outputFile}"],
|
"outputs": ["{options.outputFile}"],
|
||||||
"options": {
|
"options": {
|
||||||
"lintFilePatterns": ["apps/explorer/**/*.{ts,tsx,js,jsx}"]
|
"lintFilePatterns": ["apps/explorer/**/*.{ts,tsx,js,jsx}"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"executor": "@nrwl/jest:jest",
|
"executor": "@nx/jest:jest",
|
||||||
"outputs": ["coverage/apps/explorer"],
|
"outputs": ["{workspaceRoot}/coverage/apps/explorer"],
|
||||||
"options": {
|
"options": {
|
||||||
"jestConfig": "apps/explorer/jest.config.ts",
|
"jestConfig": "apps/explorer/jest.config.ts",
|
||||||
"passWithNoTests": true
|
"passWithNoTests": true
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"ci": true,
|
||||||
|
"codeCoverage": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"generate-types": {
|
"generate-types": {
|
||||||
"executor": "@nrwl/workspace:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"options": {
|
"options": {
|
||||||
"commands": [
|
"commands": [
|
||||||
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.67.3/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
|
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.71.4/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"build-netlify": {
|
"build-netlify": {
|
||||||
"executor": "@nrwl/workspace:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"options": {
|
"options": {
|
||||||
"commands": [
|
"commands": [
|
||||||
"cp apps/explorer/netlify.toml netlify.toml",
|
"cp apps/explorer/netlify.toml netlify.toml",
|
||||||
@@ -84,7 +91,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"build-spec": {
|
"build-spec": {
|
||||||
"executor": "@nrwl/workspace:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"options": {
|
"options": {
|
||||||
"command": "yarn tsc --project ./apps/explorer/tsconfig.spec.json"
|
"command": "yarn tsc --project ./apps/explorer/tsconfig.spec.json"
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
import {
|
||||||
|
AppFailure,
|
||||||
|
NetworkLoader,
|
||||||
|
NodeGuard,
|
||||||
|
NodeSwitcherDialog,
|
||||||
|
useEnvironment,
|
||||||
|
useInitializeEnv,
|
||||||
|
useNodeSwitcherStore,
|
||||||
|
} from '@vegaprotocol/environment';
|
||||||
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
||||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
|
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
import { router } from './routes/router-config';
|
import { router } from './routes/router-config';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
const splashLoading = (
|
const splashLoading = (
|
||||||
<Splash>
|
<Splash>
|
||||||
@@ -12,10 +22,25 @@ const splashLoading = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { VEGA_URL } = useEnvironment();
|
||||||
|
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useNodeSwitcherStore(
|
||||||
|
(store) => [store.dialogOpen, store.setDialogOpen]
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<TendermintWebsocketProvider>
|
<TendermintWebsocketProvider>
|
||||||
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
||||||
<RouterProvider router={router} fallbackElement={splashLoading} />
|
<NodeGuard
|
||||||
|
skeleton={<div>{t('Loading')}</div>}
|
||||||
|
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
|
||||||
|
>
|
||||||
|
<Suspense fallback={splashLoading}>
|
||||||
|
<RouterProvider router={router} fallbackElement={splashLoading} />
|
||||||
|
</Suspense>
|
||||||
|
</NodeGuard>
|
||||||
|
<NodeSwitcherDialog
|
||||||
|
open={nodeSwitcherOpen}
|
||||||
|
setOpen={setNodeSwitcherOpen}
|
||||||
|
/>
|
||||||
</NetworkLoader>
|
</NetworkLoader>
|
||||||
</TendermintWebsocketProvider>
|
</TendermintWebsocketProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||||
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
|
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import { AgGridColumn } from 'ag-grid-react';
|
|
||||||
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
||||||
import type { VegaICellRendererParams } from '@vegaprotocol/datagrid';
|
import type { VegaICellRendererParams } from '@vegaprotocol/datagrid';
|
||||||
import { useRef, useLayoutEffect } from 'react';
|
import { useRef, useLayoutEffect } from 'react';
|
||||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type { RowClickedEvent } from 'ag-grid-community';
|
import type { RowClickedEvent, ColDef } from 'ag-grid-community';
|
||||||
|
|
||||||
type AssetsTableProps = {
|
type AssetsTableProps = {
|
||||||
data: AssetFieldsFragment[] | null;
|
data: AssetFieldsFragment[] | null;
|
||||||
@@ -31,6 +31,58 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const columnDefs = useMemo<ColDef[]>(
|
||||||
|
() => [
|
||||||
|
{ headerName: t('Symbol'), field: 'symbol' },
|
||||||
|
{ headerName: t('Name'), field: 'name' },
|
||||||
|
{
|
||||||
|
flex: 2,
|
||||||
|
headerName: t('ID'),
|
||||||
|
field: 'id',
|
||||||
|
hide: window.innerWidth < BREAKPOINT_MD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'type',
|
||||||
|
headerName: t('Type'),
|
||||||
|
field: 'source.__typename',
|
||||||
|
hide: window.innerWidth < BREAKPOINT_MD,
|
||||||
|
valueFormatter: ({ value }: { value?: string }) =>
|
||||||
|
value ? AssetTypeMapping[value].value : '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: t('Status'),
|
||||||
|
field: 'status',
|
||||||
|
hide: window.innerWidth < BREAKPOINT_MD,
|
||||||
|
valueFormatter: ({ value }: { value?: string }) =>
|
||||||
|
value ? AssetStatusMapping[value].value : '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[navigate]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AgGrid
|
<AgGrid
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -46,60 +98,11 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
|||||||
filterParams: { buttons: ['reset'] },
|
filterParams: { buttons: ['reset'] },
|
||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
}}
|
}}
|
||||||
|
columnDefs={columnDefs}
|
||||||
suppressCellFocus={true}
|
suppressCellFocus={true}
|
||||||
onRowClicked={({ data }: RowClickedEvent) => {
|
onRowClicked={({ data }: RowClickedEvent) => {
|
||||||
navigate(data.id);
|
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,13 @@
|
|||||||
import WS from 'jest-websocket-mock';
|
import WS from 'jest-websocket-mock';
|
||||||
import useWebSocket from 'react-use-websocket';
|
import { render, screen } from '@testing-library/react';
|
||||||
import {
|
|
||||||
render,
|
|
||||||
screen,
|
|
||||||
fireEvent,
|
|
||||||
act,
|
|
||||||
waitFor,
|
|
||||||
} from '@testing-library/react';
|
|
||||||
import { TendermintWebsocketContext } from '../../contexts/websocket/tendermint-websocket-context';
|
|
||||||
import { BlocksRefetch } from './blocks-refetch';
|
import { BlocksRefetch } from './blocks-refetch';
|
||||||
|
|
||||||
const BlocksRefetchInWebsocketProvider = ({
|
const BlocksRefetchInWebsocketProvider = ({
|
||||||
callback,
|
callback,
|
||||||
mocketLocation,
|
|
||||||
}: {
|
}: {
|
||||||
callback: () => null;
|
callback: () => null;
|
||||||
mocketLocation: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const contextShape = useWebSocket(mocketLocation);
|
return <BlocksRefetch refetch={callback} />;
|
||||||
|
|
||||||
return (
|
|
||||||
<TendermintWebsocketContext.Provider value={{ ...contextShape }}>
|
|
||||||
<BlocksRefetch refetch={callback} />
|
|
||||||
</TendermintWebsocketContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Blocks refetch', () => {
|
describe('Blocks refetch', () => {
|
||||||
@@ -32,111 +16,8 @@ describe('Blocks refetch', () => {
|
|||||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
||||||
new WebSocket(mocketLocation);
|
new WebSocket(mocketLocation);
|
||||||
|
|
||||||
render(
|
render(<BlocksRefetchInWebsocketProvider callback={() => null} />);
|
||||||
<BlocksRefetchInWebsocketProvider
|
|
||||||
callback={() => null}
|
|
||||||
mocketLocation={mocketLocation}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await mocket.connected;
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('new blocks');
|
|
||||||
expect(screen.getByTestId('refresh')).toBeInTheDocument();
|
expect(screen.getByTestId('refresh')).toBeInTheDocument();
|
||||||
mocket.close();
|
mocket.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initiate callback when the button is clicked', async () => {
|
|
||||||
const mocketLocation = 'wss:localhost:3003';
|
|
||||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
|
||||||
new WebSocket(mocketLocation);
|
|
||||||
|
|
||||||
const callback = jest.fn();
|
|
||||||
render(
|
|
||||||
<BlocksRefetchInWebsocketProvider
|
|
||||||
callback={callback}
|
|
||||||
mocketLocation={mocketLocation}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await mocket.connected;
|
|
||||||
const button = screen.getByTestId('refresh');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
fireEvent.click(button);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(callback.mock.calls.length).toEqual(1);
|
|
||||||
mocket.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show new blocks as websocket is correctly updated', async () => {
|
|
||||||
const mocketLocation = 'wss:localhost:3004';
|
|
||||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
|
||||||
new WebSocket(mocketLocation);
|
|
||||||
render(
|
|
||||||
<BlocksRefetchInWebsocketProvider
|
|
||||||
callback={() => null}
|
|
||||||
mocketLocation={mocketLocation}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await mocket.connected;
|
|
||||||
|
|
||||||
// Ensuring we send an ID equal to the one the client subscribed with.
|
|
||||||
await waitFor(() => expect(mocket.messages.length).toEqual(1));
|
|
||||||
// @ts-ignore id on messages
|
|
||||||
const id = mocket.messages[0].id;
|
|
||||||
|
|
||||||
const newBlockMessage = {
|
|
||||||
id,
|
|
||||||
result: {
|
|
||||||
query: "tm.event = 'NewBlock'",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
mocket.send(newBlockMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('1 new blocks');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
mocket.send(newBlockMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('2 new blocks');
|
|
||||||
mocket.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will not show new blocks if websocket has wrong ID', async () => {
|
|
||||||
const mocketLocation = 'wss:localhost:3005';
|
|
||||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
|
||||||
new WebSocket(mocketLocation);
|
|
||||||
|
|
||||||
render(
|
|
||||||
<BlocksRefetchInWebsocketProvider
|
|
||||||
callback={() => null}
|
|
||||||
mocketLocation={mocketLocation}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await mocket.connected;
|
|
||||||
|
|
||||||
// Ensuring we send an ID equal to the one the client subscribed with.
|
|
||||||
await waitFor(() => expect(mocket.messages.length).toEqual(1));
|
|
||||||
|
|
||||||
const newBlockMessageBadId = {
|
|
||||||
id: 'blahblahblah',
|
|
||||||
result: {
|
|
||||||
query: "tm.event = 'NewBlock'",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
mocket.send(newBlockMessageBadId);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
|
|
||||||
mocket.close();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,36 +1,19 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
import { Button, Icon } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
interface BlocksRefetchProps {
|
interface BlocksRefetchProps {
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlocksRefetch = ({ refetch }: BlocksRefetchProps) => {
|
export const BlocksRefetch = ({ refetch }: BlocksRefetchProps) => {
|
||||||
const [blocksToLoad, setBlocksToLoad] = useState<number>(0);
|
|
||||||
|
|
||||||
const { messages } = useTendermintWebsocket({
|
|
||||||
query: "tm.event = 'NewBlock'",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (messages.length > 0) {
|
|
||||||
setBlocksToLoad((prev) => prev + 1);
|
|
||||||
}
|
|
||||||
}, [messages]);
|
|
||||||
|
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
refetch();
|
refetch();
|
||||||
setBlocksToLoad(0);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-4">
|
<Button onClick={refresh} data-testid="refresh" size="xs">
|
||||||
<span data-testid="new-blocks">{blocksToLoad} new blocks - </span>
|
<Icon name="refresh" className="!align-baseline mr-2" size={3} />
|
||||||
<ButtonLink onClick={refresh} data-testid="refresh">
|
{t('Load new')}
|
||||||
{t('refresh to see latest')}
|
</Button>
|
||||||
</ButtonLink>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
|
import {
|
||||||
|
useEnvironment,
|
||||||
|
useNodeSwitcherStore,
|
||||||
|
} from '@vegaprotocol/environment';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
|
import { Link, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { ENV } from '../../config/env';
|
import { ENV } from '../../config/env';
|
||||||
|
import { Routes } from '../../routes/route-names';
|
||||||
|
import { Link as RouteLink } from 'react-router-dom';
|
||||||
|
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
|
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
|
||||||
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
const setNodeSwitcherOpen = useNodeSwitcherStore(
|
||||||
|
(store) => store.setDialogOpen
|
||||||
|
);
|
||||||
|
|
||||||
const { screenSize } = useScreenDimensions();
|
const { screenSize } = useScreenDimensions();
|
||||||
const showFullFeedbackLabel = useMemo(
|
const showFullFeedbackLabel = useMemo(
|
||||||
() => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
|
() => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
|
||||||
@@ -15,56 +23,53 @@ export const Footer = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<footer className="grid grid-rows-2 lg:grid-cols-[1fr_auto] text-xs md:text-md lg:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
|
||||||
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
|
<div className="flex justify-between gap-2 align-middle">
|
||||||
<div className="flex justify-between gap-2 align-middle">
|
{GIT_COMMIT_HASH && (
|
||||||
{GIT_COMMIT_HASH && (
|
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
||||||
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
<p data-testid="git-commit-hash">
|
||||||
<p data-testid="git-commit-hash">
|
{t('Version')}:{' '}
|
||||||
{t('Version')}:{' '}
|
<Link
|
||||||
<Link
|
href={
|
||||||
href={
|
GIT_ORIGIN_URL
|
||||||
GIT_ORIGIN_URL
|
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
|
||||||
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
|
: undefined
|
||||||
: undefined
|
}
|
||||||
}
|
target={GIT_ORIGIN_URL ? '_blank' : undefined}
|
||||||
target={GIT_ORIGIN_URL ? '_blank' : undefined}
|
>
|
||||||
>
|
{GIT_COMMIT_HASH}
|
||||||
{GIT_COMMIT_HASH}
|
</Link>
|
||||||
</Link>
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
|
||||||
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
|
|
||||||
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
|
|
||||||
{t('Change')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{ENV.addresses.feedback ? (
|
||||||
<div className="flex pl-2 content-center">
|
<div className="flex pl-2 content-center">
|
||||||
<ExternalLink href={ENV.addresses.feedback}>
|
<ExternalLink href={ENV.addresses.feedback}>
|
||||||
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
|
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
</footer>
|
</div>
|
||||||
<NodeSwitcherDialog
|
<div className="pl-2 align-center lg:align-right lg:flex lg:justify-end gap-2 align-middle lg:max-w-xs lg:ml-auto">
|
||||||
open={nodeSwitcherOpen}
|
<RouteLink to={`/${Routes.DISCLAIMER}`} className="underline">
|
||||||
setOpen={setNodeSwitcherOpen}
|
Disclaimer
|
||||||
/>
|
</RouteLink>
|
||||||
</>
|
</div>
|
||||||
|
</footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NodeUrl = ({ url }: { url: string }) => {
|
export const NodeUrl = ({ url }: { url: string }) => {
|
||||||
// get base url from api url, api sub domain
|
// get base url from api url, api sub domain
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, '');
|
const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, '');
|
||||||
return (
|
return <span className="cursor-default">{nodeUrl}</span>;
|
||||||
<Link href={'https://' + nodeUrl} target="_blank">
|
|
||||||
{nodeUrl}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Header } from './header';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/environment', () => ({
|
jest.mock('@vegaprotocol/environment', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/environment'),
|
||||||
NetworkSwitcher: () => (
|
NetworkSwitcher: () => (
|
||||||
<div data-testid="network-switcher">NetworkSwitcher</div>
|
<div data-testid="network-switcher">NetworkSwitcher</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export interface InfoBlockProps {
|
export interface InfoBlockProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -1,33 +1,98 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import PartyLink from './party-link';
|
import PartyLink from './party-link';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { ExplorerNodeNamesDocument } from '../../../routes/validators/__generated__/NodeNames';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
const zeroes =
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000000';
|
||||||
|
const mocks = [
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
query: ExplorerNodeNamesDocument,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
nodesConnection: {
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '1',
|
||||||
|
name: 'Validator Node',
|
||||||
|
pubkey:
|
||||||
|
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e',
|
||||||
|
tmPubkey: 'tmPubkey1',
|
||||||
|
ethereumAddress: '0x123456789',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '2',
|
||||||
|
name: 'Node 2',
|
||||||
|
pubkey: 'pubkey2',
|
||||||
|
tmPubkey: 'tmPubkey2',
|
||||||
|
ethereumAddress: '0xabcdef123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
describe('PartyLink', () => {
|
describe('PartyLink', () => {
|
||||||
it('renders Network for 000.000 party', () => {
|
it('renders Network for 000.000 party', () => {
|
||||||
const zeroes =
|
const screen = render(
|
||||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
<MockedProvider>
|
||||||
const screen = render(<PartyLink id={zeroes} />);
|
<PartyLink id={zeroes} />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
expect(screen.getByText('Network')).toBeInTheDocument();
|
expect(screen.getByText('Network')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders Network for network party', () => {
|
it('renders Network for network party', () => {
|
||||||
const screen = render(<PartyLink id="network" />);
|
const screen = render(
|
||||||
|
<MockedProvider>
|
||||||
|
<PartyLink id="network" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
expect(screen.getByText('Network')).toBeInTheDocument();
|
expect(screen.getByText('Network')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders ID with no link for invalid party', () => {
|
it('renders ID with no link for invalid party', () => {
|
||||||
const screen = render(<PartyLink id="this-party-is-not-valid" />);
|
const screen = render(
|
||||||
|
<MockedProvider>
|
||||||
|
<PartyLink id="this-party-is-not-valid" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
|
expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('if the key is a validator, render their name instead', async () => {
|
||||||
|
const screen = render(
|
||||||
|
<MockedProvider mocks={mocks}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<PartyLink id="13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e" />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for hook to update with mock data
|
||||||
|
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
||||||
|
await expect(screen.getByText('Validator Node')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('links a valid party to the party page', () => {
|
it('links a valid party to the party page', () => {
|
||||||
const aValidParty =
|
const aValidParty =
|
||||||
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
|
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
|
||||||
|
|
||||||
const screen = render(
|
const screen = render(
|
||||||
<MemoryRouter>
|
<MockedProvider>
|
||||||
<PartyLink id={aValidParty} />
|
<MemoryRouter>
|
||||||
</MemoryRouter>
|
<PartyLink id={aValidParty} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const el = screen.getByText(aValidParty);
|
const el = screen.getByText(aValidParty);
|
||||||
|
|||||||
@@ -1,22 +1,44 @@
|
|||||||
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 type { ComponentProps } from 'react';
|
import { useMemo, type ComponentProps } from 'react';
|
||||||
import Hash from '../hash';
|
import Hash from '../hash';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
|
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
|
||||||
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
import { Icon, truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useExplorerNodeNamesQuery } from '../../../routes/validators/__generated__/NodeNames';
|
||||||
|
import type { ExplorerNodeNamesQuery } from '../../../routes/validators/__generated__/NodeNames';
|
||||||
|
|
||||||
export const SPECIAL_CASE_NETWORK_ID =
|
export const SPECIAL_CASE_NETWORK_ID =
|
||||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
'0000000000000000000000000000000000000000000000000000000000000000';
|
||||||
export const SPECIAL_CASE_NETWORK = 'network';
|
export const SPECIAL_CASE_NETWORK = 'network';
|
||||||
|
|
||||||
|
export function getNameForParty(id: string, data?: ExplorerNodeNamesQuery) {
|
||||||
|
if (!data || data?.nodesConnection?.edges?.length === 0) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validator = data.nodesConnection.edges?.find((e) => {
|
||||||
|
return e?.node.pubkey === id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validator) {
|
||||||
|
return validator.node.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
|
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
id: string;
|
id: string;
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
||||||
|
const { data } = useExplorerNodeNamesQuery();
|
||||||
|
const name = useMemo(() => getNameForParty(id, data), [data, id]);
|
||||||
|
const useName = name !== id;
|
||||||
|
|
||||||
// Some transactions will involve the 'network' party, which is alias for '000...000'
|
// 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'
|
// 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) {
|
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
|
||||||
@@ -38,13 +60,20 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<span className="whitespace-nowrap">
|
||||||
className="underline font-mono"
|
{useName && <Icon size={4} name="cube" className="mr-2" />}
|
||||||
{...props}
|
<Link
|
||||||
to={`/${Routes.PARTIES}/${id}`}
|
className="underline font-mono"
|
||||||
>
|
{...props}
|
||||||
<Hash text={truncate ? truncateMiddle(id) : id} />
|
to={`/${Routes.PARTIES}/${id}`}
|
||||||
</Link>
|
>
|
||||||
|
{useName ? (
|
||||||
|
name
|
||||||
|
) : (
|
||||||
|
<Hash text={truncate ? truncateMiddle(id, 4, 4) : id} />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import type { MarketInfoWithData } from '@vegaprotocol/market-info';
|
import type { MarketInfoWithData } from '@vegaprotocol/markets';
|
||||||
import { LiquidityInfoPanel } from '@vegaprotocol/market-info';
|
import { PriceMonitoringBoundsInfoPanel } from '@vegaprotocol/markets';
|
||||||
import { LiquidityMonitoringParametersInfoPanel } from '@vegaprotocol/market-info';
|
|
||||||
import {
|
import {
|
||||||
|
LiquidityInfoPanel,
|
||||||
|
LiquidityMonitoringParametersInfoPanel,
|
||||||
InstrumentInfoPanel,
|
InstrumentInfoPanel,
|
||||||
KeyDetailsInfoPanel,
|
KeyDetailsInfoPanel,
|
||||||
LiquidityPriceRangeInfoPanel,
|
LiquidityPriceRangeInfoPanel,
|
||||||
@@ -12,20 +13,18 @@ import {
|
|||||||
RiskModelInfoPanel,
|
RiskModelInfoPanel,
|
||||||
RiskParametersInfoPanel,
|
RiskParametersInfoPanel,
|
||||||
SettlementAssetInfoPanel,
|
SettlementAssetInfoPanel,
|
||||||
} from '@vegaprotocol/market-info';
|
} from '@vegaprotocol/markets';
|
||||||
import { MarketInfoTable } from '@vegaprotocol/market-info';
|
import { MarketInfoTable } from '@vegaprotocol/markets';
|
||||||
import type { DataSourceDefinition } from '@vegaprotocol/types';
|
import type { DataSourceDefinition } from '@vegaprotocol/types';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
|
|
||||||
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||||
if (!market) return null;
|
if (!market) return null;
|
||||||
|
|
||||||
const settlementData =
|
const settlementData = market.tradableInstrument.instrument.product
|
||||||
market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData
|
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
|
||||||
.data;
|
const terminationData = market.tradableInstrument.instrument.product
|
||||||
const terminationData =
|
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
|
||||||
market.tradableInstrument.instrument.product
|
|
||||||
.dataSourceSpecForTradingTermination.data;
|
|
||||||
|
|
||||||
const getSigners = (data: DataSourceDefinition) => {
|
const getSigners = (data: DataSourceDefinition) => {
|
||||||
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
|
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
|
||||||
@@ -41,133 +40,69 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const oraclePanels = isEqual(
|
const showTwoOracles = isEqual(
|
||||||
getSigners(settlementData),
|
getSigners(settlementData),
|
||||||
getSigners(terminationData)
|
getSigners(terminationData)
|
||||||
)
|
);
|
||||||
? [
|
|
||||||
{
|
|
||||||
title: t('Settlement Oracle'),
|
|
||||||
content: (
|
|
||||||
<OracleInfoPanel
|
|
||||||
noBorder={false}
|
|
||||||
market={market}
|
|
||||||
type="settlementData"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('Termination Oracle'),
|
|
||||||
content: (
|
|
||||||
<OracleInfoPanel
|
|
||||||
noBorder={false}
|
|
||||||
market={market}
|
|
||||||
type="termination"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
title: t('Oracle'),
|
|
||||||
content: (
|
|
||||||
<OracleInfoPanel
|
|
||||||
noBorder={false}
|
|
||||||
market={market}
|
|
||||||
type="settlementData"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const panels = [
|
const headerClassName = 'font-alpha calt text-xl mt-4 border-b-2 pb-2';
|
||||||
{
|
|
||||||
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={{
|
|
||||||
maxValidPrice: trigger.maxValidPrice,
|
|
||||||
minValidPrice: trigger.minValidPrice,
|
|
||||||
}}
|
|
||||||
decimalPlaces={market.decimalPlaces}
|
|
||||||
/>
|
|
||||||
<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} />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
...oraclePanels,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
{panels.map((p) => (
|
<h2 className={headerClassName}>{t('Key details')}</h2>
|
||||||
<div key={p.title} className="mb-3">
|
<KeyDetailsInfoPanel market={market} />
|
||||||
<h2 className="font-alpha calt text-xl">{p.title}</h2>
|
<h2 className={headerClassName}>{t('Instrument')}</h2>
|
||||||
{p.content}
|
<InstrumentInfoPanel market={market} />
|
||||||
</div>
|
<h2 className={headerClassName}>{t('Settlement asset')}</h2>
|
||||||
|
<SettlementAssetInfoPanel market={market} />
|
||||||
|
<h2 className={headerClassName}>{t('Metadata')}</h2>
|
||||||
|
<MetadataInfoPanel market={market} />
|
||||||
|
<h2 className={headerClassName}>{t('Risk model')}</h2>
|
||||||
|
<RiskModelInfoPanel market={market} />
|
||||||
|
<h2 className={headerClassName}>{t('Risk parameters')}</h2>
|
||||||
|
<RiskParametersInfoPanel market={market} />
|
||||||
|
<h2 className={headerClassName}>{t('Risk factors')}</h2>
|
||||||
|
<RiskFactorsInfoPanel market={market} />
|
||||||
|
{(market.data?.priceMonitoringBounds || []).map((trigger, i) => (
|
||||||
|
<>
|
||||||
|
<h2 className={headerClassName}>
|
||||||
|
{t('Price monitoring bounds %s', [(i + 1).toString()])}
|
||||||
|
</h2>
|
||||||
|
<PriceMonitoringBoundsInfoPanel
|
||||||
|
market={market}
|
||||||
|
triggerIndex={i + 1}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
))}
|
))}
|
||||||
</>
|
{(market.priceMonitoringSettings?.parameters?.triggers || []).map(
|
||||||
|
(trigger, i) => (
|
||||||
|
<>
|
||||||
|
<h2 className={headerClassName}>
|
||||||
|
{t('Price monitoring settings %s', [(i + 1).toString()])}
|
||||||
|
</h2>
|
||||||
|
<MarketInfoTable data={trigger} key={i} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<h2 className={headerClassName}>{t('Liquidity monitoring')}</h2>
|
||||||
|
<LiquidityMonitoringParametersInfoPanel market={market} />
|
||||||
|
<h2 className={headerClassName}>{t('Liquidity')}</h2>
|
||||||
|
<LiquidityInfoPanel market={market} />
|
||||||
|
<h2 className={headerClassName}>{t('Liquidity price range')}</h2>
|
||||||
|
<LiquidityPriceRangeInfoPanel market={market} />
|
||||||
|
{showTwoOracles ? (
|
||||||
|
<>
|
||||||
|
<h2 className={headerClassName}>{t('Settlement oracle')}</h2>
|
||||||
|
<OracleInfoPanel market={market} type="settlementData" />
|
||||||
|
<h2 className={headerClassName}>{t('Termination oracle')}</h2>
|
||||||
|
<OracleInfoPanel market={market} type="termination" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2 className={headerClassName}>{t('Oracle')}</h2>
|
||||||
|
<OracleInfoPanel market={market} type="settlementData" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { MarketFieldsFragment } from '@vegaprotocol/market-list';
|
import { useMemo } from 'react';
|
||||||
|
import type { MarketFieldsFragment } from '@vegaprotocol/markets';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import { AgGridColumn } from 'ag-grid-react';
|
import type { ColDef } from 'ag-grid-community';
|
||||||
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
||||||
import type {
|
import type {
|
||||||
VegaICellRendererParams,
|
VegaICellRendererParams,
|
||||||
@@ -39,6 +40,79 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const columnDefs = useMemo<ColDef[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
colId: 'code',
|
||||||
|
headerName: t('Code'),
|
||||||
|
field: 'tradableInstrument.instrument.code',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'name',
|
||||||
|
headerName: t('Name'),
|
||||||
|
field: 'tradableInstrument.instrument.name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: t('Status'),
|
||||||
|
field: 'state',
|
||||||
|
hide: window.innerWidth <= BREAKPOINT_MD,
|
||||||
|
valueGetter: ({
|
||||||
|
data,
|
||||||
|
}: VegaValueGetterParams<MarketFieldsFragment>) => {
|
||||||
|
return data?.state ? MarketStateMapping[data?.state] : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'asset',
|
||||||
|
headerName: t('Settlement asset'),
|
||||||
|
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
|
||||||
|
hide: window.innerWidth <= BREAKPOINT_MD,
|
||||||
|
cellRenderer: ({
|
||||||
|
data,
|
||||||
|
}: VegaICellRendererParams<
|
||||||
|
MarketFieldsFragment,
|
||||||
|
'tradableInstrument.instrument.product.settlementAsset.symbol'
|
||||||
|
>) => {
|
||||||
|
const value =
|
||||||
|
data?.tradableInstrument.instrument.product.settlementAsset;
|
||||||
|
return value ? (
|
||||||
|
<ButtonLink
|
||||||
|
onClick={(e) => {
|
||||||
|
openAssetDetailsDialog(value.id, e.target as HTMLElement);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value.symbol}
|
||||||
|
</ButtonLink>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flex: 2,
|
||||||
|
headerName: t('Market ID'),
|
||||||
|
field: 'id',
|
||||||
|
hide: window.innerWidth <= BREAKPOINT_MD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'actions',
|
||||||
|
headerName: '',
|
||||||
|
field: 'id',
|
||||||
|
cellRenderer: ({
|
||||||
|
value,
|
||||||
|
}: VegaICellRendererParams<MarketFieldsFragment, 'id'>) =>
|
||||||
|
value ? (
|
||||||
|
<Link className="underline" to={value}>
|
||||||
|
{t('View details')}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[openAssetDetailsDialog]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AgGrid
|
<AgGrid
|
||||||
ref={gridRef}
|
ref={gridRef}
|
||||||
@@ -54,79 +128,13 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
|
|||||||
filterParams: { buttons: ['reset'] },
|
filterParams: { buttons: ['reset'] },
|
||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
}}
|
}}
|
||||||
|
columnDefs={columnDefs}
|
||||||
suppressCellFocus={true}
|
suppressCellFocus={true}
|
||||||
onRowClicked={({ data, event }: RowClickedEvent) => {
|
onRowClicked={({ data, event }: RowClickedEvent) => {
|
||||||
if ((event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON') {
|
if ((event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON') {
|
||||||
navigate(data.id);
|
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ fragment ExplorerDeterministicOrderFields on Order {
|
|||||||
remaining
|
remaining
|
||||||
size
|
size
|
||||||
rejectionReason
|
rejectionReason
|
||||||
|
peggedOrder {
|
||||||
|
reference
|
||||||
|
offset
|
||||||
|
}
|
||||||
party {
|
party {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as Types from '@vegaprotocol/types';
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
const defaultOptions = {} as const;
|
const defaultOptions = {} as const;
|
||||||
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
|
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | 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, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
|
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | 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 {
|
||||||
@@ -29,6 +29,10 @@ export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
|
|||||||
remaining
|
remaining
|
||||||
size
|
size
|
||||||
rejectionReason
|
rejectionReason
|
||||||
|
peggedOrder {
|
||||||
|
reference
|
||||||
|
offset
|
||||||
|
}
|
||||||
party {
|
party {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type Amend = components['schemas']['v1OrderAmendment'];
|
|||||||
|
|
||||||
function renderAmendOrderDetails(
|
function renderAmendOrderDetails(
|
||||||
id: string,
|
id: string,
|
||||||
version: number,
|
version: number | undefined,
|
||||||
amend: Amend,
|
amend: Amend,
|
||||||
mocks: MockedResponse[]
|
mocks: MockedResponse[]
|
||||||
) {
|
) {
|
||||||
@@ -25,7 +25,11 @@ function renderAmendOrderDetails(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderExistingAmend(id: string, version: number, amend: Amend) {
|
function renderExistingAmend(
|
||||||
|
id: string,
|
||||||
|
version: number | undefined,
|
||||||
|
amend: Amend
|
||||||
|
) {
|
||||||
const mocks = [
|
const mocks = [
|
||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
@@ -49,6 +53,7 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
|
|||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||||
price: '200',
|
price: '200',
|
||||||
side: 'BUY',
|
side: 'BUY',
|
||||||
|
peggedOrder: null,
|
||||||
remaining: '99',
|
remaining: '99',
|
||||||
rejectionReason: 'rejection',
|
rejectionReason: 'rejection',
|
||||||
reference: '123',
|
reference: '123',
|
||||||
@@ -77,6 +82,56 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
query: ExplorerDeterministicOrderDocument,
|
||||||
|
variables: {
|
||||||
|
orderId: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
orderByID: {
|
||||||
|
__typename: 'Order',
|
||||||
|
id: '123',
|
||||||
|
type: 'GTT',
|
||||||
|
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||||
|
version: 100,
|
||||||
|
createdAt: '123',
|
||||||
|
updatedAt: '456',
|
||||||
|
expiresAt: '789',
|
||||||
|
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||||
|
peggedOrder: null,
|
||||||
|
price: '200',
|
||||||
|
side: 'BUY',
|
||||||
|
remaining: '99',
|
||||||
|
rejectionReason: 'rejection',
|
||||||
|
reference: '123',
|
||||||
|
size: '200',
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
id: '234',
|
||||||
|
},
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'amend-to-order-latest-version',
|
||||||
|
state: 'STATUS_ACTIVE',
|
||||||
|
positionDecimalPlaces: 2,
|
||||||
|
decimalPlaces: '5',
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
name: 'amend-to-order-latest-version-test',
|
||||||
|
product: {
|
||||||
|
__typename: 'Future',
|
||||||
|
quoteName: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
query: ExplorerMarketDocument,
|
query: ExplorerMarketDocument,
|
||||||
@@ -157,4 +212,15 @@ describe('Amend order details', () => {
|
|||||||
expect(await res.findByText('New price')).toBeInTheDocument();
|
expect(await res.findByText('New price')).toBeInTheDocument();
|
||||||
expect(await res.findByText('-7879')).toBeInTheDocument();
|
expect(await res.findByText('-7879')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Fetches latest version when version is not specified', async () => {
|
||||||
|
const amend: Amend = {
|
||||||
|
price: '-7879',
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = renderExistingAmend('123', undefined, amend);
|
||||||
|
expect(
|
||||||
|
await res.findByText('amend-to-order-latest-version')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { wrapperClasses } from './deterministic-order-details';
|
|||||||
export interface AmendOrderDetailsProps {
|
export interface AmendOrderDetailsProps {
|
||||||
id: string;
|
id: string;
|
||||||
amend: components['schemas']['v1OrderAmendment'];
|
amend: components['schemas']['v1OrderAmendment'];
|
||||||
// Version to fetch, with 0 being 'latest' and 1 being 'first'. Defaults to 0
|
// Version to fetch. Latest is provided by default
|
||||||
version?: number;
|
version?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,13 +34,11 @@ export function getSideDeltaColour(delta: string): string {
|
|||||||
* @param param0
|
* @param param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const AmendOrderDetails = ({
|
const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
|
||||||
id,
|
const variables = version ? { orderId: id, version } : { orderId: id };
|
||||||
version = 0,
|
|
||||||
amend,
|
|
||||||
}: AmendOrderDetailsProps) => {
|
|
||||||
const { data, error } = useExplorerDeterministicOrderQuery({
|
const { data, error } = useExplorerDeterministicOrderQuery({
|
||||||
variables: { orderId: id, version },
|
variables,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error || (data && !data.orderByID)) {
|
if (error || (data && !data.orderByID)) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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';
|
import SizeInMarket from '../size-in-market/size-in-market';
|
||||||
|
import { TxOrderPeggedReference } from '../txs/details/order/tx-order-peg';
|
||||||
|
|
||||||
export interface DeterministicOrderDetailsProps {
|
export interface DeterministicOrderDetailsProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -68,25 +69,35 @@ const DeterministicOrderDetails = ({
|
|||||||
<span className="mx-5 text-base">@</span>
|
<span className="mx-5 text-base">@</span>
|
||||||
<PriceInMarket price={o.price} marketId={o.market.id} />
|
<PriceInMarket price={o.price} marketId={o.market.id} />
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 mb-4">
|
<p className="text-gray-200">
|
||||||
In <MarketLink id={o.market.id} /> at <Time date={o.createdAt} />.
|
In <MarketLink id={o.market.id} /> at <Time date={o.createdAt} />.
|
||||||
</p>
|
</p>
|
||||||
|
{o.peggedOrder ? (
|
||||||
|
<p className="text-gray-200">
|
||||||
|
{t('Price peg')}:{' '}
|
||||||
|
<TxOrderPeggedReference
|
||||||
|
side={o.side}
|
||||||
|
reference={o.peggedOrder.reference}
|
||||||
|
offset={o.peggedOrder.offset}
|
||||||
|
marketId={o.market.id}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{o.reference ? (
|
{o.reference ? (
|
||||||
<p className="text-gray-500 mb-4">
|
<p className="text-gray-500 mt-4">
|
||||||
<span>{t('Reference')}</span>: {o.reference}
|
<span>{t('Reference')}</span>: {o.reference}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="grid md:grid-cols-4 gap-x-6">
|
<div className="grid md:grid-cols-4 gap-x-6 mt-4">
|
||||||
{version !== 0 ? null : (
|
<div className="mb-12 md:mb-0">
|
||||||
<div className="mb-12 md:mb-0">
|
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||||
<h2 className="text-2xl font-bold text-dark mb-4">
|
{t('Status')}
|
||||||
{t('Status')}
|
</h2>
|
||||||
</h2>
|
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
|
||||||
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
|
{statusText[o.status]}
|
||||||
{statusText[o.status]}
|
</h5>
|
||||||
</h5>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<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>
|
||||||
@@ -95,17 +106,6 @@ const DeterministicOrderDetails = ({
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{version !== 0 ? null : (
|
|
||||||
<div className="">
|
|
||||||
<h2 className="text-2xl font-bold text-dark mb-4">
|
|
||||||
{t('Remaining')}
|
|
||||||
</h2>
|
|
||||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
|
||||||
<SizeInMarket size={o.remaining} marketId={o.market.id} />
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="">
|
<div className="">
|
||||||
<h2 className="text-2xl font-bold text-dark mb-4">
|
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||||
{t('Version')}
|
{t('Version')}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const mock = {
|
|||||||
side: 'SIDE_BUY',
|
side: 'SIDE_BUY',
|
||||||
remaining: '100',
|
remaining: '100',
|
||||||
size: '100',
|
size: '100',
|
||||||
|
peggedOrder: null,
|
||||||
party: {
|
party: {
|
||||||
id: '456',
|
id: '456',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals';
|
import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals';
|
||||||
import { VoteProgress } from '@vegaprotocol/proposals';
|
import { VoteProgress } from '@vegaprotocol/proposals';
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import { AgGridColumn } from 'ag-grid-react';
|
|
||||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
||||||
import type {
|
import type {
|
||||||
@@ -9,7 +8,7 @@ import type {
|
|||||||
VegaValueFormatterParams,
|
VegaValueFormatterParams,
|
||||||
} from '@vegaprotocol/datagrid';
|
} from '@vegaprotocol/datagrid';
|
||||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { RowClickedEvent } from 'ag-grid-community';
|
import type { RowClickedEvent, ColDef } from 'ag-grid-community';
|
||||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import {
|
import {
|
||||||
@@ -64,7 +63,128 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
|||||||
title: '',
|
title: '',
|
||||||
content: null,
|
content: null,
|
||||||
});
|
});
|
||||||
|
const columnDefs = useMemo<ColDef[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
colId: 'title',
|
||||||
|
headerName: t('Title'),
|
||||||
|
field: 'rationale.title',
|
||||||
|
flex: 2,
|
||||||
|
wrapText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'type',
|
||||||
|
maxWidth: 180,
|
||||||
|
hide: window.innerWidth <= BREAKPOINT_MD,
|
||||||
|
headerName: t('Type'),
|
||||||
|
field: 'terms.change.__typename',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxWidth: 100,
|
||||||
|
headerName: t('State'),
|
||||||
|
field: 'state',
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) => {
|
||||||
|
return value ? ProposalStateMapping[value] : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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)) : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'eDate',
|
||||||
|
maxWidth: 150,
|
||||||
|
hide: window.innerWidth <= BREAKPOINT_MD,
|
||||||
|
headerName: t('Enactment date'),
|
||||||
|
field: 'terms.enactmentDatetime',
|
||||||
|
valueFormatte: ({
|
||||||
|
value,
|
||||||
|
}: VegaValueFormatterParams<
|
||||||
|
ProposalListFieldsFragment,
|
||||||
|
'terms.enactmentDatetime'
|
||||||
|
>) => {
|
||||||
|
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[requiredMajorityPercentage, tokenLink]
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AgGrid
|
<AgGrid
|
||||||
@@ -83,6 +203,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
|||||||
filterParams: { buttons: ['reset'] },
|
filterParams: { buttons: ['reset'] },
|
||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
}}
|
}}
|
||||||
|
columnDefs={columnDefs}
|
||||||
suppressCellFocus={true}
|
suppressCellFocus={true}
|
||||||
onRowClicked={({ data, event }: RowClickedEvent) => {
|
onRowClicked={({ data, event }: RowClickedEvent) => {
|
||||||
if (
|
if (
|
||||||
@@ -94,128 +215,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
|||||||
window.open(proposalPage, '_blank');
|
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
|
<JsonViewerDialog
|
||||||
open={dialog.open}
|
open={dialog.open}
|
||||||
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
|
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
|
||||||
|
|||||||
+43
-16
@@ -1,19 +1,24 @@
|
|||||||
|
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
// https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go
|
// https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go
|
||||||
export const ErrorCodes = new Map([
|
export const ErrorCodes = new Map([
|
||||||
[51, 'Transaction failed validation'],
|
[51, 'Transaction failed validation'],
|
||||||
[60, 'Transaction could not be decoded'],
|
[60, 'Transaction could not be decoded'],
|
||||||
[70, 'Error'],
|
[70, 'Error'],
|
||||||
|
[71, 'Partial success/error'],
|
||||||
[80, 'Unknown command'],
|
[80, 'Unknown command'],
|
||||||
[89, 'Rejected as spam'],
|
[89, 'Rejected as spam'],
|
||||||
[0, 'Success'],
|
[0, 'Success'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const successCodes = new Set([0]);
|
export const successCodes = new Set([0, 71]);
|
||||||
|
|
||||||
interface ChainResponseCodeProps {
|
interface ChainResponseCodeProps {
|
||||||
code: number;
|
code: number;
|
||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
hideIfOk?: boolean;
|
||||||
|
small?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,10 +30,22 @@ export const ChainResponseCode = ({
|
|||||||
code,
|
code,
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
error,
|
error,
|
||||||
|
hideIfOk = false,
|
||||||
|
small = false,
|
||||||
}: ChainResponseCodeProps) => {
|
}: ChainResponseCodeProps) => {
|
||||||
const isSuccess = successCodes.has(code);
|
if (hideIfOk && code === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const icon = isSuccess ? '✅' : '❌';
|
const isSuccess = successCodes.has(code);
|
||||||
|
const size = small ? 3 : 4;
|
||||||
|
const successColour =
|
||||||
|
code === 71 ? '!fill-vega-orange' : '!fill-vega-green-600';
|
||||||
|
const icon = isSuccess ? (
|
||||||
|
<Icon size={size} name="tick-circle" className={`${successColour}`} />
|
||||||
|
) : (
|
||||||
|
<Icon size={size} name="cross" className="!fill-vega-pink-500" />
|
||||||
|
);
|
||||||
const label = ErrorCodes.get(code) || 'Unknown response code';
|
const label = ErrorCodes.get(code) || 'Unknown response code';
|
||||||
|
|
||||||
// Hack for batches with many errors - see https://github.com/vegaprotocol/vega/issues/7245
|
// Hack for batches with many errors - see https://github.com/vegaprotocol/vega/issues/7245
|
||||||
@@ -36,18 +53,28 @@ export const ChainResponseCode = ({
|
|||||||
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
|
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div title={`Response code: ${code} - ${label}`} className="inline-block">
|
<Tooltip
|
||||||
<span
|
description={
|
||||||
className="mr-2"
|
<span>
|
||||||
aria-label={isSuccess ? 'Success' : 'Warning'}
|
Response code: {code} - {label}
|
||||||
role="img"
|
</span>
|
||||||
>
|
}
|
||||||
{icon}
|
>
|
||||||
</span>
|
<div className="mt-[-1px] inline-block">
|
||||||
{hideLabel ? null : <span>{label}</span>}
|
<span
|
||||||
{!hideLabel && !!displayError ? (
|
className="mr-2"
|
||||||
<span className="ml-1 whitespace-pre">— {displayError}</span>
|
aria-label={isSuccess ? 'Success' : 'Warning'}
|
||||||
) : null}
|
role="img"
|
||||||
</div>
|
>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
{hideLabel ? null : <span>{label}</span>}
|
||||||
|
{!hideLabel && !!displayError ? (
|
||||||
|
<span className="ml-1 whitespace-pre">
|
||||||
|
— {displayError}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import type { TxDetailsOrderProps } from './tx-order-peg';
|
||||||
|
import { TxOrderPeggedReference, getMarketDecimals } from './tx-order-peg';
|
||||||
|
import { useExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
|
||||||
|
import type { ExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
|
||||||
|
import { PeggedReference, Side } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
// Mock the useExplorerMarketQuery hook
|
||||||
|
jest.mock('../../../links/market-link/__generated__/Market', () => ({
|
||||||
|
useExplorerMarketQuery: jest.fn().mockReturnValue({
|
||||||
|
data: {
|
||||||
|
market: { decimalPlaces: 0 },
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('getSettlementAsset', () => {
|
||||||
|
it('should return the decimal places if data is defined', () => {
|
||||||
|
const data = {
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: '123',
|
||||||
|
decimalPlaces: 8,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getMarketDecimals(data as Partial<ExplorerMarketQuery>);
|
||||||
|
|
||||||
|
expect(result).toEqual(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 0 if data is undefined', () => {
|
||||||
|
const result = getMarketDecimals(undefined);
|
||||||
|
|
||||||
|
expect(result).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TxOrderPeggedReference', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock the useExplorerMarketQuery hook return value
|
||||||
|
(useExplorerMarketQuery as jest.Mock).mockReturnValue({
|
||||||
|
data: {
|
||||||
|
settlementAsset: 'some-settlement-asset',
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the offset and reference correctly', () => {
|
||||||
|
const props: TxDetailsOrderProps = {
|
||||||
|
side: Side.SIDE_BUY,
|
||||||
|
offset: '10',
|
||||||
|
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||||
|
marketId: 'some-market-id',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getByTestId } = render(<TxOrderPeggedReference {...props} />);
|
||||||
|
|
||||||
|
expect(getByTestId('pegged-reference')).toHaveTextContent('Mid + 10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if the reference is "PEGGED_REFERENCE_UNSPECIFIED"', () => {
|
||||||
|
const props: TxDetailsOrderProps = {
|
||||||
|
side: Side.SIDE_BUY,
|
||||||
|
offset: '10',
|
||||||
|
reference: 'PEGGED_REFERENCE_UNSPECIFIED',
|
||||||
|
marketId: 'some-market-id',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = render(<TxOrderPeggedReference {...props} />);
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the offset without formatting initially, then render the formatted version', () => {
|
||||||
|
const props: TxDetailsOrderProps = {
|
||||||
|
side: Side.SIDE_BUY,
|
||||||
|
offset: '10',
|
||||||
|
reference: PeggedReference.PEGGED_REFERENCE_BEST_ASK,
|
||||||
|
marketId: 'some-market-id',
|
||||||
|
};
|
||||||
|
|
||||||
|
(useExplorerMarketQuery as jest.Mock).mockReturnValue({
|
||||||
|
data: null,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const screen = render(<TxOrderPeggedReference {...props} />);
|
||||||
|
expect(screen.getByTestId('pegged-reference')).toHaveTextContent(
|
||||||
|
'Ask + 10'
|
||||||
|
);
|
||||||
|
|
||||||
|
(useExplorerMarketQuery as jest.Mock).mockReturnValue({
|
||||||
|
data: {
|
||||||
|
market: {
|
||||||
|
decimalPlaces: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
screen.rerender(<TxOrderPeggedReference {...props} />);
|
||||||
|
expect(screen.getByTestId('pegged-reference')).toHaveTextContent(
|
||||||
|
'Ask + 0.000000001'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { TableCell, TableRow } from '../../../table';
|
||||||
|
import type { VegaPeggedReference } from '../liquidity-provision/liquidity-provision-details';
|
||||||
|
import { Side, PeggedReferenceMapping } from '@vegaprotocol/types';
|
||||||
|
import { useExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
|
||||||
|
import type { ExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
|
||||||
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
|
|
||||||
|
export interface TxDetailsOrderProps {
|
||||||
|
offset: string;
|
||||||
|
reference: VegaPeggedReference;
|
||||||
|
marketId: string;
|
||||||
|
side: Side;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMarketDecimals(
|
||||||
|
data: ExplorerMarketQuery | undefined
|
||||||
|
): number {
|
||||||
|
return data?.market?.decimalPlaces || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summarises an order's peg
|
||||||
|
*/
|
||||||
|
export const TxOrderPeggedReferenceRow = ({
|
||||||
|
offset,
|
||||||
|
reference,
|
||||||
|
marketId,
|
||||||
|
side,
|
||||||
|
}: TxDetailsOrderProps) => {
|
||||||
|
return (
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Pegged order')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TxOrderPeggedReference
|
||||||
|
side={side}
|
||||||
|
offset={offset}
|
||||||
|
reference={reference}
|
||||||
|
marketId={marketId}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TxOrderPeggedReference = ({
|
||||||
|
offset,
|
||||||
|
reference,
|
||||||
|
marketId,
|
||||||
|
side,
|
||||||
|
}: TxDetailsOrderProps) => {
|
||||||
|
const { data, loading } = useExplorerMarketQuery({
|
||||||
|
variables: { id: marketId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const direction = side === Side.SIDE_BUY ? '+' : '-';
|
||||||
|
const decimalPlaces = getMarketDecimals(data);
|
||||||
|
|
||||||
|
if (reference === 'PEGGED_REFERENCE_UNSPECIFIED') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span data-testid="pegged-reference">
|
||||||
|
{PeggedReferenceMapping[reference]}
|
||||||
|
{direction}
|
||||||
|
{!loading && data
|
||||||
|
? addDecimalsFormatNumber(offset, decimalPlaces)
|
||||||
|
: offset}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { TxDetailsLiquidityAmendment } from './tx-liquidity-amend';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
describe('TxDetailsLiquidityAmendment', () => {
|
||||||
|
const mockTxData = {
|
||||||
|
hash: 'test',
|
||||||
|
command: {
|
||||||
|
liquidityProvisionAmendment: {
|
||||||
|
marketId: 'BTC-USD',
|
||||||
|
commitmentAmount: 100,
|
||||||
|
fee: '0.01',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockPubKey = '123';
|
||||||
|
const mockBlockData = {
|
||||||
|
result: {
|
||||||
|
block: {
|
||||||
|
header: {
|
||||||
|
height: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should render the component with correct data', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<MockedProvider>
|
||||||
|
<MemoryRouter>
|
||||||
|
<TxDetailsLiquidityAmendment
|
||||||
|
txData={mockTxData as BlockExplorerTransactionResult}
|
||||||
|
pubKey={mockPubKey}
|
||||||
|
blockData={mockBlockData as TendermintBlocksResponse}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText('Market')).toBeInTheDocument();
|
||||||
|
expect(getByText('BTC-USD')).toBeInTheDocument();
|
||||||
|
expect(getByText('Commitment amount')).toBeInTheDocument();
|
||||||
|
expect(getByText('100')).toBeInTheDocument();
|
||||||
|
expect(getByText('Fee')).toBeInTheDocument();
|
||||||
|
expect(getByText('1%')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display awaiting message when tx data is undefined', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<MockedProvider>
|
||||||
|
<MemoryRouter>
|
||||||
|
<TxDetailsLiquidityAmendment
|
||||||
|
txData={undefined}
|
||||||
|
pubKey={mockPubKey}
|
||||||
|
blockData={mockBlockData as TendermintBlocksResponse}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getByText('Awaiting Block Explorer transaction details')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display awaiting message when liquidityProvisionAmendment is undefined', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<MockedProvider>
|
||||||
|
<MemoryRouter>
|
||||||
|
<TxDetailsLiquidityAmendment
|
||||||
|
txData={{ command: {} } as BlockExplorerTransactionResult}
|
||||||
|
pubKey={mockPubKey}
|
||||||
|
blockData={mockBlockData as TendermintBlocksResponse}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getByText('Awaiting Block Explorer transaction details')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,6 +7,7 @@ import { TableCell, TableRow, TableWithTbody } from '../../table';
|
|||||||
import type { components } from '../../../../types/explorer';
|
import type { components } from '../../../../types/explorer';
|
||||||
import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details';
|
import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details';
|
||||||
import PriceInMarket from '../../price-in-market/price-in-market';
|
import PriceInMarket from '../../price-in-market/price-in-market';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
export type LiquidityAmendment =
|
export type LiquidityAmendment =
|
||||||
components['schemas']['v1LiquidityProvisionAmendment'];
|
components['schemas']['v1LiquidityProvisionAmendment'];
|
||||||
@@ -34,6 +35,10 @@ export const TxDetailsLiquidityAmendment = ({
|
|||||||
txData.command.liquidityProvisionAmendment;
|
txData.command.liquidityProvisionAmendment;
|
||||||
const marketId: string = amendment.marketId || '-';
|
const marketId: string = amendment.marketId || '-';
|
||||||
|
|
||||||
|
const fee = amendment.fee
|
||||||
|
? new BigNumber(amendment.fee).times(100).toString()
|
||||||
|
: '-';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||||
@@ -63,7 +68,7 @@ export const TxDetailsLiquidityAmendment = ({
|
|||||||
{amendment.fee ? (
|
{amendment.fee ? (
|
||||||
<TableRow modifier="bordered">
|
<TableRow modifier="bordered">
|
||||||
<TableCell>{t('Fee')}</TableCell>
|
<TableCell>{t('Fee')}</TableCell>
|
||||||
<TableCell>{amendment.fee}%</TableCell>
|
<TableCell>{fee}%</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : null}
|
) : null}
|
||||||
</TableWithTbody>
|
</TableWithTbody>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
describe('TxDetailsLiquiditySubmission', () => {
|
||||||
|
const mockTxData = {
|
||||||
|
hash: 'test',
|
||||||
|
command: {
|
||||||
|
liquidityProvisionSubmission: {
|
||||||
|
marketId: 'BTC-USD',
|
||||||
|
commitmentAmount: 100,
|
||||||
|
fee: '0.01',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockPubKey = '123';
|
||||||
|
const mockBlockData = {
|
||||||
|
result: {
|
||||||
|
block: {
|
||||||
|
header: {
|
||||||
|
height: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should render the component with correct data', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<MockedProvider>
|
||||||
|
<MemoryRouter>
|
||||||
|
<TxDetailsLiquiditySubmission
|
||||||
|
txData={mockTxData as BlockExplorerTransactionResult}
|
||||||
|
pubKey={mockPubKey}
|
||||||
|
blockData={mockBlockData as TendermintBlocksResponse}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText('Market')).toBeInTheDocument();
|
||||||
|
expect(getByText('BTC-USD')).toBeInTheDocument();
|
||||||
|
expect(getByText('Commitment amount')).toBeInTheDocument();
|
||||||
|
expect(getByText('100')).toBeInTheDocument();
|
||||||
|
expect(getByText('Fee')).toBeInTheDocument();
|
||||||
|
expect(getByText('1%')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display awaiting message when tx data is undefined', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<MockedProvider>
|
||||||
|
<MemoryRouter>
|
||||||
|
<TxDetailsLiquiditySubmission
|
||||||
|
txData={undefined}
|
||||||
|
pubKey={mockPubKey}
|
||||||
|
blockData={mockBlockData as TendermintBlocksResponse}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getByText('Awaiting Block Explorer transaction details')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display awaiting message when liquidityProvisionSubmission is undefined', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<MockedProvider>
|
||||||
|
<MemoryRouter>
|
||||||
|
<TxDetailsLiquiditySubmission
|
||||||
|
txData={{ command: {} } as BlockExplorerTransactionResult}
|
||||||
|
pubKey={mockPubKey}
|
||||||
|
blockData={mockBlockData as TendermintBlocksResponse}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getByText('Awaiting Block Explorer transaction details')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,6 +7,7 @@ import { TableCell, TableRow, TableWithTbody } from '../../table';
|
|||||||
import type { components } from '../../../../types/explorer';
|
import type { components } from '../../../../types/explorer';
|
||||||
import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details';
|
import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details';
|
||||||
import PriceInMarket from '../../price-in-market/price-in-market';
|
import PriceInMarket from '../../price-in-market/price-in-market';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
export type LiquiditySubmission =
|
export type LiquiditySubmission =
|
||||||
components['schemas']['v1LiquidityProvisionSubmission'];
|
components['schemas']['v1LiquidityProvisionSubmission'];
|
||||||
@@ -33,6 +34,10 @@ export const TxDetailsLiquiditySubmission = ({
|
|||||||
txData.command.liquidityProvisionSubmission;
|
txData.command.liquidityProvisionSubmission;
|
||||||
const marketId: string = submission.marketId || '-';
|
const marketId: string = submission.marketId || '-';
|
||||||
|
|
||||||
|
const fee = submission.fee
|
||||||
|
? new BigNumber(submission.fee).times(100).toString()
|
||||||
|
: '-';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||||
@@ -62,7 +67,7 @@ export const TxDetailsLiquiditySubmission = ({
|
|||||||
{submission.fee ? (
|
{submission.fee ? (
|
||||||
<TableRow modifier="bordered">
|
<TableRow modifier="bordered">
|
||||||
<TableCell>{t('Fee')}</TableCell>
|
<TableCell>{t('Fee')}</TableCell>
|
||||||
<TableCell>{submission.fee}%</TableCell>
|
<TableCell>{fee}%</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : null}
|
) : null}
|
||||||
</TableWithTbody>
|
</TableWithTbody>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { TableCell, TableRow, TableWithTbody } from '../../table';
|
|||||||
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
|
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
|
||||||
import DeterministicOrderDetails from '../../order-details/deterministic-order-details';
|
import DeterministicOrderDetails from '../../order-details/deterministic-order-details';
|
||||||
import Hash from '../../links/hash';
|
import Hash from '../../links/hash';
|
||||||
|
import { TxOrderPeggedReferenceRow } from './order/tx-order-peg';
|
||||||
|
|
||||||
interface TxDetailsOrderProps {
|
interface TxDetailsOrderProps {
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
@@ -29,6 +30,8 @@ export const TxDetailsOrder = ({
|
|||||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
}
|
}
|
||||||
const marketId = txData.command.orderSubmission.marketId || '-';
|
const marketId = txData.command.orderSubmission.marketId || '-';
|
||||||
|
const reference = txData.command.orderSubmission.peggedOrder;
|
||||||
|
const side = txData.command.orderSubmission.side;
|
||||||
|
|
||||||
let deterministicId = '';
|
let deterministicId = '';
|
||||||
|
|
||||||
@@ -63,6 +66,14 @@ export const TxDetailsOrder = ({
|
|||||||
<MarketLink id={marketId} />
|
<MarketLink id={marketId} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
{reference ? (
|
||||||
|
<TxOrderPeggedReferenceRow
|
||||||
|
side={side}
|
||||||
|
offset={reference.offset}
|
||||||
|
reference={reference.reference}
|
||||||
|
marketId={marketId}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</TableWithTbody>
|
</TableWithTbody>
|
||||||
|
|
||||||
{deterministicId.length > 0 ? (
|
{deterministicId.length > 0 ? (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint
|
|||||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||||
import ProposalLink from '../../links/proposal-link/proposal-link';
|
import ProposalLink from '../../links/proposal-link/proposal-link';
|
||||||
|
import { VoteIcon } from '../../vote-icon/vote-icon';
|
||||||
|
|
||||||
interface TxProposalVoteProps {
|
interface TxProposalVoteProps {
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
@@ -30,27 +31,22 @@ export const TxProposalVote = ({
|
|||||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const vote = txData.command.voteSubmission.value ? '👍' : '👎';
|
const vote = txData.command.voteSubmission.value === 'VALUE_YES';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||||
<TableRow modifier="bordered">
|
|
||||||
<TableCell>{t('Proposal ID')}</TableCell>
|
|
||||||
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow modifier="bordered">
|
<TableRow modifier="bordered">
|
||||||
<TableCell>{t('Proposal details')}</TableCell>
|
<TableCell>{t('Proposal details')}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<ProposalLink id={txData.command.voteSubmission.proposalId} />
|
<ProposalLink id={txData.command.voteSubmission.proposalId} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow modifier="bordered">
|
|
||||||
<TableCell>{t('Proposal')}</TableCell>
|
|
||||||
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow modifier="bordered">
|
<TableRow modifier="bordered">
|
||||||
<TableCell>{t('Vote')}</TableCell>
|
<TableCell>{t('Vote')}</TableCell>
|
||||||
<TableCell>{vote}</TableCell>
|
<TableCell>
|
||||||
|
<VoteIcon vote={vote} />
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableWithTbody>
|
</TableWithTbody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { Proposal } from './tx-proposal';
|
||||||
|
import { proposalRequiresSignatureBundle } from './tx-proposal';
|
||||||
|
|
||||||
|
describe('proposalRequiresSignatureBundle', () => {
|
||||||
|
it('should return false for freeform proposals, which do not require a signature bundle to enact', () => {
|
||||||
|
const mock = {
|
||||||
|
terms: {
|
||||||
|
newFreeform: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(proposalRequiresSignatureBundle(mock)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for newMarket proposals, which do not require a signature bundle to enact', () => {
|
||||||
|
const mock = {
|
||||||
|
terms: {
|
||||||
|
newMarket: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(proposalRequiresSignatureBundle(mock)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for newAsset proposals, which do require a signature bundle to enact', () => {
|
||||||
|
const mock = {
|
||||||
|
terms: {
|
||||||
|
newAsset: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(proposalRequiresSignatureBundle(mock)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for updateAsset proposals, which do require a signature bundle to enact', () => {
|
||||||
|
const mock = {
|
||||||
|
terms: {
|
||||||
|
updateAsset: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(proposalRequiresSignatureBundle(mock)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when bad data is supplied', () => {
|
||||||
|
expect(
|
||||||
|
proposalRequiresSignatureBundle(false as unknown as Proposal)
|
||||||
|
).toEqual(false);
|
||||||
|
expect(
|
||||||
|
proposalRequiresSignatureBundle(undefined as unknown as Proposal)
|
||||||
|
).toEqual(false);
|
||||||
|
expect(
|
||||||
|
proposalRequiresSignatureBundle({ test: false } as unknown as Proposal)
|
||||||
|
).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,11 +28,16 @@ interface TxProposalProps {
|
|||||||
* @returns boolean True if a signature bundle is required. Used to fetch a signature bundle
|
* @returns boolean True if a signature bundle is required. Used to fetch a signature bundle
|
||||||
*/
|
*/
|
||||||
export function proposalRequiresSignatureBundle(proposal?: Proposal): boolean {
|
export function proposalRequiresSignatureBundle(proposal?: Proposal): boolean {
|
||||||
|
const proposalsThatRequireBundles = ['newAsset', 'updateAsset'];
|
||||||
|
|
||||||
if (!proposal?.terms) {
|
if (!proposal?.terms) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !!['newAsset', 'updateAsset'].filter((requiredIfExists) =>
|
|
||||||
has(proposal.terms, requiredIfExists)
|
return (
|
||||||
|
proposalsThatRequireBundles.filter((requiredIfExists) =>
|
||||||
|
has(proposal.terms, requiredIfExists)
|
||||||
|
).length > 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +86,7 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
|
|||||||
|
|
||||||
const tx = proposal.terms?.newAsset || proposal.terms?.updateAsset;
|
const tx = proposal.terms?.newAsset || proposal.terms?.updateAsset;
|
||||||
|
|
||||||
|
// This component is not rendered if no bundle is required
|
||||||
const SignatureBundleComponent = proposal.terms?.newAsset
|
const SignatureBundleComponent = proposal.terms?.newAsset
|
||||||
? ProposalSignatureBundleNewAsset
|
? ProposalSignatureBundleNewAsset
|
||||||
: ProposalSignatureBundleUpdateAsset;
|
: ProposalSignatureBundleUpdateAsset;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|
||||||
|
export interface FilterLabelProps {
|
||||||
|
filters: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the list (currently limited to 1) of filters set by the
|
||||||
|
* Transaction Filter
|
||||||
|
*/
|
||||||
|
export function FilterLabel({ filters }: FilterLabelProps) {
|
||||||
|
if (!filters || filters.size !== 1) {
|
||||||
|
return (
|
||||||
|
<span data-testid="filter-empty" className="uppercase">
|
||||||
|
{t('Filter')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="filter-selected">
|
||||||
|
<span className="uppercase">{t('Filters')}:</span>
|
||||||
|
<code className="bg-vega-light-150 dark:bg-vega-light-300 px-2 rounded-md capitalize dark:text-black">
|
||||||
|
{Array.from(filters)[0]}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { TxsFilter } from './tx-filter';
|
||||||
|
import type { FilterOption } from './tx-filter';
|
||||||
|
|
||||||
|
describe('TxsFilter', () => {
|
||||||
|
it('renders holding text when nothing is selected', () => {
|
||||||
|
const filters: Set<FilterOption> = new Set([]);
|
||||||
|
const setFilters = jest.fn();
|
||||||
|
render(<TxsFilter filters={filters} setFilters={setFilters} />);
|
||||||
|
expect(screen.getByTestId('filter-empty')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Filter')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the submit order filter as selected', () => {
|
||||||
|
const filters: Set<FilterOption> = new Set(['Submit Order']);
|
||||||
|
const setFilters = jest.fn();
|
||||||
|
render(<TxsFilter filters={filters} setFilters={setFilters} />);
|
||||||
|
expect(screen.getByTestId('filter-selected')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Submit Order')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItemIndicator,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
Icon,
|
||||||
|
Button,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { FilterLabel } from './tx-filter-label';
|
||||||
|
|
||||||
|
// All possible transaction types. Should be generated.
|
||||||
|
export type FilterOption =
|
||||||
|
| 'Amend LiquidityProvision Order'
|
||||||
|
| 'Amend Order'
|
||||||
|
| 'Batch Market Instructions'
|
||||||
|
| 'Cancel LiquidityProvision Order'
|
||||||
|
| 'Cancel Order'
|
||||||
|
| 'Cancel Transfer Funds'
|
||||||
|
| 'Chain Event'
|
||||||
|
| 'Delegate'
|
||||||
|
| 'Ethereum Key Rotate Submission'
|
||||||
|
| 'Issue Signatures'
|
||||||
|
| 'Key Rotate Submission'
|
||||||
|
| 'Liquidity Provision Order'
|
||||||
|
| 'Node Signature'
|
||||||
|
| 'Node Vote'
|
||||||
|
| 'Proposal'
|
||||||
|
| 'Protocol Upgrade'
|
||||||
|
| 'Register new Node'
|
||||||
|
| 'State Variable Proposal'
|
||||||
|
| 'Submit Oracle Data'
|
||||||
|
| 'Submit Order'
|
||||||
|
| 'Transfer Funds'
|
||||||
|
| 'Undelegate'
|
||||||
|
| 'Validator Heartbeat'
|
||||||
|
| 'Vote on Proposal'
|
||||||
|
| 'Withdraw';
|
||||||
|
|
||||||
|
// Alphabetised list of transaction types to appear at the top level
|
||||||
|
export const PrimaryFilterOptions: FilterOption[] = [
|
||||||
|
'Amend LiquidityProvision Order',
|
||||||
|
'Amend Order',
|
||||||
|
'Batch Market Instructions',
|
||||||
|
'Cancel LiquidityProvision Order',
|
||||||
|
'Cancel Order',
|
||||||
|
'Cancel Transfer Funds',
|
||||||
|
'Delegate',
|
||||||
|
'Liquidity Provision Order',
|
||||||
|
'Proposal',
|
||||||
|
'Submit Oracle Data',
|
||||||
|
'Submit Order',
|
||||||
|
'Transfer Funds',
|
||||||
|
'Undelegate',
|
||||||
|
'Vote on Proposal',
|
||||||
|
'Withdraw',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Alphabetised list of transaction types to nest under a 'More...' submenu
|
||||||
|
export const SecondaryFilterOptions: FilterOption[] = [
|
||||||
|
'Chain Event',
|
||||||
|
'Ethereum Key Rotate Submission',
|
||||||
|
'Issue Signatures',
|
||||||
|
'Key Rotate Submission',
|
||||||
|
'Node Signature',
|
||||||
|
'Node Vote',
|
||||||
|
'Protocol Upgrade',
|
||||||
|
'Register new Node',
|
||||||
|
'State Variable Proposal',
|
||||||
|
'Validator Heartbeat',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AllFilterOptions: FilterOption[] = [
|
||||||
|
...PrimaryFilterOptions,
|
||||||
|
...SecondaryFilterOptions,
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface TxFilterProps {
|
||||||
|
filters: Set<FilterOption>;
|
||||||
|
setFilters: Dispatch<SetStateAction<Set<FilterOption>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a structured dropdown menu of all of the available transaction
|
||||||
|
* types. It allows a user to select one transaction type to view. Later
|
||||||
|
* it will support multiple selection, but until the API supports that it is
|
||||||
|
* one or all.
|
||||||
|
* @param filters null or Set of transaction types
|
||||||
|
* @param setFilters A function to update the filters prop
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenu
|
||||||
|
modal={false}
|
||||||
|
trigger={
|
||||||
|
<DropdownMenuTrigger className="ml-0">
|
||||||
|
<Button size="xs" data-testid="filter-trigger">
|
||||||
|
<FilterLabel filters={filters} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
{filters.size > 0 ? null : (
|
||||||
|
<>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
|
||||||
|
>
|
||||||
|
{t('Clear filters')} <Icon name="cross" />
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{PrimaryFilterOptions.map((f) => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={f}
|
||||||
|
checked={filters.has(f)}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
// NOTE: These act like radio buttons until the API supports multiple filters
|
||||||
|
setFilters(new Set([f]));
|
||||||
|
}}
|
||||||
|
id={`radio-${f}`}
|
||||||
|
>
|
||||||
|
{f}
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Icon name="tick-circle" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
))}
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
{t('More Types')}
|
||||||
|
<Icon name="chevron-right" />
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
{SecondaryFilterOptions.map((f) => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={f}
|
||||||
|
checked={filters.has(f)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
// NOTE: These act like radio buttons until the API supports multiple filters
|
||||||
|
setFilters(new Set([f]));
|
||||||
|
}}
|
||||||
|
id={`radio-${f}`}
|
||||||
|
>
|
||||||
|
{f}
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Icon name="tick-circle" className="inline" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { TxsListNavigation } from './tx-list-navigation';
|
||||||
|
|
||||||
|
const NOOP = () => {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
describe('TxsListNavigation', () => {
|
||||||
|
it('renders transaction list navigation', () => {
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={NOOP}
|
||||||
|
previousPage={NOOP}
|
||||||
|
hasMoreTxs={true}
|
||||||
|
hasPreviousPage={true}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Newer')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Older')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls previousPage when "Newer" button is clicked', () => {
|
||||||
|
const previousPageMock = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={NOOP}
|
||||||
|
previousPage={previousPageMock}
|
||||||
|
hasMoreTxs={true}
|
||||||
|
hasPreviousPage={true}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Newer'));
|
||||||
|
|
||||||
|
expect(previousPageMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls nextPage when "Older" button is clicked', () => {
|
||||||
|
const nextPageMock = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={nextPageMock}
|
||||||
|
previousPage={NOOP}
|
||||||
|
hasMoreTxs={true}
|
||||||
|
hasPreviousPage={true}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Older'));
|
||||||
|
|
||||||
|
expect(nextPageMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables "Older" button if hasMoreTxs is false', () => {
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={NOOP}
|
||||||
|
previousPage={NOOP}
|
||||||
|
hasMoreTxs={false}
|
||||||
|
hasPreviousPage={false}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Older')).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables "Newer" button if hasPreviousPage is false', () => {
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={NOOP}
|
||||||
|
previousPage={NOOP}
|
||||||
|
hasMoreTxs={true}
|
||||||
|
hasPreviousPage={false}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Newer')).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables both buttons when more and previous are false', () => {
|
||||||
|
render(
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={NOOP}
|
||||||
|
nextPage={NOOP}
|
||||||
|
previousPage={NOOP}
|
||||||
|
hasMoreTxs={false}
|
||||||
|
hasPreviousPage={false}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</TxsListNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Newer')).toBeDisabled();
|
||||||
|
expect(screen.getByText('Older')).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { BlocksRefetch } from '../blocks';
|
||||||
|
|
||||||
|
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
export interface TxListNavigationProps {
|
||||||
|
refreshTxs: () => void;
|
||||||
|
nextPage: () => void;
|
||||||
|
previousPage: () => void;
|
||||||
|
loading?: boolean;
|
||||||
|
hasPreviousPage: boolean;
|
||||||
|
hasMoreTxs: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Displays a list of transactions with filters and controls to navigate through the list.
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Transaction List and controls
|
||||||
|
*/
|
||||||
|
export const TxsListNavigation = ({
|
||||||
|
refreshTxs,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
hasMoreTxs,
|
||||||
|
hasPreviousPage,
|
||||||
|
children,
|
||||||
|
loading = false,
|
||||||
|
}: TxListNavigationProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<menu className="mb-2 w-full ">{children}</menu>
|
||||||
|
<menu className="mb-2 w-full">
|
||||||
|
<BlocksRefetch refetch={refreshTxs} />
|
||||||
|
<div className="float-right">
|
||||||
|
<Button
|
||||||
|
className="mr-2"
|
||||||
|
size="xs"
|
||||||
|
disabled={!hasPreviousPage || loading}
|
||||||
|
onClick={() => {
|
||||||
|
previousPage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Newer')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
disabled={!hasMoreTxs || loading}
|
||||||
|
onClick={() => {
|
||||||
|
nextPage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Older')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="float-right mr-2">
|
||||||
|
{loading ? (
|
||||||
|
<span className="text-vega-light-300">{t('Loading...')}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import type { components } from '../../../types/explorer';
|
import type { components } from '../../../types/explorer';
|
||||||
|
import { VoteIcon } from '../vote-icon/vote-icon';
|
||||||
|
|
||||||
interface TxOrderTypeProps {
|
interface TxOrderTypeProps {
|
||||||
orderType: string;
|
orderType: string;
|
||||||
@@ -23,6 +24,7 @@ const displayString: StringMap = {
|
|||||||
LiquidityProvisionSubmission: 'LP order',
|
LiquidityProvisionSubmission: 'LP order',
|
||||||
'Liquidity Provision Order': 'LP order',
|
'Liquidity Provision Order': 'LP order',
|
||||||
LiquidityProvisionCancellation: 'LP cancel',
|
LiquidityProvisionCancellation: 'LP cancel',
|
||||||
|
'Cancel LiquidityProvision Order': 'LP cancel',
|
||||||
LiquidityProvisionAmendment: 'LP update',
|
LiquidityProvisionAmendment: 'LP update',
|
||||||
'Amend LiquidityProvision Order': 'Amend LP',
|
'Amend LiquidityProvision Order': 'Amend LP',
|
||||||
ProposalSubmission: 'Governance Proposal',
|
ProposalSubmission: 'Governance Proposal',
|
||||||
@@ -35,9 +37,12 @@ const displayString: StringMap = {
|
|||||||
UndelegateSubmission: 'Undelegation',
|
UndelegateSubmission: 'Undelegation',
|
||||||
KeyRotateSubmission: 'Key Rotation',
|
KeyRotateSubmission: 'Key Rotation',
|
||||||
StateVariableProposal: 'State Variable',
|
StateVariableProposal: 'State Variable',
|
||||||
|
'State Variable Proposal': 'State Variable',
|
||||||
Transfer: 'Transfer',
|
Transfer: 'Transfer',
|
||||||
CancelTransfer: 'Cancel Transfer',
|
CancelTransfer: 'Cancel Transfer',
|
||||||
|
'Cancel Transfer Funds': 'Cancel Transfer',
|
||||||
ValidatorHeartbeat: 'Heartbeat',
|
ValidatorHeartbeat: 'Heartbeat',
|
||||||
|
'Validator Heartbeat': 'Heartbeat',
|
||||||
'Batch Market Instructions': 'Batch',
|
'Batch Market Instructions': 'Batch',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,12 +142,15 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
|
|||||||
let type = displayString[orderType] || orderType;
|
let type = displayString[orderType] || orderType;
|
||||||
|
|
||||||
let colours =
|
let colours =
|
||||||
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-150';
|
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-250';
|
||||||
|
|
||||||
// This will get unwieldy and should probably produce a different colour of tag
|
// This will get unwieldy and should probably produce a different colour of tag
|
||||||
if (type === 'Chain Event' && !!command?.chainEvent) {
|
if (type === 'Chain Event' && !!command?.chainEvent) {
|
||||||
type = getLabelForChainEvent(command.chainEvent);
|
type = getLabelForChainEvent(command.chainEvent);
|
||||||
colours = 'text-white dark-text-white bg-vega-pink dark:bg-vega-pink';
|
colours = 'text-white dark-text-white bg-vega-pink dark:bg-vega-pink';
|
||||||
|
} else if (type === 'Validator Heartbeat') {
|
||||||
|
colours =
|
||||||
|
'text-white dark-text-white bg-vega-light-200 dark:bg-vega-dark-100';
|
||||||
} else if (type === 'Proposal' || type === 'Governance Proposal') {
|
} else if (type === 'Proposal' || type === 'Governance Proposal') {
|
||||||
if (command && !!command.proposalSubmission) {
|
if (command && !!command.proposalSubmission) {
|
||||||
type = getLabelForProposal(command.proposalSubmission);
|
type = getLabelForProposal(command.proposalSubmission);
|
||||||
@@ -150,6 +158,17 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
|
|||||||
colours = 'text-black bg-vega-yellow';
|
colours = 'text-black bg-vega-yellow';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'Vote on Proposal') {
|
||||||
|
return (
|
||||||
|
<VoteIcon
|
||||||
|
vote={command?.voteSubmission?.value === 'VALUE_YES'}
|
||||||
|
yesText="Proposal vote"
|
||||||
|
noText="Proposal vote"
|
||||||
|
useVoteColour={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'Vote on Proposal' || type === 'Vote Submission') {
|
if (type === 'Vote on Proposal' || type === 'Vote Submission') {
|
||||||
colours = 'text-black bg-vega-yellow';
|
colours = 'text-black bg-vega-yellow';
|
||||||
}
|
}
|
||||||
@@ -157,7 +176,7 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="tx-type"
|
data-testid="tx-type"
|
||||||
className={`text-sm rounded-md leading-none px-2 py-2 inline-block ${colours}`}
|
className={`text-sm rounded-md leading-tight px-2 inline-block whitespace-nowrap ${colours}`}
|
||||||
>
|
>
|
||||||
{type}
|
{type}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
@@ -83,21 +84,22 @@ describe('Txs infinite list item', () => {
|
|||||||
|
|
||||||
it('renders data correctly', () => {
|
it('renders data correctly', () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MockedProvider>
|
||||||
<TxsInfiniteListItem
|
<MemoryRouter>
|
||||||
type="testType"
|
<TxsInfiniteListItem
|
||||||
submitter="testPubKey"
|
type="testType"
|
||||||
hash="testTxHash"
|
submitter="testPubKey"
|
||||||
block="1"
|
hash="testTxHash"
|
||||||
code={0}
|
block="1"
|
||||||
command={{}}
|
code={0}
|
||||||
/>
|
command={{}}
|
||||||
</MemoryRouter>
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash');
|
expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash');
|
||||||
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
|
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
|
||||||
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
|
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
|
||||||
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
|
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
|
||||||
expect(screen.getByTestId('tx-success')).toHaveTextContent('Success: ✅');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import { TruncatedLink } from '../truncate/truncated-link';
|
import { TruncatedLink } from '../truncate/truncated-link';
|
||||||
import { Routes } from '../../routes/route-names';
|
import { Routes } from '../../routes/route-names';
|
||||||
import { TxOrderType } from './tx-order-type';
|
import { TxOrderType } from './tx-order-type';
|
||||||
@@ -6,8 +5,25 @@ import type { BlockExplorerTransactionResult } from '../../routes/types/block-ex
|
|||||||
import { toHex } from '../search/detect-search';
|
import { toHex } from '../search/detect-search';
|
||||||
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
|
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
|
||||||
import isNumber from 'lodash/isNumber';
|
import isNumber from 'lodash/isNumber';
|
||||||
|
import { PartyLink } from '../links';
|
||||||
|
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
|
import type { Screen } from '@vegaprotocol/react-helpers';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
const TRUNCATE_LENGTH = 10;
|
const DEFAULT_TRUNCATE_LENGTH = 7;
|
||||||
|
|
||||||
|
export function getIdTruncateLength(screen: Screen): number {
|
||||||
|
if (['xxxl', 'xxl'].includes(screen)) {
|
||||||
|
return 64;
|
||||||
|
} else if (['xl', 'lg', 'md'].includes(screen)) {
|
||||||
|
return 32;
|
||||||
|
}
|
||||||
|
return DEFAULT_TRUNCATE_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldTruncateParty(screen: Screen): boolean {
|
||||||
|
return !['xxxl', 'xxl', 'xl'].includes(screen);
|
||||||
|
}
|
||||||
|
|
||||||
export const TxsInfiniteListItem = ({
|
export const TxsInfiniteListItem = ({
|
||||||
hash,
|
hash,
|
||||||
@@ -17,6 +33,12 @@ export const TxsInfiniteListItem = ({
|
|||||||
block,
|
block,
|
||||||
command,
|
command,
|
||||||
}: Partial<BlockExplorerTransactionResult>) => {
|
}: Partial<BlockExplorerTransactionResult>) => {
|
||||||
|
const { screenSize } = useScreenDimensions();
|
||||||
|
const idTruncateLength = useMemo(
|
||||||
|
() => getIdTruncateLength(screenSize),
|
||||||
|
[screenSize]
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!hash ||
|
!hash ||
|
||||||
!submitter ||
|
!submitter ||
|
||||||
@@ -29,68 +51,40 @@ export const TxsInfiniteListItem = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<tr
|
||||||
data-testid="transaction-row"
|
data-testid="transaction-row"
|
||||||
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10 py-2"
|
className="transaction-row text-left items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item py-[2px]"
|
||||||
>
|
>
|
||||||
<div
|
<td
|
||||||
className="text-sm col-span-10 md:col-span-3 leading-none"
|
className="text-sm leading-none whitespace-nowrap font-mono"
|
||||||
data-testid="tx-hash"
|
data-testid="tx-hash"
|
||||||
>
|
>
|
||||||
<span className="md:hidden uppercase text-vega-dark-300">
|
|
||||||
ID:
|
|
||||||
</span>
|
|
||||||
<TruncatedLink
|
|
||||||
to={`/${Routes.TX}/${toHex(hash)}`}
|
|
||||||
text={hash}
|
|
||||||
startChars={TRUNCATE_LENGTH}
|
|
||||||
endChars={TRUNCATE_LENGTH}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="text-sm col-span-10 md:col-span-3 leading-none"
|
|
||||||
data-testid="pub-key"
|
|
||||||
>
|
|
||||||
<span className="md:hidden uppercase text-vega-dark-300">
|
|
||||||
By:
|
|
||||||
</span>
|
|
||||||
<TruncatedLink
|
|
||||||
to={`/${Routes.PARTIES}/${submitter}`}
|
|
||||||
text={submitter}
|
|
||||||
startChars={TRUNCATE_LENGTH}
|
|
||||||
endChars={TRUNCATE_LENGTH}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm col-span-5 md:col-span-2 leading-none flex items-center">
|
|
||||||
<TxOrderType orderType={type} command={command} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="text-sm col-span-3 md:col-span-1 leading-none flex items-center"
|
|
||||||
data-testid="tx-block"
|
|
||||||
>
|
|
||||||
<span className="md:hidden uppercase text-vega-dark-300">
|
|
||||||
Block:
|
|
||||||
</span>
|
|
||||||
<TruncatedLink
|
|
||||||
to={`/${Routes.BLOCKS}/${block}`}
|
|
||||||
text={block}
|
|
||||||
startChars={TRUNCATE_LENGTH}
|
|
||||||
endChars={TRUNCATE_LENGTH}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="text-sm col-span-2 md:col-span-1 leading-none flex items-center"
|
|
||||||
data-testid="tx-success"
|
|
||||||
>
|
|
||||||
<span className="md:hidden uppercase text-vega-dark-300">
|
|
||||||
Success:
|
|
||||||
</span>
|
|
||||||
{isNumber(code) ? (
|
{isNumber(code) ? (
|
||||||
<ChainResponseCode code={code} hideLabel={true} />
|
<ChainResponseCode code={code} hideLabel={true} hideIfOk={true} />
|
||||||
) : (
|
) : (
|
||||||
code
|
code
|
||||||
)}
|
)}
|
||||||
</div>
|
<TruncatedLink
|
||||||
</div>
|
to={`/${Routes.TX}/${toHex(hash)}`}
|
||||||
|
text={hash}
|
||||||
|
startChars={idTruncateLength}
|
||||||
|
endChars={0}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="text-sm leading-none">
|
||||||
|
<TxOrderType orderType={type} command={command} />
|
||||||
|
</td>
|
||||||
|
<td className="text-sm leading-none" data-testid="pub-key">
|
||||||
|
<PartyLink truncate={shouldTruncateParty(screenSize)} id={submitter} />
|
||||||
|
</td>
|
||||||
|
<td className="text-sm items-center font-mono" data-testid="tx-block">
|
||||||
|
<TruncatedLink
|
||||||
|
to={`/${Routes.BLOCKS}/${block}`}
|
||||||
|
text={block}
|
||||||
|
startChars={5}
|
||||||
|
endChars={5}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { TxsInfiniteList } from './txs-infinite-list';
|
import { TxsInfiniteList } from './txs-infinite-list';
|
||||||
import { render, screen, fireEvent, act } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
||||||
import { Side } from '@vegaprotocol/types';
|
import { Side } from '@vegaprotocol/types';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
|
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
|
||||||
return Array.from(Array(number)).map((_) => ({
|
return Array.from(Array(number)).map((_) => ({
|
||||||
@@ -40,7 +41,7 @@ describe('Txs infinite list', () => {
|
|||||||
it('should display a "no items" message when no items provided', () => {
|
it('should display a "no items" message when no items provided', () => {
|
||||||
render(
|
render(
|
||||||
<TxsInfiniteList
|
<TxsInfiniteList
|
||||||
txs={undefined}
|
txs={undefined as unknown as BlockExplorerTransactionResult[]}
|
||||||
areTxsLoading={false}
|
areTxsLoading={false}
|
||||||
hasMoreTxs={false}
|
hasMoreTxs={false}
|
||||||
loadMoreTxs={() => null}
|
loadMoreTxs={() => null}
|
||||||
@@ -48,23 +49,7 @@ describe('Txs infinite list', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('emptylist')).toBeInTheDocument();
|
expect(screen.getByTestId('emptylist')).toBeInTheDocument();
|
||||||
expect(
|
expect(screen.getByText('No transactions found')).toBeInTheDocument();
|
||||||
screen.getByText('This chain has 0 transactions')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('error is displayed at item level', () => {
|
|
||||||
const txs = generateTxs(1);
|
|
||||||
render(
|
|
||||||
<TxsInfiniteList
|
|
||||||
txs={txs}
|
|
||||||
areTxsLoading={false}
|
|
||||||
hasMoreTxs={false}
|
|
||||||
loadMoreTxs={() => null}
|
|
||||||
error={Error('test error!')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByText('Cannot fetch transaction')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('item renders data of n length into list of n length', () => {
|
it('item renders data of n length into list of n length', () => {
|
||||||
@@ -73,85 +58,22 @@ describe('Txs infinite list', () => {
|
|||||||
const txs = generateTxs(7);
|
const txs = generateTxs(7);
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<TxsInfiniteList
|
<MockedProvider>
|
||||||
txs={txs}
|
<TxsInfiniteList
|
||||||
areTxsLoading={false}
|
txs={txs}
|
||||||
hasMoreTxs={false}
|
areTxsLoading={false}
|
||||||
loadMoreTxs={() => null}
|
hasMoreTxs={false}
|
||||||
error={undefined}
|
loadMoreTxs={() => null}
|
||||||
/>
|
error={undefined}
|
||||||
|
/>
|
||||||
|
</MockedProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen
|
screen
|
||||||
.getByTestId('infinite-scroll-wrapper')
|
.getByTestId('transactions-list')
|
||||||
.querySelectorAll('.txs-infinite-list-item')
|
.querySelectorAll('.transaction-row')
|
||||||
).toHaveLength(7);
|
).toHaveLength(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tries to load more items when required to initially fill the list', () => {
|
|
||||||
// For example, if initially rendering 15, the bottom of the list is
|
|
||||||
// in view of the viewport, and the callback should be executed
|
|
||||||
const txs = generateTxs(15);
|
|
||||||
const callback = jest.fn();
|
|
||||||
|
|
||||||
render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<TxsInfiniteList
|
|
||||||
txs={txs}
|
|
||||||
areTxsLoading={false}
|
|
||||||
hasMoreTxs={true}
|
|
||||||
loadMoreTxs={callback}
|
|
||||||
error={undefined}
|
|
||||||
/>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(callback.mock.calls.length).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not try to load more items if there are no more', () => {
|
|
||||||
const txs = generateTxs(3);
|
|
||||||
const callback = jest.fn();
|
|
||||||
|
|
||||||
render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<TxsInfiniteList
|
|
||||||
txs={txs}
|
|
||||||
areTxsLoading={false}
|
|
||||||
hasMoreTxs={false}
|
|
||||||
loadMoreTxs={callback}
|
|
||||||
error={undefined}
|
|
||||||
/>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(callback.mock.calls.length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads more items is called when scrolled', () => {
|
|
||||||
const txs = generateTxs(14);
|
|
||||||
const callback = jest.fn();
|
|
||||||
|
|
||||||
render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<TxsInfiniteList
|
|
||||||
txs={txs}
|
|
||||||
areTxsLoading={false}
|
|
||||||
hasMoreTxs={true}
|
|
||||||
loadMoreTxs={callback}
|
|
||||||
error={undefined}
|
|
||||||
/>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
fireEvent.scroll(screen.getByTestId('infinite-scroll-wrapper'), {
|
|
||||||
target: { scrollY: 2000 },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(callback.mock.calls.length).toEqual(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import React from 'react';
|
|
||||||
import { FixedSizeList as List } from 'react-window';
|
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
|
||||||
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||||
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
||||||
import EmptyList from '../empty-list/empty-list';
|
import EmptyList from '../empty-list/empty-list';
|
||||||
@@ -11,71 +7,46 @@ import { Loader } from '@vegaprotocol/ui-toolkit';
|
|||||||
interface TxsInfiniteListProps {
|
interface TxsInfiniteListProps {
|
||||||
hasMoreTxs: boolean;
|
hasMoreTxs: boolean;
|
||||||
areTxsLoading: boolean | undefined;
|
areTxsLoading: boolean | undefined;
|
||||||
txs: BlockExplorerTransactionResult[] | undefined;
|
txs: BlockExplorerTransactionResult[];
|
||||||
loadMoreTxs: () => void;
|
loadMoreTxs: () => void;
|
||||||
error: Error | undefined;
|
error: Error | undefined;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
hasFilters?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ItemProps {
|
interface ItemProps {
|
||||||
index: BlockExplorerTransactionResult;
|
tx: BlockExplorerTransactionResult;
|
||||||
style: React.CSSProperties;
|
|
||||||
isLoading: boolean;
|
|
||||||
error: Error | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
const Item = ({ tx }: ItemProps) => {
|
||||||
const NOOP = () => {};
|
const { hash, submitter, type, command, block, code, index: blockIndex } = tx;
|
||||||
|
return (
|
||||||
const Item = ({ index, style, isLoading, error }: ItemProps) => {
|
<TxsInfiniteListItem
|
||||||
let content;
|
type={type}
|
||||||
if (error) {
|
code={code}
|
||||||
content = t(`Cannot fetch transaction`);
|
command={command}
|
||||||
} else if (isLoading) {
|
submitter={submitter}
|
||||||
content = <Loader />;
|
hash={hash}
|
||||||
} else {
|
block={block}
|
||||||
const {
|
index={blockIndex}
|
||||||
hash,
|
/>
|
||||||
submitter,
|
);
|
||||||
type,
|
|
||||||
command,
|
|
||||||
block,
|
|
||||||
code,
|
|
||||||
index: blockIndex,
|
|
||||||
} = index;
|
|
||||||
content = (
|
|
||||||
<TxsInfiniteListItem
|
|
||||||
type={type}
|
|
||||||
code={code}
|
|
||||||
command={command}
|
|
||||||
submitter={submitter}
|
|
||||||
hash={hash}
|
|
||||||
block={block}
|
|
||||||
index={blockIndex}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div style={style}>{content}</div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TxsInfiniteList = ({
|
export const TxsInfiniteList = ({
|
||||||
hasMoreTxs,
|
|
||||||
areTxsLoading,
|
areTxsLoading,
|
||||||
txs,
|
txs,
|
||||||
loadMoreTxs,
|
|
||||||
error,
|
|
||||||
className,
|
className,
|
||||||
|
hasFilters = false,
|
||||||
}: TxsInfiniteListProps) => {
|
}: TxsInfiniteListProps) => {
|
||||||
const { screenSize } = useScreenDimensions();
|
if (!txs || txs.length === 0) {
|
||||||
const isStacked = ['xs', 'sm'].includes(screenSize);
|
|
||||||
|
|
||||||
if (!txs) {
|
|
||||||
if (!areTxsLoading) {
|
if (!areTxsLoading) {
|
||||||
return (
|
return (
|
||||||
<EmptyList
|
<EmptyList
|
||||||
heading={t('This chain has 0 transactions')}
|
heading={t('No transactions found')}
|
||||||
label={t('Check back soon')}
|
label={
|
||||||
|
hasFilters ? t('Try a different filter') : t('Check back soon')
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -83,56 +54,26 @@ export const TxsInfiniteList = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are more items to be loaded then add an extra row to hold a loading indicator.
|
|
||||||
const itemCount = hasMoreTxs ? txs.length + 1 : txs.length;
|
|
||||||
|
|
||||||
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
const loadMoreItems = areTxsLoading ? NOOP : loadMoreTxs;
|
|
||||||
|
|
||||||
// Every row is loaded except for our loading indicator row.
|
|
||||||
const isItemLoaded = (index: number) => !hasMoreTxs || index < txs.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} data-testid="transactions-list">
|
<div className="overflow-scroll">
|
||||||
<div className="lg:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
|
<table className={className} data-testid="transactions-list">
|
||||||
<div className="col-span-3">
|
<thead>
|
||||||
<span className="hidden xl:inline">{t('Transaction')} </span>
|
<tr className="w-full mb-3 text-vega-dark-300 uppercase text-left">
|
||||||
<span>ID</span>
|
<th>
|
||||||
</div>
|
<span className="hidden xl:inline">{t('Txn')} </span>
|
||||||
<div className="col-span-3">{t('Submitted By')}</div>
|
<span>ID</span>
|
||||||
<div className="col-span-2">{t('Type')}</div>
|
</th>
|
||||||
<div className="col-span-1">{t('Block')}</div>
|
<th>{t('Type')}</th>
|
||||||
<div className="col-span-1">{t('Success')}</div>
|
<th className="text-left">{t('From')}</th>
|
||||||
</div>
|
<th>{t('Block')}</th>
|
||||||
<div data-testid="infinite-scroll-wrapper">
|
</tr>
|
||||||
<InfiniteLoader
|
</thead>
|
||||||
isItemLoaded={isItemLoaded}
|
<tbody>
|
||||||
itemCount={itemCount}
|
{txs.map((t) => (
|
||||||
loadMoreItems={loadMoreItems}
|
<Item key={t.hash} tx={t} />
|
||||||
>
|
))}
|
||||||
{({ onItemsRendered, ref }) => (
|
</tbody>
|
||||||
<List
|
</table>
|
||||||
className="List"
|
|
||||||
height={995}
|
|
||||||
itemCount={itemCount}
|
|
||||||
itemSize={isStacked ? 134 : 50}
|
|
||||||
onItemsRendered={onItemsRendered}
|
|
||||||
ref={ref}
|
|
||||||
width={'100%'}
|
|
||||||
>
|
|
||||||
{({ index, style }) => (
|
|
||||||
<Item
|
|
||||||
index={txs[index]}
|
|
||||||
style={style}
|
|
||||||
isLoading={!isItemLoaded(index)}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
</InfiniteLoader>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
import { Routes } from '../../routes/route-names';
|
import { Table, TableRow } from '../table';
|
||||||
import { TruncatedLink } from '../truncate/truncated-link';
|
|
||||||
import { TxOrderType } from './tx-order-type';
|
|
||||||
import { Table, TableRow, TableCell } from '../table';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||||
import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response';
|
import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response';
|
||||||
import isNumber from 'lodash/isNumber';
|
|
||||||
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
|
|
||||||
import { getTxsDataUrl } from '../../hooks/use-txs-data';
|
import { getTxsDataUrl } from '../../hooks/use-txs-data';
|
||||||
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
|
||||||
import EmptyList from '../empty-list/empty-list';
|
import EmptyList from '../empty-list/empty-list';
|
||||||
|
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||||
|
|
||||||
interface TxsPerBlockProps {
|
interface TxsPerBlockProps {
|
||||||
blockHeight: string;
|
blockHeight: string;
|
||||||
txCount: number;
|
txCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const truncateLength = 5;
|
|
||||||
|
|
||||||
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
||||||
const filters = `filters[block.height]=${blockHeight}`;
|
const filters = `filters[block.height]=${blockHeight}`;
|
||||||
const url = getTxsDataUrl({ limit: txCount.toString(), filters });
|
const url = getTxsDataUrl({ limit: txCount.toString(), filters });
|
||||||
@@ -33,53 +27,23 @@ export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
|||||||
<thead>
|
<thead>
|
||||||
<TableRow modifier="bordered" className="font-mono">
|
<TableRow modifier="bordered" className="font-mono">
|
||||||
<td>{t('Transaction')}</td>
|
<td>{t('Transaction')}</td>
|
||||||
<td>{t('From')}</td>
|
|
||||||
<td>{t('Type')}</td>
|
<td>{t('Type')}</td>
|
||||||
<td>{t('Status')}</td>
|
<td>{t('From')}</td>
|
||||||
|
<td>{t('Block')}</td>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.transactions.map(
|
{data.transactions.map(
|
||||||
({ hash, submitter, type, command, code }) => {
|
({ hash, submitter, type, command, code, block }) => {
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TxsInfiniteListItem
|
||||||
modifier="bordered"
|
block={block}
|
||||||
key={hash}
|
hash={hash}
|
||||||
data-testid="transaction-row"
|
submitter={submitter}
|
||||||
>
|
type={type}
|
||||||
<TableCell
|
command={command}
|
||||||
modifier="bordered"
|
code={code}
|
||||||
className="pr-12 font-mono"
|
/>
|
||||||
>
|
|
||||||
<TruncatedLink
|
|
||||||
to={`/${Routes.TX}/${hash}`}
|
|
||||||
text={hash}
|
|
||||||
startChars={truncateLength}
|
|
||||||
endChars={truncateLength}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
modifier="bordered"
|
|
||||||
className="pr-12 font-mono"
|
|
||||||
>
|
|
||||||
<TruncatedLink
|
|
||||||
to={`/${Routes.PARTIES}/${submitter}`}
|
|
||||||
text={submitter}
|
|
||||||
startChars={truncateLength}
|
|
||||||
endChars={truncateLength}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell modifier="bordered">
|
|
||||||
<TxOrderType orderType={type} command={command} />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell modifier="bordered" className="text">
|
|
||||||
{isNumber(code) ? (
|
|
||||||
<ChainResponseCode code={code} hideLabel={true} />
|
|
||||||
) : (
|
|
||||||
code
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { VoteIcon } from './vote-icon';
|
||||||
|
|
||||||
|
describe('Vote TX icon', () => {
|
||||||
|
it('should use the text For by default for yes votes', () => {
|
||||||
|
const yes = render(<VoteIcon vote={true} />);
|
||||||
|
expect(yes.getByTestId('label')).toHaveTextContent('For');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the yesText for yes votes if specified', () => {
|
||||||
|
const yes = render(<VoteIcon vote={true} yesText="Test" />);
|
||||||
|
expect(yes.getByTestId('label')).toHaveTextContent('Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the tick icon for yes votes', () => {
|
||||||
|
const no = render(<VoteIcon vote={true} />);
|
||||||
|
expect(no.getByRole('img')).toHaveAttribute(
|
||||||
|
'aria-label',
|
||||||
|
'tick-circle icon'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the text Against by default for no votes', () => {
|
||||||
|
const no = render(<VoteIcon vote={false} />);
|
||||||
|
expect(no.getByTestId('label')).toHaveTextContent('Against');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the noText for no votes if specified', () => {
|
||||||
|
const no = render(<VoteIcon vote={false} noText="Test" />);
|
||||||
|
expect(no.getByTestId('label')).toHaveTextContent('Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the delete icon for no votes', () => {
|
||||||
|
const no = render(<VoteIcon vote={false} />);
|
||||||
|
expect(no.getByRole('img')).toHaveAttribute('aria-label', 'delete icon');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('useVoteColour prop can be used to override coloured background', () => {
|
||||||
|
const no = render(<VoteIcon vote={false} />);
|
||||||
|
expect(no.container.children[0]).toHaveClass('bg-vega-pink-550');
|
||||||
|
|
||||||
|
const monochromeNo = render(
|
||||||
|
<VoteIcon vote={false} useVoteColour={false} />
|
||||||
|
);
|
||||||
|
expect(monochromeNo.container.children[0]).not.toHaveClass(
|
||||||
|
'bg-vega-pink-550'
|
||||||
|
);
|
||||||
|
expect(monochromeNo.container.children[0]).toHaveClass('bg-vega-dark-200');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { IconName } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
export interface VoteIconProps {
|
||||||
|
// True is a yes vote, false is undefined or no vorte
|
||||||
|
vote: boolean;
|
||||||
|
// Defaults to 'For', but can be any text
|
||||||
|
yesText?: string;
|
||||||
|
// Defaults to 'Against', but can be any text
|
||||||
|
noText?: string;
|
||||||
|
// If set to false the background will not be coloured
|
||||||
|
useVoteColour?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBgColour(useVoteColour: boolean, vote: boolean) {
|
||||||
|
if (useVoteColour === false) {
|
||||||
|
return 'bg-vega-dark-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
return vote ? 'bg-vega-green-550' : 'bg-vega-pink-550';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFillColour(useVoteColour: boolean, vote: boolean) {
|
||||||
|
if (useVoteColour === false) {
|
||||||
|
return 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
return vote ? 'vega-green-300' : 'vega-pink-300';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTextColour(useVoteColour: boolean, vote: boolean) {
|
||||||
|
if (useVoteColour === false) {
|
||||||
|
return 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
return vote ? 'vega-green-200' : 'vega-pink-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a lozenge with an icon representing the way a user voted for a proposal.
|
||||||
|
* The yes and no text can be overridden
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function VoteIcon({
|
||||||
|
vote,
|
||||||
|
useVoteColour = true,
|
||||||
|
yesText = 'For',
|
||||||
|
noText = 'Against',
|
||||||
|
}: VoteIconProps) {
|
||||||
|
const label = vote ? yesText : noText;
|
||||||
|
const icon: IconName = vote ? 'tick-circle' : 'delete';
|
||||||
|
const bg = getBgColour(useVoteColour, vote);
|
||||||
|
const fill = getFillColour(useVoteColour, vote);
|
||||||
|
const text = getTextColour(useVoteColour, vote);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`voteicon inline-block py-0 px-2 py rounded-md text-white whitespace-nowrap leading-tight sm align-top ${bg}`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icon}
|
||||||
|
size={3}
|
||||||
|
className={`mr-2 p-0 mb-[-1px] fill-${fill}`}
|
||||||
|
/>
|
||||||
|
<span className={`text-${text}`} data-testid="label">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,16 +10,18 @@ import isNumber from 'lodash/isNumber';
|
|||||||
export interface TxsStateProps {
|
export interface TxsStateProps {
|
||||||
txsData: BlockExplorerTransactionResult[];
|
txsData: BlockExplorerTransactionResult[];
|
||||||
hasMoreTxs: boolean;
|
hasMoreTxs: boolean;
|
||||||
lastCursor: string;
|
cursor: string;
|
||||||
|
previousCursors: string[];
|
||||||
|
hasPreviousPage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUseTxsData {
|
export interface IUseTxsData {
|
||||||
limit?: number;
|
limit: number;
|
||||||
filters?: string;
|
filters?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGetTxsDataUrl {
|
interface IGetTxsDataUrl {
|
||||||
limit?: string;
|
limit: string;
|
||||||
filters?: string;
|
filters?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,62 +35,96 @@ export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
|
|||||||
// Hacky fix for param as array
|
// Hacky fix for param as array
|
||||||
let urlAsString = url.toString();
|
let urlAsString = url.toString();
|
||||||
if (filters) {
|
if (filters) {
|
||||||
urlAsString += '&' + filters;
|
urlAsString += '&' + filters.replace(' ', '%20');
|
||||||
}
|
}
|
||||||
|
|
||||||
return urlAsString;
|
return urlAsString;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTxsData = ({ limit, filters }: IUseTxsData) => {
|
export const useTxsData = ({ limit, filters }: IUseTxsData) => {
|
||||||
const [{ txsData, hasMoreTxs, lastCursor }, setTxsState] =
|
const [
|
||||||
useState<TxsStateProps>({
|
{ txsData, hasMoreTxs, cursor, previousCursors, hasPreviousPage },
|
||||||
txsData: [],
|
setTxsState,
|
||||||
hasMoreTxs: true,
|
] = useState<TxsStateProps>({
|
||||||
lastCursor: '',
|
txsData: [],
|
||||||
});
|
hasMoreTxs: false,
|
||||||
|
previousCursors: [],
|
||||||
|
cursor: '',
|
||||||
|
hasPreviousPage: false,
|
||||||
|
});
|
||||||
|
|
||||||
const url = getTxsDataUrl({ limit: limit?.toString(), filters });
|
const url = getTxsDataUrl({ limit: limit.toString(), filters });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: { data, error, loading },
|
state: { data, error, loading },
|
||||||
refetch,
|
refetch,
|
||||||
} = useFetch<BlockExplorerTransactions>(url, {}, false);
|
} = useFetch<BlockExplorerTransactions>(url, {}, true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && isNumber(data?.transactions?.length)) {
|
if (!loading && data && isNumber(data.transactions.length)) {
|
||||||
setTxsState((prev) => ({
|
setTxsState((prev) => {
|
||||||
txsData: [...prev.txsData, ...data.transactions],
|
return {
|
||||||
hasMoreTxs: data.transactions.length > 0,
|
...prev,
|
||||||
lastCursor:
|
txsData: data.transactions,
|
||||||
data.transactions[data.transactions.length - 1]?.cursor || '',
|
hasMoreTxs: data.transactions.length >= limit,
|
||||||
}));
|
cursor: data?.transactions.at(-1)?.cursor || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [setTxsState, data]);
|
}, [loading, setTxsState, data, limit]);
|
||||||
|
|
||||||
const loadTxs = useCallback(() => {
|
const nextPage = useCallback(() => {
|
||||||
return refetch({
|
const c = data?.transactions.at(0)?.cursor;
|
||||||
limit: limit,
|
const newPreviousCursors = c ? [...previousCursors, c] : previousCursors;
|
||||||
before: lastCursor,
|
|
||||||
});
|
|
||||||
}, [lastCursor, limit, refetch]);
|
|
||||||
|
|
||||||
const refreshTxs = useCallback(async () => {
|
|
||||||
setTxsState((prev) => ({
|
setTxsState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
lastCursor: '',
|
hasPreviousPage: true,
|
||||||
hasMoreTxs: true,
|
previousCursors: newPreviousCursors,
|
||||||
txsData: [],
|
|
||||||
}));
|
}));
|
||||||
}, [setTxsState]);
|
|
||||||
|
return refetch({
|
||||||
|
limit,
|
||||||
|
before: cursor,
|
||||||
|
});
|
||||||
|
}, [data, previousCursors, cursor, limit, refetch]);
|
||||||
|
|
||||||
|
const previousPage = useCallback(() => {
|
||||||
|
const previousCursor = [...previousCursors].pop();
|
||||||
|
const newPreviousCursors = previousCursors.slice(0, -1);
|
||||||
|
setTxsState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
hasPreviousPage: newPreviousCursors.length > 0,
|
||||||
|
previousCursors: newPreviousCursors,
|
||||||
|
}));
|
||||||
|
return refetch({
|
||||||
|
limit,
|
||||||
|
before: previousCursor,
|
||||||
|
});
|
||||||
|
}, [previousCursors, limit, refetch]);
|
||||||
|
|
||||||
|
const refreshTxs = useCallback(async () => {
|
||||||
|
setTxsState(() => ({
|
||||||
|
txsData: [],
|
||||||
|
cursor: '',
|
||||||
|
previousCursors: [],
|
||||||
|
hasMoreTxs: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
refetch({ limit });
|
||||||
|
}, [setTxsState, limit, refetch, filters]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
txsData,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
txsData,
|
|
||||||
hasMoreTxs,
|
hasMoreTxs,
|
||||||
lastCursor,
|
hasPreviousPage,
|
||||||
|
previousCursors,
|
||||||
|
cursor,
|
||||||
refreshTxs,
|
refreshTxs,
|
||||||
loadTxs,
|
nextPage,
|
||||||
|
previousPage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { Footer } from '../components/footer/footer';
|
import { Footer } from '../components/footer/footer';
|
||||||
import { Header } from '../components/header';
|
import { Header } from '../components/header';
|
||||||
import { Routes } from './route-names';
|
import { Routes } from './route-names';
|
||||||
|
import { useExplorerNodeNamesLazyQuery } from './validators/__generated__/NodeNames';
|
||||||
|
|
||||||
const DialogsContainer = () => {
|
const DialogsContainer = () => {
|
||||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
||||||
@@ -39,6 +40,7 @@ export const Layout = () => {
|
|||||||
const isHome = Boolean(useMatch(Routes.HOME));
|
const isHome = Boolean(useMatch(Routes.HOME));
|
||||||
const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
|
const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
|
||||||
const fixedWidthClasses = 'w-full max-w-[1500px] mx-auto';
|
const fixedWidthClasses = 'w-full max-w-[1500px] mx-auto';
|
||||||
|
useExplorerNodeNamesLazyQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -49,7 +51,7 @@ export const Layout = () => {
|
|||||||
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
|
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
|
||||||
'border-vega-light-200 dark:border-vega-dark-200',
|
'border-vega-light-200 dark:border-vega-dark-200',
|
||||||
'antialiased text-black dark:text-white',
|
'antialiased text-black dark:text-white',
|
||||||
'overflow-hidden relative'
|
'relative'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
|||||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||||
import compact from 'lodash/compact';
|
import compact from 'lodash/compact';
|
||||||
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||||
import { marketInfoWithDataProvider } from '@vegaprotocol/market-info';
|
import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
|
||||||
import { PageTitle } from '../../components/page-helpers/page-title';
|
import { PageTitle } from '../../components/page-helpers/page-title';
|
||||||
|
|
||||||
export const MarketPage = () => {
|
export const MarketPage = () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||||
import { marketsProvider } from '@vegaprotocol/market-list';
|
import { marketsProvider } from '@vegaprotocol/markets';
|
||||||
import { RouteTitle } from '../../components/route-title';
|
import { RouteTitle } from '../../components/route-title';
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { OracleDetailsType } from './oracle-details-type';
|
import { OracleDetailsType, isInternalSourceType } from './oracle-details-type';
|
||||||
import type { SourceTypeName } from './oracle-details-type';
|
import type { SourceType } from './oracle';
|
||||||
|
import { PropertyKeyType } from '@vegaprotocol/types';
|
||||||
|
|
||||||
function renderComponent(type: SourceTypeName) {
|
function renderComponent(type: SourceType) {
|
||||||
return <OracleDetailsType type={type} />;
|
return <OracleDetailsType sourceType={type} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWrappedComponent(type: SourceTypeName) {
|
function renderWrappedComponent(type: SourceType) {
|
||||||
return (
|
return (
|
||||||
<table>
|
<table>
|
||||||
<tbody>{renderComponent(type)}</tbody>
|
<tbody>{renderComponent(type)}</tbody>
|
||||||
@@ -14,19 +15,53 @@ function renderWrappedComponent(type: SourceTypeName) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mock(name: string): SourceType {
|
||||||
|
return {
|
||||||
|
sourceType: {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
__typename: 'Filter',
|
||||||
|
key: {
|
||||||
|
name,
|
||||||
|
type: PropertyKeyType.TYPE_STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe('Oracle type view', () => {
|
describe('Oracle type view', () => {
|
||||||
it('Renders nothing when type is null', () => {
|
it('Renders nothing when type is null', () => {
|
||||||
const res = render(renderComponent(null as unknown as SourceTypeName));
|
const res = render(renderComponent(null as unknown as SourceType));
|
||||||
expect(res.container).toBeEmptyDOMElement();
|
expect(res.container).toBeEmptyDOMElement();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders Internal time for internal sources', () => {
|
it('Renders Internal time for internal sources - timestamp', () => {
|
||||||
const res = render(renderWrappedComponent('DataSourceDefinitionInternal'));
|
const s = mock('vegaprotocol.builtin.timestamp');
|
||||||
expect(res.getByText('Internal time')).toBeInTheDocument();
|
expect(isInternalSourceType(s)).toEqual(true);
|
||||||
|
const res = render(renderWrappedComponent(s));
|
||||||
|
expect(res.getByText('Internal data')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders Internal time for internal sources - potential future types', () => {
|
||||||
|
const s = mock('vegaprotocol.builtin.boolean');
|
||||||
|
expect(isInternalSourceType(s)).toEqual(true);
|
||||||
|
const res = render(renderWrappedComponent(s));
|
||||||
|
expect(res.getByText('Internal data')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders External data otherwise - prices.external.whatever', () => {
|
||||||
|
const s = mock('prices.external.whatever');
|
||||||
|
expect(isInternalSourceType(s)).toEqual(false);
|
||||||
|
const res = render(renderWrappedComponent(s));
|
||||||
|
expect(res.getByText('External data')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders External data otherwise', () => {
|
it('Renders External data otherwise', () => {
|
||||||
const res = render(renderWrappedComponent('DataSourceDefinitionExternal'));
|
const s = mock('prices.external.vegaprotocol.builtin.');
|
||||||
|
expect(isInternalSourceType(s)).toEqual(false);
|
||||||
|
const res = render(renderWrappedComponent(s));
|
||||||
expect(res.getByText('External data')).toBeInTheDocument();
|
expect(res.getByText('External data')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,27 +1,51 @@
|
|||||||
import { TableRow, TableCell, TableHeader } from '../../../components/table';
|
import { TableRow, TableCell, TableHeader } from '../../../components/table';
|
||||||
import type { SourceType } from './oracle';
|
import type { SourceType } from './oracle';
|
||||||
|
|
||||||
export type SourceTypeName = SourceType['__typename'] | undefined;
|
/**
|
||||||
|
* Basic function to determine if a source is internal or external.
|
||||||
|
*
|
||||||
|
* This should be distinguishable using __typename, but the type is incorrectly
|
||||||
|
* reported at the moment, so instead we check the filters.
|
||||||
|
*
|
||||||
|
* @param s SourceType
|
||||||
|
* @returns boolean True if the source is internal
|
||||||
|
*/
|
||||||
|
export function isInternalSourceType(s: SourceType) {
|
||||||
|
if ('filters' in s.sourceType) {
|
||||||
|
const filters = s.sourceType.filters;
|
||||||
|
|
||||||
|
if (filters) {
|
||||||
|
return (
|
||||||
|
filters?.filter((f) => {
|
||||||
|
return f.key.name?.indexOf('vegaprotocol.builtin.') === 0;
|
||||||
|
}).length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
interface OracleDetailsTypeProps {
|
interface OracleDetailsTypeProps {
|
||||||
type: SourceTypeName;
|
sourceType: SourceType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a a single table row for the Oracle Details view that shows
|
* Renders a a single table row for the Oracle Details view that shows
|
||||||
* if the oracle is using the internal time oracle or external data
|
* if the oracle is using the internal time oracle or external data
|
||||||
*/
|
*/
|
||||||
export function OracleDetailsType({ type }: OracleDetailsTypeProps) {
|
export function OracleDetailsType({ sourceType }: OracleDetailsTypeProps) {
|
||||||
if (!type) {
|
if (!sourceType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isInternal = isInternalSourceType(sourceType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow modifier="bordered">
|
<TableRow modifier="bordered">
|
||||||
<TableHeader scope="row">Type</TableHeader>
|
<TableHeader scope="row">Type</TableHeader>
|
||||||
<TableCell modifier="bordered">
|
<TableCell modifier="bordered">
|
||||||
{type === 'DataSourceDefinitionInternal'
|
{isInternal ? 'Internal data' : 'External data'}
|
||||||
? 'Internal time'
|
|
||||||
: 'External data'}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import { render } from '@testing-library/react';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import type { SourceType } from './oracle';
|
import type { SourceType } from './oracle';
|
||||||
import { OracleSigners } from './oracle-signers';
|
import { OracleSigners } from './oracle-signers';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
function renderComponent(sourceType: SourceType) {
|
function renderComponent(sourceType: SourceType) {
|
||||||
return (
|
return (
|
||||||
<MemoryRouter>
|
<MockedProvider>
|
||||||
<OracleSigners sourceType={sourceType} />
|
<MemoryRouter>
|
||||||
</MemoryRouter>
|
<OracleSigners sourceType={sourceType} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const OracleDetails = ({
|
|||||||
<OracleLink id={id} />
|
<OracleLink id={id} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<OracleDetailsType type={sourceType.__typename} />
|
<OracleDetailsType sourceType={sourceType} />
|
||||||
<OracleSigners sourceType={sourceType} />
|
<OracleSigners sourceType={sourceType} />
|
||||||
<OracleMarkets id={id} />
|
<OracleMarkets id={id} />
|
||||||
<TableRow modifier="bordered">
|
<TableRow modifier="bordered">
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { RouteTitle } from '../../components/route-title';
|
||||||
|
|
||||||
|
export const Disclaimer = () => {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="px-40 max-sm:px-0 max-md:px-10 mb-4 max-w-5xl">
|
||||||
|
<RouteTitle data-testid="disclaimer-header">
|
||||||
|
{t('Disclaimer')}
|
||||||
|
</RouteTitle>
|
||||||
|
<p className="mt-3">
|
||||||
|
The Vega Block Explorer is an application that allows users to, among
|
||||||
|
other things, browse through blocks, view wallet addresses, network
|
||||||
|
hashrate, transaction data and other key information on the Vega
|
||||||
|
blockchain. It is free, public and open source software. Software
|
||||||
|
upgrades may contain bugs or security vulnerabilities that might
|
||||||
|
result in loss of functionality.
|
||||||
|
</p>
|
||||||
|
<p className="mt-3">
|
||||||
|
The Vega Block Explorer uses data from nodes on the Vega Blockchain.
|
||||||
|
The developers of the Vega Block Explorer do not operate or run the
|
||||||
|
Vega Blockchain or any other blockchain.
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 font-semibold">
|
||||||
|
The Vega Block Explorer is provided “as is”. The developers of the
|
||||||
|
Vega Block Explorer make no representations or warranties of any kind,
|
||||||
|
whether express or implied, statutory or otherwise regarding the Vega
|
||||||
|
Block Explorer. They disclaim all warranties of merchantability,
|
||||||
|
quality, fitness for purpose. They disclaim all warranties that the
|
||||||
|
Vega Block Explorer is free of harmful components or errors.
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 font-bold">
|
||||||
|
No developer of the Vega Block Explorer accepts any responsibility
|
||||||
|
for, or liability to users in connection with their use of the Vega
|
||||||
|
Block Explorer.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { SubHeading } from '../../../components/sub-heading';
|
import { SubHeading } from '../../../components/sub-heading';
|
||||||
import { toNonHex } from '../../../components/search/detect-search';
|
import { toNonHex } from '../../../components/search/detect-search';
|
||||||
@@ -14,8 +14,11 @@ import { PartyBlockStake } from './components/party-block-stake';
|
|||||||
import { PartyBlockAccounts } from './components/party-block-accounts';
|
import { PartyBlockAccounts } from './components/party-block-accounts';
|
||||||
import { isValidPartyId } from './components/party-id-error';
|
import { isValidPartyId } from './components/party-id-error';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
|
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
||||||
|
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
||||||
|
|
||||||
const Party = () => {
|
const Party = () => {
|
||||||
|
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
||||||
const { party } = useParams<{ party: string }>();
|
const { party } = useParams<{ party: string }>();
|
||||||
|
|
||||||
useDocumentTitle(['Public keys', party || '-']);
|
useDocumentTitle(['Public keys', party || '-']);
|
||||||
@@ -24,10 +27,24 @@ const Party = () => {
|
|||||||
const partyId = toNonHex(party ? party : '');
|
const partyId = toNonHex(party ? party : '');
|
||||||
const { isMobile } = useScreenDimensions();
|
const { isMobile } = useScreenDimensions();
|
||||||
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
|
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
|
||||||
const filters = `filters[tx.submitter]=${partyId}`;
|
const baseFilters = `filters[tx.submitter]=${partyId}`;
|
||||||
const { hasMoreTxs, loadTxs, error, txsData, loading } = useTxsData({
|
const f =
|
||||||
limit: 10,
|
filters && filters.size === 1
|
||||||
filters,
|
? `${baseFilters}&filters[cmd.type]=${Array.from(filters)[0]}`
|
||||||
|
: baseFilters;
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasMoreTxs,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
error,
|
||||||
|
refreshTxs,
|
||||||
|
loading,
|
||||||
|
txsData,
|
||||||
|
hasPreviousPage,
|
||||||
|
} = useTxsData({
|
||||||
|
limit: 25,
|
||||||
|
filters: f,
|
||||||
});
|
});
|
||||||
|
|
||||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
@@ -81,14 +98,24 @@ const Party = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SubHeading>{t('Transactions')}</SubHeading>
|
<SubHeading>{t('Transactions')}</SubHeading>
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={refreshTxs}
|
||||||
|
nextPage={nextPage}
|
||||||
|
previousPage={previousPage}
|
||||||
|
hasPreviousPage={hasPreviousPage}
|
||||||
|
loading={loading}
|
||||||
|
hasMoreTxs={hasMoreTxs}
|
||||||
|
>
|
||||||
|
<TxsFilter filters={filters} setFilters={setFilters} />
|
||||||
|
</TxsListNavigation>
|
||||||
{!error && txsData ? (
|
{!error && txsData ? (
|
||||||
<TxsInfiniteList
|
<TxsInfiniteList
|
||||||
hasMoreTxs={hasMoreTxs}
|
hasMoreTxs={hasMoreTxs}
|
||||||
areTxsLoading={loading}
|
areTxsLoading={loading}
|
||||||
txs={txsData}
|
txs={txsData}
|
||||||
loadMoreTxs={loadTxs}
|
loadMoreTxs={nextPage}
|
||||||
error={error}
|
error={error}
|
||||||
className="mb-28"
|
className="mb-28 w-full"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Splash>
|
<Splash>
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ export const Routes = {
|
|||||||
MARKETS: 'markets',
|
MARKETS: 'markets',
|
||||||
ORACLES: 'oracles',
|
ORACLES: 'oracles',
|
||||||
NETWORK_PARAMETERS: 'network-parameters',
|
NETWORK_PARAMETERS: 'network-parameters',
|
||||||
|
DISCLAIMER: 'disclaimer',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { AssetLink, MarketLink } from '../components/links';
|
|||||||
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||||
import { remove0x } from '@vegaprotocol/utils';
|
import { remove0x } from '@vegaprotocol/utils';
|
||||||
import { PartyAccountsByAsset } from './parties/id/accounts';
|
import { PartyAccountsByAsset } from './parties/id/accounts';
|
||||||
|
import { Disclaimer } from './pages/disclaimer';
|
||||||
|
|
||||||
export type Navigable = {
|
export type Navigable = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -335,6 +336,17 @@ export const routerConfig: Route[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: Routes.DISCLAIMER,
|
||||||
|
element: <Disclaimer />,
|
||||||
|
handle: {
|
||||||
|
name: t('Disclaimer'),
|
||||||
|
text: t('Disclaimer'),
|
||||||
|
breadcrumb: () => (
|
||||||
|
<Link to={Routes.DISCLAIMER}>{t('Disclaimer')}</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
...partiesRoutes,
|
...partiesRoutes,
|
||||||
...assetsRoutes,
|
...assetsRoutes,
|
||||||
...genesisRoutes,
|
...genesisRoutes,
|
||||||
|
|||||||
@@ -1,29 +1,74 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { RouteTitle } from '../../../components/route-title';
|
import { RouteTitle } from '../../../components/route-title';
|
||||||
import { BlocksRefetch } from '../../../components/blocks';
|
|
||||||
import { TxsInfiniteList } from '../../../components/txs';
|
import { TxsInfiniteList } from '../../../components/txs';
|
||||||
import { useTxsData } from '../../../hooks/use-txs-data';
|
import { useTxsData } from '../../../hooks/use-txs-data';
|
||||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||||
|
|
||||||
const BE_TXS_PER_REQUEST = 20;
|
import { useState } from 'react';
|
||||||
|
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
||||||
|
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
||||||
|
|
||||||
|
const BE_TXS_PER_REQUEST = 25;
|
||||||
|
|
||||||
export const TxsList = () => {
|
export const TxsList = () => {
|
||||||
useDocumentTitle(['Transactions']);
|
useDocumentTitle(['Transactions']);
|
||||||
|
|
||||||
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
|
|
||||||
useTxsData({ limit: BE_TXS_PER_REQUEST });
|
|
||||||
return (
|
return (
|
||||||
<section className="md:p-2 lg:p-4 xl:p-6">
|
<section className="md:p-2 lg:p-4 xl:p-6 relative">
|
||||||
<RouteTitle>{t('Transactions')}</RouteTitle>
|
<RouteTitle>{t('Transactions')}</RouteTitle>
|
||||||
<BlocksRefetch refetch={refreshTxs} />
|
<TxsListFiltered />
|
||||||
<TxsInfiniteList
|
|
||||||
hasMoreTxs={hasMoreTxs}
|
|
||||||
areTxsLoading={loading}
|
|
||||||
txs={txsData}
|
|
||||||
loadMoreTxs={loadTxs}
|
|
||||||
error={error}
|
|
||||||
className="mb-28"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a list of transactions with filters and controls to navigate through the list.
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Transaction List and controls
|
||||||
|
*/
|
||||||
|
export const TxsListFiltered = () => {
|
||||||
|
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
||||||
|
|
||||||
|
const f =
|
||||||
|
filters && filters.size === 1
|
||||||
|
? `filters[cmd.type]=${Array.from(filters)[0]}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasMoreTxs,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
error,
|
||||||
|
refreshTxs,
|
||||||
|
loading,
|
||||||
|
txsData,
|
||||||
|
hasPreviousPage,
|
||||||
|
} = useTxsData({
|
||||||
|
limit: BE_TXS_PER_REQUEST,
|
||||||
|
filters: f,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TxsListNavigation
|
||||||
|
refreshTxs={refreshTxs}
|
||||||
|
nextPage={nextPage}
|
||||||
|
previousPage={previousPage}
|
||||||
|
hasPreviousPage={hasPreviousPage}
|
||||||
|
loading={loading}
|
||||||
|
hasMoreTxs={hasMoreTxs}
|
||||||
|
>
|
||||||
|
<TxsFilter filters={filters} setFilters={setFilters} />
|
||||||
|
</TxsListNavigation>
|
||||||
|
<TxsInfiniteList
|
||||||
|
hasFilters={filters.size > 0}
|
||||||
|
hasMoreTxs={hasMoreTxs}
|
||||||
|
areTxsLoading={loading}
|
||||||
|
txs={txsData}
|
||||||
|
loadMoreTxs={nextPage}
|
||||||
|
error={error}
|
||||||
|
className="mb-28 w-full min-w-[400px]"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { TxDetails } from './tx-details';
|
import { TxDetails } from './tx-details';
|
||||||
import type {
|
import type {
|
||||||
BlockExplorerTransactionResult,
|
BlockExplorerTransactionResult,
|
||||||
ValidatorHeartbeat,
|
ValidatorHeartbeat,
|
||||||
} from '../../../routes/types/block-explorer-response';
|
} from '../../../routes/types/block-explorer-response';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
// Note: Long enough that there is a truncated output and a full output
|
// Note: Long enough that there is a truncated output and a full output
|
||||||
const pubKey =
|
const pubKey =
|
||||||
@@ -27,9 +28,11 @@ const txData: BlockExplorerTransactionResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderComponent = (txData: BlockExplorerTransactionResult) => (
|
const renderComponent = (txData: BlockExplorerTransactionResult) => (
|
||||||
<Router>
|
<MemoryRouter>
|
||||||
<TxDetails txData={txData} pubKey={pubKey} />
|
<MockedProvider>
|
||||||
</Router>
|
<TxDetails txData={txData} pubKey={pubKey} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('Transaction details', () => {
|
describe('Transaction details', () => {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
query ExplorerNodeNames {
|
||||||
|
nodesConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
pubkey
|
||||||
|
tmPubkey
|
||||||
|
ethereumAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
export type ExplorerNodeNamesQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type ExplorerNodeNamesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, pubkey: string, tmPubkey: string, ethereumAddress: string } } | null> | null } };
|
||||||
|
|
||||||
|
|
||||||
|
export const ExplorerNodeNamesDocument = gql`
|
||||||
|
query ExplorerNodeNames {
|
||||||
|
nodesConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
pubkey
|
||||||
|
tmPubkey
|
||||||
|
ethereumAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useExplorerNodeNamesQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useExplorerNodeNamesQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useExplorerNodeNamesQuery` 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 } = useExplorerNodeNamesQuery({
|
||||||
|
* variables: {
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useExplorerNodeNamesQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>(ExplorerNodeNamesDocument, options);
|
||||||
|
}
|
||||||
|
export function useExplorerNodeNamesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>(ExplorerNodeNamesDocument, options);
|
||||||
|
}
|
||||||
|
export type ExplorerNodeNamesQueryHookResult = ReturnType<typeof useExplorerNodeNamesQuery>;
|
||||||
|
export type ExplorerNodeNamesLazyQueryHookResult = ReturnType<typeof useExplorerNodeNamesLazyQuery>;
|
||||||
|
export type ExplorerNodeNamesQueryResult = Apollo.QueryResult<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
@import 'ag-grid-community/dist/styles/ag-grid.css';
|
@import 'ag-grid-community/styles/ag-grid.css';
|
||||||
@import 'ag-grid-community/dist/styles/ag-theme-balham.css';
|
@import 'ag-grid-community/styles/ag-theme-balham.css';
|
||||||
@import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css';
|
|
||||||
|
|
||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@@ -32,9 +31,9 @@
|
|||||||
line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px);
|
line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vega-ag-grid .ag-row {
|
.vega-ag-grid .ag-row,
|
||||||
border-width: 1px 0;
|
.vega-ag-grid .ag-cell {
|
||||||
border-bottom: 1px solid transparent;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light variables */
|
/* Light variables */
|
||||||
@@ -46,7 +45,6 @@
|
|||||||
--ag-header-column-separator-color: theme(colors.neutral[300]);
|
--ag-header-column-separator-color: theme(colors.neutral[300]);
|
||||||
--ag-row-border-color: theme(colors.white);
|
--ag-row-border-color: theme(colors.white);
|
||||||
--ag-row-hover-color: theme(colors.neutral[100]);
|
--ag-row-hover-color: theme(colors.neutral[100]);
|
||||||
--ag-font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark variables */
|
/* Dark variables */
|
||||||
@@ -58,5 +56,8 @@
|
|||||||
--ag-header-column-separator-color: theme(colors.neutral[600]);
|
--ag-header-column-separator-color: theme(colors.neutral[600]);
|
||||||
--ag-row-border-color: theme(colors.black);
|
--ag-row-border-color: theme(colors.black);
|
||||||
--ag-row-hover-color: theme(colors.neutral[800]);
|
--ag-row-hover-color: theme(colors.neutral[800]);
|
||||||
--ag-font-size: 12px;
|
}
|
||||||
|
|
||||||
|
.voteicon svg {
|
||||||
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
|
|||||||
+428
-421
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user