1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

Merge branch 'main' into issue510-match-providers-algo-dataset

This commit is contained in:
Bogdan Fazakas 2021-06-29 00:08:45 +03:00
commit b0356f559c
161 changed files with 52515 additions and 15897 deletions

View File

@ -1,10 +1,26 @@
# Default network, possible values: # Default network, possible values:
# "development", "ropsten", "rinkeby", "mainnet", "polygon" # "development", "ropsten", "rinkeby", "mainnet", "polygon", "moonbeamalpha"
GATSBY_NETWORK="rinkeby" GATSBY_NETWORK="rinkeby"
#GATSBY_INFURA_PROJECT_ID="xxx" #GATSBY_INFURA_PROJECT_ID="xxx"
#GATSBY_MARKET_FEE_ADDRESS="0xxx" #GATSBY_MARKET_FEE_ADDRESS="0xxx"
#GATSBY_ANALYTICS_ID="xxx"
#GATSBY_PORTIS_ID="xxx" #GATSBY_PORTIS_ID="xxx"
#
# ADVANCED SETTINGS
#
# Toggle pricing options presented during price creation
#GATSBY_ALLOW_FIXED_PRICING="true" #GATSBY_ALLOW_FIXED_PRICING="true"
#GATSBY_ALLOW_DYNAMIC_PRICING="true" #GATSBY_ALLOW_DYNAMIC_PRICING="true"
#GATSBY_ALLOW_FREE_PRICING="false"
# Define RBAC server URL to implement permission based restrictions
#GATSBY_RBAC_URL="http://localhost:3000"
# Enables another asset editing button holder further advanced settings
#GATSBY_ALLOW_ADVANCED_SETTINGS="true"
# Allow/Deny Lists
#GATSBY_CREDENTIAL_TYPE="address"

View File

