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",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier/@typescript-eslint"
|
||||
"prettier/@typescript-eslint",
|
||||
"plugin:cypress/recommended"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"plugins": ["@typescript-eslint", "prettier", "cypress"],
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"error",
|
||||
{ "multiline": { "delimiter": "none" } }
|
||||
]
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"jest": true
|
||||
"jest": true,
|
||||
"cypress/globals": true
|
||||
}
|
||||
}
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -12,6 +12,7 @@ dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
@ -20,3 +21,7 @@ dist
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# cypress
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
|
@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
coverage
|
||||
|
31
.travis.yml
31
.travis.yml
@ -1,24 +1,49 @@
|
||||
dist: xenial
|
||||
sudo: required
|
||||
language: node_js
|
||||
node_js:
|
||||
- '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:
|
||||
- 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:
|
||||
# - ./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
|
||||
- npm run test:e2e || travis_terminate 1
|
||||
- ./scripts/build.sh
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
cache:
|
||||
npm: true
|
||||
directories:
|
||||
- node_modules
|
||||
# cache folder with Cypress binary
|
||||
- ~/.cache
|
||||
|
||||
deploy:
|
||||
- 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).
|
||||
|
||||
#### [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)
|
||||
|
||||
> 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.
|
||||
|
||||
- [🏄 Get Started](#-get-started)
|
||||
- [🏖 Remote Ocean: Nile](#-remote-ocean-nile)
|
||||
- [🐳 Use with Barge](#-use-with-barge)
|
||||
- [👩🔬 Testing](#-testing)
|
||||
- [✨ Code Style](#-code-style)
|
||||
- [🛳 Production](#-production)
|
||||
- [⬆️ Releases](#️-releases)
|
||||
- [🎁 Contributing](#-contributing)
|
||||
- [🏛 License](#-license)
|
||||
- [🏄 Get Started](#-Get-Started)
|
||||
- [🏖 Remote Ocean: Pacific](#-Remote-Ocean-Pacific)
|
||||
- [🐳 Use with Barge](#-Use-with-Barge)
|
||||
- [⛵️ Environment Variables](#️-Environment-Variables)
|
||||
- [Client](#Client)
|
||||
- [Server](#Server)
|
||||
- [👩🔬 Testing](#-Testing)
|
||||
- [Unit Tests](#Unit-Tests)
|
||||
- [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
|
||||
|
||||
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)
|
||||
- **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:
|
||||
|
||||
@ -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`.
|
||||
|
||||
### 🏖 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
|
||||
2. under New Network, enter `https://nile.dev-ocean.com` as the custom RPC URL
|
||||
3. Hit _Save_, and you’re now connected to Nile
|
||||
2. under New Network, enter `https://pacific.oceanprotocol.com` as the custom RPC URL
|
||||
3. Hit _Save_, and you’re now connected to Pacific
|
||||
|
||||
### 🐳 Use with Barge
|
||||
|
||||
@ -69,18 +77,50 @@ cd barge
|
||||
./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
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
```bash
|
||||
@ -97,6 +137,22 @@ cd server/
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- updates CHANGELOG.md file with commit messages
|
||||
- commits and pushes everything
|
||||
- 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:
|
||||
|
||||
- To bump a patch version: `npm run release`
|
||||
- To bump a minor version: `npm run release-minor`
|
||||
- To bump a major version: `npm run release-major`
|
||||
- To bump a minor version: `npm run release minor`
|
||||
- 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)
|
||||
|
||||
## 📜 Changelog
|
||||
|
||||
See the [CHANGELOG.md](./CHANGELOG.md) file. This file is auto-generated during the above mentioned release process.
|
||||
|
||||
## 🎁 Contributing
|
||||
|
||||
See the page titled "[Ways to Contribute](https://docs.oceanprotocol.com/concepts/contributing/)" in the Ocean Protocol documentation.
|
||||
|
@ -1 +1,2 @@
|
||||
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 = {
|
||||
isLogged: false,
|
||||
isLoading: false,
|
||||
isWeb3: false,
|
||||
isOceanNetwork: false,
|
||||
isBurner: false,
|
||||
isWeb3Capable: false,
|
||||
account: '',
|
||||
web3: {},
|
||||
ocean: {},
|
||||
...oceanMock,
|
||||
balance: { eth: 0, ocn: 0 },
|
||||
network: '',
|
||||
requestFromFaucet: jest.fn(),
|
||||
unlockAccounts: jest.fn(),
|
||||
loginMetamask: jest.fn(),
|
||||
loginBurnerWallet: jest.fn(),
|
||||
message: ''
|
||||
}
|
||||
|
||||
const userMockConnected = {
|
||||
isLogged: true,
|
||||
isLoading: false,
|
||||
isWeb3: true,
|
||||
isOceanNetwork: true,
|
||||
isBurner: false,
|
||||
isWeb3Capable: true,
|
||||
account: '0xxxxxx',
|
||||
web3: {},
|
||||
ocean: {},
|
||||
...oceanMock,
|
||||
balance: { eth: 0, ocn: 0 },
|
||||
network: '',
|
||||
requestFromFaucet: jest.fn(),
|
||||
unlockAccounts: jest.fn(),
|
||||
loginMetamask: jest.fn(),
|
||||
loginBurnerWallet: jest.fn(),
|
||||
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",
|
||||
"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",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@ -13,50 +13,57 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@oceanprotocol/art": "^2.2.0",
|
||||
"@oceanprotocol/squid": "^0.5.10",
|
||||
"@oceanprotocol/squid": "^0.6.4",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@sindresorhus/slugify": "^0.9.1",
|
||||
"axios": "^0.19.0",
|
||||
"bip39": "^3.0.2",
|
||||
"classnames": "^2.2.6",
|
||||
"ethereum-blockies": "MyEtherWallet/blockies",
|
||||
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
||||
"filesize": "^4.1.2",
|
||||
"history": "^4.9.0",
|
||||
"is-url": "^1.2.4",
|
||||
"moment": "^2.24.0",
|
||||
"query-string": "^6.5.0",
|
||||
"query-string": "^6.8.1",
|
||||
"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-dotdotdot": "^1.3.0",
|
||||
"react-ga": "^2.5.7",
|
||||
"react-ga": "^2.6.0",
|
||||
"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-paginate": "^6.3.0",
|
||||
"react-popper": "^1.3.3",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-transition-group": "^4.0.0",
|
||||
"slugify": "^1.3.4",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-transition-group": "^4.1.1",
|
||||
"truffle-hdwallet-provider": "^1.0.13",
|
||||
"web3": "1.0.0-beta.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-mock/state": "^0.1.8",
|
||||
"@testing-library/react": "^8.0.4",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/filesize": "^4.1.0",
|
||||
"@types/is-url": "^1.2.28",
|
||||
"@types/jest": "^24.0.12",
|
||||
"@types/react": "^16.8.15",
|
||||
"@types/jest": "^24.0.15",
|
||||
"@types/react": "^16.8.22",
|
||||
"@types/react-datepicker": "^2.3.0",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/react-dotdotdot": "^1.2.0",
|
||||
"@types/react-helmet": "^5.0.8",
|
||||
"@types/react-modal": "^3.8.2",
|
||||
"@types/react-paginate": "^6.2.1",
|
||||
"@types/react-router-dom": "^4.3.2",
|
||||
"@types/react-transition-group": "^2.9.1",
|
||||
"@types/web3": "^1.0.18",
|
||||
"jest-dom": "^3.1.4",
|
||||
"@types/react-router-dom": "^4.3.4",
|
||||
"@types/react-transition-group": "^2.9.2",
|
||||
"@types/web3": "^1.0.19",
|
||||
"jest-dom": "^3.5.0",
|
||||
"jest-mock-axios": "^3.0.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"react-scripts": "^3.0.0",
|
||||
"react-testing-library": "^7.0.0",
|
||||
"typescript": "^3.4.5"
|
||||
"typescript": "3.4.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -71,7 +78,8 @@
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"!src/serviceWorker.ts"
|
||||
"!src/serviceWorker.ts",
|
||||
"!src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -12,39 +12,6 @@
|
||||
|
||||
<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>
|
||||
.loader {
|
||||
display: block;
|
||||
|
@ -21,5 +21,5 @@
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"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 { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import App from './App'
|
||||
import { User } from './context'
|
||||
import { userMock } from '../__mocks__/user-mock'
|
||||
import { userMock, userMockConnected } from '../__mocks__/user-mock'
|
||||
|
||||
describe('App', () => {
|
||||
it('should be able to run tests', () => {
|
||||
@ -10,7 +10,11 @@ describe('App', () => {
|
||||
})
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(<App />)
|
||||
const { container } = render(
|
||||
<User.Provider value={userMockConnected}>
|
||||
<App />
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
@ -1,14 +1,18 @@
|
||||
import React from 'react'
|
||||
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 { User } from './context'
|
||||
import { userMockConnected } from '../__mocks__/user-mock'
|
||||
|
||||
describe('Routes', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(
|
||||
<Router>
|
||||
<Routes />
|
||||
</Router>
|
||||
<User.Provider value={userMockConnected}>
|
||||
<Router>
|
||||
<Routes />
|
||||
</Router>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
@ -1,28 +1,32 @@
|
||||
import React from 'react'
|
||||
import { Route, Switch } from 'react-router-dom'
|
||||
import withTracker from './hoc/withTracker'
|
||||
|
||||
import About from './routes/About'
|
||||
import Details from './routes/Details/'
|
||||
import Home from './routes/Home'
|
||||
import NotFound from './routes/NotFound'
|
||||
import Publish from './routes/Publish/'
|
||||
import Search from './routes/Search'
|
||||
import Faucet from './routes/Faucet'
|
||||
import History from './routes/History'
|
||||
import Channels from './routes/Channels'
|
||||
import Styleguide from './routes/Styleguide'
|
||||
|
||||
import Asset from './components/templates/Asset'
|
||||
import Channel from './components/templates/Channel'
|
||||
|
||||
const Routes = () => (
|
||||
<Switch>
|
||||
<Route exact component={withTracker(Home)} path="/" />
|
||||
<Route component={withTracker(Styleguide)} path="/styleguide" />
|
||||
<Route component={withTracker(About)} path="/about" />
|
||||
<Route component={withTracker(Publish)} path="/publish" />
|
||||
<Route component={withTracker(Search)} path="/search" />
|
||||
<Route component={withTracker(Details)} path="/asset/:did" />
|
||||
<Route component={withTracker(Faucet)} path="/faucet" />
|
||||
<Route component={withTracker(History)} path="/history" />
|
||||
<Route component={withTracker(NotFound)} />
|
||||
<Route component={Home} exact path="/" />
|
||||
<Route component={Styleguide} path="/styleguide" />
|
||||
<Route component={About} path="/about" />
|
||||
<Route component={Publish} path="/publish" />
|
||||
<Route component={Search} path="/search" />
|
||||
<Route component={Asset} path="/asset/:did" />
|
||||
<Route component={Faucet} path="/faucet" />
|
||||
<Route component={History} path="/history" />
|
||||
<Route component={Channels} exact path="/channels" />
|
||||
<Route component={Channel} path="/channels/:channel" />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
)
|
||||
|
||||
|
@ -2,18 +2,82 @@
|
||||
|
||||
.account {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
|
||||
> div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-small;
|
||||
> div:first-of-type {
|
||||
flex: 0 0 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
@ -21,4 +85,5 @@
|
||||
display: inline-block;
|
||||
margin-right: $spacer / 3;
|
||||
margin-left: 0;
|
||||
border: 1px solid $brand-grey-lighter;
|
||||
}
|
||||
|
@ -1,28 +1,61 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-testing-library'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import { toDataUrl } from 'ethereum-blockies'
|
||||
import Account from './Account'
|
||||
import { User } from '../../context'
|
||||
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||
|
||||
describe('Account', () => {
|
||||
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()
|
||||
})
|
||||
|
||||
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')
|
||||
fireEvent.click(getByText('Unlock Account'))
|
||||
})
|
||||
|
||||
it('outputs blockie img', () => {
|
||||
const account = '0xxxxxxxxxxxxxxx'
|
||||
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')).toHaveAttribute(
|
||||
'src',
|
||||
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 { toDataUrl } from 'ethereum-blockies'
|
||||
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 }) => {
|
||||
const blockies = account && toDataUrl(account)
|
||||
export default class Account extends PureComponent<
|
||||
{},
|
||||
{ isAccountInfoOpen: boolean }
|
||||
> {
|
||||
public static contextType = User
|
||||
|
||||
return account && blockies ? (
|
||||
<div className={styles.account}>
|
||||
<img className={styles.blockies} src={blockies} alt="Blockies" />
|
||||
<Dotdotdot clamp={1}>{account}</Dotdotdot>
|
||||
</div>
|
||||
) : (
|
||||
<em>No account selected</em>
|
||||
)
|
||||
public state = {
|
||||
isAccountInfoOpen: false
|
||||
}
|
||||
|
||||
private toggleAccountInfo() {
|
||||
this.setState({ isAccountInfoOpen: !this.state.isAccountInfoOpen })
|
||||
}
|
||||
|
||||
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 { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import { BrowserRouter as Router } from 'react-router-dom'
|
||||
import Button from './Button'
|
||||
|
||||
|
@ -12,6 +12,7 @@ interface ButtonProps {
|
||||
onClick?: any
|
||||
disabled?: boolean
|
||||
to?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export default class Button extends PureComponent<ButtonProps, any> {
|
||||
|
@ -2,8 +2,24 @@
|
||||
|
||||
.categoryImage {
|
||||
height: 4rem;
|
||||
background-size: cover;
|
||||
background-size: 100%;
|
||||
background-position: center;
|
||||
margin-bottom: $spacer / $line-height;
|
||||
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 { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import CategoryImage from './CategoryImage'
|
||||
import formPublish from '../../data/form-publish.json'
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import cx from 'classnames'
|
||||
import styles from './CategoryImage.module.scss'
|
||||
|
||||
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 urbanplanning from '../../img/categories/urbanplanning.jpg'
|
||||
import visualart from '../../img/categories/visualart.jpg'
|
||||
import aiforgood from '../../img/aiforgood.jpg'
|
||||
import fallback from '@oceanprotocol/art/jellyfish/jellyfish-back.svg'
|
||||
|
||||
const categoryImageFile = (category: string) => {
|
||||
@ -95,6 +97,8 @@ const categoryImageFile = (category: string) => {
|
||||
case 'mathematics':
|
||||
return mathematics
|
||||
case 'Medicine':
|
||||
case 'Health & Medicine':
|
||||
case 'Health':
|
||||
case 'medicine':
|
||||
return medicine
|
||||
case 'Other':
|
||||
@ -133,23 +137,31 @@ const categoryImageFile = (category: string) => {
|
||||
case 'Visual Arts & Design':
|
||||
case 'visualart':
|
||||
return visualart
|
||||
// technically no category
|
||||
// but corresponding to title of a channel
|
||||
case 'AI For Good':
|
||||
return dataofdata
|
||||
return aiforgood
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
export default class CategoryImage extends PureComponent<{ category: string }> {
|
||||
export default class CategoryImage extends PureComponent<{
|
||||
category: string
|
||||
header?: boolean
|
||||
dimmed?: boolean
|
||||
}> {
|
||||
public render() {
|
||||
const image = categoryImageFile(this.props.category)
|
||||
const classNames = cx(styles.categoryImage, {
|
||||
[styles.header]: this.props.header,
|
||||
[styles.dimmed]: this.props.dimmed
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.categoryImage}
|
||||
style={{
|
||||
backgroundImage: `url(${image})`
|
||||
}}
|
||||
className={classNames}
|
||||
style={{ 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 { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import Form from './Form'
|
||||
|
||||
describe('Form', () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import Input from './Input'
|
||||
|
||||
describe('Input', () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { PureComponent, FormEvent, ChangeEvent } from 'react'
|
||||
import slugify from 'slugify'
|
||||
import slugify from '@sindresorhus/slugify'
|
||||
import DatePicker from 'react-datepicker'
|
||||
import cx from 'classnames'
|
||||
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}>
|
||||
<input
|
||||
className={styles.radio}
|
||||
id={slugify(option, {
|
||||
lower: true
|
||||
})}
|
||||
id={slugify(option)}
|
||||
type={type}
|
||||
name={name}
|
||||
value={slugify(option, {
|
||||
lower: true
|
||||
})}
|
||||
disabled={disabled}
|
||||
// value={slugify(option, {
|
||||
// lower: true
|
||||
// })}
|
||||
// disabled={disabled}
|
||||
value={slugify(option)}
|
||||
/>
|
||||
<label
|
||||
className={styles.radioLabel}
|
||||
htmlFor={slugify(option, {
|
||||
lower: true
|
||||
})}
|
||||
htmlFor={slugify(option)}
|
||||
>
|
||||
{option}
|
||||
</label>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import InputGroup from './InputGroup'
|
||||
|
||||
describe('InputGroup', () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import Label from './Label'
|
||||
|
||||
describe('Label', () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import Row from './Row'
|
||||
|
||||
describe('Row', () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import Markdown from './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;
|
||||
margin-top: $spacer * $line-height;
|
||||
margin-bottom: $spacer / 2;
|
||||
line-height: 1.3;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
@ -25,6 +26,21 @@
|
||||
|
||||
.spinnerMessage {
|
||||
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 {
|
||||
|
@ -1,10 +1,27 @@
|
||||
import React from 'react'
|
||||
import styles from './Spinner.module.scss'
|
||||
|
||||
const Spinner = ({ message }: { message?: string }) => (
|
||||
<div className={styles.spinner}>
|
||||
{message && <div className={styles.spinnerMessage}>{message}</div>}
|
||||
</div>
|
||||
)
|
||||
const Spinner = ({
|
||||
message,
|
||||
small,
|
||||
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
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import cx from 'classnames'
|
||||
import { User } from '../../../context'
|
||||
import { User, Market } from '../../../context'
|
||||
import styles from './Indicator.module.scss'
|
||||
|
||||
const Indicator = ({
|
||||
@ -19,15 +19,19 @@ const Indicator = ({
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<User.Consumer>
|
||||
{states =>
|
||||
!states.isWeb3 ? (
|
||||
<span className={styles.statusIndicator} />
|
||||
) : !states.isLogged || !states.isOceanNetwork ? (
|
||||
<span className={styles.statusIndicatorCloseEnough} />
|
||||
) : states.isLogged ? (
|
||||
<span className={styles.statusIndicatorActive} />
|
||||
) : null
|
||||
}
|
||||
{user => (
|
||||
<Market.Consumer>
|
||||
{market =>
|
||||
!user.isLogged || !market.networkMatch ? (
|
||||
<span
|
||||
className={styles.statusIndicatorCloseEnough}
|
||||
/>
|
||||
) : user.isLogged ? (
|
||||
<span className={styles.statusIndicatorActive} />
|
||||
) : null
|
||||
}
|
||||
</Market.Consumer>
|
||||
)}
|
||||
</User.Consumer>
|
||||
</div>
|
||||
)
|
||||
|
@ -40,9 +40,15 @@ $popoverWidth: 18rem;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* stylelint-disable */
|
||||
button {
|
||||
font-size: $font-size-small;
|
||||
svg,
|
||||
&[data-action] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-enable */
|
||||
}
|
||||
|
||||
.balance {
|
||||
@ -50,6 +56,10 @@ $popoverWidth: 18rem;
|
||||
margin-left: $spacer / 2;
|
||||
white-space: nowrap;
|
||||
|
||||
strong {
|
||||
color: $brand-grey-lighter;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import Popover from './Popover'
|
||||
import { userMock, userMockConnected } from '../../../../__mocks__/user-mock'
|
||||
import { User } from '../../../context'
|
||||
import { marketMock } from '../../../../__mocks__/market-mock'
|
||||
import { User, Market } from '../../../context'
|
||||
|
||||
describe('Popover', () => {
|
||||
it('renders without crashing', () => {
|
||||
@ -25,12 +26,14 @@ describe('Popover', () => {
|
||||
|
||||
it('renders correct network', () => {
|
||||
const { container } = render(
|
||||
<User.Provider value={{ ...userMockConnected, network: 'Nile' }}>
|
||||
<Popover forwardedRef={() => null} style={{}} />
|
||||
<User.Provider value={{ ...userMockConnected, network: 'Pacific' }}>
|
||||
<Market.Provider value={{ ...marketMock }}>
|
||||
<Popover forwardedRef={() => null} style={{}} />
|
||||
</Market.Provider>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
expect(container.firstChild).toHaveTextContent('Connected to Nile')
|
||||
expect(container.firstChild).toHaveTextContent('Connected to Pacific')
|
||||
})
|
||||
|
||||
it('renders with wrong network', () => {
|
||||
@ -38,7 +41,6 @@ describe('Popover', () => {
|
||||
<User.Provider
|
||||
value={{
|
||||
...userMockConnected,
|
||||
isOceanNetwork: false,
|
||||
network: '1'
|
||||
}}
|
||||
>
|
||||
|
@ -1,20 +1,16 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import Account from '../../atoms/Account'
|
||||
import { User } from '../../../context'
|
||||
import { User, Market } from '../../../context'
|
||||
import styles from './Popover.module.scss'
|
||||
|
||||
export default class Popover extends PureComponent<{
|
||||
forwardedRef: (ref: HTMLElement | null) => void
|
||||
style: React.CSSProperties
|
||||
forwardedRef?: (ref: HTMLElement | null) => void
|
||||
style?: React.CSSProperties
|
||||
}> {
|
||||
public static contextType = User
|
||||
|
||||
public render() {
|
||||
const {
|
||||
account,
|
||||
balance,
|
||||
network,
|
||||
isWeb3,
|
||||
isOceanNetwork
|
||||
} = this.context
|
||||
const { account, balance, network } = this.context
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -22,15 +18,10 @@ export default class Popover extends PureComponent<{
|
||||
ref={this.props.forwardedRef}
|
||||
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}>
|
||||
<Account account={account} />
|
||||
<Account />
|
||||
</div>
|
||||
|
||||
{account && balance && (
|
||||
@ -52,16 +43,28 @@ export default class Popover extends PureComponent<{
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.popoverInfoline}>
|
||||
{network && !isOceanNetwork
|
||||
? 'Please connect to Custom RPC\n https://nile.dev-ocean.com'
|
||||
: network && `Connected to ${network} network`}
|
||||
</div>
|
||||
<Market.Consumer>
|
||||
{market => (
|
||||
<div className={styles.popoverInfoline}>
|
||||
{network && !market.networkMatch
|
||||
? `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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Popover.contextType = User
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent } from 'react-testing-library'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import AccountStatus from '.'
|
||||
|
||||
describe('AccountStatus', () => {
|
||||
@ -10,9 +10,7 @@ describe('AccountStatus', () => {
|
||||
|
||||
it('togglePopover fires', () => {
|
||||
const { container } = render(<AccountStatus />)
|
||||
|
||||
const indicator = container.querySelector('.statusIndicator')
|
||||
|
||||
const indicator = container.querySelector('.status')
|
||||
indicator && fireEvent.mouseOver(indicator)
|
||||
expect(container.querySelector('.popover')).toBeInTheDocument()
|
||||
indicator && fireEvent.mouseOut(indicator)
|
||||
|
@ -20,6 +20,12 @@
|
||||
color: inherit;
|
||||
border-color: $brand-pink;
|
||||
transform: none;
|
||||
|
||||
// category image
|
||||
> div:first-child {
|
||||
opacity: 1;
|
||||
background-size: 105%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +35,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.minimal {
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.assetList {
|
||||
> a {
|
||||
color: $brand-grey-dark;
|
@ -2,10 +2,19 @@ import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import moment from 'moment'
|
||||
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'
|
||||
|
||||
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 { base } = metadata
|
||||
|
||||
@ -22,17 +31,22 @@ const AssetLink = ({ asset, list }: { asset: any; list?: boolean }) => {
|
||||
</Link>
|
||||
</article>
|
||||
) : (
|
||||
<article className={styles.asset}>
|
||||
<article
|
||||
className={
|
||||
minimal ? cx(styles.asset, styles.minimal) : styles.asset
|
||||
}
|
||||
>
|
||||
<Link to={`/asset/${asset.id}`}>
|
||||
{base.categories && (
|
||||
<CategoryImage category={base.categories[0]} />
|
||||
{base.categories && !minimal && (
|
||||
<CategoryImage dimmed category={base.categories[0]} />
|
||||
)}
|
||||
<h1>{base.name}</h1>
|
||||
|
||||
<div className={styles.description}>
|
||||
<Dotdotdot clamp={3}>{base.description}</Dotdotdot>
|
||||
</div>
|
||||
|
||||
{!minimal && (
|
||||
<div className={styles.description}>
|
||||
<Dotdotdot clamp={3}>{base.description}</Dotdotdot>
|
||||
</div>
|
||||
)}
|
||||
<footer className={styles.assetFooter}>
|
||||
{base.categories && <div>{base.categories[0]}</div>}
|
||||
</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 { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import Pagination from './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 { User } from '../../context'
|
||||
import Spinner from '../atoms/Spinner'
|
||||
import Asset from '../molecules/Asset'
|
||||
import AssetTeaser from '../molecules/AssetTeaser'
|
||||
import styles from './AssetsUser.module.scss'
|
||||
|
||||
export default class AssetsUser extends PureComponent<
|
||||
@ -57,10 +57,9 @@ export default class AssetsUser extends PureComponent<
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { account, isOceanNetwork } = this.context
|
||||
const { account } = this.context
|
||||
|
||||
return (
|
||||
isOceanNetwork &&
|
||||
account && (
|
||||
<div className={styles.assetsUser}>
|
||||
{this.props.recent && (
|
||||
@ -82,7 +81,7 @@ export default class AssetsUser extends PureComponent<
|
||||
)
|
||||
.filter(asset => !!asset)
|
||||
.map((asset: any) => (
|
||||
<Asset
|
||||
<AssetTeaser
|
||||
list={this.props.list}
|
||||
key={asset.id}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.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 { Market } from '../../context'
|
||||
import React, { useContext } from 'react'
|
||||
import { Market, User } from '../../context'
|
||||
import Content from '../atoms/Content'
|
||||
import { ReactComponent as AiCommons } from '../../img/aicommons.svg'
|
||||
import styles from './Footer.module.scss'
|
||||
|
||||
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>
|
||||
<p>
|
||||
Online since March 2019.
|
||||
<Market.Consumer>
|
||||
{state =>
|
||||
state.totalAssets > 0 &&
|
||||
` With a total of ${
|
||||
state.totalAssets
|
||||
} registered assets.`
|
||||
}
|
||||
</Market.Consumer>
|
||||
</p>
|
||||
<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>
|
||||
</aside>
|
||||
|
||||
<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
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { ReactComponent as Logo } from '@oceanprotocol/art/logo/logo.svg'
|
||||
import { User } from '../../context'
|
||||
import AccountStatus from '../molecules/AccountStatus'
|
||||
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;
|
||||
color: $brand-grey;
|
||||
position: relative;
|
||||
border-bottom: .1rem solid $brand-grey-lighter;
|
||||
border-top: .1rem solid $brand-grey-lighter;
|
||||
padding-top: $spacer / 2;
|
||||
padding-bottom: $spacer / 2;
|
||||
text-align: left;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
.warnings {
|
||||
padding-left: $spacer;
|
||||
.account {
|
||||
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 {
|
||||
margin-left: -($spacer);
|
||||
margin-right: $spacer / 2;
|
||||
margin-left: -($spacer / 1.2);
|
||||
margin-right: $spacer / 2.5;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -1,23 +1,34 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import Web3message from './Web3message'
|
||||
import { User } from '../../context'
|
||||
import { User, Market } from '../../context'
|
||||
import { userMock, userMockConnected } from '../../../__mocks__/user-mock'
|
||||
import { marketMock } from '../../../__mocks__/market-mock'
|
||||
|
||||
describe('Web3message', () => {
|
||||
it('renders with noWeb3 message', () => {
|
||||
it('renders with burner wallet message', () => {
|
||||
const { container } = render(
|
||||
<User.Provider value={{ ...userMock }}>
|
||||
<Web3message />
|
||||
<User.Provider value={{ ...userMockConnected, isBurner: true }}>
|
||||
<Market.Provider value={{ ...marketMock }}>
|
||||
<Web3message extended />
|
||||
</Market.Provider>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toHaveTextContent('Not a Web3 Browser')
|
||||
expect(container.firstChild).toHaveTextContent('Burner Wallet')
|
||||
})
|
||||
|
||||
it('renders with wrongNetwork message', () => {
|
||||
const { container } = render(
|
||||
<User.Provider value={{ ...userMock, isWeb3: true }}>
|
||||
<Web3message />
|
||||
<User.Provider value={{ ...userMockConnected, network: 'Pacific' }}>
|
||||
<Market.Provider
|
||||
value={{
|
||||
...marketMock,
|
||||
networkMatch: false,
|
||||
network: 'Nile'
|
||||
}}
|
||||
>
|
||||
<Web3message extended />
|
||||
</Market.Provider>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toHaveTextContent(
|
||||
@ -27,38 +38,23 @@ describe('Web3message', () => {
|
||||
|
||||
it('renders with noAccount message', () => {
|
||||
const { container } = render(
|
||||
<User.Provider
|
||||
value={{ ...userMock, isWeb3: true, isOceanNetwork: true }}
|
||||
>
|
||||
<Web3message />
|
||||
<User.Provider value={userMock}>
|
||||
<Market.Provider value={marketMock}>
|
||||
<Web3message extended />
|
||||
</Market.Provider>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toHaveTextContent('No accounts detected')
|
||||
expect(container.firstChild).toHaveTextContent('No account selected')
|
||||
})
|
||||
|
||||
it('renders with hasAccount message', () => {
|
||||
const { container } = render(
|
||||
<User.Provider value={userMockConnected}>
|
||||
<Web3message />
|
||||
<Market.Provider value={marketMock}>
|
||||
<Web3message />
|
||||
</Market.Provider>
|
||||
</User.Provider>
|
||||
)
|
||||
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 Account from '../atoms/Account'
|
||||
import Button from '../atoms/Button'
|
||||
import AccountStatus from '../molecules/AccountStatus'
|
||||
import styles from './Web3message.module.scss'
|
||||
import { User } from '../../context'
|
||||
import { User, Market } from '../../context'
|
||||
import content from '../../data/web3message.json'
|
||||
|
||||
export default class Web3message extends PureComponent {
|
||||
private message = (
|
||||
message: string,
|
||||
account?: string,
|
||||
unlockAccounts?: () => any
|
||||
) => (
|
||||
<div className={styles.message}>
|
||||
{account ? (
|
||||
<Account account={account} />
|
||||
) : (
|
||||
<div className={styles.warnings}>
|
||||
<AccountStatus className={styles.status} />
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />{' '}
|
||||
{unlockAccounts && (
|
||||
<Button onClick={() => unlockAccounts()} link>
|
||||
Unlock Account
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
export default class Web3message extends PureComponent<{ extended?: boolean }> {
|
||||
public static contextType = Market
|
||||
|
||||
private messageOceanNetwork = () =>
|
||||
this.context.network === 'Pacific'
|
||||
? content.wrongNetworkPacific
|
||||
: this.context.network === 'Nile'
|
||||
? content.wrongNetworkNile
|
||||
: this.context.network === 'Duero'
|
||||
? content.wrongNetworkDuero
|
||||
: content.wrongNetworkSpree
|
||||
|
||||
private Message = () => {
|
||||
const { networkMatch, network } = this.context
|
||||
|
||||
return (
|
||||
<User.Consumer>
|
||||
{user => (
|
||||
<em
|
||||
dangerouslySetInnerHTML={{
|
||||
__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() {
|
||||
const {
|
||||
isWeb3,
|
||||
isOceanNetwork,
|
||||
isLogged,
|
||||
account,
|
||||
unlockAccounts
|
||||
} = this.context
|
||||
const { networkMatch } = this.context
|
||||
|
||||
return !isWeb3
|
||||
? this.message(content.noweb3)
|
||||
: !isOceanNetwork
|
||||
? this.message(content.wrongNetwork)
|
||||
: !isLogged
|
||||
? this.message(content.noAccount, '', unlockAccounts)
|
||||
: isLogged
|
||||
? this.message(content.hasAccount, account)
|
||||
: null
|
||||
return (
|
||||
<div className={styles.message}>
|
||||
<div className={styles.account}>
|
||||
<Account />
|
||||
</div>
|
||||
|
||||
{(!networkMatch || this.props.extended) && (
|
||||
<div className={styles.text}>
|
||||
<AccountStatus className={styles.status} />
|
||||
<this.Message />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Web3message.contextType = User
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import '../../styles/variables';
|
||||
@import '../../../styles/variables';
|
||||
|
||||
.metaPrimary {
|
||||
margin-bottom: $spacer;
|
||||
@ -46,18 +46,41 @@
|
||||
.description {
|
||||
// respect line breaks from textarea
|
||||
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 {
|
||||
border-top: 1px solid $brand-grey-lighter;
|
||||
border-bottom: 1px solid $brand-grey-lighter;
|
||||
padding-top: $spacer;
|
||||
padding-bottom: $spacer;
|
||||
.metaFixed {
|
||||
border: 1px solid $brand-grey-lighter;
|
||||
padding: $spacer;
|
||||
border-radius: $border-radius;
|
||||
margin-top: $spacer;
|
||||
margin-bottom: $spacer;
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin-bottom: $spacer * $line-height;
|
||||
font-size: $font-size-small;
|
||||
position: relative;
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
@ -65,7 +88,7 @@
|
||||
|
||||
@media (min-width: $break-point--small) {
|
||||
display: flex;
|
||||
margin-bottom: 0;
|
||||
margin-bottom: $spacer / 3;
|
||||
}
|
||||
|
||||
&: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 {
|
||||
display: block;
|
||||
|
||||
@ -90,10 +124,15 @@
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* stylelint-disable declaration-no-important */
|
||||
code {
|
||||
display: inline;
|
||||
display: block;
|
||||
padding: 0 !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
/* stylelint-enable declaration-no-important */
|
||||
|
||||
@media (min-width: $break-point--small) {
|
||||
width: 70%;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import { DDO, MetaData } from '@oceanprotocol/squid'
|
||||
import { BrowserRouter as Router } from 'react-router-dom'
|
||||
import AssetDetails, { renderDatafilesLine } from './AssetDetails'
|
@ -1,17 +1,18 @@
|
||||
import React, { PureComponent, ChangeEvent } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import Moment from 'react-moment'
|
||||
import { DDO, MetaData, Logger } from '@oceanprotocol/squid'
|
||||
import Input from '../../components/atoms/Form/Input'
|
||||
import Markdown from '../../components/atoms/Markdown'
|
||||
import { User } from '../../context'
|
||||
import Input from '../../atoms/Form/Input'
|
||||
import Markdown from '../../atoms/Markdown'
|
||||
import { User } from '../../../context'
|
||||
import CategoryLink from '../../atoms/CategoryLink'
|
||||
import styles from './AssetDetails.module.scss'
|
||||
import AssetFilesDetails from './AssetFilesDetails'
|
||||
import Button from '../../components/atoms/Button'
|
||||
import Spinner from '../../components/atoms/Spinner'
|
||||
import { serviceHost, servicePort, serviceScheme } from '../../config'
|
||||
import Button from '../../atoms/Button'
|
||||
import Spinner from '../../atoms/Spinner'
|
||||
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) =>
|
||||
files.length === 1 ? (
|
||||
@ -84,7 +85,7 @@ export default class AssetDetails extends PureComponent<
|
||||
private fetch = async (method: string, body: any) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${serviceScheme}://${serviceHost}:${servicePort}/api/v1/ddo/${
|
||||
`${serviceUri}/api/v1/ddo/${
|
||||
this.props.ddo
|
||||
}`,
|
||||
{
|
||||
@ -213,7 +214,7 @@ export default class AssetDetails extends PureComponent<
|
||||
/>
|
||||
) : (
|
||||
// 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 }) =>
|
||||
@ -297,15 +298,15 @@ export default class AssetDetails extends PureComponent<
|
||||
|
||||
<div className={styles.metaPrimaryData}>
|
||||
<span
|
||||
title={`Date created, published on ${
|
||||
base.datePublished
|
||||
}`}
|
||||
title={`Date created, published on ${base.datePublished}`}
|
||||
>
|
||||
<this.Date value={dateCreated} />
|
||||
</span>
|
||||
|
||||
{categories && <this.Category value={categories} />}
|
||||
|
||||
{/*base.categories && (
|
||||
<CategoryLink category={base.categories[0]} />
|
||||
)*/}
|
||||
{base.files &&
|
||||
!isEditMode &&
|
||||
renderDatafilesLine(base.files)}
|
||||
@ -316,28 +317,42 @@ export default class AssetDetails extends PureComponent<
|
||||
|
||||
<this.MetadataActions />
|
||||
|
||||
<ul className={styles.meta}>
|
||||
<li>
|
||||
<span className={styles.metaLabel}>
|
||||
<strong>Author</strong>
|
||||
</span>
|
||||
<span className={styles.metaValue}>{base.author}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={styles.metaLabel}>
|
||||
<strong>License</strong>
|
||||
</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>
|
||||
<Report did={ddo.id} title={metadata.base.name} />
|
||||
|
||||
<div className={styles.metaFixed}>
|
||||
<h2
|
||||
className={styles.metaFixedTitle}
|
||||
title="This metadata can not be changed because it is used to generate the checksums for the DDO, and to encrypt the file urls."
|
||||
>
|
||||
Fixed Metadata
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<span className={styles.metaLabel}>
|
||||
<strong>Author</strong>
|
||||
</span>
|
||||
<span className={styles.metaValue}>
|
||||
{base.author}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={styles.metaLabel}>
|
||||
<strong>License</strong>
|
||||
</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
|
||||
files={base.files ? base.files : []}
|
@ -1,4 +1,4 @@
|
||||
@import '../../styles/variables';
|
||||
@import '../../../styles/variables';
|
||||
|
||||
.buttonMain {
|
||||
margin: auto;
|
||||
@ -25,13 +25,16 @@
|
||||
|
||||
.file {
|
||||
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;
|
||||
margin-bottom: $spacer / 2;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
height: 8rem;
|
||||
width: 6rem;
|
||||
height: 8.5rem;
|
||||
width: 6.5rem;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
@ -54,6 +57,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: $font-size-mini;
|
||||
font-weight: $font-weight-base;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
// move spinner a bit up
|
||||
+ div {
|
||||
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 {
|
||||
text-align: center;
|
@ -1,11 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import React from 'react'
|
||||
import { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import { DDO } from '@oceanprotocol/squid'
|
||||
import AssetFilesDetails from './AssetFilesDetails'
|
||||
import { User } from '../../context'
|
||||
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||
|
||||
describe('AssetFilesDetails', () => {
|
||||
it('renders without crashing', () => {
|
||||
@ -28,16 +26,4 @@ describe('AssetFilesDetails', () => {
|
||||
)
|
||||
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 { DDO, File } from '@oceanprotocol/squid'
|
||||
import AssetFile from './AssetFile'
|
||||
import { User } from '../../context'
|
||||
import Web3message from '../../components/organisms/Web3message'
|
||||
import { User } from '../../../context'
|
||||
import Web3message from '../../organisms/Web3message'
|
||||
import styles from './AssetFilesDetails.module.scss'
|
||||
|
||||
export default class AssetFilesDetails extends PureComponent<{
|
||||
@ -19,9 +19,7 @@ export default class AssetFilesDetails extends PureComponent<{
|
||||
<AssetFile key={file.index} ddo={ddo} file={file} />
|
||||
))}
|
||||
</div>
|
||||
{(!this.context.isOceanNetwork || !this.context.isLogged) && (
|
||||
<Web3message />
|
||||
)}
|
||||
<Web3message />
|
||||
</>
|
||||
) : (
|
||||
<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 { render } from 'react-testing-library'
|
||||
import { render } from '@testing-library/react'
|
||||
import { createMemoryHistory, createLocation } from 'history'
|
||||
import Details from './index'
|
||||
|
||||
const history = createMemoryHistory()
|
||||
const location = createLocation('/asset/did:xxx')
|
||||
|
||||
describe('Details', () => {
|
||||
it('renders loading state by default', () => {
|
||||
const { container } = render(
|
||||
<Details
|
||||
location={{
|
||||
search: '',
|
||||
pathname: '/',
|
||||
state: '',
|
||||
hash: ''
|
||||
}}
|
||||
match={{ params: { did: '' } }}
|
||||
history={history}
|
||||
location={location}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
)
|
||||
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…
Reference in New Issue
Block a user