mirror of
https://github.com/oceanprotocol/commons.git
synced 2023-03-15 18:03:00 +01:00
merge
This commit is contained in:
commit
a7d6af6006
@ -1 +0,0 @@
|
|||||||
node_modules
|
|
11
.eslintrc
11
.eslintrc
@ -10,19 +10,22 @@
|
|||||||
"prettier/standard",
|
"prettier/standard",
|
||||||
"plugin:prettier/recommended",
|
"plugin:prettier/recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"prettier/@typescript-eslint"
|
"prettier/@typescript-eslint",
|
||||||
|
"plugin:cypress/recommended"
|
||||||
],
|
],
|
||||||
"plugins": ["@typescript-eslint", "prettier"],
|
"plugins": ["@typescript-eslint", "prettier", "cypress"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/explicit-function-return-type": 0,
|
"@typescript-eslint/explicit-function-return-type": 0,
|
||||||
"@typescript-eslint/member-delimiter-style": [
|
"@typescript-eslint/member-delimiter-style": [
|
||||||
"error",
|
"error",
|
||||||
{ "multiline": { "delimiter": "none" } }
|
{ "multiline": { "delimiter": "none" } }
|
||||||
]
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "off"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"es6": true,
|
"es6": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"jest": true
|
"jest": true,
|
||||||
|
"cypress/globals": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -12,6 +12,7 @@ dist
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
@ -20,3 +21,7 @@ dist
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
# cypress
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
coverage
|
||||||
|
31
.travis.yml
31
.travis.yml
@ -1,24 +1,49 @@
|
|||||||
dist: xenial
|
dist: xenial
|
||||||
|
sudo: required
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- '11'
|
- '11'
|
||||||
|
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
# for Cypress
|
||||||
|
- libgconf-2-4
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
# run E2E tests against these values
|
||||||
|
- REACT_APP_NODE_URI="https://pacific.oceanprotocol.com"
|
||||||
|
- REACT_APP_AQUARIUS_URI="https://aquarius.test.oceanprotocol.com"
|
||||||
|
- REACT_APP_BRIZO_URI="https://brizo.test.oceanprotocol.com"
|
||||||
|
- REACT_APP_SECRET_STORE_URI="https://secret-store.oceanprotocol.com"
|
||||||
|
- REACT_APP_FAUCET_URI="https://faucet.oceanprotocol.com"
|
||||||
|
- REACT_APP_BRIZO_ADDRESS="0x0474ed05ba757dde575dfaaaa267d9e7f3643abc"
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- npm install -g npm
|
- npm install -g npm
|
||||||
- npm install -g codacy-coverage
|
- npm install -g codacy-coverage truffle ganache-cli
|
||||||
|
# Fixes an issue where the max file watch count is exceeded, triggering ENOSPC
|
||||||
|
# https://stackoverflow.com/questions/22475849/node-js-error-enospc#32600959
|
||||||
|
- echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
|
||||||
|
|
||||||
script:
|
script:
|
||||||
# - ./scripts/install.sh # runs automatically with npm ci
|
# - ./scripts/install.sh # runs automatically with npm ci
|
||||||
- ./scripts/test.sh
|
# executing `npm test` scripts individually here, so first one failing will exit the build
|
||||||
|
- npm run lint || travis_terminate 1
|
||||||
|
- ./scripts/test.sh || travis_terminate 1
|
||||||
- ./scripts/coverage.sh
|
- ./scripts/coverage.sh
|
||||||
|
- npm run test:e2e || travis_terminate 1
|
||||||
- ./scripts/build.sh
|
- ./scripts/build.sh
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
|
npm: true
|
||||||
directories:
|
directories:
|
||||||
- node_modules
|
# cache folder with Cypress binary
|
||||||
|
- ~/.cache
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
- provider: script
|
- provider: script
|
||||||
|
173
CHANGELOG.md
173
CHANGELOG.md
@ -4,6 +4,179 @@ All notable changes to this project will be documented in this file. Dates are d
|
|||||||
|
|
||||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
#### [v0.6.3](https://github.com/oceanprotocol/commons/compare/v0.6.2...v0.6.3)
|
||||||
|
|
||||||
|
> 9 July 2019
|
||||||
|
|
||||||
|
- server: adapt to new tsc output [`362a266`](https://github.com/oceanprotocol/commons/commit/362a26651822f450d4b9a528c7fa46d4b9bbbfba)
|
||||||
|
|
||||||
|
#### [v0.6.2](https://github.com/oceanprotocol/commons/compare/v0.6.1...v0.6.2)
|
||||||
|
|
||||||
|
> 9 July 2019
|
||||||
|
|
||||||
|
- Release 0.6.2 [`2a9d747`](https://github.com/oceanprotocol/commons/commit/2a9d7476c8a3038e2edcadc36df8861921512f2a)
|
||||||
|
- server build fix [`4ed72f3`](https://github.com/oceanprotocol/commons/commit/4ed72f382c2c0ea0ee5fedc0bdb6050555b88361)
|
||||||
|
|
||||||
|
#### [v0.6.1](https://github.com/oceanprotocol/commons/compare/v0.6.0...v0.6.1)
|
||||||
|
|
||||||
|
> 9 July 2019
|
||||||
|
|
||||||
|
- Reporting data sets [`#172`](https://github.com/oceanprotocol/commons/pull/172)
|
||||||
|
- email sending via sendgrid [`cee4997`](https://github.com/oceanprotocol/commons/commit/cee49978c495a64b20e3a5f3a3ae296dbef140dd)
|
||||||
|
- send Slack message [`fa078c6`](https://github.com/oceanprotocol/commons/commit/fa078c6c4c5677a3f7bc98751c5c726f377a1e00)
|
||||||
|
- add Modal component [`cb1e0ca`](https://github.com/oceanprotocol/commons/commit/cb1e0ca624a6a0aa72b87cf913f581efbe81d320)
|
||||||
|
|
||||||
|
#### [v0.6.0](https://github.com/oceanprotocol/commons/compare/v0.5.4...v0.6.0)
|
||||||
|
|
||||||
|
> 4 July 2019
|
||||||
|
|
||||||
|
- default all connections to Pacific [`#164`](https://github.com/oceanprotocol/commons/pull/164)
|
||||||
|
- fix version tags, fixed squid-js [`#166`](https://github.com/oceanprotocol/commons/pull/166)
|
||||||
|
- Connect to pacific [`#167`](https://github.com/oceanprotocol/commons/pull/167)
|
||||||
|
- Cypress cleanup & fixes [`#165`](https://github.com/oceanprotocol/commons/pull/165)
|
||||||
|
- End-to-end testing setup with Cypress [`#134`](https://github.com/oceanprotocol/commons/pull/134)
|
||||||
|
- fresh package-lock [`9b9db4b`](https://github.com/oceanprotocol/commons/commit/9b9db4b655b10d2255201a2abc37b234be2e9c68)
|
||||||
|
- bump packages [`96e5363`](https://github.com/oceanprotocol/commons/commit/96e5363859c4c2cda1d00928cf3595daac8e1e84)
|
||||||
|
- add docs, consolidate more config values [`8717621`](https://github.com/oceanprotocol/commons/commit/87176212efd0648873e1f3f26c7767dd475da52c)
|
||||||
|
|
||||||
|
#### [v0.5.4](https://github.com/oceanprotocol/commons/compare/v0.5.3...v0.5.4)
|
||||||
|
|
||||||
|
> 25 June 2019
|
||||||
|
|
||||||
|
- bump to squid-js v0.6.0 [`#163`](https://github.com/oceanprotocol/commons/pull/163)
|
||||||
|
- switch to axios for file url check [`#162`](https://github.com/oceanprotocol/commons/pull/162)
|
||||||
|
- revert package-lock [`24b68ba`](https://github.com/oceanprotocol/commons/commit/24b68baa2488da52ad2cb510b2750ab5cafef4ac)
|
||||||
|
- use pacific, fix export vars [`67f7368`](https://github.com/oceanprotocol/commons/commit/67f736809b8097434c7960b4be7ccd208e182b98)
|
||||||
|
- remove faucet + target nile node [`3b3c08f`](https://github.com/oceanprotocol/commons/commit/3b3c08f5490b56e17e20248d12f61c8eabce4b52)
|
||||||
|
|
||||||
|
#### [v0.5.3](https://github.com/oceanprotocol/commons/compare/v0.5.2...v0.5.3)
|
||||||
|
|
||||||
|
> 19 June 2019
|
||||||
|
|
||||||
|
- SEO component [`#159`](https://github.com/oceanprotocol/commons/pull/159)
|
||||||
|
- switch to axios for file publish [`7043266`](https://github.com/oceanprotocol/commons/commit/70432662542a4de24133bfdd8906e569adfeb606)
|
||||||
|
- Release 0.5.3 [`75a262f`](https://github.com/oceanprotocol/commons/commit/75a262f5d633bd0f053ead0b5fd44d4ad56bd029)
|
||||||
|
|
||||||
|
#### [v0.5.2](https://github.com/oceanprotocol/commons/compare/v0.5.1...v0.5.2)
|
||||||
|
|
||||||
|
> 19 June 2019
|
||||||
|
|
||||||
|
- add config values for Pacific connection [`#161`](https://github.com/oceanprotocol/commons/pull/161)
|
||||||
|
- Refactor VersionNumbers to be sourced from squid-js [`#160`](https://github.com/oceanprotocol/commons/pull/160)
|
||||||
|
- fix web3 version + use truffle-hdwallet [`d364a7b`](https://github.com/oceanprotocol/commons/commit/d364a7beef5dd547b2d41f2907736481734b1c8b)
|
||||||
|
- add tests [`e5960d3`](https://github.com/oceanprotocol/commons/commit/e5960d3fd621ebe3d7af267d98cfcce1262e93fb)
|
||||||
|
- output overall status [`059ae62`](https://github.com/oceanprotocol/commons/commit/059ae62f967f40d9635cc729d31ab84fa913df8b)
|
||||||
|
|
||||||
|
#### [v0.5.1](https://github.com/oceanprotocol/commons/compare/v0.5.0...v0.5.1)
|
||||||
|
|
||||||
|
> 14 June 2019
|
||||||
|
|
||||||
|
- Submarine links and Pacific support [`#158`](https://github.com/oceanprotocol/commons/pull/158)
|
||||||
|
- tweak CI app starting [`81cb06e`](https://github.com/oceanprotocol/commons/commit/81cb06e19b56a069c0f1827e909005831d42606e)
|
||||||
|
- prototype using truffle-privatekey-provider & ganache to provide accounts [`d8f4ebe`](https://github.com/oceanprotocol/commons/commit/d8f4ebe7b4ed2592a6e8b01e52e5ed3ffefd4aa5)
|
||||||
|
- cleanup, add asset fixture, add eslint-plugin-cypress [`8f73c08`](https://github.com/oceanprotocol/commons/commit/8f73c08fdd1de3d04b0aa171515378ca75c8fe67)
|
||||||
|
|
||||||
|
#### [v0.5.0](https://github.com/oceanprotocol/commons/compare/v0.4.5...v0.5.0)
|
||||||
|
|
||||||
|
> 12 June 2019
|
||||||
|
|
||||||
|
- make price a string, Aquarius 0.2.7 validation updates [`#142`](https://github.com/oceanprotocol/commons/pull/142)
|
||||||
|
- simplify Ocean URIs and env variables, document .env usage [`#157`](https://github.com/oceanprotocol/commons/pull/157)
|
||||||
|
- version number fixes [`#156`](https://github.com/oceanprotocol/commons/pull/156)
|
||||||
|
- additions to Ocean versions output [`#155`](https://github.com/oceanprotocol/commons/pull/155)
|
||||||
|
- highly simplify Ocean URIs and env variables [`4b919f2`](https://github.com/oceanprotocol/commons/commit/4b919f2ee86b5e507d293fbe6684d296c2574d28)
|
||||||
|
- update tests [`264a066`](https://github.com/oceanprotocol/commons/commit/264a066874dbe55950f54754924e2a5c4d2b69ec)
|
||||||
|
- mock file fetch request [`7d727bc`](https://github.com/oceanprotocol/commons/commit/7d727bcdf95ada24304cc03a2a63c35a644d2a08)
|
||||||
|
|
||||||
|
#### [v0.4.5](https://github.com/oceanprotocol/commons/compare/v0.4.4...v0.4.5)
|
||||||
|
|
||||||
|
> 6 June 2019
|
||||||
|
|
||||||
|
- hotfix for failing server run on Docker [`#154`](https://github.com/oceanprotocol/commons/pull/154)
|
||||||
|
- Release 0.4.5 [`e499430`](https://github.com/oceanprotocol/commons/commit/e4994308ad369595d88e1d7b524d986d80c58cd7)
|
||||||
|
|
||||||
|
#### [v0.4.4](https://github.com/oceanprotocol/commons/compare/v0.4.3...v0.4.4)
|
||||||
|
|
||||||
|
> 6 June 2019
|
||||||
|
|
||||||
|
- switch to new Travis caching strategy [`#153`](https://github.com/oceanprotocol/commons/pull/153)
|
||||||
|
- fix account address display in Firefox [`#152`](https://github.com/oceanprotocol/commons/pull/152)
|
||||||
|
- Release 0.4.4 [`5f19fbe`](https://github.com/oceanprotocol/commons/commit/5f19fbe4206045a6ba086173403f1594d0806be1)
|
||||||
|
|
||||||
|
#### [v0.4.3](https://github.com/oceanprotocol/commons/compare/v0.4.2...v0.4.3)
|
||||||
|
|
||||||
|
> 3 June 2019
|
||||||
|
|
||||||
|
- output event messages during publishing flow [`#151`](https://github.com/oceanprotocol/commons/pull/151)
|
||||||
|
- squid-js v0.5.14 [`b27c458`](https://github.com/oceanprotocol/commons/commit/b27c458fb20af086063556831f8bc695c5f1c89b)
|
||||||
|
- Release 0.4.3 [`303a7bf`](https://github.com/oceanprotocol/commons/commit/303a7bf12ca26a716de38ad9a14ef274c60b661b)
|
||||||
|
- update tests [`56604b9`](https://github.com/oceanprotocol/commons/commit/56604b972c6dc99953e31cc4a4e24cd7c0e0a34e)
|
||||||
|
|
||||||
|
#### [v0.4.2](https://github.com/oceanprotocol/commons/compare/v0.4.1...v0.4.2)
|
||||||
|
|
||||||
|
> 31 May 2019
|
||||||
|
|
||||||
|
- test fixes [`#150`](https://github.com/oceanprotocol/commons/pull/150)
|
||||||
|
- Reuse agreements on consume flow [`#148`](https://github.com/oceanprotocol/commons/pull/148)
|
||||||
|
- show faucet version number, small refactor [`#147`](https://github.com/oceanprotocol/commons/pull/147)
|
||||||
|
- change brizo fallback address [`#146`](https://github.com/oceanprotocol/commons/pull/146)
|
||||||
|
- prevent test runner conflicts, kick out jasmine, lock TypeScript [`9888d78`](https://github.com/oceanprotocol/commons/commit/9888d78f99cebc84e8f10aa0402c2ef0bd15df49)
|
||||||
|
- formatting and test tweaks [`1e6b334`](https://github.com/oceanprotocol/commons/commit/1e6b334cdca79e93a585d8c23f6551142d7bf1c0)
|
||||||
|
- prevent text runner conflicts, kick out jasmine, TypeScript update [`4fd623f`](https://github.com/oceanprotocol/commons/commit/4fd623f28e3d1b1d3799ce59c6563cfee81646d2)
|
||||||
|
|
||||||
|
#### [v0.4.1](https://github.com/oceanprotocol/commons/compare/v0.4.0...v0.4.1)
|
||||||
|
|
||||||
|
> 28 May 2019
|
||||||
|
|
||||||
|
- output version numbers, simplify release tasks, make automatic changelog work [`#145`](https://github.com/oceanprotocol/commons/pull/145)
|
||||||
|
- version numbers as component, fetch Brizo & Aquarius [`043d942`](https://github.com/oceanprotocol/commons/commit/043d9429ac76fe4af8099f46d73986e0e488a055)
|
||||||
|
- simplify release tasks, automatic changelog [`41d6726`](https://github.com/oceanprotocol/commons/commit/41d6726beda89a50a3d2c1232451226cabf174e3)
|
||||||
|
- package-locks [`c742426`](https://github.com/oceanprotocol/commons/commit/c742426b1a00a1f5776458dda133742256f4bdd9)
|
||||||
|
|
||||||
|
#### [v0.4.0](https://github.com/oceanprotocol/commons/compare/v0.3.2...v0.4.0)
|
||||||
|
|
||||||
|
> 28 May 2019
|
||||||
|
|
||||||
|
- AI For Good: channels, new front-page & categories list [`#125`](https://github.com/oceanprotocol/commons/pull/125)
|
||||||
|
- asset files & fixed metadata tweaks [`be6c478`](https://github.com/oceanprotocol/commons/commit/be6c478ca723e7face0fc0d37129fd9ff183cffe)
|
||||||
|
- cleanup contentType in one central place, add more manual replacements [`5e94d73`](https://github.com/oceanprotocol/commons/commit/5e94d73197275a89e9460f98dab5408a7dc1f52a)
|
||||||
|
- fix tests [`d74a4c0`](https://github.com/oceanprotocol/commons/commit/d74a4c0cbca57e5bf0f8e687148dd0332334acfc)
|
||||||
|
|
||||||
|
#### [v0.3.2](https://github.com/oceanprotocol/commons/compare/v0.3.1...v0.3.2)
|
||||||
|
|
||||||
|
> 27 May 2019
|
||||||
|
|
||||||
|
- Add range error handling [`#144`](https://github.com/oceanprotocol/commons/pull/144)
|
||||||
|
- rebase fix [`90b163b`](https://github.com/oceanprotocol/commons/commit/90b163b2aa1703fd4e450e4e83076ed4522b0aad)
|
||||||
|
- category search, make multiple layouts on one page possible [`1b1ac5c`](https://github.com/oceanprotocol/commons/commit/1b1ac5c9ef75e3a67d949a07e8177245e7912fe7)
|
||||||
|
- channel teaser component, use on channels page [`1b7d343`](https://github.com/oceanprotocol/commons/commit/1b7d34398490a14a0fd62db7b28e54dab14acc30)
|
||||||
|
|
||||||
|
#### [v0.3.1](https://github.com/oceanprotocol/commons/compare/v0.3.0...v0.3.1)
|
||||||
|
|
||||||
|
> 27 May 2019
|
||||||
|
|
||||||
|
- remove AI For Good as category [`#143`](https://github.com/oceanprotocol/commons/pull/143)
|
||||||
|
- update changelog [`88872cc`](https://github.com/oceanprotocol/commons/commit/88872cc020f0c10efcd282169c491b5e05cef916)
|
||||||
|
- Release 0.3.1 [`54f3f17`](https://github.com/oceanprotocol/commons/commit/54f3f170c3fa2be750963afa19103d1c2e202586)
|
||||||
|
|
||||||
|
#### [v0.3.0](https://github.com/oceanprotocol/commons/compare/v0.2.14...v0.3.0)
|
||||||
|
|
||||||
|
> 21 May 2019
|
||||||
|
|
||||||
|
- Consume feedback mesages [`#110`](https://github.com/oceanprotocol/commons/pull/110)
|
||||||
|
- fresh package-lock [`743fe53`](https://github.com/oceanprotocol/commons/commit/743fe533dc848dd1f120e6086e3ef311a23395a0)
|
||||||
|
- update changelog [`01e68b6`](https://github.com/oceanprotocol/commons/commit/01e68b6632b44c71ee8f6f0521d56355d2c4f6a7)
|
||||||
|
- bump required component versions [`d6a7800`](https://github.com/oceanprotocol/commons/commit/d6a7800cc4b408f12e2e6d4b8c5c6100536ab2d7)
|
||||||
|
|
||||||
|
#### [v0.2.14](https://github.com/oceanprotocol/commons/compare/v0.2.13...v0.2.14)
|
||||||
|
|
||||||
|
> 20 May 2019
|
||||||
|
|
||||||
|
- AI Commons link [`#137`](https://github.com/oceanprotocol/commons/pull/137)
|
||||||
|
- message tweaks [`0e12204`](https://github.com/oceanprotocol/commons/commit/0e12204a5a0b72b070f5b7a8cc9d14c351cff3c2)
|
||||||
|
- message output refactor, testing [`879f511`](https://github.com/oceanprotocol/commons/commit/879f51170ea0dfdf97d72a3594ac73b920d52162)
|
||||||
|
- add AI Commons logo [`081772c`](https://github.com/oceanprotocol/commons/commit/081772ce3764b95d67cf685c5d71c335c7f68d47)
|
||||||
|
|
||||||
#### [v0.2.13](https://github.com/oceanprotocol/commons/compare/v0.2.12...v0.2.13)
|
#### [v0.2.13](https://github.com/oceanprotocol/commons/compare/v0.2.12...v0.2.13)
|
||||||
|
|
||||||
> 20 May 2019
|
> 20 May 2019
|
||||||
|
108
README.md
108
README.md
@ -22,22 +22,28 @@
|
|||||||
|
|
||||||
If you're a developer and want to contribute to, or want to utilize this marketplace's code in your projects, then keep on reading.
|
If you're a developer and want to contribute to, or want to utilize this marketplace's code in your projects, then keep on reading.
|
||||||
|
|
||||||
- [🏄 Get Started](#-get-started)
|
- [🏄 Get Started](#-Get-Started)
|
||||||
- [🏖 Remote Ocean: Nile](#-remote-ocean-nile)
|
- [🏖 Remote Ocean: Pacific](#-Remote-Ocean-Pacific)
|
||||||
- [🐳 Use with Barge](#-use-with-barge)
|
- [🐳 Use with Barge](#-Use-with-Barge)
|
||||||
- [👩🔬 Testing](#-testing)
|
- [⛵️ Environment Variables](#️-Environment-Variables)
|
||||||
- [✨ Code Style](#-code-style)
|
- [Client](#Client)
|
||||||
- [🛳 Production](#-production)
|
- [Server](#Server)
|
||||||
- [⬆️ Releases](#️-releases)
|
- [👩🔬 Testing](#-Testing)
|
||||||
- [🎁 Contributing](#-contributing)
|
- [Unit Tests](#Unit-Tests)
|
||||||
- [🏛 License](#-license)
|
- [End-to-End Integration Tests](#End-to-End-Integration-Tests)
|
||||||
|
- [✨ Code Style](#-Code-Style)
|
||||||
|
- [🛳 Production](#-Production)
|
||||||
|
- [⬆️ Releases](#️-Releases)
|
||||||
|
- [📜 Changelog](#-Changelog)
|
||||||
|
- [🎁 Contributing](#-Contributing)
|
||||||
|
- [🏛 License](#-License)
|
||||||
|
|
||||||
## 🏄 Get Started
|
## 🏄 Get Started
|
||||||
|
|
||||||
This repo contains a client and a server, both written in TypeScript:
|
This repo contains a client and a server, both written in TypeScript:
|
||||||
|
|
||||||
- **client**: React app setup with [squid-js](https://github.com/oceanprotocol/squid-js), bootstrapped with [Create React App](https://github.com/facebook/create-react-app)
|
- **client**: React app setup with [squid-js](https://github.com/oceanprotocol/squid-js), bootstrapped with [Create React App](https://github.com/facebook/create-react-app)
|
||||||
- **server**: Node.js app, utilizing [Express](https://expressjs.com). The server provides various microservices, like remote file checking.
|
- **server**: Node.js app, utilizing [Express](https://expressjs.com). The server provides various microservices, like remote file checking. The endpoints are documented in [server Readme](server/).
|
||||||
|
|
||||||
To spin up both, the client and the server in a watch mode for local development, execute:
|
To spin up both, the client and the server in a watch mode for local development, execute:
|
||||||
|
|
||||||
@ -48,15 +54,17 @@ npm start
|
|||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) to view the client in the browser. The page will reload if you make edits to files in either `./client` or `./server`.
|
Open [http://localhost:3000](http://localhost:3000) to view the client in the browser. The page will reload if you make edits to files in either `./client` or `./server`.
|
||||||
|
|
||||||
### 🏖 Remote Ocean: Nile
|
### 🏖 Remote Ocean: Pacific
|
||||||
|
|
||||||
To make use of all the functionality, you need to connect to the Ocean network. By default, the client will connect to Ocean components running within [Ocean's Nile test network](https://docs.oceanprotocol.com/concepts/testnets/#the-nile-testnet) remotely.
|
To make use of all the functionality, you need to connect to an Ocean network.
|
||||||
|
|
||||||
This means you need to connect with your MetaMask to the Nile network too. To do this:
|
By default, the client will connect to Ocean components running within [Ocean's Pacific network](https://docs.oceanprotocol.com/concepts/pacific-network/) remotely.
|
||||||
|
|
||||||
|
By default, the client uses a burner wallet connected to the correct network automatically. If you choose to use MetaMask, you need to connect to the Pacific network. To do this:
|
||||||
|
|
||||||
1. select Custom RPC in the network dropdown in MetaMask
|
1. select Custom RPC in the network dropdown in MetaMask
|
||||||
2. under New Network, enter `https://nile.dev-ocean.com` as the custom RPC URL
|
2. under New Network, enter `https://pacific.oceanprotocol.com` as the custom RPC URL
|
||||||
3. Hit _Save_, and you’re now connected to Nile
|
3. Hit _Save_, and you’re now connected to Pacific
|
||||||
|
|
||||||
### 🐳 Use with Barge
|
### 🐳 Use with Barge
|
||||||
|
|
||||||
@ -69,18 +77,50 @@ cd barge
|
|||||||
./start_ocean.sh --latest --no-pleuston --local-spree-node
|
./start_ocean.sh --latest --no-pleuston --local-spree-node
|
||||||
```
|
```
|
||||||
|
|
||||||
Modify `./client/src/config.ts` to use those local connections.
|
Modify `./client/src/config.ts` or set environment variables to use those local connections.
|
||||||
|
|
||||||
|
### ⛵️ Environment Variables
|
||||||
|
|
||||||
|
#### Client
|
||||||
|
|
||||||
|
The `./client/src/config.ts` file is setup to prioritize environment variables for setting each Ocean component endpoint.
|
||||||
|
|
||||||
|
By setting environment variables, you can easily switch between Ocean networks the commons client connects to, without directly modifying `./client/src/config.ts`. This is helpful e.g. for local development so you don't accidentially commit changes to the config file.
|
||||||
|
|
||||||
|
For local development, you can use a `.env.local` file. There's an example file with the most common network configurations preconfigured:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp client/.env.local.example client/.env.local
|
||||||
|
|
||||||
|
# uncomment the config you need
|
||||||
|
vi client/.env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Server
|
||||||
|
|
||||||
|
The server uses its own environment variables too:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp server/.env.example server/.env
|
||||||
|
|
||||||
|
# edit variables
|
||||||
|
vi server/.env
|
||||||
|
```
|
||||||
|
|
||||||
## 👩🔬 Testing
|
## 👩🔬 Testing
|
||||||
|
|
||||||
Test suite is setup with [Jest](https://jestjs.io) and [react-testing-library](https://github.com/kentcdodds/react-testing-library).
|
Test suite is setup with [Jest](https://jestjs.io) and [react-testing-library](https://github.com/kentcdodds/react-testing-library) for unit testing, and [Cypress](https://www.cypress.io) for integration testing.
|
||||||
|
|
||||||
To run all tests, including all linting tests:
|
To run all linting, unit and integration tests in one go, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The endpoints the integration tests run against are defined by your [Environment Variables](#️-Environment-Variables), and Cypress-specific variables in `cypress.json`.
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
For local development, you can start the test runners for client & server in a watch mode.
|
For local development, you can start the test runners for client & server in a watch mode.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -97,6 +137,22 @@ cd server/
|
|||||||
npm run test:watch
|
npm run test:watch
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### End-to-End Integration Tests
|
||||||
|
|
||||||
|
To run all integration tests in headless mode, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically spin up all required resources to run the integrations tests, and then run them.
|
||||||
|
|
||||||
|
You can also use the UI of Cypress to run and inspect the integration tests locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run cypress:open
|
||||||
|
```
|
||||||
|
|
||||||
## ✨ Code Style
|
## ✨ Code Style
|
||||||
|
|
||||||
For linting and auto-formatting you can use from the root of the project:
|
For linting and auto-formatting you can use from the root of the project:
|
||||||
@ -121,24 +177,28 @@ Builds the client for production to the `./client/build` folder, and the server
|
|||||||
|
|
||||||
## ⬆️ Releases
|
## ⬆️ Releases
|
||||||
|
|
||||||
Running any release task does the following:
|
From a clean `master` branch you can run any release task doing the following:
|
||||||
|
|
||||||
- bumps the project version
|
- bumps the project version in `package.json`, `client/package.json`, `server/package.json`
|
||||||
|
- auto-generates and updates the CHANGELOG.md file from commit messages
|
||||||
- creates a Git tag
|
- creates a Git tag
|
||||||
- updates CHANGELOG.md file with commit messages
|
|
||||||
- commits and pushes everything
|
- commits and pushes everything
|
||||||
- creates a GitHub release with commit messages as description
|
- creates a GitHub release with commit messages as description
|
||||||
|
|
||||||
You can execute the script using {major|minor|patch} as first argument to bump the version accordingly:
|
You can execute the script using {major|minor|patch} as first argument to bump the version accordingly:
|
||||||
|
|
||||||
- To bump a patch version: `npm run release`
|
- To bump a patch version: `npm run release`
|
||||||
- To bump a minor version: `npm run release-minor`
|
- To bump a minor version: `npm run release minor`
|
||||||
- To bump a major version: `npm run release-major`
|
- To bump a major version: `npm run release major`
|
||||||
|
|
||||||
By creating the Git tag with these tasks, Travis will trigger a new Kubernetes deployment automatically aftr a successful tag build.
|
By creating the Git tag with these tasks, Travis will trigger a new Kubernetes live deployment automatically, after a successful tag build.
|
||||||
|
|
||||||
For the GitHub releases steps a GitHub personal access token, exported as `GITHUB_TOKEN` is required. [Setup](https://github.com/release-it/release-it#github-releases)
|
For the GitHub releases steps a GitHub personal access token, exported as `GITHUB_TOKEN` is required. [Setup](https://github.com/release-it/release-it#github-releases)
|
||||||
|
|
||||||
|
## 📜 Changelog
|
||||||
|
|
||||||
|
See the [CHANGELOG.md](./CHANGELOG.md) file. This file is auto-generated during the above mentioned release process.
|
||||||
|
|
||||||
## 🎁 Contributing
|
## 🎁 Contributing
|
||||||
|
|
||||||
See the page titled "[Ways to Contribute](https://docs.oceanprotocol.com/concepts/contributing/)" in the Ocean Protocol documentation.
|
See the page titled "[Ways to Contribute](https://docs.oceanprotocol.com/concepts/contributing/)" in the Ocean Protocol documentation.
|
||||||
|
@ -1 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
.env.local
|
||||||
|
55
client/.env.local.example
Normal file
55
client/.env.local.example
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
#
|
||||||
|
# When none of the following variables are set,
|
||||||
|
# Commons will default connecting to Pacific
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Connect to Pacific
|
||||||
|
#
|
||||||
|
REACT_APP_NODE_URI="https://pacific.oceanprotocol.com"
|
||||||
|
REACT_APP_SECRET_STORE_URI="https://secret-store.oceanprotocol.com"
|
||||||
|
REACT_APP_FAUCET_URI="https://faucet.oceanprotocol.com"
|
||||||
|
# Pacific Test instances
|
||||||
|
REACT_APP_AQUARIUS_URI="https://aquarius.test.oceanprotocol.com"
|
||||||
|
REACT_APP_BRIZO_URI="https://brizo.test.oceanprotocol.com"
|
||||||
|
REACT_APP_BRIZO_ADDRESS="0x0474ed05ba757dde575dfaaaa267d9e7f3643abc"
|
||||||
|
# Pacific Commons instances
|
||||||
|
# REACT_APP_AQUARIUS_URI="https://aquarius.commons.oceanprotocol.com"
|
||||||
|
# REACT_APP_BRIZO_URI="https://brizo.commons.oceanprotocol.com"
|
||||||
|
# REACT_APP_BRIZO_ADDRESS="0x008c25ed3594e094db4592f4115d5fa74c4f41ea"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Connect to Nile
|
||||||
|
#
|
||||||
|
# REACT_APP_NODE_URI="https://nile.dev-ocean.com"
|
||||||
|
# REACT_APP_SECRET_STORE_URI="https://secret-store.nile.dev-ocean.com"
|
||||||
|
# REACT_APP_FAUCET_URI="https://faucet.nile.dev-ocean.com"
|
||||||
|
# REACT_APP_BRIZO_ADDRESS="0x4aaab179035dc57b35e2ce066919048686f82972"
|
||||||
|
# Nile Test instances
|
||||||
|
# REACT_APP_AQUARIUS_URI="https://aquarius.nile.dev-ocean.com"
|
||||||
|
# REACT_APP_BRIZO_URI="https://brizo.nile.dev-ocean.com"
|
||||||
|
# Nile Commons instances
|
||||||
|
# REACT_APP_AQUARIUS_URI="https://aquarius.marketplace.dev-ocean.com"
|
||||||
|
# REACT_APP_BRIZO_URI="https://brizo.marketplace.dev-ocean.com"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Connect to Duero
|
||||||
|
#
|
||||||
|
# REACT_APP_NODE_URI="https://duero.dev-ocean.com"
|
||||||
|
# REACT_APP_AQUARIUS_URI="https://aquarius.duero.dev-ocean.com"
|
||||||
|
# REACT_APP_BRIZO_URI="https://brizo.duero.dev-ocean.com"
|
||||||
|
# REACT_APP_SECRET_STORE_URI="https://secret-store.duero.dev-ocean.com"
|
||||||
|
# REACT_APP_FAUCET_URI="https://faucet.duero.dev-ocean.com"
|
||||||
|
# REACT_APP_BRIZO_ADDRESS="0x9d4ed58293f71122ad6a733c1603927a150735d0"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Connect to Spree (local with Barge)
|
||||||
|
#
|
||||||
|
# REACT_APP_NODE_URI="http://localhost:8545"
|
||||||
|
# REACT_APP_AQUARIUS_URI="http://localhost:5000"
|
||||||
|
# REACT_APP_BRIZO_URI="http://localhost:8030"
|
||||||
|
# REACT_APP_SECRET_STORE_URI="http://localhost:12001"
|
||||||
|
# REACT_APP_FAUCET_URI="http://localhost:3001"
|
||||||
|
# REACT_APP_BRIZO_ADDRESS="0x00bd138abd70e2f00903268f3db08f2d25677c9e"
|
||||||
|
|
||||||
|
REACT_APP_REPORT_EMAIL="test@example.com"
|
2
client/__mocks__/axios.js
Normal file
2
client/__mocks__/axios.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import mockAxios from 'jest-mock-axios'
|
||||||
|
export default mockAxios
|
8
client/__mocks__/market-mock.ts
Normal file
8
client/__mocks__/market-mock.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const marketMock = {
|
||||||
|
totalAssets: 1000,
|
||||||
|
categories: ['category'],
|
||||||
|
network: 'Pacific',
|
||||||
|
networkMatch: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export { marketMock }
|
66
client/__mocks__/ocean-mock.ts
Normal file
66
client/__mocks__/ocean-mock.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
const oceanMock = {
|
||||||
|
ocean: {
|
||||||
|
accounts: {
|
||||||
|
list: () => ['xxx', 'xxx']
|
||||||
|
},
|
||||||
|
aquarius: {
|
||||||
|
queryMetadata: () => {
|
||||||
|
return {
|
||||||
|
results: [],
|
||||||
|
totalResults: 1,
|
||||||
|
totalPages: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
resolve: jest.fn(),
|
||||||
|
order: () => {
|
||||||
|
return {
|
||||||
|
next: jest.fn()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
consume: jest.fn()
|
||||||
|
},
|
||||||
|
keeper: {
|
||||||
|
conditions: {
|
||||||
|
accessSecretStoreCondition: {
|
||||||
|
getGrantedDidByConsumer: () => {
|
||||||
|
return {
|
||||||
|
find: jest.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
get: jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
squid: {
|
||||||
|
name: 'Squid-js',
|
||||||
|
status: 'Working'
|
||||||
|
},
|
||||||
|
aquarius: {
|
||||||
|
name: 'Aquarius',
|
||||||
|
status: 'Working'
|
||||||
|
},
|
||||||
|
brizo: {
|
||||||
|
name: 'Brizo',
|
||||||
|
network: 'Nile',
|
||||||
|
status: 'Working',
|
||||||
|
contracts: {
|
||||||
|
hello: 'hello',
|
||||||
|
hello2: 'hello2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
ok: true,
|
||||||
|
network: true,
|
||||||
|
contracts: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default oceanMock
|
@ -1,30 +1,34 @@
|
|||||||
|
import oceanMock from './ocean-mock'
|
||||||
|
|
||||||
const userMock = {
|
const userMock = {
|
||||||
isLogged: false,
|
isLogged: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isWeb3: false,
|
isBurner: false,
|
||||||
isOceanNetwork: false,
|
isWeb3Capable: false,
|
||||||
account: '',
|
account: '',
|
||||||
web3: {},
|
web3: {},
|
||||||
ocean: {},
|
...oceanMock,
|
||||||
balance: { eth: 0, ocn: 0 },
|
balance: { eth: 0, ocn: 0 },
|
||||||
network: '',
|
network: '',
|
||||||
requestFromFaucet: jest.fn(),
|
requestFromFaucet: jest.fn(),
|
||||||
unlockAccounts: jest.fn(),
|
loginMetamask: jest.fn(),
|
||||||
|
loginBurnerWallet: jest.fn(),
|
||||||
message: ''
|
message: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMockConnected = {
|
const userMockConnected = {
|
||||||
isLogged: true,
|
isLogged: true,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isWeb3: true,
|
isBurner: false,
|
||||||
isOceanNetwork: true,
|
isWeb3Capable: true,
|
||||||
account: '0xxxxxx',
|
account: '0xxxxxx',
|
||||||
web3: {},
|
web3: {},
|
||||||
ocean: {},
|
...oceanMock,
|
||||||
balance: { eth: 0, ocn: 0 },
|
balance: { eth: 0, ocn: 0 },
|
||||||
network: '',
|
network: '',
|
||||||
requestFromFaucet: jest.fn(),
|
requestFromFaucet: jest.fn(),
|
||||||
unlockAccounts: jest.fn(),
|
loginMetamask: jest.fn(),
|
||||||
|
loginBurnerWallet: jest.fn(),
|
||||||
message: ''
|
message: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7088
client/package-lock.json
generated
7088
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "commons-client",
|
"name": "commons-client",
|
||||||
"description": "Ocean Protocol marketplace frontend to explore, download, and publish open data sets.",
|
"description": "Ocean Protocol marketplace frontend to explore, download, and publish open data sets.",
|
||||||
"version": "0.1.0",
|
"version": "0.6.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
@ -13,50 +13,57 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oceanprotocol/art": "^2.2.0",
|
"@oceanprotocol/art": "^2.2.0",
|
||||||
"@oceanprotocol/squid": "^0.5.10",
|
"@oceanprotocol/squid": "^0.6.4",
|
||||||
"@oceanprotocol/typographies": "^0.1.0",
|
"@oceanprotocol/typographies": "^0.1.0",
|
||||||
|
"@sindresorhus/slugify": "^0.9.1",
|
||||||
|
"axios": "^0.19.0",
|
||||||
|
"bip39": "^3.0.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"ethereum-blockies": "MyEtherWallet/blockies",
|
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
||||||
"filesize": "^4.1.2",
|
"filesize": "^4.1.2",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
"is-url": "^1.2.4",
|
"is-url": "^1.2.4",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"query-string": "^6.5.0",
|
"query-string": "^6.8.1",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-datepicker": "^2.5.0",
|
"react-collapsed": "^2.0.1",
|
||||||
|
"react-datepicker": "^2.7.0",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"react-dotdotdot": "^1.3.0",
|
"react-dotdotdot": "^1.3.0",
|
||||||
"react-ga": "^2.5.7",
|
"react-ga": "^2.6.0",
|
||||||
"react-helmet": "^5.2.1",
|
"react-helmet": "^5.2.1",
|
||||||
"react-markdown": "^4.0.8",
|
"react-markdown": "^4.1.0",
|
||||||
|
"react-modal": "^3.8.2",
|
||||||
"react-moment": "^0.9.2",
|
"react-moment": "^0.9.2",
|
||||||
"react-paginate": "^6.3.0",
|
"react-paginate": "^6.3.0",
|
||||||
"react-popper": "^1.3.3",
|
"react-popper": "^1.3.3",
|
||||||
"react-router-dom": "^5.0.0",
|
"react-router-dom": "^5.0.1",
|
||||||
"react-transition-group": "^4.0.0",
|
"react-transition-group": "^4.1.1",
|
||||||
"slugify": "^1.3.4",
|
"truffle-hdwallet-provider": "^1.0.13",
|
||||||
"web3": "1.0.0-beta.37"
|
"web3": "1.0.0-beta.37"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-mock/state": "^0.1.8",
|
"@react-mock/state": "^0.1.8",
|
||||||
|
"@testing-library/react": "^8.0.4",
|
||||||
"@types/classnames": "^2.2.7",
|
"@types/classnames": "^2.2.7",
|
||||||
"@types/filesize": "^4.1.0",
|
"@types/filesize": "^4.1.0",
|
||||||
"@types/is-url": "^1.2.28",
|
"@types/is-url": "^1.2.28",
|
||||||
"@types/jest": "^24.0.12",
|
"@types/jest": "^24.0.15",
|
||||||
"@types/react": "^16.8.15",
|
"@types/react": "^16.8.22",
|
||||||
"@types/react-datepicker": "^2.3.0",
|
"@types/react-datepicker": "^2.3.0",
|
||||||
"@types/react-dom": "^16.8.4",
|
"@types/react-dom": "^16.8.4",
|
||||||
"@types/react-dotdotdot": "^1.2.0",
|
"@types/react-dotdotdot": "^1.2.0",
|
||||||
"@types/react-helmet": "^5.0.8",
|
"@types/react-helmet": "^5.0.8",
|
||||||
|
"@types/react-modal": "^3.8.2",
|
||||||
"@types/react-paginate": "^6.2.1",
|
"@types/react-paginate": "^6.2.1",
|
||||||
"@types/react-router-dom": "^4.3.2",
|
"@types/react-router-dom": "^4.3.4",
|
||||||
"@types/react-transition-group": "^2.9.1",
|
"@types/react-transition-group": "^2.9.2",
|
||||||
"@types/web3": "^1.0.18",
|
"@types/web3": "^1.0.19",
|
||||||
"jest-dom": "^3.1.4",
|
"jest-dom": "^3.5.0",
|
||||||
|
"jest-mock-axios": "^3.0.0",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.12.0",
|
||||||
"react-scripts": "^3.0.0",
|
"react-scripts": "^3.0.0",
|
||||||
"react-testing-library": "^7.0.0",
|
"typescript": "3.4.5"
|
||||||
"typescript": "^3.4.5"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -71,7 +78,8 @@
|
|||||||
"jest": {
|
"jest": {
|
||||||
"collectCoverageFrom": [
|
"collectCoverageFrom": [
|
||||||
"src/**/*.{ts,tsx}",
|
"src/**/*.{ts,tsx}",
|
||||||
"!src/serviceWorker.ts"
|
"!src/serviceWorker.ts",
|
||||||
|
"!src/**/*.d.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,39 +12,6 @@
|
|||||||
|
|
||||||
<title>Commons</title>
|
<title>Commons</title>
|
||||||
|
|
||||||
<meta
|
|
||||||
content="A marketplace to find and publish open data sets in the Ocean Network."
|
|
||||||
name="description"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
content="https://commons.oceanprotocol.com/share.png"
|
|
||||||
name="image"
|
|
||||||
/>
|
|
||||||
<link href="https://commons.oceanprotocol.com" rel="canonical" />
|
|
||||||
|
|
||||||
<meta content="https://commons.oceanprotocol.com" property="og:url" />
|
|
||||||
<meta content="Commons" property="og:title" />
|
|
||||||
<meta
|
|
||||||
content="A marketplace to find and publish open data sets in the Ocean Network."
|
|
||||||
property="og:description"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
content="https://commons.oceanprotocol.com/share.png"
|
|
||||||
property="og:image"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<meta content="summary_large_image" name="twitter:card" />
|
|
||||||
<meta content="@oceanprotocol" name="twitter:creator" />
|
|
||||||
<meta content="Commons" name="twitter:title" />
|
|
||||||
<meta
|
|
||||||
content="A marketplace to find and publish open data sets in the Ocean Network."
|
|
||||||
name="twitter:description"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
content="https://commons.oceanprotocol.com/share.png"
|
|
||||||
name="twitter:image"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.loader {
|
.loader {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -21,5 +21,5 @@
|
|||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#141414",
|
"theme_color": "#141414",
|
||||||
"background_color": "#141414"
|
"background_color": "#ffffff"
|
||||||
}
|
}
|
||||||
|
2
client/public/robots.txt
Normal file
2
client/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /search
|
1
client/src/@types/react-collapsed/index.d.ts
vendored
Normal file
1
client/src/@types/react-collapsed/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'react-collapsed'
|
1
client/src/@types/truffle-hdwallet-provider'/index.d.ts
vendored
Normal file
1
client/src/@types/truffle-hdwallet-provider'/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'truffle-hdwallet-provider'
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import { User } from './context'
|
import { User } from './context'
|
||||||
import { userMock } from '../__mocks__/user-mock'
|
import { userMock, userMockConnected } from '../__mocks__/user-mock'
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
it('should be able to run tests', () => {
|
it('should be able to run tests', () => {
|
||||||
@ -10,7 +10,11 @@ describe('App', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it('renders without crashing', () => {
|
||||||
const { container } = render(<App />)
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<App />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
expect(container.firstChild).toBeInTheDocument()
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { BrowserRouter as Router } from 'react-router-dom'
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import Routes from './Routes'
|
import Routes from './Routes'
|
||||||
|
import { User } from './context'
|
||||||
|
import { userMockConnected } from '../__mocks__/user-mock'
|
||||||
|
|
||||||
describe('Routes', () => {
|
describe('Routes', () => {
|
||||||
it('renders without crashing', () => {
|
it('renders without crashing', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<Router>
|
<User.Provider value={userMockConnected}>
|
||||||
<Routes />
|
<Router>
|
||||||
</Router>
|
<Routes />
|
||||||
|
</Router>
|
||||||
|
</User.Provider>
|
||||||
)
|
)
|
||||||
expect(container.firstChild).toBeInTheDocument()
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
@ -1,28 +1,32 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Route, Switch } from 'react-router-dom'
|
import { Route, Switch } from 'react-router-dom'
|
||||||
import withTracker from './hoc/withTracker'
|
|
||||||
|
|
||||||
import About from './routes/About'
|
import About from './routes/About'
|
||||||
import Details from './routes/Details/'
|
|
||||||
import Home from './routes/Home'
|
import Home from './routes/Home'
|
||||||
import NotFound from './routes/NotFound'
|
import NotFound from './routes/NotFound'
|
||||||
import Publish from './routes/Publish/'
|
import Publish from './routes/Publish/'
|
||||||
import Search from './routes/Search'
|
import Search from './routes/Search'
|
||||||
import Faucet from './routes/Faucet'
|
import Faucet from './routes/Faucet'
|
||||||
import History from './routes/History'
|
import History from './routes/History'
|
||||||
|
import Channels from './routes/Channels'
|
||||||
import Styleguide from './routes/Styleguide'
|
import Styleguide from './routes/Styleguide'
|
||||||
|
|
||||||
|
import Asset from './components/templates/Asset'
|
||||||
|
import Channel from './components/templates/Channel'
|
||||||
|
|
||||||
const Routes = () => (
|
const Routes = () => (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact component={withTracker(Home)} path="/" />
|
<Route component={Home} exact path="/" />
|
||||||
<Route component={withTracker(Styleguide)} path="/styleguide" />
|
<Route component={Styleguide} path="/styleguide" />
|
||||||
<Route component={withTracker(About)} path="/about" />
|
<Route component={About} path="/about" />
|
||||||
<Route component={withTracker(Publish)} path="/publish" />
|
<Route component={Publish} path="/publish" />
|
||||||
<Route component={withTracker(Search)} path="/search" />
|
<Route component={Search} path="/search" />
|
||||||
<Route component={withTracker(Details)} path="/asset/:did" />
|
<Route component={Asset} path="/asset/:did" />
|
||||||
<Route component={withTracker(Faucet)} path="/faucet" />
|
<Route component={Faucet} path="/faucet" />
|
||||||
<Route component={withTracker(History)} path="/history" />
|
<Route component={History} path="/history" />
|
||||||
<Route component={withTracker(NotFound)} />
|
<Route component={Channels} exact path="/channels" />
|
||||||
|
<Route component={Channel} path="/channels/:channel" />
|
||||||
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2,18 +2,82 @@
|
|||||||
|
|
||||||
.account {
|
.account {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
> div {
|
> div:first-of-type {
|
||||||
white-space: nowrap;
|
flex: 0 0 80%;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-family: $font-family-monospace;
|
|
||||||
font-size: $font-size-small;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accountId {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-family: $font-family-monospace;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unlock {
|
||||||
|
font-size: $font-size-small !important; // stylelint-disable-line
|
||||||
|
margin-left: $spacer / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountType {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: calc(1.5rem + #{$spacer / 3});
|
||||||
|
font-size: $font-size-small;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
background: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
fill: currentColor;
|
||||||
|
margin-right: $spacer / 8;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.open {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seedphrase {
|
||||||
|
margin-top: $spacer / 2;
|
||||||
|
margin-left: calc(1.5rem + #{$spacer / 4});
|
||||||
|
margin-right: calc(1.5rem + #{$spacer / 4});
|
||||||
|
|
||||||
|
code {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: $spacer / 2 $spacer;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
background: $body-background;
|
||||||
|
border: 1px solid $brand-grey-lighter;
|
||||||
|
margin-bottom: $spacer / 4;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.seedphraseHelp {
|
||||||
|
color: $brand-grey-light;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.blockies {
|
.blockies {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
@ -21,4 +85,5 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: $spacer / 3;
|
margin-right: $spacer / 3;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
border: 1px solid $brand-grey-lighter;
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,61 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render, fireEvent } from '@testing-library/react'
|
||||||
import { toDataUrl } from 'ethereum-blockies'
|
import { toDataUrl } from 'ethereum-blockies'
|
||||||
import Account from './Account'
|
import Account from './Account'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||||
|
|
||||||
describe('Account', () => {
|
describe('Account', () => {
|
||||||
it('renders without crashing', () => {
|
it('renders without crashing', () => {
|
||||||
const { container } = render(<Account account={'0xxxxxxxxxxxxxxx'} />)
|
const { container } = render(
|
||||||
|
<User.Provider
|
||||||
|
value={{ ...userMockConnected, account: '0xxxxxxxxxxxxxxx' }}
|
||||||
|
>
|
||||||
|
<Account />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
expect(container.firstChild).toBeInTheDocument()
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('outputs empty state without account', () => {
|
it('outputs empty state without account', () => {
|
||||||
const { container } = render(<Account account={''} />)
|
const { container, getByText } = render(
|
||||||
|
<User.Provider value={{ ...userMockConnected, account: '' }}>
|
||||||
|
<Account />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
expect(container.firstChild).toHaveTextContent('No account selected')
|
expect(container.firstChild).toHaveTextContent('No account selected')
|
||||||
|
fireEvent.click(getByText('Unlock Account'))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('outputs blockie img', () => {
|
it('outputs blockie img', () => {
|
||||||
const account = '0xxxxxxxxxxxxxxx'
|
const account = '0xxxxxxxxxxxxxxx'
|
||||||
const blockies = toDataUrl(account)
|
const blockies = toDataUrl(account)
|
||||||
|
|
||||||
const { container } = render(<Account account={account} />)
|
const { container } = render(
|
||||||
|
<User.Provider value={{ ...userMockConnected, account }}>
|
||||||
|
<Account />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
expect(container.querySelector('.blockies')).toBeInTheDocument()
|
expect(container.querySelector('.blockies')).toBeInTheDocument()
|
||||||
expect(container.querySelector('.blockies')).toHaveAttribute(
|
expect(container.querySelector('.blockies')).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
blockies
|
blockies
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Account info can be toggled', () => {
|
||||||
|
const { container, getByText } = render(
|
||||||
|
<User.Provider
|
||||||
|
value={{
|
||||||
|
...userMockConnected,
|
||||||
|
isBurner: true,
|
||||||
|
account: '0xxxxxxxxxxxxxxx'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Account />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
fireEvent.click(getByText('Burner Wallet'))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,19 +1,89 @@
|
|||||||
import React from 'react'
|
import React, { PureComponent } from 'react'
|
||||||
import Dotdotdot from 'react-dotdotdot'
|
import Dotdotdot from 'react-dotdotdot'
|
||||||
import { toDataUrl } from 'ethereum-blockies'
|
import { toDataUrl } from 'ethereum-blockies'
|
||||||
import styles from './Account.module.scss'
|
import styles from './Account.module.scss'
|
||||||
|
import WalletSelector from '../organisms/WalletSelector'
|
||||||
|
import content from '../../data/web3message.json'
|
||||||
|
import { ReactComponent as Caret } from '../../img/caret.svg'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import Button from './Button'
|
||||||
|
|
||||||
const Account = ({ account }: { account: string }) => {
|
export default class Account extends PureComponent<
|
||||||
const blockies = account && toDataUrl(account)
|
{},
|
||||||
|
{ isAccountInfoOpen: boolean }
|
||||||
|
> {
|
||||||
|
public static contextType = User
|
||||||
|
|
||||||
return account && blockies ? (
|
public state = {
|
||||||
<div className={styles.account}>
|
isAccountInfoOpen: false
|
||||||
<img className={styles.blockies} src={blockies} alt="Blockies" />
|
}
|
||||||
<Dotdotdot clamp={1}>{account}</Dotdotdot>
|
|
||||||
</div>
|
private toggleAccountInfo() {
|
||||||
) : (
|
this.setState({ isAccountInfoOpen: !this.state.isAccountInfoOpen })
|
||||||
<em>No account selected</em>
|
}
|
||||||
)
|
|
||||||
|
public render() {
|
||||||
|
const { account, isBurner, loginMetamask, isWeb3Capable } = this.context
|
||||||
|
const { isAccountInfoOpen } = this.state
|
||||||
|
const seedphrase = localStorage.getItem('seedphrase') as string
|
||||||
|
const blockies = account && toDataUrl(account)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.account}>
|
||||||
|
{account ? (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
className={styles.blockies}
|
||||||
|
src={blockies}
|
||||||
|
alt="Blockies"
|
||||||
|
/>
|
||||||
|
<Dotdotdot className={styles.accountId} clamp={2}>
|
||||||
|
{account}
|
||||||
|
</Dotdotdot>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className={styles.blockies} />
|
||||||
|
<em className={styles.noAccount}>
|
||||||
|
No account selected
|
||||||
|
</em>
|
||||||
|
<Button
|
||||||
|
link
|
||||||
|
className={styles.unlock}
|
||||||
|
onClick={() => loginMetamask()}
|
||||||
|
>
|
||||||
|
Unlock Account
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.accountType}>
|
||||||
|
{isBurner ? (
|
||||||
|
<button
|
||||||
|
className={styles.toggle}
|
||||||
|
onClick={() => this.toggleAccountInfo()}
|
||||||
|
title="Show More Account Info"
|
||||||
|
>
|
||||||
|
<Caret
|
||||||
|
className={isAccountInfoOpen ? styles.open : ''}
|
||||||
|
/>{' '}
|
||||||
|
Burner Wallet
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
'MetaMask'
|
||||||
|
)}
|
||||||
|
{isWeb3Capable && <WalletSelector />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isBurner && isAccountInfoOpen && (
|
||||||
|
<div className={styles.seedphrase}>
|
||||||
|
<code>{seedphrase}</code>
|
||||||
|
<p className={styles.seedphraseHelp}>
|
||||||
|
{content.seedphrase}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Account
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import { BrowserRouter as Router } from 'react-router-dom'
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ interface ButtonProps {
|
|||||||
onClick?: any
|
onClick?: any
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
to?: string
|
to?: string
|
||||||
|
name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Button extends PureComponent<ButtonProps, any> {
|
export default class Button extends PureComponent<ButtonProps, any> {
|
||||||
|
@ -2,8 +2,24 @@
|
|||||||
|
|
||||||
.categoryImage {
|
.categoryImage {
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
background-size: cover;
|
background-size: 100%;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
margin-bottom: $spacer / $line-height;
|
margin-bottom: $spacer / $line-height;
|
||||||
background-color: $body-background;
|
background-color: $body-background;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: .85;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
border: 1px solid $brand-grey-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
composes: categoryImage;
|
||||||
|
height: 8rem;
|
||||||
|
margin-top: $spacer / $line-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dimmed {
|
||||||
|
composes: categoryImage;
|
||||||
|
opacity: .6;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import CategoryImage from './CategoryImage'
|
import CategoryImage from './CategoryImage'
|
||||||
import formPublish from '../../data/form-publish.json'
|
import formPublish from '../../data/form-publish.json'
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react'
|
import React, { PureComponent } from 'react'
|
||||||
|
import cx from 'classnames'
|
||||||
import styles from './CategoryImage.module.scss'
|
import styles from './CategoryImage.module.scss'
|
||||||
|
|
||||||
import agriculture from '../../img/categories/agriculture.jpg'
|
import agriculture from '../../img/categories/agriculture.jpg'
|
||||||
@ -33,6 +34,7 @@ import theology from '../../img/categories/theology.jpg'
|
|||||||
import transport from '../../img/categories/transport.jpg'
|
import transport from '../../img/categories/transport.jpg'
|
||||||
import urbanplanning from '../../img/categories/urbanplanning.jpg'
|
import urbanplanning from '../../img/categories/urbanplanning.jpg'
|
||||||
import visualart from '../../img/categories/visualart.jpg'
|
import visualart from '../../img/categories/visualart.jpg'
|
||||||
|
import aiforgood from '../../img/aiforgood.jpg'
|
||||||
import fallback from '@oceanprotocol/art/jellyfish/jellyfish-back.svg'
|
import fallback from '@oceanprotocol/art/jellyfish/jellyfish-back.svg'
|
||||||
|
|
||||||
const categoryImageFile = (category: string) => {
|
const categoryImageFile = (category: string) => {
|
||||||
@ -95,6 +97,8 @@ const categoryImageFile = (category: string) => {
|
|||||||
case 'mathematics':
|
case 'mathematics':
|
||||||
return mathematics
|
return mathematics
|
||||||
case 'Medicine':
|
case 'Medicine':
|
||||||
|
case 'Health & Medicine':
|
||||||
|
case 'Health':
|
||||||
case 'medicine':
|
case 'medicine':
|
||||||
return medicine
|
return medicine
|
||||||
case 'Other':
|
case 'Other':
|
||||||
@ -133,23 +137,31 @@ const categoryImageFile = (category: string) => {
|
|||||||
case 'Visual Arts & Design':
|
case 'Visual Arts & Design':
|
||||||
case 'visualart':
|
case 'visualart':
|
||||||
return visualart
|
return visualart
|
||||||
|
// technically no category
|
||||||
|
// but corresponding to title of a channel
|
||||||
case 'AI For Good':
|
case 'AI For Good':
|
||||||
return dataofdata
|
return aiforgood
|
||||||
default:
|
default:
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CategoryImage extends PureComponent<{ category: string }> {
|
export default class CategoryImage extends PureComponent<{
|
||||||
|
category: string
|
||||||
|
header?: boolean
|
||||||
|
dimmed?: boolean
|
||||||
|
}> {
|
||||||
public render() {
|
public render() {
|
||||||
const image = categoryImageFile(this.props.category)
|
const image = categoryImageFile(this.props.category)
|
||||||
|
const classNames = cx(styles.categoryImage, {
|
||||||
|
[styles.header]: this.props.header,
|
||||||
|
[styles.dimmed]: this.props.dimmed
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.categoryImage}
|
className={classNames}
|
||||||
style={{
|
style={{ backgroundImage: `url(${image})` }}
|
||||||
backgroundImage: `url(${image})`
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
23
client/src/components/atoms/CategoryLink.tsx
Normal file
23
client/src/components/atoms/CategoryLink.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
const CategoryLink = ({
|
||||||
|
category,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
category: string
|
||||||
|
children?: any
|
||||||
|
className?: string
|
||||||
|
}) => (
|
||||||
|
<Link
|
||||||
|
to={`/search?categories=${encodeURIComponent(category)}`}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children || category}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default CategoryLink
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import Form from './Form'
|
import Form from './Form'
|
||||||
|
|
||||||
describe('Form', () => {
|
describe('Form', () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import Input from './Input'
|
import Input from './Input'
|
||||||
|
|
||||||
describe('Input', () => {
|
describe('Input', () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent, FormEvent, ChangeEvent } from 'react'
|
import React, { PureComponent, FormEvent, ChangeEvent } from 'react'
|
||||||
import slugify from 'slugify'
|
import slugify from '@sindresorhus/slugify'
|
||||||
import DatePicker from 'react-datepicker'
|
import DatePicker from 'react-datepicker'
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import { ReactComponent as SearchIcon } from '../../../img/search.svg'
|
import { ReactComponent as SearchIcon } from '../../../img/search.svg'
|
||||||
@ -136,21 +136,18 @@ export default class Input extends PureComponent<InputProps, InputState> {
|
|||||||
<div className={styles.radioWrap} key={index}>
|
<div className={styles.radioWrap} key={index}>
|
||||||
<input
|
<input
|
||||||
className={styles.radio}
|
className={styles.radio}
|
||||||
id={slugify(option, {
|
id={slugify(option)}
|
||||||
lower: true
|
|
||||||
})}
|
|
||||||
type={type}
|
type={type}
|
||||||
name={name}
|
name={name}
|
||||||
value={slugify(option, {
|
// value={slugify(option, {
|
||||||
lower: true
|
// lower: true
|
||||||
})}
|
// })}
|
||||||
disabled={disabled}
|
// disabled={disabled}
|
||||||
|
value={slugify(option)}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className={styles.radioLabel}
|
className={styles.radioLabel}
|
||||||
htmlFor={slugify(option, {
|
htmlFor={slugify(option)}
|
||||||
lower: true
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{option}
|
{option}
|
||||||
</label>
|
</label>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import InputGroup from './InputGroup'
|
import InputGroup from './InputGroup'
|
||||||
|
|
||||||
describe('InputGroup', () => {
|
describe('InputGroup', () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import Label from './Label'
|
import Label from './Label'
|
||||||
|
|
||||||
describe('Label', () => {
|
describe('Label', () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import Row from './Row'
|
import Row from './Row'
|
||||||
|
|
||||||
describe('Row', () => {
|
describe('Row', () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import Markdown from './Markdown'
|
import Markdown from './Markdown'
|
||||||
|
|
||||||
describe('Markdown', () => {
|
describe('Markdown', () => {
|
||||||
|
93
client/src/components/atoms/Modal.module.scss
Normal file
93
client/src/components/atoms/Modal.module.scss
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
// prevent background scrolling
|
||||||
|
:global(.ReactModal__Body--open) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalOverlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba($brand-black, .7);
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
animation: fadeIn .2s ease-out backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
padding: $spacer;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
background: $body-background;
|
||||||
|
margin: $spacer auto;
|
||||||
|
max-width: $break-point--small;
|
||||||
|
position: relative;
|
||||||
|
animation: moveUp .2s ease-out backwards;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
padding: $spacer * 2 $spacer * 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: $font-size-h3;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
font-size: $font-size-h2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 0;
|
||||||
|
margin-top: $spacer / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: 0;
|
||||||
|
top: $spacer / 4;
|
||||||
|
right: $spacer / 2;
|
||||||
|
font-size: $font-size-h2;
|
||||||
|
color: $brand-grey;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveUp {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, 1rem, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
17
client/src/components/atoms/Modal.test.tsx
Normal file
17
client/src/components/atoms/Modal.test.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import Modal from './Modal'
|
||||||
|
import ReactModal from 'react-modal'
|
||||||
|
|
||||||
|
describe('Modal', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
ReactModal.setAppElement(document.createElement('div'))
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Modal title="Hello" isOpen toggleModal={() => null}>
|
||||||
|
Hello
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
expect(document.querySelector('.ReactModalPortal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
55
client/src/components/atoms/Modal.tsx
Normal file
55
client/src/components/atoms/Modal.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactModal from 'react-modal'
|
||||||
|
import styles from './Modal.module.scss'
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'test') ReactModal.setAppElement('#root')
|
||||||
|
|
||||||
|
const Modal = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
isOpen,
|
||||||
|
toggleModal,
|
||||||
|
children,
|
||||||
|
onAfterOpen,
|
||||||
|
onRequestClose,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
isOpen: boolean
|
||||||
|
toggleModal: () => void
|
||||||
|
children: any
|
||||||
|
onAfterOpen?: () => void
|
||||||
|
onRequestClose?: () => void
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ReactModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onAfterOpen={onAfterOpen}
|
||||||
|
onRequestClose={onRequestClose}
|
||||||
|
contentLabel={title}
|
||||||
|
className={styles.modal}
|
||||||
|
overlayClassName={styles.modalOverlay}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={styles.close}
|
||||||
|
onClick={toggleModal}
|
||||||
|
data-testid="closeModal"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<header className={styles.header}>
|
||||||
|
<h2 className={styles.title}>{title}</h2>
|
||||||
|
{description && (
|
||||||
|
<p className={styles.description}>{description}</p>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</ReactModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal
|
71
client/src/components/atoms/Seo.tsx
Normal file
71
client/src/components/atoms/Seo.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Helmet from 'react-helmet'
|
||||||
|
import { withRouter, RouteComponentProps } from 'react-router-dom'
|
||||||
|
import meta from '../../data/meta.json'
|
||||||
|
import imageDefault from '../../img/share.png'
|
||||||
|
|
||||||
|
const MetaTags = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
url,
|
||||||
|
image
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
url: string
|
||||||
|
image: string
|
||||||
|
}) => (
|
||||||
|
<Helmet defaultTitle={meta.title} titleTemplate={`%s - ${meta.title}`}>
|
||||||
|
<html lang="en" />
|
||||||
|
|
||||||
|
{title && <title>{title}</title>}
|
||||||
|
|
||||||
|
{/* General tags */}
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<meta name="image" content={image} />
|
||||||
|
<link rel="canonical" href={url} />
|
||||||
|
|
||||||
|
{/* OpenGraph tags */}
|
||||||
|
<meta property="og:url" content={url} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:image" content={image} />
|
||||||
|
|
||||||
|
{/* Twitter Card tags */}
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:creator" content="@oceanprotocol" />
|
||||||
|
<meta name="twitter:title" content={title} />
|
||||||
|
<meta name="twitter:description" content={description} />
|
||||||
|
<meta name="twitter:image" content={image} />
|
||||||
|
|
||||||
|
{/* Prevent search engine indexing except for live */}
|
||||||
|
{window.location.hostname !== 'commons.oceanprotocol.com' && (
|
||||||
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
|
)}
|
||||||
|
</Helmet>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SeoProps extends RouteComponentProps {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
shareImage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Seo = ({ title, description, shareImage, location }: SeoProps) => {
|
||||||
|
title = title || meta.title
|
||||||
|
description = description || meta.description
|
||||||
|
shareImage = shareImage || meta.url + imageDefault
|
||||||
|
|
||||||
|
const url = meta.url + location.pathname + location.search
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MetaTags
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
url={url}
|
||||||
|
image={shareImage}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(Seo)
|
@ -5,6 +5,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: $spacer * $line-height;
|
margin-top: $spacer * $line-height;
|
||||||
margin-bottom: $spacer / 2;
|
margin-bottom: $spacer / 2;
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: '';
|
||||||
@ -25,6 +26,21 @@
|
|||||||
|
|
||||||
.spinnerMessage {
|
.spinnerMessage {
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
|
padding-top: $spacer / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
composes: spinner;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
width: $font-size-small;
|
||||||
|
height: $font-size-small;
|
||||||
|
margin-top: -($font-size-small);
|
||||||
|
margin-left: -($font-size-small / 2);
|
||||||
|
border-width: .1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spinner {
|
@keyframes spinner {
|
||||||
|
@ -1,10 +1,27 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styles from './Spinner.module.scss'
|
import styles from './Spinner.module.scss'
|
||||||
|
|
||||||
const Spinner = ({ message }: { message?: string }) => (
|
const Spinner = ({
|
||||||
<div className={styles.spinner}>
|
message,
|
||||||
{message && <div className={styles.spinnerMessage}>{message}</div>}
|
small,
|
||||||
</div>
|
className
|
||||||
)
|
}: {
|
||||||
|
message?: string
|
||||||
|
small?: boolean
|
||||||
|
className?: string
|
||||||
|
}) => {
|
||||||
|
const classes = className || (small ? styles.small : styles.spinner)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes}>
|
||||||
|
{message && (
|
||||||
|
<div
|
||||||
|
className={styles.spinnerMessage}
|
||||||
|
dangerouslySetInnerHTML={{ __html: message }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default Spinner
|
export default Spinner
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import { User } from '../../../context'
|
import { User, Market } from '../../../context'
|
||||||
import styles from './Indicator.module.scss'
|
import styles from './Indicator.module.scss'
|
||||||
|
|
||||||
const Indicator = ({
|
const Indicator = ({
|
||||||
@ -19,15 +19,19 @@ const Indicator = ({
|
|||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
>
|
>
|
||||||
<User.Consumer>
|
<User.Consumer>
|
||||||
{states =>
|
{user => (
|
||||||
!states.isWeb3 ? (
|
<Market.Consumer>
|
||||||
<span className={styles.statusIndicator} />
|
{market =>
|
||||||
) : !states.isLogged || !states.isOceanNetwork ? (
|
!user.isLogged || !market.networkMatch ? (
|
||||||
<span className={styles.statusIndicatorCloseEnough} />
|
<span
|
||||||
) : states.isLogged ? (
|
className={styles.statusIndicatorCloseEnough}
|
||||||
<span className={styles.statusIndicatorActive} />
|
/>
|
||||||
) : null
|
) : user.isLogged ? (
|
||||||
}
|
<span className={styles.statusIndicatorActive} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</Market.Consumer>
|
||||||
|
)}
|
||||||
</User.Consumer>
|
</User.Consumer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -40,9 +40,15 @@ $popoverWidth: 18rem;
|
|||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* stylelint-disable */
|
||||||
button {
|
button {
|
||||||
font-size: $font-size-small;
|
svg,
|
||||||
|
&[data-action] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* stylelint-enable */
|
||||||
}
|
}
|
||||||
|
|
||||||
.balance {
|
.balance {
|
||||||
@ -50,6 +56,10 @@ $popoverWidth: 18rem;
|
|||||||
margin-left: $spacer / 2;
|
margin-left: $spacer / 2;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: $brand-grey-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import Popover from './Popover'
|
import Popover from './Popover'
|
||||||
import { userMock, userMockConnected } from '../../../../__mocks__/user-mock'
|
import { userMock, userMockConnected } from '../../../../__mocks__/user-mock'
|
||||||
import { User } from '../../../context'
|
import { marketMock } from '../../../../__mocks__/market-mock'
|
||||||
|
import { User, Market } from '../../../context'
|
||||||
|
|
||||||
describe('Popover', () => {
|
describe('Popover', () => {
|
||||||
it('renders without crashing', () => {
|
it('renders without crashing', () => {
|
||||||
@ -25,12 +26,14 @@ describe('Popover', () => {
|
|||||||
|
|
||||||
it('renders correct network', () => {
|
it('renders correct network', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<User.Provider value={{ ...userMockConnected, network: 'Nile' }}>
|
<User.Provider value={{ ...userMockConnected, network: 'Pacific' }}>
|
||||||
<Popover forwardedRef={() => null} style={{}} />
|
<Market.Provider value={{ ...marketMock }}>
|
||||||
|
<Popover forwardedRef={() => null} style={{}} />
|
||||||
|
</Market.Provider>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
)
|
)
|
||||||
expect(container.firstChild).toBeInTheDocument()
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
expect(container.firstChild).toHaveTextContent('Connected to Nile')
|
expect(container.firstChild).toHaveTextContent('Connected to Pacific')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders with wrong network', () => {
|
it('renders with wrong network', () => {
|
||||||
@ -38,7 +41,6 @@ describe('Popover', () => {
|
|||||||
<User.Provider
|
<User.Provider
|
||||||
value={{
|
value={{
|
||||||
...userMockConnected,
|
...userMockConnected,
|
||||||
isOceanNetwork: false,
|
|
||||||
network: '1'
|
network: '1'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,20 +1,16 @@
|
|||||||
import React, { PureComponent } from 'react'
|
import React, { PureComponent } from 'react'
|
||||||
import Account from '../../atoms/Account'
|
import Account from '../../atoms/Account'
|
||||||
import { User } from '../../../context'
|
import { User, Market } from '../../../context'
|
||||||
import styles from './Popover.module.scss'
|
import styles from './Popover.module.scss'
|
||||||
|
|
||||||
export default class Popover extends PureComponent<{
|
export default class Popover extends PureComponent<{
|
||||||
forwardedRef: (ref: HTMLElement | null) => void
|
forwardedRef?: (ref: HTMLElement | null) => void
|
||||||
style: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
}> {
|
}> {
|
||||||
|
public static contextType = User
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const { account, balance, network } = this.context
|
||||||
account,
|
|
||||||
balance,
|
|
||||||
network,
|
|
||||||
isWeb3,
|
|
||||||
isOceanNetwork
|
|
||||||
} = this.context
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -22,15 +18,10 @@ export default class Popover extends PureComponent<{
|
|||||||
ref={this.props.forwardedRef}
|
ref={this.props.forwardedRef}
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
>
|
>
|
||||||
{!isWeb3 ? (
|
{
|
||||||
<div className={styles.popoverInfoline}>
|
|
||||||
No Web3 detected. Use a browser with MetaMask installed
|
|
||||||
to publish assets.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<div className={styles.popoverInfoline}>
|
<div className={styles.popoverInfoline}>
|
||||||
<Account account={account} />
|
<Account />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{account && balance && (
|
{account && balance && (
|
||||||
@ -52,16 +43,28 @@ export default class Popover extends PureComponent<{
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.popoverInfoline}>
|
<Market.Consumer>
|
||||||
{network && !isOceanNetwork
|
{market => (
|
||||||
? 'Please connect to Custom RPC\n https://nile.dev-ocean.com'
|
<div className={styles.popoverInfoline}>
|
||||||
: network && `Connected to ${network} network`}
|
{network && !market.networkMatch
|
||||||
</div>
|
? `Please connect to Custom RPC
|
||||||
|
${
|
||||||
|
market.network === 'Pacific'
|
||||||
|
? 'https://pacific.oceanprotocol.com'
|
||||||
|
: market.network === 'Nile'
|
||||||
|
? 'https://nile.dev-ocean.com'
|
||||||
|
: market.network === 'Duero'
|
||||||
|
? 'https://duero.dev-ocean.com'
|
||||||
|
: 'http://localhost:8545'
|
||||||
|
}`
|
||||||
|
: network &&
|
||||||
|
`Connected to ${network} network`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Market.Consumer>
|
||||||
</>
|
</>
|
||||||
)}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Popover.contextType = User
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render, fireEvent } from 'react-testing-library'
|
import { render, fireEvent } from '@testing-library/react'
|
||||||
import AccountStatus from '.'
|
import AccountStatus from '.'
|
||||||
|
|
||||||
describe('AccountStatus', () => {
|
describe('AccountStatus', () => {
|
||||||
@ -10,9 +10,7 @@ describe('AccountStatus', () => {
|
|||||||
|
|
||||||
it('togglePopover fires', () => {
|
it('togglePopover fires', () => {
|
||||||
const { container } = render(<AccountStatus />)
|
const { container } = render(<AccountStatus />)
|
||||||
|
const indicator = container.querySelector('.status')
|
||||||
const indicator = container.querySelector('.statusIndicator')
|
|
||||||
|
|
||||||
indicator && fireEvent.mouseOver(indicator)
|
indicator && fireEvent.mouseOver(indicator)
|
||||||
expect(container.querySelector('.popover')).toBeInTheDocument()
|
expect(container.querySelector('.popover')).toBeInTheDocument()
|
||||||
indicator && fireEvent.mouseOut(indicator)
|
indicator && fireEvent.mouseOut(indicator)
|
||||||
|
@ -20,6 +20,12 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
border-color: $brand-pink;
|
border-color: $brand-pink;
|
||||||
transform: none;
|
transform: none;
|
||||||
|
|
||||||
|
// category image
|
||||||
|
> div:first-child {
|
||||||
|
opacity: 1;
|
||||||
|
background-size: 105%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,6 +35,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.minimal {
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.assetList {
|
.assetList {
|
||||||
> a {
|
> a {
|
||||||
color: $brand-grey-dark;
|
color: $brand-grey-dark;
|
@ -2,10 +2,19 @@ import React from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import Dotdotdot from 'react-dotdotdot'
|
import Dotdotdot from 'react-dotdotdot'
|
||||||
import styles from './Asset.module.scss'
|
import cx from 'classnames'
|
||||||
|
import styles from './AssetTeaser.module.scss'
|
||||||
import CategoryImage from '../atoms/CategoryImage'
|
import CategoryImage from '../atoms/CategoryImage'
|
||||||
|
|
||||||
const AssetLink = ({ asset, list }: { asset: any; list?: boolean }) => {
|
const AssetTeaser = ({
|
||||||
|
asset,
|
||||||
|
list,
|
||||||
|
minimal
|
||||||
|
}: {
|
||||||
|
asset: any
|
||||||
|
list?: boolean
|
||||||
|
minimal?: boolean
|
||||||
|
}) => {
|
||||||
const { metadata } = asset.findServiceByType('Metadata')
|
const { metadata } = asset.findServiceByType('Metadata')
|
||||||
const { base } = metadata
|
const { base } = metadata
|
||||||
|
|
||||||
@ -22,17 +31,22 @@ const AssetLink = ({ asset, list }: { asset: any; list?: boolean }) => {
|
|||||||
</Link>
|
</Link>
|
||||||
</article>
|
</article>
|
||||||
) : (
|
) : (
|
||||||
<article className={styles.asset}>
|
<article
|
||||||
|
className={
|
||||||
|
minimal ? cx(styles.asset, styles.minimal) : styles.asset
|
||||||
|
}
|
||||||
|
>
|
||||||
<Link to={`/asset/${asset.id}`}>
|
<Link to={`/asset/${asset.id}`}>
|
||||||
{base.categories && (
|
{base.categories && !minimal && (
|
||||||
<CategoryImage category={base.categories[0]} />
|
<CategoryImage dimmed category={base.categories[0]} />
|
||||||
)}
|
)}
|
||||||
<h1>{base.name}</h1>
|
<h1>{base.name}</h1>
|
||||||
|
|
||||||
<div className={styles.description}>
|
{!minimal && (
|
||||||
<Dotdotdot clamp={3}>{base.description}</Dotdotdot>
|
<div className={styles.description}>
|
||||||
</div>
|
<Dotdotdot clamp={3}>{base.description}</Dotdotdot>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<footer className={styles.assetFooter}>
|
<footer className={styles.assetFooter}>
|
||||||
{base.categories && <div>{base.categories[0]}</div>}
|
{base.categories && <div>{base.categories[0]}</div>}
|
||||||
</footer>
|
</footer>
|
||||||
@ -41,4 +55,4 @@ const AssetLink = ({ asset, list }: { asset: any; list?: boolean }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AssetLink
|
export default AssetTeaser
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import Pagination from './Pagination'
|
import Pagination from './Pagination'
|
||||||
|
|
||||||
describe('Pagination', () => {
|
describe('Pagination', () => {
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
composes: spinner, small from '../../atoms/Spinner.module.scss';
|
||||||
|
margin-right: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit {
|
||||||
|
margin-left: $spacer / 8;
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: $brand-grey-light;
|
||||||
|
font-size: $font-size-mini;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.network {
|
||||||
|
color: $brand-grey-light;
|
||||||
|
text-transform: capitalize;
|
||||||
|
margin-left: $spacer / 8;
|
||||||
|
font-size: $font-size-mini;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import VersionNumber from './VersionNumber'
|
||||||
|
|
||||||
|
describe('VersionNumber', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(<VersionNumber name="Commons" />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with all props set', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<VersionNumber
|
||||||
|
name="Commons"
|
||||||
|
version="6.6.6"
|
||||||
|
network="Nile"
|
||||||
|
commit="xxxxxxxxxxx"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
expect(container.firstChild).toHaveTextContent('6.6.6')
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { OceanPlatformTechStatus } from '@oceanprotocol/squid'
|
||||||
|
import slugify from '@sindresorhus/slugify'
|
||||||
|
import Spinner from '../../atoms/Spinner'
|
||||||
|
import styles from './VersionNumber.module.scss'
|
||||||
|
|
||||||
|
const VersionNumber = ({
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
network,
|
||||||
|
status,
|
||||||
|
commit
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
version?: string
|
||||||
|
network?: string
|
||||||
|
status?: OceanPlatformTechStatus
|
||||||
|
commit?: string
|
||||||
|
}) =>
|
||||||
|
version ? (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href={`https://github.com/oceanprotocol/${slugify(
|
||||||
|
name
|
||||||
|
)}/releases/tag/v${version}`}
|
||||||
|
title="Go to release on GitHub"
|
||||||
|
>
|
||||||
|
<code>v{version}</code>
|
||||||
|
</a>
|
||||||
|
{commit && (
|
||||||
|
<a
|
||||||
|
href={`https://github.com/oceanprotocol/${slugify(
|
||||||
|
name
|
||||||
|
)}/commit/${commit}`}
|
||||||
|
className={styles.commit}
|
||||||
|
title={`Go to commit ${commit} on GitHub`}
|
||||||
|
>
|
||||||
|
<code>{commit.substring(0, 7)}</code>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{network && <span className={styles.network}>{` ${network}`}</span>}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
{status === OceanPlatformTechStatus.Loading ? (
|
||||||
|
<Spinner className={styles.spinner} small />
|
||||||
|
) : (
|
||||||
|
status || 'Could not get version'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default VersionNumber
|
@ -0,0 +1,37 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.status {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: $spacer / 2;
|
||||||
|
padding-bottom: $spacer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: $spacer / 1.5;
|
||||||
|
margin-right: $spacer / 1.5;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator,
|
||||||
|
.indicatorActive {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: $spacer / 4;
|
||||||
|
margin-bottom: -.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
composes: statusIndicator from '../AccountStatus/Indicator.module.scss';
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicatorActive {
|
||||||
|
composes: statusIndicatorActive from '../AccountStatus/Indicator.module.scss';
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicatorLabel {
|
||||||
|
font-family: $font-family-title;
|
||||||
|
color: $brand-grey;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import VersionStatus from './VersionStatus'
|
||||||
|
|
||||||
|
describe('VersionStatus', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<VersionStatus
|
||||||
|
status={{ ok: false, contracts: false, network: false }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders true states', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<VersionStatus
|
||||||
|
status={{ ok: true, contracts: false, network: false }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from './VersionStatus.module.scss'
|
||||||
|
|
||||||
|
const statusInfo: { [key: string]: string } = {
|
||||||
|
ok: 'Shows if connection to all component endpoints can be established.',
|
||||||
|
network: 'Shows if all components are on the same network.',
|
||||||
|
contracts: 'Shows if contracts loaded by components are the same version.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const VersionStatus = ({
|
||||||
|
status
|
||||||
|
}: {
|
||||||
|
status: { ok: boolean; network: boolean; contracts: boolean }
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.status}>
|
||||||
|
{Object.entries(status).map(([key, value]) => (
|
||||||
|
<div
|
||||||
|
className={styles.element}
|
||||||
|
key={key}
|
||||||
|
title={statusInfo[key]}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
value === true
|
||||||
|
? styles.indicatorActive
|
||||||
|
: styles.indicator
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
<span className={styles.indicatorLabel}>
|
||||||
|
{key === 'ok' ? 'components' : key}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VersionStatus
|
@ -0,0 +1,57 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.tableWrap {
|
||||||
|
// make 'em scrollable
|
||||||
|
overflow: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
border-top: 1px solid $brand-grey-lighter;
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin-left: $spacer;
|
||||||
|
width: calc(100% - #{$spacer});
|
||||||
|
margin-bottom: -1px;
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: $spacer / 6 $spacer / 2;
|
||||||
|
|
||||||
|
// stylelint-disable-next-line selector-max-compound-selectors
|
||||||
|
&,
|
||||||
|
code {
|
||||||
|
font-size: $font-size-mini;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: $spacer / 4 $spacer / 2 $spacer / 4 $spacer * 1.3;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stylelint-disable-next-line selector-no-qualifying-type
|
||||||
|
&[colspan] {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $brand-grey;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
&,
|
||||||
|
code {
|
||||||
|
color: $brand-pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
min-width: 15rem;
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { VersionTableContracts } from './VersionTable'
|
||||||
|
|
||||||
|
describe('VersionTableContracts', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<VersionTableContracts
|
||||||
|
contracts={{ hello: 'hello', hello2: 'hello2' }}
|
||||||
|
network="nile"
|
||||||
|
keeperVersion="6.6.6"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correct Submarine links', () => {
|
||||||
|
const { container, rerender } = render(
|
||||||
|
<VersionTableContracts
|
||||||
|
contracts={{ hello: 'hello', hello2: 'hello2' }}
|
||||||
|
network="duero"
|
||||||
|
keeperVersion="6.6.6"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('tr:last-child a').href).toMatch(
|
||||||
|
/submarine.duero.dev-ocean/
|
||||||
|
)
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<VersionTableContracts
|
||||||
|
contracts={{ hello: 'hello', hello2: 'hello2' }}
|
||||||
|
network="nile"
|
||||||
|
keeperVersion="6.6.6"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('tr:last-child a').href).toMatch(
|
||||||
|
/submarine.nile.dev-ocean/
|
||||||
|
)
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<VersionTableContracts
|
||||||
|
contracts={{ hello: 'hello', hello2: 'hello2' }}
|
||||||
|
network="pacific"
|
||||||
|
keeperVersion="6.6.6"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('tr:last-child a').href).toMatch(
|
||||||
|
/submarine.oceanprotocol/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
112
client/src/components/molecules/VersionNumbers/VersionTable.tsx
Normal file
112
client/src/components/molecules/VersionNumbers/VersionTable.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { VersionNumbersState } from '.'
|
||||||
|
import VersionTableRow from './VersionTableRow'
|
||||||
|
import styles from './VersionTable.module.scss'
|
||||||
|
import VersionNumber from './VersionNumber'
|
||||||
|
|
||||||
|
import {
|
||||||
|
serviceUri,
|
||||||
|
nodeUri,
|
||||||
|
aquariusUri,
|
||||||
|
brizoUri,
|
||||||
|
brizoAddress,
|
||||||
|
secretStoreUri,
|
||||||
|
faucetUri
|
||||||
|
} from '../../../config'
|
||||||
|
|
||||||
|
const commonsConfig = {
|
||||||
|
serviceUri,
|
||||||
|
nodeUri,
|
||||||
|
aquariusUri,
|
||||||
|
brizoUri,
|
||||||
|
brizoAddress,
|
||||||
|
secretStoreUri,
|
||||||
|
faucetUri
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VersionTableContracts = ({
|
||||||
|
contracts,
|
||||||
|
network,
|
||||||
|
keeperVersion
|
||||||
|
}: {
|
||||||
|
contracts: {
|
||||||
|
[contractName: string]: string
|
||||||
|
}
|
||||||
|
network: string
|
||||||
|
keeperVersion?: string
|
||||||
|
}) => (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Keeper Contracts</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<VersionNumber
|
||||||
|
name={'Keeper Contracts'}
|
||||||
|
version={keeperVersion}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{contracts &&
|
||||||
|
Object.keys(contracts)
|
||||||
|
// sort alphabetically
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map(key => {
|
||||||
|
const submarineLink = `https://submarine.${
|
||||||
|
network === 'pacific'
|
||||||
|
? 'oceanprotocol'
|
||||||
|
: `${network}.dev-ocean`
|
||||||
|
}.com/address/${contracts[key]}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={key}>
|
||||||
|
<td>
|
||||||
|
<code className={styles.label}>{key}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href={submarineLink}>
|
||||||
|
<code>{contracts[key]}</code>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const VersionTableCommons = () => (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(commonsConfig).map(([key, value]) => (
|
||||||
|
<tr key={key}>
|
||||||
|
<td>
|
||||||
|
<code className={styles.label}>{key}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>{value}</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
|
||||||
|
const VersionTable = ({ data }: { data: VersionNumbersState }) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.tableWrap}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(data)
|
||||||
|
.filter(([key]) => key !== 'status')
|
||||||
|
.map(([key, value]) => (
|
||||||
|
<VersionTableRow key={key} value={value} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VersionTable
|
@ -0,0 +1,23 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
display: inline-block;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: -1rem;
|
||||||
|
margin-top: -.1rem;
|
||||||
|
padding-right: .5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: $brand-grey-light;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.open {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import useCollapse from 'react-collapsed'
|
||||||
|
import slugify from '@sindresorhus/slugify'
|
||||||
|
import styles from './VersionTableRow.module.scss'
|
||||||
|
import { VersionTableContracts, VersionTableCommons } from './VersionTable'
|
||||||
|
import VersionNumber from './VersionNumber'
|
||||||
|
import { ReactComponent as Caret } from '../../../img/caret.svg'
|
||||||
|
|
||||||
|
const VersionTableRow = ({ value }: { value: any }) => {
|
||||||
|
const collapseStyles = {
|
||||||
|
transitionDuration: '0.01s'
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandStyles = {
|
||||||
|
transitionDuration: '0.01s',
|
||||||
|
transitionTimingFunction: 'ease-out'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getCollapseProps, getToggleProps, isOpen } = useCollapse({
|
||||||
|
collapseStyles,
|
||||||
|
expandStyles
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{(value.name === 'Commons' || value.contracts) && (
|
||||||
|
<button className={styles.handle} {...getToggleProps()}>
|
||||||
|
<Caret className={isOpen ? styles.open : ''} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href={`https://github.com/oceanprotocol/${slugify(
|
||||||
|
value.name || value.software
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
<strong>{value.name || value.software}</strong>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<VersionNumber
|
||||||
|
name={value.name || value.software}
|
||||||
|
version={value.version}
|
||||||
|
status={value.status}
|
||||||
|
network={value.network}
|
||||||
|
commit={value.commit}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{value.name === 'Commons' && (
|
||||||
|
<tr {...getCollapseProps()}>
|
||||||
|
<td colSpan={2}>
|
||||||
|
<VersionTableCommons />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{value.contracts && (
|
||||||
|
<tr {...getCollapseProps()}>
|
||||||
|
<td colSpan={2}>
|
||||||
|
<VersionTableContracts
|
||||||
|
contracts={value.contracts}
|
||||||
|
network={value.network || ''}
|
||||||
|
keeperVersion={value.keeperVersion}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VersionTableRow
|
@ -0,0 +1,13 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.versionsTitle {
|
||||||
|
font-size: $font-size-large;
|
||||||
|
margin-bottom: $spacer / 2;
|
||||||
|
margin-top: $spacer * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versionsMinimal {
|
||||||
|
font-family: $font-family-monospace;
|
||||||
|
font-size: $font-size-mini;
|
||||||
|
margin-top: $spacer;
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import mockAxios from 'jest-mock-axios'
|
||||||
|
import { StateMock } from '@react-mock/state'
|
||||||
|
import VersionNumbers from '.'
|
||||||
|
|
||||||
|
import { User } from '../../../context'
|
||||||
|
import { userMockConnected } from '../../../../__mocks__/user-mock'
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockAxios.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
const stateMockIncomplete = {
|
||||||
|
commons: {
|
||||||
|
name: 'Commons',
|
||||||
|
version: undefined
|
||||||
|
},
|
||||||
|
squid: {
|
||||||
|
name: 'Squid-js',
|
||||||
|
version: undefined
|
||||||
|
},
|
||||||
|
aquarius: {
|
||||||
|
name: 'Aquarius',
|
||||||
|
version: undefined
|
||||||
|
},
|
||||||
|
brizo: {
|
||||||
|
name: 'Brizo',
|
||||||
|
version: undefined,
|
||||||
|
contracts: undefined,
|
||||||
|
network: undefined,
|
||||||
|
keeperVersion: undefined,
|
||||||
|
keeperUrl: undefined
|
||||||
|
},
|
||||||
|
faucet: {
|
||||||
|
name: 'Faucet',
|
||||||
|
version: undefined
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
ok: false,
|
||||||
|
network: false,
|
||||||
|
contracts: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
software: 'Faucet',
|
||||||
|
version: '6.6.6'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockResponseFaulty = {
|
||||||
|
status: 404,
|
||||||
|
statusText: 'Not Found',
|
||||||
|
data: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('VersionNumbers', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<VersionNumbers />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
mockAxios.mockResponse(mockResponse)
|
||||||
|
expect(mockAxios.get).toHaveBeenCalled()
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders without proper component response', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<StateMock state={stateMockIncomplete}>
|
||||||
|
<VersionNumbers />
|
||||||
|
</StateMock>
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
mockAxios.mockResponse(mockResponseFaulty)
|
||||||
|
expect(mockAxios.get).toHaveBeenCalled()
|
||||||
|
expect(container.querySelector('table')).toHaveTextContent(
|
||||||
|
'Could not get version'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
178
client/src/components/molecules/VersionNumbers/index.tsx
Normal file
178
client/src/components/molecules/VersionNumbers/index.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import {
|
||||||
|
OceanPlatformVersions,
|
||||||
|
OceanPlatformTechStatus,
|
||||||
|
Logger
|
||||||
|
} from '@oceanprotocol/squid'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { version } from '../../../../package.json'
|
||||||
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
|
import { nodeUri, faucetUri } from '../../../config'
|
||||||
|
import { User, Market } from '../../../context'
|
||||||
|
|
||||||
|
import VersionTable from './VersionTable'
|
||||||
|
import VersionStatus from './VersionStatus'
|
||||||
|
|
||||||
|
interface VersionNumbersProps {
|
||||||
|
minimal?: boolean
|
||||||
|
account: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionNumbersState extends OceanPlatformVersions {
|
||||||
|
commons: {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
network: string
|
||||||
|
}
|
||||||
|
faucet: {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
network: string
|
||||||
|
status: OceanPlatformTechStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class VersionNumbers extends PureComponent<
|
||||||
|
VersionNumbersProps,
|
||||||
|
VersionNumbersState
|
||||||
|
> {
|
||||||
|
public static contextType = User
|
||||||
|
|
||||||
|
// construct values which are not part of any response
|
||||||
|
public commonsVersion =
|
||||||
|
process.env.NODE_ENV === 'production' ? version : `${version}-dev`
|
||||||
|
public commonsNetwork = new URL(nodeUri).hostname.split('.')[0]
|
||||||
|
public faucetNetwork = faucetUri.includes('dev-ocean')
|
||||||
|
? new URL(faucetUri).hostname.split('.')[1]
|
||||||
|
: 'Pacific'
|
||||||
|
|
||||||
|
// define a minimal default state to fill UI
|
||||||
|
public state: VersionNumbersState = {
|
||||||
|
commons: {
|
||||||
|
name: 'Commons',
|
||||||
|
network: this.commonsNetwork,
|
||||||
|
version: this.commonsVersion
|
||||||
|
},
|
||||||
|
squid: {
|
||||||
|
name: 'Squid-js',
|
||||||
|
status: OceanPlatformTechStatus.Loading
|
||||||
|
},
|
||||||
|
aquarius: {
|
||||||
|
name: 'Aquarius',
|
||||||
|
status: OceanPlatformTechStatus.Loading
|
||||||
|
},
|
||||||
|
brizo: {
|
||||||
|
name: 'Brizo',
|
||||||
|
status: OceanPlatformTechStatus.Loading
|
||||||
|
},
|
||||||
|
faucet: {
|
||||||
|
name: 'Faucet',
|
||||||
|
version: '',
|
||||||
|
network: this.faucetNetwork,
|
||||||
|
status: OceanPlatformTechStatus.Loading
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
ok: false,
|
||||||
|
network: false,
|
||||||
|
contracts: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for canceling axios requests
|
||||||
|
public signal = axios.CancelToken.source()
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.getOceanVersions()
|
||||||
|
this.getFaucetVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async componentDidUpdate(prevProps: any) {
|
||||||
|
// Workaround: Using account prop instead of getting it from
|
||||||
|
// context to be able to compare. Cause there is no `prevContext`.
|
||||||
|
if (prevProps.account !== this.props.account) {
|
||||||
|
this.getOceanVersions()
|
||||||
|
this.getFaucetVersion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.signal.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOceanVersions() {
|
||||||
|
const { ocean } = this.context
|
||||||
|
// wait until ocean object is properly populated
|
||||||
|
if (ocean.versions === undefined) return
|
||||||
|
|
||||||
|
const response = await ocean.versions.get()
|
||||||
|
const { squid, brizo, aquarius, status } = response
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
squid,
|
||||||
|
brizo,
|
||||||
|
aquarius,
|
||||||
|
status
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getFaucetVersion() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(faucetUri, {
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
cancelToken: this.signal.token
|
||||||
|
})
|
||||||
|
|
||||||
|
// fail silently
|
||||||
|
if (response.status !== 200) return
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
faucet: {
|
||||||
|
...this.state.faucet,
|
||||||
|
version: response.data.version,
|
||||||
|
status: OceanPlatformTechStatus.Working
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
!axios.isCancel(error) && Logger.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MinimalOutput = () => {
|
||||||
|
const { commons, squid, brizo, aquarius } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Market.Consumer>
|
||||||
|
{market => (
|
||||||
|
<p className={styles.versionsMinimal}>
|
||||||
|
<a
|
||||||
|
title={`${squid.name} v${squid.version}\n${brizo.name} v${brizo.version}\n${aquarius.name} v${aquarius.version}`}
|
||||||
|
href={'/about'}
|
||||||
|
>
|
||||||
|
v{commons.version}{' '}
|
||||||
|
{market.network && `(${market.network})`}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Market.Consumer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { minimal } = this.props
|
||||||
|
|
||||||
|
return minimal ? (
|
||||||
|
<this.MinimalOutput />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2 className={styles.versionsTitle} id="#oceanversions">
|
||||||
|
Ocean Components Status
|
||||||
|
</h2>
|
||||||
|
<VersionStatus status={this.state.status} />
|
||||||
|
<VersionTable data={this.state} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
42
client/src/components/organisms/AssetsLatest.module.scss
Normal file
42
client/src/components/organisms/AssetsLatest.module.scss
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.latestAssetsWrap {
|
||||||
|
// full width break out of container
|
||||||
|
margin-right: calc(-50vw + 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.latestAssets {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: $spacer;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
padding: $spacer / 2 $spacer;
|
||||||
|
border-left: 1px solid $brand-grey-lighter;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar,
|
||||||
|
&::-moz-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> article {
|
||||||
|
min-width: calc(18rem + #{$spacer});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: $font-size-h4;
|
||||||
|
text-align: center;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
border-bottom: 1px solid $brand-grey-lighter;
|
||||||
|
padding-bottom: $spacer / 3;
|
||||||
|
margin-top: $spacer * 3;
|
||||||
|
margin-bottom: $spacer / 2;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
19
client/src/components/organisms/AssetsLatest.test.tsx
Normal file
19
client/src/components/organisms/AssetsLatest.test.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import AssetsLatest from './AssetsLatest'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||||
|
|
||||||
|
describe('AssetsLatest', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<AssetsLatest />
|
||||||
|
</BrowserRouter>
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
79
client/src/components/organisms/AssetsLatest.tsx
Normal file
79
client/src/components/organisms/AssetsLatest.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import { Logger } from '@oceanprotocol/squid'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import Spinner from '../atoms/Spinner'
|
||||||
|
import AssetTeaser from '../molecules/AssetTeaser'
|
||||||
|
import styles from './AssetsLatest.module.scss'
|
||||||
|
|
||||||
|
interface AssetsLatestState {
|
||||||
|
latestAssets?: any[]
|
||||||
|
isLoadingLatest?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AssetsLatest extends PureComponent<{}, AssetsLatestState> {
|
||||||
|
public state = { latestAssets: [], isLoadingLatest: true }
|
||||||
|
|
||||||
|
public _isMounted: boolean = false
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this._isMounted = true
|
||||||
|
this._isMounted && this.getLatestAssets()
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this._isMounted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLatestAssets = async () => {
|
||||||
|
const { ocean } = this.context
|
||||||
|
|
||||||
|
const searchQuery = {
|
||||||
|
offset: 15,
|
||||||
|
page: 1,
|
||||||
|
query: {},
|
||||||
|
sort: {
|
||||||
|
created: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const search = await ocean.aquarius.queryMetadata(searchQuery)
|
||||||
|
this.setState({
|
||||||
|
latestAssets: search.results,
|
||||||
|
isLoadingLatest: false
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error.message)
|
||||||
|
this.setState({ isLoadingLatest: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { latestAssets, isLoadingLatest } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2 className={styles.title}>Latest published assets</h2>
|
||||||
|
<div className={styles.latestAssetsWrap}>
|
||||||
|
{isLoadingLatest ? (
|
||||||
|
<Spinner message="Loading..." />
|
||||||
|
) : latestAssets && latestAssets.length ? (
|
||||||
|
<div className={styles.latestAssets}>
|
||||||
|
{latestAssets.map((asset: any) => (
|
||||||
|
<AssetTeaser
|
||||||
|
key={asset.id}
|
||||||
|
asset={asset}
|
||||||
|
minimal
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>No data sets found.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetsLatest.contextType = User
|
@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'
|
|||||||
import { Logger } from '@oceanprotocol/squid'
|
import { Logger } from '@oceanprotocol/squid'
|
||||||
import { User } from '../../context'
|
import { User } from '../../context'
|
||||||
import Spinner from '../atoms/Spinner'
|
import Spinner from '../atoms/Spinner'
|
||||||
import Asset from '../molecules/Asset'
|
import AssetTeaser from '../molecules/AssetTeaser'
|
||||||
import styles from './AssetsUser.module.scss'
|
import styles from './AssetsUser.module.scss'
|
||||||
|
|
||||||
export default class AssetsUser extends PureComponent<
|
export default class AssetsUser extends PureComponent<
|
||||||
@ -57,10 +57,9 @@ export default class AssetsUser extends PureComponent<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { account, isOceanNetwork } = this.context
|
const { account } = this.context
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isOceanNetwork &&
|
|
||||||
account && (
|
account && (
|
||||||
<div className={styles.assetsUser}>
|
<div className={styles.assetsUser}>
|
||||||
{this.props.recent && (
|
{this.props.recent && (
|
||||||
@ -82,7 +81,7 @@ export default class AssetsUser extends PureComponent<
|
|||||||
)
|
)
|
||||||
.filter(asset => !!asset)
|
.filter(asset => !!asset)
|
||||||
.map((asset: any) => (
|
.map((asset: any) => (
|
||||||
<Asset
|
<AssetTeaser
|
||||||
list={this.props.list}
|
list={this.props.list}
|
||||||
key={asset.id}
|
key={asset.id}
|
||||||
asset={asset}
|
asset={asset}
|
||||||
|
86
client/src/components/organisms/ChannelTeaser.module.scss
Normal file
86
client/src/components/organisms/ChannelTeaser.module.scss
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.channel {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--medium) {
|
||||||
|
padding-top: $spacer * 2;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
&:first-child {
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--medium) {
|
||||||
|
margin-right: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $break-point--medium) {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
flex: 0 0 calc(18rem + #{$spacer * 2});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// style channel teaser following another one
|
||||||
|
+ .channel {
|
||||||
|
border-top: 1px solid $brand-grey-lighter;
|
||||||
|
margin-top: $spacer * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelTitle {
|
||||||
|
margin-top: $spacer * 4;
|
||||||
|
margin-bottom: $spacer / 4;
|
||||||
|
color: $brand-black;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--medium) {
|
||||||
|
margin-top: -($spacer / 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelHeader {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
// category image
|
||||||
|
// stylelint-disable-next-line
|
||||||
|
.channelTitle + div {
|
||||||
|
opacity: 1;
|
||||||
|
background-size: 105%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelTeaser {
|
||||||
|
color: $brand-grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelResults {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-gap: $spacer;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
19
client/src/components/organisms/ChannelTeaser.test.tsx
Normal file
19
client/src/components/organisms/ChannelTeaser.test.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import ChannelTeaser from './ChannelTeaser'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||||
|
|
||||||
|
describe('ChannelTeaser', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<ChannelTeaser channel="ai-for-good" />
|
||||||
|
</BrowserRouter>
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
99
client/src/components/organisms/ChannelTeaser.tsx
Normal file
99
client/src/components/organisms/ChannelTeaser.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import { Logger } from '@oceanprotocol/squid'
|
||||||
|
import Spinner from '../atoms/Spinner'
|
||||||
|
import AssetTeaser from '../molecules/AssetTeaser'
|
||||||
|
import styles from './ChannelTeaser.module.scss'
|
||||||
|
import channels from '../../data/channels.json'
|
||||||
|
import CategoryImage from '../atoms/CategoryImage'
|
||||||
|
|
||||||
|
interface ChannelTeaserProps {
|
||||||
|
channel: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChannelTeaserState {
|
||||||
|
channelAssets?: any[]
|
||||||
|
isLoadingChannel?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ChannelTeaser extends Component<
|
||||||
|
ChannelTeaserProps,
|
||||||
|
ChannelTeaserState
|
||||||
|
> {
|
||||||
|
public static contextType = User
|
||||||
|
|
||||||
|
// Get channel content
|
||||||
|
public channel = channels.items
|
||||||
|
.filter(({ tag }) => tag === this.props.channel)
|
||||||
|
.map(channel => channel)[0]
|
||||||
|
|
||||||
|
public state = {
|
||||||
|
channelAssets: [],
|
||||||
|
isLoadingChannel: true
|
||||||
|
}
|
||||||
|
|
||||||
|
public async componentDidMount() {
|
||||||
|
this.getChannelAssets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private getChannelAssets = async () => {
|
||||||
|
const { ocean } = this.context
|
||||||
|
|
||||||
|
const searchQuery = {
|
||||||
|
offset: 2,
|
||||||
|
page: 1,
|
||||||
|
query: {
|
||||||
|
tags: [this.channel.tag]
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
created: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const search = await ocean.aquarius.queryMetadata(searchQuery)
|
||||||
|
this.setState({
|
||||||
|
channelAssets: search.results,
|
||||||
|
isLoadingChannel: false
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error.message)
|
||||||
|
this.setState({ isLoadingChannel: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { channelAssets, isLoadingChannel } = this.state
|
||||||
|
const { title, tag, teaser } = this.channel
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.channel}>
|
||||||
|
<div>
|
||||||
|
<header className={styles.channelHeader}>
|
||||||
|
<Link to={`/channels/${tag}`}>
|
||||||
|
<h2 className={styles.channelTitle}>{title}</h2>
|
||||||
|
<CategoryImage category={title} />
|
||||||
|
|
||||||
|
<p className={styles.channelTeaser}>{teaser}</p>
|
||||||
|
<p>Browse the channel →</p>
|
||||||
|
</Link>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{isLoadingChannel ? (
|
||||||
|
<Spinner message="Loading..." />
|
||||||
|
) : channelAssets && channelAssets.length ? (
|
||||||
|
<div className={styles.channelResults}>
|
||||||
|
{channelAssets.map((asset: any) => (
|
||||||
|
<AssetTeaser key={asset.id} asset={asset} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>No data sets found.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -71,3 +71,23 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aicommons {
|
||||||
|
svg {
|
||||||
|
width: 100px;
|
||||||
|
height: auto;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-top: -.05rem;
|
||||||
|
margin-left: $spacer / 6;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
svg {
|
||||||
|
fill: $brand-pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,44 +1,53 @@
|
|||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Market } from '../../context'
|
import { Market, User } from '../../context'
|
||||||
import Content from '../atoms/Content'
|
import Content from '../atoms/Content'
|
||||||
|
import { ReactComponent as AiCommons } from '../../img/aicommons.svg'
|
||||||
import styles from './Footer.module.scss'
|
import styles from './Footer.module.scss'
|
||||||
|
|
||||||
import meta from '../../data/meta.json'
|
import meta from '../../data/meta.json'
|
||||||
|
import VersionNumbers from '../molecules/VersionNumbers'
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
const market = useContext(Market)
|
||||||
|
const user = useContext(User)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<aside className={styles.stats}>
|
||||||
|
<Content wide>
|
||||||
|
<p>
|
||||||
|
Online since March 2019.
|
||||||
|
{market.totalAssets > 0 &&
|
||||||
|
` With a total of ${market.totalAssets} registered assets.`}
|
||||||
|
</p>
|
||||||
|
<p className={styles.aicommons}>
|
||||||
|
Proud supporter of{' '}
|
||||||
|
<a
|
||||||
|
href="https://aicommons.com/?utm_source=commons.oceanprotocol.com"
|
||||||
|
title="AI Commons"
|
||||||
|
>
|
||||||
|
<AiCommons />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<VersionNumbers account={user.account} minimal />
|
||||||
|
</Content>
|
||||||
|
</aside>
|
||||||
|
|
||||||
const Footer = () => (
|
|
||||||
<footer className={styles.footer}>
|
|
||||||
<aside className={styles.stats}>
|
|
||||||
<Content wide>
|
<Content wide>
|
||||||
<p>
|
<small>
|
||||||
Online since March 2019.
|
© {new Date().getFullYear()}{' '}
|
||||||
<Market.Consumer>
|
<a href={meta.social[0].url}>{meta.company}</a> — All
|
||||||
{state =>
|
Rights Reserved
|
||||||
state.totalAssets > 0 &&
|
</small>
|
||||||
` With a total of ${
|
|
||||||
state.totalAssets
|
<nav className={styles.links}>
|
||||||
} registered assets.`
|
{meta.social.map(site => (
|
||||||
}
|
<a key={site.title} href={site.url}>
|
||||||
</Market.Consumer>
|
{site.title}
|
||||||
</p>
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
</Content>
|
</Content>
|
||||||
</aside>
|
</footer>
|
||||||
|
)
|
||||||
<Content wide>
|
}
|
||||||
<small>
|
|
||||||
© {new Date().getFullYear()}{' '}
|
|
||||||
<a href={meta.social[0].url}>{meta.company}</a> — All
|
|
||||||
Rights Reserved
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<nav className={styles.links}>
|
|
||||||
{meta.social.map(site => (
|
|
||||||
<a key={site.title} href={site.url}>
|
|
||||||
{site.title}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</Content>
|
|
||||||
</footer>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default Footer
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react'
|
import React, { PureComponent } from 'react'
|
||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
import { ReactComponent as Logo } from '@oceanprotocol/art/logo/logo.svg'
|
import { ReactComponent as Logo } from '@oceanprotocol/art/logo/logo.svg'
|
||||||
import { User } from '../../context'
|
|
||||||
import AccountStatus from '../molecules/AccountStatus'
|
import AccountStatus from '../molecules/AccountStatus'
|
||||||
import styles from './Header.module.scss'
|
import styles from './Header.module.scss'
|
||||||
|
|
||||||
@ -40,5 +39,3 @@ export default class Header extends PureComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Header.contextType = User
|
|
||||||
|
73
client/src/components/organisms/WalletSelector.module.scss
Normal file
73
client/src/components/organisms/WalletSelector.module.scss
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.openLink {
|
||||||
|
font-size: $font-size-small !important; // stylelint-disable-line
|
||||||
|
margin-left: $spacer / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
background: $brand-white;
|
||||||
|
border: 1px solid $brand-grey-lighter;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: $spacer / 1.5;
|
||||||
|
font-family: $font-family-base;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border .2s ease-out;
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
flex-basis: 48%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
border-color: $brand-pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonActive {
|
||||||
|
composes: button;
|
||||||
|
pointer-events: none;
|
||||||
|
background: $body-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
position: absolute;
|
||||||
|
right: $spacer / 3;
|
||||||
|
top: $spacer / 4;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonIcon {
|
||||||
|
font-size: $font-size-h4;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: $spacer / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonTitle {
|
||||||
|
font-size: $font-size-base;
|
||||||
|
margin-bottom: $spacer / 2;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonDescription {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
}
|
23
client/src/components/organisms/WalletSelector.test.tsx
Normal file
23
client/src/components/organisms/WalletSelector.test.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, fireEvent } from '@testing-library/react'
|
||||||
|
import ReactModal from 'react-modal'
|
||||||
|
import WalletSelector from './WalletSelector'
|
||||||
|
import { User, Market } from '../../context'
|
||||||
|
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||||
|
import { marketMock } from '../../../__mocks__/market-mock'
|
||||||
|
|
||||||
|
describe('WalletSelector', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
ReactModal.setAppElement(document.createElement('div'))
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<Market.Provider value={marketMock}>
|
||||||
|
<WalletSelector />
|
||||||
|
</Market.Provider>
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
fireEvent.click(container.querySelector('button'))
|
||||||
|
})
|
||||||
|
})
|
108
client/src/components/organisms/WalletSelector.tsx
Normal file
108
client/src/components/organisms/WalletSelector.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import Modal from '../atoms/Modal'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import styles from './WalletSelector.module.scss'
|
||||||
|
import Button from '../atoms/Button'
|
||||||
|
import content from '../../data/wallets.json'
|
||||||
|
|
||||||
|
export default class WalletSelector extends PureComponent<
|
||||||
|
{},
|
||||||
|
{ isModalOpen: boolean }
|
||||||
|
> {
|
||||||
|
public static contextType = User
|
||||||
|
|
||||||
|
public state = {
|
||||||
|
isModalOpen: false
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleModal = () => {
|
||||||
|
this.setState({ isModalOpen: !this.state.isModalOpen })
|
||||||
|
}
|
||||||
|
|
||||||
|
private loginBurnerWallet = () => {
|
||||||
|
this.context.loginBurnerWallet()
|
||||||
|
this.toggleModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
private loginMetamask = () => {
|
||||||
|
this.context.loginMetamask()
|
||||||
|
this.context.logoutBurnerWallet()
|
||||||
|
this.toggleModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
private WalletButton = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
icon: string
|
||||||
|
}) => {
|
||||||
|
const active =
|
||||||
|
(title === 'Burner Wallet' && this.context.isBurner) ||
|
||||||
|
(title === 'MetaMask' && !this.context.isBurner)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={active ? styles.buttonActive : styles.button}
|
||||||
|
onClick={
|
||||||
|
title === 'MetaMask'
|
||||||
|
? this.loginMetamask
|
||||||
|
: this.loginBurnerWallet
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 className={styles.buttonTitle}>
|
||||||
|
<span
|
||||||
|
className={styles.buttonIcon}
|
||||||
|
role="img"
|
||||||
|
aria-label={title}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<span className={styles.buttonDescription}>
|
||||||
|
{description}
|
||||||
|
</span>
|
||||||
|
{active && (
|
||||||
|
<span className={styles.selected}>Selected</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
link
|
||||||
|
className={styles.openLink}
|
||||||
|
onClick={this.toggleModal}
|
||||||
|
data-action="wallet"
|
||||||
|
>
|
||||||
|
{content.title}
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
title={content.title}
|
||||||
|
description={content.description}
|
||||||
|
isOpen={this.state.isModalOpen}
|
||||||
|
toggleModal={this.toggleModal}
|
||||||
|
>
|
||||||
|
<div className={styles.info}>
|
||||||
|
{content.buttons.map(({ title, description, icon }) => (
|
||||||
|
<this.WalletButton
|
||||||
|
key={title}
|
||||||
|
title={title}
|
||||||
|
icon={icon}
|
||||||
|
description={description}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -4,19 +4,27 @@
|
|||||||
margin-bottom: $spacer;
|
margin-bottom: $spacer;
|
||||||
color: $brand-grey;
|
color: $brand-grey;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-bottom: .1rem solid $brand-grey-lighter;
|
|
||||||
border-top: .1rem solid $brand-grey-lighter;
|
|
||||||
padding-top: $spacer / 2;
|
padding-top: $spacer / 2;
|
||||||
padding-bottom: $spacer / 2;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
font-size: $font-size-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warnings {
|
.account {
|
||||||
padding-left: $spacer;
|
margin-bottom: $spacer / 2;
|
||||||
|
background: $brand-white;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
border: 1px solid $brand-grey-lighter;
|
||||||
|
padding: $spacer / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
padding-left: $spacer * 1.5;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
margin-left: -($spacer);
|
margin-left: -($spacer / 1.2);
|
||||||
margin-right: $spacer / 2;
|
margin-right: $spacer / 2.5;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,34 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render, fireEvent } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import Web3message from './Web3message'
|
import Web3message from './Web3message'
|
||||||
import { User } from '../../context'
|
import { User, Market } from '../../context'
|
||||||
import { userMock, userMockConnected } from '../../../__mocks__/user-mock'
|
import { userMock, userMockConnected } from '../../../__mocks__/user-mock'
|
||||||
|
import { marketMock } from '../../../__mocks__/market-mock'
|
||||||
|
|
||||||
describe('Web3message', () => {
|
describe('Web3message', () => {
|
||||||
it('renders with noWeb3 message', () => {
|
it('renders with burner wallet message', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<User.Provider value={{ ...userMock }}>
|
<User.Provider value={{ ...userMockConnected, isBurner: true }}>
|
||||||
<Web3message />
|
<Market.Provider value={{ ...marketMock }}>
|
||||||
|
<Web3message extended />
|
||||||
|
</Market.Provider>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
)
|
)
|
||||||
expect(container.firstChild).toHaveTextContent('Not a Web3 Browser')
|
expect(container.firstChild).toHaveTextContent('Burner Wallet')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders with wrongNetwork message', () => {
|
it('renders with wrongNetwork message', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<User.Provider value={{ ...userMock, isWeb3: true }}>
|
<User.Provider value={{ ...userMockConnected, network: 'Pacific' }}>
|
||||||
<Web3message />
|
<Market.Provider
|
||||||
|
value={{
|
||||||
|
...marketMock,
|
||||||
|
networkMatch: false,
|
||||||
|
network: 'Nile'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Web3message extended />
|
||||||
|
</Market.Provider>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
)
|
)
|
||||||
expect(container.firstChild).toHaveTextContent(
|
expect(container.firstChild).toHaveTextContent(
|
||||||
@ -27,38 +38,23 @@ describe('Web3message', () => {
|
|||||||
|
|
||||||
it('renders with noAccount message', () => {
|
it('renders with noAccount message', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<User.Provider
|
<User.Provider value={userMock}>
|
||||||
value={{ ...userMock, isWeb3: true, isOceanNetwork: true }}
|
<Market.Provider value={marketMock}>
|
||||||
>
|
<Web3message extended />
|
||||||
<Web3message />
|
</Market.Provider>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
)
|
)
|
||||||
expect(container.firstChild).toHaveTextContent('No accounts detected')
|
expect(container.firstChild).toHaveTextContent('No account selected')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders with hasAccount message', () => {
|
it('renders with hasAccount message', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<User.Provider value={userMockConnected}>
|
<User.Provider value={userMockConnected}>
|
||||||
<Web3message />
|
<Market.Provider value={marketMock}>
|
||||||
|
<Web3message />
|
||||||
|
</Market.Provider>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
)
|
)
|
||||||
expect(container.firstChild).toHaveTextContent('0xxxxxx')
|
expect(container.firstChild).toHaveTextContent('0xxxxxx')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('button click fires unlockAccounts', () => {
|
|
||||||
const { getByText } = render(
|
|
||||||
<User.Provider
|
|
||||||
value={{
|
|
||||||
...userMock,
|
|
||||||
isWeb3: true,
|
|
||||||
isOceanNetwork: true
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Web3message />
|
|
||||||
</User.Provider>
|
|
||||||
)
|
|
||||||
|
|
||||||
fireEvent.click(getByText('Unlock Account'))
|
|
||||||
expect(userMock.unlockAccounts).toBeCalled()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -1,53 +1,66 @@
|
|||||||
import React, { PureComponent } from 'react'
|
import React, { PureComponent } from 'react'
|
||||||
import Account from '../atoms/Account'
|
import Account from '../atoms/Account'
|
||||||
import Button from '../atoms/Button'
|
|
||||||
import AccountStatus from '../molecules/AccountStatus'
|
import AccountStatus from '../molecules/AccountStatus'
|
||||||
import styles from './Web3message.module.scss'
|
import styles from './Web3message.module.scss'
|
||||||
import { User } from '../../context'
|
import { User, Market } from '../../context'
|
||||||
import content from '../../data/web3message.json'
|
import content from '../../data/web3message.json'
|
||||||
|
|
||||||
export default class Web3message extends PureComponent {
|
export default class Web3message extends PureComponent<{ extended?: boolean }> {
|
||||||
private message = (
|
public static contextType = Market
|
||||||
message: string,
|
|
||||||
account?: string,
|
private messageOceanNetwork = () =>
|
||||||
unlockAccounts?: () => any
|
this.context.network === 'Pacific'
|
||||||
) => (
|
? content.wrongNetworkPacific
|
||||||
<div className={styles.message}>
|
: this.context.network === 'Nile'
|
||||||
{account ? (
|
? content.wrongNetworkNile
|
||||||
<Account account={account} />
|
: this.context.network === 'Duero'
|
||||||
) : (
|
? content.wrongNetworkDuero
|
||||||
<div className={styles.warnings}>
|
: content.wrongNetworkSpree
|
||||||
<AccountStatus className={styles.status} />
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: message }} />{' '}
|
private Message = () => {
|
||||||
{unlockAccounts && (
|
const { networkMatch, network } = this.context
|
||||||
<Button onClick={() => unlockAccounts()} link>
|
|
||||||
Unlock Account
|
return (
|
||||||
</Button>
|
<User.Consumer>
|
||||||
)}
|
{user => (
|
||||||
</div>
|
<em
|
||||||
)}
|
dangerouslySetInnerHTML={{
|
||||||
</div>
|
__html:
|
||||||
)
|
!networkMatch && !user.isBurner
|
||||||
|
? this.messageOceanNetwork()
|
||||||
|
: !user.isLogged
|
||||||
|
? content.noAccount
|
||||||
|
: user.isBurner
|
||||||
|
? content.hasBurnerWallet
|
||||||
|
: user.isLogged
|
||||||
|
? content.hasMetaMaskWallet.replace(
|
||||||
|
'NETWORK',
|
||||||
|
network
|
||||||
|
)
|
||||||
|
: ''
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</User.Consumer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const { networkMatch } = this.context
|
||||||
isWeb3,
|
|
||||||
isOceanNetwork,
|
|
||||||
isLogged,
|
|
||||||
account,
|
|
||||||
unlockAccounts
|
|
||||||
} = this.context
|
|
||||||
|
|
||||||
return !isWeb3
|
return (
|
||||||
? this.message(content.noweb3)
|
<div className={styles.message}>
|
||||||
: !isOceanNetwork
|
<div className={styles.account}>
|
||||||
? this.message(content.wrongNetwork)
|
<Account />
|
||||||
: !isLogged
|
</div>
|
||||||
? this.message(content.noAccount, '', unlockAccounts)
|
|
||||||
: isLogged
|
{(!networkMatch || this.props.extended) && (
|
||||||
? this.message(content.hasAccount, account)
|
<div className={styles.text}>
|
||||||
: null
|
<AccountStatus className={styles.status} />
|
||||||
|
<this.Message />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Web3message.contextType = User
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../styles/variables';
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
.metaPrimary {
|
.metaPrimary {
|
||||||
margin-bottom: $spacer;
|
margin-bottom: $spacer;
|
||||||
@ -46,18 +46,41 @@
|
|||||||
.description {
|
.description {
|
||||||
// respect line breaks from textarea
|
// respect line breaks from textarea
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
|
|
||||||
|
// handle assets where heading are used extensively in the description
|
||||||
|
h1 {
|
||||||
|
font-size: $font-size-h3;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: $font-size-h4;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: $font-size-h5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: $font-size-base;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta {
|
.metaFixed {
|
||||||
border-top: 1px solid $brand-grey-lighter;
|
border: 1px solid $brand-grey-lighter;
|
||||||
border-bottom: 1px solid $brand-grey-lighter;
|
padding: $spacer;
|
||||||
padding-top: $spacer;
|
border-radius: $border-radius;
|
||||||
padding-bottom: $spacer;
|
|
||||||
margin-top: $spacer;
|
margin-top: $spacer;
|
||||||
margin-bottom: $spacer;
|
margin-bottom: $spacer * $line-height;
|
||||||
list-style: none;
|
|
||||||
padding-left: 0;
|
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -65,7 +88,7 @@
|
|||||||
|
|
||||||
@media (min-width: $break-point--small) {
|
@media (min-width: $break-point--small) {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 0;
|
margin-bottom: $spacer / 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
@ -74,6 +97,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metaFixedTitle {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
margin: 0;
|
||||||
|
font-family: $font-family-base;
|
||||||
|
font-weight: $font-weight-base;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
position: absolute;
|
||||||
|
bottom: $spacer / 4;
|
||||||
|
right: $spacer / 4;
|
||||||
|
}
|
||||||
|
|
||||||
.metaLabel {
|
.metaLabel {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@ -90,10 +124,15 @@
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* stylelint-disable declaration-no-important */
|
||||||
code {
|
code {
|
||||||
display: inline;
|
display: block;
|
||||||
|
padding: 0 !important;
|
||||||
|
background: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* stylelint-enable declaration-no-important */
|
||||||
|
|
||||||
@media (min-width: $break-point--small) {
|
@media (min-width: $break-point--small) {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import { DDO, MetaData } from '@oceanprotocol/squid'
|
import { DDO, MetaData } from '@oceanprotocol/squid'
|
||||||
import { BrowserRouter as Router } from 'react-router-dom'
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
import AssetDetails, { renderDatafilesLine } from './AssetDetails'
|
import AssetDetails, { renderDatafilesLine } from './AssetDetails'
|
@ -1,17 +1,18 @@
|
|||||||
import React, { PureComponent, ChangeEvent } from 'react'
|
import React, { PureComponent, ChangeEvent } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import Moment from 'react-moment'
|
import Moment from 'react-moment'
|
||||||
import { DDO, MetaData, Logger } from '@oceanprotocol/squid'
|
import { DDO, MetaData, Logger } from '@oceanprotocol/squid'
|
||||||
import Input from '../../components/atoms/Form/Input'
|
import Input from '../../atoms/Form/Input'
|
||||||
import Markdown from '../../components/atoms/Markdown'
|
import Markdown from '../../atoms/Markdown'
|
||||||
import { User } from '../../context'
|
import { User } from '../../../context'
|
||||||
|
import CategoryLink from '../../atoms/CategoryLink'
|
||||||
import styles from './AssetDetails.module.scss'
|
import styles from './AssetDetails.module.scss'
|
||||||
import AssetFilesDetails from './AssetFilesDetails'
|
import AssetFilesDetails from './AssetFilesDetails'
|
||||||
import Button from '../../components/atoms/Button'
|
import Button from '../../atoms/Button'
|
||||||
import Spinner from '../../components/atoms/Spinner'
|
import Spinner from '../../atoms/Spinner'
|
||||||
import { serviceHost, servicePort, serviceScheme } from '../../config'
|
import Report from './Report'
|
||||||
|
import { serviceUri } from '../../../config'
|
||||||
|
|
||||||
const { steps } = require('../../data/form-publish.json') // eslint-disable-line
|
const { steps } = require('../../../data/form-publish.json') // eslint-disable-line
|
||||||
|
|
||||||
export const renderDatafilesLine = (files: any) =>
|
export const renderDatafilesLine = (files: any) =>
|
||||||
files.length === 1 ? (
|
files.length === 1 ? (
|
||||||
@ -84,7 +85,7 @@ export default class AssetDetails extends PureComponent<
|
|||||||
private fetch = async (method: string, body: any) => {
|
private fetch = async (method: string, body: any) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${serviceScheme}://${serviceHost}:${servicePort}/api/v1/ddo/${
|
`${serviceUri}/api/v1/ddo/${
|
||||||
this.props.ddo
|
this.props.ddo
|
||||||
}`,
|
}`,
|
||||||
{
|
{
|
||||||
@ -213,7 +214,7 @@ export default class AssetDetails extends PureComponent<
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
// TODO: Make this link to search for respective category
|
// TODO: Make this link to search for respective category
|
||||||
<Link to={`/search?text=${value[0]}`}>{value[0]}</Link>
|
<CategoryLink category={value[0]} />
|
||||||
)
|
)
|
||||||
|
|
||||||
private Description = ({ value }: { value: string }) =>
|
private Description = ({ value }: { value: string }) =>
|
||||||
@ -297,15 +298,15 @@ export default class AssetDetails extends PureComponent<
|
|||||||
|
|
||||||
<div className={styles.metaPrimaryData}>
|
<div className={styles.metaPrimaryData}>
|
||||||
<span
|
<span
|
||||||
title={`Date created, published on ${
|
title={`Date created, published on ${base.datePublished}`}
|
||||||
base.datePublished
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<this.Date value={dateCreated} />
|
<this.Date value={dateCreated} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{categories && <this.Category value={categories} />}
|
{categories && <this.Category value={categories} />}
|
||||||
|
{/*base.categories && (
|
||||||
|
<CategoryLink category={base.categories[0]} />
|
||||||
|
)*/}
|
||||||
{base.files &&
|
{base.files &&
|
||||||
!isEditMode &&
|
!isEditMode &&
|
||||||
renderDatafilesLine(base.files)}
|
renderDatafilesLine(base.files)}
|
||||||
@ -316,28 +317,42 @@ export default class AssetDetails extends PureComponent<
|
|||||||
|
|
||||||
<this.MetadataActions />
|
<this.MetadataActions />
|
||||||
|
|
||||||
<ul className={styles.meta}>
|
<Report did={ddo.id} title={metadata.base.name} />
|
||||||
<li>
|
|
||||||
<span className={styles.metaLabel}>
|
<div className={styles.metaFixed}>
|
||||||
<strong>Author</strong>
|
<h2
|
||||||
</span>
|
className={styles.metaFixedTitle}
|
||||||
<span className={styles.metaValue}>{base.author}</span>
|
title="This metadata can not be changed because it is used to generate the checksums for the DDO, and to encrypt the file urls."
|
||||||
</li>
|
>
|
||||||
<li>
|
Fixed Metadata
|
||||||
<span className={styles.metaLabel}>
|
</h2>
|
||||||
<strong>License</strong>
|
<ul>
|
||||||
</span>
|
<li>
|
||||||
<span className={styles.metaValue}>{base.license}</span>
|
<span className={styles.metaLabel}>
|
||||||
</li>
|
<strong>Author</strong>
|
||||||
<li>
|
</span>
|
||||||
<span className={styles.metaLabel}>
|
<span className={styles.metaValue}>
|
||||||
<strong>DID</strong>
|
{base.author}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.metaValue}>
|
</li>
|
||||||
<code>{ddo.id}</code>
|
<li>
|
||||||
</span>
|
<span className={styles.metaLabel}>
|
||||||
</li>
|
<strong>License</strong>
|
||||||
</ul>
|
</span>
|
||||||
|
<span className={styles.metaValue}>
|
||||||
|
{base.license}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className={styles.metaLabel}>
|
||||||
|
<strong>DID</strong>
|
||||||
|
</span>
|
||||||
|
<span className={styles.metaValue}>
|
||||||
|
<code>{ddo.id}</code>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<AssetFilesDetails
|
<AssetFilesDetails
|
||||||
files={base.files ? base.files : []}
|
files={base.files ? base.files : []}
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../styles/variables';
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
.buttonMain {
|
.buttonMain {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@ -25,13 +25,16 @@
|
|||||||
|
|
||||||
.file {
|
.file {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: $brand-grey;
|
background: $brand-grey-dark
|
||||||
|
url('../../../../node_modules/@oceanprotocol/art/jellyfish/jellyfish-grid.svg')
|
||||||
|
no-repeat -1rem 4.5rem;
|
||||||
|
background-size: 100%;
|
||||||
padding: $spacer $spacer / 2;
|
padding: $spacer $spacer / 2;
|
||||||
margin-bottom: $spacer / 2;
|
margin-bottom: $spacer / 2;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 8rem;
|
height: 8.5rem;
|
||||||
width: 6rem;
|
width: 6.5rem;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: '';
|
||||||
@ -54,6 +57,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
font-size: $font-size-mini;
|
||||||
|
font-weight: $font-weight-base;
|
||||||
|
opacity: .75;
|
||||||
|
}
|
||||||
|
|
||||||
// move spinner a bit up
|
// move spinner a bit up
|
||||||
+ div {
|
+ div {
|
||||||
margin-top: $spacer / 2;
|
margin-top: $spacer / 2;
|
140
client/src/components/templates/Asset/AssetFile.test.tsx
Normal file
140
client/src/components/templates/Asset/AssetFile.test.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { render, fireEvent } from '@testing-library/react'
|
||||||
|
import { DDO } from '@oceanprotocol/squid'
|
||||||
|
import { StateMock } from '@react-mock/state'
|
||||||
|
import ReactGA from 'react-ga'
|
||||||
|
import { User, Market } from '../../../context'
|
||||||
|
import AssetFile, { messages } from './AssetFile'
|
||||||
|
import { userMockConnected } from '../../../../__mocks__/user-mock'
|
||||||
|
import { marketMock } from '../../../../__mocks__/market-mock'
|
||||||
|
|
||||||
|
const file = {
|
||||||
|
index: 0,
|
||||||
|
url: 'https://hello.com',
|
||||||
|
contentType: 'application/x-zip',
|
||||||
|
contentLength: 100
|
||||||
|
}
|
||||||
|
|
||||||
|
const ddo = ({
|
||||||
|
id: 'xxx',
|
||||||
|
findServiceByType: () => {
|
||||||
|
return { serviceDefinitionId: 'xxx' }
|
||||||
|
}
|
||||||
|
} as any) as DDO
|
||||||
|
|
||||||
|
ReactGA.initialize('foo', { testMode: true })
|
||||||
|
|
||||||
|
describe('AssetFile', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(<AssetFile file={file} ddo={ddo} />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('button to be disabled when not connected', () => {
|
||||||
|
const { container } = render(<AssetFile file={file} ddo={ddo} />)
|
||||||
|
expect(container.querySelector('button')).toHaveAttribute('disabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('button to be enabled when connected', async () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<Market.Provider value={marketMock}>
|
||||||
|
<AssetFile file={file} ddo={ddo} />
|
||||||
|
</Market.Provider>
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
const button = getByText('Get file')
|
||||||
|
expect(button).not.toHaveAttribute('disabled')
|
||||||
|
|
||||||
|
fireEvent.click(button)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders feedback message: initial', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<StateMock state={{ isLoading: true, step: 99 }}>
|
||||||
|
<AssetFile file={file} ddo={ddo} />
|
||||||
|
</StateMock>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.spinner')).toHaveTextContent(
|
||||||
|
messages[99]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders feedback message: CreatingAgreement', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<StateMock state={{ isLoading: true, step: 0 }}>
|
||||||
|
<AssetFile file={file} ddo={ddo} />
|
||||||
|
</StateMock>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.spinner')).toHaveTextContent(
|
||||||
|
messages[0].replace(/<(?:.|\n)*?>/gm, '')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders feedback message: AgreementInitialized', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<StateMock state={{ isLoading: true, step: 1 }}>
|
||||||
|
<AssetFile file={file} ddo={ddo} />
|
||||||
|
</StateMock>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.spinner')).toHaveTextContent(
|
||||||
|
messages[1].replace(/<(?:.|\n)*?>/gm, '')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders feedback message: LockingPayment', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<StateMock state={{ isLoading: true, step: 2 }}>
|
||||||
|
<AssetFile file={file} ddo={ddo} />
|
||||||
|
</StateMock>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.spinner')).toHaveTextContent(
|
||||||
|
messages[2].replace(/<(?:.|\n)*?>/gm, '')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders feedback message: LockedPayment', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<StateMock state={{ isLoading: true, step: 3 }}>
|
||||||
|
<AssetFile file={file} ddo={ddo} />
|
||||||
|
</StateMock>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.spinner')).toHaveTextContent(
|
||||||
|
messages[3].replace(/<(?:.|\n)*?>/gm, '')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders feedback message: before consume', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<StateMock state={{ isLoading: true, step: 4 }}>
|
||||||
|
<AssetFile file={file} ddo={ddo} />
|
||||||
|
</StateMock>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.spinner')).toHaveTextContent(
|
||||||
|
messages[4].replace(/<(?:.|\n)*?>/gm, '')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders loading state', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<StateMock state={{ isLoading: true }}>
|
||||||
|
<AssetFile file={file} ddo={ddo} />
|
||||||
|
</StateMock>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.spinner')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders error', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<StateMock state={{ error: 'Hello Error' }}>
|
||||||
|
<AssetFile file={file} ddo={ddo} />
|
||||||
|
</StateMock>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.error')).toBeInTheDocument()
|
||||||
|
expect(container.querySelector('.error')).toHaveTextContent(
|
||||||
|
'Hello Error'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
163
client/src/components/templates/Asset/AssetFile.tsx
Normal file
163
client/src/components/templates/Asset/AssetFile.tsx
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import { Logger, DDO, File } from '@oceanprotocol/squid'
|
||||||
|
import filesize from 'filesize'
|
||||||
|
import Button from '../../atoms/Button'
|
||||||
|
import Spinner from '../../atoms/Spinner'
|
||||||
|
import { User, Market } from '../../../context'
|
||||||
|
import styles from './AssetFile.module.scss'
|
||||||
|
import ReactGA from 'react-ga'
|
||||||
|
import cleanupContentType from '../../../utils/cleanupContentType'
|
||||||
|
|
||||||
|
export const messages: any = {
|
||||||
|
99: 'Decrypting file URL...',
|
||||||
|
0: '1/3<br />Asking for agreement signature...',
|
||||||
|
1: '1/3<br />Agreement initialized.',
|
||||||
|
2: '2/3<br />Asking for two payment confirmations...',
|
||||||
|
3: '2/3<br />Payment confirmed. Requesting access...',
|
||||||
|
4: '3/3<br /> Access granted. Consuming file...'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AssetFileProps {
|
||||||
|
file: File
|
||||||
|
ddo: DDO
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AssetFileState {
|
||||||
|
isLoading: boolean
|
||||||
|
error: string
|
||||||
|
step: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AssetFile extends PureComponent<
|
||||||
|
AssetFileProps,
|
||||||
|
AssetFileState
|
||||||
|
> {
|
||||||
|
public static contextType = User
|
||||||
|
|
||||||
|
public state = {
|
||||||
|
isLoading: false,
|
||||||
|
error: '',
|
||||||
|
step: 99
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetState = () =>
|
||||||
|
this.setState({
|
||||||
|
isLoading: true,
|
||||||
|
error: '',
|
||||||
|
step: 99
|
||||||
|
})
|
||||||
|
|
||||||
|
private purchaseAsset = async (ddo: DDO, index: number) => {
|
||||||
|
this.resetState()
|
||||||
|
|
||||||
|
ReactGA.event({
|
||||||
|
category: 'Purchase',
|
||||||
|
action: 'purchaseAsset-start ' + ddo.id
|
||||||
|
})
|
||||||
|
|
||||||
|
const { ocean } = this.context
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accounts = await ocean.accounts.list()
|
||||||
|
const service = ddo.findServiceByType('Access')
|
||||||
|
|
||||||
|
const agreements = await ocean.keeper.conditions.accessSecretStoreCondition.getGrantedDidByConsumer(
|
||||||
|
accounts[0].id
|
||||||
|
)
|
||||||
|
const agreement = agreements.find((element: any) => {
|
||||||
|
return element.did === ddo.id
|
||||||
|
})
|
||||||
|
|
||||||
|
let agreementId
|
||||||
|
|
||||||
|
if (agreement) {
|
||||||
|
;({ agreementId } = agreement)
|
||||||
|
} else {
|
||||||
|
agreementId = await ocean.assets
|
||||||
|
.order(ddo.id, service.serviceDefinitionId, accounts[0])
|
||||||
|
.next((step: number) => this.setState({ step }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// manually add another step here for better UX
|
||||||
|
this.setState({ step: 4 })
|
||||||
|
|
||||||
|
const path = await ocean.assets.consume(
|
||||||
|
agreementId,
|
||||||
|
ddo.id,
|
||||||
|
service.serviceDefinitionId,
|
||||||
|
accounts[0],
|
||||||
|
'',
|
||||||
|
index
|
||||||
|
)
|
||||||
|
Logger.log('path', path)
|
||||||
|
ReactGA.event({
|
||||||
|
category: 'Purchase',
|
||||||
|
action: 'purchaseAsset-end ' + ddo.id
|
||||||
|
})
|
||||||
|
this.setState({ isLoading: false })
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('error', error.message)
|
||||||
|
this.setState({
|
||||||
|
isLoading: false,
|
||||||
|
error: `${error.message}. Sorry about that, can you try again?`
|
||||||
|
})
|
||||||
|
ReactGA.event({
|
||||||
|
category: 'Purchase',
|
||||||
|
action: 'purchaseAsset-error ' + error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { ddo, file } = this.props
|
||||||
|
const { isLoading, error, step } = this.state
|
||||||
|
const { isLogged } = this.context
|
||||||
|
const { index, contentType, contentLength } = file
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.fileWrap}>
|
||||||
|
<ul key={index} className={styles.file}>
|
||||||
|
{contentType || contentLength ? (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
{contentType && cleanupContentType(contentType)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{contentLength && contentLength > 0
|
||||||
|
? filesize(contentLength)
|
||||||
|
: ''}
|
||||||
|
</li>
|
||||||
|
{/* <li>{encoding}</li> */}
|
||||||
|
{/* <li>{compression}</li> */}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<li className={styles.empty}>No file info available</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<Spinner message={messages[step]} />
|
||||||
|
) : (
|
||||||
|
<Market.Consumer>
|
||||||
|
{market => (
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
className={styles.buttonMain}
|
||||||
|
// weird 0 hack so TypeScript is happy
|
||||||
|
onClick={() =>
|
||||||
|
this.purchaseAsset(ddo, index || 0)
|
||||||
|
}
|
||||||
|
disabled={!isLogged || !market.networkMatch}
|
||||||
|
name="Download"
|
||||||
|
>
|
||||||
|
Get file
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Market.Consumer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error !== '' && <div className={styles.error}>{error}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../styles/variables';
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
.files {
|
.files {
|
||||||
text-align: center;
|
text-align: center;
|
@ -1,11 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
import { DDO } from '@oceanprotocol/squid'
|
import { DDO } from '@oceanprotocol/squid'
|
||||||
import AssetFilesDetails from './AssetFilesDetails'
|
import AssetFilesDetails from './AssetFilesDetails'
|
||||||
import { User } from '../../context'
|
|
||||||
import { userMockConnected } from '../../../__mocks__/user-mock'
|
|
||||||
|
|
||||||
describe('AssetFilesDetails', () => {
|
describe('AssetFilesDetails', () => {
|
||||||
it('renders without crashing', () => {
|
it('renders without crashing', () => {
|
||||||
@ -28,16 +26,4 @@ describe('AssetFilesDetails', () => {
|
|||||||
)
|
)
|
||||||
expect(container.firstChild).toHaveTextContent('No files attached.')
|
expect(container.firstChild).toHaveTextContent('No files attached.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('hides Web3message when all connected', () => {
|
|
||||||
const { container } = render(
|
|
||||||
<User.Provider value={userMockConnected}>
|
|
||||||
<AssetFilesDetails
|
|
||||||
files={[{ index: 0, url: '' }]}
|
|
||||||
ddo={({} as any) as DDO}
|
|
||||||
/>
|
|
||||||
</User.Provider>
|
|
||||||
)
|
|
||||||
expect(container.querySelector('.status')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
})
|
@ -1,8 +1,8 @@
|
|||||||
import React, { PureComponent } from 'react'
|
import React, { PureComponent } from 'react'
|
||||||
import { DDO, File } from '@oceanprotocol/squid'
|
import { DDO, File } from '@oceanprotocol/squid'
|
||||||
import AssetFile from './AssetFile'
|
import AssetFile from './AssetFile'
|
||||||
import { User } from '../../context'
|
import { User } from '../../../context'
|
||||||
import Web3message from '../../components/organisms/Web3message'
|
import Web3message from '../../organisms/Web3message'
|
||||||
import styles from './AssetFilesDetails.module.scss'
|
import styles from './AssetFilesDetails.module.scss'
|
||||||
|
|
||||||
export default class AssetFilesDetails extends PureComponent<{
|
export default class AssetFilesDetails extends PureComponent<{
|
||||||
@ -19,9 +19,7 @@ export default class AssetFilesDetails extends PureComponent<{
|
|||||||
<AssetFile key={file.index} ddo={ddo} file={file} />
|
<AssetFile key={file.index} ddo={ddo} file={file} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{(!this.context.isOceanNetwork || !this.context.isLogged) && (
|
<Web3message />
|
||||||
<Web3message />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div>No files attached.</div>
|
<div>No files attached.</div>
|
49
client/src/components/templates/Asset/Report.module.scss
Normal file
49
client/src/components/templates/Asset/Report.module.scss
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openLink {
|
||||||
|
margin: 0;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
background: $brand-white;
|
||||||
|
padding: $spacer;
|
||||||
|
border: 1px solid $brand-grey-lighter;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: $font-size-base;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: $spacer / 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
border-bottom: 1px solid $brand-grey-lighter;
|
||||||
|
padding-bottom: $spacer / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: $red;
|
||||||
|
padding: $spacer / 2;
|
||||||
|
text-align: center;
|
||||||
|
color: $brand-white;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
composes: error;
|
||||||
|
background: $green;
|
||||||
|
}
|
42
client/src/components/templates/Asset/Report.test.tsx
Normal file
42
client/src/components/templates/Asset/Report.test.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, fireEvent, wait } from '@testing-library/react'
|
||||||
|
import ReactModal from 'react-modal'
|
||||||
|
import mockAxios from 'jest-mock-axios'
|
||||||
|
import Report from './Report'
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockAxios.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
data: { status: 'success' }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Report', () => {
|
||||||
|
it('renders without crashing', async () => {
|
||||||
|
ReactModal.setAppElement(document.createElement('div'))
|
||||||
|
|
||||||
|
const { getByText, getByLabelText, getByTestId } = render(
|
||||||
|
<Report did="did:xxx" title="Hello" />
|
||||||
|
)
|
||||||
|
// Renders button by default
|
||||||
|
expect(getByText('Report Data Set')).toBeInTheDocument()
|
||||||
|
|
||||||
|
// open modal
|
||||||
|
fireEvent.click(getByText('Report Data Set'))
|
||||||
|
await wait(() => expect(getByText('did:xxx')).toBeInTheDocument())
|
||||||
|
|
||||||
|
// add comment
|
||||||
|
const comment = getByLabelText('Comment')
|
||||||
|
fireEvent.change(comment, {
|
||||||
|
target: { value: 'Plants' }
|
||||||
|
})
|
||||||
|
expect(comment).toHaveTextContent('Plants')
|
||||||
|
fireEvent.click(getByTestId('report'))
|
||||||
|
mockAxios.mockResponse(mockResponse)
|
||||||
|
// expect(mockAxios.post).toHaveBeenCalled()
|
||||||
|
|
||||||
|
// close modal
|
||||||
|
fireEvent.click(getByTestId('closeModal'))
|
||||||
|
})
|
||||||
|
})
|
156
client/src/components/templates/Asset/Report.tsx
Normal file
156
client/src/components/templates/Asset/Report.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import React, { PureComponent, ChangeEvent } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Logger } from '@oceanprotocol/squid'
|
||||||
|
import Modal from '../../atoms/Modal'
|
||||||
|
import styles from './Report.module.scss'
|
||||||
|
import Button from '../../atoms/Button'
|
||||||
|
import Input from '../../atoms/Form/Input'
|
||||||
|
import Form from '../../atoms/Form/Form'
|
||||||
|
import { serviceUri } from '../../../config'
|
||||||
|
import Spinner from '../../atoms/Spinner'
|
||||||
|
|
||||||
|
export default class Report extends PureComponent<
|
||||||
|
{ did: string; title: string },
|
||||||
|
{
|
||||||
|
isModalOpen: boolean
|
||||||
|
comment: string
|
||||||
|
message: string
|
||||||
|
isSending: boolean
|
||||||
|
hasError?: boolean
|
||||||
|
hasSuccess?: boolean
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
public state = {
|
||||||
|
isModalOpen: false,
|
||||||
|
comment: '',
|
||||||
|
message: 'Sending...',
|
||||||
|
isSending: false,
|
||||||
|
hasError: false,
|
||||||
|
hasSuccess: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// for canceling axios requests
|
||||||
|
public signal = axios.CancelToken.source()
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.signal.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({
|
||||||
|
comment: event.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleModal = () => {
|
||||||
|
this.setState({ isModalOpen: !this.state.isModalOpen })
|
||||||
|
this.state.isModalOpen && this.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
private reset() {
|
||||||
|
this.setState({
|
||||||
|
comment: '',
|
||||||
|
message: 'Sending...',
|
||||||
|
isSending: false,
|
||||||
|
hasError: false,
|
||||||
|
hasSuccess: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendEmail = async (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
this.setState({ isSending: true })
|
||||||
|
|
||||||
|
const msg = {
|
||||||
|
to: process.env.REACT_APP_REPORT_EMAIL,
|
||||||
|
from: 'info@oceanprotocol.com',
|
||||||
|
subject: `[Report] ${this.props.title}`,
|
||||||
|
html: `<p>The following data set was reported:</p><p><strong>${this.props.title}</strong><br /><a style="color:#ff4092;text-decoration:none" href="https://commons.oceanprotocol.com/asset/${this.props.did}"><code>${this.props.did}</code></a></p><blockquote><em>${this.state.comment}</em></blockquote>`
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
url: `${serviceUri}/api/v1/report`,
|
||||||
|
data: { msg },
|
||||||
|
cancelToken: this.signal.token
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isSending: false,
|
||||||
|
hasSuccess: true,
|
||||||
|
message: 'Thanks for the report! We will take a look soon.'
|
||||||
|
})
|
||||||
|
return response.data.result
|
||||||
|
} catch (error) {
|
||||||
|
!axios.isCancel(error) &&
|
||||||
|
this.setState({
|
||||||
|
message: error.message,
|
||||||
|
isSending: false,
|
||||||
|
hasError: true
|
||||||
|
}) &&
|
||||||
|
Logger.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Button
|
||||||
|
link
|
||||||
|
className={styles.openLink}
|
||||||
|
onClick={this.toggleModal}
|
||||||
|
>
|
||||||
|
Report Data Set
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
title="Report Data Set"
|
||||||
|
description="Found some faulty metadata, wrongly attributed data, or anything else wrong with this data set? Tell us about it and we will take a look."
|
||||||
|
isOpen={this.state.isModalOpen}
|
||||||
|
toggleModal={this.toggleModal}
|
||||||
|
>
|
||||||
|
<div className={styles.info}>
|
||||||
|
<h3>{this.props.title}</h3>
|
||||||
|
<p>
|
||||||
|
<code>{this.props.did}</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{this.state.isSending ? (
|
||||||
|
<Spinner message={this.state.message} />
|
||||||
|
) : this.state.hasError ? (
|
||||||
|
<div className={styles.error}>
|
||||||
|
{this.state.message}
|
||||||
|
</div>
|
||||||
|
) : this.state.hasSuccess ? (
|
||||||
|
<div className={styles.success}>
|
||||||
|
{this.state.message}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Form minimal>
|
||||||
|
<Input
|
||||||
|
type="textarea"
|
||||||
|
name="comment"
|
||||||
|
label="Comment"
|
||||||
|
help="Briefly describe what is wrong with this asset. If you want to get contacted by us, add your email at the end."
|
||||||
|
required
|
||||||
|
value={this.state.comment}
|
||||||
|
onChange={this.inputChange}
|
||||||
|
rows={1}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
onClick={(e: Event) => this.sendEmail(e)}
|
||||||
|
disabled={this.state.comment === ''}
|
||||||
|
data-testid="report"
|
||||||
|
>
|
||||||
|
Report Data Set
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
12
client/src/components/templates/Asset/index.module.scss
Normal file
12
client/src/components/templates/Asset/index.module.scss
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.error {
|
||||||
|
text-align: center;
|
||||||
|
margin: 20vh auto 0 auto;
|
||||||
|
background: $red;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
padding: $spacer / 2;
|
||||||
|
width: fit-content;
|
||||||
|
color: $brand-white;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
}
|
@ -1,18 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from 'react-testing-library'
|
import { render } from '@testing-library/react'
|
||||||
|
import { createMemoryHistory, createLocation } from 'history'
|
||||||
import Details from './index'
|
import Details from './index'
|
||||||
|
|
||||||
|
const history = createMemoryHistory()
|
||||||
|
const location = createLocation('/asset/did:xxx')
|
||||||
|
|
||||||
describe('Details', () => {
|
describe('Details', () => {
|
||||||
it('renders loading state by default', () => {
|
it('renders loading state by default', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<Details
|
<Details
|
||||||
location={{
|
history={history}
|
||||||
search: '',
|
location={location}
|
||||||
pathname: '/',
|
match={{ params: '', path: '', url: '', isExact: true }}
|
||||||
state: '',
|
|
||||||
hash: ''
|
|
||||||
}}
|
|
||||||
match={{ params: { did: '' } }}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
expect(container.firstChild).toBeInTheDocument()
|
expect(container.firstChild).toBeInTheDocument()
|
85
client/src/components/templates/Asset/index.tsx
Normal file
85
client/src/components/templates/Asset/index.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import { DDO, MetaData, Logger } from '@oceanprotocol/squid'
|
||||||
|
import Route from '../Route'
|
||||||
|
import Spinner from '../../atoms/Spinner'
|
||||||
|
import { User } from '../../../context'
|
||||||
|
import AssetDetails from './AssetDetails'
|
||||||
|
import stylesApp from '../../../App.module.scss'
|
||||||
|
import Content from '../../atoms/Content'
|
||||||
|
import CategoryImage from '../../atoms/CategoryImage'
|
||||||
|
import styles from './index.module.scss'
|
||||||
|
import withTracker from '../../../hoc/withTracker'
|
||||||
|
|
||||||
|
interface AssetProps {
|
||||||
|
match: {
|
||||||
|
params: {
|
||||||
|
did: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AssetState {
|
||||||
|
ddo: DDO
|
||||||
|
metadata: MetaData
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class Asset extends Component<AssetProps, AssetState> {
|
||||||
|
public static contextType = User
|
||||||
|
|
||||||
|
public state = {
|
||||||
|
ddo: ({} as any) as DDO,
|
||||||
|
metadata: ({ base: { name: '' } } as any) as MetaData,
|
||||||
|
error: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
public async componentDidMount() {
|
||||||
|
this.getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getData() {
|
||||||
|
try {
|
||||||
|
const { ocean } = this.context
|
||||||
|
const ddo = await ocean.assets.resolve(this.props.match.params.did)
|
||||||
|
const { metadata } = ddo.findServiceByType('Metadata')
|
||||||
|
this.setState({ ddo, metadata })
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error.message)
|
||||||
|
this.setState({
|
||||||
|
error: `We encountered an error: ${error.message}.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { metadata, ddo, error } = this.state
|
||||||
|
const isLoading = metadata.base.name === ''
|
||||||
|
|
||||||
|
return isLoading ? (
|
||||||
|
<div className={stylesApp.loader}>
|
||||||
|
<Spinner message={'Loading asset...'} />
|
||||||
|
</div>
|
||||||
|
) : error !== '' ? (
|
||||||
|
<div className={styles.error}>{error}</div>
|
||||||
|
) : (
|
||||||
|
<Route
|
||||||
|
title={metadata.base.name}
|
||||||
|
image={
|
||||||
|
metadata.base.categories && (
|
||||||
|
<CategoryImage
|
||||||
|
header
|
||||||
|
dimmed
|
||||||
|
category={metadata.base.categories[0]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Content>
|
||||||
|
<AssetDetails metadata={metadata} ddo={ddo} />
|
||||||
|
</Content>
|
||||||
|
</Route>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTracker(Asset)
|
21
client/src/components/templates/Channel.module.scss
Normal file
21
client/src/components/templates/Channel.module.scss
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.results {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-gap: $spacer;
|
||||||
|
max-width: calc(18rem + #{$spacer * 2});
|
||||||
|
margin: auto;
|
||||||
|
margin-top: $spacer * 2;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
max-width: none;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $break-point--medium) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
27
client/src/components/templates/Channel.test.tsx
Normal file
27
client/src/components/templates/Channel.test.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import Channel from './Channel'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import { createMemoryHistory } from 'history'
|
||||||
|
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||||
|
import { MemoryRouter } from 'react-router'
|
||||||
|
|
||||||
|
describe('Channel', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const history = createMemoryHistory()
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<Channel
|
||||||
|
match={{
|
||||||
|
params: { channel: 'ai-for-good' }
|
||||||
|
}}
|
||||||
|
history={history}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user