@ -1,11 +1,12 @@
{ {
"parser": "babel-eslint",
"extends": ["eslint:recommended", "prettier"], "extends": ["eslint:recommended", "prettier"],
"env": { "es6": true, "browser": true, "node": true, "jest": true }, "parserOptions": {
"sourceType": "module",
"ecmaFeatures": { "jsx": true }
},
"env": { "browser": true, "node": true, "es2020": true, "jest": true },
"settings": { "settings": {
"react": { "react": { "version": "detect" }
"version": "detect"
}
}, },
"overrides": [ "overrides": [
{ {
@ -20,9 +21,6 @@
"plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended", "plugin:prettier/recommended",
"prettier/react",
"prettier/standard",
"prettier/@typescript-eslint",
"plugin:react-hooks/recommended" "plugin:react-hooks/recommended"
], ],
"plugins": ["@typescript-eslint", "prettier"], "plugins": ["@typescript-eslint", "prettier"],

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
16

View File

@ -25,6 +25,8 @@
- [🛳 Production](#-production) - [🛳 Production](#-production)
- [⬆️ Deployment](#-deployment) - [⬆️ Deployment](#-deployment)
- [💖 Contributing](#-contributing) - [💖 Contributing](#-contributing)
- [🍴 Forking](#-forking)
- [💻 Advanced Features](#-advanced-features)
- [🏛 License](#-license) - [🏛 License](#-license)
## 🏄 Get Started ## 🏄 Get Started
@ -37,6 +39,9 @@ To start local development:
git clone git@github.com:oceanprotocol/market.git git clone git@github.com:oceanprotocol/market.git
cd market cd market
# when using nvm to manage Node.js versions
nvm use
npm install npm install
npm start npm start
``` ```
@ -358,6 +363,28 @@ We welcome contributions in form of bug reports, feature requests, code changes,
- [Code of Conduct →](https://docs.oceanprotocol.com/concepts/code-of-conduct/) - [Code of Conduct →](https://docs.oceanprotocol.com/concepts/code-of-conduct/)
- [Reporting Vulnerabilities →](https://docs.oceanprotocol.com/concepts/vulnerabilities/) - [Reporting Vulnerabilities →](https://docs.oceanprotocol.com/concepts/vulnerabilities/)
## 🍴 Forking
We encourage you to fork this repository and create your own data marketplace. When you publish your forked version of this market there are a few elements that you are required to change for copyright reasons:
- The typeface is copyright protected and needs to be changed unless you purchase a license for it.
- The Ocean Protocol logo is a trademark of the Ocean Protocol Foundation and must be removed from forked versions of the market.
- The name "Ocean Market" is also copyright protected and should be changed to the name of your market.
Additionally, we would also advise that your retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team.
Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace!
## 💻 Advanced Features
Ocean Market also includes a number of advanced features that are suitable for an enterprise data market, such as:
- Role based access control
- Allow and deny lists
- Free pricing
[See our seperate guide on advanced features](docs/advancedSettings.md)
## 🏛 License ## 🏛 License
```text ```text

View File

@ -2,8 +2,7 @@ module.exports = {
client: { client: {
service: { service: {
name: 'ocean', name: 'ocean',
url: url: 'https://subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph',
'https://subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph',
// optional disable SSL validation check // optional disable SSL validation check
skipSSLValidation: true skipSSLValidation: true
} }

View File

@ -4,6 +4,7 @@ module.exports = {
// networks in their wallet. // networks in their wallet.
// Ocean Protocol contracts are deployed for: 'mainnet', 'rinkeby', 'development' // Ocean Protocol contracts are deployed for: 'mainnet', 'rinkeby', 'development'
network: process.env.GATSBY_NETWORK || 'mainnet', network: process.env.GATSBY_NETWORK || 'mainnet',
rbacUrl: process.env.GATSBY_RBAC_URL,
infuraProjectId: process.env.GATSBY_INFURA_PROJECT_ID || 'xxx', infuraProjectId: process.env.GATSBY_INFURA_PROJECT_ID || 'xxx',
@ -40,8 +41,13 @@ module.exports = {
// Wallets // Wallets
portisId: process.env.GATSBY_PORTIS_ID || 'xxx', portisId: process.env.GATSBY_PORTIS_ID || 'xxx',
// Used to show or hide the fixed and dynamic price options // Used to show or hide the fixed, dynamic or free price options
// tab to publishers during the price creation. // tab to publishers during the price creation.
allowFixedPricing: process.env.GATSBY_ALLOW_FIXED_PRICING || 'true', allowFixedPricing: process.env.GATSBY_ALLOW_FIXED_PRICING || 'true',
allowDynamicPricing: process.env.GATSBY_ALLOW_DYNAMIC_PRICING || 'true' allowDynamicPricing: process.env.GATSBY_ALLOW_DYNAMIC_PRICING || 'true',
allowFreePricing: process.env.GATSBY_ALLOW_FREE_PRICING || 'false',
// Used to show or hide advanced settings button in asset details page
allowAdvancedSettings: process.env.GATSBY_ALLOW_ADVANCED_SETTINGS || 'false',
credentialType: process.env.GATSBY_CREDENTIAL_TYPE || 'address'
} }

View File

@ -20,6 +20,15 @@
"rows": 10, "rows": 10,
"required": true "required": true
}, },
{
"name": "price",
"label": "New Price",
"type": "number",
"min": "1",
"placeholder": "0",
"help": "Enter a new price.",
"required": true
},
{ {
"name": "links", "name": "links",
"label": "Sample file", "label": "Sample file",

View File

@ -0,0 +1,31 @@
{
"description": "Update advanced settings of this data set. Updating these settings will create an on-chain transaction you have to approve in your wallet.",
"form": {
"success": "🎉 Successfully updated. 🎉",
"successAction": "Close",
"error": "Updating DDO failed.",
"data": [
{
"name": "allow",
"label": "Allow ETH Address",
"placeholder": "e.g. 0x12345678901234567890abcd",
"help": "Enter ETH address and click ADD button to append the list. Only ETH address in allow list can consume this asset. If the list is empty means anyone can download or compute this asset",
"type": "credentials"
},
{
"name": "deny",
"label": "Deny ETH Address",
"placeholder": "e.g. 0x12345678901234567890abcd",
"help": "Enter ETH address and click ADD button to append the list. If ETH address is fall under deny list, download or compute of this asset is denied",
"type": "credentials"
},
{
"name": "isOrderDisabled",
"label": "Disable Consumption",
"help": "Disable dataset being download or compute when dataset undergoing maintenance.",
"type": "checkbox",
"options": ["Disable"]
}
]
}
}

View File

@ -6,13 +6,6 @@
"successAction": "Close", "successAction": "Close",
"error": "Updating DDO failed.", "error": "Updating DDO failed.",
"data": [ "data": [
{
"name": "allowAllPublishedAlgorithms",
"label": "All Algorithms",
"help": "Allow any published algorithm to run on this data set.",
"type": "checkbox",
"options": ["Allow any published algorithm"]
},
{ {
"name": "publisherTrustedAlgorithms", "name": "publisherTrustedAlgorithms",
"label": "Selected Algorithms", "label": "Selected Algorithms",
@ -21,6 +14,13 @@
"multiple": true, "multiple": true,
"options": [], "options": [],
"sortOptions": false "sortOptions": false
},
{
"name": "allowAllPublishedAlgorithms",
"label": "All Algorithms",
"help": "Allow any published algorithm to run on this data set.",
"type": "checkbox",
"options": ["Allow any published algorithm"]
} }
] ]
} }

View File

@ -1,4 +1,7 @@
{ {
"title": "History", "title": "History",
"description": "Find the data sets and jobs that you previously accessed." "description": "Find the data sets and jobs that you previously accessed.",
"compute": {
"storage": "Results are stored for 30 days."
}
} }

View File

@ -19,7 +19,7 @@
"name": "files", "name": "files",
"label": "File", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "placeholder": "e.g. https://file.com/file.json",
"help": "Please enter the URL to your algorithm file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing.", "help": "Please enter the URL to your algorithm file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing. Some restrictions apply:\n\n- max. running time: 1 min.\n- [Writing Algorithms for Compute to Data](https://docs.oceanprotocol.com/tutorials/compute-to-data-algorithms/)",
"type": "files", "type": "files",
"required": true "required": true
}, },
@ -28,8 +28,8 @@
"label": "Docker Image", "label": "Docker Image",
"placeholder": "e.g. python3.7", "placeholder": "e.g. python3.7",
"help": "Please select an image to run your algorithm.", "help": "Please select an image to run your algorithm.",
"type": "select", "type": "boxSelection",
"options": ["node:latest", "python:latest", "custom image"], "options": [],
"required": true "required": true
}, },
{ {
@ -56,6 +56,13 @@
"sortOptions": false, "sortOptions": false,
"required": true "required": true
}, },
{
"name": "dataTokenOptions",
"label": "Datatoken Name & Symbol",
"type": "datatoken",
"help": "The datatoken for this algorithm will be created with this name & symbol.",
"required": true
},
{ {
"name": "entrypoint", "name": "entrypoint",
"label": "Entrypoint", "label": "Entrypoint",

View File

@ -19,7 +19,7 @@
"name": "files", "name": "files",
"label": "File", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "placeholder": "e.g. https://file.com/file.json",
"help": "Please enter the URL to your data set file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing.", "help": "Please enter the URL to your data set file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing. For a compute data set, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size.",
"type": "files", "type": "files",
"required": true "required": true
}, },
@ -34,7 +34,7 @@
"name": "access", "name": "access",
"label": "Access Type", "label": "Access Type",
"help": "Choose how you want your files to be accessible for the specified price.", "help": "Choose how you want your files to be accessible for the specified price.",
"type": "select", "type": "boxSelection",
"options": ["Download", "Compute"], "options": ["Download", "Compute"],
"required": true "required": true
}, },

View File

@ -21,6 +21,10 @@
"communityFee": "Explain community fee...", "communityFee": "Explain community fee...",
"marketplaceFee": "Explain marketplace fee..." "marketplaceFee": "Explain marketplace fee..."
} }
},
"free": {
"title": "Free",
"info": "Set your data set as free. The datatoken for this data set will be given for free via creating a faucet."
} }
}, },
"pool": { "pool": {

29
docs/advancedSettings.md Normal file
View File

@ -0,0 +1,29 @@
# Advanced Settings
**Table of Contents**
- [Role based Access Control](#rbac-settings)
- [Allow and Deny lists](#allow-and-deny-list-settings)
- [Free Pricing](#free-pricing-settings)
## RBAC settings
- Setup and host the Ocean role based access control (RBAC) server. Follow the instructions in the [RBAC repository](https://github.com/oceanprotocol/RBAC-Server)
- The RBAC server can store roles in [Keycloak](https://www.keycloak.org/) or a json file.
- In your .env file, set the value of the `GATSBY_RBAC_URL` environmental variable to the URL of the Ocean RBAC server that you have hosted, e.g. `GATSBY_RBAC_URL= "http://localhost:3000"`
- Users of your marketplace will now require the correct role ("user", "consumer", "publisher") to access features in your marketplace. The market will check the role that has been allocated to the user based on the address that they have connected to the market with.
- The following features have been wrapped in the `Permission` component and will be restricted once the `GATSBY_RBAC_URL` has been defined:
- Viewing or searching datasets requires the user to have permison to `browse`
- Purchasing or trading a datatoken, or adding liquidity to a pool require the user to have permison to `consume`
- Publishing a dataset requires the user to have permison to `publish`
- You can change the permission resrictions by either removing the `Permission` component or passing in a different eventType prop e.g. `<Permission eventType="browse">`.
## Allow and Deny List Settings
- To enable allow and deny lists you need to add the following environmental variable to your .env file: `GATSBY_ALLOW_ADVANCED_SETTINGS="true"`
- Publishers in your market will now have the ability to restrict who can consume their datasets.
## Free Pricing Settings
- To allow publishers to set pricing as "Free" you need to add the following environmental variable to your .env file: `GATSBY_ALLOW_FREE_PRICING="true"`
- This allocates the datatokens to the [dispenser contract](https://github.com/oceanprotocol/contracts/blob/main/contracts/dispenser/Dispenser.sol) which dispenses data tokens to users for free. Publishers in your market will now be able to offer their datasets to users for free (excluding gas costs).

View File

@ -43,7 +43,7 @@ exports.onCreatePage = async ({ page, actions }) => {
exports.onCreateWebpackConfig = ({ actions }) => { exports.onCreateWebpackConfig = ({ actions }) => {
actions.setWebpackConfig({ actions.setWebpackConfig({
node: { node: {
// 'fs' fix for squid.js // 'fs' fix for ocean.js
fs: 'empty' fs: 'empty'
}, },
// fix for 'got'/'swarm-js' dependency // fix for 'got'/'swarm-js' dependency

62289
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,122 +23,112 @@
"postinstall": "husky install" "postinstall": "husky install"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.3.11", "@apollo/client": "^3.3.19",
"@coingecko/cryptoformat": "^0.4.2", "@coingecko/cryptoformat": "^0.4.2",
"@loadable/component": "^5.14.1", "@loadable/component": "^5.15.0",
"@oceanprotocol/art": "^3.0.0", "@oceanprotocol/art": "^3.0.0",
"@oceanprotocol/lib": "^0.14.1", "@oceanprotocol/lib": "^0.15.1",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@portis/web3": "^3.0.3", "@portis/web3": "^4.0.4",
"@sindresorhus/slugify": "^1.0.0", "@sindresorhus/slugify": "^2.1.0",
"@tippyjs/react": "^4.2.0", "@tippyjs/react": "^4.2.5",
"@types/classnames": "^2.2.11", "@walletconnect/web3-provider": "^1.4.1",
"@vercel/node": "^1.8.5",
"@walletconnect/web3-provider": "^1.3.4",
"axios": "^0.21.1", "axios": "^0.21.1",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"classnames": "^2.2.6", "classnames": "^2.3.1",
"cross-fetch": "^3.0.6", "cross-fetch": "^3.1.4",
"date-fns": "^2.16.1", "date-fns": "^2.22.1",
"decimal.js": "^10.2.1", "decimal.js": "^10.2.1",
"dom-confetti": "^0.2.2", "dom-confetti": "^0.2.2",
"dotenv": "^8.2.0", "dotenv": "^10.0.0",
"ethereum-address": "0.0.4", "ethereum-address": "0.0.4",
"ethereum-blockies": "github:MyEtherWallet/blockies", "ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^6.1.0", "filesize": "^6.3.0",
"formik": "^2.2.6", "formik": "^2.2.9",
"gatsby": "^2.30.2", "gatsby": "^2.32.13",
"gatsby-image": "^2.9.0", "gatsby-image": "^2.9.0",
"gatsby-plugin-manifest": "^2.10.0", "gatsby-plugin-manifest": "^2.10.0",
"gatsby-plugin-react-helmet": "^3.8.0", "gatsby-plugin-react-helmet": "^3.8.0",
"gatsby-plugin-remove-trailing-slashes": "^2.8.0", "gatsby-plugin-remove-trailing-slashes": "^2.8.0",
"gatsby-plugin-sharp": "^2.12.1", "gatsby-plugin-sharp": "^2.14.4",
"gatsby-plugin-svgr": "^2.1.0", "gatsby-plugin-svgr": "^2.1.0",
"gatsby-plugin-use-dark-mode": "^1.2.0", "gatsby-plugin-use-dark-mode": "^1.3.0",
"gatsby-plugin-webpack-size": "^1.0.0", "gatsby-plugin-webpack-size": "^2.0.1",
"gatsby-source-filesystem": "^2.9.0", "gatsby-source-filesystem": "^2.9.0",
"gatsby-source-graphql": "^2.12.0", "gatsby-source-graphql": "^2.12.0",
"gatsby-transformer-json": "^2.9.0", "gatsby-transformer-json": "^2.9.0",
"gatsby-transformer-remark": "^2.14.0", "gatsby-transformer-remark": "^2.16.1",
"gatsby-transformer-sharp": "^2.10.1", "gatsby-transformer-sharp": "^2.12.1",
"intersection-observer": "^0.12.0", "graphql": "14.7.0",
"is-url-superb": "^5.0.0", "is-url-superb": "^6.0.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
"query-string": "^6.13.8", "query-string": "^7.0.0",
"react": "^17.0.1", "react": "^17.0.2",
"react-chartjs-2": "^2.11.1", "react-chartjs-2": "^2.11.2",
"react-data-table-component": "^6.11.6", "react-data-table-component": "^6.11.7",
"react-dom": "^17.0.1", "react-dom": "^17.0.2",
"react-dotdotdot": "^1.3.1", "react-dotdotdot": "^1.3.1",
"react-dropzone": "^11.2.4",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-intersection-observer": "^8.31.0", "react-markdown": "^6.0.2",
"react-markdown": "^5.0.3", "react-modal": "^3.14.2",
"react-modal": "^3.12.1", "react-paginate": "^7.1.3",
"react-paginate": "^7.0.0", "react-spring": "^9.2.1",
"react-spring": "^8.0.27", "react-tabs": "^3.2.2",
"react-tabs": "^3.1.2", "react-toastify": "^7.0.4",
"react-toastify": "^6.2.0",
"remove-markdown": "^0.3.0", "remove-markdown": "^0.3.0",
"shortid": "^2.2.16", "shortid": "^2.2.16",
"slugify": "^1.4.6", "slugify": "^1.5.3",
"swr": "^0.3.11", "swr": "^0.5.6",
"use-dark-mode": "^2.3.1", "use-dark-mode": "^2.3.1",
"web3": "^1.3.4", "web3": "^1.3.6",
"web3modal": "^1.9.3", "web3modal": "^1.9.3",
"yup": "^0.32.6" "yup": "^0.32.9"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@storybook/addon-actions": "^6.1.14",
"@storybook/addon-storyshots": "^6.1.14",
"@storybook/react": "^6.1.14",
"@svgr/webpack": "^5.5.0", "@svgr/webpack": "^5.5.0",
"@testing-library/jest-dom": "^5.11.9", "@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.3", "@testing-library/react": "^11.2.7",
"@types/chart.js": "^2.9.29", "@types/chart.js": "^2.9.32",
"@types/jest": "^26.0.20", "@types/classnames": "^2.3.1",
"@types/jest": "^26.0.23",
"@types/loadable__component": "^5.13.1", "@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3", "@types/lodash.debounce": "^4.0.3",
"@types/lodash.omit": "^4.5.6", "@types/lodash.omit": "^4.5.6",
"@types/node": "^14.14.20", "@types/node": "^15.6.1",
"@types/react": "^17.0.0", "@types/react": "^17.0.8",
"@types/react-helmet": "^6.1.0", "@types/react-helmet": "^6.1.1",
"@types/react-modal": "^3.10.6", "@types/react-modal": "^3.12.0",
"@types/react-paginate": "^6.2.1", "@types/react-paginate": "^7.1.0",
"@types/react-tabs": "^2.3.2", "@types/react-tabs": "^2.3.2",
"@types/remove-markdown": "^0.1.1", "@types/remove-markdown": "^0.3.0",
"@types/shortid": "0.0.29", "@types/shortid": "0.0.29",
"@types/yup": "^0.29.11", "@types/yup": "^0.29.11",
"@typescript-eslint/eslint-plugin": "^4.13.0", "@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.13.0", "@typescript-eslint/parser": "^4.26.0",
"apollo": "^2.32.1", "apollo": "^2.33.4",
"babel-loader": "^8.2.2", "eslint": "^7.27.0",
"babel-preset-react-app": "^10.0.0",
"eslint": "^7.17.0",
"eslint-config-oceanprotocol": "^1.5.0", "eslint-config-oceanprotocol": "^1.5.0",
"eslint-config-prettier": "^7.1.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.22.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"husky": "^5.0.8", "husky": "^6.0.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"prettier": "^2.2.1", "prettier": "^2.3.0",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"serve": "^11.3.2", "serve": "^11.3.2",
"source-map-explorer": "^2.5.2", "source-map-explorer": "^2.5.2",
"typescript": "^4.1.3" "typescript": "^4.3.2"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/oceanprotocol/market" "url": "https://github.com/oceanprotocol/market"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=14"
}, },
"browserslist": [ "browserslist": [
">0.2%", ">0.2%",

View File

@ -1,11 +1,6 @@
export interface ComputeJobMetaData { import { ComputeJob } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
jobId: string
did: string export interface ComputeJobMetaData extends ComputeJob {
dateCreated: string
dateFinished: string
assetName: string assetName: string
status: number assetDtSymbol: string
statusText: string
algorithmLogUrl: string
resultsUrls: string[]
} }

View File

@ -49,6 +49,7 @@ export interface MetadataPublishFormAlgorithm {
dockerImage: string dockerImage: string
algorithmPrivacy: boolean algorithmPrivacy: boolean
timeout: string timeout: string
dataTokenOptions: DataTokenOptions
termsAndConditions: boolean termsAndConditions: boolean
// ---- optional fields ---- // ---- optional fields ----
image: string image: string
@ -61,6 +62,7 @@ export interface MetadataEditForm {
name: string name: string
description: string description: string
timeout: string timeout: string
price?: number
links?: string | EditableMetadataLinks[] links?: string | EditableMetadataLinks[]
} }

View File

@ -1,5 +1,3 @@
declare module 'intersection-observer'
declare module 'ethereum-blockies' { declare module 'ethereum-blockies' {
export function toDataUrl(address: string): string export function toDataUrl(address: string): string
} }

View File

@ -0,0 +1,71 @@
.button {
display: inline-block;
position: relative;
min-width: auto;
}
.button:hover,
.button:focus {
transform: none;
}
.logoWrap {
position: relative;
display: inline-block;
z-index: 1;
}
.logoWrap::before {
content: '+';
color: var(--color-secondary);
font-family: var(--font-family-base);
font-weight: var(--font-weight-base);
font-size: 1.25em;
position: absolute;
right: 0.05em;
top: 0.05em;
line-height: 0;
}
.logo {
width: 1.6em;
height: 1.6em;
display: inline-block;
margin-bottom: -0.35em;
border-radius: 50%;
border: 0.065rem solid var(--color-secondary);
margin-right: calc(var(--spacer) / 10);
transition: 0.2s ease-out;
}
.button:hover .logo,
.button:focus .logo {
border-color: var(--color-primary);
}
.button:hover .logoWrap::before,
.button:focus .logoWrap::before {
color: var(--color-primary);
}
.text {
display: inline-block;
position: relative;
}
.minimal .text {
opacity: 0;
transform: translate3d(-1rem, 0, 0);
transition: 0.2s ease-out;
z-index: 0;
white-space: pre;
position: absolute;
left: 100%;
top: 0.15rem;
}
.minimal:hover .text,
.minimal:focus .text {
opacity: 1;
transform: translate3d(0, 0, 0);
}

View File

@ -0,0 +1,53 @@
import React, { ReactElement } from 'react'
import classNames from 'classnames/bind'
import { addTokenToWallet } from '../../utils/web3'
import { useWeb3 } from '../../providers/Web3'
import Button from './Button'
import styles from './AddToken.module.css'
const cx = classNames.bind(styles)
export default function AddToken({
address,
symbol,
logo,
text,
className,
minimal
}: {
address: string
symbol: string
logo: string // needs to be a remote image
text?: string
className?: string
minimal?: boolean
}): ReactElement {
const { web3Provider } = useWeb3()
const styleClasses = cx({
button: true,
minimal: minimal,
[className]: className
})
async function handleAddToken() {
if (!web3Provider) return
await addTokenToWallet(web3Provider, address, symbol, logo)
}
return (
<Button
className={styleClasses}
style="text"
size="small"
onClick={handleAddToken}
>
<span className={styles.logoWrap}>
<img src={logo} className={styles.logo} width="16" height="16" />
</span>
<span className={styles.text}>{text || `Add ${symbol}`}</span>
</Button>
)
}

View File

@ -1,9 +1,9 @@
.icon { .icon {
fill: var(--brand-grey-light); fill: currentColor;
width: 1.1em; width: 1em;
height: 1.1em; height: 1em;
vertical-align: baseline; vertical-align: baseline;
margin-bottom: -0.2em; margin-bottom: -0.1em;
display: inline-block; display: inline-block;
} }

View File

@ -3,6 +3,7 @@ import styles from './AssetType.module.css'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import { ReactComponent as Compute } from '../../images/compute.svg' import { ReactComponent as Compute } from '../../images/compute.svg'
import { ReactComponent as Download } from '../../images/download.svg' import { ReactComponent as Download } from '../../images/download.svg'
import { ReactComponent as Lock } from '../../images/lock.svg'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -25,6 +26,8 @@ export default function AssetType({
</div> </div>
{accessType === 'access' ? ( {accessType === 'access' ? (
<Download role="img" aria-label="Download" className={styles.icon} /> <Download role="img" aria-label="Download" className={styles.icon} />
) : accessType === 'compute' && type === 'algorithm' ? (
<Lock role="img" aria-label="Private" className={styles.icon} />
) : ( ) : (
<Compute role="img" aria-label="Compute" className={styles.icon} /> <Compute role="img" aria-label="Compute" className={styles.icon} />
)} )}

View File

@ -5,6 +5,7 @@
margin: 0; margin: 0;
display: inline-block; display: inline-block;
width: fit-content; width: fit-content;
min-width: 7rem;
padding: calc(var(--spacer) / 3) var(--spacer); padding: calc(var(--spacer) / 3) var(--spacer);
font-size: var(--font-size-base); font-size: var(--font-size-base);
font-family: var(--font-family-base); font-family: var(--font-family-base);

View File

@ -1,57 +1,57 @@
import React from 'react' // import React from 'react'
import { action } from '@storybook/addon-actions' // // import { action } from '@storybook/addon-actions'
import Button from './Button' // import Button from './Button'
export default { // export default {
title: 'Atoms/Button' // title: 'Atoms/Button'
} // }
export const Default = () => ( // export const Default = () => (
<> // <>
<Button onClick={action('clicked')}>Hello Button</Button> // <Button onClick={action('clicked')}>Hello Button</Button>
<br /> // <br />
<br /> // <br />
<Button size="small" onClick={action('clicked')}> // <Button size="small" onClick={action('clicked')}>
Hello Button // Hello Button
</Button> // </Button>
</> // </>
) // )
export const Primary = () => ( // export const Primary = () => (
<> // <>
<Button style="primary" onClick={action('clicked')}> // <Button style="primary" onClick={action('clicked')}>
Hello Button // Hello Button
</Button> // </Button>
<br /> // <br />
<br /> // <br />
<Button style="primary" size="small" onClick={action('clicked')}> // <Button style="primary" size="small" onClick={action('clicked')}>
Hello Button // Hello Button
</Button> // </Button>
</> // </>
) // )
export const Ghost = () => ( // export const Ghost = () => (
<> // <>
<Button style="ghost" onClick={action('clicked')}> // <Button style="ghost" onClick={action('clicked')}>
Hello Button // Hello Button
</Button> // </Button>
<br /> // <br />
<br /> // <br />
<Button style="ghost" size="small" onClick={action('clicked')}> // <Button style="ghost" size="small" onClick={action('clicked')}>
Hello Button // Hello Button
</Button> // </Button>
</> // </>
) // )
export const Text = () => ( // export const Text = () => (
<> // <>
<Button style="text" onClick={action('clicked')}> // <Button style="text" onClick={action('clicked')}>
Hello Button // Hello Button
</Button> // </Button>
<br /> // <br />
<br /> // <br />
<Button style="text" size="small" onClick={action('clicked')}> // <Button style="text" size="small" onClick={action('clicked')}>
Hello Button // Hello Button
</Button> // </Button>
</> // </>
) // )

View File

@ -21,6 +21,8 @@ interface ButtonBuyProps {
onClick?: (e: FormEvent<HTMLButtonElement>) => void onClick?: (e: FormEvent<HTMLButtonElement>) => void
stepText?: string stepText?: string
type?: 'submit' type?: 'submit'
priceType?: string
algorithmPriceType?: string
} }
function getConsumeHelpText( function getConsumeHelpText(
@ -87,15 +89,21 @@ export default function ButtonBuy({
onClick, onClick,
stepText, stepText,
isLoading, isLoading,
type type,
priceType,
algorithmPriceType
}: ButtonBuyProps): ReactElement { }: ButtonBuyProps): ReactElement {
const buttonText = const buttonText =
action === 'download' action === 'download'
? hasPreviousOrder ? hasPreviousOrder
? 'Download' ? 'Download'
: priceType === 'free'
? 'Get'
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}` : `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
: hasPreviousOrder && hasPreviousOrderSelectedComputeAsset : hasPreviousOrder && hasPreviousOrderSelectedComputeAsset
? 'Start Compute Job' ? 'Start Compute Job'
: priceType === 'free' && algorithmPriceType === 'free'
? 'Order Compute Job'
: `Buy Compute Job` : `Buy Compute Job`
return ( return (

View File

@ -1,20 +1,30 @@
import React, { ReactElement, ReactNode, useEffect, useState } from 'react' import React, { ReactElement, ReactNode, useEffect, useState } from 'react'
import { ReactComponent as External } from '../../images/external.svg' import { ReactComponent as External } from '../../images/external.svg'
import styles from './ExplorerLink.module.css' import classNames from 'classnames/bind'
import { ConfigHelperConfig } from '@oceanprotocol/lib' import { ConfigHelperConfig } from '@oceanprotocol/lib'
import { useOcean } from '../../providers/Ocean' import { useOcean } from '../../providers/Ocean'
import styles from './ExplorerLink.module.css'
const cx = classNames.bind(styles)
export default function ExplorerLink({ export default function ExplorerLink({
path, path,
children children,
className
}: { }: {
networkId: number networkId: number
path: string path: string
children: ReactNode children: ReactNode
className?: string
}): ReactElement { }): ReactElement {
const { config } = useOcean() const { config } = useOcean()
const [url, setUrl] = useState<string>() const [url, setUrl] = useState<string>()
const styleClasses = cx({
link: true,
[className]: className
})
useEffect(() => { useEffect(() => {
setUrl((config as ConfigHelperConfig).explorerUri) setUrl((config as ConfigHelperConfig).explorerUri)
}, [config]) }, [config])
@ -25,7 +35,7 @@ export default function ExplorerLink({
title={`View on ${(config as ConfigHelperConfig).explorerUri}`} title={`View on ${(config as ConfigHelperConfig).explorerUri}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className={styles.link} className={styleClasses}
> >
{children} <External /> {children} <External />
</a> </a>

View File

@ -25,3 +25,7 @@
width: 4.5rem; width: 4.5rem;
padding: calc(var(--spacer) / 2) calc(var(--spacer) / 4); padding: calc(var(--spacer) / 2) calc(var(--spacer) / 4);
} }
.loaderWrap {
margin-right: calc(var(--spacer) / 6);
}

View File

@ -4,17 +4,28 @@ import filesize from 'filesize'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import cleanupContentType from '../../utils/cleanupContentType' import cleanupContentType from '../../utils/cleanupContentType'
import styles from './File.module.css' import styles from './File.module.css'
import Loader from '../atoms/Loader'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
function LoaderArea() {
return (
<div className={styles.loaderWrap}>
<Loader />
</div>
)
}
export default function File({ export default function File({
file, file,
className, className,
small small,
isLoading
}: { }: {
file: FileMetadata file: FileMetadata
className?: string className?: string
small?: boolean small?: boolean
isLoading?: boolean
}): ReactElement { }): ReactElement {
if (!file) return null if (!file) return null
@ -26,17 +37,23 @@ export default function File({
return ( return (
<ul className={styleClasses}> <ul className={styleClasses}>
{file.contentType || file.contentLength ? ( {isLoading === false || isLoading === undefined ? (
<> <>
<li>{cleanupContentType(file.contentType)}</li> {file.contentType || file.contentLength ? (
<li> <>
{file.contentLength && file.contentLength !== '0' <li>{cleanupContentType(file.contentType)}</li>
? filesize(Number(file.contentLength)) <li>
: ''} {file.contentLength && file.contentLength !== '0'
</li> ? filesize(Number(file.contentLength))
: ''}
</li>
</>
) : (
<li className={styles.empty}>No file info available</li>
)}
</> </>
) : ( ) : (
<li className={styles.empty}>No file info available</li> <LoaderArea />
)} )}
</ul> </ul>
) )

View File

@ -4,11 +4,15 @@ import styles from './InputElement.module.css'
import { InputProps } from '.' import { InputProps } from '.'
import FilesInput from '../../molecules/FormFields/FilesInput' import FilesInput from '../../molecules/FormFields/FilesInput'
import Terms from '../../molecules/FormFields/Terms' import Terms from '../../molecules/FormFields/Terms'
import BoxSelection, {
BoxSelectionOption
} from '../../molecules/FormFields/BoxSelection'
import Datatoken from '../../molecules/FormFields/Datatoken' import Datatoken from '../../molecules/FormFields/Datatoken'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import AssetSelection, { import AssetSelection, {
AssetSelectionAsset AssetSelectionAsset
} from '../../molecules/FormFields/AssetSelection' } from '../../molecules/FormFields/AssetSelection'
import Credentials from '../../molecules/FormFields/Credential'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -91,6 +95,7 @@ export default function InputElement({
id={slugify(option)} id={slugify(option)}
type={type} type={type}
name={name} name={name}
defaultChecked={props.defaultChecked}
{...props} {...props}
/> />
<label className={styles.radioLabel} htmlFor={slugify(option)}> <label className={styles.radioLabel} htmlFor={slugify(option)}>
@ -103,7 +108,7 @@ export default function InputElement({
case 'assetSelection': case 'assetSelection':
return ( return (
<AssetSelection <AssetSelection
assets={(options as unknown) as AssetSelectionAsset[]} assets={options as unknown as AssetSelectionAsset[]}
{...field} {...field}
{...props} {...props}
/> />
@ -111,7 +116,7 @@ export default function InputElement({
case 'assetSelectionMultiple': case 'assetSelectionMultiple':
return ( return (
<AssetSelection <AssetSelection
assets={(options as unknown) as AssetSelectionAsset[]} assets={options as unknown as AssetSelectionAsset[]}
multiple multiple
disabled={disabled} disabled={disabled}
{...field} {...field}
@ -124,6 +129,17 @@ export default function InputElement({
return <Datatoken name={name} {...field} {...props} /> return <Datatoken name={name} {...field} {...props} />
case 'terms': case 'terms':
return <Terms name={name} options={options} {...field} {...props} /> return <Terms name={name} options={options} {...field} {...props} />
case 'boxSelection':
return (
<BoxSelection
name={name}
options={options as unknown as BoxSelectionOption[]}
{...field}
{...props}
/>
)
case 'credentials':
return <Credentials name={name} {...field} {...props} />
default: default:
return prefix || postfix ? ( return prefix || postfix ? (
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}> <div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>

View File

@ -5,10 +5,6 @@
list-style-position: inside; list-style-position: inside;
} }
.item span {
color: var(--brand-grey-dark);
}
.ulItem { .ulItem {
list-style-type: square; list-style-type: square;
} }

View File

@ -1,5 +1,7 @@
.loaderWrap { .loaderWrap {
display: flex; display: flex;
align-items: center;
justify-content: center;
} }
.loader { .loader {

View File

@ -13,10 +13,9 @@ const Markdown = ({
// https://github.com/rexxars/react-markdown/issues/105#issuecomment-351585313 // https://github.com/rexxars/react-markdown/issues/105#issuecomment-351585313
const textCleaned = text?.replace(/\\n/g, '\n ') const textCleaned = text?.replace(/\\n/g, '\n ')
return ( return (
<ReactMarkdown <ReactMarkdown className={`${styles.markdown} ${className}`}>
source={textCleaned} {textCleaned}
className={`${styles.markdown} ${className}`} </ReactMarkdown>
/>
) )
} }

View File

@ -42,15 +42,20 @@ export default function PriceUnit({
return ( return (
<div className={styleClasses}> <div className={styleClasses}>
<div> {type && type === 'free' ? (
{Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '} <div> Free </div>
<span className={styles.symbol}>{symbol || 'OCEAN'}</span> ) : (
{type && type === 'pool' && ( <>
<Badge label="pool" className={styles.badge} /> <div>
)} {Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '}
</div> <span className={styles.symbol}>{symbol || 'OCEAN'}</span>
{type && type === 'pool' && (
{conversion && <Conversion price={price} />} <Badge label="pool" className={styles.badge} />
)}
</div>
{conversion && <Conversion price={price} />}
</>
)}
</div> </div>
) )
} }

View File

@ -16,7 +16,7 @@ export default function Price({
small?: boolean small?: boolean
conversion?: boolean conversion?: boolean
}): ReactElement { }): ReactElement {
return price?.value ? ( return price?.value || price?.type === 'free' ? (
<PriceUnit <PriceUnit
price={`${price.value}`} price={`${price.value}`}
className={className} className={className}
@ -24,17 +24,18 @@ export default function Price({
conversion={conversion} conversion={conversion}
type={price.type} type={price.type}
/> />
) : !price || !price.address || price.address === '' ? ( ) : !price || price?.type === '' ? (
<div className={styles.empty}> <div className={styles.empty}>
No price set{' '} No price set{' '}
<Tooltip content="No pricing mechanism has been set on this asset yet." /> <Tooltip content="No pricing mechanism has been set on this asset yet." />
</div> </div>
) : price.isConsumable !== 'true' ? (
<div className={styles.empty}>
Low liquidity{' '}
<Tooltip content="This pool does not have enough liquidity for using this data set." />
</div>
) : ( ) : (
// TODO: Hacky hack, put back some check for low liquidity
// ) : price.isConsumable !== 'true' ? (
// <div className={styles.empty}>
// Low liquidity{' '}
// <Tooltip content="This pool does not have enough liquidity for using this data set." />
// </div>
<Loader message="Retrieving price..." /> <Loader message="Retrieving price..." />
) )
} }

View File

@ -68,7 +68,6 @@ export default function Publisher({
> >
{name} {name}
</Link> </Link>
<div className={styles.links}> <div className={styles.links}>
{' — '} {' — '}
{profile && ( {profile && (

View File

@ -0,0 +1,59 @@
.display {
composes: selection from './FormFields/AssetSelection.module.css';
}
.display [class*='loaderWrap'] {
margin: calc(var(--spacer) / 3);
}
.scroll {
composes: scroll from './FormFields/AssetSelection.module.css';
margin-top: 0;
border-top: none;
width: 100%;
}
.row {
composes: row from './FormFields/AssetSelection.module.css';
}
.row:last-child {
border-bottom: none;
}
.row:first-child {
border-top: none;
}
.row:hover {
background-color: var(--background-content);
}
.info {
display: block;
width: 100%;
}
.title {
composes: title from './FormFields/AssetSelection.module.css';
}
.hover:hover {
color: var(--color-primary);
}
.price {
composes: price from './FormFields/AssetSelection.module.css';
}
.price [class*='symbol'] {
font-size: calc(var(--font-size-small) / 1.2) !important;
}
.did {
composes: did from './FormFields/AssetSelection.module.css';
}
.empty {
composes: empty from './FormFields/AssetSelection.module.css';
}

View File

@ -0,0 +1,49 @@
import React from 'react'
import Dotdotdot from 'react-dotdotdot'
import { Link } from 'gatsby'
import PriceUnit from '../atoms/Price/PriceUnit'
import Loader from '../atoms/Loader'
import styles from './AssetComputeList.module.css'
import { AssetSelectionAsset } from './FormFields/AssetSelection'
function Empty() {
return <div className={styles.empty}>No assets found.</div>
}
export default function AssetComputeSelection({
assets
}: {
assets: AssetSelectionAsset[]
}): JSX.Element {
return (
<div className={styles.display}>
<div className={styles.scroll}>
{!assets ? (
<Loader />
) : assets && !assets.length ? (
<Empty />
) : (
assets.map((asset: AssetSelectionAsset) => (
<Link
to={`/asset/${asset.did}`}
className={styles.row}
key={asset.did}
>
<div className={styles.info}>
<h3 className={styles.title}>
<Dotdotdot clamp={1} tagName="span">
{asset.name}
</Dotdotdot>
</h3>
<Dotdotdot clamp={1} tagName="code" className={styles.did}>
{asset.symbol} | {asset.did}
</Dotdotdot>
</div>
<PriceUnit price={asset.price} small className={styles.price} />
</Link>
))
)}
</div>
</div>
)
}

View File

@ -2,9 +2,10 @@ import AssetTeaser from '../molecules/AssetTeaser'
import * as React from 'react' import * as React from 'react'
import { DDO } from '@oceanprotocol/lib' import { DDO } from '@oceanprotocol/lib'
import ddo from '../../../tests/unit/__fixtures__/ddo' import ddo from '../../../tests/unit/__fixtures__/ddo'
import { AssetListPrices } from '../../utils/subgraph'
export default { export default {
title: 'Molecules/Asset Teaser' title: 'Molecules/Asset Teaser'
} }
export const Default = () => <AssetTeaser ddo={ddo as DDO} /> export const Default = () => <AssetTeaser ddo={ddo as DDO} price={undefined} />

View File

@ -3,7 +3,7 @@ import { Link } from 'gatsby'
import Dotdotdot from 'react-dotdotdot' import Dotdotdot from 'react-dotdotdot'
import Price from '../atoms/Price' import Price from '../atoms/Price'
import styles from './AssetTeaser.module.css' import styles from './AssetTeaser.module.css'
import { DDO } from '@oceanprotocol/lib' import { DDO, BestPrice } from '@oceanprotocol/lib'
import removeMarkdown from 'remove-markdown' import removeMarkdown from 'remove-markdown'
import Publisher from '../atoms/Publisher' import Publisher from '../atoms/Publisher'
import Time from '../atoms/Time' import Time from '../atoms/Time'
@ -11,9 +11,13 @@ import AssetType from '../atoms/AssetType'
declare type AssetTeaserProps = { declare type AssetTeaserProps = {
ddo: DDO ddo: DDO
price: BestPrice
} }
const AssetTeaser: React.FC<AssetTeaserProps> = ({ ddo }: AssetTeaserProps) => { const AssetTeaser: React.FC<AssetTeaserProps> = ({
ddo,
price
}: AssetTeaserProps) => {
const { attributes } = ddo.findServiceByType('metadata') const { attributes } = ddo.findServiceByType('metadata')
const { name, type } = attributes.main const { name, type } = attributes.main
const { dataTokenInfo } = ddo const { dataTokenInfo } = ddo
@ -47,7 +51,7 @@ const AssetTeaser: React.FC<AssetTeaserProps> = ({ ddo }: AssetTeaserProps) => {
</div> </div>
<footer className={styles.foot}> <footer className={styles.foot}>
<Price price={ddo.price} small /> <Price price={price} small />
<p className={styles.date}> <p className={styles.date}>
<Time date={ddo?.created} relative /> <Time date={ddo?.created} relative />
</p> </p>

View File

@ -107,7 +107,9 @@
.did { .did {
padding: 0; padding: 0;
font-size: var(--font-size-mini); /* font-size: var(--font-size-mini); */
/* hack to make DotDotDot clamp work in Safari*/
font-size: 0.63rem;
display: block; display: block;
text-align: left; text-align: left;
color: var(--color-secondary); color: var(--color-secondary);

View File

@ -4,9 +4,9 @@ import slugify from 'slugify'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import PriceUnit from '../../atoms/Price/PriceUnit' import PriceUnit from '../../atoms/Price/PriceUnit'
import { ReactComponent as External } from '../../../images/external.svg' import { ReactComponent as External } from '../../../images/external.svg'
import styles from './AssetSelection.module.css'
import InputElement from '../../atoms/Input/InputElement' import InputElement from '../../atoms/Input/InputElement'
import Loader from '../../atoms/Loader' import Loader from '../../atoms/Loader'
import styles from './AssetSelection.module.css'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -107,7 +107,12 @@ export default function AssetSelection({
</Dotdotdot> </Dotdotdot>
</label> </label>
<PriceUnit price={asset.price} small className={styles.price} /> <PriceUnit
price={asset.price}
type={asset.price === '0' ? 'free' : undefined}
small
className={styles.price}
/>
</div> </div>
)) ))
)} )}

View File

@ -0,0 +1,57 @@
.boxSelectionsWrapper {
display: flex;
justify-content: space-between;
gap: 0 calc(var(--spacer) / 4);
}
.boxSelectionsWrapper > div {
width: 100%;
}
.boxSelection {
display: block;
flex: 1 1 0px;
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2)
calc(var(--spacer) / 4) calc(var(--spacer) / 2) !important;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
color: var(--color-secondary);
cursor: pointer;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}
.title {
font-weight: var(--font-weight-bold);
text-align: center;
}
.boxSelectionsWrapper input[type='radio'] {
position: fixed;
opacity: 0;
pointer-events: none;
}
input[type='radio']:checked + label {
color: var(--font-color-text);
border-color: var(--color-secondary);
}
.boxSelection svg {
width: var(--font-size-h4);
height: var(--font-size-h4);
fill: currentColor;
margin-bottom: calc(var(--spacer) / 5);
}
.boxSelectionsWrapper label {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
color: var(--color-secondary);
font-weight: normal;
}

View File

@ -0,0 +1,70 @@
import React, { ChangeEvent } from 'react'
import classNames from 'classnames/bind'
import Loader from '../../atoms/Loader'
import styles from './BoxSelection.module.css'
const cx = classNames.bind(styles)
export interface BoxSelectionOption {
name: string
checked: boolean
title: JSX.Element | string
icon?: JSX.Element
text?: JSX.Element | string
}
export default function BoxSelection({
name,
options,
disabled,
handleChange,
...props
}: {
name: string
options: BoxSelectionOption[]
disabled?: boolean
handleChange?: (event: ChangeEvent<HTMLInputElement>) => void
}): JSX.Element {
const styleClassesWrapper = cx({
boxSelectionsWrapper: true,
[styles.disabled]: disabled
})
const styleClassesInput = cx({
input: true,
radio: true
})
return (
<div className={styleClassesWrapper}>
{!options ? (
<Loader />
) : (
options.map((value: BoxSelectionOption) => (
<div key={value.name}>
<input
id={value.name}
type="radio"
className={styleClassesInput}
defaultChecked={value.checked}
onChange={(event) => handleChange(event)}
{...props}
disabled={disabled}
value={value.name}
name={name}
/>
<label
className={`${styles.boxSelection} ${styles.label}`}
htmlFor={value.name}
title={value.name}
>
{value.icon}
<span className={styles.title}>{value.title}</span>
{value.text}
</label>
</div>
))
)}
</div>
)
}

View File

@ -0,0 +1,40 @@
.chip {
border: 1px solid var(--border-color);
display: flex;
padding-left: 10px;
}
.buttonWrapper {
width: 100%;
text-align: right;
}
.crossButton {
min-width: 0;
}
.crossButton svg {
display: inline-block;
width: var(--font-size-large);
height: var(--font-size-large);
fill: var(--brand-pink);
vertical-align: middle;
}
.scroll {
border-top: 1px solid var(--border-color);
min-height: fit-content;
max-height: 200px;
position: relative;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.credential {
padding: 0;
border: 1px solid var(--border-color);
background-color: var(--background-highlight);
border-radius: var(--border-radius);
font-size: var(--font-size-small);
min-height: 200px;
}

View File

@ -0,0 +1,81 @@
import { useField } from 'formik'
import { InputProps } from '../../../atoms/Input'
import React, { useState, ChangeEvent, FormEvent, useEffect } from 'react'
import InputGroup from '../../../atoms/Input/InputGroup'
import Button from '../../../atoms/Button'
import styles from './Credential.module.css'
import { isAddress } from 'web3-utils'
import { toast } from 'react-toastify'
import { ReactComponent as Cross } from '../../../../images/cross.svg'
import InputElement from '../../../atoms/Input/InputElement'
export default function Credentials(props: InputProps) {
const [field, meta, helpers] = useField(props.name)
const [arrayInput, setArrayInput] = useState<string[]>(field.value || [])
const [value, setValue] = useState('')
useEffect(() => {
helpers.setValue(arrayInput)
}, [arrayInput])
function handleDeleteChip(value: string) {
const newInput = arrayInput.filter((input) => input !== value)
setArrayInput(newInput)
helpers.setValue(newInput)
}
function handleAddValue(e: FormEvent<HTMLButtonElement>) {
e.preventDefault()
if (!isAddress(value)) {
toast.error('Wallet address is invalid')
return
}
if (arrayInput.includes(value)) {
toast.error('Wallet address already added into list')
return
}
setArrayInput((arrayInput) => [...arrayInput, value])
setValue('')
}
return (
<div className={styles.credential}>
<InputGroup>
<InputElement
type="text"
name="address"
size="default"
placeholder={props.placeholder}
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setValue(e.target.value)
}
/>
<Button
onClick={(e: FormEvent<HTMLButtonElement>) => handleAddValue(e)}
>
Add
</Button>
</InputGroup>
<div className={styles.scroll}>
{arrayInput &&
arrayInput.map((value) => {
return (
<div className={styles.chip} key={value}>
<code>{value}</code>
<span className={styles.buttonWrapper}>
<Button
className={styles.crossButton}
style="text"
onClick={(even) => handleDeleteChip(value)}
>
<Cross />
</Button>
</span>
</div>
)
})}
</div>
</div>
)
}

View File

@ -1,5 +1,4 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import isUrl from 'is-url-superb'
import Button from '../../../atoms/Button' import Button from '../../../atoms/Button'
import { FieldInputProps, useField } from 'formik' import { FieldInputProps, useField } from 'formik'
import Loader from '../../../atoms/Loader' import Loader from '../../../atoms/Loader'
@ -28,12 +27,8 @@ export default function FileInput({
<Button <Button
style="primary" style="primary"
size="small" size="small"
onClick={(e: React.SyntheticEvent) => handleButtonClick(e, field.value)} onClick={(e: React.SyntheticEvent) => e.preventDefault()}
disabled={ disabled={!field.value}
!field.value ||
// weird static page build fix so is-url-superb won't error
!isUrl(typeof field.value === 'string' ? field.value : '')
}
> >
{isLoading ? <Loader /> : 'Add File'} {isLoading ? <Loader /> : 'Add File'}
</Button> </Button>

View File

@ -14,7 +14,7 @@ export default function FilesInput(props: InputProps): ReactElement {
const [fileUrl, setFileUrl] = useState<string>() const [fileUrl, setFileUrl] = useState<string>()
const { config } = useOcean() const { config } = useOcean()
useEffect(() => { function loadFileInfo() {
const source = axios.CancelToken.source() const source = axios.CancelToken.source()
async function validateUrl() { async function validateUrl() {
@ -33,11 +33,16 @@ export default function FilesInput(props: InputProps): ReactElement {
setIsLoading(false) setIsLoading(false)
} }
} }
fileUrl && validateUrl() fileUrl && validateUrl()
return () => { return () => {
source.cancel() source.cancel()
} }
}
useEffect(() => {
loadFileInfo()
}, [fileUrl, config.providerUri]) }, [fileUrl, config.providerUri])
async function handleButtonClick(e: React.SyntheticEvent, url: string) { async function handleButtonClick(e: React.SyntheticEvent, url: string) {
@ -48,6 +53,11 @@ export default function FilesInput(props: InputProps): ReactElement {
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf' // File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
e.preventDefault() e.preventDefault()
// In the case when the user re-add the same URL after it was removed (by accident or intentionally)
if (fileUrl === url) {
loadFileInfo()
}
setFileUrl(url) setFileUrl(url)
} }

View File

@ -14,6 +14,10 @@ const query = graphql`
export default function Terms(props: InputProps): ReactElement { export default function Terms(props: InputProps): ReactElement {
const data = useStaticQuery(query) const data = useStaticQuery(query)
const termsProps: InputProps = {
...props,
defaultChecked: props.value.toString() === 'true'
}
return ( return (
<> <>
@ -21,7 +25,7 @@ export default function Terms(props: InputProps): ReactElement {
className={styles.terms} className={styles.terms}
dangerouslySetInnerHTML={{ __html: data.terms.html }} dangerouslySetInnerHTML={{ __html: data.terms.html }}
/> />
<InputElement {...props} type="checkbox" /> <InputElement {...termsProps} type="checkbox" />
</> </>
) )
} }

View File

@ -135,6 +135,11 @@ export function MetadataAlgorithmPreview({
<h2 className={styles.previewTitle}>Preview</h2> <h2 className={styles.previewTitle}>Preview</h2>
<header> <header>
{values.name && <h3 className={styles.title}>{values.name}</h3>} {values.name && <h3 className={styles.title}>{values.name}</h3>}
{values.dataTokenOptions?.name && (
<p
className={styles.datatoken}
>{`${values.dataTokenOptions.name}${values.dataTokenOptions.symbol}`}</p>
)}
{values.description && <Description description={values.description} />} {values.description && <Description description={values.description} />}
<div className={styles.asset}> <div className={styles.asset}>

View File

@ -2,7 +2,6 @@ import React, { ReactElement, useEffect, useState } from 'react'
import { useWeb3 } from '../../providers/Web3' import { useWeb3 } from '../../providers/Web3'
import { addCustomNetwork, NetworkObject } from '../../utils/web3' import { addCustomNetwork, NetworkObject } from '../../utils/web3'
import { getOceanConfig } from '../../utils/ocean' import { getOceanConfig } from '../../utils/ocean'
import { getProviderInfo } from 'web3modal'
import { useOcean } from '../../providers/Ocean' import { useOcean } from '../../providers/Ocean'
import { useSiteMetadata } from '../../hooks/useSiteMetadata' import { useSiteMetadata } from '../../hooks/useSiteMetadata'
import AnnouncementBanner, { import AnnouncementBanner, {
@ -19,7 +18,7 @@ const networkMatic: NetworkObject = {
} }
export default function NetworkBanner(): ReactElement { export default function NetworkBanner(): ReactElement {
const { web3Provider } = useWeb3() const { web3Provider, web3ProviderInfo } = useWeb3()
const { config, connect } = useOcean() const { config, connect } = useOcean()
const { announcement } = useSiteMetadata() const { announcement } = useSiteMetadata()
@ -51,10 +50,9 @@ export default function NetworkBanner(): ReactElement {
} }
useEffect(() => { useEffect(() => {
if (!web3Provider && !config) return if (!web3ProviderInfo || (!web3Provider && !config)) return
const providerInfo = getProviderInfo(web3Provider) switch (web3ProviderInfo.name) {
switch (providerInfo?.name) {
case 'Web3': case 'Web3':
if (config.networkId !== 137) { if (config.networkId !== 137) {
setText(announcement.main) setText(announcement.main)
@ -80,7 +78,7 @@ export default function NetworkBanner(): ReactElement {
setAction(undefined) setAction(undefined)
} }
} }
}, [web3Provider, config, announcement]) }, [web3Provider, web3ProviderInfo, config, announcement])
return <AnnouncementBanner text={text} action={action} /> return <AnnouncementBanner text={text} action={action} />
} }

View File

@ -17,20 +17,38 @@ export default function SearchBar({
filters?: boolean filters?: boolean
size?: 'small' | 'large' size?: 'small' | 'large'
}): ReactElement { }): ReactElement {
const [value, setValue] = useState(initialValue || '') let [value, setValue] = useState(initialValue || '')
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
}
async function startSearch(e: FormEvent<HTMLButtonElement>) { async function startSearch(e: FormEvent<HTMLButtonElement>) {
e.preventDefault() e.preventDefault()
if (value === '') return if (value === '') value = ' '
const urlEncodedValue = encodeURIComponent(value) const urlEncodedValue = encodeURIComponent(value)
const url = await addExistingParamsToUrl(location, 'text') const url = await addExistingParamsToUrl(location, [
'text',
'owner',
'tags'
])
navigate(`${url}&text=${urlEncodedValue}`) navigate(`${url}&text=${urlEncodedValue}`)
} }
async function emptySearch() {
const searchParams = new URLSearchParams(window.location.href)
const text = searchParams.get('text')
if (text !== ('' || undefined || null)) {
const url = await addExistingParamsToUrl(location, [
'text',
'owner',
'tags'
])
navigate(`${url}&text=%20`)
}
}
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
e.target.value === '' && emptySearch()
}
return ( return (
<form className={styles.form}> <form className={styles.form}>
<InputGroup> <InputGroup>

View File

@ -1,14 +1,13 @@
.buttons { .buttons {
display: flex; display: grid;
justify-content: space-between; gap: calc(var(--spacer) / 4);
grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
padding-bottom: calc(var(--spacer) / 8); padding-bottom: calc(var(--spacer) / 8);
} }
.button { .button {
display: block; width: auto;
flex: 0 0 48%; padding: calc(var(--spacer) / 3) calc(var(--spacer) / 4) !important;
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2)
calc(var(--spacer) / 4) calc(var(--spacer) / 2) !important;
border-radius: var(--border-radius); border-radius: var(--border-radius);
text-transform: none; text-transform: none;
} }
@ -31,3 +30,7 @@
color: var(--font-color-text); color: var(--font-color-text);
border-color: var(--color-secondary); border-color: var(--color-secondary);
} }
.appearances div[class*='boxSelectionsWrapper'] {
padding-bottom: calc(var(--spacer) / 8);
}

View File

@ -1,42 +1,44 @@
import React, { ReactElement } from 'react' import React, { ReactElement, ChangeEvent } from 'react'
import { DarkMode } from 'use-dark-mode' import { DarkMode } from 'use-dark-mode'
import Button from '../../atoms/Button'
import FormHelp from '../../atoms/Input/Help' import FormHelp from '../../atoms/Input/Help'
import Label from '../../atoms/Input/Label' import Label from '../../atoms/Input/Label'
import styles from './Appearance.module.css'
import { ReactComponent as Moon } from '../../../images/moon.svg' import { ReactComponent as Moon } from '../../../images/moon.svg'
import { ReactComponent as Sun } from '../../../images/sun.svg' import { ReactComponent as Sun } from '../../../images/sun.svg'
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection'
const buttons = ['Light', 'Dark'] import styles from './Appearance.module.css'
export default function Appearance({ export default function Appearance({
darkMode darkMode
}: { }: {
darkMode: DarkMode darkMode: DarkMode
}): ReactElement { }): ReactElement {
return ( const options: BoxSelectionOption[] = [
<li> {
<Label htmlFor="">Appearance</Label> name: 'Light',
<div className={styles.buttons}> checked: !darkMode.value,
{buttons.map((button) => { title: 'Light',
const isDark = button === 'Dark' icon: <Sun />
const selected = },
(isDark && darkMode.value) || (!isDark && !darkMode.value) {
name: 'Dark',
checked: darkMode.value,
title: 'Dark',
icon: <Moon />
}
]
return ( function handleChange(event: ChangeEvent<HTMLInputElement>) {
<Button event.target.value === 'Dark' ? darkMode.enable() : darkMode.disable()
key={button} }
className={`${styles.button} ${selected ? styles.selected : ''}`}
size="small" return (
style="text" <li className={styles.appearances}>
onClick={() => (isDark ? darkMode.enable() : darkMode.disable())} <Label htmlFor="">Appearance</Label>
> <BoxSelection
{isDark ? <Moon /> : <Sun />} options={options}
{button} name="appearanceMode"
</Button> handleChange={handleChange}
) />
})}
</div>
<FormHelp>Defaults to your OS setting, select to override.</FormHelp> <FormHelp>Defaults to your OS setting, select to override.</FormHelp>
</li> </li>
) )

View File

@ -17,3 +17,14 @@
.selected { .selected {
composes: selected from './Appearance.module.css'; composes: selected from './Appearance.module.css';
} }
.chains div[class*='boxSelectionsWrapper'] {
display: grid;
gap: calc(var(--spacer) / 4);
grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
padding-bottom: calc(var(--spacer) / 8);
}
.chains label[class*='boxSelection'] {
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 4) !important;
}

View File

@ -1,52 +1,58 @@
import { ConfigHelperConfig } from '@oceanprotocol/lib' import { ConfigHelperConfig } from '@oceanprotocol/lib'
import React, { ReactElement } from 'react' import React, { ReactElement, ChangeEvent } from 'react'
import { useOcean } from '../../../providers/Ocean' import { useOcean } from '../../../providers/Ocean'
import { useWeb3 } from '../../../providers/Web3' import { useWeb3 } from '../../../providers/Web3'
import { getOceanConfig } from '../../../utils/ocean' import { getOceanConfig } from '../../../utils/ocean'
import Button from '../../atoms/Button'
import FormHelp from '../../atoms/Input/Help' import FormHelp from '../../atoms/Input/Help'
import Label from '../../atoms/Input/Label' import Label from '../../atoms/Input/Label'
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection'
import styles from './Chain.module.css' import styles from './Chain.module.css'
export default function Chain(): ReactElement { export default function Chain(): ReactElement {
const { web3 } = useWeb3() const { web3 } = useWeb3()
const { config, connect } = useOcean() const { config, connect } = useOcean()
async function connectOcean(networkName: string) { async function connectOcean(event: ChangeEvent<HTMLInputElement>) {
const config = getOceanConfig(networkName) const config = getOceanConfig(event.target.value)
await connect(config) await connect(config)
} }
const chains = [ function isNetworkSelected(oceanConfig: string) {
{ name: 'ETH', oceanConfig: 'mainnet' }, return (config as ConfigHelperConfig).network === oceanConfig
{ name: 'Polygon/Matic', oceanConfig: 'polygon' } }
const options: BoxSelectionOption[] = [
{
name: 'mainnet',
checked: isNetworkSelected('mainnet'),
title: 'ETH',
text: 'Mainnet'
},
{
name: 'polygon',
checked: isNetworkSelected('polygon'),
title: 'Polygon/Matic',
text: 'Mainnet'
},
{
name: 'moonbeamalpha',
checked: isNetworkSelected('moonbeamalpha'),
title: 'Moonbase Alpha',
text: 'Testnet'
}
] ]
// TODO: to fully solve https://github.com/oceanprotocol/market/issues/432 // TODO: to fully solve https://github.com/oceanprotocol/market/issues/432
// there are more considerations for users with a wallet connected (wallet network vs. setting network). // there are more considerations for users with a wallet connected (wallet network vs. setting network).
// For now, only show the setting for non-wallet users. // For now, only show the setting for non-wallet users.
return !web3 ? ( return !web3 ? (
<li> <li className={styles.chains}>
<Label htmlFor="">Chain</Label> <Label htmlFor="">Chain</Label>
<div className={styles.buttons}> <BoxSelection
{chains.map((button) => { options={options}
const selected = name="chain"
(config as ConfigHelperConfig).network === button.oceanConfig handleChange={connectOcean}
/>
return (
<Button
key={button.name}
className={`${styles.button} ${selected ? styles.selected : ''}`}
size="small"
style="text"
onClick={() => connectOcean(button.oceanConfig)}
>
{button.name}
<span>Mainnet</span>
</Button>
)
})}
</div>
<FormHelp>Switch the data source for the interface.</FormHelp> <FormHelp>Switch the data source for the interface.</FormHelp>
</li> </li>
) : null ) : null

View File

@ -42,7 +42,7 @@
justify-content: space-between; justify-content: space-between;
} }
.actions span { .walletLogoWrap {
display: block; display: block;
} }
@ -84,3 +84,7 @@
.walletInfo button { .walletInfo button {
margin-top: calc(var(--spacer) / 5) !important; margin-top: calc(var(--spacer) / 5) !important;
} }
.addToken {
margin-left: 0.3rem;
}

View File

@ -1,33 +1,24 @@
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import Button from '../../atoms/Button'
import styles from './Details.module.css'
import { useOcean } from '../../../providers/Ocean'
import Web3Feedback from './Feedback'
import { getProviderInfo, IProviderInfo } from 'web3modal'
import Conversion from '../../atoms/Price/Conversion'
import { formatCurrency } from '@coingecko/cryptoformat' import { formatCurrency } from '@coingecko/cryptoformat'
import { useOcean } from '../../../providers/Ocean'
import { useUserPreferences } from '../../../providers/UserPreferences' import { useUserPreferences } from '../../../providers/UserPreferences'
import Button from '../../atoms/Button'
import AddToken from '../../atoms/AddToken'
import Conversion from '../../atoms/Price/Conversion'
import { useWeb3 } from '../../../providers/Web3' import { useWeb3 } from '../../../providers/Web3'
import { addOceanToWallet } from '../../../utils/web3'
import { Logger } from '@oceanprotocol/lib' import Web3Feedback from './Feedback'
import styles from './Details.module.css'
export default function Details(): ReactElement { export default function Details(): ReactElement {
const { web3Provider, connect, logout, networkData } = useWeb3() const { web3Provider, web3ProviderInfo, connect, logout, networkData } =
useWeb3()
const { balance, config } = useOcean() const { balance, config } = useOcean()
const { locale } = useUserPreferences() const { locale } = useUserPreferences()
const [providerInfo, setProviderInfo] = useState<IProviderInfo>()
const [mainCurrency, setMainCurrency] = useState<string>() const [mainCurrency, setMainCurrency] = useState<string>()
// const [portisNetwork, setPortisNetwork] = useState<string>() // const [portisNetwork, setPortisNetwork] = useState<string>()
// Workaround cause getInjectedProviderName() always returns `MetaMask`
// https://github.com/oceanprotocol/market/issues/332
useEffect(() => {
if (!web3Provider) return
const providerInfo = getProviderInfo(web3Provider)
setProviderInfo(providerInfo)
}, [web3Provider])
useEffect(() => { useEffect(() => {
if (!networkData) return if (!networkData) return
@ -61,11 +52,11 @@ export default function Details(): ReactElement {
<li className={styles.actions}> <li className={styles.actions}>
<div title="Connected provider" className={styles.walletInfo}> <div title="Connected provider" className={styles.walletInfo}>
<span> <span className={styles.walletLogoWrap}>
<img className={styles.walletLogo} src={providerInfo?.logo} /> <img className={styles.walletLogo} src={web3ProviderInfo?.logo} />
{providerInfo?.name} {web3ProviderInfo?.name}
</span> </span>
{/* {providerInfo?.name === 'Portis' && ( {/* {web3ProviderInfo?.name === 'Portis' && (
<InputElement <InputElement
name="network" name="network"
type="select" type="select"
@ -75,20 +66,17 @@ export default function Details(): ReactElement {
onChange={handlePortisNetworkChange} onChange={handlePortisNetworkChange}
/> />
)} */} )} */}
{providerInfo?.name === 'MetaMask' && ( {web3ProviderInfo?.name === 'MetaMask' && (
<Button <AddToken
style="text" address={config.oceanTokenAddress}
size="small" symbol={config.oceanTokenSymbol}
onClick={() => { logo="https://raw.githubusercontent.com/oceanprotocol/art/main/logo/token.png"
addOceanToWallet(config, web3Provider) className={styles.addToken}
}} />
>
{`Add ${config.oceanTokenSymbol}`}
</Button>
)} )}
</div> </div>
<p> <p>
{providerInfo?.name === 'Portis' && ( {web3ProviderInfo?.name === 'Portis' && (
<Button <Button
style="text" style="text"
size="small" size="small"

View File

@ -82,10 +82,8 @@ export default function FormStartCompute({
const data = useStaticQuery(contentQuery) const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childPagesJson const content = data.content.edges[0].node.childPagesJson
const { const { isValid, values }: FormikContextType<{ algorithm: string }> =
isValid, useFormikContext()
values
}: FormikContextType<{ algorithm: string }> = useFormikContext()
const { price, ddo } = useAsset() const { price, ddo } = useAsset()
const [totalPrice, setTotalPrice] = useState(price?.value) const [totalPrice, setTotalPrice] = useState(price?.value)
@ -108,17 +106,21 @@ export default function FormStartCompute({
useEffect(() => { useEffect(() => {
if (!price || !algorithmPrice) return if (!price || !algorithmPrice) return
const priceDataset = hasPreviousOrder ? 0 : Number(price.value) const priceDataset =
const priceAlgo = hasPreviousOrderSelectedComputeAsset hasPreviousOrder || hasDatatoken ? 0 : Number(price.value)
? 0 const priceAlgo =
: Number(algorithmPrice.value) hasPreviousOrderSelectedComputeAsset || hasDatatokenSelectedComputeAsset
? 0
: Number(algorithmPrice.value)
setTotalPrice(priceDataset + priceAlgo) setTotalPrice(priceDataset + priceAlgo)
}, [ }, [
price, price,
algorithmPrice, algorithmPrice,
hasPreviousOrder, hasPreviousOrder,
hasPreviousOrderSelectedComputeAsset hasDatatoken,
hasPreviousOrderSelectedComputeAsset,
hasDatatokenSelectedComputeAsset
]) ])
return ( return (
@ -138,7 +140,9 @@ export default function FormStartCompute({
hasPreviousOrderSelectedComputeAsset={ hasPreviousOrderSelectedComputeAsset={
hasPreviousOrderSelectedComputeAsset hasPreviousOrderSelectedComputeAsset
} }
hasDatatoken={hasDatatoken}
selectedComputeAssetTimeout={selectedComputeAssetTimeout} selectedComputeAssetTimeout={selectedComputeAssetTimeout}
hasDatatokenSelectedComputeAsset={hasDatatokenSelectedComputeAsset}
algorithmPrice={algorithmPrice} algorithmPrice={algorithmPrice}
totalPrice={totalPrice} totalPrice={totalPrice}
/> />
@ -162,6 +166,8 @@ export default function FormStartCompute({
stepText={stepText} stepText={stepText}
isLoading={isLoading} isLoading={isLoading}
type="submit" type="submit"
priceType={price?.type}
algorithmPriceType={algorithmPrice?.type}
/> />
</Form> </Form>
) )

View File

@ -8,8 +8,10 @@ import styles from './PriceOutput.module.css'
interface PriceOutputProps { interface PriceOutputProps {
totalPrice: number totalPrice: number
hasPreviousOrder: boolean hasPreviousOrder: boolean
hasDatatoken: boolean
assetTimeout: string assetTimeout: string
hasPreviousOrderSelectedComputeAsset: boolean hasPreviousOrderSelectedComputeAsset: boolean
hasDatatokenSelectedComputeAsset: boolean
algorithmPrice: BestPrice algorithmPrice: BestPrice
selectedComputeAssetTimeout: string selectedComputeAssetTimeout: string
} }
@ -17,11 +19,13 @@ interface PriceOutputProps {
function Row({ function Row({
price, price,
hasPreviousOrder, hasPreviousOrder,
hasDatatoken,
timeout, timeout,
sign sign
}: { }: {
price: number price: number
hasPreviousOrder?: boolean hasPreviousOrder?: boolean
hasDatatoken?: boolean
timeout?: string timeout?: string
sign?: string sign?: string
}) { }) {
@ -30,7 +34,7 @@ function Row({
<div className={styles.sign}>{sign}</div> <div className={styles.sign}>{sign}</div>
<div> <div>
<PriceUnit <PriceUnit
price={hasPreviousOrder ? '0' : `${price}`} price={hasPreviousOrder || hasDatatoken ? '0' : `${price}`}
small small
className={styles.price} className={styles.price}
/> />
@ -48,8 +52,10 @@ function Row({
export default function PriceOutput({ export default function PriceOutput({
totalPrice, totalPrice,
hasPreviousOrder, hasPreviousOrder,
hasDatatoken,
assetTimeout, assetTimeout,
hasPreviousOrderSelectedComputeAsset, hasPreviousOrderSelectedComputeAsset,
hasDatatokenSelectedComputeAsset,
algorithmPrice, algorithmPrice,
selectedComputeAssetTimeout selectedComputeAssetTimeout
}: PriceOutputProps): ReactElement { }: PriceOutputProps): ReactElement {
@ -63,11 +69,13 @@ export default function PriceOutput({
<div className={styles.calculation}> <div className={styles.calculation}>
<Row <Row
hasPreviousOrder={hasPreviousOrder} hasPreviousOrder={hasPreviousOrder}
hasDatatoken={hasDatatoken}
price={price?.value} price={price?.value}
timeout={assetTimeout} timeout={assetTimeout}
/> />
<Row <Row
hasPreviousOrder={hasPreviousOrderSelectedComputeAsset} hasPreviousOrder={hasPreviousOrderSelectedComputeAsset}
hasDatatoken={hasDatatokenSelectedComputeAsset}
price={algorithmPrice?.value} price={algorithmPrice?.value}
timeout={selectedComputeAssetTimeout} timeout={selectedComputeAssetTimeout}
sign="+" sign="+"

View File

@ -3,7 +3,6 @@ import {
DDO, DDO,
File as FileMetadata, File as FileMetadata,
Logger, Logger,
ServiceType,
publisherTrustedAlgorithm, publisherTrustedAlgorithm,
BestPrice BestPrice
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
@ -26,52 +25,39 @@ import {
getInitialValues, getInitialValues,
validationSchema validationSchema
} from '../../../../models/FormStartComputeDataset' } from '../../../../models/FormStartComputeDataset'
import { ComputeAlgorithm } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute' import {
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection' ComputeAlgorithm,
ComputeOutput
} from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
import { SearchQuery } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache' import { SearchQuery } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import axios from 'axios' import axios from 'axios'
import FormStartComputeDataset from './FormComputeDataset' import FormStartComputeDataset from './FormComputeDataset'
import styles from './index.module.css' import styles from './index.module.css'
import SuccessConfetti from '../../../atoms/SuccessConfetti' import SuccessConfetti from '../../../atoms/SuccessConfetti'
import Button from '../../../atoms/Button' import Button from '../../../atoms/Button'
import { gql, useQuery } from '@apollo/client'
import { FrePrice } from '../../../../@types/apollo/FrePrice'
import { PoolPrice } from '../../../../@types/apollo/PoolPrice'
import { secondsToString } from '../../../../utils/metadata' import { secondsToString } from '../../../../utils/metadata'
import { getPreviousOrders } from '../../../../utils/subgraph' import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
import AlgorithmDatasetsListForCompute from '../../AssetContent/AlgorithmDatasetsListForCompute'
import { getPreviousOrders, getPrice } from '../../../../utils/subgraph'
const SuccessAction = () => ( const SuccessAction = () => (
<Button style="text" to="/history" size="small"> <Button style="text" to="/history?defaultTab=ComputeJobs" size="small">
Go to history Go to history
</Button> </Button>
) )
const freQuery = gql`
query AlgorithmFrePrice($datatoken: String) {
fixedRateExchanges(orderBy: id, where: { datatoken: $datatoken }) {
rate
id
}
}
`
const poolQuery = gql`
query AlgorithmPoolPrice($datatoken: String) {
pools(where: { datatokenAddress: $datatoken }) {
spotPrice
}
}
`
export default function Compute({ export default function Compute({
isBalanceSufficient, isBalanceSufficient,
dtBalance, dtBalance,
file file,
fileIsLoading
}: { }: {
isBalanceSufficient: boolean isBalanceSufficient: boolean
dtBalance: string dtBalance: string
file: FileMetadata file: FileMetadata
fileIsLoading?: boolean
}): ReactElement { }): ReactElement {
const { marketFeeAddress } = useSiteMetadata() const { appConfig } = useSiteMetadata()
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { ocean, account, config } = useOcean() const { ocean, account, config } = useOcean()
const { price, type, ddo } = useAsset() const { price, type, ddo } = useAsset()
@ -86,38 +72,15 @@ export default function Compute({
const [isPublished, setIsPublished] = useState(false) const [isPublished, setIsPublished] = useState(false)
const [hasPreviousDatasetOrder, setHasPreviousDatasetOrder] = useState(false) const [hasPreviousDatasetOrder, setHasPreviousDatasetOrder] = useState(false)
const [previousDatasetOrderId, setPreviousDatasetOrderId] = useState<string>() const [previousDatasetOrderId, setPreviousDatasetOrderId] = useState<string>()
const [hasPreviousAlgorithmOrder, setHasPreviousAlgorithmOrder] = useState( const [hasPreviousAlgorithmOrder, setHasPreviousAlgorithmOrder] =
false useState(false)
)
const [algorithmDTBalance, setalgorithmDTBalance] = useState<string>() const [algorithmDTBalance, setalgorithmDTBalance] = useState<string>()
const [algorithmPrice, setAlgorithmPrice] = useState<BestPrice>() const [algorithmPrice, setAlgorithmPrice] = useState<BestPrice>()
const [variables, setVariables] = useState({}) const [previousAlgorithmOrderId, setPreviousAlgorithmOrderId] =
const [ useState<string>()
previousAlgorithmOrderId,
setPreviousAlgorithmOrderId
] = useState<string>()
const [datasetTimeout, setDatasetTimeout] = useState<string>() const [datasetTimeout, setDatasetTimeout] = useState<string>()
const [algorithmTimeout, setAlgorithmTimeout] = useState<string>() const [algorithmTimeout, setAlgorithmTimeout] = useState<string>()
/* eslint-disable @typescript-eslint/no-unused-vars */
const {
refetch: refetchFre,
startPolling: startPollingFre,
data: frePrice
} = useQuery<FrePrice>(freQuery, {
variables,
skip: false
})
const {
refetch: refetchPool,
startPolling: startPollingPool,
data: poolPrice
} = useQuery<PoolPrice>(poolQuery, {
variables,
skip: false
})
/* eslint-enable @typescript-eslint/no-unused-vars */
const isComputeButtonDisabled = const isComputeButtonDisabled =
isJobStarting === true || file === null || !ocean || !isBalanceSufficient isJobStarting === true || file === null || !ocean || !isBalanceSufficient
const hasDatatoken = Number(dtBalance) >= 1 const hasDatatoken = Number(dtBalance) >= 1
@ -163,7 +126,7 @@ export default function Compute({
const algorithmQuery = const algorithmQuery =
trustedAlgorithmList.length > 0 ? `(${algoQuerry}) AND` : `` trustedAlgorithmList.length > 0 ? `(${algoQuerry}) AND` : ``
const query = { const query = {
page: 1, offset: 500,
query: { query: {
query_string: { query_string: {
query: `${algorithmQuery} service.attributes.main.type:algorithm -isInPurgatory:true` query: `${algorithmQuery} service.attributes.main.type:algorithm -isInPurgatory:true`
@ -213,37 +176,10 @@ export default function Compute({
setDatasetTimeout(secondsToString(timeout)) setDatasetTimeout(secondsToString(timeout))
}, [ddo]) }, [ddo])
useEffect(() => {
if (
!frePrice ||
frePrice.fixedRateExchanges.length === 0 ||
algorithmPrice.type !== 'exchange'
)
return
setAlgorithmPrice((prevState) => ({
...prevState,
value: frePrice.fixedRateExchanges[0].rate,
address: frePrice.fixedRateExchanges[0].id
}))
}, [frePrice])
useEffect(() => {
if (
!poolPrice ||
poolPrice.pools.length === 0 ||
algorithmPrice.type !== 'pool'
)
return
setAlgorithmPrice((prevState) => ({
...prevState,
value: poolPrice.pools[0].spotPrice
}))
}, [poolPrice])
const initMetadata = useCallback(async (ddo: DDO): Promise<void> => { const initMetadata = useCallback(async (ddo: DDO): Promise<void> => {
if (!ddo) return if (!ddo) return
setAlgorithmPrice(ddo.price) const price = await getPrice(ddo)
setVariables({ datatoken: ddo?.dataToken.toLowerCase() }) setAlgorithmPrice(price)
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -259,26 +195,30 @@ export default function Compute({
}, [ocean, ddo, accountId]) }, [ocean, ddo, accountId])
useEffect(() => { useEffect(() => {
if (!ocean || !accountId || !selectedAlgorithmAsset) return if (!selectedAlgorithmAsset) return
if (selectedAlgorithmAsset.findServiceByType('access')) {
checkPreviousOrders(selectedAlgorithmAsset).then(() => {
if (
!hasPreviousAlgorithmOrder &&
selectedAlgorithmAsset.findServiceByType('compute')
) {
checkPreviousOrders(selectedAlgorithmAsset)
}
})
} else if (selectedAlgorithmAsset.findServiceByType('compute')) {
checkPreviousOrders(selectedAlgorithmAsset)
}
checkAssetDTBalance(selectedAlgorithmAsset)
initMetadata(selectedAlgorithmAsset) initMetadata(selectedAlgorithmAsset)
const { timeout } = ( const { timeout } = (
ddo.findServiceByType('access') || ddo.findServiceByType('compute') ddo.findServiceByType('access') || ddo.findServiceByType('compute')
).attributes.main ).attributes.main
setAlgorithmTimeout(secondsToString(timeout)) setAlgorithmTimeout(secondsToString(timeout))
if (accountId) {
if (selectedAlgorithmAsset.findServiceByType('access')) {
checkPreviousOrders(selectedAlgorithmAsset).then(() => {
if (
!hasPreviousAlgorithmOrder &&
selectedAlgorithmAsset.findServiceByType('compute')
) {
checkPreviousOrders(selectedAlgorithmAsset)
}
})
} else if (selectedAlgorithmAsset.findServiceByType('compute')) {
checkPreviousOrders(selectedAlgorithmAsset)
}
}
ocean && checkAssetDTBalance(selectedAlgorithmAsset)
}, [selectedAlgorithmAsset, ocean, accountId, hasPreviousAlgorithmOrder]) }, [selectedAlgorithmAsset, ocean, accountId, hasPreviousAlgorithmOrder])
// Output errors in toast UI // Output errors in toast UI
@ -358,7 +298,9 @@ export default function Compute({
ddo.id, ddo.id,
computeService.index, computeService.index,
computeAlgorithm, computeAlgorithm,
marketFeeAddress appConfig.marketFeeAddress,
undefined,
false
) )
assetOrderId && assetOrderId &&
@ -376,7 +318,9 @@ export default function Compute({
serviceAlgo.type, serviceAlgo.type,
accountId, accountId,
serviceAlgo.index, serviceAlgo.index,
marketFeeAddress appConfig.marketFeeAddress,
undefined,
false
) )
algorithmAssetOrderId && algorithmAssetOrderId &&
@ -395,7 +339,10 @@ export default function Compute({
computeAlgorithm.transferTxId = algorithmAssetOrderId computeAlgorithm.transferTxId = algorithmAssetOrderId
Logger.log('[compute] Starting compute job.') Logger.log('[compute] Starting compute job.')
const output = {} const output: ComputeOutput = {
publishAlgorithmLog: true,
publishOutput: true
}
const response = await ocean.compute.start( const response = await ocean.compute.start(
ddo.id, ddo.id,
assetOrderId, assetOrderId,
@ -414,9 +361,12 @@ export default function Compute({
Logger.log('[compute] Starting compute job response: ', response) Logger.log('[compute] Starting compute job response: ', response)
setHasPreviousDatasetOrder(true) await checkPreviousOrders(selectedAlgorithmAsset)
await checkPreviousOrders(ddo)
setIsPublished(true) setIsPublished(true)
} catch (error) { } catch (error) {
await checkPreviousOrders(selectedAlgorithmAsset)
await checkPreviousOrders(ddo)
setError('Failed to start job!') setError('Failed to start job!')
Logger.error('[compute] Failed to start job: ', error.message) Logger.error('[compute] Failed to start job: ', error.message)
} finally { } finally {
@ -427,15 +377,18 @@ export default function Compute({
return ( return (
<> <>
<div className={styles.info}> <div className={styles.info}>
<File file={file} small /> <File file={file} isLoading={fileIsLoading} small />
<Price price={price} conversion /> <Price price={price} conversion />
</div> </div>
{type === 'algorithm' ? ( {type === 'algorithm' ? (
<Alert <>
text="This algorithm has been set to private by the publisher and can't be downloaded. You can run it against any allowed data sets though!" <Alert
state="info" text="This algorithm has been set to private by the publisher and can't be downloaded. You can run it against any allowed data sets though!"
/> state="info"
/>
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} />
</>
) : ( ) : (
<Formik <Formik
initialValues={getInitialValues()} initialValues={getInitialValues()}
@ -475,7 +428,9 @@ export default function Compute({
action={<SuccessAction />} action={<SuccessAction />}
/> />
)} )}
<Web3Feedback isBalanceSufficient={isBalanceSufficient} /> {type !== 'algorithm' && (
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
)}
</footer> </footer>
</> </>
) )

View File

@ -5,7 +5,10 @@
.info { .info {
display: flex; display: flex;
width: 100%; width: auto;
margin-left: -2rem;
margin-right: -2rem;
padding: 0 calc(var(--spacer)) 0 calc(var(--spacer));
} }
.filewrapper { .filewrapper {

View File

@ -16,6 +16,7 @@ import { useWeb3 } from '../../../providers/Web3'
import { usePricing } from '../../../hooks/usePricing' import { usePricing } from '../../../hooks/usePricing'
import { useConsume } from '../../../hooks/useConsume' import { useConsume } from '../../../hooks/useConsume'
import ButtonBuy from '../../atoms/ButtonBuy' import ButtonBuy from '../../atoms/ButtonBuy'
import AlgorithmDatasetsListForCompute from '../AssetContent/AlgorithmDatasetsListForCompute'
const previousOrderQuery = gql` const previousOrderQuery = gql`
query PreviousOrder($id: String!, $account: String!) { query PreviousOrder($id: String!, $account: String!) {
@ -35,31 +36,28 @@ export default function Consume({
ddo, ddo,
file, file,
isBalanceSufficient, isBalanceSufficient,
dtBalance dtBalance,
fileIsLoading
}: { }: {
ddo: DDO ddo: DDO
file: FileMetadata file: FileMetadata
isBalanceSufficient: boolean isBalanceSufficient: boolean
dtBalance: string dtBalance: string
fileIsLoading?: boolean
}): ReactElement { }): ReactElement {
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { ocean } = useOcean() const { ocean } = useOcean()
const { marketFeeAddress } = useSiteMetadata() const { appConfig } = useSiteMetadata()
const [hasPreviousOrder, setHasPreviousOrder] = useState(false) const [hasPreviousOrder, setHasPreviousOrder] = useState(false)
const [previousOrderId, setPreviousOrderId] = useState<string>() const [previousOrderId, setPreviousOrderId] = useState<string>()
const { isInPurgatory, price, type } = useAsset() const { isInPurgatory, price, type } = useAsset()
const { const { buyDT, pricingStepText, pricingError, pricingIsLoading } =
buyDT, usePricing()
pricingStepText, const { consumeStepText, consume, consumeError, isLoading } = useConsume()
pricingError,
pricingIsLoading
} = usePricing()
const { consumeStepText, consume, consumeError } = useConsume()
const [isDisabled, setIsDisabled] = useState(true) const [isDisabled, setIsDisabled] = useState(true)
const [hasDatatoken, setHasDatatoken] = useState(false) const [hasDatatoken, setHasDatatoken] = useState(false)
const [isConsumable, setIsConsumable] = useState(true) const [isConsumable, setIsConsumable] = useState(true)
const [assetTimeout, setAssetTimeout] = useState('') const [assetTimeout, setAssetTimeout] = useState('')
const { data } = useQuery<OrdersData>(previousOrderQuery, { const { data } = useQuery<OrdersData>(previousOrderQuery, {
variables: { variables: {
id: ddo.dataToken?.toLowerCase(), id: ddo.dataToken?.toLowerCase(),
@ -90,7 +88,7 @@ export default function Consume({
useEffect(() => { useEffect(() => {
const { timeout } = ddo.findServiceByType('access').attributes.main const { timeout } = ddo.findServiceByType('access').attributes.main
setAssetTimeout(secondsToString(timeout)) setAssetTimeout(timeout.toString())
}, [ddo]) }, [ddo])
useEffect(() => { useEffect(() => {
@ -126,22 +124,28 @@ export default function Consume({
]) ])
async function handleConsume() { async function handleConsume() {
!hasPreviousOrder && !hasDatatoken && (await buyDT('1', price, ddo)) if (!hasPreviousOrder && !hasDatatoken) {
await consume( const tx = await buyDT('1', price, ddo)
if (tx === undefined) return
}
const error = await consume(
ddo.id, ddo.id,
ddo.dataToken, ddo.dataToken,
'access', 'access',
marketFeeAddress, appConfig.marketFeeAddress,
previousOrderId previousOrderId
) )
setHasPreviousOrder(true) error || setHasPreviousOrder(true)
} }
// Output errors in UI // Output errors in UI
useEffect(() => { useEffect(() => {
consumeError && toast.error(consumeError) consumeError && toast.error(consumeError)
}, [consumeError])
useEffect(() => {
pricingError && toast.error(pricingError) pricingError && toast.error(pricingError)
}, [consumeError, pricingError]) }, [pricingError])
const PurchaseButton = () => ( const PurchaseButton = () => (
<ButtonBuy <ButtonBuy
@ -152,10 +156,11 @@ export default function Consume({
dtSymbol={ddo.dataTokenInfo?.symbol} dtSymbol={ddo.dataTokenInfo?.symbol}
dtBalance={dtBalance} dtBalance={dtBalance}
onClick={handleConsume} onClick={handleConsume}
assetTimeout={assetTimeout} assetTimeout={secondsToString(parseInt(assetTimeout))}
assetType={type} assetType={type}
stepText={consumeStepText || pricingStepText} stepText={consumeStepText || pricingStepText}
isLoading={pricingIsLoading} isLoading={pricingIsLoading || isLoading}
priceType={price?.type}
/> />
) )
@ -163,13 +168,16 @@ export default function Consume({
<aside className={styles.consume}> <aside className={styles.consume}>
<div className={styles.info}> <div className={styles.info}>
<div className={styles.filewrapper}> <div className={styles.filewrapper}>
<File file={file} /> <File file={file} isLoading={fileIsLoading} />
</div> </div>
<div className={styles.pricewrapper}> <div className={styles.pricewrapper}>
<Price price={price} conversion /> <Price price={price} conversion />
{!isInPurgatory && <PurchaseButton />} {!isInPurgatory && <PurchaseButton />}
</div> </div>
</div> </div>
{type === 'algorithm' && (
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} />
)}
<footer className={styles.feedback}> <footer className={styles.feedback}>
<Web3Feedback isBalanceSufficient={isBalanceSufficient} /> <Web3Feedback isBalanceSufficient={isBalanceSufficient} />
</footer> </footer>

View File

@ -0,0 +1,48 @@
import { DDO, Credentials, CredentialType } from '@oceanprotocol/lib'
import React, { ReactElement, useEffect, useState } from 'react'
import { AdvancedSettingsForm } from '../../../../models/FormEditCredential'
import { useOcean } from '../../../../providers/Ocean'
import DebugOutput from '../../../atoms/DebugOutput'
export interface AdvancedSettings {
credentail: Credentials
isOrderDisabled: boolean
}
export default function DebugEditCredential({
values,
ddo,
credentialType
}: {
values: AdvancedSettingsForm
ddo: DDO
credentialType: CredentialType
}): ReactElement {
const { ocean } = useOcean()
const [advancedSettings, setAdvancedSettings] = useState<AdvancedSettings>()
useEffect(() => {
if (!ocean) return
async function transformValues() {
const newDdo = await ocean.assets.updateCredentials(
ddo,
credentialType,
values.allow,
values.deny
)
setAdvancedSettings({
credentail: newDdo.credentials,
isOrderDisabled: values.isOrderDisabled
})
}
transformValues()
}, [values, ddo, ocean])
return (
<>
<DebugOutput title="Collected Form Values" output={values} />
<DebugOutput title="Transformed Form Values" output={advancedSettings} />
</>
)
}

View File

@ -13,10 +13,8 @@ export default function DebugEditCompute({
ddo: DDO ddo: DDO
}): ReactElement { }): ReactElement {
const { ocean } = useOcean() const { ocean } = useOcean()
const [ const [formTransformed, setFormTransformed] =
formTransformed, useState<ServiceComputePrivacy>()
setFormTransformed
] = useState<ServiceComputePrivacy>()
useEffect(() => { useEffect(() => {
if (!ocean) return if (!ocean) return

View File

@ -0,0 +1,163 @@
import { Formik } from 'formik'
import React, { ReactElement, useState } from 'react'
import { useAsset } from '../../../../providers/Asset'
import { useUserPreferences } from '../../../../providers/UserPreferences'
import styles from './index.module.css'
import { Logger, CredentialType, DDO } from '@oceanprotocol/lib'
import MetadataFeedback from '../../../molecules/MetadataFeedback'
import { graphql, useStaticQuery } from 'gatsby'
import { useWeb3 } from '../../../../providers/Web3'
import { useOcean } from '../../../../providers/Ocean'
import FormAdvancedSettings from './FormAdvancedSettings'
import {
AdvancedSettingsForm,
getInitialValues,
validationSchema
} from '../../../../models/FormEditCredential'
import DebugEditCredential from './DebugEditAdvancedSettings'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
const contentQuery = graphql`
query EditAvanceSettingsQuery {
content: allFile(
filter: { relativePath: { eq: "pages/editAdvancedSettings.json" } }
) {
edges {
node {
childPagesJson {
description
form {
success
successAction
error
data {
name
placeholder
label
help
type
options
}
}
}
}
}
}
}
`
function getDefaultCredentialType(credentialType: string): CredentialType {
switch (credentialType) {
case 'address':
return CredentialType.address
case 'credential3Box':
return CredentialType.credential3Box
default:
return CredentialType.address
}
}
export default function EditAdvancedSettings({
setShowEdit
}: {
setShowEdit: (show: boolean) => void
}): ReactElement {
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childPagesJson
const { debug } = useUserPreferences()
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { metadata, ddo, refreshDdo } = useAsset()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const { appConfig } = useSiteMetadata()
const hasFeedback = error || success
const credentialType = getDefaultCredentialType(appConfig.credentialType)
async function handleSubmit(
values: Partial<AdvancedSettingsForm>,
resetForm: () => void
) {
try {
let newDdo: DDO
newDdo = await ocean.assets.updateCredentials(
ddo,
credentialType,
values.allow,
values.deny
)
newDdo = await ocean.assets.editMetadata(newDdo, {
status: {
isOrderDisabled: values.isOrderDisabled
}
})
const storedddo = await ocean.assets.updateMetadata(newDdo, accountId)
if (!storedddo) {
setError(content.form.error)
Logger.error(content.form.error)
return
} else {
setSuccess(content.form.success)
resetForm()
}
} catch (error) {
Logger.error(error.message)
setError(error.message)
}
}
return (
<Formik
initialValues={getInitialValues(ddo, credentialType)}
validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
await handleSubmit(values, resetForm)
}}
>
{({ isSubmitting, values }) =>
isSubmitting || hasFeedback ? (
<MetadataFeedback
title="Updating Data Set"
error={error}
success={success}
setError={setError}
successAction={{
name: content.form.successAction,
onClick: async () => {
await refreshDdo()
setShowEdit(false)
}
}}
/>
) : (
<>
<p className={styles.description}>{content.description}</p>
<article className={styles.grid}>
<FormAdvancedSettings
data={content.form.data}
setShowEdit={setShowEdit}
/>
</article>
{debug === true && (
<div className={styles.grid}>
<DebugEditCredential
values={values}
ddo={ddo}
credentialType={credentialType}
/>
</div>
)}
</>
)
}
</Formik>
)
}

View File

@ -16,6 +16,10 @@ import { useUserPreferences } from '../../../../providers/UserPreferences'
import DebugEditCompute from './DebugEditCompute' import DebugEditCompute from './DebugEditCompute'
import styles from './index.module.css' import styles from './index.module.css'
import { transformComputeFormToServiceComputePrivacy } from '../../../../utils/compute' import { transformComputeFormToServiceComputePrivacy } from '../../../../utils/compute'
import {
setMinterToDispenser,
setMinterToPublisher
} from '../../../../utils/freePrice'
const contentQuery = graphql` const contentQuery = graphql`
query EditComputeDataQuery { query EditComputeDataQuery {
@ -62,7 +66,7 @@ export default function EditComputeDataset({
const { debug } = useUserPreferences() const { debug } = useUserPreferences()
const { ocean } = useOcean() const { ocean } = useOcean()
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { ddo, refreshDdo } = useAsset() const { ddo, refreshDdo, price } = useAsset()
const [success, setSuccess] = useState<string>() const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
@ -73,6 +77,15 @@ export default function EditComputeDataset({
resetForm: () => void resetForm: () => void
) { ) {
try { try {
if (price.type === 'free') {
const tx = await setMinterToPublisher(
ocean,
ddo.dataToken,
accountId,
setError
)
if (!tx) return
}
const privacy = await transformComputeFormToServiceComputePrivacy( const privacy = await transformComputeFormToServiceComputePrivacy(
values, values,
ocean ocean
@ -99,6 +112,15 @@ export default function EditComputeDataset({
Logger.error(content.form.error) Logger.error(content.form.error)
return return
} else { } else {
if (price.type === 'free') {
const tx = await setMinterToDispenser(
ocean,
ddo.dataToken,
accountId,
setError
)
if (!tx) return
}
// Edit succeeded // Edit succeeded
setSuccess(content.form.success) setSuccess(content.form.success)
resetForm() resetForm()

View File

@ -0,0 +1,59 @@
import React, { ChangeEvent, ReactElement } from 'react'
import styles from './FormEditMetadata.module.css'
import { Field, Form, FormikContextType, useFormikContext } from 'formik'
import Button from '../../../atoms/Button'
import Input from '../../../atoms/Input'
import { FormFieldProps } from '../../../../@types/Form'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import { AdvancedSettingsForm } from '../../../../models/FormEditCredential'
export default function FormAdvancedSettings({
data,
setShowEdit
}: {
data: FormFieldProps[]
setShowEdit: (show: boolean) => void
}): ReactElement {
const { accountId } = useWeb3()
const { ocean, config } = useOcean()
const {
isValid,
validateField,
setFieldValue
}: FormikContextType<Partial<AdvancedSettingsForm>> = useFormikContext()
function handleFieldChange(
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps
) {
validateField(field.name)
if (e.target.type === 'checkbox')
setFieldValue(field.name, e.target.checked)
else setFieldValue(field.name, e.target.value)
}
return (
<Form className={styles.form}>
{data.map((field: FormFieldProps) => (
<Field
key={field.name}
{...field}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
))}
<footer className={styles.actions}>
<Button style="primary" disabled={!ocean || !accountId || !isValid}>
Submit
</Button>
<Button style="text" onClick={() => setShowEdit(false)}>
Cancel
</Button>
</footer>
</Form>
)
}

View File

@ -29,22 +29,19 @@ export default function FormEditComputeDataset({
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { ocean, config } = useOcean() const { ocean, config } = useOcean()
const { ddo } = useAsset() const { ddo } = useAsset()
const { const { isValid, values }: FormikContextType<ComputePrivacyForm> =
isValid, useFormikContext()
values
}: FormikContextType<ComputePrivacyForm> = useFormikContext()
const [allAlgorithms, setAllAlgorithms] = useState<AssetSelectionAsset[]>() const [allAlgorithms, setAllAlgorithms] = useState<AssetSelectionAsset[]>()
const { publisherTrustedAlgorithms } = ddo?.findServiceByType( const { publisherTrustedAlgorithms } =
'compute' ddo?.findServiceByType('compute').attributes.main.privacy
).attributes.main.privacy
async function getAlgorithmList( async function getAlgorithmList(
publisherTrustedAlgorithms: PublisherTrustedAlgorithm[] publisherTrustedAlgorithms: PublisherTrustedAlgorithm[]
): Promise<AssetSelectionAsset[]> { ): Promise<AssetSelectionAsset[]> {
const source = axios.CancelToken.source() const source = axios.CancelToken.source()
const query = { const query = {
page: 1, offset: 500,
query: { query: {
query_string: { query_string: {
query: `service.attributes.main.type:algorithm -isInPurgatory:true` query: `service.attributes.main.type:algorithm -isInPurgatory:true`

View File

@ -47,15 +47,17 @@ export default function FormEditMetadata({
data, data,
setShowEdit, setShowEdit,
setTimeoutStringValue, setTimeoutStringValue,
values values,
showPrice
}: { }: {
data: FormFieldProps[] data: FormFieldProps[]
setShowEdit: (show: boolean) => void setShowEdit: (show: boolean) => void
setTimeoutStringValue: (value: string) => void setTimeoutStringValue: (value: string) => void
values: Partial<MetadataPublishFormDataset> values: Partial<MetadataPublishFormDataset>
showPrice: boolean
}): ReactElement { }): ReactElement {
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { ocean } = useOcean() const { ocean, config } = useOcean()
const { const {
isValid, isValid,
validateField, validateField,
@ -79,16 +81,20 @@ export default function FormEditMetadata({
return ( return (
<Form className={styles.form}> <Form className={styles.form}>
{data.map((field: FormFieldProps) => ( {data.map(
<Field (field: FormFieldProps) =>
key={field.name} (!showPrice && field.name === 'price') || (
{...field} <Field
component={Input} key={field.name}
onChange={(e: ChangeEvent<HTMLInputElement>) => {...field}
handleFieldChange(e, field) component={Input}
} prefix={field.name === 'price' && config.oceanTokenSymbol}
/> onChange={(e: ChangeEvent<HTMLInputElement>) =>
))} handleFieldChange(e, field)
}
/>
)
)}
<footer className={styles.actions}> <footer className={styles.actions}>
<Button <Button

View File

@ -18,6 +18,10 @@ import MetadataFeedback from '../../../molecules/MetadataFeedback'
import { graphql, useStaticQuery } from 'gatsby' import { graphql, useStaticQuery } from 'gatsby'
import { useWeb3 } from '../../../../providers/Web3' import { useWeb3 } from '../../../../providers/Web3'
import { useOcean } from '../../../../providers/Ocean' import { useOcean } from '../../../../providers/Ocean'
import {
setMinterToDispenser,
setMinterToPublisher
} from '../../../../utils/freePrice'
const contentQuery = graphql` const contentQuery = graphql`
query EditMetadataQuery { query EditMetadataQuery {
@ -36,6 +40,7 @@ const contentQuery = graphql`
label label
help help
type type
min
required required
sortOptions sortOptions
options options
@ -60,7 +65,7 @@ export default function Edit({
const { debug } = useUserPreferences() const { debug } = useUserPreferences()
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { ocean } = useOcean() const { ocean } = useOcean()
const { metadata, ddo, refreshDdo } = useAsset() const { metadata, ddo, refreshDdo, price } = useAsset()
const [success, setSuccess] = useState<string>() const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const [timeoutStringValue, setTimeoutStringValue] = useState<string>() const [timeoutStringValue, setTimeoutStringValue] = useState<string>()
@ -70,11 +75,32 @@ export default function Edit({
const hasFeedback = error || success const hasFeedback = error || success
async function updateFixedPrice(newPrice: number) {
const setPriceResp = await ocean.fixedRateExchange.setRate(
price.address,
newPrice,
accountId
)
if (!setPriceResp) {
setError(content.form.error)
Logger.error(content.form.error)
}
}
async function handleSubmit( async function handleSubmit(
values: Partial<MetadataEditForm>, values: Partial<MetadataEditForm>,
resetForm: () => void resetForm: () => void
) { ) {
try { try {
if (price.type === 'free') {
const tx = await setMinterToPublisher(
ocean,
ddo.dataToken,
accountId,
setError
)
if (!tx) return
}
// Construct new DDO with new values // Construct new DDO with new values
const ddoEditedMetdata = await ocean.assets.editMetadata(ddo, { const ddoEditedMetdata = await ocean.assets.editMetadata(ddo, {
title: values.name, title: values.name,
@ -82,6 +108,10 @@ export default function Edit({
links: typeof values.links !== 'string' ? values.links : [] links: typeof values.links !== 'string' ? values.links : []
}) })
price.type === 'exchange' &&
values.price !== price.value &&
(await updateFixedPrice(values.price))
if (!ddoEditedMetdata) { if (!ddoEditedMetdata) {
setError(content.form.error) setError(content.form.error)
Logger.error(content.form.error) Logger.error(content.form.error)
@ -115,6 +145,15 @@ export default function Edit({
Logger.error(content.form.error) Logger.error(content.form.error)
return return
} else { } else {
if (price.type === 'free') {
const tx = await setMinterToDispenser(
ocean,
ddo.dataToken,
accountId,
setError
)
if (!tx) return
}
// Edit succeeded // Edit succeeded
setSuccess(content.form.success) setSuccess(content.form.success)
resetForm() resetForm()
@ -127,7 +166,7 @@ export default function Edit({
return ( return (
<Formik <Formik
initialValues={getInitialValues(metadata, timeout)} initialValues={getInitialValues(metadata, timeout, price.value)}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => { onSubmit={async (values, { resetForm }) => {
// move user's focus to top of screen // move user's focus to top of screen
@ -160,6 +199,7 @@ export default function Edit({
setShowEdit={setShowEdit} setShowEdit={setShowEdit}
setTimeoutStringValue={setTimeoutStringValue} setTimeoutStringValue={setTimeoutStringValue}
values={initialValues} values={initialValues}
showPrice={price.type === 'exchange'}
/> />
<aside> <aside>

View File

@ -90,7 +90,7 @@ export default function FormAdd({
amountMax, amountMax,
coin, coin,
poolAddress, poolAddress,
ocean.pool, ocean?.pool,
setNewPoolTokens, setNewPoolTokens,
setNewPoolShare setNewPoolShare
]) ])

View File

@ -48,11 +48,8 @@ export default function Output({
coin: string coin: string
}): ReactElement { }): ReactElement {
const data = useStaticQuery(contentQuery) const data = useStaticQuery(contentQuery)
const { const { help, titleIn, titleOut } =
help, data.content.edges[0].node.childContentJson.pool.add.output
titleIn,
titleOut
} = data.content.edges[0].node.childContentJson.pool.add.output
// Connect with form // Connect with form
const { values }: FormikContextType<FormAddLiquidity> = useFormikContext() const { values }: FormikContextType<FormAddLiquidity> = useFormikContext()

View File

@ -174,7 +174,7 @@ export default function Add({
) : ( ) : (
<Alert <Alert
className={styles.warning} className={styles.warning}
text={content.warning.main} text={content.warning}
state="info" state="info"
action={{ action={{
name: 'I understand', name: 'I understand',

View File

@ -123,11 +123,10 @@ export default function Graph(): ReactElement {
const { price } = useAsset() const { price } = useAsset()
const [lastBlock, setLastBlock] = useState(0) const [lastBlock, setLastBlock] = useState<number>(0)
const [priceHistory, setPriceHistory] = useState([]) const [priceHistory, setPriceHistory] = useState([])
const [liquidityHistory, setLiquidityHistory] = useState([]) const [liquidityHistory, setLiquidityHistory] = useState([])
const [timestamps, setTimestamps] = useState([]) const [timestamps, setTimestamps] = useState([])
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [graphData, setGraphData] = useState<ChartData>() const [graphData, setGraphData] = useState<ChartData>()
@ -156,7 +155,6 @@ export default function Graph(): ReactElement {
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
}) })
] ]
setTimestamps(latestTimestamps) setTimestamps(latestTimestamps)
const latestLiquidtyHistory = [ const latestLiquidtyHistory = [
@ -170,17 +168,20 @@ export default function Graph(): ReactElement {
...priceHistory, ...priceHistory,
...data.poolTransactions.map((item) => item.spotPrice) ...data.poolTransactions.map((item) => item.spotPrice)
] ]
setPriceHistory(latestPriceHistory) setPriceHistory(latestPriceHistory)
if (data.poolTransactions.length > 0) { if (data.poolTransactions.length > 0) {
const newBlock =
data.poolTransactions[data.poolTransactions.length - 1].block
if (newBlock === lastBlock) return
setLastBlock( setLastBlock(
data.poolTransactions[data.poolTransactions.length - 1].block data.poolTransactions[data.poolTransactions.length - 1].block
) )
refetch() refetch()
} else { } else {
setIsLoading(false)
setGraphData({ setGraphData({
labels: timestamps.slice(0), labels: latestTimestamps.slice(0),
datasets: [ datasets: [
{ {
...lineStyle, ...lineStyle,
@ -194,6 +195,7 @@ export default function Graph(): ReactElement {
} }
] ]
}) })
setIsLoading(false)
} }
}, [data, graphType]) }, [data, graphType])

View File

@ -21,6 +21,7 @@ import UserLiquidity from '../../../atoms/UserLiquidity'
import InputElement from '../../../atoms/Input/InputElement' import InputElement from '../../../atoms/Input/InputElement'
import { useOcean } from '../../../../providers/Ocean' import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3' import { useWeb3 } from '../../../../providers/Web3'
import Decimal from 'decimal.js'
const contentQuery = graphql` const contentQuery = graphql`
query PoolRemoveQuery { query PoolRemoveQuery {
@ -80,6 +81,8 @@ export default function Remove({
const [minOceanAmount, setMinOceanAmount] = useState<string>('0') const [minOceanAmount, setMinOceanAmount] = useState<string>('0')
const [minDatatokenAmount, setMinDatatokenAmount] = useState<string>('0') const [minDatatokenAmount, setMinDatatokenAmount] = useState<string>('0')
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
async function handleRemoveLiquidity() { async function handleRemoveLiquidity() {
setIsLoading(true) setIsLoading(true)
try { try {
@ -155,21 +158,19 @@ export default function Remove({
totalPoolTokens totalPoolTokens
]) ])
async function calculateAmountOfOceansRemoved(amountPoolShares: string) {
const oceanAmount = await ocean.pool.getOceanRemovedforPoolShares(
poolAddress,
amountPoolShares
)
setAmountOcean(oceanAmount)
}
useEffect(() => { useEffect(() => {
const minOceanAmount = const minOceanAmount = new Decimal(amountOcean)
(Number(amountOcean) * (100 - Number(slippage))) / 100 .mul(new Decimal(100).minus(new Decimal(slippage)))
const minDatatokenAmount = .dividedBy(100)
(Number(amountDatatoken) * (100 - Number(slippage))) / 100 .toString()
setMinOceanAmount(`${minOceanAmount}`)
setMinDatatokenAmount(`${minDatatokenAmount}`) const minDatatokenAmount = new Decimal(amountDatatoken)
.mul(new Decimal(100).minus(new Decimal(slippage)))
.dividedBy(100)
.toString()
setMinOceanAmount(minOceanAmount.slice(0, 18))
setMinDatatokenAmount(minDatatokenAmount.slice(0, 18))
}, [slippage, amountOcean, amountDatatoken, isAdvanced]) }, [slippage, amountOcean, amountDatatoken, isAdvanced])
// Set amountPoolShares based on set slider value // Set amountPoolShares based on set slider value
@ -177,19 +178,24 @@ export default function Remove({
setAmountPercent(e.target.value) setAmountPercent(e.target.value)
if (!poolTokens) return if (!poolTokens) return
const amountPoolShares = (Number(e.target.value) / 100) * Number(poolTokens) const amountPoolShares = new Decimal(e.target.value)
setAmountPoolShares(`${amountPoolShares}`) .dividedBy(100)
calculateAmountOfOceansRemoved(`${amountPoolShares}`) .mul(new Decimal(poolTokens))
.toString()
setAmountPoolShares(`${amountPoolShares.slice(0, 18)}`)
} }
function handleMaxButton(e: ChangeEvent<HTMLInputElement>) { function handleMaxButton(e: ChangeEvent<HTMLInputElement>) {
e.preventDefault() e.preventDefault()
setAmountPercent(amountMaxPercent) setAmountPercent(amountMaxPercent)
const amountPoolShares = const amountPoolShares = new Decimal(amountMaxPercent)
(Number(amountMaxPercent) / 100) * Number(poolTokens) .dividedBy(100)
setAmountPoolShares(`${amountPoolShares}`) .mul(new Decimal(poolTokens))
calculateAmountOfOceansRemoved(`${amountPoolShares}`) .toString()
setAmountPoolShares(`${amountPoolShares.slice(0, 18)}`)
} }
function handleAdvancedButton(e: FormEvent<HTMLButtonElement>) { function handleAdvancedButton(e: FormEvent<HTMLButtonElement>) {

View File

@ -7,7 +7,7 @@ import { useAsset } from '../../../../providers/Asset'
export default function Transactions(): ReactElement { export default function Transactions(): ReactElement {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const { ddo } = useAsset() const { price } = useAsset()
function handleClick() { function handleClick() {
setOpen(!open) setOpen(!open)
} }
@ -29,7 +29,7 @@ export default function Transactions(): ReactElement {
</Button> </Button>
</h3> </h3>
{open === true && ( {open === true && (
<PoolTransactions poolAddress={ddo.price?.address} minimal /> <PoolTransactions poolAddress={price?.address} minimal />
)} )}
</div> </div>
) )

View File

@ -44,6 +44,7 @@ const poolLiquidityQuery = gql`
id id
totalShares totalShares
swapFee swapFee
spotPrice
tokens { tokens {
tokenAddress tokenAddress
balance balance
@ -82,10 +83,8 @@ export default function Pool(): ReactElement {
const [totalUserLiquidityInOcean, setTotalUserLiquidityInOcean] = useState(0) const [totalUserLiquidityInOcean, setTotalUserLiquidityInOcean] = useState(0)
const [totalLiquidityInOcean, setTotalLiquidityInOcean] = useState(0) const [totalLiquidityInOcean, setTotalLiquidityInOcean] = useState(0)
const [ const [creatorTotalLiquidityInOcean, setCreatorTotalLiquidityInOcean] =
creatorTotalLiquidityInOcean, useState(0)
setCreatorTotalLiquidityInOcean
] = useState(0)
const [creatorLiquidity, setCreatorLiquidity] = useState<PoolBalance>() const [creatorLiquidity, setCreatorLiquidity] = useState<PoolBalance>()
const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>() const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>()
const [creatorPoolShare, setCreatorPoolShare] = useState<string>() const [creatorPoolShare, setCreatorPoolShare] = useState<string>()
@ -94,8 +93,8 @@ export default function Pool(): ReactElement {
const [refreshPool, setRefreshPool] = useState(false) const [refreshPool, setRefreshPool] = useState(false)
const { data: dataLiquidity } = useQuery<PoolLiquidity>(poolLiquidityQuery, { const { data: dataLiquidity } = useQuery<PoolLiquidity>(poolLiquidityQuery, {
variables: { variables: {
id: ddo.price.address.toLowerCase(), id: price.address.toLowerCase(),
shareId: `${ddo.price.address.toLowerCase()}-${ddo.publicKey[0].owner.toLowerCase()}` shareId: `${price.address.toLowerCase()}-${ddo.publicKey[0].owner.toLowerCase()}`
}, },
pollInterval: 5000 pollInterval: 5000
}) })
@ -141,7 +140,8 @@ export default function Pool(): ReactElement {
setCreatorLiquidity(creatorLiquidity) setCreatorLiquidity(creatorLiquidity)
const totalCreatorLiquidityInOcean = const totalCreatorLiquidityInOcean =
creatorLiquidity?.ocean + creatorLiquidity?.datatoken * price?.value creatorLiquidity?.ocean +
creatorLiquidity?.datatoken * dataLiquidity.pool.spotPrice
setCreatorTotalLiquidityInOcean(totalCreatorLiquidityInOcean) setCreatorTotalLiquidityInOcean(totalCreatorLiquidityInOcean)
const creatorPoolShare = const creatorPoolShare =
price?.ocean && price?.ocean &&
@ -168,7 +168,8 @@ export default function Pool(): ReactElement {
const totalUserLiquidityInOcean = const totalUserLiquidityInOcean =
userLiquidity?.ocean + userLiquidity?.datatoken * price?.value userLiquidity?.ocean + userLiquidity?.datatoken * price?.value
setTotalUserLiquidityInOcean(totalUserLiquidityInOcean) setTotalUserLiquidityInOcean(totalUserLiquidityInOcean)
const totalLiquidityInOcean = price?.ocean + price?.datatoken * price?.value const totalLiquidityInOcean =
Number(price?.ocean) + Number(price?.datatoken) * Number(price?.value)
setTotalLiquidityInOcean(totalLiquidityInOcean) setTotalLiquidityInOcean(totalLiquidityInOcean)
}, [userLiquidity, price, poolTokens, totalPoolTokens]) }, [userLiquidity, price, poolTokens, totalPoolTokens])
@ -250,7 +251,7 @@ export default function Pool(): ReactElement {
<ExplorerLink <ExplorerLink
networkId={networkId} networkId={networkId}
path={ path={
networkId === 137 networkId === 137 || networkId === 1287
? `tokens/${ddo.dataToken}` ? `tokens/${ddo.dataToken}`
: `token/${ddo.dataToken}` : `token/${ddo.dataToken}`
} }

View File

@ -9,10 +9,11 @@ export async function getMaxPercentRemove(
poolAddress poolAddress
) )
const amountMaxPoolShares = await ocean.pool.getPoolSharesRequiredToRemoveOcean( const amountMaxPoolShares =
poolAddress, await ocean.pool.getPoolSharesRequiredToRemoveOcean(
amountMaxOcean poolAddress,
) amountMaxOcean
)
let amountMaxPercent = `${Math.floor( let amountMaxPercent = `${Math.floor(
(Number(amountMaxPoolShares) / Number(poolTokens)) * 100 (Number(amountMaxPoolShares) / Number(poolTokens)) * 100

View File

@ -64,7 +64,7 @@ export default function FormTrade({
.required('Required') .required('Required')
.nullable(), .nullable(),
datatoken: Yup.number() datatoken: Yup.number()
.max(maxDt, `Must be less or equal than ${maximumDt}`) .max(maximumDt, (param) => `Must be less or equal than ${param.max}`)
.min(0.00001, (param) => `Must be more or equal to ${param.min}`) .min(0.00001, (param) => `Must be more or equal to ${param.min}`)
.required('Required') .required('Required')
.nullable(), .nullable(),

View File

@ -6,10 +6,8 @@ import styles from './Slippage.module.css'
export default function Slippage(): ReactElement { export default function Slippage(): ReactElement {
// Connect with form // Connect with form
const { const { setFieldValue, values }: FormikContextType<FormTradeData> =
setFieldValue, useFormikContext()
values
}: FormikContextType<FormTradeData> = useFormikContext()
function handleChange(e: ChangeEvent<HTMLSelectElement>) { function handleChange(e: ChangeEvent<HTMLSelectElement>) {
setFieldValue('slippage', e.target.value) setFieldValue('slippage', e.target.value)

View File

@ -1,8 +1,9 @@
import React, { ReactElement, useState, useEffect } from 'react' import React, { ReactElement, useState, useEffect } from 'react'
import Permission from '../Permission'
import styles from './index.module.css' import styles from './index.module.css'
import Compute from './Compute' import Compute from './Compute'
import Consume from './Consume' import Consume from './Consume'
import { Logger } from '@oceanprotocol/lib' import { Logger, File as FileMetadata, DID } from '@oceanprotocol/lib'
import Tabs from '../../atoms/Tabs' import Tabs from '../../atoms/Tabs'
import compareAsBN from '../../../utils/compareAsBN' import compareAsBN from '../../../utils/compareAsBN'
import Pool from './Pool' import Pool from './Pool'
@ -10,21 +11,47 @@ import Trade from './Trade'
import { useAsset } from '../../../providers/Asset' import { useAsset } from '../../../providers/Asset'
import { useOcean } from '../../../providers/Ocean' import { useOcean } from '../../../providers/Ocean'
import { useWeb3 } from '../../../providers/Web3' import { useWeb3 } from '../../../providers/Web3'
import { fileinfo, getFileInfo } from '../../../utils/provider'
import axios from 'axios'
export default function AssetActions(): ReactElement { export default function AssetActions(): ReactElement {
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { ocean, balance, account } = useOcean() const { config, ocean, balance, account } = useOcean()
const { price, ddo, metadata } = useAsset() const { price, ddo } = useAsset()
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>() const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const [dtBalance, setDtBalance] = useState<string>() const [dtBalance, setDtBalance] = useState<string>()
const [fileMetadata, setFileMetadata] = useState<FileMetadata>(Object)
const [fileIsLoading, setFileIsLoading] = useState<boolean>(false)
const isCompute = Boolean(ddo?.findServiceByType('compute')) const isCompute = Boolean(ddo?.findServiceByType('compute'))
useEffect(() => {
const { attributes } = ddo.findServiceByType('metadata')
setFileMetadata(attributes.main.files[0])
// !!!!! do not remove this, we will enable this again after fileInfo endpoint is fixed !!!
// if (!config) return
// const source = axios.CancelToken.source()
// async function initFileInfo() {
// setFileIsLoading(true)
// try {
// const fileInfo = await getFileInfo(
// DID.parse(`${ddo.id}`),
// config.providerUri,
// source.token
// )
// setFileMetadata(fileInfo.data[0])
// } catch (error) {
// Logger.error(error.message)
// } finally {
// setFileIsLoading(false)
// }
// }
// initFileInfo()
}, [config, ddo.id])
// Get and set user DT balance // Get and set user DT balance
useEffect(() => { useEffect(() => {
if (!ocean || !accountId) return if (!ocean || !accountId) return
async function init() { async function init() {
try { try {
const dtBalance = await ocean.datatokens.balance( const dtBalance = await ocean.datatokens.balance(
@ -41,6 +68,7 @@ export default function AssetActions(): ReactElement {
// Check user balance against price // Check user balance against price
useEffect(() => { useEffect(() => {
if (price?.type === 'free') setIsBalanceSufficient(true)
if (!price?.value || !account || !balance?.ocean || !dtBalance) return if (!price?.value || !account || !balance?.ocean || !dtBalance) return
setIsBalanceSufficient( setIsBalanceSufficient(
@ -56,14 +84,16 @@ export default function AssetActions(): ReactElement {
<Compute <Compute
dtBalance={dtBalance} dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient} isBalanceSufficient={isBalanceSufficient}
file={metadata?.main.files[0]} file={fileMetadata}
fileIsLoading={fileIsLoading}
/> />
) : ( ) : (
<Consume <Consume
ddo={ddo} ddo={ddo}
dtBalance={dtBalance} dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient} isBalanceSufficient={isBalanceSufficient}
file={metadata?.main.files[0]} file={fileMetadata}
fileIsLoading={fileIsLoading}
/> />
) )
@ -74,10 +104,7 @@ export default function AssetActions(): ReactElement {
} }
] ]
// Check from metadata, cause that is available earlier price?.type === 'pool' &&
const hasPool = ddo?.price?.type === 'pool'
hasPool &&
tabs.push( tabs.push(
{ {
title: 'Pool', title: 'Pool',
@ -89,5 +116,9 @@ export default function AssetActions(): ReactElement {
} }
) )
return <Tabs items={tabs} className={styles.actions} /> return (
<Permission eventType="consume">
<Tabs items={tabs} className={styles.actions} />
</Permission>
)
} }

View File

@ -0,0 +1,29 @@
.datasetsContainer {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
width: auto;
margin-left: -2rem;
margin-right: -2rem;
border-top: 1px solid var(--border-color);
margin-top: calc(var(--spacer) / 2);
}
.datasetsContainer div[class*='AssetSelection-module--selection'] {
width: 100%;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-left: 0;
border-right: 0;
padding: 0;
}
.datasetsContainer .text {
margin-bottom: calc(var(--spacer) / 2);
margin-top: calc(var(--spacer) / 2);
text-align: center;
color: var(--font-color-text);
font-size: var(--font-size-base);
font-family: var(--font-family-heading);
}

View File

@ -0,0 +1,36 @@
import React, { ReactElement, useEffect, useState } from 'react'
import styles from './AlgorithmDatasetsListForCompute.module.css'
import { getAlgorithmDatasetsForCompute } from '../../../utils/aquarius'
import { AssetSelectionAsset } from '../../molecules/FormFields/AssetSelection'
import AssetComputeList from '../../molecules/AssetComputeList'
import { useOcean } from '../../../providers/Ocean'
import { useAsset } from '../../../providers/Asset'
export default function AlgorithmDatasetsListForCompute({
algorithmDid
}: {
algorithmDid: string
}): ReactElement {
const { config } = useOcean()
const { type } = useAsset()
const [datasetsForCompute, setDatasetsForCompute] =
useState<AssetSelectionAsset[]>()
useEffect(() => {
async function getDatasetsAllowedForCompute() {
const datasets = await getAlgorithmDatasetsForCompute(
algorithmDid,
config.metadataCacheUri
)
setDatasetsForCompute(datasets)
}
type === 'algorithm' && getDatasetsAllowedForCompute()
}, [type])
return (
<div className={styles.datasetsContainer}>
<h3 className={styles.text}>Datasets algorithm is allowed to run on</h3>
<AssetComputeList assets={datasetsForCompute} />
</div>
)
}

View File

@ -1,7 +1,7 @@
.bookmark { .bookmark {
position: absolute; position: absolute;
top: -10px; top: -10px;
right: calc(var(--spacer) / 4); right: calc(var(--spacer) / 8);
appearance: none; appearance: none;
background: none; background: none;
border: none; border: none;

View File

@ -6,12 +6,10 @@ import { useAsset } from '../../../providers/Asset'
export default function MetaFull(): ReactElement { export default function MetaFull(): ReactElement {
const { ddo, metadata, isInPurgatory, type } = useAsset() const { ddo, metadata, isInPurgatory, type } = useAsset()
const { algorithm } = ddo.findServiceByType('metadata').attributes.main
function DockerImage() { function DockerImage() {
const algorithmContainer = ddo.findServiceByType('metadata').attributes.main const { image, tag } = algorithm.container
.algorithm.container
const { image } = algorithmContainer
const { tag } = algorithmContainer
return <span>{`${image}:${tag}`}</span> return <span>{`${image}:${tag}`}</span>
} }
@ -25,7 +23,7 @@ export default function MetaFull(): ReactElement {
content={<Publisher account={ddo?.publicKey[0].owner} />} content={<Publisher account={ddo?.publicKey[0].owner} />}
/> />
{type === 'algorithm' && ( {type === 'algorithm' && algorithm && (
<MetaItem title="Docker Image" content={<DockerImage />} /> <MetaItem title="Docker Image" content={<DockerImage />} />
)} )}
<MetaItem title="DID" content={<code>{ddo?.id}</code>} /> <MetaItem title="DID" content={<code>{ddo?.id}</code>} />

View File

@ -1,25 +1,51 @@
.meta { .meta {
margin-bottom: calc(var(--spacer) / 1.5); margin-bottom: calc(var(--spacer) / 1.5);
color: var(--color-secondary); color: var(--color-secondary);
}
.meta p {
margin-bottom: 0;
}
.date {
font-size: var(--font-size-small); font-size: var(--font-size-small);
} }
.typeAndDate { .asset {
margin-bottom: calc(var(--spacer) / 12); margin-left: -2rem;
display: flex; margin-right: -2rem;
padding-left: 2rem;
padding-right: 3rem;
border-bottom: 1px solid var(--border-color);
margin-bottom: calc(var(--spacer) / 1.5);
padding-bottom: calc(var(--spacer) / 1.75);
} }
.typeDetails { @media (min-width: 40rem) {
.asset {
margin-top: -0.65rem;
}
}
.assetType {
display: inline-block;
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
padding-right: calc(var(--spacer) / 3.5); padding-right: calc(var(--spacer) / 3.5);
margin-right: calc(var(--spacer) / 4); margin-right: calc(var(--spacer) / 4);
width: auto; }
.datatoken {
white-space: pre;
margin-right: calc(var(--spacer) / 3);
}
.byline {
font-size: var(--font-size-small); font-size: var(--font-size-small);
} }
.updated {
font-size: var(--font-size-mini);
}
.addWrap {
padding-left: calc(var(--spacer) / 5);
border-left: 1px solid var(--border-color);
display: inline-block;
}
.add {
font-size: var(--font-size-mini);
}

View File

@ -3,47 +3,65 @@ import { useAsset } from '../../../providers/Asset'
import { useWeb3 } from '../../../providers/Web3' import { useWeb3 } from '../../../providers/Web3'
import ExplorerLink from '../../atoms/ExplorerLink' import ExplorerLink from '../../atoms/ExplorerLink'
import Publisher from '../../atoms/Publisher' import Publisher from '../../atoms/Publisher'
import AddToken from '../../atoms/AddToken'
import Time from '../../atoms/Time' import Time from '../../atoms/Time'
import styles from './MetaMain.module.css'
import AssetType from '../../atoms/AssetType' import AssetType from '../../atoms/AssetType'
import styles from './MetaMain.module.css'
export default function MetaMain(): ReactElement { export default function MetaMain(): ReactElement {
const { ddo, owner, type } = useAsset() const { ddo, owner, type } = useAsset()
const { networkId } = useWeb3() const { networkId, web3ProviderInfo } = useWeb3()
const isCompute = Boolean(ddo?.findServiceByType('compute')) const isCompute = Boolean(ddo?.findServiceByType('compute'))
const accessType = isCompute ? 'compute' : 'access' const accessType = isCompute ? 'compute' : 'access'
return ( return (
<aside className={styles.meta}> <aside className={styles.meta}>
<div className={styles.typeAndDate}> <header className={styles.asset}>
<AssetType <AssetType
type={type} type={type}
accessType={accessType} accessType={accessType}
className={styles.typeDetails} className={styles.assetType}
/> />
<p className={styles.date}>
<Time date={ddo?.created} relative />
{ddo?.created !== ddo?.updated && (
<>
{' — '}
updated <Time date={ddo?.updated} relative />
</>
)}
</p>
</div>
<p>
<ExplorerLink <ExplorerLink
className={styles.datatoken}
networkId={networkId} networkId={networkId}
path={ path={
networkId === 137 networkId === 137 || networkId === 1287
? `tokens/${ddo?.dataToken}` ? `tokens/${ddo?.dataToken}`
: `token/${ddo?.dataToken}` : `token/${ddo?.dataToken}`
} }
> >
{`${ddo?.dataTokenInfo.name}${ddo?.dataTokenInfo.symbol}`} {`${ddo?.dataTokenInfo.name}${ddo?.dataTokenInfo.symbol}`}
</ExplorerLink> </ExplorerLink>
</p>
Published By <Publisher account={owner} /> {web3ProviderInfo?.name === 'MetaMask' && (
<span className={styles.addWrap}>
<AddToken
address={ddo?.dataTokenInfo.address}
symbol={ddo?.dataTokenInfo.symbol}
logo="https://raw.githubusercontent.com/oceanprotocol/art/main/logo/datatoken.png"
text={`Add ${ddo?.dataTokenInfo.symbol} to wallet`}
className={styles.add}
minimal
/>
</span>
)}
</header>
<div className={styles.byline}>
Published By <Publisher account={owner} />
<p>
<Time date={ddo?.created} relative />
{ddo?.created !== ddo?.updated && (
<>
{' — '}
<span className={styles.updated}>
updated <Time date={ddo?.updated} relative />
</span>
</>
)}
</p>
</div>
</aside> </aside>
) )
} }

View File

@ -0,0 +1,3 @@
.free {
composes: content from './index.module.css';
}

View File

@ -0,0 +1,21 @@
import React, { ReactElement } from 'react'
import stylesIndex from './index.module.css'
import styles from './Free.module.css'
import FormHelp from '../../../../atoms/Input/Help'
import { DDO } from '@oceanprotocol/lib'
import Price from './Price'
export default function Free({
ddo,
content
}: {
ddo: DDO
content: any
}): ReactElement {
return (
<div className={styles.free}>
<FormHelp className={stylesIndex.help}>{content.info}</FormHelp>
<Price ddo={ddo} free />
</div>
)
}

View File

@ -10,10 +10,12 @@ import usePricing from '../../../../../hooks/usePricing'
export default function Price({ export default function Price({
ddo, ddo,
firstPrice firstPrice,
free
}: { }: {
ddo: DDO ddo: DDO
firstPrice?: string firstPrice?: string
free?: boolean
}): ReactElement { }): ReactElement {
const [field, meta] = useField('price') const [field, meta] = useField('price')
const { getDTName, getDTSymbol } = usePricing() const { getDTName, getDTSymbol } = usePricing()
@ -38,17 +40,27 @@ export default function Price({
<div className={styles.price}> <div className={styles.price}>
<div className={styles.grid}> <div className={styles.grid}>
<div className={styles.form}> <div className={styles.form}>
<Input {free ? (
value={field.value} <Input
name="price" value="0"
type="number" name="price"
prefix="OCEAN" type="number"
min="1" prefix="OCEAN"
{...field} readOnly
additionalComponent={ />
<Conversion price={field.value} className={styles.conversion} /> ) : (
} <Input
/> value={field.value}
name="price"
type="number"
prefix="OCEAN"
min="1"
{...field}
additionalComponent={
<Conversion price={field.value} className={styles.conversion} />
}
/>
)}
<Error meta={meta} /> <Error meta={meta} />
</div> </div>
<div className={styles.datatoken}> <div className={styles.datatoken}>

View File

@ -45,3 +45,8 @@
padding-left: var(--spacer); padding-left: var(--spacer);
padding-right: var(--spacer); padding-right: var(--spacer);
} }
.free {
text-align: center;
margin-bottom: calc(var(--spacer) / 1.5);
}

View File

@ -3,6 +3,7 @@ import styles from './index.module.css'
import Tabs from '../../../../atoms/Tabs' import Tabs from '../../../../atoms/Tabs'
import Fixed from './Fixed' import Fixed from './Fixed'
import Dynamic from './Dynamic' import Dynamic from './Dynamic'
import Free from './Free'
import { useFormikContext } from 'formik' import { useFormikContext } from 'formik'
import { useUserPreferences } from '../../../../../providers/UserPreferences' import { useUserPreferences } from '../../../../../providers/UserPreferences'
import { PriceOptionsMarket } from '../../../../../@types/MetaData' import { PriceOptionsMarket } from '../../../../../@types/MetaData'
@ -25,19 +26,15 @@ export default function FormPricing({
// Connect with form // Connect with form
const { values, setFieldValue, submitForm } = useFormikContext() const { values, setFieldValue, submitForm } = useFormikContext()
const { const { price, oceanAmount, weightOnOcean, weightOnDataToken, type } =
price, values as PriceOptionsMarket
oceanAmount,
weightOnOcean,
weightOnDataToken,
type
} = values as PriceOptionsMarket
// Switch type value upon tab change // Switch type value upon tab change
function handleTabChange(tabName: string) { function handleTabChange(tabName: string) {
const type = tabName.toLowerCase() const type = tabName.toLowerCase()
setFieldValue('type', type) setFieldValue('type', type)
type === 'fixed' && setFieldValue('dtAmount', 1000) type === 'fixed' && setFieldValue('dtAmount', 1000)
type === 'free' && price < 1 && setFieldValue('price', 1)
} }
// Always update everything when price value changes // Always update everything when price value changes
@ -62,6 +59,12 @@ export default function FormPricing({
title: content.dynamic.title, title: content.dynamic.title,
content: <Dynamic content={content.dynamic} ddo={ddo} /> content: <Dynamic content={content.dynamic} ddo={ddo} />
} }
: undefined,
appConfig.allowFreePricing === 'true'
? {
title: content.free.title,
content: <Free content={content.free} ddo={ddo} />
}
: undefined : undefined
].filter((tab) => tab !== undefined) ].filter((tab) => tab !== undefined)

View File

@ -40,6 +40,10 @@ const query = graphql`
marketplaceFee marketplaceFee
} }
} }
free {
title
info
}
} }
} }
} }
@ -57,12 +61,8 @@ export default function Pricing({ ddo }: { ddo: DDO }): ReactElement {
const [showPricing, setShowPricing] = useState(false) const [showPricing, setShowPricing] = useState(false)
const [success, setSuccess] = useState<string>() const [success, setSuccess] = useState<string>()
const { const { createPricing, pricingIsLoading, pricingError, pricingStepText } =
createPricing, usePricing()
pricingIsLoading,
pricingError,
pricingStepText
} = usePricing()
const hasFeedback = pricingIsLoading || typeof success !== 'undefined' const hasFeedback = pricingIsLoading || typeof success !== 'undefined'

View File

@ -17,6 +17,8 @@ import MetaMain from './MetaMain'
import EditHistory from './EditHistory' import EditHistory from './EditHistory'
import { useWeb3 } from '../../../providers/Web3' import { useWeb3 } from '../../../providers/Web3'
import styles from './index.module.css' import styles from './index.module.css'
import EditAdvancedSettings from '../AssetActions/Edit/EditAdvancedSettings'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
export interface AssetContentProps { export interface AssetContentProps {
path?: string path?: string
@ -48,14 +50,19 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
const [showPricing, setShowPricing] = useState(false) const [showPricing, setShowPricing] = useState(false)
const [showEdit, setShowEdit] = useState<boolean>() const [showEdit, setShowEdit] = useState<boolean>()
const [showEditCompute, setShowEditCompute] = useState<boolean>() const [showEditCompute, setShowEditCompute] = useState<boolean>()
const { ddo, price, metadata } = useAsset() const [showEditAdvancedSettings, setShowEditAdvancedSettings] =
useState<boolean>()
const isOwner = accountId === owner const [isOwner, setIsOwner] = useState(false)
const { ddo, price, metadata, type } = useAsset()
const { appConfig } = useSiteMetadata()
useEffect(() => { useEffect(() => {
if (!price) return if (!accountId || !owner) return
const isOwner = accountId.toLowerCase() === owner.toLowerCase()
setIsOwner(isOwner)
setShowPricing(isOwner && price.type === '') setShowPricing(isOwner && price.type === '')
}, [isOwner, price]) }, [accountId, price, owner])
function handleEditButton() { function handleEditButton() {
// move user's focus to top of screen // move user's focus to top of screen
@ -68,10 +75,17 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
setShowEditCompute(true) setShowEditCompute(true)
} }
function handleEditAdvancedSettingsButton() {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
setShowEditAdvancedSettings(true)
}
return showEdit ? ( return showEdit ? (
<Edit setShowEdit={setShowEdit} /> <Edit setShowEdit={setShowEdit} />
) : showEditCompute ? ( ) : showEditCompute ? (
<EditComputeDataset setShowEdit={setShowEditCompute} /> <EditComputeDataset setShowEdit={setShowEditCompute} />
) : showEditAdvancedSettings ? (
<EditAdvancedSettings setShowEdit={setShowEditAdvancedSettings} />
) : ( ) : (
<article className={styles.grid}> <article className={styles.grid}>
<div> <div>
@ -101,7 +115,19 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
<Button style="text" size="small" onClick={handleEditButton}> <Button style="text" size="small" onClick={handleEditButton}>
Edit Metadata Edit Metadata
</Button> </Button>
{ddo.findServiceByType('compute') && ( {appConfig.allowAdvancedSettings === 'true' && (
<>
<span className={styles.separator}>|</span>
<Button
style="text"
size="small"
onClick={handleEditAdvancedSettingsButton}
>
Edit Advanced Settings
</Button>
</>
)}
{ddo.findServiceByType('compute') && type === 'dataset' && (
<> <>
<span className={styles.separator}>|</span> <span className={styles.separator}>|</span>
<Button <Button

View File

@ -16,3 +16,9 @@
font-size: var(--font-size-small); font-size: var(--font-size-small);
font-style: italic; font-style: italic;
} }
.loaderWrap {
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -1,17 +1,28 @@
import AssetTeaser from '../molecules/AssetTeaser' import AssetTeaser from '../molecules/AssetTeaser'
import React from 'react' import React, { useEffect, useState } from 'react'
import Pagination from '../molecules/Pagination' import Pagination from '../molecules/Pagination'
import styles from './AssetList.module.css' import styles from './AssetList.module.css'
import { DDO } from '@oceanprotocol/lib' import { DDO } from '@oceanprotocol/lib'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import { getAssetsBestPrices, AssetListPrices } from '../../utils/subgraph'
import Loader from '../atoms/Loader'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
function LoaderArea() {
return (
<div className={styles.loaderWrap}>
<Loader />
</div>
)
}
declare type AssetListProps = { declare type AssetListProps = {
assets: DDO[] assets: DDO[]
showPagination: boolean showPagination: boolean
page?: number page?: number
totalPages?: number totalPages?: number
isLoading?: boolean
onPageChange?: React.Dispatch<React.SetStateAction<number>> onPageChange?: React.Dispatch<React.SetStateAction<number>>
className?: string className?: string
} }
@ -21,9 +32,22 @@ const AssetList: React.FC<AssetListProps> = ({
showPagination, showPagination,
page, page,
totalPages, totalPages,
isLoading,
onPageChange, onPageChange,
className className
}) => { }) => {
const [assetsWithPrices, setAssetWithPrices] = useState<AssetListPrices[]>()
const [loading, setLoading] = useState<boolean>(true)
useEffect(() => {
if (!assets) return
isLoading && setLoading(true)
getAssetsBestPrices(assets).then((asset) => {
setAssetWithPrices(asset)
setLoading(false)
})
}, [assets])
// // This changes the page field inside the query // // This changes the page field inside the query
function handlePageChange(selected: number) { function handlePageChange(selected: number) {
onPageChange(selected + 1) onPageChange(selected + 1)
@ -34,11 +58,19 @@ const AssetList: React.FC<AssetListProps> = ({
[className]: className [className]: className
}) })
return ( return assetsWithPrices &&
!loading &&
(isLoading === undefined || isLoading === false) ? (
<> <>
<div className={styleClasses}> <div className={styleClasses}>
{assets.length > 0 ? ( {assetsWithPrices.length > 0 ? (
assets.map((ddo) => <AssetTeaser ddo={ddo} key={ddo.id} />) assetsWithPrices.map((assetWithPrice) => (
<AssetTeaser
ddo={assetWithPrice.ddo}
price={assetWithPrice.price}
key={assetWithPrice.ddo.id}
/>
))
) : ( ) : (
<div className={styles.empty}>No results found.</div> <div className={styles.empty}>No results found.</div>
)} )}
@ -52,6 +84,8 @@ const AssetList: React.FC<AssetListProps> = ({
/> />
)} )}
</> </>
) : (
<LoaderArea />
) )
} }

View File

@ -0,0 +1,62 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { useWeb3 } from '../../providers/Web3'
import rbacRequest from '../../utils/rbac'
import Alert from '../atoms/Alert'
import Loader from '../atoms/Loader'
import appConfig from '../../../app.config'
export default function Permission({
eventType,
children
}: {
eventType: string
children: ReactElement
}): ReactElement {
const url = appConfig.rbacUrl
const [data, updateData] = useState<boolean | 'ERROR'>()
const [errorMessage, updateError] = useState<string>()
const [messageState, updateMessageState] =
useState<'error' | 'warning' | 'info' | 'success'>()
const { accountId } = useWeb3()
useEffect(() => {
if (url === undefined) return
const getData = async () => {
if (accountId === undefined) {
updateError('Please make sure your wallet is connected to proceed.')
updateMessageState('info')
} else {
const data = await rbacRequest(eventType, accountId)
updateData(data)
if (data === 'ERROR') {
updateError(
'There was an error verifying your permissions. Please refresh the page or conntact your network administrator'
)
updateMessageState('error')
} else if (data === false) {
updateError(
`Sorry, you don't have permission to ${eventType}. Please make sure you have connected your registered address.`
)
updateMessageState('warning')
} else if (data !== true) {
updateError(
'An unkown error occured. Please conntact your network administrator'
)
updateMessageState('error')
}
}
}
getData()
}, [eventType, accountId, url])
if (url === undefined || data === true) {
return <>{children}</>
}
return (
<>
<Alert text={errorMessage} state={messageState} />
<br />
<Loader />
</>
)
}

View File

@ -1,3 +0,0 @@
.title {
margin-bottom: calc(var(--spacer) / 4);
}

Some files were not shown because too many files have changed in this diff Show More