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:
commit
b0356f559c
22
.env.example
22
.env.example
@ -1,10 +1,26 @@
|
||||
# Default network, possible values:
|
||||
# "development", "ropsten", "rinkeby", "mainnet", "polygon"
|
||||
# "development", "ropsten", "rinkeby", "mainnet", "polygon", "moonbeamalpha"
|
||||
GATSBY_NETWORK="rinkeby"
|
||||
|
||||
#GATSBY_INFURA_PROJECT_ID="xxx"
|
||||
#GATSBY_MARKET_FEE_ADDRESS="0xxx"
|
||||
#GATSBY_ANALYTICS_ID="xxx"
|
||||
#GATSBY_PORTIS_ID="xxx"
|
||||
|
||||
|
||||
#
|
||||
# ADVANCED SETTINGS
|
||||
#
|
||||
|
||||
# Toggle pricing options presented during price creation
|
||||
#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"
|
||||
|
14
.eslintrc
14
.eslintrc
@ -1,11 +1,12 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"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": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
"react": { "version": "detect" }
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
@ -20,9 +21,6 @@
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier/react",
|
||||
"prettier/standard",
|
||||
"prettier/@typescript-eslint",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
|
27
README.md
27
README.md
@ -25,6 +25,8 @@
|
||||
- [🛳 Production](#-production)
|
||||
- [⬆️ Deployment](#️-deployment)
|
||||
- [💖 Contributing](#-contributing)
|
||||
- [🍴 Forking](#-forking)
|
||||
- [💻 Advanced Features](#-advanced-features)
|
||||
- [🏛 License](#-license)
|
||||
|
||||
## 🏄 Get Started
|
||||
@ -37,6 +39,9 @@ To start local development:
|
||||
git clone git@github.com:oceanprotocol/market.git
|
||||
cd market
|
||||
|
||||
# when using nvm to manage Node.js versions
|
||||
nvm use
|
||||
|
||||
npm install
|
||||
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/)
|
||||
- [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
|
||||
|
||||
```text
|
||||
|
@ -2,8 +2,7 @@ module.exports = {
|
||||
client: {
|
||||
service: {
|
||||
name: 'ocean',
|
||||
url:
|
||||
'https://subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph',
|
||||
url: 'https://subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph',
|
||||
// optional disable SSL validation check
|
||||
skipSSLValidation: true
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ module.exports = {
|
||||
// networks in their wallet.
|
||||
// Ocean Protocol contracts are deployed for: 'mainnet', 'rinkeby', 'development'
|
||||
network: process.env.GATSBY_NETWORK || 'mainnet',
|
||||
rbacUrl: process.env.GATSBY_RBAC_URL,
|
||||
|
||||
infuraProjectId: process.env.GATSBY_INFURA_PROJECT_ID || 'xxx',
|
||||
|
||||
@ -40,8 +41,13 @@ module.exports = {
|
||||
// Wallets
|
||||
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.
|
||||
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'
|
||||
}
|
||||
|
@ -20,6 +20,15 @@
|
||||
"rows": 10,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"label": "New Price",
|
||||
"type": "number",
|
||||
"min": "1",
|
||||
"placeholder": "0",
|
||||
"help": "Enter a new price.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "links",
|
||||
"label": "Sample file",
|
||||
|
31
content/pages/editAdvancedSettings.json
Normal file
31
content/pages/editAdvancedSettings.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -6,13 +6,6 @@
|
||||
"successAction": "Close",
|
||||
"error": "Updating DDO failed.",
|
||||
"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",
|
||||
"label": "Selected Algorithms",
|
||||
@ -21,6 +14,13 @@
|
||||
"multiple": true,
|
||||
"options": [],
|
||||
"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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
"name": "files",
|
||||
"label": "File",
|
||||
"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",
|
||||
"required": true
|
||||
},
|
||||
@ -28,8 +28,8 @@
|
||||
"label": "Docker Image",
|
||||
"placeholder": "e.g. python3.7",
|
||||
"help": "Please select an image to run your algorithm.",
|
||||
"type": "select",
|
||||
"options": ["node:latest", "python:latest", "custom image"],
|
||||
"type": "boxSelection",
|
||||
"options": [],
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
@ -56,6 +56,13 @@
|
||||
"sortOptions": false,
|
||||
"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",
|
||||
"label": "Entrypoint",
|
||||
|
@ -19,7 +19,7 @@
|
||||
"name": "files",
|
||||
"label": "File",
|
||||
"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",
|
||||
"required": true
|
||||
},
|
||||
@ -34,7 +34,7 @@
|
||||
"name": "access",
|
||||
"label": "Access Type",
|
||||
"help": "Choose how you want your files to be accessible for the specified price.",
|
||||
"type": "select",
|
||||
"type": "boxSelection",
|
||||
"options": ["Download", "Compute"],
|
||||
"required": true
|
||||
},
|
||||
|
@ -21,6 +21,10 @@
|
||||
"communityFee": "Explain community 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": {
|
||||
|
29
docs/advancedSettings.md
Normal file
29
docs/advancedSettings.md
Normal 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).
|
@ -43,7 +43,7 @@ exports.onCreatePage = async ({ page, actions }) => {
|
||||
exports.onCreateWebpackConfig = ({ actions }) => {
|
||||
actions.setWebpackConfig({
|
||||
node: {
|
||||
// 'fs' fix for squid.js
|
||||
// 'fs' fix for ocean.js
|
||||
fs: 'empty'
|
||||
},
|
||||
// fix for 'got'/'swarm-js' dependency
|
||||
|
62289
package-lock.json
generated
62289
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
126
package.json
126
package.json
@ -23,122 +23,112 @@
|
||||
"postinstall": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.3.11",
|
||||
"@apollo/client": "^3.3.19",
|
||||
"@coingecko/cryptoformat": "^0.4.2",
|
||||
"@loadable/component": "^5.14.1",
|
||||
"@loadable/component": "^5.15.0",
|
||||
"@oceanprotocol/art": "^3.0.0",
|
||||
"@oceanprotocol/lib": "^0.14.1",
|
||||
"@oceanprotocol/lib": "^0.15.1",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@portis/web3": "^3.0.3",
|
||||
"@sindresorhus/slugify": "^1.0.0",
|
||||
"@tippyjs/react": "^4.2.0",
|
||||
"@types/classnames": "^2.2.11",
|
||||
"@vercel/node": "^1.8.5",
|
||||
"@walletconnect/web3-provider": "^1.3.4",
|
||||
"@portis/web3": "^4.0.4",
|
||||
"@sindresorhus/slugify": "^2.1.0",
|
||||
"@tippyjs/react": "^4.2.5",
|
||||
"@walletconnect/web3-provider": "^1.4.1",
|
||||
"axios": "^0.21.1",
|
||||
"chart.js": "^2.9.4",
|
||||
"classnames": "^2.2.6",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"date-fns": "^2.16.1",
|
||||
"classnames": "^2.3.1",
|
||||
"cross-fetch": "^3.1.4",
|
||||
"date-fns": "^2.22.1",
|
||||
"decimal.js": "^10.2.1",
|
||||
"dom-confetti": "^0.2.2",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"ethereum-address": "0.0.4",
|
||||
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
||||
"filesize": "^6.1.0",
|
||||
"formik": "^2.2.6",
|
||||
"gatsby": "^2.30.2",
|
||||
"filesize": "^6.3.0",
|
||||
"formik": "^2.2.9",
|
||||
"gatsby": "^2.32.13",
|
||||
"gatsby-image": "^2.9.0",
|
||||
"gatsby-plugin-manifest": "^2.10.0",
|
||||
"gatsby-plugin-react-helmet": "^3.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-use-dark-mode": "^1.2.0",
|
||||
"gatsby-plugin-webpack-size": "^1.0.0",
|
||||
"gatsby-plugin-use-dark-mode": "^1.3.0",
|
||||
"gatsby-plugin-webpack-size": "^2.0.1",
|
||||
"gatsby-source-filesystem": "^2.9.0",
|
||||
"gatsby-source-graphql": "^2.12.0",
|
||||
"gatsby-transformer-json": "^2.9.0",
|
||||
"gatsby-transformer-remark": "^2.14.0",
|
||||
"gatsby-transformer-sharp": "^2.10.1",
|
||||
"intersection-observer": "^0.12.0",
|
||||
"is-url-superb": "^5.0.0",
|
||||
"gatsby-transformer-remark": "^2.16.1",
|
||||
"gatsby-transformer-sharp": "^2.12.1",
|
||||
"graphql": "14.7.0",
|
||||
"is-url-superb": "^6.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"query-string": "^6.13.8",
|
||||
"react": "^17.0.1",
|
||||
"react-chartjs-2": "^2.11.1",
|
||||
"react-data-table-component": "^6.11.6",
|
||||
"react-dom": "^17.0.1",
|
||||
"query-string": "^7.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-chartjs-2": "^2.11.2",
|
||||
"react-data-table-component": "^6.11.7",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dotdotdot": "^1.3.1",
|
||||
"react-dropzone": "^11.2.4",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intersection-observer": "^8.31.0",
|
||||
"react-markdown": "^5.0.3",
|
||||
"react-modal": "^3.12.1",
|
||||
"react-paginate": "^7.0.0",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-tabs": "^3.1.2",
|
||||
"react-toastify": "^6.2.0",
|
||||
"react-markdown": "^6.0.2",
|
||||
"react-modal": "^3.14.2",
|
||||
"react-paginate": "^7.1.3",
|
||||
"react-spring": "^9.2.1",
|
||||
"react-tabs": "^3.2.2",
|
||||
"react-toastify": "^7.0.4",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"shortid": "^2.2.16",
|
||||
"slugify": "^1.4.6",
|
||||
"swr": "^0.3.11",
|
||||
"slugify": "^1.5.3",
|
||||
"swr": "^0.5.6",
|
||||
"use-dark-mode": "^2.3.1",
|
||||
"web3": "^1.3.4",
|
||||
"web3": "^1.3.6",
|
||||
"web3modal": "^1.9.3",
|
||||
"yup": "^0.32.6"
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"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",
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^11.2.3",
|
||||
"@types/chart.js": "^2.9.29",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@testing-library/jest-dom": "^5.12.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@types/chart.js": "^2.9.32",
|
||||
"@types/classnames": "^2.3.1",
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/loadable__component": "^5.13.1",
|
||||
"@types/lodash.debounce": "^4.0.3",
|
||||
"@types/lodash.omit": "^4.5.6",
|
||||
"@types/node": "^14.14.20",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-helmet": "^6.1.0",
|
||||
"@types/react-modal": "^3.10.6",
|
||||
"@types/react-paginate": "^6.2.1",
|
||||
"@types/node": "^15.6.1",
|
||||
"@types/react": "^17.0.8",
|
||||
"@types/react-helmet": "^6.1.1",
|
||||
"@types/react-modal": "^3.12.0",
|
||||
"@types/react-paginate": "^7.1.0",
|
||||
"@types/react-tabs": "^2.3.2",
|
||||
"@types/remove-markdown": "^0.1.1",
|
||||
"@types/remove-markdown": "^0.3.0",
|
||||
"@types/shortid": "0.0.29",
|
||||
"@types/yup": "^0.29.11",
|
||||
"@typescript-eslint/eslint-plugin": "^4.13.0",
|
||||
"@typescript-eslint/parser": "^4.13.0",
|
||||
"apollo": "^2.32.1",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-preset-react-app": "^10.0.0",
|
||||
"eslint": "^7.17.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
"@typescript-eslint/parser": "^4.26.0",
|
||||
"apollo": "^2.33.4",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-oceanprotocol": "^1.5.0",
|
||||
"eslint-config-prettier": "^7.1.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"husky": "^5.0.8",
|
||||
"husky": "^6.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"prettier": "^2.2.1",
|
||||
"prettier": "^2.3.0",
|
||||
"pretty-quick": "^3.1.0",
|
||||
"serve": "^11.3.2",
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"typescript": "^4.1.3"
|
||||
"typescript": "^4.3.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/oceanprotocol/market"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=14"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
|
13
src/@types/ComputeJobMetaData.d.ts
vendored
13
src/@types/ComputeJobMetaData.d.ts
vendored
@ -1,11 +1,6 @@
|
||||
export interface ComputeJobMetaData {
|
||||
jobId: string
|
||||
did: string
|
||||
dateCreated: string
|
||||
dateFinished: string
|
||||
import { ComputeJob } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
|
||||
|
||||
export interface ComputeJobMetaData extends ComputeJob {
|
||||
assetName: string
|
||||
status: number
|
||||
statusText: string
|
||||
algorithmLogUrl: string
|
||||
resultsUrls: string[]
|
||||
assetDtSymbol: string
|
||||
}
|
||||
|
2
src/@types/MetaData.d.ts
vendored
2
src/@types/MetaData.d.ts
vendored
@ -49,6 +49,7 @@ export interface MetadataPublishFormAlgorithm {
|
||||
dockerImage: string
|
||||
algorithmPrivacy: boolean
|
||||
timeout: string
|
||||
dataTokenOptions: DataTokenOptions
|
||||
termsAndConditions: boolean
|
||||
// ---- optional fields ----
|
||||
image: string
|
||||
@ -61,6 +62,7 @@ export interface MetadataEditForm {
|
||||
name: string
|
||||
description: string
|
||||
timeout: string
|
||||
price?: number
|
||||
links?: string | EditableMetadataLinks[]
|
||||
}
|
||||
|
||||
|
2
src/@types/node_modules.d.ts
vendored
2
src/@types/node_modules.d.ts
vendored
@ -1,5 +1,3 @@
|
||||
declare module 'intersection-observer'
|
||||
|
||||
declare module 'ethereum-blockies' {
|
||||
export function toDataUrl(address: string): string
|
||||
}
|
||||
|
71
src/components/atoms/AddToken.module.css
Normal file
71
src/components/atoms/AddToken.module.css
Normal 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);
|
||||
}
|
53
src/components/atoms/AddToken.tsx
Normal file
53
src/components/atoms/AddToken.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
.icon {
|
||||
fill: var(--brand-grey-light);
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
fill: currentColor;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: baseline;
|
||||
margin-bottom: -0.2em;
|
||||
margin-bottom: -0.1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import styles from './AssetType.module.css'
|
||||
import classNames from 'classnames/bind'
|
||||
import { ReactComponent as Compute } from '../../images/compute.svg'
|
||||
import { ReactComponent as Download } from '../../images/download.svg'
|
||||
import { ReactComponent as Lock } from '../../images/lock.svg'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
@ -25,6 +26,8 @@ export default function AssetType({
|
||||
</div>
|
||||
{accessType === 'access' ? (
|
||||
<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} />
|
||||
)}
|
||||
|
@ -5,6 +5,7 @@
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
min-width: 7rem;
|
||||
padding: calc(var(--spacer) / 3) var(--spacer);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
|
@ -1,57 +1,57 @@
|
||||
import React from 'react'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
import Button from './Button'
|
||||
// import React from 'react'
|
||||
// // import { action } from '@storybook/addon-actions'
|
||||
// import Button from './Button'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Button'
|
||||
}
|
||||
// export default {
|
||||
// title: 'Atoms/Button'
|
||||
// }
|
||||
|
||||
export const Default = () => (
|
||||
<>
|
||||
<Button onClick={action('clicked')}>Hello Button</Button>
|
||||
<br />
|
||||
<br />
|
||||
<Button size="small" onClick={action('clicked')}>
|
||||
Hello Button
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
// export const Default = () => (
|
||||
// <>
|
||||
// <Button onClick={action('clicked')}>Hello Button</Button>
|
||||
// <br />
|
||||
// <br />
|
||||
// <Button size="small" onClick={action('clicked')}>
|
||||
// Hello Button
|
||||
// </Button>
|
||||
// </>
|
||||
// )
|
||||
|
||||
export const Primary = () => (
|
||||
<>
|
||||
<Button style="primary" onClick={action('clicked')}>
|
||||
Hello Button
|
||||
</Button>
|
||||
<br />
|
||||
<br />
|
||||
<Button style="primary" size="small" onClick={action('clicked')}>
|
||||
Hello Button
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
// export const Primary = () => (
|
||||
// <>
|
||||
// <Button style="primary" onClick={action('clicked')}>
|
||||
// Hello Button
|
||||
// </Button>
|
||||
// <br />
|
||||
// <br />
|
||||
// <Button style="primary" size="small" onClick={action('clicked')}>
|
||||
// Hello Button
|
||||
// </Button>
|
||||
// </>
|
||||
// )
|
||||
|
||||
export const Ghost = () => (
|
||||
<>
|
||||
<Button style="ghost" onClick={action('clicked')}>
|
||||
Hello Button
|
||||
</Button>
|
||||
<br />
|
||||
<br />
|
||||
<Button style="ghost" size="small" onClick={action('clicked')}>
|
||||
Hello Button
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
// export const Ghost = () => (
|
||||
// <>
|
||||
// <Button style="ghost" onClick={action('clicked')}>
|
||||
// Hello Button
|
||||
// </Button>
|
||||
// <br />
|
||||
// <br />
|
||||
// <Button style="ghost" size="small" onClick={action('clicked')}>
|
||||
// Hello Button
|
||||
// </Button>
|
||||
// </>
|
||||
// )
|
||||
|
||||
export const Text = () => (
|
||||
<>
|
||||
<Button style="text" onClick={action('clicked')}>
|
||||
Hello Button
|
||||
</Button>
|
||||
<br />
|
||||
<br />
|
||||
<Button style="text" size="small" onClick={action('clicked')}>
|
||||
Hello Button
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
// export const Text = () => (
|
||||
// <>
|
||||
// <Button style="text" onClick={action('clicked')}>
|
||||
// Hello Button
|
||||
// </Button>
|
||||
// <br />
|
||||
// <br />
|
||||
// <Button style="text" size="small" onClick={action('clicked')}>
|
||||
// Hello Button
|
||||
// </Button>
|
||||
// </>
|
||||
// )
|
||||
|
@ -21,6 +21,8 @@ interface ButtonBuyProps {
|
||||
onClick?: (e: FormEvent<HTMLButtonElement>) => void
|
||||
stepText?: string
|
||||
type?: 'submit'
|
||||
priceType?: string
|
||||
algorithmPriceType?: string
|
||||
}
|
||||
|
||||
function getConsumeHelpText(
|
||||
@ -87,15 +89,21 @@ export default function ButtonBuy({
|
||||
onClick,
|
||||
stepText,
|
||||
isLoading,
|
||||
type
|
||||
type,
|
||||
priceType,
|
||||
algorithmPriceType
|
||||
}: ButtonBuyProps): ReactElement {
|
||||
const buttonText =
|
||||
action === 'download'
|
||||
? hasPreviousOrder
|
||||
? 'Download'
|
||||
: priceType === 'free'
|
||||
? 'Get'
|
||||
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
|
||||
: hasPreviousOrder && hasPreviousOrderSelectedComputeAsset
|
||||
? 'Start Compute Job'
|
||||
: priceType === 'free' && algorithmPriceType === 'free'
|
||||
? 'Order Compute Job'
|
||||
: `Buy Compute Job`
|
||||
|
||||
return (
|
||||
|
@ -1,20 +1,30 @@
|
||||
import React, { ReactElement, ReactNode, useEffect, useState } from 'react'
|
||||
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 { useOcean } from '../../providers/Ocean'
|
||||
import styles from './ExplorerLink.module.css'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
export default function ExplorerLink({
|
||||
path,
|
||||
children
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
networkId: number
|
||||
path: string
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}): ReactElement {
|
||||
const { config } = useOcean()
|
||||
const [url, setUrl] = useState<string>()
|
||||
|
||||
const styleClasses = cx({
|
||||
link: true,
|
||||
[className]: className
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setUrl((config as ConfigHelperConfig).explorerUri)
|
||||
}, [config])
|
||||
@ -25,7 +35,7 @@ export default function ExplorerLink({
|
||||
title={`View on ${(config as ConfigHelperConfig).explorerUri}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.link}
|
||||
className={styleClasses}
|
||||
>
|
||||
{children} <External />
|
||||
</a>
|
||||
|
@ -25,3 +25,7 @@
|
||||
width: 4.5rem;
|
||||
padding: calc(var(--spacer) / 2) calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.loaderWrap {
|
||||
margin-right: calc(var(--spacer) / 6);
|
||||
}
|
||||
|
@ -4,17 +4,28 @@ import filesize from 'filesize'
|
||||
import classNames from 'classnames/bind'
|
||||
import cleanupContentType from '../../utils/cleanupContentType'
|
||||
import styles from './File.module.css'
|
||||
import Loader from '../atoms/Loader'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
function LoaderArea() {
|
||||
return (
|
||||
<div className={styles.loaderWrap}>
|
||||
<Loader />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function File({
|
||||
file,
|
||||
className,
|
||||
small
|
||||
small,
|
||||
isLoading
|
||||
}: {
|
||||
file: FileMetadata
|
||||
className?: string
|
||||
small?: boolean
|
||||
isLoading?: boolean
|
||||
}): ReactElement {
|
||||
if (!file) return null
|
||||
|
||||
@ -26,17 +37,23 @@ export default function File({
|
||||
|
||||
return (
|
||||
<ul className={styleClasses}>
|
||||
{file.contentType || file.contentLength ? (
|
||||
{isLoading === false || isLoading === undefined ? (
|
||||
<>
|
||||
<li>{cleanupContentType(file.contentType)}</li>
|
||||
<li>
|
||||
{file.contentLength && file.contentLength !== '0'
|
||||
? filesize(Number(file.contentLength))
|
||||
: ''}
|
||||
</li>
|
||||
{file.contentType || file.contentLength ? (
|
||||
<>
|
||||
<li>{cleanupContentType(file.contentType)}</li>
|
||||
<li>
|
||||
{file.contentLength && file.contentLength !== '0'
|
||||
? 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>
|
||||
)
|
||||
|
@ -4,11 +4,15 @@ import styles from './InputElement.module.css'
|
||||
import { InputProps } from '.'
|
||||
import FilesInput from '../../molecules/FormFields/FilesInput'
|
||||
import Terms from '../../molecules/FormFields/Terms'
|
||||
import BoxSelection, {
|
||||
BoxSelectionOption
|
||||
} from '../../molecules/FormFields/BoxSelection'
|
||||
import Datatoken from '../../molecules/FormFields/Datatoken'
|
||||
import classNames from 'classnames/bind'
|
||||
import AssetSelection, {
|
||||
AssetSelectionAsset
|
||||
} from '../../molecules/FormFields/AssetSelection'
|
||||
import Credentials from '../../molecules/FormFields/Credential'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
@ -91,6 +95,7 @@ export default function InputElement({
|
||||
id={slugify(option)}
|
||||
type={type}
|
||||
name={name}
|
||||
defaultChecked={props.defaultChecked}
|
||||
{...props}
|
||||
/>
|
||||
<label className={styles.radioLabel} htmlFor={slugify(option)}>
|
||||
@ -103,7 +108,7 @@ export default function InputElement({
|
||||
case 'assetSelection':
|
||||
return (
|
||||
<AssetSelection
|
||||
assets={(options as unknown) as AssetSelectionAsset[]}
|
||||
assets={options as unknown as AssetSelectionAsset[]}
|
||||
{...field}
|
||||
{...props}
|
||||
/>
|
||||
@ -111,7 +116,7 @@ export default function InputElement({
|
||||
case 'assetSelectionMultiple':
|
||||
return (
|
||||
<AssetSelection
|
||||
assets={(options as unknown) as AssetSelectionAsset[]}
|
||||
assets={options as unknown as AssetSelectionAsset[]}
|
||||
multiple
|
||||
disabled={disabled}
|
||||
{...field}
|
||||
@ -124,6 +129,17 @@ export default function InputElement({
|
||||
return <Datatoken name={name} {...field} {...props} />
|
||||
case 'terms':
|
||||
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:
|
||||
return prefix || postfix ? (
|
||||
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>
|
||||
|
@ -5,10 +5,6 @@
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.item span {
|
||||
color: var(--brand-grey-dark);
|
||||
}
|
||||
|
||||
.ulItem {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
.loaderWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loader {
|
||||
|
@ -13,10 +13,9 @@ const Markdown = ({
|
||||
// https://github.com/rexxars/react-markdown/issues/105#issuecomment-351585313
|
||||
const textCleaned = text?.replace(/\\n/g, '\n ')
|
||||
return (
|
||||
<ReactMarkdown
|
||||
source={textCleaned}
|
||||
className={`${styles.markdown} ${className}`}
|
||||
/>
|
||||
<ReactMarkdown className={`${styles.markdown} ${className}`}>
|
||||
{textCleaned}
|
||||
</ReactMarkdown>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -42,15 +42,20 @@ export default function PriceUnit({
|
||||
|
||||
return (
|
||||
<div className={styleClasses}>
|
||||
<div>
|
||||
{Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '}
|
||||
<span className={styles.symbol}>{symbol || 'OCEAN'}</span>
|
||||
{type && type === 'pool' && (
|
||||
<Badge label="pool" className={styles.badge} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{conversion && <Conversion price={price} />}
|
||||
{type && type === 'free' ? (
|
||||
<div> Free </div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
{Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '}
|
||||
<span className={styles.symbol}>{symbol || 'OCEAN'}</span>
|
||||
{type && type === 'pool' && (
|
||||
<Badge label="pool" className={styles.badge} />
|
||||
)}
|
||||
</div>
|
||||
{conversion && <Conversion price={price} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ export default function Price({
|
||||
small?: boolean
|
||||
conversion?: boolean
|
||||
}): ReactElement {
|
||||
return price?.value ? (
|
||||
return price?.value || price?.type === 'free' ? (
|
||||
<PriceUnit
|
||||
price={`${price.value}`}
|
||||
className={className}
|
||||
@ -24,17 +24,18 @@ export default function Price({
|
||||
conversion={conversion}
|
||||
type={price.type}
|
||||
/>
|
||||
) : !price || !price.address || price.address === '' ? (
|
||||
) : !price || price?.type === '' ? (
|
||||
<div className={styles.empty}>
|
||||
No price set{' '}
|
||||
<Tooltip content="No pricing mechanism has been set on this asset yet." />
|
||||
</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..." />
|
||||
)
|
||||
}
|
||||
|
@ -68,7 +68,6 @@ export default function Publisher({
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
|
||||
<div className={styles.links}>
|
||||
{' — '}
|
||||
{profile && (
|
||||
|
59
src/components/molecules/AssetComputeList.module.css
Normal file
59
src/components/molecules/AssetComputeList.module.css
Normal 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';
|
||||
}
|
49
src/components/molecules/AssetComputeList.tsx
Normal file
49
src/components/molecules/AssetComputeList.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -2,9 +2,10 @@ import AssetTeaser from '../molecules/AssetTeaser'
|
||||
import * as React from 'react'
|
||||
import { DDO } from '@oceanprotocol/lib'
|
||||
import ddo from '../../../tests/unit/__fixtures__/ddo'
|
||||
import { AssetListPrices } from '../../utils/subgraph'
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Asset Teaser'
|
||||
}
|
||||
|
||||
export const Default = () => <AssetTeaser ddo={ddo as DDO} />
|
||||
export const Default = () => <AssetTeaser ddo={ddo as DDO} price={undefined} />
|
||||
|
@ -3,7 +3,7 @@ import { Link } from 'gatsby'
|
||||
import Dotdotdot from 'react-dotdotdot'
|
||||
import Price from '../atoms/Price'
|
||||
import styles from './AssetTeaser.module.css'
|
||||
import { DDO } from '@oceanprotocol/lib'
|
||||
import { DDO, BestPrice } from '@oceanprotocol/lib'
|
||||
import removeMarkdown from 'remove-markdown'
|
||||
import Publisher from '../atoms/Publisher'
|
||||
import Time from '../atoms/Time'
|
||||
@ -11,9 +11,13 @@ import AssetType from '../atoms/AssetType'
|
||||
|
||||
declare type AssetTeaserProps = {
|
||||
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 { name, type } = attributes.main
|
||||
const { dataTokenInfo } = ddo
|
||||
@ -47,7 +51,7 @@ const AssetTeaser: React.FC<AssetTeaserProps> = ({ ddo }: AssetTeaserProps) => {
|
||||
</div>
|
||||
|
||||
<footer className={styles.foot}>
|
||||
<Price price={ddo.price} small />
|
||||
<Price price={price} small />
|
||||
<p className={styles.date}>
|
||||
<Time date={ddo?.created} relative />
|
||||
</p>
|
||||
|
@ -107,7 +107,9 @@
|
||||
|
||||
.did {
|
||||
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;
|
||||
text-align: left;
|
||||
color: var(--color-secondary);
|
||||
|
@ -4,9 +4,9 @@ import slugify from 'slugify'
|
||||
import classNames from 'classnames/bind'
|
||||
import PriceUnit from '../../atoms/Price/PriceUnit'
|
||||
import { ReactComponent as External } from '../../../images/external.svg'
|
||||
import styles from './AssetSelection.module.css'
|
||||
import InputElement from '../../atoms/Input/InputElement'
|
||||
import Loader from '../../atoms/Loader'
|
||||
import styles from './AssetSelection.module.css'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
@ -107,7 +107,12 @@ export default function AssetSelection({
|
||||
</Dotdotdot>
|
||||
</label>
|
||||
|
||||
<PriceUnit price={asset.price} small className={styles.price} />
|
||||
<PriceUnit
|
||||
price={asset.price}
|
||||
type={asset.price === '0' ? 'free' : undefined}
|
||||
small
|
||||
className={styles.price}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
57
src/components/molecules/FormFields/BoxSelection.module.css
Normal file
57
src/components/molecules/FormFields/BoxSelection.module.css
Normal 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;
|
||||
}
|
70
src/components/molecules/FormFields/BoxSelection.tsx
Normal file
70
src/components/molecules/FormFields/BoxSelection.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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;
|
||||
}
|
81
src/components/molecules/FormFields/Credential/index.tsx
Normal file
81
src/components/molecules/FormFields/Credential/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import isUrl from 'is-url-superb'
|
||||
import Button from '../../../atoms/Button'
|
||||
import { FieldInputProps, useField } from 'formik'
|
||||
import Loader from '../../../atoms/Loader'
|
||||
@ -28,12 +27,8 @@ export default function FileInput({
|
||||
<Button
|
||||
style="primary"
|
||||
size="small"
|
||||
onClick={(e: React.SyntheticEvent) => handleButtonClick(e, field.value)}
|
||||
disabled={
|
||||
!field.value ||
|
||||
// weird static page build fix so is-url-superb won't error
|
||||
!isUrl(typeof field.value === 'string' ? field.value : '')
|
||||
}
|
||||
onClick={(e: React.SyntheticEvent) => e.preventDefault()}
|
||||
disabled={!field.value}
|
||||
>
|
||||
{isLoading ? <Loader /> : 'Add File'}
|
||||
</Button>
|
||||
|
@ -14,7 +14,7 @@ export default function FilesInput(props: InputProps): ReactElement {
|
||||
const [fileUrl, setFileUrl] = useState<string>()
|
||||
const { config } = useOcean()
|
||||
|
||||
useEffect(() => {
|
||||
function loadFileInfo() {
|
||||
const source = axios.CancelToken.source()
|
||||
|
||||
async function validateUrl() {
|
||||
@ -33,11 +33,16 @@ export default function FilesInput(props: InputProps): ReactElement {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fileUrl && validateUrl()
|
||||
|
||||
return () => {
|
||||
source.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadFileInfo()
|
||||
}, [fileUrl, config.providerUri])
|
||||
|
||||
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'
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,10 @@ const query = graphql`
|
||||
|
||||
export default function Terms(props: InputProps): ReactElement {
|
||||
const data = useStaticQuery(query)
|
||||
const termsProps: InputProps = {
|
||||
...props,
|
||||
defaultChecked: props.value.toString() === 'true'
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -21,7 +25,7 @@ export default function Terms(props: InputProps): ReactElement {
|
||||
className={styles.terms}
|
||||
dangerouslySetInnerHTML={{ __html: data.terms.html }}
|
||||
/>
|
||||
<InputElement {...props} type="checkbox" />
|
||||
<InputElement {...termsProps} type="checkbox" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -135,6 +135,11 @@ export function MetadataAlgorithmPreview({
|
||||
<h2 className={styles.previewTitle}>Preview</h2>
|
||||
<header>
|
||||
{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} />}
|
||||
|
||||
<div className={styles.asset}>
|
||||
|
@ -2,7 +2,6 @@ import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { useWeb3 } from '../../providers/Web3'
|
||||
import { addCustomNetwork, NetworkObject } from '../../utils/web3'
|
||||
import { getOceanConfig } from '../../utils/ocean'
|
||||
import { getProviderInfo } from 'web3modal'
|
||||
import { useOcean } from '../../providers/Ocean'
|
||||
import { useSiteMetadata } from '../../hooks/useSiteMetadata'
|
||||
import AnnouncementBanner, {
|
||||
@ -19,7 +18,7 @@ const networkMatic: NetworkObject = {
|
||||
}
|
||||
|
||||
export default function NetworkBanner(): ReactElement {
|
||||
const { web3Provider } = useWeb3()
|
||||
const { web3Provider, web3ProviderInfo } = useWeb3()
|
||||
const { config, connect } = useOcean()
|
||||
const { announcement } = useSiteMetadata()
|
||||
|
||||
@ -51,10 +50,9 @@ export default function NetworkBanner(): ReactElement {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!web3Provider && !config) return
|
||||
if (!web3ProviderInfo || (!web3Provider && !config)) return
|
||||
|
||||
const providerInfo = getProviderInfo(web3Provider)
|
||||
switch (providerInfo?.name) {
|
||||
switch (web3ProviderInfo.name) {
|
||||
case 'Web3':
|
||||
if (config.networkId !== 137) {
|
||||
setText(announcement.main)
|
||||
@ -80,7 +78,7 @@ export default function NetworkBanner(): ReactElement {
|
||||
setAction(undefined)
|
||||
}
|
||||
}
|
||||
}, [web3Provider, config, announcement])
|
||||
}, [web3Provider, web3ProviderInfo, config, announcement])
|
||||
|
||||
return <AnnouncementBanner text={text} action={action} />
|
||||
}
|
||||
|
@ -17,20 +17,38 @@ export default function SearchBar({
|
||||
filters?: boolean
|
||||
size?: 'small' | 'large'
|
||||
}): ReactElement {
|
||||
const [value, setValue] = useState(initialValue || '')
|
||||
|
||||
function handleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
setValue(e.target.value)
|
||||
}
|
||||
let [value, setValue] = useState(initialValue || '')
|
||||
|
||||
async function startSearch(e: FormEvent<HTMLButtonElement>) {
|
||||
e.preventDefault()
|
||||
if (value === '') return
|
||||
if (value === '') value = ' '
|
||||
const urlEncodedValue = encodeURIComponent(value)
|
||||
const url = await addExistingParamsToUrl(location, 'text')
|
||||
const url = await addExistingParamsToUrl(location, [
|
||||
'text',
|
||||
'owner',
|
||||
'tags'
|
||||
])
|
||||
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 (
|
||||
<form className={styles.form}>
|
||||
<InputGroup>
|
||||
|
@ -1,14 +1,13 @@
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
gap: calc(var(--spacer) / 4);
|
||||
grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
|
||||
padding-bottom: calc(var(--spacer) / 8);
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
flex: 0 0 48%;
|
||||
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2)
|
||||
calc(var(--spacer) / 4) calc(var(--spacer) / 2) !important;
|
||||
width: auto;
|
||||
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 4) !important;
|
||||
border-radius: var(--border-radius);
|
||||
text-transform: none;
|
||||
}
|
||||
@ -31,3 +30,7 @@
|
||||
color: var(--font-color-text);
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.appearances div[class*='boxSelectionsWrapper'] {
|
||||
padding-bottom: calc(var(--spacer) / 8);
|
||||
}
|
||||
|
@ -1,42 +1,44 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import React, { ReactElement, ChangeEvent } from 'react'
|
||||
import { DarkMode } from 'use-dark-mode'
|
||||
import Button from '../../atoms/Button'
|
||||
import FormHelp from '../../atoms/Input/Help'
|
||||
import Label from '../../atoms/Input/Label'
|
||||
import styles from './Appearance.module.css'
|
||||
import { ReactComponent as Moon } from '../../../images/moon.svg'
|
||||
import { ReactComponent as Sun } from '../../../images/sun.svg'
|
||||
|
||||
const buttons = ['Light', 'Dark']
|
||||
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection'
|
||||
import styles from './Appearance.module.css'
|
||||
|
||||
export default function Appearance({
|
||||
darkMode
|
||||
}: {
|
||||
darkMode: DarkMode
|
||||
}): ReactElement {
|
||||
return (
|
||||
<li>
|
||||
<Label htmlFor="">Appearance</Label>
|
||||
<div className={styles.buttons}>
|
||||
{buttons.map((button) => {
|
||||
const isDark = button === 'Dark'
|
||||
const selected =
|
||||
(isDark && darkMode.value) || (!isDark && !darkMode.value)
|
||||
const options: BoxSelectionOption[] = [
|
||||
{
|
||||
name: 'Light',
|
||||
checked: !darkMode.value,
|
||||
title: 'Light',
|
||||
icon: <Sun />
|
||||
},
|
||||
{
|
||||
name: 'Dark',
|
||||
checked: darkMode.value,
|
||||
title: 'Dark',
|
||||
icon: <Moon />
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={button}
|
||||
className={`${styles.button} ${selected ? styles.selected : ''}`}
|
||||
size="small"
|
||||
style="text"
|
||||
onClick={() => (isDark ? darkMode.enable() : darkMode.disable())}
|
||||
>
|
||||
{isDark ? <Moon /> : <Sun />}
|
||||
{button}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
event.target.value === 'Dark' ? darkMode.enable() : darkMode.disable()
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={styles.appearances}>
|
||||
<Label htmlFor="">Appearance</Label>
|
||||
<BoxSelection
|
||||
options={options}
|
||||
name="appearanceMode"
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<FormHelp>Defaults to your OS setting, select to override.</FormHelp>
|
||||
</li>
|
||||
)
|
||||
|
@ -17,3 +17,14 @@
|
||||
.selected {
|
||||
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;
|
||||
}
|
||||
|
@ -1,52 +1,58 @@
|
||||
import { ConfigHelperConfig } from '@oceanprotocol/lib'
|
||||
import React, { ReactElement } from 'react'
|
||||
import React, { ReactElement, ChangeEvent } from 'react'
|
||||
import { useOcean } from '../../../providers/Ocean'
|
||||
import { useWeb3 } from '../../../providers/Web3'
|
||||
import { getOceanConfig } from '../../../utils/ocean'
|
||||
import Button from '../../atoms/Button'
|
||||
import FormHelp from '../../atoms/Input/Help'
|
||||
import Label from '../../atoms/Input/Label'
|
||||
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection'
|
||||
import styles from './Chain.module.css'
|
||||
|
||||
export default function Chain(): ReactElement {
|
||||
const { web3 } = useWeb3()
|
||||
const { config, connect } = useOcean()
|
||||
|
||||
async function connectOcean(networkName: string) {
|
||||
const config = getOceanConfig(networkName)
|
||||
async function connectOcean(event: ChangeEvent<HTMLInputElement>) {
|
||||
const config = getOceanConfig(event.target.value)
|
||||
await connect(config)
|
||||
}
|
||||
|
||||
const chains = [
|
||||
{ name: 'ETH', oceanConfig: 'mainnet' },
|
||||
{ name: 'Polygon/Matic', oceanConfig: 'polygon' }
|
||||
function isNetworkSelected(oceanConfig: string) {
|
||||
return (config as ConfigHelperConfig).network === oceanConfig
|
||||
}
|
||||
|
||||
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
|
||||
// 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.
|
||||
return !web3 ? (
|
||||
<li>
|
||||
<li className={styles.chains}>
|
||||
<Label htmlFor="">Chain</Label>
|
||||
<div className={styles.buttons}>
|
||||
{chains.map((button) => {
|
||||
const selected =
|
||||
(config as ConfigHelperConfig).network === button.oceanConfig
|
||||
|
||||
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>
|
||||
<BoxSelection
|
||||
options={options}
|
||||
name="chain"
|
||||
handleChange={connectOcean}
|
||||
/>
|
||||
<FormHelp>Switch the data source for the interface.</FormHelp>
|
||||
</li>
|
||||
) : null
|
||||
|
@ -42,7 +42,7 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.actions span {
|
||||
.walletLogoWrap {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -84,3 +84,7 @@
|
||||
.walletInfo button {
|
||||
margin-top: calc(var(--spacer) / 5) !important;
|
||||
}
|
||||
|
||||
.addToken {
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
|
@ -1,33 +1,24 @@
|
||||
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 { useOcean } from '../../../providers/Ocean'
|
||||
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 { addOceanToWallet } from '../../../utils/web3'
|
||||
import { Logger } from '@oceanprotocol/lib'
|
||||
|
||||
import Web3Feedback from './Feedback'
|
||||
import styles from './Details.module.css'
|
||||
|
||||
export default function Details(): ReactElement {
|
||||
const { web3Provider, connect, logout, networkData } = useWeb3()
|
||||
const { web3Provider, web3ProviderInfo, connect, logout, networkData } =
|
||||
useWeb3()
|
||||
const { balance, config } = useOcean()
|
||||
const { locale } = useUserPreferences()
|
||||
|
||||
const [providerInfo, setProviderInfo] = useState<IProviderInfo>()
|
||||
const [mainCurrency, setMainCurrency] = 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(() => {
|
||||
if (!networkData) return
|
||||
|
||||
@ -61,11 +52,11 @@ export default function Details(): ReactElement {
|
||||
|
||||
<li className={styles.actions}>
|
||||
<div title="Connected provider" className={styles.walletInfo}>
|
||||
<span>
|
||||
<img className={styles.walletLogo} src={providerInfo?.logo} />
|
||||
{providerInfo?.name}
|
||||
<span className={styles.walletLogoWrap}>
|
||||
<img className={styles.walletLogo} src={web3ProviderInfo?.logo} />
|
||||
{web3ProviderInfo?.name}
|
||||
</span>
|
||||
{/* {providerInfo?.name === 'Portis' && (
|
||||
{/* {web3ProviderInfo?.name === 'Portis' && (
|
||||
<InputElement
|
||||
name="network"
|
||||
type="select"
|
||||
@ -75,20 +66,17 @@ export default function Details(): ReactElement {
|
||||
onChange={handlePortisNetworkChange}
|
||||
/>
|
||||
)} */}
|
||||
{providerInfo?.name === 'MetaMask' && (
|
||||
<Button
|
||||
style="text"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
addOceanToWallet(config, web3Provider)
|
||||
}}
|
||||
>
|
||||
{`Add ${config.oceanTokenSymbol}`}
|
||||
</Button>
|
||||
{web3ProviderInfo?.name === 'MetaMask' && (
|
||||
<AddToken
|
||||
address={config.oceanTokenAddress}
|
||||
symbol={config.oceanTokenSymbol}
|
||||
logo="https://raw.githubusercontent.com/oceanprotocol/art/main/logo/token.png"
|
||||
className={styles.addToken}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p>
|
||||
{providerInfo?.name === 'Portis' && (
|
||||
{web3ProviderInfo?.name === 'Portis' && (
|
||||
<Button
|
||||
style="text"
|
||||
size="small"
|
||||
|
@ -82,10 +82,8 @@ export default function FormStartCompute({
|
||||
const data = useStaticQuery(contentQuery)
|
||||
const content = data.content.edges[0].node.childPagesJson
|
||||
|
||||
const {
|
||||
isValid,
|
||||
values
|
||||
}: FormikContextType<{ algorithm: string }> = useFormikContext()
|
||||
const { isValid, values }: FormikContextType<{ algorithm: string }> =
|
||||
useFormikContext()
|
||||
const { price, ddo } = useAsset()
|
||||
const [totalPrice, setTotalPrice] = useState(price?.value)
|
||||
|
||||
@ -108,17 +106,21 @@ export default function FormStartCompute({
|
||||
useEffect(() => {
|
||||
if (!price || !algorithmPrice) return
|
||||
|
||||
const priceDataset = hasPreviousOrder ? 0 : Number(price.value)
|
||||
const priceAlgo = hasPreviousOrderSelectedComputeAsset
|
||||
? 0
|
||||
: Number(algorithmPrice.value)
|
||||
const priceDataset =
|
||||
hasPreviousOrder || hasDatatoken ? 0 : Number(price.value)
|
||||
const priceAlgo =
|
||||
hasPreviousOrderSelectedComputeAsset || hasDatatokenSelectedComputeAsset
|
||||
? 0
|
||||
: Number(algorithmPrice.value)
|
||||
|
||||
setTotalPrice(priceDataset + priceAlgo)
|
||||
}, [
|
||||
price,
|
||||
algorithmPrice,
|
||||
hasPreviousOrder,
|
||||
hasPreviousOrderSelectedComputeAsset
|
||||
hasDatatoken,
|
||||
hasPreviousOrderSelectedComputeAsset,
|
||||
hasDatatokenSelectedComputeAsset
|
||||
])
|
||||
|
||||
return (
|
||||
@ -138,7 +140,9 @@ export default function FormStartCompute({
|
||||
hasPreviousOrderSelectedComputeAsset={
|
||||
hasPreviousOrderSelectedComputeAsset
|
||||
}
|
||||
hasDatatoken={hasDatatoken}
|
||||
selectedComputeAssetTimeout={selectedComputeAssetTimeout}
|
||||
hasDatatokenSelectedComputeAsset={hasDatatokenSelectedComputeAsset}
|
||||
algorithmPrice={algorithmPrice}
|
||||
totalPrice={totalPrice}
|
||||
/>
|
||||
@ -162,6 +166,8 @@ export default function FormStartCompute({
|
||||
stepText={stepText}
|
||||
isLoading={isLoading}
|
||||
type="submit"
|
||||
priceType={price?.type}
|
||||
algorithmPriceType={algorithmPrice?.type}
|
||||
/>
|
||||
</Form>
|
||||
)
|
||||
|
@ -8,8 +8,10 @@ import styles from './PriceOutput.module.css'
|
||||
interface PriceOutputProps {
|
||||
totalPrice: number
|
||||
hasPreviousOrder: boolean
|
||||
hasDatatoken: boolean
|
||||
assetTimeout: string
|
||||
hasPreviousOrderSelectedComputeAsset: boolean
|
||||
hasDatatokenSelectedComputeAsset: boolean
|
||||
algorithmPrice: BestPrice
|
||||
selectedComputeAssetTimeout: string
|
||||
}
|
||||
@ -17,11 +19,13 @@ interface PriceOutputProps {
|
||||
function Row({
|
||||
price,
|
||||
hasPreviousOrder,
|
||||
hasDatatoken,
|
||||
timeout,
|
||||
sign
|
||||
}: {
|
||||
price: number
|
||||
hasPreviousOrder?: boolean
|
||||
hasDatatoken?: boolean
|
||||
timeout?: string
|
||||
sign?: string
|
||||
}) {
|
||||
@ -30,7 +34,7 @@ function Row({
|
||||
<div className={styles.sign}>{sign}</div>
|
||||
<div>
|
||||
<PriceUnit
|
||||
price={hasPreviousOrder ? '0' : `${price}`}
|
||||
price={hasPreviousOrder || hasDatatoken ? '0' : `${price}`}
|
||||
small
|
||||
className={styles.price}
|
||||
/>
|
||||
@ -48,8 +52,10 @@ function Row({
|
||||
export default function PriceOutput({
|
||||
totalPrice,
|
||||
hasPreviousOrder,
|
||||
hasDatatoken,
|
||||
assetTimeout,
|
||||
hasPreviousOrderSelectedComputeAsset,
|
||||
hasDatatokenSelectedComputeAsset,
|
||||
algorithmPrice,
|
||||
selectedComputeAssetTimeout
|
||||
}: PriceOutputProps): ReactElement {
|
||||
@ -63,11 +69,13 @@ export default function PriceOutput({
|
||||
<div className={styles.calculation}>
|
||||
<Row
|
||||
hasPreviousOrder={hasPreviousOrder}
|
||||
hasDatatoken={hasDatatoken}
|
||||
price={price?.value}
|
||||
timeout={assetTimeout}
|
||||
/>
|
||||
<Row
|
||||
hasPreviousOrder={hasPreviousOrderSelectedComputeAsset}
|
||||
hasDatatoken={hasDatatokenSelectedComputeAsset}
|
||||
price={algorithmPrice?.value}
|
||||
timeout={selectedComputeAssetTimeout}
|
||||
sign="+"
|
||||
|
@ -3,7 +3,6 @@ import {
|
||||
DDO,
|
||||
File as FileMetadata,
|
||||
Logger,
|
||||
ServiceType,
|
||||
publisherTrustedAlgorithm,
|
||||
BestPrice
|
||||
} from '@oceanprotocol/lib'
|
||||
@ -26,52 +25,39 @@ import {
|
||||
getInitialValues,
|
||||
validationSchema
|
||||
} from '../../../../models/FormStartComputeDataset'
|
||||
import { ComputeAlgorithm } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
|
||||
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
|
||||
import {
|
||||
ComputeAlgorithm,
|
||||
ComputeOutput
|
||||
} from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
|
||||
import { SearchQuery } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
|
||||
import axios from 'axios'
|
||||
import FormStartComputeDataset from './FormComputeDataset'
|
||||
import styles from './index.module.css'
|
||||
import SuccessConfetti from '../../../atoms/SuccessConfetti'
|
||||
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 { getPreviousOrders } from '../../../../utils/subgraph'
|
||||
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
|
||||
import AlgorithmDatasetsListForCompute from '../../AssetContent/AlgorithmDatasetsListForCompute'
|
||||
import { getPreviousOrders, getPrice } from '../../../../utils/subgraph'
|
||||
|
||||
const SuccessAction = () => (
|
||||
<Button style="text" to="/history" size="small">
|
||||
<Button style="text" to="/history?defaultTab=ComputeJobs" size="small">
|
||||
Go to history →
|
||||
</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({
|
||||
isBalanceSufficient,
|
||||
dtBalance,
|
||||
file
|
||||
file,
|
||||
fileIsLoading
|
||||
}: {
|
||||
isBalanceSufficient: boolean
|
||||
dtBalance: string
|
||||
file: FileMetadata
|
||||
fileIsLoading?: boolean
|
||||
}): ReactElement {
|
||||
const { marketFeeAddress } = useSiteMetadata()
|
||||
const { appConfig } = useSiteMetadata()
|
||||
const { accountId } = useWeb3()
|
||||
const { ocean, account, config } = useOcean()
|
||||
const { price, type, ddo } = useAsset()
|
||||
@ -86,38 +72,15 @@ export default function Compute({
|
||||
const [isPublished, setIsPublished] = useState(false)
|
||||
const [hasPreviousDatasetOrder, setHasPreviousDatasetOrder] = useState(false)
|
||||
const [previousDatasetOrderId, setPreviousDatasetOrderId] = useState<string>()
|
||||
const [hasPreviousAlgorithmOrder, setHasPreviousAlgorithmOrder] = useState(
|
||||
false
|
||||
)
|
||||
const [hasPreviousAlgorithmOrder, setHasPreviousAlgorithmOrder] =
|
||||
useState(false)
|
||||
const [algorithmDTBalance, setalgorithmDTBalance] = useState<string>()
|
||||
const [algorithmPrice, setAlgorithmPrice] = useState<BestPrice>()
|
||||
const [variables, setVariables] = useState({})
|
||||
const [
|
||||
previousAlgorithmOrderId,
|
||||
setPreviousAlgorithmOrderId
|
||||
] = useState<string>()
|
||||
const [previousAlgorithmOrderId, setPreviousAlgorithmOrderId] =
|
||||
useState<string>()
|
||||
const [datasetTimeout, setDatasetTimeout] = 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 =
|
||||
isJobStarting === true || file === null || !ocean || !isBalanceSufficient
|
||||
const hasDatatoken = Number(dtBalance) >= 1
|
||||
@ -163,7 +126,7 @@ export default function Compute({
|
||||
const algorithmQuery =
|
||||
trustedAlgorithmList.length > 0 ? `(${algoQuerry}) AND` : ``
|
||||
const query = {
|
||||
page: 1,
|
||||
offset: 500,
|
||||
query: {
|
||||
query_string: {
|
||||
query: `${algorithmQuery} service.attributes.main.type:algorithm -isInPurgatory:true`
|
||||
@ -213,37 +176,10 @@ export default function Compute({
|
||||
setDatasetTimeout(secondsToString(timeout))
|
||||
}, [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> => {
|
||||
if (!ddo) return
|
||||
setAlgorithmPrice(ddo.price)
|
||||
setVariables({ datatoken: ddo?.dataToken.toLowerCase() })
|
||||
const price = await getPrice(ddo)
|
||||
setAlgorithmPrice(price)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@ -259,26 +195,30 @@ export default function Compute({
|
||||
}, [ocean, ddo, accountId])
|
||||
|
||||
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)
|
||||
|
||||
const { timeout } = (
|
||||
ddo.findServiceByType('access') || ddo.findServiceByType('compute')
|
||||
).attributes.main
|
||||
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])
|
||||
|
||||
// Output errors in toast UI
|
||||
@ -358,7 +298,9 @@ export default function Compute({
|
||||
ddo.id,
|
||||
computeService.index,
|
||||
computeAlgorithm,
|
||||
marketFeeAddress
|
||||
appConfig.marketFeeAddress,
|
||||
undefined,
|
||||
false
|
||||
)
|
||||
|
||||
assetOrderId &&
|
||||
@ -376,7 +318,9 @@ export default function Compute({
|
||||
serviceAlgo.type,
|
||||
accountId,
|
||||
serviceAlgo.index,
|
||||
marketFeeAddress
|
||||
appConfig.marketFeeAddress,
|
||||
undefined,
|
||||
false
|
||||
)
|
||||
|
||||
algorithmAssetOrderId &&
|
||||
@ -395,7 +339,10 @@ export default function Compute({
|
||||
computeAlgorithm.transferTxId = algorithmAssetOrderId
|
||||
Logger.log('[compute] Starting compute job.')
|
||||
|
||||
const output = {}
|
||||
const output: ComputeOutput = {
|
||||
publishAlgorithmLog: true,
|
||||
publishOutput: true
|
||||
}
|
||||
const response = await ocean.compute.start(
|
||||
ddo.id,
|
||||
assetOrderId,
|
||||
@ -414,9 +361,12 @@ export default function Compute({
|
||||
|
||||
Logger.log('[compute] Starting compute job response: ', response)
|
||||
|
||||
setHasPreviousDatasetOrder(true)
|
||||
await checkPreviousOrders(selectedAlgorithmAsset)
|
||||
await checkPreviousOrders(ddo)
|
||||
setIsPublished(true)
|
||||
} catch (error) {
|
||||
await checkPreviousOrders(selectedAlgorithmAsset)
|
||||
await checkPreviousOrders(ddo)
|
||||
setError('Failed to start job!')
|
||||
Logger.error('[compute] Failed to start job: ', error.message)
|
||||
} finally {
|
||||
@ -427,15 +377,18 @@ export default function Compute({
|
||||
return (
|
||||
<>
|
||||
<div className={styles.info}>
|
||||
<File file={file} small />
|
||||
<File file={file} isLoading={fileIsLoading} small />
|
||||
<Price price={price} conversion />
|
||||
</div>
|
||||
|
||||
{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!"
|
||||
state="info"
|
||||
/>
|
||||
<>
|
||||
<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!"
|
||||
state="info"
|
||||
/>
|
||||
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} />
|
||||
</>
|
||||
) : (
|
||||
<Formik
|
||||
initialValues={getInitialValues()}
|
||||
@ -475,7 +428,9 @@ export default function Compute({
|
||||
action={<SuccessAction />}
|
||||
/>
|
||||
)}
|
||||
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
|
||||
{type !== 'algorithm' && (
|
||||
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
|
||||
)}
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
|
@ -5,7 +5,10 @@
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
width: auto;
|
||||
margin-left: -2rem;
|
||||
margin-right: -2rem;
|
||||
padding: 0 calc(var(--spacer)) 0 calc(var(--spacer));
|
||||
}
|
||||
|
||||
.filewrapper {
|
||||
|
@ -16,6 +16,7 @@ import { useWeb3 } from '../../../providers/Web3'
|
||||
import { usePricing } from '../../../hooks/usePricing'
|
||||
import { useConsume } from '../../../hooks/useConsume'
|
||||
import ButtonBuy from '../../atoms/ButtonBuy'
|
||||
import AlgorithmDatasetsListForCompute from '../AssetContent/AlgorithmDatasetsListForCompute'
|
||||
|
||||
const previousOrderQuery = gql`
|
||||
query PreviousOrder($id: String!, $account: String!) {
|
||||
@ -35,31 +36,28 @@ export default function Consume({
|
||||
ddo,
|
||||
file,
|
||||
isBalanceSufficient,
|
||||
dtBalance
|
||||
dtBalance,
|
||||
fileIsLoading
|
||||
}: {
|
||||
ddo: DDO
|
||||
file: FileMetadata
|
||||
isBalanceSufficient: boolean
|
||||
dtBalance: string
|
||||
fileIsLoading?: boolean
|
||||
}): ReactElement {
|
||||
const { accountId } = useWeb3()
|
||||
const { ocean } = useOcean()
|
||||
const { marketFeeAddress } = useSiteMetadata()
|
||||
const { appConfig } = useSiteMetadata()
|
||||
const [hasPreviousOrder, setHasPreviousOrder] = useState(false)
|
||||
const [previousOrderId, setPreviousOrderId] = useState<string>()
|
||||
const { isInPurgatory, price, type } = useAsset()
|
||||
const {
|
||||
buyDT,
|
||||
pricingStepText,
|
||||
pricingError,
|
||||
pricingIsLoading
|
||||
} = usePricing()
|
||||
const { consumeStepText, consume, consumeError } = useConsume()
|
||||
const { buyDT, pricingStepText, pricingError, pricingIsLoading } =
|
||||
usePricing()
|
||||
const { consumeStepText, consume, consumeError, isLoading } = useConsume()
|
||||
const [isDisabled, setIsDisabled] = useState(true)
|
||||
const [hasDatatoken, setHasDatatoken] = useState(false)
|
||||
const [isConsumable, setIsConsumable] = useState(true)
|
||||
const [assetTimeout, setAssetTimeout] = useState('')
|
||||
|
||||
const { data } = useQuery<OrdersData>(previousOrderQuery, {
|
||||
variables: {
|
||||
id: ddo.dataToken?.toLowerCase(),
|
||||
@ -90,7 +88,7 @@ export default function Consume({
|
||||
|
||||
useEffect(() => {
|
||||
const { timeout } = ddo.findServiceByType('access').attributes.main
|
||||
setAssetTimeout(secondsToString(timeout))
|
||||
setAssetTimeout(timeout.toString())
|
||||
}, [ddo])
|
||||
|
||||
useEffect(() => {
|
||||
@ -126,22 +124,28 @@ export default function Consume({
|
||||
])
|
||||
|
||||
async function handleConsume() {
|
||||
!hasPreviousOrder && !hasDatatoken && (await buyDT('1', price, ddo))
|
||||
await consume(
|
||||
if (!hasPreviousOrder && !hasDatatoken) {
|
||||
const tx = await buyDT('1', price, ddo)
|
||||
if (tx === undefined) return
|
||||
}
|
||||
const error = await consume(
|
||||
ddo.id,
|
||||
ddo.dataToken,
|
||||
'access',
|
||||
marketFeeAddress,
|
||||
appConfig.marketFeeAddress,
|
||||
previousOrderId
|
||||
)
|
||||
setHasPreviousOrder(true)
|
||||
error || setHasPreviousOrder(true)
|
||||
}
|
||||
|
||||
// Output errors in UI
|
||||
useEffect(() => {
|
||||
consumeError && toast.error(consumeError)
|
||||
}, [consumeError])
|
||||
|
||||
useEffect(() => {
|
||||
pricingError && toast.error(pricingError)
|
||||
}, [consumeError, pricingError])
|
||||
}, [pricingError])
|
||||
|
||||
const PurchaseButton = () => (
|
||||
<ButtonBuy
|
||||
@ -152,10 +156,11 @@ export default function Consume({
|
||||
dtSymbol={ddo.dataTokenInfo?.symbol}
|
||||
dtBalance={dtBalance}
|
||||
onClick={handleConsume}
|
||||
assetTimeout={assetTimeout}
|
||||
assetTimeout={secondsToString(parseInt(assetTimeout))}
|
||||
assetType={type}
|
||||
stepText={consumeStepText || pricingStepText}
|
||||
isLoading={pricingIsLoading}
|
||||
isLoading={pricingIsLoading || isLoading}
|
||||
priceType={price?.type}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -163,13 +168,16 @@ export default function Consume({
|
||||
<aside className={styles.consume}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.filewrapper}>
|
||||
<File file={file} />
|
||||
<File file={file} isLoading={fileIsLoading} />
|
||||
</div>
|
||||
<div className={styles.pricewrapper}>
|
||||
<Price price={price} conversion />
|
||||
{!isInPurgatory && <PurchaseButton />}
|
||||
</div>
|
||||
</div>
|
||||
{type === 'algorithm' && (
|
||||
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} />
|
||||
)}
|
||||
<footer className={styles.feedback}>
|
||||
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
|
||||
</footer>
|
||||
|
@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
@ -13,10 +13,8 @@ export default function DebugEditCompute({
|
||||
ddo: DDO
|
||||
}): ReactElement {
|
||||
const { ocean } = useOcean()
|
||||
const [
|
||||
formTransformed,
|
||||
setFormTransformed
|
||||
] = useState<ServiceComputePrivacy>()
|
||||
const [formTransformed, setFormTransformed] =
|
||||
useState<ServiceComputePrivacy>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!ocean) return
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -16,6 +16,10 @@ import { useUserPreferences } from '../../../../providers/UserPreferences'
|
||||
import DebugEditCompute from './DebugEditCompute'
|
||||
import styles from './index.module.css'
|
||||
import { transformComputeFormToServiceComputePrivacy } from '../../../../utils/compute'
|
||||
import {
|
||||
setMinterToDispenser,
|
||||
setMinterToPublisher
|
||||
} from '../../../../utils/freePrice'
|
||||
|
||||
const contentQuery = graphql`
|
||||
query EditComputeDataQuery {
|
||||
@ -62,7 +66,7 @@ export default function EditComputeDataset({
|
||||
const { debug } = useUserPreferences()
|
||||
const { ocean } = useOcean()
|
||||
const { accountId } = useWeb3()
|
||||
const { ddo, refreshDdo } = useAsset()
|
||||
const { ddo, refreshDdo, price } = useAsset()
|
||||
const [success, setSuccess] = useState<string>()
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
@ -73,6 +77,15 @@ export default function EditComputeDataset({
|
||||
resetForm: () => void
|
||||
) {
|
||||
try {
|
||||
if (price.type === 'free') {
|
||||
const tx = await setMinterToPublisher(
|
||||
ocean,
|
||||
ddo.dataToken,
|
||||
accountId,
|
||||
setError
|
||||
)
|
||||
if (!tx) return
|
||||
}
|
||||
const privacy = await transformComputeFormToServiceComputePrivacy(
|
||||
values,
|
||||
ocean
|
||||
@ -99,6 +112,15 @@ export default function EditComputeDataset({
|
||||
Logger.error(content.form.error)
|
||||
return
|
||||
} else {
|
||||
if (price.type === 'free') {
|
||||
const tx = await setMinterToDispenser(
|
||||
ocean,
|
||||
ddo.dataToken,
|
||||
accountId,
|
||||
setError
|
||||
)
|
||||
if (!tx) return
|
||||
}
|
||||
// Edit succeeded
|
||||
setSuccess(content.form.success)
|
||||
resetForm()
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -29,22 +29,19 @@ export default function FormEditComputeDataset({
|
||||
const { accountId } = useWeb3()
|
||||
const { ocean, config } = useOcean()
|
||||
const { ddo } = useAsset()
|
||||
const {
|
||||
isValid,
|
||||
values
|
||||
}: FormikContextType<ComputePrivacyForm> = useFormikContext()
|
||||
const { isValid, values }: FormikContextType<ComputePrivacyForm> =
|
||||
useFormikContext()
|
||||
const [allAlgorithms, setAllAlgorithms] = useState<AssetSelectionAsset[]>()
|
||||
|
||||
const { publisherTrustedAlgorithms } = ddo?.findServiceByType(
|
||||
'compute'
|
||||
).attributes.main.privacy
|
||||
const { publisherTrustedAlgorithms } =
|
||||
ddo?.findServiceByType('compute').attributes.main.privacy
|
||||
|
||||
async function getAlgorithmList(
|
||||
publisherTrustedAlgorithms: PublisherTrustedAlgorithm[]
|
||||
): Promise<AssetSelectionAsset[]> {
|
||||
const source = axios.CancelToken.source()
|
||||
const query = {
|
||||
page: 1,
|
||||
offset: 500,
|
||||
query: {
|
||||
query_string: {
|
||||
query: `service.attributes.main.type:algorithm -isInPurgatory:true`
|
||||
|
@ -47,15 +47,17 @@ export default function FormEditMetadata({
|
||||
data,
|
||||
setShowEdit,
|
||||
setTimeoutStringValue,
|
||||
values
|
||||
values,
|
||||
showPrice
|
||||
}: {
|
||||
data: FormFieldProps[]
|
||||
setShowEdit: (show: boolean) => void
|
||||
setTimeoutStringValue: (value: string) => void
|
||||
values: Partial<MetadataPublishFormDataset>
|
||||
showPrice: boolean
|
||||
}): ReactElement {
|
||||
const { accountId } = useWeb3()
|
||||
const { ocean } = useOcean()
|
||||
const { ocean, config } = useOcean()
|
||||
const {
|
||||
isValid,
|
||||
validateField,
|
||||
@ -79,16 +81,20 @@ export default function FormEditMetadata({
|
||||
|
||||
return (
|
||||
<Form className={styles.form}>
|
||||
{data.map((field: FormFieldProps) => (
|
||||
<Field
|
||||
key={field.name}
|
||||
{...field}
|
||||
component={Input}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
handleFieldChange(e, field)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{data.map(
|
||||
(field: FormFieldProps) =>
|
||||
(!showPrice && field.name === 'price') || (
|
||||
<Field
|
||||
key={field.name}
|
||||
{...field}
|
||||
component={Input}
|
||||
prefix={field.name === 'price' && config.oceanTokenSymbol}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
handleFieldChange(e, field)
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
<footer className={styles.actions}>
|
||||
<Button
|
||||
|
@ -18,6 +18,10 @@ import MetadataFeedback from '../../../molecules/MetadataFeedback'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import { useWeb3 } from '../../../../providers/Web3'
|
||||
import { useOcean } from '../../../../providers/Ocean'
|
||||
import {
|
||||
setMinterToDispenser,
|
||||
setMinterToPublisher
|
||||
} from '../../../../utils/freePrice'
|
||||
|
||||
const contentQuery = graphql`
|
||||
query EditMetadataQuery {
|
||||
@ -36,6 +40,7 @@ const contentQuery = graphql`
|
||||
label
|
||||
help
|
||||
type
|
||||
min
|
||||
required
|
||||
sortOptions
|
||||
options
|
||||
@ -60,7 +65,7 @@ export default function Edit({
|
||||
const { debug } = useUserPreferences()
|
||||
const { accountId } = useWeb3()
|
||||
const { ocean } = useOcean()
|
||||
const { metadata, ddo, refreshDdo } = useAsset()
|
||||
const { metadata, ddo, refreshDdo, price } = useAsset()
|
||||
const [success, setSuccess] = useState<string>()
|
||||
const [error, setError] = useState<string>()
|
||||
const [timeoutStringValue, setTimeoutStringValue] = useState<string>()
|
||||
@ -70,11 +75,32 @@ export default function Edit({
|
||||
|
||||
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(
|
||||
values: Partial<MetadataEditForm>,
|
||||
resetForm: () => void
|
||||
) {
|
||||
try {
|
||||
if (price.type === 'free') {
|
||||
const tx = await setMinterToPublisher(
|
||||
ocean,
|
||||
ddo.dataToken,
|
||||
accountId,
|
||||
setError
|
||||
)
|
||||
if (!tx) return
|
||||
}
|
||||
// Construct new DDO with new values
|
||||
const ddoEditedMetdata = await ocean.assets.editMetadata(ddo, {
|
||||
title: values.name,
|
||||
@ -82,6 +108,10 @@ export default function Edit({
|
||||
links: typeof values.links !== 'string' ? values.links : []
|
||||
})
|
||||
|
||||
price.type === 'exchange' &&
|
||||
values.price !== price.value &&
|
||||
(await updateFixedPrice(values.price))
|
||||
|
||||
if (!ddoEditedMetdata) {
|
||||
setError(content.form.error)
|
||||
Logger.error(content.form.error)
|
||||
@ -115,6 +145,15 @@ export default function Edit({
|
||||
Logger.error(content.form.error)
|
||||
return
|
||||
} else {
|
||||
if (price.type === 'free') {
|
||||
const tx = await setMinterToDispenser(
|
||||
ocean,
|
||||
ddo.dataToken,
|
||||
accountId,
|
||||
setError
|
||||
)
|
||||
if (!tx) return
|
||||
}
|
||||
// Edit succeeded
|
||||
setSuccess(content.form.success)
|
||||
resetForm()
|
||||
@ -127,7 +166,7 @@ export default function Edit({
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={getInitialValues(metadata, timeout)}
|
||||
initialValues={getInitialValues(metadata, timeout, price.value)}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={async (values, { resetForm }) => {
|
||||
// move user's focus to top of screen
|
||||
@ -160,6 +199,7 @@ export default function Edit({
|
||||
setShowEdit={setShowEdit}
|
||||
setTimeoutStringValue={setTimeoutStringValue}
|
||||
values={initialValues}
|
||||
showPrice={price.type === 'exchange'}
|
||||
/>
|
||||
|
||||
<aside>
|
||||
|
@ -90,7 +90,7 @@ export default function FormAdd({
|
||||
amountMax,
|
||||
coin,
|
||||
poolAddress,
|
||||
ocean.pool,
|
||||
ocean?.pool,
|
||||
setNewPoolTokens,
|
||||
setNewPoolShare
|
||||
])
|
||||
|
@ -48,11 +48,8 @@ export default function Output({
|
||||
coin: string
|
||||
}): ReactElement {
|
||||
const data = useStaticQuery(contentQuery)
|
||||
const {
|
||||
help,
|
||||
titleIn,
|
||||
titleOut
|
||||
} = data.content.edges[0].node.childContentJson.pool.add.output
|
||||
const { help, titleIn, titleOut } =
|
||||
data.content.edges[0].node.childContentJson.pool.add.output
|
||||
|
||||
// Connect with form
|
||||
const { values }: FormikContextType<FormAddLiquidity> = useFormikContext()
|
||||
|
@ -174,7 +174,7 @@ export default function Add({
|
||||
) : (
|
||||
<Alert
|
||||
className={styles.warning}
|
||||
text={content.warning.main}
|
||||
text={content.warning}
|
||||
state="info"
|
||||
action={{
|
||||
name: 'I understand',
|
||||
|
@ -123,11 +123,10 @@ export default function Graph(): ReactElement {
|
||||
|
||||
const { price } = useAsset()
|
||||
|
||||
const [lastBlock, setLastBlock] = useState(0)
|
||||
const [lastBlock, setLastBlock] = useState<number>(0)
|
||||
const [priceHistory, setPriceHistory] = useState([])
|
||||
const [liquidityHistory, setLiquidityHistory] = useState([])
|
||||
const [timestamps, setTimestamps] = useState([])
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [graphData, setGraphData] = useState<ChartData>()
|
||||
|
||||
@ -156,7 +155,6 @@ export default function Graph(): ReactElement {
|
||||
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
|
||||
})
|
||||
]
|
||||
|
||||
setTimestamps(latestTimestamps)
|
||||
|
||||
const latestLiquidtyHistory = [
|
||||
@ -170,17 +168,20 @@ export default function Graph(): ReactElement {
|
||||
...priceHistory,
|
||||
...data.poolTransactions.map((item) => item.spotPrice)
|
||||
]
|
||||
|
||||
setPriceHistory(latestPriceHistory)
|
||||
|
||||
if (data.poolTransactions.length > 0) {
|
||||
const newBlock =
|
||||
data.poolTransactions[data.poolTransactions.length - 1].block
|
||||
if (newBlock === lastBlock) return
|
||||
setLastBlock(
|
||||
data.poolTransactions[data.poolTransactions.length - 1].block
|
||||
)
|
||||
refetch()
|
||||
} else {
|
||||
setIsLoading(false)
|
||||
setGraphData({
|
||||
labels: timestamps.slice(0),
|
||||
labels: latestTimestamps.slice(0),
|
||||
datasets: [
|
||||
{
|
||||
...lineStyle,
|
||||
@ -194,6 +195,7 @@ export default function Graph(): ReactElement {
|
||||
}
|
||||
]
|
||||
})
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [data, graphType])
|
||||
|
||||
|
@ -21,6 +21,7 @@ import UserLiquidity from '../../../atoms/UserLiquidity'
|
||||
import InputElement from '../../../atoms/Input/InputElement'
|
||||
import { useOcean } from '../../../../providers/Ocean'
|
||||
import { useWeb3 } from '../../../../providers/Web3'
|
||||
import Decimal from 'decimal.js'
|
||||
|
||||
const contentQuery = graphql`
|
||||
query PoolRemoveQuery {
|
||||
@ -80,6 +81,8 @@ export default function Remove({
|
||||
const [minOceanAmount, setMinOceanAmount] = useState<string>('0')
|
||||
const [minDatatokenAmount, setMinDatatokenAmount] = useState<string>('0')
|
||||
|
||||
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
|
||||
|
||||
async function handleRemoveLiquidity() {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
@ -155,21 +158,19 @@ export default function Remove({
|
||||
totalPoolTokens
|
||||
])
|
||||
|
||||
async function calculateAmountOfOceansRemoved(amountPoolShares: string) {
|
||||
const oceanAmount = await ocean.pool.getOceanRemovedforPoolShares(
|
||||
poolAddress,
|
||||
amountPoolShares
|
||||
)
|
||||
setAmountOcean(oceanAmount)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const minOceanAmount =
|
||||
(Number(amountOcean) * (100 - Number(slippage))) / 100
|
||||
const minDatatokenAmount =
|
||||
(Number(amountDatatoken) * (100 - Number(slippage))) / 100
|
||||
setMinOceanAmount(`${minOceanAmount}`)
|
||||
setMinDatatokenAmount(`${minDatatokenAmount}`)
|
||||
const minOceanAmount = new Decimal(amountOcean)
|
||||
.mul(new Decimal(100).minus(new Decimal(slippage)))
|
||||
.dividedBy(100)
|
||||
.toString()
|
||||
|
||||
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])
|
||||
|
||||
// Set amountPoolShares based on set slider value
|
||||
@ -177,19 +178,24 @@ export default function Remove({
|
||||
setAmountPercent(e.target.value)
|
||||
if (!poolTokens) return
|
||||
|
||||
const amountPoolShares = (Number(e.target.value) / 100) * Number(poolTokens)
|
||||
setAmountPoolShares(`${amountPoolShares}`)
|
||||
calculateAmountOfOceansRemoved(`${amountPoolShares}`)
|
||||
const amountPoolShares = new Decimal(e.target.value)
|
||||
.dividedBy(100)
|
||||
.mul(new Decimal(poolTokens))
|
||||
.toString()
|
||||
|
||||
setAmountPoolShares(`${amountPoolShares.slice(0, 18)}`)
|
||||
}
|
||||
|
||||
function handleMaxButton(e: ChangeEvent<HTMLInputElement>) {
|
||||
e.preventDefault()
|
||||
setAmountPercent(amountMaxPercent)
|
||||
|
||||
const amountPoolShares =
|
||||
(Number(amountMaxPercent) / 100) * Number(poolTokens)
|
||||
setAmountPoolShares(`${amountPoolShares}`)
|
||||
calculateAmountOfOceansRemoved(`${amountPoolShares}`)
|
||||
const amountPoolShares = new Decimal(amountMaxPercent)
|
||||
.dividedBy(100)
|
||||
.mul(new Decimal(poolTokens))
|
||||
.toString()
|
||||
|
||||
setAmountPoolShares(`${amountPoolShares.slice(0, 18)}`)
|
||||
}
|
||||
|
||||
function handleAdvancedButton(e: FormEvent<HTMLButtonElement>) {
|
||||
|
@ -7,7 +7,7 @@ import { useAsset } from '../../../../providers/Asset'
|
||||
|
||||
export default function Transactions(): ReactElement {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { ddo } = useAsset()
|
||||
const { price } = useAsset()
|
||||
function handleClick() {
|
||||
setOpen(!open)
|
||||
}
|
||||
@ -29,7 +29,7 @@ export default function Transactions(): ReactElement {
|
||||
</Button>
|
||||
</h3>
|
||||
{open === true && (
|
||||
<PoolTransactions poolAddress={ddo.price?.address} minimal />
|
||||
<PoolTransactions poolAddress={price?.address} minimal />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
@ -44,6 +44,7 @@ const poolLiquidityQuery = gql`
|
||||
id
|
||||
totalShares
|
||||
swapFee
|
||||
spotPrice
|
||||
tokens {
|
||||
tokenAddress
|
||||
balance
|
||||
@ -82,10 +83,8 @@ export default function Pool(): ReactElement {
|
||||
const [totalUserLiquidityInOcean, setTotalUserLiquidityInOcean] = useState(0)
|
||||
const [totalLiquidityInOcean, setTotalLiquidityInOcean] = useState(0)
|
||||
|
||||
const [
|
||||
creatorTotalLiquidityInOcean,
|
||||
setCreatorTotalLiquidityInOcean
|
||||
] = useState(0)
|
||||
const [creatorTotalLiquidityInOcean, setCreatorTotalLiquidityInOcean] =
|
||||
useState(0)
|
||||
const [creatorLiquidity, setCreatorLiquidity] = useState<PoolBalance>()
|
||||
const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>()
|
||||
const [creatorPoolShare, setCreatorPoolShare] = useState<string>()
|
||||
@ -94,8 +93,8 @@ export default function Pool(): ReactElement {
|
||||
const [refreshPool, setRefreshPool] = useState(false)
|
||||
const { data: dataLiquidity } = useQuery<PoolLiquidity>(poolLiquidityQuery, {
|
||||
variables: {
|
||||
id: ddo.price.address.toLowerCase(),
|
||||
shareId: `${ddo.price.address.toLowerCase()}-${ddo.publicKey[0].owner.toLowerCase()}`
|
||||
id: price.address.toLowerCase(),
|
||||
shareId: `${price.address.toLowerCase()}-${ddo.publicKey[0].owner.toLowerCase()}`
|
||||
},
|
||||
pollInterval: 5000
|
||||
})
|
||||
@ -141,7 +140,8 @@ export default function Pool(): ReactElement {
|
||||
setCreatorLiquidity(creatorLiquidity)
|
||||
|
||||
const totalCreatorLiquidityInOcean =
|
||||
creatorLiquidity?.ocean + creatorLiquidity?.datatoken * price?.value
|
||||
creatorLiquidity?.ocean +
|
||||
creatorLiquidity?.datatoken * dataLiquidity.pool.spotPrice
|
||||
setCreatorTotalLiquidityInOcean(totalCreatorLiquidityInOcean)
|
||||
const creatorPoolShare =
|
||||
price?.ocean &&
|
||||
@ -168,7 +168,8 @@ export default function Pool(): ReactElement {
|
||||
const totalUserLiquidityInOcean =
|
||||
userLiquidity?.ocean + userLiquidity?.datatoken * price?.value
|
||||
setTotalUserLiquidityInOcean(totalUserLiquidityInOcean)
|
||||
const totalLiquidityInOcean = price?.ocean + price?.datatoken * price?.value
|
||||
const totalLiquidityInOcean =
|
||||
Number(price?.ocean) + Number(price?.datatoken) * Number(price?.value)
|
||||
setTotalLiquidityInOcean(totalLiquidityInOcean)
|
||||
}, [userLiquidity, price, poolTokens, totalPoolTokens])
|
||||
|
||||
@ -250,7 +251,7 @@ export default function Pool(): ReactElement {
|
||||
<ExplorerLink
|
||||
networkId={networkId}
|
||||
path={
|
||||
networkId === 137
|
||||
networkId === 137 || networkId === 1287
|
||||
? `tokens/${ddo.dataToken}`
|
||||
: `token/${ddo.dataToken}`
|
||||
}
|
||||
|
@ -9,10 +9,11 @@ export async function getMaxPercentRemove(
|
||||
poolAddress
|
||||
)
|
||||
|
||||
const amountMaxPoolShares = await ocean.pool.getPoolSharesRequiredToRemoveOcean(
|
||||
poolAddress,
|
||||
amountMaxOcean
|
||||
)
|
||||
const amountMaxPoolShares =
|
||||
await ocean.pool.getPoolSharesRequiredToRemoveOcean(
|
||||
poolAddress,
|
||||
amountMaxOcean
|
||||
)
|
||||
|
||||
let amountMaxPercent = `${Math.floor(
|
||||
(Number(amountMaxPoolShares) / Number(poolTokens)) * 100
|
||||
|
@ -64,7 +64,7 @@ export default function FormTrade({
|
||||
.required('Required')
|
||||
.nullable(),
|
||||
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}`)
|
||||
.required('Required')
|
||||
.nullable(),
|
||||
|
@ -6,10 +6,8 @@ import styles from './Slippage.module.css'
|
||||
|
||||
export default function Slippage(): ReactElement {
|
||||
// Connect with form
|
||||
const {
|
||||
setFieldValue,
|
||||
values
|
||||
}: FormikContextType<FormTradeData> = useFormikContext()
|
||||
const { setFieldValue, values }: FormikContextType<FormTradeData> =
|
||||
useFormikContext()
|
||||
|
||||
function handleChange(e: ChangeEvent<HTMLSelectElement>) {
|
||||
setFieldValue('slippage', e.target.value)
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { ReactElement, useState, useEffect } from 'react'
|
||||
import Permission from '../Permission'
|
||||
import styles from './index.module.css'
|
||||
import Compute from './Compute'
|
||||
import Consume from './Consume'
|
||||
import { Logger } from '@oceanprotocol/lib'
|
||||
import { Logger, File as FileMetadata, DID } from '@oceanprotocol/lib'
|
||||
import Tabs from '../../atoms/Tabs'
|
||||
import compareAsBN from '../../../utils/compareAsBN'
|
||||
import Pool from './Pool'
|
||||
@ -10,21 +11,47 @@ import Trade from './Trade'
|
||||
import { useAsset } from '../../../providers/Asset'
|
||||
import { useOcean } from '../../../providers/Ocean'
|
||||
import { useWeb3 } from '../../../providers/Web3'
|
||||
import { fileinfo, getFileInfo } from '../../../utils/provider'
|
||||
import axios from 'axios'
|
||||
|
||||
export default function AssetActions(): ReactElement {
|
||||
const { accountId } = useWeb3()
|
||||
const { ocean, balance, account } = useOcean()
|
||||
const { price, ddo, metadata } = useAsset()
|
||||
const { config, ocean, balance, account } = useOcean()
|
||||
const { price, ddo } = useAsset()
|
||||
|
||||
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
|
||||
const [dtBalance, setDtBalance] = useState<string>()
|
||||
|
||||
const [fileMetadata, setFileMetadata] = useState<FileMetadata>(Object)
|
||||
const [fileIsLoading, setFileIsLoading] = useState<boolean>(false)
|
||||
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
|
||||
useEffect(() => {
|
||||
if (!ocean || !accountId) return
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const dtBalance = await ocean.datatokens.balance(
|
||||
@ -41,6 +68,7 @@ export default function AssetActions(): ReactElement {
|
||||
|
||||
// Check user balance against price
|
||||
useEffect(() => {
|
||||
if (price?.type === 'free') setIsBalanceSufficient(true)
|
||||
if (!price?.value || !account || !balance?.ocean || !dtBalance) return
|
||||
|
||||
setIsBalanceSufficient(
|
||||
@ -56,14 +84,16 @@ export default function AssetActions(): ReactElement {
|
||||
<Compute
|
||||
dtBalance={dtBalance}
|
||||
isBalanceSufficient={isBalanceSufficient}
|
||||
file={metadata?.main.files[0]}
|
||||
file={fileMetadata}
|
||||
fileIsLoading={fileIsLoading}
|
||||
/>
|
||||
) : (
|
||||
<Consume
|
||||
ddo={ddo}
|
||||
dtBalance={dtBalance}
|
||||
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
|
||||
const hasPool = ddo?.price?.type === 'pool'
|
||||
|
||||
hasPool &&
|
||||
price?.type === 'pool' &&
|
||||
tabs.push(
|
||||
{
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
.bookmark {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: calc(var(--spacer) / 4);
|
||||
right: calc(var(--spacer) / 8);
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
|
@ -6,12 +6,10 @@ import { useAsset } from '../../../providers/Asset'
|
||||
|
||||
export default function MetaFull(): ReactElement {
|
||||
const { ddo, metadata, isInPurgatory, type } = useAsset()
|
||||
const { algorithm } = ddo.findServiceByType('metadata').attributes.main
|
||||
|
||||
function DockerImage() {
|
||||
const algorithmContainer = ddo.findServiceByType('metadata').attributes.main
|
||||
.algorithm.container
|
||||
const { image } = algorithmContainer
|
||||
const { tag } = algorithmContainer
|
||||
const { image, tag } = algorithm.container
|
||||
return <span>{`${image}:${tag}`}</span>
|
||||
}
|
||||
|
||||
@ -25,7 +23,7 @@ export default function MetaFull(): ReactElement {
|
||||
content={<Publisher account={ddo?.publicKey[0].owner} />}
|
||||
/>
|
||||
|
||||
{type === 'algorithm' && (
|
||||
{type === 'algorithm' && algorithm && (
|
||||
<MetaItem title="Docker Image" content={<DockerImage />} />
|
||||
)}
|
||||
<MetaItem title="DID" content={<code>{ddo?.id}</code>} />
|
||||
|
@ -1,25 +1,51 @@
|
||||
.meta {
|
||||
margin-bottom: calc(var(--spacer) / 1.5);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.meta p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.typeAndDate {
|
||||
margin-bottom: calc(var(--spacer) / 12);
|
||||
display: flex;
|
||||
.asset {
|
||||
margin-left: -2rem;
|
||||
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);
|
||||
padding-right: calc(var(--spacer) / 3.5);
|
||||
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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
@ -3,47 +3,65 @@ import { useAsset } from '../../../providers/Asset'
|
||||
import { useWeb3 } from '../../../providers/Web3'
|
||||
import ExplorerLink from '../../atoms/ExplorerLink'
|
||||
import Publisher from '../../atoms/Publisher'
|
||||
import AddToken from '../../atoms/AddToken'
|
||||
import Time from '../../atoms/Time'
|
||||
import styles from './MetaMain.module.css'
|
||||
import AssetType from '../../atoms/AssetType'
|
||||
import styles from './MetaMain.module.css'
|
||||
|
||||
export default function MetaMain(): ReactElement {
|
||||
const { ddo, owner, type } = useAsset()
|
||||
const { networkId } = useWeb3()
|
||||
const { networkId, web3ProviderInfo } = useWeb3()
|
||||
const isCompute = Boolean(ddo?.findServiceByType('compute'))
|
||||
const accessType = isCompute ? 'compute' : 'access'
|
||||
|
||||
return (
|
||||
<aside className={styles.meta}>
|
||||
<div className={styles.typeAndDate}>
|
||||
<header className={styles.asset}>
|
||||
<AssetType
|
||||
type={type}
|
||||
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
|
||||
className={styles.datatoken}
|
||||
networkId={networkId}
|
||||
path={
|
||||
networkId === 137
|
||||
networkId === 137 || networkId === 1287
|
||||
? `tokens/${ddo?.dataToken}`
|
||||
: `token/${ddo?.dataToken}`
|
||||
}
|
||||
>
|
||||
{`${ddo?.dataTokenInfo.name} — ${ddo?.dataTokenInfo.symbol}`}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
.free {
|
||||
composes: content from './index.module.css';
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -10,10 +10,12 @@ import usePricing from '../../../../../hooks/usePricing'
|
||||
|
||||
export default function Price({
|
||||
ddo,
|
||||
firstPrice
|
||||
firstPrice,
|
||||
free
|
||||
}: {
|
||||
ddo: DDO
|
||||
firstPrice?: string
|
||||
free?: boolean
|
||||
}): ReactElement {
|
||||
const [field, meta] = useField('price')
|
||||
const { getDTName, getDTSymbol } = usePricing()
|
||||
@ -38,17 +40,27 @@ export default function Price({
|
||||
<div className={styles.price}>
|
||||
<div className={styles.grid}>
|
||||
<div className={styles.form}>
|
||||
<Input
|
||||
value={field.value}
|
||||
name="price"
|
||||
type="number"
|
||||
prefix="OCEAN"
|
||||
min="1"
|
||||
{...field}
|
||||
additionalComponent={
|
||||
<Conversion price={field.value} className={styles.conversion} />
|
||||
}
|
||||
/>
|
||||
{free ? (
|
||||
<Input
|
||||
value="0"
|
||||
name="price"
|
||||
type="number"
|
||||
prefix="OCEAN"
|
||||
readOnly
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={field.value}
|
||||
name="price"
|
||||
type="number"
|
||||
prefix="OCEAN"
|
||||
min="1"
|
||||
{...field}
|
||||
additionalComponent={
|
||||
<Conversion price={field.value} className={styles.conversion} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Error meta={meta} />
|
||||
</div>
|
||||
<div className={styles.datatoken}>
|
||||
|
@ -45,3 +45,8 @@
|
||||
padding-left: var(--spacer);
|
||||
padding-right: var(--spacer);
|
||||
}
|
||||
|
||||
.free {
|
||||
text-align: center;
|
||||
margin-bottom: calc(var(--spacer) / 1.5);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import styles from './index.module.css'
|
||||
import Tabs from '../../../../atoms/Tabs'
|
||||
import Fixed from './Fixed'
|
||||
import Dynamic from './Dynamic'
|
||||
import Free from './Free'
|
||||
import { useFormikContext } from 'formik'
|
||||
import { useUserPreferences } from '../../../../../providers/UserPreferences'
|
||||
import { PriceOptionsMarket } from '../../../../../@types/MetaData'
|
||||
@ -25,19 +26,15 @@ export default function FormPricing({
|
||||
|
||||
// Connect with form
|
||||
const { values, setFieldValue, submitForm } = useFormikContext()
|
||||
const {
|
||||
price,
|
||||
oceanAmount,
|
||||
weightOnOcean,
|
||||
weightOnDataToken,
|
||||
type
|
||||
} = values as PriceOptionsMarket
|
||||
const { price, oceanAmount, weightOnOcean, weightOnDataToken, type } =
|
||||
values as PriceOptionsMarket
|
||||
|
||||
// Switch type value upon tab change
|
||||
function handleTabChange(tabName: string) {
|
||||
const type = tabName.toLowerCase()
|
||||
setFieldValue('type', type)
|
||||
type === 'fixed' && setFieldValue('dtAmount', 1000)
|
||||
type === 'free' && price < 1 && setFieldValue('price', 1)
|
||||
}
|
||||
|
||||
// Always update everything when price value changes
|
||||
@ -62,6 +59,12 @@ export default function FormPricing({
|
||||
title: content.dynamic.title,
|
||||
content: <Dynamic content={content.dynamic} ddo={ddo} />
|
||||
}
|
||||
: undefined,
|
||||
appConfig.allowFreePricing === 'true'
|
||||
? {
|
||||
title: content.free.title,
|
||||
content: <Free content={content.free} ddo={ddo} />
|
||||
}
|
||||
: undefined
|
||||
].filter((tab) => tab !== undefined)
|
||||
|
||||
|
@ -40,6 +40,10 @@ const query = graphql`
|
||||
marketplaceFee
|
||||
}
|
||||
}
|
||||
free {
|
||||
title
|
||||
info
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -57,12 +61,8 @@ export default function Pricing({ ddo }: { ddo: DDO }): ReactElement {
|
||||
const [showPricing, setShowPricing] = useState(false)
|
||||
const [success, setSuccess] = useState<string>()
|
||||
|
||||
const {
|
||||
createPricing,
|
||||
pricingIsLoading,
|
||||
pricingError,
|
||||
pricingStepText
|
||||
} = usePricing()
|
||||
const { createPricing, pricingIsLoading, pricingError, pricingStepText } =
|
||||
usePricing()
|
||||
|
||||
const hasFeedback = pricingIsLoading || typeof success !== 'undefined'
|
||||
|
||||
|
@ -17,6 +17,8 @@ import MetaMain from './MetaMain'
|
||||
import EditHistory from './EditHistory'
|
||||
import { useWeb3 } from '../../../providers/Web3'
|
||||
import styles from './index.module.css'
|
||||
import EditAdvancedSettings from '../AssetActions/Edit/EditAdvancedSettings'
|
||||
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
|
||||
|
||||
export interface AssetContentProps {
|
||||
path?: string
|
||||
@ -48,14 +50,19 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
|
||||
const [showPricing, setShowPricing] = useState(false)
|
||||
const [showEdit, setShowEdit] = useState<boolean>()
|
||||
const [showEditCompute, setShowEditCompute] = useState<boolean>()
|
||||
const { ddo, price, metadata } = useAsset()
|
||||
|
||||
const isOwner = accountId === owner
|
||||
const [showEditAdvancedSettings, setShowEditAdvancedSettings] =
|
||||
useState<boolean>()
|
||||
const [isOwner, setIsOwner] = useState(false)
|
||||
const { ddo, price, metadata, type } = useAsset()
|
||||
const { appConfig } = useSiteMetadata()
|
||||
|
||||
useEffect(() => {
|
||||
if (!price) return
|
||||
if (!accountId || !owner) return
|
||||
|
||||
const isOwner = accountId.toLowerCase() === owner.toLowerCase()
|
||||
setIsOwner(isOwner)
|
||||
setShowPricing(isOwner && price.type === '')
|
||||
}, [isOwner, price])
|
||||
}, [accountId, price, owner])
|
||||
|
||||
function handleEditButton() {
|
||||
// move user's focus to top of screen
|
||||
@ -68,10 +75,17 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
|
||||
setShowEditCompute(true)
|
||||
}
|
||||
|
||||
function handleEditAdvancedSettingsButton() {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||
setShowEditAdvancedSettings(true)
|
||||
}
|
||||
|
||||
return showEdit ? (
|
||||
<Edit setShowEdit={setShowEdit} />
|
||||
) : showEditCompute ? (
|
||||
<EditComputeDataset setShowEdit={setShowEditCompute} />
|
||||
) : showEditAdvancedSettings ? (
|
||||
<EditAdvancedSettings setShowEdit={setShowEditAdvancedSettings} />
|
||||
) : (
|
||||
<article className={styles.grid}>
|
||||
<div>
|
||||
@ -101,7 +115,19 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
|
||||
<Button style="text" size="small" onClick={handleEditButton}>
|
||||
Edit Metadata
|
||||
</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>
|
||||
<Button
|
||||
|
@ -16,3 +16,9 @@
|
||||
font-size: var(--font-size-small);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.loaderWrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
@ -1,17 +1,28 @@
|
||||
import AssetTeaser from '../molecules/AssetTeaser'
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Pagination from '../molecules/Pagination'
|
||||
import styles from './AssetList.module.css'
|
||||
import { DDO } from '@oceanprotocol/lib'
|
||||
import classNames from 'classnames/bind'
|
||||
import { getAssetsBestPrices, AssetListPrices } from '../../utils/subgraph'
|
||||
import Loader from '../atoms/Loader'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
function LoaderArea() {
|
||||
return (
|
||||
<div className={styles.loaderWrap}>
|
||||
<Loader />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
declare type AssetListProps = {
|
||||
assets: DDO[]
|
||||
showPagination: boolean
|
||||
page?: number
|
||||
totalPages?: number
|
||||
isLoading?: boolean
|
||||
onPageChange?: React.Dispatch<React.SetStateAction<number>>
|
||||
className?: string
|
||||
}
|
||||
@ -21,9 +32,22 @@ const AssetList: React.FC<AssetListProps> = ({
|
||||
showPagination,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
onPageChange,
|
||||
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
|
||||
function handlePageChange(selected: number) {
|
||||
onPageChange(selected + 1)
|
||||
@ -34,11 +58,19 @@ const AssetList: React.FC<AssetListProps> = ({
|
||||
[className]: className
|
||||
})
|
||||
|
||||
return (
|
||||
return assetsWithPrices &&
|
||||
!loading &&
|
||||
(isLoading === undefined || isLoading === false) ? (
|
||||
<>
|
||||
<div className={styleClasses}>
|
||||
{assets.length > 0 ? (
|
||||
assets.map((ddo) => <AssetTeaser ddo={ddo} key={ddo.id} />)
|
||||
{assetsWithPrices.length > 0 ? (
|
||||
assetsWithPrices.map((assetWithPrice) => (
|
||||
<AssetTeaser
|
||||
ddo={assetWithPrice.ddo}
|
||||
price={assetWithPrice.price}
|
||||
key={assetWithPrice.ddo.id}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.empty}>No results found.</div>
|
||||
)}
|
||||
@ -52,6 +84,8 @@ const AssetList: React.FC<AssetListProps> = ({
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<LoaderArea />
|
||||
)
|
||||
}
|
||||
|
||||
|
62
src/components/organisms/Permission.tsx
Normal file
62
src/components/organisms/Permission.tsx
Normal 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 />
|
||||
</>
|
||||
)
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user