1
0
mirror of https://github.com/oceanprotocol/commons.git synced 2023-03-15 18:03:00 +01:00
This commit is contained in:
Jernej Pregelj 2019-07-17 15:27:09 +02:00
commit a7d6af6006
226 changed files with 13755 additions and 7588 deletions

View File

@ -1 +0,0 @@
node_modules

View File

@ -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
View File

@ -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

View File

@ -1,3 +1,4 @@
node_modules
build
dist
coverage

View File

@ -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

View File

@ -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
View File

@ -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 youre now connected to Nile
2. under New Network, enter `https://pacific.oceanprotocol.com` as the custom RPC URL
3. Hit _Save_, and youre 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.

View File

@ -1 +1,2 @@
node_modules
.env.local

55
client/.env.local.example Normal file
View 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"

View File

@ -0,0 +1,2 @@
import mockAxios from 'jest-mock-axios'
export default mockAxios

View File

@ -0,0 +1,8 @@
const marketMock = {
totalAssets: 1000,
categories: ['category'],
network: 'Pacific',
networkMatch: true
}
export { marketMock }

View 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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
]
}
}

View File

@ -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;

View File

@ -21,5 +21,5 @@
"start_url": ".",
"display": "standalone",
"theme_color": "#141414",
"background_color": "#141414"
"background_color": "#ffffff"
}

2
client/public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /search

View File

@ -0,0 +1 @@
declare module 'react-collapsed'

View File

@ -0,0 +1 @@
declare module 'truffle-hdwallet-provider'

View File

@ -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()
})

View File

@ -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()
})

View File

@ -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>
)

View File

@ -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;
}

View File

@ -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'))
})
})

View File

@ -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

View File

@ -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'

View File

@ -12,6 +12,7 @@ interface ButtonProps {
onClick?: any
disabled?: boolean
to?: string
name?: string
}
export default class Button extends PureComponent<ButtonProps, any> {

View File

@ -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;
}

View File

@ -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'

View File

@ -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})` }}
/>
)
}

View 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

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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>

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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', () => {

View 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);
}
}

View 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()
})
})

View 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"
>
&times;
</button>
<header className={styles.header}>
<h2 className={styles.title}>{title}</h2>
{description && (
<p className={styles.description}>{description}</p>
)}
</header>
{children}
</ReactModal>
)
}
export default Modal

View 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)

View File

@ -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 {

View File

@ -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

View File

@ -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>
)

View File

@ -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;
}

View File

@ -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'
}}
>

View File

@ -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

View File

@ -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)

View File

@ -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;

View File

@ -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

View File

@ -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', () => {

View File

@ -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;
}

View File

@ -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')
})
})

View File

@ -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

View File

@ -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;
}

View File

@ -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()
})
})

View File

@ -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

View File

@ -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;
}

View File

@ -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/
)
})
})

View 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

View File

@ -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);
}

View File

@ -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

View File

@ -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;
}

View File

@ -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'
)
})
})

View 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} />
</>
)
}
}

View 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;
}
}

View 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()
})
})

View 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

View File

@ -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}

View 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;
}
}

View 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()
})
})

View 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>
)
}
}

View File

@ -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;
}
}
}
}

View File

@ -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>
&copy; {new Date().getFullYear()}{' '}
<a href={meta.social[0].url}>{meta.company}</a> &mdash; 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>
&copy; {new Date().getFullYear()}{' '}
<a href={meta.social[0].url}>{meta.company}</a> &mdash; 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>
)
}

View File

@ -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

View 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;
}

View 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'))
})
})

View 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>
</>
)
}
}

View File

@ -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;
}

View File

@ -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()
})
})

View File

@ -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

View File

@ -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%;
}

View File

@ -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'

View File

@ -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 : []}

View File

@ -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;

View 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'
)
})
})

View 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>
)
}
}

View File

@ -1,4 +1,4 @@
@import '../../styles/variables';
@import '../../../styles/variables';
.files {
text-align: center;

View File

@ -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()
})
})

View File

@ -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>

View 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;
}

View 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'))
})
})

View 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>
)
}
}

View 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;
}

View File

@ -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()

View 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)

View 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);
}
}

View 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