mirror of
https://github.com/oceanprotocol/market.git
synced 2024-06-29 00:57:50 +02:00
Merge pull request #376 from oceanprotocol/feature/compute
Compute-to-data
This commit is contained in:
commit
020d4fa05a
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -12,5 +12,5 @@ public/storybook
|
|||
.artifacts
|
||||
.vercel
|
||||
repo-metadata.json
|
||||
networks-metadata.json
|
||||
content/networks-metadata.json
|
||||
src/@types/apollo
|
27
content/pages/editComputeDataset.json
Normal file
27
content/pages/editComputeDataset.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"description": "Only selected algorithms are allowed to run on this data set. Updating these settings will create an on-chain transaction you have to approve in your wallet.",
|
||||
"form": {
|
||||
"title": "Set allowed algorithms",
|
||||
"success": "🎉 Successfully updated. 🎉",
|
||||
"successAction": "Close",
|
||||
"error": "Updating DDO failed.",
|
||||
"data": [
|
||||
{
|
||||
"name": "publisherTrustedAlgorithms",
|
||||
"label": "Selected Algorithms",
|
||||
"help": "Choose one or multiple algorithms you trust to allow them to run on this data set.",
|
||||
"type": "assetSelectionMultiple",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
{
|
||||
"title": "Publish",
|
||||
"description": "Highlight the important features of your data set to make it more discoverable and catch the interest of data consumers.",
|
||||
"warning": "Given the beta status, publishing on Ropsten or Rinkeby first is strongly recommended. Please familiarize yourself with [the market](https://oceanprotocol.com/technology/marketplaces), [the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13), and the [Terms of Use](/terms).",
|
||||
"form": {
|
||||
"title": "Publish",
|
||||
"data": [
|
||||
{
|
||||
"name": "name",
|
||||
"label": "Title",
|
||||
"placeholder": "e.g. Shapes of Desert Plants",
|
||||
"help": "Enter a concise title.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"label": "Description",
|
||||
"help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics).",
|
||||
"type": "textarea",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"type": "files",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "links",
|
||||
"label": "Sample file",
|
||||
"placeholder": "e.g. https://file.com/samplefile.json",
|
||||
"help": "Please enter the URL to a sample of your data set file and click \"ADD FILE\" to validate the data. This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing.",
|
||||
"type": "files"
|
||||
},
|
||||
{
|
||||
"name": "access",
|
||||
"label": "Access Type",
|
||||
"help": "Choose how you want your files to be accessible for the specified price.",
|
||||
"type": "select",
|
||||
"options": ["Download"],
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"label": "Timeout",
|
||||
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
|
||||
"type": "select",
|
||||
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
|
||||
"sortOptions": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "dataTokenOptions",
|
||||
"label": "Datatoken Name & Symbol",
|
||||
"type": "datatoken",
|
||||
"help": "The datatoken for this data set will be created with this name & symbol.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"label": "Author",
|
||||
"placeholder": "e.g. Jelly McJellyfish",
|
||||
"help": "Give proper attribution for your data set.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"label": "Tags",
|
||||
"placeholder": "e.g. logistics, ai",
|
||||
"help": "Separate tags with comma."
|
||||
},
|
||||
{
|
||||
"name": "termsAndConditions",
|
||||
"label": "Terms & Conditions",
|
||||
"type": "terms",
|
||||
"options": ["I agree to these Terms and Conditions"],
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"success": "Asset Created!"
|
||||
}
|
||||
}
|
103
content/pages/publish/form-algorithm.json
Normal file
103
content/pages/publish/form-algorithm.json
Normal file
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"title": "Publish an Algorithm",
|
||||
"data": [
|
||||
{
|
||||
"name": "name",
|
||||
"label": "Title",
|
||||
"placeholder": "e.g. Shapes of Desert Plants",
|
||||
"help": "Enter a concise title.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"label": "Description",
|
||||
"help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics).",
|
||||
"type": "textarea",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"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. 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
|
||||
},
|
||||
{
|
||||
"name": "dockerImage",
|
||||
"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"],
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"label": "Image URL",
|
||||
"placeholder": "e.g. oceanprotocol/algo_dockers or https://example.com/image_path",
|
||||
"help": "Provide the name of a public Docker image or the full url if you have it hosted in a 3rd party repo",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "containerTag",
|
||||
"label": "Docker Image Tag",
|
||||
"placeholder": "e.g. latest",
|
||||
"help": "Provide the tag for your Docker image.",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"label": "Timeout",
|
||||
"help": "Define how long buyers should be able to download the algorithm again after the initial purchase.",
|
||||
"placeholder": "Forever",
|
||||
"type": "select",
|
||||
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
|
||||
"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",
|
||||
"placeholder": "e.g. python $ALGO",
|
||||
"help": "Provide the entrypoint for your algorithm.",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "algorithmPrivacy",
|
||||
"label": "Algorithm Privacy",
|
||||
"type": "checkbox",
|
||||
"options": ["Keep my algorithm private"],
|
||||
"help": "By default, your algorithm can be downloaded for a fixed or dynamic price in addition to running in compute jobs. Enabling this option will prevent downloading, so your algorithm can only be run as part of a compute job on a data set.",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"label": "Author",
|
||||
"placeholder": "e.g. Jelly McJellyfish",
|
||||
"help": "Give proper attribution for your algorithm.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"label": "Tags",
|
||||
"placeholder": "e.g. logistics, ai",
|
||||
"help": "Separate tags with comma."
|
||||
},
|
||||
{
|
||||
"name": "termsAndConditions",
|
||||
"label": "Terms & Conditions",
|
||||
"type": "terms",
|
||||
"options": ["I agree to these Terms and Conditions"],
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"success": "Algorithm Published!"
|
||||
}
|
79
content/pages/publish/form-dataset.json
Normal file
79
content/pages/publish/form-dataset.json
Normal file
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"title": "Publish a Data Set",
|
||||
"data": [
|
||||
{
|
||||
"name": "name",
|
||||
"label": "Title",
|
||||
"placeholder": "e.g. Shapes of Desert Plants",
|
||||
"help": "Enter a concise title.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"label": "Description",
|
||||
"help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics).",
|
||||
"type": "textarea",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"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. 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
|
||||
},
|
||||
{
|
||||
"name": "links",
|
||||
"label": "Sample file",
|
||||
"placeholder": "e.g. https://file.com/samplefile.json",
|
||||
"help": "Please enter the URL to a sample of your data set file and click \"ADD FILE\" to validate the data. This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing.",
|
||||
"type": "files"
|
||||
},
|
||||
{
|
||||
"name": "access",
|
||||
"label": "Access Type",
|
||||
"help": "Choose how you want your files to be accessible for the specified price.",
|
||||
"type": "select",
|
||||
"options": ["Download", "Compute"],
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"label": "Timeout",
|
||||
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
|
||||
"type": "select",
|
||||
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
|
||||
"sortOptions": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "dataTokenOptions",
|
||||
"label": "Datatoken Name & Symbol",
|
||||
"type": "datatoken",
|
||||
"help": "The datatoken for this data set will be created with this name & symbol.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"label": "Author",
|
||||
"placeholder": "e.g. Jelly McJellyfish",
|
||||
"help": "Give proper attribution for your data set.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"label": "Tags",
|
||||
"placeholder": "e.g. logistics, ai",
|
||||
"help": "Separate tags with comma."
|
||||
},
|
||||
{
|
||||
"name": "termsAndConditions",
|
||||
"label": "Terms & Conditions",
|
||||
"type": "terms",
|
||||
"options": ["I agree to these Terms and Conditions"],
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"success": "Asset Created!"
|
||||
}
|
5
content/pages/publish/index.json
Normal file
5
content/pages/publish/index.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"title": "Publish",
|
||||
"description": "Highlight the important features of your data set or algorithm to make it more discoverable and catch the interest of data consumers.",
|
||||
"warning": "Given the beta status, publishing on Ropsten or Rinkeby first is strongly recommended. Please familiarize yourself with [the market](https://oceanprotocol.com/technology/marketplaces), [the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13), and the [Terms of Use](/terms)."
|
||||
}
|
16
content/pages/startComputeDataset.json
Normal file
16
content/pages/startComputeDataset.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"form": {
|
||||
"success": "🎉 Your Compute job started. 🎉",
|
||||
"error": "Compute job could not be started.",
|
||||
"data": [
|
||||
{
|
||||
"name": "algorithm",
|
||||
"label": "Select an algorithm to start a compute job",
|
||||
"type": "assetSelection",
|
||||
"value": false,
|
||||
"options": [],
|
||||
"sortOptions": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
4109
package-lock.json
generated
4109
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -27,7 +27,7 @@
|
|||
"@coingecko/cryptoformat": "^0.4.2",
|
||||
"@loadable/component": "^5.14.1",
|
||||
"@oceanprotocol/art": "^3.0.0",
|
||||
"@oceanprotocol/lib": "^0.13.0",
|
||||
"@oceanprotocol/lib": "^0.14.4",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@portis/web3": "^3.0.3",
|
||||
"@sindresorhus/slugify": "^1.0.0",
|
||||
|
@ -62,6 +62,7 @@
|
|||
"gatsby-transformer-remark": "^2.14.0",
|
||||
"gatsby-transformer-sharp": "^2.10.1",
|
||||
"intersection-observer": "^0.12.0",
|
||||
"is-url-superb": "^5.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.omit": "^4.5.0",
|
||||
|
@ -93,8 +94,8 @@
|
|||
"@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",
|
||||
"@storybook/addon-storyshots": "^6.2.8",
|
||||
"@storybook/react": "^6.2.8",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^11.2.3",
|
||||
|
|
4
src/@types/ComputeDataset.ts
Normal file
4
src/@types/ComputeDataset.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface AlgorithmOption {
|
||||
did: string
|
||||
name: string
|
||||
}
|
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
|
||||
}
|
||||
|
|
6
src/@types/Form.d.ts
vendored
6
src/@types/Form.d.ts
vendored
|
@ -1,10 +1,14 @@
|
|||
import { AssetSelectionAsset } from '../../molecules/FormFields/AssetSelection'
|
||||
|
||||
export interface FormFieldProps {
|
||||
label: string
|
||||
name: string
|
||||
type?: string
|
||||
options?: string[]
|
||||
options?: string[] | AssetSelectionAsset[]
|
||||
sortOptions?: boolean
|
||||
required?: boolean
|
||||
multiple?: boolean
|
||||
disabled?: boolean
|
||||
help?: string
|
||||
placeholder?: string
|
||||
pattern?: string
|
||||
|
|
38
src/@types/MetaData.d.ts
vendored
38
src/@types/MetaData.d.ts
vendored
|
@ -25,15 +25,7 @@ export interface PriceOptionsMarket extends PriceOptions {
|
|||
swapFee: number
|
||||
}
|
||||
|
||||
export interface MetadataEditForm {
|
||||
name: string
|
||||
description: string
|
||||
timeout: string
|
||||
price?: number
|
||||
links?: string | EditableMetadataLinks[]
|
||||
}
|
||||
|
||||
export interface MetadataPublishForm {
|
||||
export interface MetadataPublishFormDataset {
|
||||
// ---- required fields ----
|
||||
name: string
|
||||
description: string
|
||||
|
@ -45,7 +37,33 @@ export interface MetadataPublishForm {
|
|||
termsAndConditions: boolean
|
||||
// ---- optional fields ----
|
||||
tags?: string
|
||||
links?: string | File[]
|
||||
links?: string | EditableMetadataLinks[]
|
||||
}
|
||||
|
||||
export interface MetadataPublishFormAlgorithm {
|
||||
// ---- required fields ----
|
||||
name: string
|
||||
description: string
|
||||
files: string | File[]
|
||||
author: string
|
||||
dockerImage: string
|
||||
algorithmPrivacy: boolean
|
||||
timeout: string
|
||||
dataTokenOptions: DataTokenOptions
|
||||
termsAndConditions: boolean
|
||||
// ---- optional fields ----
|
||||
image: string
|
||||
containerTag: string
|
||||
entrypoint: string
|
||||
tags?: string
|
||||
}
|
||||
|
||||
export interface MetadataEditForm {
|
||||
name: string
|
||||
description: string
|
||||
timeout: string
|
||||
price?: number
|
||||
links?: string | EditableMetadataLinks[]
|
||||
}
|
||||
|
||||
export interface ServiceMetadataMarket extends ServiceMetadata {
|
||||
|
|
16
src/components/atoms/AssetType.module.css
Normal file
16
src/components/atoms/AssetType.module.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
.icon {
|
||||
fill: currentColor;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: baseline;
|
||||
margin-bottom: -0.1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.typeLabel {
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding-right: calc(var(--spacer) / 3.5);
|
||||
margin-right: calc(var(--spacer) / 4);
|
||||
}
|
36
src/components/atoms/AssetType.tsx
Normal file
36
src/components/atoms/AssetType.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
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)
|
||||
|
||||
export default function AssetType({
|
||||
type,
|
||||
accessType,
|
||||
className
|
||||
}: {
|
||||
type: string
|
||||
accessType: string
|
||||
className?: string
|
||||
}): ReactElement {
|
||||
const styleClasses = cx({
|
||||
[className]: className
|
||||
})
|
||||
return (
|
||||
<div className={styleClasses}>
|
||||
<div className={styles.typeLabel}>
|
||||
{type === 'dataset' ? 'data set' : 'algorithm'}
|
||||
</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} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
.box {
|
||||
display: block;
|
||||
background: var(--background-body);
|
||||
background: var(--background-content);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 6px 17px 0 var(--box-shadow-color);
|
||||
|
|
|
@ -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);
|
||||
|
|
14
src/components/atoms/ButtonBuy.module.css
Normal file
14
src/components/atoms/ButtonBuy.module.css
Normal file
|
@ -0,0 +1,14 @@
|
|||
.actions {
|
||||
width: 100%;
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.help {
|
||||
font-size: var(--font-size-mini);
|
||||
color: var(--color-secondary);
|
||||
margin-top: calc(var(--spacer) / 3);
|
||||
}
|
||||
|
||||
.help:not(:empty) {
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
141
src/components/atoms/ButtonBuy.tsx
Normal file
141
src/components/atoms/ButtonBuy.tsx
Normal file
|
@ -0,0 +1,141 @@
|
|||
import React, { FormEvent, ReactElement } from 'react'
|
||||
import Button from './Button'
|
||||
import styles from './ButtonBuy.module.css'
|
||||
import Loader from './Loader'
|
||||
|
||||
interface ButtonBuyProps {
|
||||
action: 'download' | 'compute'
|
||||
disabled: boolean
|
||||
hasPreviousOrder: boolean
|
||||
hasDatatoken: boolean
|
||||
dtSymbol: string
|
||||
dtBalance: string
|
||||
assetType: string
|
||||
assetTimeout: string
|
||||
hasPreviousOrderSelectedComputeAsset?: boolean
|
||||
hasDatatokenSelectedComputeAsset?: boolean
|
||||
dtSymbolSelectedComputeAsset?: string
|
||||
dtBalanceSelectedComputeAsset?: string
|
||||
selectedComputeAssetType?: string
|
||||
isLoading: boolean
|
||||
onClick?: (e: FormEvent<HTMLButtonElement>) => void
|
||||
stepText?: string
|
||||
type?: 'submit'
|
||||
}
|
||||
|
||||
function getConsumeHelpText(
|
||||
dtBalance: string,
|
||||
dtSymbol: string,
|
||||
hasDatatoken: boolean,
|
||||
hasPreviousOrder: boolean,
|
||||
assetType: string
|
||||
) {
|
||||
const text = hasPreviousOrder
|
||||
? `You bought this ${assetType} already allowing you to use it without paying again.`
|
||||
: hasDatatoken
|
||||
? `You own ${dtBalance} ${dtSymbol} allowing you to use this data set by spending 1 ${dtSymbol}, but without paying OCEAN again.`
|
||||
: `For using this ${assetType}, you will buy 1 ${dtSymbol} and immediately spend it back to the publisher and pool.`
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
function getComputeAssetHelpText(
|
||||
hasPreviousOrder: boolean,
|
||||
hasDatatoken: boolean,
|
||||
dtSymbol: string,
|
||||
dtBalance: string,
|
||||
assetType: string,
|
||||
hasPreviousOrderSelectedComputeAsset?: boolean,
|
||||
hasDatatokenSelectedComputeAsset?: boolean,
|
||||
dtSymbolSelectedComputeAsset?: string,
|
||||
dtBalanceSelectedComputeAsset?: string,
|
||||
selectedComputeAssetType?: string
|
||||
) {
|
||||
const computeAssetHelpText = getConsumeHelpText(
|
||||
dtBalance,
|
||||
dtSymbol,
|
||||
hasDatatoken,
|
||||
hasPreviousOrder,
|
||||
assetType
|
||||
)
|
||||
const text =
|
||||
!dtSymbolSelectedComputeAsset && !dtBalanceSelectedComputeAsset
|
||||
? ''
|
||||
: hasPreviousOrderSelectedComputeAsset
|
||||
? `You already bought the selected ${selectedComputeAssetType}, allowing you to use it without paying again.`
|
||||
: hasDatatokenSelectedComputeAsset
|
||||
? `You own ${dtBalanceSelectedComputeAsset} ${dtSymbolSelectedComputeAsset} allowing you to use the selected ${selectedComputeAssetType} by spending 1 ${dtSymbolSelectedComputeAsset}, but without paying OCEAN again.`
|
||||
: `Additionally, you will buy 1 ${dtSymbolSelectedComputeAsset} for the ${selectedComputeAssetType} and spend it back to its publisher and pool.`
|
||||
|
||||
return `${computeAssetHelpText} ${text}`
|
||||
}
|
||||
|
||||
export default function ButtonBuy({
|
||||
action,
|
||||
disabled,
|
||||
hasPreviousOrder,
|
||||
hasDatatoken,
|
||||
dtSymbol,
|
||||
dtBalance,
|
||||
assetType,
|
||||
assetTimeout,
|
||||
hasPreviousOrderSelectedComputeAsset,
|
||||
hasDatatokenSelectedComputeAsset,
|
||||
dtSymbolSelectedComputeAsset,
|
||||
dtBalanceSelectedComputeAsset,
|
||||
selectedComputeAssetType,
|
||||
onClick,
|
||||
stepText,
|
||||
isLoading,
|
||||
type
|
||||
}: ButtonBuyProps): ReactElement {
|
||||
const buttonText =
|
||||
action === 'download'
|
||||
? hasPreviousOrder
|
||||
? 'Download'
|
||||
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
|
||||
: hasPreviousOrder && hasPreviousOrderSelectedComputeAsset
|
||||
? 'Start Compute Job'
|
||||
: `Buy Compute Job`
|
||||
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
{isLoading ? (
|
||||
<Loader message={stepText} />
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
style="primary"
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
<div className={styles.help}>
|
||||
{action === 'download'
|
||||
? getConsumeHelpText(
|
||||
dtBalance,
|
||||
dtSymbol,
|
||||
hasDatatoken,
|
||||
hasPreviousOrder,
|
||||
assetType
|
||||
)
|
||||
: getComputeAssetHelpText(
|
||||
hasPreviousOrder,
|
||||
hasDatatoken,
|
||||
dtSymbol,
|
||||
dtBalance,
|
||||
assetType,
|
||||
hasPreviousOrderSelectedComputeAsset,
|
||||
hasDatatokenSelectedComputeAsset,
|
||||
dtSymbolSelectedComputeAsset,
|
||||
dtBalanceSelectedComputeAsset,
|
||||
selectedComputeAssetType
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
.file.small {
|
||||
font-size: var(--font-size-mini);
|
||||
height: 5.75rem;
|
||||
height: 5.5rem;
|
||||
width: 4.5rem;
|
||||
padding: calc(var(--spacer) / 2) calc(var(--spacer) / 4);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
border: 1px solid var(--border-color);
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
background: var(--background-body);
|
||||
background: var(--background-content);
|
||||
padding: calc(var(--spacer) / 3);
|
||||
margin: 0;
|
||||
border-radius: var(--border-radius);
|
||||
|
@ -100,6 +100,12 @@
|
|||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.algorithmLabel {
|
||||
display: grid;
|
||||
gap: var(--spacer);
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
|
||||
.radio,
|
||||
.checkbox {
|
||||
composes: input;
|
||||
|
@ -140,16 +146,17 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.radio {
|
||||
.radio,
|
||||
.radio::after {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.radio::after {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
background: var(--brand-white);
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
.checkbox::after {
|
||||
|
|
|
@ -6,18 +6,22 @@ import FilesInput from '../../molecules/FormFields/FilesInput'
|
|||
import Terms from '../../molecules/FormFields/Terms'
|
||||
import Datatoken from '../../molecules/FormFields/Datatoken'
|
||||
import classNames from 'classnames/bind'
|
||||
import AssetSelection, {
|
||||
AssetSelectionAsset
|
||||
} from '../../molecules/FormFields/AssetSelection'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
const DefaultInput = ({
|
||||
size,
|
||||
className,
|
||||
prefix,
|
||||
postfix,
|
||||
additionalComponent,
|
||||
...props
|
||||
}: InputProps) => (
|
||||
<input
|
||||
className={cx({ input: true, [size]: size })}
|
||||
className={cx({ input: true, [size]: size, [className]: className })}
|
||||
id={props.name}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -33,13 +37,14 @@ export default function InputElement({
|
|||
size,
|
||||
field,
|
||||
label,
|
||||
multiple,
|
||||
disabled,
|
||||
help,
|
||||
form,
|
||||
additionalComponent,
|
||||
...props
|
||||
}: InputProps): ReactElement {
|
||||
const styleClasses = cx({ select: true, [size]: size })
|
||||
|
||||
switch (type) {
|
||||
case 'select': {
|
||||
const sortedOptions =
|
||||
|
@ -47,7 +52,12 @@ export default function InputElement({
|
|||
? options
|
||||
: options.sort((a: string, b: string) => a.localeCompare(b))
|
||||
return (
|
||||
<select id={name} className={styleClasses} {...props}>
|
||||
<select
|
||||
id={name}
|
||||
className={styleClasses}
|
||||
{...props}
|
||||
multiple={multiple}
|
||||
>
|
||||
{field !== undefined && field.value === '' && (
|
||||
<option value="">---</option>
|
||||
)}
|
||||
|
@ -91,6 +101,24 @@ export default function InputElement({
|
|||
))}
|
||||
</div>
|
||||
)
|
||||
case 'assetSelection':
|
||||
return (
|
||||
<AssetSelection
|
||||
assets={(options as unknown) as AssetSelectionAsset[]}
|
||||
{...field}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
case 'assetSelectionMultiple':
|
||||
return (
|
||||
<AssetSelection
|
||||
assets={(options as unknown) as AssetSelectionAsset[]}
|
||||
multiple
|
||||
disabled={disabled}
|
||||
{...field}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
case 'files':
|
||||
return <FilesInput name={name} {...field} {...props} />
|
||||
case 'datatoken':
|
||||
|
@ -107,6 +135,7 @@ export default function InputElement({
|
|||
name={name}
|
||||
type={type || 'text'}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
{postfix && (
|
||||
|
@ -118,6 +147,7 @@ export default function InputElement({
|
|||
name={name}
|
||||
type={type || 'text'}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -41,6 +41,7 @@ export interface InputProps {
|
|||
step?: string
|
||||
defaultChecked?: boolean
|
||||
size?: 'mini' | 'small' | 'large' | 'default'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function Input(props: Partial<InputProps>): ReactElement {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.action {
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
color: var(--color-secondary);
|
||||
background-color: var(--background-body);
|
||||
border: 1px solid var(--border-color);
|
||||
margin-right: -1px;
|
||||
min-width: 100px;
|
||||
|
@ -29,7 +30,7 @@
|
|||
}
|
||||
|
||||
.tab[aria-selected='true'] {
|
||||
background: var(--font-color-heading);
|
||||
background-color: var(--font-color-heading);
|
||||
color: var(--background-body);
|
||||
border-color: var(--font-color-heading);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
.content {
|
||||
composes: box from './Box.module.css';
|
||||
padding: calc(var(--spacer) / 4);
|
||||
width: calc(100% - var(--spacer) / 3);
|
||||
max-width: 25rem;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.algorithm .link {
|
||||
background-color: var(--background-body);
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
overflow-wrap: break-word;
|
||||
|
@ -49,18 +53,6 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.accessLabel {
|
||||
font-size: var(--font-size-mini);
|
||||
width: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: var(--brand-black);
|
||||
background: var(--brand-grey-lighter);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.symbol {
|
||||
display: block;
|
||||
}
|
||||
|
@ -69,3 +61,11 @@
|
|||
font-size: var(--font-size-mini);
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.typeDetails {
|
||||
position: absolute;
|
||||
top: calc(var(--spacer) / 3);
|
||||
right: calc(var(--spacer) / 3);
|
||||
width: auto;
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ import Price from '../atoms/Price'
|
|||
import styles from './AssetTeaser.module.css'
|
||||
import { DDO } from '@oceanprotocol/lib'
|
||||
import removeMarkdown from 'remove-markdown'
|
||||
import Tooltip from '../atoms/Tooltip'
|
||||
import Publisher from '../atoms/Publisher'
|
||||
import Time from '../atoms/Time'
|
||||
import AssetType from '../atoms/AssetType'
|
||||
|
||||
declare type AssetTeaserProps = {
|
||||
ddo: DDO
|
||||
|
@ -15,28 +15,28 @@ declare type AssetTeaserProps = {
|
|||
|
||||
const AssetTeaser: React.FC<AssetTeaserProps> = ({ ddo }: AssetTeaserProps) => {
|
||||
const { attributes } = ddo.findServiceByType('metadata')
|
||||
const { name } = attributes.main
|
||||
const { name, type } = attributes.main
|
||||
const { dataTokenInfo } = ddo
|
||||
const isCompute = Boolean(ddo.findServiceByType('compute'))
|
||||
const isCompute = Boolean(ddo?.findServiceByType('compute'))
|
||||
const accessType = isCompute ? 'compute' : 'access'
|
||||
const { owner } = ddo.publicKey[0]
|
||||
|
||||
return (
|
||||
<article className={styles.teaser}>
|
||||
<article className={`${styles.teaser} ${styles[type]}`}>
|
||||
<Link to={`/asset/${ddo.id}`} className={styles.link}>
|
||||
<header className={styles.header}>
|
||||
<Tooltip
|
||||
placement="left"
|
||||
content={dataTokenInfo?.name}
|
||||
className={styles.symbol}
|
||||
>
|
||||
{dataTokenInfo?.symbol}
|
||||
</Tooltip>
|
||||
<div className={styles.symbol}>{dataTokenInfo?.symbol}</div>
|
||||
<Dotdotdot clamp={3}>
|
||||
<h1 className={styles.title}>{name}</h1>
|
||||
</Dotdotdot>
|
||||
<Publisher account={owner} minimal className={styles.publisher} />
|
||||
</header>
|
||||
{isCompute && <div className={styles.accessLabel}>Compute</div>}
|
||||
|
||||
<AssetType
|
||||
type={type}
|
||||
accessType={accessType}
|
||||
className={styles.typeDetails}
|
||||
/>
|
||||
|
||||
<div className={styles.content}>
|
||||
<Dotdotdot tagName="p" clamp={3}>
|
||||
|
|
126
src/components/molecules/FormFields/AssetSelection.module.css
Normal file
126
src/components/molecules/FormFields/AssetSelection.module.css
Normal file
|
@ -0,0 +1,126 @@
|
|||
.selection {
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--background-highlight);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
font-size: var(--font-size-small);
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selection [class*='loaderWrap'] {
|
||||
margin: calc(var(--spacer) / 3);
|
||||
}
|
||||
|
||||
.scroll {
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: calc(var(--spacer) / 4);
|
||||
min-height: fit-content;
|
||||
max-height: 50vh;
|
||||
position: relative;
|
||||
/* smooth overflow scrolling for pre-iOS 13 */
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: calc(var(--spacer) / 10);
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
min-width: 1.2rem;
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
margin-right: calc(var(--spacer) / 3);
|
||||
}
|
||||
|
||||
.radio {
|
||||
composes: radio from '../../atoms/Input/InputElement.module.css';
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
composes: checkbox from '../../atoms/Input/InputElement.module.css';
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-small);
|
||||
margin-top: calc(var(--spacer) / 12);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-block;
|
||||
margin-left: calc(var(--spacer) / 8);
|
||||
}
|
||||
|
||||
.link svg {
|
||||
margin: 0;
|
||||
fill: var(--color-primary);
|
||||
width: 0.7em;
|
||||
height: 0.7em;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: var(--spacer);
|
||||
justify-content: space-between;
|
||||
margin-top: calc(var(--spacer) / 12);
|
||||
}
|
||||
|
||||
.price {
|
||||
white-space: pre;
|
||||
font-size: calc(var(--font-size-small) / 1.1) !important;
|
||||
padding-left: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.price [class*='symbol'] {
|
||||
font-size: calc(var(--font-size-small) / 1.2) !important;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: calc(var(--spacer) / 4) calc(var(--spacer) / 2);
|
||||
width: calc(100% - var(--spacer));
|
||||
}
|
||||
|
||||
.did {
|
||||
padding: 0;
|
||||
/* 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);
|
||||
/* makes sure DotDotDot will kick in */
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: var(--spacer) calc(var(--spacer) / 2);
|
||||
text-align: center;
|
||||
color: var(--color-secondary);
|
||||
}
|
117
src/components/molecules/FormFields/AssetSelection.tsx
Normal file
117
src/components/molecules/FormFields/AssetSelection.tsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
import React, { ChangeEvent, useState } from 'react'
|
||||
import Dotdotdot from 'react-dotdotdot'
|
||||
import slugify from 'slugify'
|
||||
import classNames from 'classnames/bind'
|
||||
import PriceUnit from '../../atoms/Price/PriceUnit'
|
||||
import { ReactComponent as External } from '../../../images/external.svg'
|
||||
import InputElement from '../../atoms/Input/InputElement'
|
||||
import Loader from '../../atoms/Loader'
|
||||
import styles from './AssetSelection.module.css'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
export interface AssetSelectionAsset {
|
||||
did: string
|
||||
name: string
|
||||
price: string
|
||||
checked: boolean
|
||||
symbol: string
|
||||
}
|
||||
|
||||
function Empty() {
|
||||
return <div className={styles.empty}>No assets found.</div>
|
||||
}
|
||||
|
||||
export default function AssetSelection({
|
||||
assets,
|
||||
multiple,
|
||||
disabled,
|
||||
...props
|
||||
}: {
|
||||
assets: AssetSelectionAsset[]
|
||||
multiple?: boolean
|
||||
disabled?: boolean
|
||||
}): JSX.Element {
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
|
||||
const styleClassesInput = cx({
|
||||
input: true,
|
||||
[styles.checkbox]: multiple,
|
||||
[styles.radio]: !multiple
|
||||
})
|
||||
|
||||
function handleSearchInput(e: ChangeEvent<HTMLInputElement>) {
|
||||
setSearchValue(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.selection} ${disabled ? styles.disabled : ''}`}>
|
||||
<InputElement
|
||||
type="search"
|
||||
name="search"
|
||||
size="small"
|
||||
placeholder="Search by title, datatoken, or DID..."
|
||||
value={searchValue}
|
||||
onChange={handleSearchInput}
|
||||
className={styles.search}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={styles.scroll}>
|
||||
{!assets ? (
|
||||
<Loader />
|
||||
) : assets && !assets.length ? (
|
||||
<Empty />
|
||||
) : (
|
||||
assets
|
||||
.filter((asset: AssetSelectionAsset) =>
|
||||
searchValue !== ''
|
||||
? asset.name
|
||||
.toLowerCase()
|
||||
.includes(searchValue.toLowerCase()) ||
|
||||
asset.did.toLowerCase().includes(searchValue.toLowerCase()) ||
|
||||
asset.symbol.toLowerCase().includes(searchValue.toLowerCase())
|
||||
: asset
|
||||
)
|
||||
.map((asset: AssetSelectionAsset) => (
|
||||
<div className={styles.row} key={asset.did}>
|
||||
<input
|
||||
id={slugify(asset.did)}
|
||||
type={multiple ? 'checkbox' : 'radio'}
|
||||
className={styleClassesInput}
|
||||
defaultChecked={asset.checked}
|
||||
{...props}
|
||||
disabled={disabled}
|
||||
value={asset.did}
|
||||
/>
|
||||
<label
|
||||
className={styles.label}
|
||||
htmlFor={slugify(asset.did)}
|
||||
title={asset.name}
|
||||
>
|
||||
<h3 className={styles.title}>
|
||||
<Dotdotdot clamp={1} tagName="span">
|
||||
{asset.name}
|
||||
</Dotdotdot>
|
||||
<a
|
||||
className={styles.link}
|
||||
href={`/asset/${asset.did}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<External />
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<Dotdotdot clamp={1} tagName="code" className={styles.did}>
|
||||
{asset.symbol} | {asset.did}
|
||||
</Dotdotdot>
|
||||
</label>
|
||||
|
||||
<PriceUnit price={asset.price} small className={styles.price} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -14,5 +14,5 @@
|
|||
}
|
||||
|
||||
.info {
|
||||
width: .85rem
|
||||
}
|
||||
width: 0.85rem;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.preview {
|
||||
font-size: var(--font-size-small);
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
margin-bottom: var(--spacer);
|
||||
}
|
||||
|
||||
|
@ -10,7 +11,14 @@
|
|||
.metaFull {
|
||||
display: grid;
|
||||
gap: var(--spacer);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
||||
}
|
||||
|
||||
.metaAlgorithm {
|
||||
display: grid;
|
||||
gap: var(--spacer);
|
||||
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
||||
margin-bottom: var(--spacer);
|
||||
}
|
||||
|
||||
.previewTitle {
|
||||
|
|
|
@ -5,7 +5,10 @@ import Tags from '../atoms/Tags'
|
|||
import MetaItem from '../organisms/AssetContent/MetaItem'
|
||||
import styles from './MetadataPreview.module.css'
|
||||
import File from '../atoms/File'
|
||||
import { MetadataPublishForm } from '../../@types/MetaData'
|
||||
import {
|
||||
MetadataPublishFormDataset,
|
||||
MetadataPublishFormAlgorithm
|
||||
} from '../../@types/MetaData'
|
||||
import Button from '../atoms/Button'
|
||||
import { transformTags } from '../../utils/metadata'
|
||||
|
||||
|
@ -42,7 +45,7 @@ function Description({ description }: { description: string }) {
|
|||
)
|
||||
}
|
||||
|
||||
function MetaFull({ values }: { values: Partial<MetadataPublishForm> }) {
|
||||
function MetaFull({ values }: { values: Partial<MetadataPublishFormDataset> }) {
|
||||
return (
|
||||
<div className={styles.metaFull}>
|
||||
{Object.entries(values)
|
||||
|
@ -56,6 +59,8 @@ function MetaFull({ values }: { values: Partial<MetadataPublishForm> }) {
|
|||
key.includes('links') ||
|
||||
key.includes('termsAndConditions') ||
|
||||
key.includes('dataTokenOptions') ||
|
||||
key.includes('dockerImage') ||
|
||||
key.includes('algorithmPrivacy') ||
|
||||
value === undefined ||
|
||||
value === ''
|
||||
)
|
||||
|
@ -82,10 +87,10 @@ function Sample({ url }: { url: string }) {
|
|||
)
|
||||
}
|
||||
|
||||
export default function MetadataPreview({
|
||||
export function MetadataPreview({
|
||||
values
|
||||
}: {
|
||||
values: Partial<MetadataPublishForm>
|
||||
values: Partial<MetadataPublishFormDataset>
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className={styles.preview}>
|
||||
|
@ -119,3 +124,52 @@ export default function MetadataPreview({
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MetadataAlgorithmPreview({
|
||||
values
|
||||
}: {
|
||||
values: Partial<MetadataPublishFormAlgorithm>
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className={styles.preview}>
|
||||
<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}>
|
||||
{values.files?.length > 0 && typeof values.files !== 'string' && (
|
||||
<File
|
||||
file={values.files[0] as FileMetadata}
|
||||
className={styles.file}
|
||||
small
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{values.tags && <Tags items={transformTags(values.tags)} />}
|
||||
</header>
|
||||
<div className={styles.metaAlgorithm}>
|
||||
{values.dockerImage && (
|
||||
<MetaItem
|
||||
key="dockerImage"
|
||||
title="Docker Image"
|
||||
content={values.dockerImage}
|
||||
/>
|
||||
)}
|
||||
{values.algorithmPrivacy && (
|
||||
<MetaItem
|
||||
key="privateAlgorithm"
|
||||
title="Private Algorithm"
|
||||
content="Yes"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<MetaFull values={values} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,3 +11,7 @@
|
|||
.form label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form input {
|
||||
background-color: var(--background-content);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
border-radius: var(--border-radius);
|
||||
padding: calc(var(--spacer) / 4);
|
||||
white-space: nowrap;
|
||||
background: none;
|
||||
background: var(--background-content);
|
||||
margin: 0;
|
||||
transition: border 0.2s ease-out;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -76,11 +76,11 @@
|
|||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.walletInfo{
|
||||
.walletInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.walletInfo button{
|
||||
.walletInfo button {
|
||||
margin-top: calc(var(--spacer) / 5) !important;
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
.info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
margin-bottom: var(--spacer);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-top: -1rem;
|
||||
margin-left: -2rem;
|
||||
margin-right: -2rem;
|
||||
padding: 0 var(--spacer) calc(var(--spacer) / 2) var(--spacer);
|
||||
}
|
||||
|
||||
.filewrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: var(--spacer);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feedback {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.help {
|
||||
composes: help from './index.module.css';
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
import React, { useState, ReactElement, ChangeEvent, useEffect } from 'react'
|
||||
import { DDO, Logger } from '@oceanprotocol/lib'
|
||||
import Loader from '../../atoms/Loader'
|
||||
import Web3Feedback from '../../molecules/Wallet/Feedback'
|
||||
import Price from '../../atoms/Price'
|
||||
import File from '../../atoms/File'
|
||||
import { computeOptions, useCompute } from '../../../hooks/useCompute'
|
||||
import styles from './Compute.module.css'
|
||||
import Input from '../../atoms/Input'
|
||||
import Alert from '../../atoms/Alert'
|
||||
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
|
||||
import checkPreviousOrder from '../../../utils/checkPreviousOrder'
|
||||
import { useOcean } from '../../../providers/Ocean'
|
||||
import { useWeb3 } from '../../../providers/Web3'
|
||||
import { usePricing } from '../../../hooks/usePricing'
|
||||
import { useAsset } from '../../../providers/Asset'
|
||||
|
||||
export default function Compute({
|
||||
ddo,
|
||||
isBalanceSufficient,
|
||||
dtBalance
|
||||
}: {
|
||||
ddo: DDO
|
||||
isBalanceSufficient: boolean
|
||||
dtBalance: string
|
||||
}): ReactElement {
|
||||
const { marketFeeAddress } = useSiteMetadata()
|
||||
const { accountId } = useWeb3()
|
||||
const { ocean } = useOcean()
|
||||
const { compute, isLoading, computeStepText, computeError } = useCompute()
|
||||
const { buyDT, dtSymbol } = usePricing(ddo)
|
||||
const { price } = useAsset()
|
||||
const computeService = ddo.findServiceByType('compute')
|
||||
const metadataService = ddo.findServiceByType('metadata')
|
||||
|
||||
const [isJobStarting, setIsJobStarting] = useState(false)
|
||||
const [, setError] = useState('')
|
||||
const [computeType, setComputeType] = useState('nodejs')
|
||||
const [computeContainer, setComputeContainer] = useState(
|
||||
computeOptions[0].value
|
||||
)
|
||||
const [algorithmRawCode, setAlgorithmRawCode] = useState('')
|
||||
const [isPublished, setIsPublished] = useState(false)
|
||||
const [file, setFile] = useState(null)
|
||||
const [hasPreviousOrder, setHasPreviousOrder] = useState(false)
|
||||
const [previousOrderId, setPreviousOrderId] = useState<string>()
|
||||
const isComputeButtonDisabled =
|
||||
isJobStarting === true ||
|
||||
file === null ||
|
||||
computeType === '' ||
|
||||
!ocean ||
|
||||
!isBalanceSufficient
|
||||
const hasDatatoken = Number(dtBalance) >= 1
|
||||
|
||||
useEffect(() => {
|
||||
if (!ocean || !accountId) return
|
||||
|
||||
async function checkPreviousOrders() {
|
||||
const orderId = await checkPreviousOrder(ocean, accountId, ddo, 'compute')
|
||||
setPreviousOrderId(orderId)
|
||||
setHasPreviousOrder(!!orderId)
|
||||
}
|
||||
checkPreviousOrders()
|
||||
}, [ocean, ddo, accountId])
|
||||
|
||||
const handleSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const comType = event.target.value
|
||||
setComputeType(comType)
|
||||
|
||||
const selectedComputeOption = computeOptions.find((x) => x.name === comType)
|
||||
if (selectedComputeOption !== undefined)
|
||||
setComputeContainer(selectedComputeOption.value)
|
||||
}
|
||||
|
||||
// const startJob = async () => {
|
||||
// try {
|
||||
// if (!ocean) return
|
||||
|
||||
// setIsJobStarting(true)
|
||||
// setIsPublished(false)
|
||||
// setError('')
|
||||
|
||||
// !hasPreviousOrder && !hasDatatoken && (await buyDT('1'))
|
||||
|
||||
// await compute(
|
||||
// ddo.id,
|
||||
// computeService,
|
||||
// ddo.dataToken,
|
||||
// algorithmRawCode,
|
||||
// computeContainer,
|
||||
// marketFeeAddress,
|
||||
// previousOrderId
|
||||
// )
|
||||
|
||||
// setHasPreviousOrder(true)
|
||||
// setIsPublished(true)
|
||||
// setFile(null)
|
||||
// } catch (error) {
|
||||
// setError('Failed to start job!')
|
||||
// Logger.error(error.message)
|
||||
// } finally {
|
||||
// setIsJobStarting(false)
|
||||
// }
|
||||
// }
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.filewrapper}>
|
||||
<File file={metadataService.attributes.main.files[0]} small />
|
||||
</div>
|
||||
<div className={styles.pricewrapper}>
|
||||
<Price price={price} conversion />
|
||||
{hasDatatoken && (
|
||||
<div className={styles.hasTokens}>
|
||||
You own {dtBalance} {dtSymbol} allowing you to use this data set
|
||||
without paying again.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="select"
|
||||
name="algorithm"
|
||||
label="Select image to run the algorithm"
|
||||
placeholder=""
|
||||
size="small"
|
||||
value={computeType}
|
||||
options={computeOptions.map((x) => x.name)}
|
||||
onChange={handleSelectChange}
|
||||
/>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{isLoading ? (
|
||||
<Loader message={computeStepText} />
|
||||
) : (
|
||||
<Alert text="Compute is coming back at a later stage." state="info" />
|
||||
// <Button
|
||||
// style="primary"
|
||||
// onClick={() => startJob()}
|
||||
// disabled={isComputeButtonDisabled}
|
||||
// >
|
||||
// {hasDatatoken || hasPreviousOrder ? 'Start job' : 'Buy'}
|
||||
// </Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className={styles.feedback}>
|
||||
{computeError !== undefined && (
|
||||
<Alert text={computeError} state="error" />
|
||||
)}
|
||||
{isPublished && (
|
||||
<Alert
|
||||
title="Your job started!"
|
||||
text="Watch the progress in the history page."
|
||||
state="success"
|
||||
/>
|
||||
)}
|
||||
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
.form {
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.form > div > label,
|
||||
.form [class*='ButtonBuy-module--actions'] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form > div > label {
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
margin-left: -1rem;
|
||||
}
|
||||
|
||||
.form [class*='AssetSelection-module--selection'] {
|
||||
margin-left: -2rem;
|
||||
margin-right: -2rem;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
padding: 0;
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import styles from './FormComputeDataset.module.css'
|
||||
import { Field, Form, FormikContextType, useFormikContext } from 'formik'
|
||||
import Input from '../../../atoms/Input'
|
||||
import { FormFieldProps } from '../../../../@types/Form'
|
||||
import { useStaticQuery, graphql } from 'gatsby'
|
||||
import { DDO, BestPrice } from '@oceanprotocol/lib'
|
||||
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
|
||||
import ButtonBuy from '../../../atoms/ButtonBuy'
|
||||
import PriceOutput from './PriceOutput'
|
||||
import { useAsset } from '../../../../providers/Asset'
|
||||
|
||||
const contentQuery = graphql`
|
||||
query StartComputeDatasetQuery {
|
||||
content: allFile(
|
||||
filter: { relativePath: { eq: "pages/startComputeDataset.json" } }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
childPagesJson {
|
||||
description
|
||||
form {
|
||||
success
|
||||
successAction
|
||||
error
|
||||
data {
|
||||
name
|
||||
label
|
||||
help
|
||||
type
|
||||
required
|
||||
sortOptions
|
||||
options
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function FormStartCompute({
|
||||
algorithms,
|
||||
ddoListAlgorithms,
|
||||
setSelectedAlgorithm,
|
||||
isLoading,
|
||||
isComputeButtonDisabled,
|
||||
hasPreviousOrder,
|
||||
hasDatatoken,
|
||||
dtBalance,
|
||||
assetType,
|
||||
assetTimeout,
|
||||
hasPreviousOrderSelectedComputeAsset,
|
||||
hasDatatokenSelectedComputeAsset,
|
||||
dtSymbolSelectedComputeAsset,
|
||||
dtBalanceSelectedComputeAsset,
|
||||
selectedComputeAssetType,
|
||||
selectedComputeAssetTimeout,
|
||||
stepText,
|
||||
algorithmPrice
|
||||
}: {
|
||||
algorithms: AssetSelectionAsset[]
|
||||
ddoListAlgorithms: DDO[]
|
||||
setSelectedAlgorithm: React.Dispatch<React.SetStateAction<DDO>>
|
||||
isLoading: boolean
|
||||
isComputeButtonDisabled: boolean
|
||||
hasPreviousOrder: boolean
|
||||
hasDatatoken: boolean
|
||||
dtBalance: string
|
||||
assetType: string
|
||||
assetTimeout: string
|
||||
hasPreviousOrderSelectedComputeAsset?: boolean
|
||||
hasDatatokenSelectedComputeAsset?: boolean
|
||||
dtSymbolSelectedComputeAsset?: string
|
||||
dtBalanceSelectedComputeAsset?: string
|
||||
selectedComputeAssetType?: string
|
||||
selectedComputeAssetTimeout?: string
|
||||
stepText: string
|
||||
algorithmPrice: BestPrice
|
||||
}): ReactElement {
|
||||
const data = useStaticQuery(contentQuery)
|
||||
const content = data.content.edges[0].node.childPagesJson
|
||||
|
||||
const {
|
||||
isValid,
|
||||
values
|
||||
}: FormikContextType<{ algorithm: string }> = useFormikContext()
|
||||
const { price, ddo } = useAsset()
|
||||
const [totalPrice, setTotalPrice] = useState(price?.value)
|
||||
|
||||
function getAlgorithmAsset(algorithmId: string): DDO {
|
||||
let assetDdo = null
|
||||
ddoListAlgorithms.forEach((ddo: DDO) => {
|
||||
if (ddo.id === algorithmId) assetDdo = ddo
|
||||
})
|
||||
return assetDdo
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!values.algorithm) return
|
||||
setSelectedAlgorithm(getAlgorithmAsset(values.algorithm))
|
||||
}, [values.algorithm])
|
||||
|
||||
//
|
||||
// Set price for calculation output
|
||||
//
|
||||
useEffect(() => {
|
||||
if (!price || !algorithmPrice) return
|
||||
|
||||
const priceDataset =
|
||||
hasPreviousOrder || hasDatatoken ? 0 : Number(price.value)
|
||||
const priceAlgo =
|
||||
hasPreviousOrderSelectedComputeAsset || hasDatatokenSelectedComputeAsset
|
||||
? 0
|
||||
: Number(algorithmPrice.value)
|
||||
|
||||
setTotalPrice(priceDataset + priceAlgo)
|
||||
}, [
|
||||
price,
|
||||
algorithmPrice,
|
||||
hasPreviousOrder,
|
||||
hasDatatoken,
|
||||
hasPreviousOrderSelectedComputeAsset,
|
||||
hasDatatokenSelectedComputeAsset
|
||||
])
|
||||
|
||||
return (
|
||||
<Form className={styles.form}>
|
||||
{content.form.data.map((field: FormFieldProps) => (
|
||||
<Field
|
||||
key={field.name}
|
||||
{...field}
|
||||
options={algorithms}
|
||||
component={Input}
|
||||
/>
|
||||
))}
|
||||
|
||||
<PriceOutput
|
||||
hasPreviousOrder={hasPreviousOrder}
|
||||
assetTimeout={assetTimeout}
|
||||
hasPreviousOrderSelectedComputeAsset={
|
||||
hasPreviousOrderSelectedComputeAsset
|
||||
}
|
||||
hasDatatoken={hasDatatoken}
|
||||
selectedComputeAssetTimeout={selectedComputeAssetTimeout}
|
||||
hasDatatokenSelectedComputeAsset={hasDatatokenSelectedComputeAsset}
|
||||
algorithmPrice={algorithmPrice}
|
||||
totalPrice={totalPrice}
|
||||
/>
|
||||
|
||||
<ButtonBuy
|
||||
action="compute"
|
||||
disabled={isComputeButtonDisabled || !isValid}
|
||||
hasPreviousOrder={hasPreviousOrder}
|
||||
hasDatatoken={hasDatatoken}
|
||||
dtSymbol={ddo.dataTokenInfo.symbol}
|
||||
dtBalance={dtBalance}
|
||||
assetTimeout={assetTimeout}
|
||||
assetType={assetType}
|
||||
hasPreviousOrderSelectedComputeAsset={
|
||||
hasPreviousOrderSelectedComputeAsset
|
||||
}
|
||||
hasDatatokenSelectedComputeAsset={hasDatatokenSelectedComputeAsset}
|
||||
dtSymbolSelectedComputeAsset={dtSymbolSelectedComputeAsset}
|
||||
dtBalanceSelectedComputeAsset={dtBalanceSelectedComputeAsset}
|
||||
selectedComputeAssetType={selectedComputeAssetType}
|
||||
stepText={stepText}
|
||||
isLoading={isLoading}
|
||||
type="submit"
|
||||
/>
|
||||
</Form>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
.priceComponent {
|
||||
margin-left: -2rem;
|
||||
margin-right: -2rem;
|
||||
margin-top: -1rem;
|
||||
margin-bottom: calc(var(--spacer) / 1.5);
|
||||
padding-left: calc(var(--spacer) / 2);
|
||||
padding-right: calc(var(--spacer) / 2);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: calc(var(--spacer) / 3);
|
||||
text-align: center;
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.priceComponent > * {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.calculation {
|
||||
min-width: 12rem;
|
||||
}
|
||||
|
||||
.timeout {
|
||||
display: block;
|
||||
text-align: right;
|
||||
font-size: var(--font-size-mini);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.calculation .price {
|
||||
font-size: var(--font-size-small) !important;
|
||||
}
|
||||
|
||||
.priceRow {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-top: calc(var(--spacer) / 7);
|
||||
padding-bottom: calc(var(--spacer) / 7);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.priceRow:last-child {
|
||||
border-bottom: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sign {
|
||||
display: inline-block;
|
||||
width: 5%;
|
||||
text-align: left;
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import { BestPrice } from '@oceanprotocol/lib'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { useAsset } from '../../../../providers/Asset'
|
||||
import PriceUnit from '../../../atoms/Price/PriceUnit'
|
||||
import Tooltip from '../../../atoms/Tooltip'
|
||||
import styles from './PriceOutput.module.css'
|
||||
|
||||
interface PriceOutputProps {
|
||||
totalPrice: number
|
||||
hasPreviousOrder: boolean
|
||||
hasDatatoken: boolean
|
||||
assetTimeout: string
|
||||
hasPreviousOrderSelectedComputeAsset: boolean
|
||||
hasDatatokenSelectedComputeAsset: boolean
|
||||
algorithmPrice: BestPrice
|
||||
selectedComputeAssetTimeout: string
|
||||
}
|
||||
|
||||
function Row({
|
||||
price,
|
||||
hasPreviousOrder,
|
||||
hasDatatoken,
|
||||
timeout,
|
||||
sign
|
||||
}: {
|
||||
price: number
|
||||
hasPreviousOrder?: boolean
|
||||
hasDatatoken?: boolean
|
||||
timeout?: string
|
||||
sign?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.priceRow}>
|
||||
<div className={styles.sign}>{sign}</div>
|
||||
<div>
|
||||
<PriceUnit
|
||||
price={hasPreviousOrder || hasDatatoken ? '0' : `${price}`}
|
||||
small
|
||||
className={styles.price}
|
||||
/>
|
||||
<span className={styles.timeout}>
|
||||
{timeout &&
|
||||
timeout !== 'Forever' &&
|
||||
!hasPreviousOrder &&
|
||||
`for ${timeout}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PriceOutput({
|
||||
totalPrice,
|
||||
hasPreviousOrder,
|
||||
hasDatatoken,
|
||||
assetTimeout,
|
||||
hasPreviousOrderSelectedComputeAsset,
|
||||
hasDatatokenSelectedComputeAsset,
|
||||
algorithmPrice,
|
||||
selectedComputeAssetTimeout
|
||||
}: PriceOutputProps): ReactElement {
|
||||
const { price } = useAsset()
|
||||
|
||||
return (
|
||||
<div className={styles.priceComponent}>
|
||||
You will pay <PriceUnit price={`${totalPrice}`} small />
|
||||
<Tooltip
|
||||
content={
|
||||
<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="+"
|
||||
/>
|
||||
<Row price={totalPrice} sign="=" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
.info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-top: -1rem;
|
||||
margin-left: -2rem;
|
||||
margin-right: -2rem;
|
||||
padding: 0 calc(var(--spacer) / 2) calc(var(--spacer) / 2)
|
||||
calc(var(--spacer) * 1.5);
|
||||
}
|
||||
|
||||
.feedback {
|
||||
width: 100%;
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.feedback:empty {
|
||||
margin-top: 0;
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import Compute from './Compute'
|
||||
import ddo from '../../../../tests/unit/__fixtures__/ddo'
|
||||
import { DDO } from '@oceanprotocol/lib'
|
||||
import Compute from '.'
|
||||
import ddo from '../../../../../tests/unit/__fixtures__/ddo'
|
||||
|
||||
export default {
|
||||
title: 'Organisms/Compute',
|
||||
|
@ -13,5 +12,9 @@ export default {
|
|||
}
|
||||
|
||||
export const Default = (): ReactElement => (
|
||||
<Compute ddo={ddo as DDO} dtBalance="1" isBalanceSufficient />
|
||||
<Compute
|
||||
dtBalance="1"
|
||||
isBalanceSufficient
|
||||
file={ddo.service[0].attributes.main.files[0]}
|
||||
/>
|
||||
)
|
498
src/components/organisms/AssetActions/Compute/index.tsx
Normal file
498
src/components/organisms/AssetActions/Compute/index.tsx
Normal file
|
@ -0,0 +1,498 @@
|
|||
import React, { useState, ReactElement, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
DDO,
|
||||
File as FileMetadata,
|
||||
Logger,
|
||||
ServiceType,
|
||||
publisherTrustedAlgorithm,
|
||||
BestPrice
|
||||
} from '@oceanprotocol/lib'
|
||||
import { toast } from 'react-toastify'
|
||||
import Price from '../../../atoms/Price'
|
||||
import File from '../../../atoms/File'
|
||||
import Alert from '../../../atoms/Alert'
|
||||
import Web3Feedback from '../../../molecules/Wallet/Feedback'
|
||||
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
|
||||
import { useOcean } from '../../../../providers/Ocean'
|
||||
import { useWeb3 } from '../../../../providers/Web3'
|
||||
import { usePricing } from '../../../../hooks/usePricing'
|
||||
import { useAsset } from '../../../../providers/Asset'
|
||||
import {
|
||||
queryMetadata,
|
||||
transformDDOToAssetSelection
|
||||
} from '../../../../utils/aquarius'
|
||||
import { Formik } from 'formik'
|
||||
import {
|
||||
getInitialValues,
|
||||
validationSchema
|
||||
} from '../../../../models/FormStartComputeDataset'
|
||||
import {
|
||||
ComputeAlgorithm,
|
||||
ComputeOutput
|
||||
} from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
|
||||
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
|
||||
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'
|
||||
|
||||
const SuccessAction = () => (
|
||||
<Button style="text" to="/history" 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
|
||||
consumePrice
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function Compute({
|
||||
isBalanceSufficient,
|
||||
dtBalance,
|
||||
file
|
||||
}: {
|
||||
isBalanceSufficient: boolean
|
||||
dtBalance: string
|
||||
file: FileMetadata
|
||||
}): ReactElement {
|
||||
const { marketFeeAddress } = useSiteMetadata()
|
||||
const { accountId } = useWeb3()
|
||||
const { ocean, account, config } = useOcean()
|
||||
const { price, type, ddo } = useAsset()
|
||||
const { buyDT, pricingError, pricingStepText } = usePricing()
|
||||
const [isJobStarting, setIsJobStarting] = useState(false)
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
const [algorithmList, setAlgorithmList] = useState<AssetSelectionAsset[]>()
|
||||
const [ddoAlgorithmList, setDdoAlgorithmList] = useState<DDO[]>()
|
||||
const [selectedAlgorithmAsset, setSelectedAlgorithmAsset] = useState<DDO>()
|
||||
const [hasAlgoAssetDatatoken, setHasAlgoAssetDatatoken] = useState<boolean>()
|
||||
const [isPublished, setIsPublished] = useState(false)
|
||||
const [hasPreviousDatasetOrder, setHasPreviousDatasetOrder] = useState(false)
|
||||
const [previousDatasetOrderId, setPreviousDatasetOrderId] = useState<string>()
|
||||
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 [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
|
||||
|
||||
async function checkPreviousOrders(ddo: DDO) {
|
||||
const { timeout } = (
|
||||
ddo.findServiceByType('access') || ddo.findServiceByType('compute')
|
||||
).attributes.main
|
||||
const orderId = await getPreviousOrders(
|
||||
ddo.dataToken?.toLowerCase(),
|
||||
accountId?.toLowerCase(),
|
||||
timeout.toString()
|
||||
)
|
||||
const assetType = ddo.findServiceByType('metadata').attributes.main.type
|
||||
if (assetType === 'algorithm') {
|
||||
setPreviousAlgorithmOrderId(orderId)
|
||||
setHasPreviousAlgorithmOrder(!!orderId)
|
||||
} else {
|
||||
setPreviousDatasetOrderId(orderId)
|
||||
setHasPreviousDatasetOrder(!!orderId)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAssetDTBalance(asset: DDO) {
|
||||
const AssetDtBalance = await ocean.datatokens.balance(
|
||||
asset.dataToken,
|
||||
accountId
|
||||
)
|
||||
setalgorithmDTBalance(AssetDtBalance)
|
||||
setHasAlgoAssetDatatoken(Number(AssetDtBalance) >= 1)
|
||||
}
|
||||
|
||||
function getQuerryString(
|
||||
trustedAlgorithmList: publisherTrustedAlgorithm[]
|
||||
): SearchQuery {
|
||||
let algoQuerry = ''
|
||||
trustedAlgorithmList.forEach((trusteAlgo) => {
|
||||
algoQuerry += `id:"${trusteAlgo.did}" OR `
|
||||
})
|
||||
if (trustedAlgorithmList.length >= 1) {
|
||||
algoQuerry = algoQuerry.substring(0, algoQuerry.length - 3)
|
||||
}
|
||||
const algorithmQuery =
|
||||
trustedAlgorithmList.length > 0 ? `(${algoQuerry}) AND` : ``
|
||||
const query = {
|
||||
page: 1,
|
||||
query: {
|
||||
query_string: {
|
||||
query: `${algorithmQuery} service.attributes.main.type:algorithm -isInPurgatory:true`
|
||||
}
|
||||
},
|
||||
sort: { created: -1 }
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
async function getAlgorithmList(): Promise<AssetSelectionAsset[]> {
|
||||
const source = axios.CancelToken.source()
|
||||
const computeService = ddo.findServiceByType('compute')
|
||||
let algorithmSelectionList: AssetSelectionAsset[]
|
||||
if (
|
||||
!computeService.attributes.main.privacy ||
|
||||
!computeService.attributes.main.privacy.publisherTrustedAlgorithms ||
|
||||
(computeService.attributes.main.privacy.publisherTrustedAlgorithms
|
||||
.length === 0 &&
|
||||
!computeService.attributes.main.privacy.allowAllPublishedAlgorithms)
|
||||
) {
|
||||
algorithmSelectionList = []
|
||||
} else {
|
||||
const gueryResults = await queryMetadata(
|
||||
getQuerryString(
|
||||
computeService.attributes.main.privacy.publisherTrustedAlgorithms
|
||||
),
|
||||
config.metadataCacheUri,
|
||||
source.token
|
||||
)
|
||||
setDdoAlgorithmList(gueryResults.results)
|
||||
algorithmSelectionList = await transformDDOToAssetSelection(
|
||||
gueryResults.results,
|
||||
config.metadataCacheUri,
|
||||
[]
|
||||
)
|
||||
}
|
||||
return algorithmSelectionList
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { timeout } = (
|
||||
ddo.findServiceByType('access') || ddo.findServiceByType('compute')
|
||||
).attributes.main
|
||||
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].consumePrice === '-1'
|
||||
? poolPrice.pools[0].spotPrice
|
||||
: poolPrice.pools[0].consumePrice
|
||||
}))
|
||||
}, [poolPrice])
|
||||
|
||||
const initMetadata = useCallback(async (ddo: DDO): Promise<void> => {
|
||||
if (!ddo) return
|
||||
setAlgorithmPrice(ddo.price)
|
||||
setVariables({ datatoken: ddo?.dataToken.toLowerCase() })
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!ddo) return
|
||||
getAlgorithmList().then((algorithms) => {
|
||||
setAlgorithmList(algorithms)
|
||||
})
|
||||
}, [ddo])
|
||||
|
||||
useEffect(() => {
|
||||
if (!ocean || !accountId) return
|
||||
checkPreviousOrders(ddo)
|
||||
}, [ocean, ddo, accountId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedAlgorithmAsset) return
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
const newError = error || pricingError
|
||||
if (!newError) return
|
||||
toast.error(newError)
|
||||
}, [error, pricingError])
|
||||
|
||||
async function startJob(algorithmId: string) {
|
||||
try {
|
||||
if (!ocean) return
|
||||
|
||||
setIsJobStarting(true)
|
||||
setIsPublished(false)
|
||||
setError(undefined)
|
||||
|
||||
const computeService = ddo.findServiceByType('compute')
|
||||
const serviceAlgo = selectedAlgorithmAsset.findServiceByType('access')
|
||||
? selectedAlgorithmAsset.findServiceByType('access')
|
||||
: selectedAlgorithmAsset.findServiceByType('compute')
|
||||
|
||||
const computeAlgorithm: ComputeAlgorithm = {
|
||||
did: selectedAlgorithmAsset.id,
|
||||
serviceIndex: serviceAlgo.index,
|
||||
dataToken: selectedAlgorithmAsset.dataToken
|
||||
}
|
||||
const allowed = await ocean.compute.isOrderable(
|
||||
ddo.id,
|
||||
computeService.index,
|
||||
computeAlgorithm
|
||||
)
|
||||
Logger.log('[compute] Is data set orderable?', allowed)
|
||||
|
||||
if (!allowed) {
|
||||
setError(
|
||||
'Data set is not orderable in combination with selected algorithm.'
|
||||
)
|
||||
Logger.error(
|
||||
'[compute] Error starting compute job. Dataset is not orderable in combination with selected algorithm.'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasPreviousDatasetOrder && !hasDatatoken) {
|
||||
const tx = await buyDT('1', price, ddo)
|
||||
if (!tx) {
|
||||
setError('Error buying datatoken.')
|
||||
Logger.error('[compute] Error buying datatoken for data set ', ddo.id)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPreviousAlgorithmOrder && !hasAlgoAssetDatatoken) {
|
||||
const tx = await buyDT('1', algorithmPrice, selectedAlgorithmAsset)
|
||||
if (!tx) {
|
||||
setError('Error buying datatoken.')
|
||||
Logger.error(
|
||||
'[compute] Error buying datatoken for algorithm ',
|
||||
selectedAlgorithmAsset.id
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: pricingError is always undefined even upon errors during buyDT for whatever reason.
|
||||
// So manually drop out above, but ideally could be replaced with this alone.
|
||||
if (pricingError) {
|
||||
setError(pricingError)
|
||||
return
|
||||
}
|
||||
|
||||
const assetOrderId = hasPreviousDatasetOrder
|
||||
? previousDatasetOrderId
|
||||
: await ocean.compute.orderAsset(
|
||||
accountId,
|
||||
ddo.id,
|
||||
computeService.index,
|
||||
computeAlgorithm,
|
||||
marketFeeAddress,
|
||||
undefined,
|
||||
false
|
||||
)
|
||||
|
||||
assetOrderId &&
|
||||
Logger.log(
|
||||
`[compute] Got ${
|
||||
hasPreviousDatasetOrder ? 'existing' : 'new'
|
||||
} order ID for dataset: `,
|
||||
assetOrderId
|
||||
)
|
||||
|
||||
const algorithmAssetOrderId = hasPreviousAlgorithmOrder
|
||||
? previousAlgorithmOrderId
|
||||
: await ocean.compute.orderAlgorithm(
|
||||
algorithmId,
|
||||
serviceAlgo.type,
|
||||
accountId,
|
||||
serviceAlgo.index,
|
||||
marketFeeAddress,
|
||||
undefined,
|
||||
false
|
||||
)
|
||||
|
||||
algorithmAssetOrderId &&
|
||||
Logger.log(
|
||||
`[compute] Got ${
|
||||
hasPreviousAlgorithmOrder ? 'existing' : 'new'
|
||||
} order ID for algorithm: `,
|
||||
algorithmAssetOrderId
|
||||
)
|
||||
|
||||
if (!assetOrderId || !algorithmAssetOrderId) {
|
||||
setError('Error ordering assets.')
|
||||
return
|
||||
}
|
||||
|
||||
computeAlgorithm.transferTxId = algorithmAssetOrderId
|
||||
Logger.log('[compute] Starting compute job.')
|
||||
|
||||
const output: ComputeOutput = {
|
||||
publishAlgorithmLog: true,
|
||||
publishOutput: true
|
||||
}
|
||||
const response = await ocean.compute.start(
|
||||
ddo.id,
|
||||
assetOrderId,
|
||||
ddo.dataToken,
|
||||
account,
|
||||
computeAlgorithm,
|
||||
output,
|
||||
`${computeService.index}`,
|
||||
computeService.type
|
||||
)
|
||||
|
||||
if (!response) {
|
||||
setError('Error starting compute job.')
|
||||
return
|
||||
}
|
||||
|
||||
Logger.log('[compute] Starting compute job response: ', response)
|
||||
|
||||
setHasPreviousDatasetOrder(true)
|
||||
setIsPublished(true)
|
||||
} catch (error) {
|
||||
setError('Failed to start job!')
|
||||
Logger.error('[compute] Failed to start job: ', error.message)
|
||||
} finally {
|
||||
setIsJobStarting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.info}>
|
||||
<File file={file} 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"
|
||||
/>
|
||||
) : (
|
||||
<Formik
|
||||
initialValues={getInitialValues()}
|
||||
validateOnMount
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={async (values) => await startJob(values.algorithm)}
|
||||
>
|
||||
<FormStartComputeDataset
|
||||
algorithms={algorithmList}
|
||||
ddoListAlgorithms={ddoAlgorithmList}
|
||||
setSelectedAlgorithm={setSelectedAlgorithmAsset}
|
||||
isLoading={isJobStarting}
|
||||
isComputeButtonDisabled={isComputeButtonDisabled}
|
||||
hasPreviousOrder={hasPreviousDatasetOrder}
|
||||
hasDatatoken={hasDatatoken}
|
||||
dtBalance={dtBalance}
|
||||
assetType={type}
|
||||
assetTimeout={datasetTimeout}
|
||||
hasPreviousOrderSelectedComputeAsset={hasPreviousAlgorithmOrder}
|
||||
hasDatatokenSelectedComputeAsset={hasAlgoAssetDatatoken}
|
||||
dtSymbolSelectedComputeAsset={
|
||||
selectedAlgorithmAsset?.dataTokenInfo?.symbol
|
||||
}
|
||||
dtBalanceSelectedComputeAsset={algorithmDTBalance}
|
||||
selectedComputeAssetType="algorithm"
|
||||
selectedComputeAssetTimeout={algorithmTimeout}
|
||||
stepText={pricingStepText || 'Starting Compute Job...'}
|
||||
algorithmPrice={algorithmPrice}
|
||||
/>
|
||||
</Formik>
|
||||
)}
|
||||
|
||||
<footer className={styles.feedback}>
|
||||
{isPublished && (
|
||||
<SuccessConfetti
|
||||
success="Your job started successfully! Watch the progress on the history page."
|
||||
action={<SuccessAction />}
|
||||
/>
|
||||
)}
|
||||
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -12,19 +12,6 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.help {
|
||||
composes: help from './index.module.css';
|
||||
}
|
||||
|
||||
.help:not(:empty) {
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.feedback {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { File as FileMetadata, DDO } from '@oceanprotocol/lib'
|
||||
import Button from '../../atoms/Button'
|
||||
import File from '../../atoms/File'
|
||||
import Price from '../../atoms/Price'
|
||||
import Web3Feedback from '../../molecules/Wallet/Feedback'
|
||||
import styles from './Consume.module.css'
|
||||
import Loader from '../../atoms/Loader'
|
||||
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
|
||||
import { useAsset } from '../../../providers/Asset'
|
||||
import { secondsToString } from '../../../utils/metadata'
|
||||
|
@ -17,6 +15,7 @@ import { useOcean } from '../../../providers/Ocean'
|
|||
import { useWeb3 } from '../../../providers/Web3'
|
||||
import { usePricing } from '../../../hooks/usePricing'
|
||||
import { useConsume } from '../../../hooks/useConsume'
|
||||
import ButtonBuy from '../../atoms/ButtonBuy'
|
||||
|
||||
const previousOrderQuery = gql`
|
||||
query PreviousOrder($id: String!, $account: String!) {
|
||||
|
@ -32,26 +31,6 @@ const previousOrderQuery = gql`
|
|||
}
|
||||
`
|
||||
|
||||
function getHelpText(
|
||||
token: {
|
||||
dtBalance: string
|
||||
dtSymbol: string
|
||||
},
|
||||
hasDatatoken: boolean,
|
||||
hasPreviousOrder: boolean,
|
||||
timeout: string
|
||||
) {
|
||||
const { dtBalance, dtSymbol } = token
|
||||
const assetTimeout = timeout === 'Forever' ? '' : ` for ${timeout}`
|
||||
const text = hasPreviousOrder
|
||||
? `You bought this data set already allowing you to download it without paying again${assetTimeout}.`
|
||||
: hasDatatoken
|
||||
? `You own ${dtBalance} ${dtSymbol} allowing you to use this data set by spending 1 ${dtSymbol}, but without paying OCEAN again.`
|
||||
: `For using this data set, you will buy 1 ${dtSymbol} and immediately spend it back to the publisher and pool.`
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
export default function Consume({
|
||||
ddo,
|
||||
file,
|
||||
|
@ -68,10 +47,13 @@ export default function Consume({
|
|||
const { marketFeeAddress } = useSiteMetadata()
|
||||
const [hasPreviousOrder, setHasPreviousOrder] = useState(false)
|
||||
const [previousOrderId, setPreviousOrderId] = useState<string>()
|
||||
const { isInPurgatory, price } = useAsset()
|
||||
const { buyDT, pricingStepText, pricingError, pricingIsLoading } = usePricing(
|
||||
ddo
|
||||
)
|
||||
const { isInPurgatory, price, type } = useAsset()
|
||||
const {
|
||||
buyDT,
|
||||
pricingStepText,
|
||||
pricingError,
|
||||
pricingIsLoading
|
||||
} = usePricing()
|
||||
const { consumeStepText, consume, consumeError } = useConsume()
|
||||
const [isDisabled, setIsDisabled] = useState(true)
|
||||
const [hasDatatoken, setHasDatatoken] = useState(false)
|
||||
|
@ -144,7 +126,7 @@ export default function Consume({
|
|||
])
|
||||
|
||||
async function handleConsume() {
|
||||
!hasPreviousOrder && !hasDatatoken && (await buyDT('1', price))
|
||||
!hasPreviousOrder && !hasDatatoken && (await buyDT('1', price, ddo))
|
||||
await consume(
|
||||
ddo.id,
|
||||
ddo.dataToken,
|
||||
|
@ -162,29 +144,19 @@ export default function Consume({
|
|||
}, [consumeError, pricingError])
|
||||
|
||||
const PurchaseButton = () => (
|
||||
<div className={styles.actions}>
|
||||
{consumeStepText || pricingIsLoading ? (
|
||||
<Loader message={consumeStepText || pricingStepText} />
|
||||
) : (
|
||||
<>
|
||||
<Button style="primary" onClick={handleConsume} disabled={isDisabled}>
|
||||
{hasPreviousOrder
|
||||
? 'Download'
|
||||
: `Buy ${
|
||||
assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`
|
||||
}`}
|
||||
</Button>
|
||||
<div className={styles.help}>
|
||||
{getHelpText(
|
||||
{ dtBalance, dtSymbol: ddo.dataTokenInfo.symbol },
|
||||
hasDatatoken,
|
||||
hasPreviousOrder,
|
||||
assetTimeout
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ButtonBuy
|
||||
action="download"
|
||||
disabled={isDisabled}
|
||||
hasPreviousOrder={hasPreviousOrder}
|
||||
hasDatatoken={hasDatatoken}
|
||||
dtSymbol={ddo.dataTokenInfo?.symbol}
|
||||
dtBalance={dtBalance}
|
||||
onClick={handleConsume}
|
||||
assetTimeout={assetTimeout}
|
||||
assetType={type}
|
||||
stepText={consumeStepText || pricingStepText}
|
||||
isLoading={pricingIsLoading}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -198,7 +170,6 @@ export default function Consume({
|
|||
{!isInPurgatory && <PurchaseButton />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className={styles.feedback}>
|
||||
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
|
||||
</footer>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { DDO, ServiceComputePrivacy } from '@oceanprotocol/lib'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { ComputePrivacyForm } from '../../../../models/FormEditComputeDataset'
|
||||
import { useOcean } from '../../../../providers/Ocean'
|
||||
import { transformComputeFormToServiceComputePrivacy } from '../../../../utils/compute'
|
||||
import DebugOutput from '../../../atoms/DebugOutput'
|
||||
|
||||
export default function DebugEditCompute({
|
||||
values,
|
||||
ddo
|
||||
}: {
|
||||
values: ComputePrivacyForm
|
||||
ddo: DDO
|
||||
}): ReactElement {
|
||||
const { ocean } = useOcean()
|
||||
const [
|
||||
formTransformed,
|
||||
setFormTransformed
|
||||
] = useState<ServiceComputePrivacy>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!ocean) return
|
||||
|
||||
async function transformValues() {
|
||||
const privacy = await transformComputeFormToServiceComputePrivacy(
|
||||
values,
|
||||
ocean
|
||||
)
|
||||
setFormTransformed(privacy)
|
||||
}
|
||||
transformValues()
|
||||
}, [values, ddo, ocean])
|
||||
|
||||
return (
|
||||
<>
|
||||
<DebugOutput title="Collected Form Values" output={values} />
|
||||
<DebugOutput title="Transformed Form Values" output={formTransformed} />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { DDO } from '@oceanprotocol/lib'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { MetadataPublishForm } from '../../../../@types/MetaData'
|
||||
import { MetadataPublishFormDataset } from '../../../../@types/MetaData'
|
||||
import { transformPublishFormToMetadata } from '../../../../utils/metadata'
|
||||
import DebugOutput from '../../../atoms/DebugOutput'
|
||||
|
||||
|
@ -8,7 +8,7 @@ export default function Debug({
|
|||
values,
|
||||
ddo
|
||||
}: {
|
||||
values: Partial<MetadataPublishForm>
|
||||
values: Partial<MetadataPublishFormDataset>
|
||||
ddo: DDO
|
||||
}): ReactElement {
|
||||
const newDdo = {
|
|
@ -0,0 +1,161 @@
|
|||
import { useOcean } from '../../../../providers/Ocean'
|
||||
import { useWeb3 } from '../../../../providers/Web3'
|
||||
import { Formik } from 'formik'
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import {
|
||||
validationSchema,
|
||||
getInitialValues,
|
||||
ComputePrivacyForm
|
||||
} from '../../../../models/FormEditComputeDataset'
|
||||
import { useAsset } from '../../../../providers/Asset'
|
||||
import FormEditComputeDataset from './FormEditComputeDataset'
|
||||
import { Logger, ServiceComputePrivacy } from '@oceanprotocol/lib'
|
||||
import MetadataFeedback from '../../../molecules/MetadataFeedback'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import { useUserPreferences } from '../../../../providers/UserPreferences'
|
||||
import DebugEditCompute from './DebugEditCompute'
|
||||
import styles from './index.module.css'
|
||||
import { transformComputeFormToServiceComputePrivacy } from '../../../../utils/compute'
|
||||
|
||||
const contentQuery = graphql`
|
||||
query EditComputeDataQuery {
|
||||
content: allFile(
|
||||
filter: { relativePath: { eq: "pages/editComputeDataset.json" } }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
childPagesJson {
|
||||
description
|
||||
form {
|
||||
title
|
||||
success
|
||||
successAction
|
||||
error
|
||||
data {
|
||||
name
|
||||
placeholder
|
||||
label
|
||||
help
|
||||
type
|
||||
required
|
||||
sortOptions
|
||||
options
|
||||
multiple
|
||||
rows
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function EditComputeDataset({
|
||||
setShowEdit
|
||||
}: {
|
||||
setShowEdit: (show: boolean) => void
|
||||
}): ReactElement {
|
||||
const data = useStaticQuery(contentQuery)
|
||||
const content = data.content.edges[0].node.childPagesJson
|
||||
|
||||
const { debug } = useUserPreferences()
|
||||
const { ocean } = useOcean()
|
||||
const { accountId } = useWeb3()
|
||||
const { ddo, refreshDdo } = useAsset()
|
||||
const [success, setSuccess] = useState<string>()
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
const hasFeedback = error || success
|
||||
|
||||
async function handleSubmit(
|
||||
values: ComputePrivacyForm,
|
||||
resetForm: () => void
|
||||
) {
|
||||
try {
|
||||
const privacy = await transformComputeFormToServiceComputePrivacy(
|
||||
values,
|
||||
ocean
|
||||
)
|
||||
|
||||
const ddoEditedComputePrivacy = await ocean.compute.editComputePrivacy(
|
||||
ddo,
|
||||
1,
|
||||
privacy as ServiceComputePrivacy
|
||||
)
|
||||
|
||||
if (!ddoEditedComputePrivacy) {
|
||||
setError(content.form.error)
|
||||
Logger.error(content.form.error)
|
||||
return
|
||||
}
|
||||
|
||||
const storedddo = await ocean.assets.updateMetadata(
|
||||
ddoEditedComputePrivacy,
|
||||
accountId
|
||||
)
|
||||
if (!storedddo) {
|
||||
setError(content.form.error)
|
||||
Logger.error(content.form.error)
|
||||
return
|
||||
} else {
|
||||
// Edit succeeded
|
||||
setSuccess(content.form.success)
|
||||
resetForm()
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
setError(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={getInitialValues(
|
||||
ddo.findServiceByType('compute').attributes.main.privacy
|
||||
)}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={async (values, { resetForm }) => {
|
||||
// move user's focus to top of screen
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||
// kick off editing
|
||||
await handleSubmit(values, resetForm)
|
||||
}}
|
||||
>
|
||||
{({ values, isSubmitting }) =>
|
||||
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}>
|
||||
<FormEditComputeDataset
|
||||
title={content.form.title}
|
||||
data={content.form.data}
|
||||
setShowEdit={setShowEdit}
|
||||
/>
|
||||
</article>
|
||||
|
||||
{debug === true && (
|
||||
<div className={styles.grid}>
|
||||
<DebugEditCompute values={values} ddo={ddo} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Formik>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { Field, Form, FormikContextType, useFormikContext } from 'formik'
|
||||
import Button from '../../../atoms/Button'
|
||||
import Input from '../../../atoms/Input'
|
||||
import { useOcean } from '../../../../providers/Ocean'
|
||||
import { useWeb3 } from '../../../../providers/Web3'
|
||||
import { FormFieldProps } from '../../../../@types/Form'
|
||||
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
|
||||
import stylesIndex from './index.module.css'
|
||||
import styles from './FormEditMetadata.module.css'
|
||||
import {
|
||||
queryMetadata,
|
||||
transformDDOToAssetSelection
|
||||
} from '../../../../utils/aquarius'
|
||||
import { useAsset } from '../../../../providers/Asset'
|
||||
import { ComputePrivacyForm } from '../../../../models/FormEditComputeDataset'
|
||||
import { publisherTrustedAlgorithm as PublisherTrustedAlgorithm } from '@oceanprotocol/lib'
|
||||
import axios from 'axios'
|
||||
|
||||
export default function FormEditComputeDataset({
|
||||
data,
|
||||
title,
|
||||
setShowEdit
|
||||
}: {
|
||||
data: FormFieldProps[]
|
||||
title: string
|
||||
setShowEdit: (show: boolean) => void
|
||||
}): ReactElement {
|
||||
const { accountId } = useWeb3()
|
||||
const { ocean, config } = useOcean()
|
||||
const { ddo } = useAsset()
|
||||
const {
|
||||
isValid,
|
||||
values
|
||||
}: FormikContextType<ComputePrivacyForm> = useFormikContext()
|
||||
const [allAlgorithms, setAllAlgorithms] = useState<AssetSelectionAsset[]>()
|
||||
|
||||
const { publisherTrustedAlgorithms } = ddo?.findServiceByType(
|
||||
'compute'
|
||||
).attributes.main.privacy
|
||||
|
||||
async function getAlgorithmList(
|
||||
publisherTrustedAlgorithms: PublisherTrustedAlgorithm[]
|
||||
): Promise<AssetSelectionAsset[]> {
|
||||
const source = axios.CancelToken.source()
|
||||
const query = {
|
||||
page: 1,
|
||||
query: {
|
||||
query_string: {
|
||||
query: `service.attributes.main.type:algorithm -isInPurgatory:true`
|
||||
}
|
||||
},
|
||||
sort: { created: -1 }
|
||||
}
|
||||
const querryResult = await queryMetadata(
|
||||
query,
|
||||
config.metadataCacheUri,
|
||||
source.token
|
||||
)
|
||||
const algorithmSelectionList = await transformDDOToAssetSelection(
|
||||
querryResult.results,
|
||||
config.metadataCacheUri,
|
||||
publisherTrustedAlgorithms
|
||||
)
|
||||
return algorithmSelectionList
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAlgorithmList(publisherTrustedAlgorithms).then((algorithms) => {
|
||||
setAllAlgorithms(algorithms)
|
||||
})
|
||||
}, [config.metadataCacheUri, publisherTrustedAlgorithms])
|
||||
|
||||
return (
|
||||
<Form className={styles.form}>
|
||||
<h3 className={stylesIndex.title}>{title}</h3>
|
||||
{data.map((field: FormFieldProps) => (
|
||||
<Field
|
||||
key={field.name}
|
||||
{...field}
|
||||
options={
|
||||
field.name === 'publisherTrustedAlgorithms'
|
||||
? allAlgorithms
|
||||
: field.options
|
||||
}
|
||||
disabled={
|
||||
field.name === 'publisherTrustedAlgorithms'
|
||||
? values.allowAllPublishedAlgorithms
|
||||
: false
|
||||
}
|
||||
component={Input}
|
||||
/>
|
||||
))}
|
||||
<footer className={styles.actions}>
|
||||
<Button style="primary" disabled={!ocean || !accountId || !isValid}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button style="text" onClick={() => setShowEdit(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</footer>
|
||||
</Form>
|
||||
)
|
||||
}
|
|
@ -22,3 +22,7 @@
|
|||
margin-left: calc(var(--spacer) / 2);
|
||||
margin-right: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
select[multiple] {
|
||||
height: 130px;
|
||||
}
|
||||
|
|
|
@ -4,14 +4,14 @@ import { Field, Form, FormikContextType, useFormikContext } from 'formik'
|
|||
import Button from '../../../atoms/Button'
|
||||
import Input from '../../../atoms/Input'
|
||||
import { FormFieldProps } from '../../../../@types/Form'
|
||||
import { MetadataPublishForm } from '../../../../@types/MetaData'
|
||||
import { MetadataPublishFormDataset } from '../../../../@types/MetaData'
|
||||
import { checkIfTimeoutInPredefinedValues } from '../../../../utils/metadata'
|
||||
import { useOcean } from '../../../../providers/Ocean'
|
||||
import { useWeb3 } from '../../../../providers/Web3'
|
||||
|
||||
function handleTimeoutCustomOption(
|
||||
data: FormFieldProps[],
|
||||
values: Partial<MetadataPublishForm>
|
||||
values: Partial<MetadataPublishFormDataset>
|
||||
) {
|
||||
const timeoutFieldContent = data.filter(
|
||||
(field) => field.name === 'timeout'
|
||||
|
@ -53,7 +53,7 @@ export default function FormEditMetadata({
|
|||
data: FormFieldProps[]
|
||||
setShowEdit: (show: boolean) => void
|
||||
setTimeoutStringValue: (value: string) => void
|
||||
values: Partial<MetadataPublishForm>
|
||||
values: Partial<MetadataPublishFormDataset>
|
||||
showPrice: boolean
|
||||
}): ReactElement {
|
||||
const { accountId } = useWeb3()
|
||||
|
@ -62,7 +62,7 @@ export default function FormEditMetadata({
|
|||
isValid,
|
||||
validateField,
|
||||
setFieldValue
|
||||
}: FormikContextType<Partial<MetadataPublishForm>> = useFormikContext()
|
||||
}: FormikContextType<Partial<MetadataPublishFormDataset>> = useFormikContext()
|
||||
|
||||
// Manually handle change events instead of using `handleChange` from Formik.
|
||||
// Workaround for default `validateOnChange` not kicking in
|
||||
|
|
|
@ -8,3 +8,14 @@
|
|||
margin-top: -1.5rem;
|
||||
max-width: 50rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-large);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: calc(var(--spacer) / 2);
|
||||
margin-top: -1rem;
|
||||
margin-left: -2rem;
|
||||
margin-right: -2rem;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import {
|
|||
} from '../../../../models/FormEditMetadata'
|
||||
import { useAsset } from '../../../../providers/Asset'
|
||||
import { useUserPreferences } from '../../../../providers/UserPreferences'
|
||||
import MetadataPreview from '../../../molecules/MetadataPreview'
|
||||
import Debug from './Debug'
|
||||
import { MetadataPreview } from '../../../molecules/MetadataPreview'
|
||||
import Debug from './DebugEditMetadata'
|
||||
import Web3Feedback from '../../../molecules/Wallet/Feedback'
|
||||
import FormEditMetadata from './FormEditMetadata'
|
||||
import { mapTimeoutStringToSeconds } from '../../../../utils/metadata'
|
||||
|
@ -65,6 +65,9 @@ export default function Edit({
|
|||
const [success, setSuccess] = useState<string>()
|
||||
const [error, setError] = useState<string>()
|
||||
const [timeoutStringValue, setTimeoutStringValue] = useState<string>()
|
||||
const timeout = ddo.findServiceByType('access')
|
||||
? ddo.findServiceByType('access').attributes.main.timeout
|
||||
: ddo.findServiceByType('compute').attributes.main.timeout
|
||||
|
||||
const hasFeedback = error || success
|
||||
|
||||
|
@ -103,7 +106,9 @@ export default function Edit({
|
|||
}
|
||||
let ddoEditedTimeout = ddoEditedMetdata
|
||||
if (timeoutStringValue !== values.timeout) {
|
||||
const service = ddoEditedMetdata.findServiceByType('access')
|
||||
const service =
|
||||
ddoEditedMetdata.findServiceByType('access') ||
|
||||
ddoEditedMetdata.findServiceByType('compute')
|
||||
const timeout = mapTimeoutStringToSeconds(values.timeout)
|
||||
ddoEditedTimeout = await ocean.assets.editServiceTimeout(
|
||||
ddoEditedMetdata,
|
||||
|
@ -139,11 +144,7 @@ export default function Edit({
|
|||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={getInitialValues(
|
||||
metadata,
|
||||
ddo.findServiceByType('access').attributes.main.timeout,
|
||||
price.value
|
||||
)}
|
||||
initialValues={getInitialValues(metadata, timeout, price.value)}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={async (values, { resetForm }) => {
|
||||
// move user's focus to top of screen
|
||||
|
|
|
@ -4,9 +4,3 @@
|
|||
margin: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.help {
|
||||
font-size: var(--font-size-mini);
|
||||
color: var(--color-secondary);
|
||||
margin-top: calc(var(--spacer) / 3);
|
||||
}
|
||||
|
|
|
@ -54,9 +54,9 @@ export default function AssetActions(): ReactElement {
|
|||
|
||||
const UseContent = isCompute ? (
|
||||
<Compute
|
||||
ddo={ddo}
|
||||
dtBalance={dtBalance}
|
||||
isBalanceSufficient={isBalanceSufficient}
|
||||
file={metadata?.main.files[0]}
|
||||
/>
|
||||
) : (
|
||||
<Consume
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
margin-top: var(--spacer);
|
||||
display: grid;
|
||||
gap: var(--spacer);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
}
|
||||
|
||||
.metaFull code {
|
||||
|
|
|
@ -5,7 +5,15 @@ import Publisher from '../../atoms/Publisher'
|
|||
import { useAsset } from '../../../providers/Asset'
|
||||
|
||||
export default function MetaFull(): ReactElement {
|
||||
const { ddo, metadata, isInPurgatory } = useAsset()
|
||||
const { ddo, metadata, isInPurgatory, type } = useAsset()
|
||||
|
||||
function DockerImage() {
|
||||
const algorithmContainer = ddo.findServiceByType('metadata').attributes.main
|
||||
.algorithm.container
|
||||
const { image } = algorithmContainer
|
||||
const { tag } = algorithmContainer
|
||||
return <span>{`${image}:${tag}`}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.metaFull}>
|
||||
|
@ -16,6 +24,10 @@ export default function MetaFull(): ReactElement {
|
|||
title="Owner"
|
||||
content={<Publisher account={ddo?.publicKey[0].owner} />}
|
||||
/>
|
||||
|
||||
{type === 'algorithm' && (
|
||||
<MetaItem title="Docker Image" content={<DockerImage />} />
|
||||
)}
|
||||
<MetaItem title="DID" content={<code>{ddo?.id}</code>} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -10,3 +10,8 @@
|
|||
color: var(--color-secondary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.content {
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ export default function MetaItem({
|
|||
return (
|
||||
<div className={styles.metaItem}>
|
||||
<h3 className={styles.title}>{title}</h3>
|
||||
{content}
|
||||
<div className={styles.content}>{content}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,36 @@
|
|||
.meta {
|
||||
margin-bottom: calc(var(--spacer) / 1.5);
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.meta p {
|
||||
margin-bottom: 0;
|
||||
.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);
|
||||
}
|
||||
|
||||
.date {
|
||||
@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);
|
||||
}
|
||||
|
||||
.byline {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.updated {
|
||||
font-size: var(--font-size-mini);
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
|
|
@ -5,14 +5,22 @@ import ExplorerLink from '../../atoms/ExplorerLink'
|
|||
import Publisher from '../../atoms/Publisher'
|
||||
import Time from '../../atoms/Time'
|
||||
import styles from './MetaMain.module.css'
|
||||
import AssetType from '../../atoms/AssetType'
|
||||
|
||||
export default function MetaMain(): ReactElement {
|
||||
const { ddo, owner } = useAsset()
|
||||
const { ddo, owner, type } = useAsset()
|
||||
const { networkId } = useWeb3()
|
||||
const isCompute = Boolean(ddo?.findServiceByType('compute'))
|
||||
const accessType = isCompute ? 'compute' : 'access'
|
||||
|
||||
return (
|
||||
<aside className={styles.meta}>
|
||||
<p>
|
||||
<header className={styles.asset}>
|
||||
<AssetType
|
||||
type={type}
|
||||
accessType={accessType}
|
||||
className={styles.assetType}
|
||||
/>
|
||||
<ExplorerLink
|
||||
networkId={networkId}
|
||||
path={
|
||||
|
@ -23,19 +31,22 @@ export default function MetaMain(): ReactElement {
|
|||
>
|
||||
{`${ddo?.dataTokenInfo.name} — ${ddo?.dataTokenInfo.symbol}`}
|
||||
</ExplorerLink>
|
||||
</p>
|
||||
<div>
|
||||
</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>
|
||||
<p className={styles.date}>
|
||||
<Time date={ddo?.created} relative />
|
||||
{ddo?.created !== ddo?.updated && (
|
||||
<>
|
||||
{' — '}
|
||||
updated <Time date={ddo?.updated} relative />
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Conversion from '../../../../atoms/Price/Conversion'
|
||||
import { useField } from 'formik'
|
||||
import React, { ReactElement } from 'react'
|
||||
import React, { ReactElement, useState, useEffect } from 'react'
|
||||
import Input from '../../../../atoms/Input'
|
||||
import styles from './Price.module.css'
|
||||
import Error from './Error'
|
||||
|
@ -16,7 +16,23 @@ export default function Price({
|
|||
firstPrice?: string
|
||||
}): ReactElement {
|
||||
const [field, meta] = useField('price')
|
||||
const { dtName, dtSymbol } = usePricing(ddo)
|
||||
const { getDTName, getDTSymbol } = usePricing()
|
||||
const [dtSymbol, setDtSymbol] = useState<string>()
|
||||
const [dtName, setDtName] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!ddo) return
|
||||
async function setDatatokenSymbol(ddo: DDO) {
|
||||
const dtSymbol = await getDTSymbol(ddo)
|
||||
setDtSymbol(dtSymbol)
|
||||
}
|
||||
async function setDatatokenName(ddo: DDO) {
|
||||
const dtName = await getDTName(ddo)
|
||||
setDtName(dtName)
|
||||
}
|
||||
setDatatokenSymbol(ddo)
|
||||
setDatatokenName(ddo)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.price}>
|
||||
|
|
|
@ -62,7 +62,7 @@ export default function Pricing({ ddo }: { ddo: DDO }): ReactElement {
|
|||
pricingIsLoading,
|
||||
pricingError,
|
||||
pricingStepText
|
||||
} = usePricing(ddo)
|
||||
} = usePricing()
|
||||
|
||||
const hasFeedback = pricingIsLoading || typeof success !== 'undefined'
|
||||
|
||||
|
@ -74,7 +74,7 @@ export default function Pricing({ ddo }: { ddo: DDO }): ReactElement {
|
|||
swapFee: `${values.swapFee / 100}`
|
||||
}
|
||||
|
||||
const tx = await createPricing(priceOptions)
|
||||
const tx = await createPricing(priceOptions, ddo)
|
||||
|
||||
// Pricing failed
|
||||
if (!tx || pricingError) {
|
||||
|
|
|
@ -42,3 +42,7 @@
|
|||
margin-left: calc(var(--spacer) / 4);
|
||||
margin-right: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { graphql, useStaticQuery } from 'gatsby'
|
|||
import Markdown from '../../atoms/Markdown'
|
||||
import MetaFull from './MetaFull'
|
||||
import MetaSecondary from './MetaSecondary'
|
||||
import styles from './index.module.css'
|
||||
import AssetActions from '../AssetActions'
|
||||
import { useUserPreferences } from '../../../providers/UserPreferences'
|
||||
import Pricing from './Pricing'
|
||||
|
@ -12,10 +11,12 @@ import { useAsset } from '../../../providers/Asset'
|
|||
import Alert from '../../atoms/Alert'
|
||||
import Button from '../../atoms/Button'
|
||||
import Edit from '../AssetActions/Edit'
|
||||
import EditComputeDataset from '../AssetActions/Edit/EditComputeDataset'
|
||||
import DebugOutput from '../../atoms/DebugOutput'
|
||||
import MetaMain from './MetaMain'
|
||||
import EditHistory from './EditHistory'
|
||||
import { useWeb3 } from '../../../providers/Web3'
|
||||
import styles from './index.module.css'
|
||||
|
||||
export interface AssetContentProps {
|
||||
path?: string
|
||||
|
@ -46,8 +47,9 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
|
|||
const { owner, isInPurgatory, purgatoryData } = useAsset()
|
||||
const [showPricing, setShowPricing] = useState(false)
|
||||
const [showEdit, setShowEdit] = useState<boolean>()
|
||||
const [showEditCompute, setShowEditCompute] = useState<boolean>()
|
||||
const [isOwner, setIsOwner] = useState(false)
|
||||
const { ddo, price, metadata } = useAsset()
|
||||
const { ddo, price, metadata, type } = useAsset()
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountId || !owner) return
|
||||
|
@ -63,8 +65,15 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
|
|||
setShowEdit(true)
|
||||
}
|
||||
|
||||
function handleEditComputeButton() {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||
setShowEditCompute(true)
|
||||
}
|
||||
|
||||
return showEdit ? (
|
||||
<Edit setShowEdit={setShowEdit} />
|
||||
) : showEditCompute ? (
|
||||
<EditComputeDataset setShowEdit={setShowEditCompute} />
|
||||
) : (
|
||||
<article className={styles.grid}>
|
||||
<div>
|
||||
|
@ -94,6 +103,18 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
|
|||
<Button style="text" size="small" onClick={handleEditButton}>
|
||||
Edit Metadata
|
||||
</Button>
|
||||
{ddo.findServiceByType('compute') && type === 'dataset' && (
|
||||
<>
|
||||
<span className={styles.separator}>|</span>
|
||||
<Button
|
||||
style="text"
|
||||
size="small"
|
||||
onClick={handleEditComputeButton}
|
||||
>
|
||||
Edit Compute Settings
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.title {
|
||||
margin-bottom: calc(var(--spacer) / 4);
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
import { Logger } from '@oceanprotocol/lib'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import Loader from '../../atoms/Loader'
|
||||
import Modal from '../../atoms/Modal'
|
||||
import { ComputeJobMetaData } from '../../../@types/ComputeJobMetaData'
|
||||
import Time from '../../atoms/Time'
|
||||
import shortid from 'shortid'
|
||||
import styles from './ComputeDetails.module.css'
|
||||
import { Status } from './ComputeJobs'
|
||||
import { ListItem } from '../../atoms/Lists'
|
||||
import { useOcean } from '../../../providers/Ocean'
|
||||
|
||||
export default function ComputeDetailsModal({
|
||||
computeJob,
|
||||
isOpen,
|
||||
onToggleModal
|
||||
}: {
|
||||
computeJob: ComputeJobMetaData
|
||||
isOpen: boolean
|
||||
onToggleModal: () => void
|
||||
}): ReactElement {
|
||||
const { ocean, account } = useOcean()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const isFinished = computeJob.dateFinished !== null
|
||||
|
||||
useEffect(() => {
|
||||
async function getDetails() {
|
||||
if (!account || !ocean || !computeJob || !isOpen || !isFinished) return
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const job = await ocean.compute.status(
|
||||
account,
|
||||
computeJob.did,
|
||||
undefined,
|
||||
undefined,
|
||||
computeJob.jobId
|
||||
)
|
||||
if (job?.length > 0) {
|
||||
computeJob.algorithmLogUrl = job[0].algorithmLogUrl
|
||||
// hack because ComputeJob returns resultsUrl instead of resultsUrls, issue created already
|
||||
computeJob.resultsUrls =
|
||||
(job[0] as any).resultsUrl !== '' ? (job[0] as any).resultsUrl : []
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
getDetails()
|
||||
}, [ocean, account, isOpen, computeJob, isFinished])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Compute job details"
|
||||
isOpen={isOpen}
|
||||
onToggleModal={onToggleModal}
|
||||
>
|
||||
<h3 className={styles.title}>{computeJob.assetName}</h3>
|
||||
<p>
|
||||
Created <Time date={computeJob.dateCreated} isUnix relative />
|
||||
{computeJob.dateFinished && (
|
||||
<>
|
||||
<br />
|
||||
Finished <Time date={computeJob.dateFinished} isUnix relative />
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<Status>{computeJob.statusText}</Status>
|
||||
|
||||
{isFinished &&
|
||||
(isLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<>
|
||||
<ul>
|
||||
<ListItem>
|
||||
{computeJob.algorithmLogUrl ? (
|
||||
<a
|
||||
href={computeJob.algorithmLogUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View Log
|
||||
</a>
|
||||
) : (
|
||||
'No logs found'
|
||||
)}
|
||||
</ListItem>
|
||||
|
||||
{computeJob.resultsUrls?.map((url, i) =>
|
||||
url ? (
|
||||
<ListItem key={shortid.generate()}>
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
View Result {i}
|
||||
</a>
|
||||
</ListItem>
|
||||
) : (
|
||||
'No results found.'
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
))}
|
||||
</Modal>
|
||||
)
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import Time from '../../atoms/Time'
|
||||
import styles from './ComputeJobs.module.css'
|
||||
import Button from '../../atoms/Button'
|
||||
import ComputeDetails from './ComputeDetails'
|
||||
import { ComputeJobMetaData } from '../../../@types/ComputeJobMetaData'
|
||||
import { Link } from 'gatsby'
|
||||
import { Logger } from '@oceanprotocol/lib'
|
||||
import Dotdotdot from 'react-dotdotdot'
|
||||
import Table from '../../atoms/Table'
|
||||
import { useOcean } from '../../../providers/Ocean'
|
||||
|
||||
function DetailsButton({ row }: { row: ComputeJobMetaData }): ReactElement {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button style="text" size="small" onClick={() => setIsDialogOpen(true)}>
|
||||
Show Details
|
||||
</Button>
|
||||
<ComputeDetails
|
||||
computeJob={row}
|
||||
isOpen={isDialogOpen}
|
||||
onToggleModal={() => setIsDialogOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function Status({ children }: { children: string }): ReactElement {
|
||||
return <div className={styles.status}>{children}</div>
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'Data Set',
|
||||
selector: function getAssetRow(row: ComputeJobMetaData) {
|
||||
return (
|
||||
<Dotdotdot clamp={2}>
|
||||
<Link to={`/asset/${row.did}`}>{row.assetName}</Link>
|
||||
</Dotdotdot>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Created',
|
||||
selector: function getTimeRow(row: ComputeJobMetaData) {
|
||||
return <Time date={row.dateCreated} isUnix relative />
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Finished',
|
||||
selector: function getTimeRow(row: ComputeJobMetaData) {
|
||||
return <Time date={row.dateFinished} isUnix relative />
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
selector: function getStatus(row: ComputeJobMetaData) {
|
||||
return <Status>{row.statusText}</Status>
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
selector: function getActions(row: ComputeJobMetaData) {
|
||||
return <DetailsButton row={row} />
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default function ComputeJobs(): ReactElement {
|
||||
const { ocean, account } = useOcean()
|
||||
const [jobs, setJobs] = useState<ComputeJobMetaData[]>()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
async function getTitle(did: string) {
|
||||
const ddo = await ocean.metadataCache.retrieveDDO(did)
|
||||
const metadata = ddo.findServiceByType('metadata')
|
||||
return metadata.attributes.main.name
|
||||
}
|
||||
|
||||
async function getJobs() {
|
||||
if (!ocean || !account) return
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const orderHistory = await ocean.assets.getOrderHistory(
|
||||
account,
|
||||
'compute',
|
||||
100
|
||||
)
|
||||
const jobs: ComputeJobMetaData[] = []
|
||||
|
||||
for (let i = 0; i < orderHistory.length; i++) {
|
||||
const assetName = await getTitle(orderHistory[i].did)
|
||||
const computeJob = await ocean.compute.status(
|
||||
account,
|
||||
orderHistory[i].did,
|
||||
undefined,
|
||||
undefined,
|
||||
orderHistory[i].transactionHash,
|
||||
undefined,
|
||||
false
|
||||
)
|
||||
computeJob.forEach((item) => {
|
||||
jobs.push({
|
||||
did: orderHistory[i].did,
|
||||
jobId: item.jobId,
|
||||
dateCreated: item.dateCreated,
|
||||
dateFinished: item.dateFinished,
|
||||
assetName: assetName,
|
||||
status: item.status,
|
||||
statusText: item.statusText,
|
||||
algorithmLogUrl: '',
|
||||
resultsUrls: []
|
||||
})
|
||||
})
|
||||
}
|
||||
const jobsSorted = jobs.sort((a, b) => {
|
||||
if (a.dateCreated > b.dateCreated) return -1
|
||||
if (a.dateCreated < b.dateCreated) return 1
|
||||
return 0
|
||||
})
|
||||
setJobs(jobsSorted)
|
||||
} catch (error) {
|
||||
Logger.log(error.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
getJobs()
|
||||
}, [ocean, account])
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
data={jobs}
|
||||
isLoading={isLoading}
|
||||
defaultSortField="row.dateCreated"
|
||||
defaultSortAsc={false}
|
||||
/>
|
||||
)
|
||||
}
|
72
src/components/pages/History/ComputeJobs/Details.module.css
Normal file
72
src/components/pages/History/ComputeJobs/Details.module.css
Normal file
|
@ -0,0 +1,72 @@
|
|||
.main {
|
||||
margin-bottom: var(--spacer);
|
||||
}
|
||||
|
||||
.main > div:first-child {
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.asset {
|
||||
composes: box from '../../../atoms/Box.module.css';
|
||||
box-shadow: none;
|
||||
padding: calc(var(--spacer) / 2);
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.asset + .asset {
|
||||
margin-left: var(--spacer);
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.asset + .asset:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
top: -1.15rem;
|
||||
bottom: 3px;
|
||||
width: 1px;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.asset p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.asset code {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.assetTitle {
|
||||
margin-bottom: 0;
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--font-color-text);
|
||||
}
|
||||
|
||||
.assetLink {
|
||||
display: inline-block;
|
||||
margin-left: calc(var(--spacer) / 8);
|
||||
}
|
||||
|
||||
.assetLink svg {
|
||||
margin: 0;
|
||||
fill: var(--color-primary);
|
||||
width: 0.6em;
|
||||
height: 0.6em;
|
||||
}
|
||||
|
||||
.assetMeta,
|
||||
.assetMeta code {
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: grid;
|
||||
gap: var(--spacer);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin-top: calc(var(--spacer) * 1.5);
|
||||
}
|
119
src/components/pages/History/ComputeJobs/Details.tsx
Normal file
119
src/components/pages/History/ComputeJobs/Details.tsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import { ComputeJobMetaData } from '../../../../@types/ComputeJobMetaData'
|
||||
import Time from '../../../atoms/Time'
|
||||
import Button from '../../../atoms/Button'
|
||||
import Modal from '../../../atoms/Modal'
|
||||
import MetaItem from '../../../organisms/AssetContent/MetaItem'
|
||||
import { ReactComponent as External } from '../../../../images/external.svg'
|
||||
import { retrieveDDO } from '../../../../utils/aquarius'
|
||||
import { useOcean } from '../../../../providers/Ocean'
|
||||
import Results from './Results'
|
||||
import styles from './Details.module.css'
|
||||
|
||||
function Asset({
|
||||
title,
|
||||
symbol,
|
||||
did
|
||||
}: {
|
||||
title: string
|
||||
symbol: string
|
||||
did: string
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.asset}>
|
||||
<h3 className={styles.assetTitle}>
|
||||
{title}{' '}
|
||||
<a
|
||||
className={styles.assetLink}
|
||||
href={`/asset/${did}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<External />
|
||||
</a>
|
||||
</h3>
|
||||
<p className={styles.assetMeta}>
|
||||
{symbol} | <code>{did}</code>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DetailsAssets({ job }: { job: ComputeJobMetaData }) {
|
||||
const { config } = useOcean()
|
||||
const [algoName, setAlgoName] = useState<string>()
|
||||
const [algoDtSymbol, setAlgoDtSymbol] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
async function getAlgoMetadata() {
|
||||
const source = axios.CancelToken.source()
|
||||
|
||||
const ddo = await retrieveDDO(
|
||||
job.algoDID,
|
||||
config.metadataCacheUri,
|
||||
source.token
|
||||
)
|
||||
setAlgoDtSymbol(ddo.dataTokenInfo.symbol)
|
||||
|
||||
const { attributes } = ddo.findServiceByType('metadata')
|
||||
setAlgoName(attributes?.main.name)
|
||||
}
|
||||
getAlgoMetadata()
|
||||
}, [config?.metadataCacheUri, job.algoDID])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Asset
|
||||
title={job.assetName}
|
||||
symbol={job.assetDtSymbol}
|
||||
did={job.inputDID[0]}
|
||||
/>
|
||||
<Asset title={algoName} symbol={algoDtSymbol} did={job.algoDID} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Details({
|
||||
job
|
||||
}: {
|
||||
job: ComputeJobMetaData
|
||||
}): ReactElement {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button style="text" size="small" onClick={() => setIsDialogOpen(true)}>
|
||||
Show Details
|
||||
</Button>
|
||||
<Modal
|
||||
title={job.statusText}
|
||||
isOpen={isDialogOpen}
|
||||
onToggleModal={() => setIsDialogOpen(false)}
|
||||
>
|
||||
<DetailsAssets job={job} />
|
||||
<Results job={job} />
|
||||
|
||||
<div className={styles.meta}>
|
||||
<MetaItem
|
||||
title="Created"
|
||||
content={<Time date={job.dateCreated} isUnix relative />}
|
||||
/>
|
||||
{job.dateFinished && (
|
||||
<MetaItem
|
||||
title="Finished"
|
||||
content={<Time date={job.dateFinished} isUnix relative />}
|
||||
/>
|
||||
)}
|
||||
<MetaItem title="Job ID" content={<code>{job.jobId}</code>} />
|
||||
{job.resultsDid && (
|
||||
<MetaItem
|
||||
title="Published Results DID"
|
||||
content={<code>{job.resultsDid}</code>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
.results {
|
||||
composes: asset from './Details.module.css';
|
||||
border-bottom-left-radius: var(--border-radius) !important;
|
||||
}
|
||||
|
||||
.help {
|
||||
margin-top: calc(var(--spacer) / 3);
|
||||
}
|
112
src/components/pages/History/ComputeJobs/Results.tsx
Normal file
112
src/components/pages/History/ComputeJobs/Results.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import { Logger } from '@oceanprotocol/lib'
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import Loader from '../../../atoms/Loader'
|
||||
import { ComputeJobMetaData } from '../../../../@types/ComputeJobMetaData'
|
||||
import { ListItem } from '../../../atoms/Lists'
|
||||
import Button from '../../../atoms/Button'
|
||||
import { useOcean } from '../../../../providers/Ocean'
|
||||
import styles from './Results.module.css'
|
||||
import FormHelp from '../../../atoms/Input/Help'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
|
||||
export const contentQuery = graphql`
|
||||
query HistoryPageComputeResultsQuery {
|
||||
content: allFile(filter: { relativePath: { eq: "pages/history.json" } }) {
|
||||
edges {
|
||||
node {
|
||||
childPagesJson {
|
||||
compute {
|
||||
storage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function Results({
|
||||
job
|
||||
}: {
|
||||
job: ComputeJobMetaData
|
||||
}): ReactElement {
|
||||
const data = useStaticQuery(contentQuery)
|
||||
const content = data.content.edges[0].node.childPagesJson
|
||||
|
||||
const { ocean, account } = useOcean()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [hasFetched, setHasFetched] = useState(false)
|
||||
const isFinished = job.dateFinished !== null
|
||||
|
||||
async function getResults() {
|
||||
if (!account || !ocean || !job) return
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const jobStatus = await ocean.compute.status(
|
||||
account,
|
||||
job.did,
|
||||
undefined,
|
||||
undefined,
|
||||
job.jobId
|
||||
)
|
||||
if (jobStatus?.length > 0) {
|
||||
job.algorithmLogUrl = jobStatus[0].algorithmLogUrl
|
||||
job.resultsUrl = jobStatus[0].resultsUrl
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setHasFetched(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.results}>
|
||||
{hasFetched ? (
|
||||
<ul>
|
||||
<ListItem>
|
||||
{job.algorithmLogUrl ? (
|
||||
<a href={job.algorithmLogUrl} target="_blank" rel="noreferrer">
|
||||
View Log
|
||||
</a>
|
||||
) : (
|
||||
'No logs found.'
|
||||
)}
|
||||
</ListItem>
|
||||
|
||||
{job.resultsUrl &&
|
||||
Array.isArray(job.resultsUrl) &&
|
||||
job.resultsUrl.map((url, i) =>
|
||||
url ? (
|
||||
<ListItem key={job.jobId}>
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
View Result {i + 1}
|
||||
</a>
|
||||
</ListItem>
|
||||
) : (
|
||||
<ListItem>No results found.</ListItem>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
) : (
|
||||
<Button
|
||||
style="primary"
|
||||
size="small"
|
||||
onClick={() => getResults()}
|
||||
disabled={isLoading || !isFinished}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader />
|
||||
) : !isFinished ? (
|
||||
'Waiting for results...'
|
||||
) : (
|
||||
'Get Results'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<FormHelp className={styles.help}>{content.compute.storage}</FormHelp>
|
||||
</div>
|
||||
)
|
||||
}
|
245
src/components/pages/History/ComputeJobs/index.tsx
Normal file
245
src/components/pages/History/ComputeJobs/index.tsx
Normal file
|
@ -0,0 +1,245 @@
|
|||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import web3 from 'web3'
|
||||
import Time from '../../../atoms/Time'
|
||||
import { Link } from 'gatsby'
|
||||
import { DDO, Logger, Service, Provider } from '@oceanprotocol/lib'
|
||||
import { ComputeJobMetaData } from '../../../../@types/ComputeJobMetaData'
|
||||
import Dotdotdot from 'react-dotdotdot'
|
||||
import Table from '../../../atoms/Table'
|
||||
import { useOcean } from '../../../../providers/Ocean'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import { useWeb3 } from '../../../../providers/Web3'
|
||||
import { queryMetadata } from '../../../../utils/aquarius'
|
||||
import axios, { CancelToken } from 'axios'
|
||||
import { ComputeOrders } from '../../../../@types/apollo/ComputeOrders'
|
||||
import Details from './Details'
|
||||
import styles from './index.module.css'
|
||||
import { ComputeJob } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
|
||||
|
||||
const getComputeOrders = gql`
|
||||
query ComputeOrders($user: String!) {
|
||||
tokenOrders(
|
||||
orderBy: timestamp
|
||||
orderDirection: desc
|
||||
where: { payer: $user }
|
||||
) {
|
||||
id
|
||||
serviceId
|
||||
datatokenId {
|
||||
address
|
||||
}
|
||||
tx
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export function Status({ children }: { children: string }): ReactElement {
|
||||
return <div className={styles.status}>{children}</div>
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'Data Set',
|
||||
selector: function getAssetRow(row: ComputeJobMetaData) {
|
||||
return (
|
||||
<Dotdotdot clamp={2}>
|
||||
<Link to={`/asset/${row.inputDID[0]}`}>{row.assetName}</Link>
|
||||
</Dotdotdot>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Created',
|
||||
selector: function getTimeRow(row: ComputeJobMetaData) {
|
||||
return <Time date={row.dateCreated} isUnix relative />
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Finished',
|
||||
selector: function getTimeRow(row: ComputeJobMetaData) {
|
||||
return <Time date={row.dateFinished} isUnix relative />
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
selector: function getStatus(row: ComputeJobMetaData) {
|
||||
return <Status>{row.statusText}</Status>
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
selector: function getActions(row: ComputeJobMetaData) {
|
||||
return <Details job={row} />
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
async function getAssetMetadata(
|
||||
queryDtList: string,
|
||||
metadataCacheUri: string,
|
||||
cancelToken: CancelToken
|
||||
): Promise<DDO[]> {
|
||||
const queryDid = {
|
||||
page: 1,
|
||||
offset: 100,
|
||||
query: {
|
||||
query_string: {
|
||||
query: `(${queryDtList}) AND service.attributes.main.type:dataset AND service.type:compute`,
|
||||
fields: ['dataToken']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await queryMetadata(queryDid, metadataCacheUri, cancelToken)
|
||||
|
||||
return result.results
|
||||
}
|
||||
|
||||
export default function ComputeJobs(): ReactElement {
|
||||
const { ocean, account, config } = useOcean()
|
||||
const { accountId } = useWeb3()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
|
||||
const { data } = useQuery<ComputeOrders>(getComputeOrders, {
|
||||
variables: {
|
||||
user: accountId?.toLowerCase()
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (data === undefined || !config?.metadataCacheUri) return
|
||||
|
||||
async function getJobs() {
|
||||
if (!ocean || !account) return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
const dtList = []
|
||||
const computeJobs: ComputeJobMetaData[] = []
|
||||
for (let i = 0; i < data.tokenOrders.length; i++) {
|
||||
dtList.push(data.tokenOrders[i].datatokenId.address)
|
||||
}
|
||||
const queryDtList = JSON.stringify(dtList)
|
||||
.replace(/,/g, ' ')
|
||||
.replace(/"/g, '')
|
||||
.replace(/(\[|\])/g, '')
|
||||
|
||||
try {
|
||||
const source = axios.CancelToken.source()
|
||||
const assets = await getAssetMetadata(
|
||||
queryDtList,
|
||||
config.metadataCacheUri,
|
||||
source.token
|
||||
)
|
||||
const providers: Provider[] = []
|
||||
const serviceEndpoints: string[] = []
|
||||
for (let i = 0; i < data.tokenOrders.length; i++) {
|
||||
try {
|
||||
const did = web3.utils
|
||||
.toChecksumAddress(data.tokenOrders[i].datatokenId.address)
|
||||
.replace('0x', 'did:op:')
|
||||
|
||||
const ddo = assets.filter((x) => x.id === did)[0]
|
||||
|
||||
if (!ddo) continue
|
||||
|
||||
const service = ddo.service.filter(
|
||||
(x: Service) => x.index === data.tokenOrders[i].serviceId
|
||||
)[0]
|
||||
|
||||
if (!service || service.type !== 'compute') continue
|
||||
const { serviceEndpoint } = service
|
||||
|
||||
const wasProviderQueried =
|
||||
serviceEndpoints.filter((x) => x === serviceEndpoint).length > 0
|
||||
|
||||
if (wasProviderQueried) continue
|
||||
serviceEndpoints.push(serviceEndpoint)
|
||||
} catch (err) {
|
||||
Logger.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (let i = 0; i < serviceEndpoints.length; i++) {
|
||||
const instanceConfig = {
|
||||
config,
|
||||
web3: config.web3Provider,
|
||||
logger: Logger,
|
||||
ocean: ocean
|
||||
}
|
||||
const provider = await Provider.getInstance(instanceConfig)
|
||||
await provider.setBaseUrl(serviceEndpoints[i])
|
||||
const hasSameCompute =
|
||||
providers.filter(
|
||||
(x) => x.computeAddress === provider.computeAddress
|
||||
).length > 0
|
||||
if (!hasSameCompute) providers.push(provider)
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(err)
|
||||
}
|
||||
for (let i = 0; i < providers.length; i++) {
|
||||
try {
|
||||
const providerComputeJobs = (await providers[i].computeStatus(
|
||||
'',
|
||||
account,
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
)) as ComputeJob[]
|
||||
|
||||
// means the provider uri is not good, so we ignore it and move on
|
||||
if (!providerComputeJobs) continue
|
||||
providerComputeJobs.sort((a, b) => {
|
||||
if (a.dateCreated > b.dateCreated) {
|
||||
return -1
|
||||
}
|
||||
if (a.dateCreated < b.dateCreated) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
for (let j = 0; j < providerComputeJobs.length; j++) {
|
||||
const job = providerComputeJobs[j]
|
||||
const did = job.inputDID[0]
|
||||
const ddo = assets.filter((x) => x.id === did)[0]
|
||||
|
||||
if (!ddo) continue
|
||||
const serviceMetadata = ddo.service.filter(
|
||||
(x: Service) => x.type === 'metadata'
|
||||
)[0]
|
||||
|
||||
const compJob: ComputeJobMetaData = {
|
||||
...job,
|
||||
assetName: serviceMetadata.attributes.main.name,
|
||||
assetDtSymbol: ddo.dataTokenInfo.symbol
|
||||
}
|
||||
computeJobs.push(compJob)
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(err)
|
||||
}
|
||||
}
|
||||
setJobs(computeJobs)
|
||||
} catch (error) {
|
||||
Logger.log(error.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
getJobs()
|
||||
}, [ocean, account, data, config?.metadataCacheUri])
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
data={jobs}
|
||||
isLoading={isLoading}
|
||||
defaultSortField="row.dateCreated"
|
||||
defaultSortAsc={false}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -18,11 +18,7 @@
|
|||
|
||||
.content {
|
||||
margin-top: var(--spacer);
|
||||
background-color: var(--background-content) !important;
|
||||
}
|
||||
|
||||
.tabs div[class*='tabs'] {
|
||||
background-color: var(--background-content);
|
||||
background-color: var(--background-body);
|
||||
}
|
||||
|
||||
.tabs ul[class*='tabList'] {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import ComputeJobs from './ComputeJobs'
|
||||
import styles from './index.module.css'
|
||||
import Tabs from '../../atoms/Tabs'
|
||||
import PoolShares from './PoolShares'
|
||||
import PoolTransactions from '../../molecules/PoolTransactions'
|
||||
import PublishedList from './PublishedList'
|
||||
import Downloads from './Downloads'
|
||||
import Tabs from '../../atoms/Tabs'
|
||||
import ComputeJobs from './ComputeJobs'
|
||||
import styles from './index.module.css'
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
|
|
|
@ -111,7 +111,7 @@ export default function HomePage(): ReactElement {
|
|||
style="text"
|
||||
to="/search?priceType=pool&sort=liquidity&sortOrder=desc"
|
||||
>
|
||||
All data sets with pool →
|
||||
Data sets and algorithms with pool →
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
@ -121,7 +121,7 @@ export default function HomePage(): ReactElement {
|
|||
query={queryLatest}
|
||||
action={
|
||||
<Button style="text" to="/search?sort=created&sortOrder=desc">
|
||||
All data sets →
|
||||
All data sets and algorithms →
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import { MetadataPublishForm } from '../../../@types/MetaData'
|
||||
import { MetadataPublishFormDataset } from '../../../@types/MetaData'
|
||||
import DebugOutput from '../../atoms/DebugOutput'
|
||||
import styles from './index.module.css'
|
||||
import { transformPublishFormToMetadata } from '../../../utils/metadata'
|
||||
|
@ -7,7 +7,7 @@ import { transformPublishFormToMetadata } from '../../../utils/metadata'
|
|||
export default function Debug({
|
||||
values
|
||||
}: {
|
||||
values: Partial<MetadataPublishForm>
|
||||
values: Partial<MetadataPublishFormDataset>
|
||||
}): ReactElement {
|
||||
const ddo = {
|
||||
'@context': 'https://w3id.org/did/v1',
|
||||
|
|
163
src/components/pages/Publish/FormAlgoPublish.tsx
Normal file
163
src/components/pages/Publish/FormAlgoPublish.tsx
Normal file
|
@ -0,0 +1,163 @@
|
|||
import React, {
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
FormEvent,
|
||||
ChangeEvent
|
||||
} from 'react'
|
||||
import { useStaticQuery, graphql } from 'gatsby'
|
||||
import styles from './FormPublish.module.css'
|
||||
import { useOcean } from '../../../providers/Ocean'
|
||||
import { useFormikContext, Field, Form, FormikContextType } from 'formik'
|
||||
import Input from '../../atoms/Input'
|
||||
import Button from '../../atoms/Button'
|
||||
import { FormContent, FormFieldProps } from '../../../@types/Form'
|
||||
import { MetadataPublishFormAlgorithm } from '../../../@types/MetaData'
|
||||
import { initialValues as initialValuesAlgorithm } from '../../../models/FormAlgoPublish'
|
||||
|
||||
import stylesIndex from './index.module.css'
|
||||
|
||||
const query = graphql`
|
||||
query {
|
||||
content: allFile(
|
||||
filter: { relativePath: { eq: "pages/publish/form-algorithm.json" } }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
childPublishJson {
|
||||
title
|
||||
data {
|
||||
name
|
||||
placeholder
|
||||
label
|
||||
help
|
||||
type
|
||||
required
|
||||
sortOptions
|
||||
options
|
||||
}
|
||||
warning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function FormPublish(): ReactElement {
|
||||
const data = useStaticQuery(query)
|
||||
const content: FormContent = data.content.edges[0].node.childPublishJson
|
||||
const { ocean, account } = useOcean()
|
||||
const {
|
||||
status,
|
||||
setStatus,
|
||||
isValid,
|
||||
setErrors,
|
||||
setTouched,
|
||||
resetForm,
|
||||
initialValues,
|
||||
validateField,
|
||||
setFieldValue
|
||||
}: FormikContextType<MetadataPublishFormAlgorithm> = useFormikContext()
|
||||
const [selectedDockerImage, setSelectedDockerImage] = useState<string>(
|
||||
initialValues.dockerImage
|
||||
)
|
||||
|
||||
// reset form validation on every mount
|
||||
useEffect(() => {
|
||||
setErrors({})
|
||||
setTouched({})
|
||||
}, [setErrors, setTouched])
|
||||
|
||||
function handleImageSelectChange(imageSelected: string) {
|
||||
switch (imageSelected) {
|
||||
case 'node:latest': {
|
||||
setFieldValue('image', 'node')
|
||||
setFieldValue('containerTag', 'latest')
|
||||
setFieldValue('entrypoint', 'node $ALGO')
|
||||
break
|
||||
}
|
||||
case 'python:latest': {
|
||||
setFieldValue('image', 'oceanprotocol/algo_dockers')
|
||||
setFieldValue('containerTag', 'python-panda')
|
||||
setFieldValue('entrypoint', 'python $ALGO')
|
||||
break
|
||||
}
|
||||
default: {
|
||||
setFieldValue('image', '')
|
||||
setFieldValue('containerTag', '')
|
||||
setFieldValue('entrypoint', '')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manually handle change events instead of using `handleChange` from Formik.
|
||||
// Workaround for default `validateOnChange` not kicking in
|
||||
function handleFieldChange(
|
||||
e: ChangeEvent<HTMLInputElement>,
|
||||
field: FormFieldProps
|
||||
) {
|
||||
const value =
|
||||
field.type === 'checkbox' || field.type === 'terms'
|
||||
? !JSON.parse(e.target.value)
|
||||
: e.target.value
|
||||
if (field.name === 'dockerImage') {
|
||||
setSelectedDockerImage(e.target.value)
|
||||
handleImageSelectChange(e.target.value)
|
||||
}
|
||||
validateField(field.name)
|
||||
setFieldValue(field.name, value)
|
||||
}
|
||||
|
||||
const resetFormAndClearStorage = (e: FormEvent<Element>) => {
|
||||
e.preventDefault()
|
||||
resetForm({
|
||||
values: initialValuesAlgorithm as MetadataPublishFormAlgorithm,
|
||||
status: 'empty'
|
||||
})
|
||||
setStatus('empty')
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
className={styles.form}
|
||||
// do we need this?
|
||||
onChange={() => status === 'empty' && setStatus(null)}
|
||||
>
|
||||
<h2 className={stylesIndex.formTitle}>{content.title}</h2>
|
||||
{content.data.map(
|
||||
(field: FormFieldProps) =>
|
||||
((field.name !== 'entrypoint' &&
|
||||
field.name !== 'image' &&
|
||||
field.name !== 'containerTag') ||
|
||||
selectedDockerImage === 'custom image') && (
|
||||
<Field
|
||||
key={field.name}
|
||||
{...field}
|
||||
component={Input}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
handleFieldChange(e, field)
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
<footer className={styles.actions}>
|
||||
<Button
|
||||
style="primary"
|
||||
type="submit"
|
||||
disabled={!ocean || !account || !isValid || status === 'empty'}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
{status !== 'empty' && (
|
||||
<Button style="text" size="small" onClick={resetFormAndClearStorage}>
|
||||
Reset Form
|
||||
</Button>
|
||||
)}
|
||||
</footer>
|
||||
</Form>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
.form {
|
||||
composes: box from '../../atoms/Box.module.css';
|
||||
margin-bottom: var(--spacer);
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
|
|
@ -1,17 +1,45 @@
|
|||
import React, { ReactElement, useEffect, FormEvent, ChangeEvent } from 'react'
|
||||
import styles from './FormPublish.module.css'
|
||||
import { useStaticQuery, graphql } from 'gatsby'
|
||||
import { useFormikContext, Field, Form, FormikContextType } from 'formik'
|
||||
import Input from '../../atoms/Input'
|
||||
import Button from '../../atoms/Button'
|
||||
import { FormContent, FormFieldProps } from '../../../@types/Form'
|
||||
import { MetadataPublishForm } from '../../../@types/MetaData'
|
||||
import { MetadataPublishFormDataset } from '../../../@types/MetaData'
|
||||
import { initialValues as initialValuesDataset } from '../../../models/FormAlgoPublish'
|
||||
import { useOcean } from '../../../providers/Ocean'
|
||||
import stylesIndex from './index.module.css'
|
||||
import styles from './FormPublish.module.css'
|
||||
|
||||
export default function FormPublish({
|
||||
content
|
||||
}: {
|
||||
content: FormContent
|
||||
}): ReactElement {
|
||||
const query = graphql`
|
||||
query {
|
||||
content: allFile(
|
||||
filter: { relativePath: { eq: "pages/publish/form-dataset.json" } }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
childPublishJson {
|
||||
title
|
||||
data {
|
||||
name
|
||||
placeholder
|
||||
label
|
||||
help
|
||||
type
|
||||
required
|
||||
sortOptions
|
||||
options
|
||||
}
|
||||
warning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function FormPublish(): ReactElement {
|
||||
const data = useStaticQuery(query)
|
||||
const content: FormContent = data.content.edges[0].node.childPublishJson
|
||||
const { ocean, account } = useOcean()
|
||||
const {
|
||||
status,
|
||||
|
@ -23,7 +51,7 @@ export default function FormPublish({
|
|||
initialValues,
|
||||
validateField,
|
||||
setFieldValue
|
||||
}: FormikContextType<MetadataPublishForm> = useFormikContext()
|
||||
}: FormikContextType<MetadataPublishFormDataset> = useFormikContext()
|
||||
|
||||
// reset form validation on every mount
|
||||
useEffect(() => {
|
||||
|
@ -48,7 +76,10 @@ export default function FormPublish({
|
|||
|
||||
const resetFormAndClearStorage = (e: FormEvent<Element>) => {
|
||||
e.preventDefault()
|
||||
resetForm({ values: initialValues, status: 'empty' })
|
||||
resetForm({
|
||||
values: initialValuesDataset as MetadataPublishFormDataset,
|
||||
status: 'empty'
|
||||
})
|
||||
setStatus('empty')
|
||||
}
|
||||
|
||||
|
@ -58,6 +89,7 @@ export default function FormPublish({
|
|||
// do we need this?
|
||||
onChange={() => status === 'empty' && setStatus(null)}
|
||||
>
|
||||
<h2 className={stylesIndex.formTitle}>{content.title}</h2>
|
||||
{content.data.map((field: FormFieldProps) => (
|
||||
<Field
|
||||
key={field.name}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
.tabs ul[class*='tabList'] {
|
||||
background-color: var(--background-content);
|
||||
border: 1px solid var(--border-color);
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.tabs div[class*='tabContent'] {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: calc(var(--spacer) * 1.5);
|
||||
|
@ -16,8 +29,17 @@ div.alert {
|
|||
grid-template-columns: 1.618fr 1fr;
|
||||
}
|
||||
|
||||
.tabs ul[class*='tabList'] {
|
||||
/* fake the above 1.618fr column */
|
||||
max-width: calc((100% / 1.618) - calc(var(--spacer) / 1.075));
|
||||
}
|
||||
|
||||
.sticky {
|
||||
position: sticky;
|
||||
top: calc(var(--spacer) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.formTitle {
|
||||
font-size: var(--font-size-h4);
|
||||
}
|
||||
|
|
|
@ -1,19 +1,32 @@
|
|||
import React, { ReactElement, useState } from 'react'
|
||||
import { Formik } from 'formik'
|
||||
import React, { ReactElement, useState, useEffect } from 'react'
|
||||
import { Formik, FormikState } from 'formik'
|
||||
import { usePublish } from '../../../hooks/usePublish'
|
||||
import styles from './index.module.css'
|
||||
import FormPublish from './FormPublish'
|
||||
import FormAlgoPublish from './FormAlgoPublish'
|
||||
import Web3Feedback from '../../molecules/Wallet/Feedback'
|
||||
import { FormContent } from '../../../@types/Form'
|
||||
import Tabs from '../../atoms/Tabs'
|
||||
import { initialValues, validationSchema } from '../../../models/FormPublish'
|
||||
import {
|
||||
initialValues as initialValuesAlgorithm,
|
||||
validationSchema as validationSchemaAlgorithm
|
||||
} from '../../../models/FormAlgoPublish'
|
||||
import {
|
||||
transformPublishFormToMetadata,
|
||||
mapTimeoutStringToSeconds
|
||||
transformPublishAlgorithmFormToMetadata,
|
||||
mapTimeoutStringToSeconds,
|
||||
validateDockerImage
|
||||
} from '../../../utils/metadata'
|
||||
import MetadataPreview from '../../molecules/MetadataPreview'
|
||||
import { MetadataPublishForm } from '../../../@types/MetaData'
|
||||
import {
|
||||
MetadataPreview,
|
||||
MetadataAlgorithmPreview
|
||||
} from '../../molecules/MetadataPreview'
|
||||
import {
|
||||
MetadataPublishFormDataset,
|
||||
MetadataPublishFormAlgorithm
|
||||
} from '../../../@types/MetaData'
|
||||
import { useUserPreferences } from '../../../providers/UserPreferences'
|
||||
import { Logger, Metadata } from '@oceanprotocol/lib'
|
||||
import { Logger, Metadata, MetadataMain } from '@oceanprotocol/lib'
|
||||
import { Persist } from '../../atoms/FormikPersist'
|
||||
import Debug from './Debug'
|
||||
import Alert from '../../atoms/Alert'
|
||||
|
@ -21,12 +34,39 @@ import MetadataFeedback from '../../molecules/MetadataFeedback'
|
|||
import { useAccountPurgatory } from '../../../hooks/useAccountPurgatory'
|
||||
import { useWeb3 } from '../../../providers/Web3'
|
||||
|
||||
const formName = 'ocean-publish-form'
|
||||
const formNameDatasets = 'ocean-publish-form-datasets'
|
||||
const formNameAlgorithms = 'ocean-publish-form-algorithms'
|
||||
|
||||
function TabContent({
|
||||
publishType,
|
||||
values
|
||||
}: {
|
||||
publishType: MetadataMain['type']
|
||||
values: Partial<MetadataPublishFormAlgorithm | MetadataPublishFormDataset>
|
||||
}) {
|
||||
return (
|
||||
<article className={styles.grid}>
|
||||
{publishType === 'dataset' ? <FormPublish /> : <FormAlgoPublish />}
|
||||
|
||||
<aside>
|
||||
<div className={styles.sticky}>
|
||||
{publishType === 'dataset' ? (
|
||||
<MetadataPreview values={values} />
|
||||
) : (
|
||||
<MetadataAlgorithmPreview values={values} />
|
||||
)}
|
||||
|
||||
<Web3Feedback />
|
||||
</div>
|
||||
</aside>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PublishPage({
|
||||
content
|
||||
}: {
|
||||
content: { warning: string; form: FormContent }
|
||||
content: { warning: string }
|
||||
}): ReactElement {
|
||||
const { debug } = useUserPreferences()
|
||||
const { accountId } = useWeb3()
|
||||
|
@ -34,13 +74,54 @@ export default function PublishPage({
|
|||
const { publish, publishError, isLoading, publishStepText } = usePublish()
|
||||
const [success, setSuccess] = useState<string>()
|
||||
const [error, setError] = useState<string>()
|
||||
const [title, setTitle] = useState<string>()
|
||||
const [did, setDid] = useState<string>()
|
||||
|
||||
const [algoInitialValues, setAlgoInitialValues] = useState<
|
||||
Partial<MetadataPublishFormAlgorithm>
|
||||
>(
|
||||
(localStorage.getItem('ocean-publish-form-algorithms') &&
|
||||
(JSON.parse(localStorage.getItem('ocean-publish-form-algorithms'))
|
||||
.initialValues as MetadataPublishFormAlgorithm)) ||
|
||||
initialValuesAlgorithm
|
||||
)
|
||||
const [datasetInitialValues, setdatasetInitialValues] = useState<
|
||||
Partial<MetadataPublishFormDataset>
|
||||
>(
|
||||
(localStorage.getItem('ocean-publish-form-datasets') &&
|
||||
(JSON.parse(localStorage.getItem('ocean-publish-form-datasets'))
|
||||
.initialValues as MetadataPublishFormDataset)) ||
|
||||
initialValues
|
||||
)
|
||||
const [publishType, setPublishType] = useState<MetadataMain['type']>(
|
||||
'dataset'
|
||||
)
|
||||
const hasFeedback = isLoading || error || success
|
||||
|
||||
const emptyAlgoDT = Object.values(algoInitialValues.dataTokenOptions).every(
|
||||
(value) => value === ''
|
||||
)
|
||||
const emptyDatasetDT = Object.values(
|
||||
datasetInitialValues.dataTokenOptions
|
||||
).every((value) => value === '')
|
||||
|
||||
if (emptyAlgoDT) {
|
||||
algoInitialValues.dataTokenOptions = datasetInitialValues.dataTokenOptions
|
||||
} else {
|
||||
if (emptyDatasetDT)
|
||||
datasetInitialValues.dataTokenOptions = algoInitialValues.dataTokenOptions
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
publishType === 'dataset'
|
||||
? setTitle('Publishing Data Set')
|
||||
: setTitle('Publishing Algorithm')
|
||||
}, [publishType])
|
||||
|
||||
async function handleSubmit(
|
||||
values: Partial<MetadataPublishForm>,
|
||||
resetForm: () => void
|
||||
values: Partial<MetadataPublishFormDataset>,
|
||||
resetForm: (
|
||||
nextState?: Partial<FormikState<Partial<MetadataPublishFormDataset>>>
|
||||
) => void
|
||||
): Promise<void> {
|
||||
const metadata = transformPublishFormToMetadata(values)
|
||||
const timeout = mapTimeoutStringToSeconds(values.timeout)
|
||||
|
@ -74,7 +155,60 @@ export default function PublishPage({
|
|||
setSuccess(
|
||||
'🎉 Successfully published. 🎉 Now create a price on your data set.'
|
||||
)
|
||||
resetForm()
|
||||
resetForm({
|
||||
values: initialValues as MetadataPublishFormDataset,
|
||||
status: 'empty'
|
||||
})
|
||||
} catch (error) {
|
||||
setError(error.message)
|
||||
Logger.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAlgorithmSubmit(
|
||||
values: Partial<MetadataPublishFormAlgorithm>,
|
||||
resetForm: (
|
||||
nextState?: Partial<FormikState<Partial<MetadataPublishFormAlgorithm>>>
|
||||
) => void
|
||||
): Promise<void> {
|
||||
const metadata = transformPublishAlgorithmFormToMetadata(values)
|
||||
const timeout = mapTimeoutStringToSeconds(values.timeout)
|
||||
|
||||
// TODO: put back check once #572 is resolved
|
||||
// https://github.com/oceanprotocol/market/issues/572
|
||||
const validDockerImage = true
|
||||
// const validDockerImage =
|
||||
// values.dockerImage === 'custom image'
|
||||
// ? await validateDockerImage(values.image, values.containerTag)
|
||||
// : true
|
||||
try {
|
||||
if (validDockerImage) {
|
||||
Logger.log('Publish algorithm with ', metadata, values.dataTokenOptions)
|
||||
|
||||
const ddo = await publish(
|
||||
(metadata as unknown) as Metadata,
|
||||
values.algorithmPrivacy === true ? 'compute' : 'access',
|
||||
values.dataTokenOptions,
|
||||
timeout
|
||||
)
|
||||
|
||||
// Publish failed
|
||||
if (!ddo || publishError) {
|
||||
setError(publishError || 'Publishing DDO failed.')
|
||||
Logger.error(publishError || 'Publishing DDO failed.')
|
||||
return
|
||||
}
|
||||
|
||||
// Publish succeeded
|
||||
setDid(ddo.id)
|
||||
setSuccess(
|
||||
'🎉 Successfully published. 🎉 Now create a price for your algorithm.'
|
||||
)
|
||||
resetForm({
|
||||
values: initialValuesAlgorithm as MetadataPublishFormAlgorithm,
|
||||
status: 'empty'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error.message)
|
||||
Logger.error(error.message)
|
||||
|
@ -83,55 +217,85 @@ export default function PublishPage({
|
|||
|
||||
return isInPurgatory && purgatoryData ? null : (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
initialValues={
|
||||
publishType === 'dataset' ? datasetInitialValues : algoInitialValues
|
||||
}
|
||||
initialStatus="empty"
|
||||
validationSchema={validationSchema}
|
||||
validationSchema={
|
||||
publishType === 'dataset' ? validationSchema : validationSchemaAlgorithm
|
||||
}
|
||||
onSubmit={async (values, { resetForm }) => {
|
||||
// move user's focus to top of screen
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||
// kick off publishing
|
||||
await handleSubmit(values, resetForm)
|
||||
publishType === 'dataset'
|
||||
? await handleSubmit(values, resetForm)
|
||||
: await handleAlgorithmSubmit(values, resetForm)
|
||||
}}
|
||||
enableReinitialize
|
||||
>
|
||||
{({ values }) => (
|
||||
<>
|
||||
<Persist name={formName} ignoreFields={['isSubmitting']} />
|
||||
{({ values }) => {
|
||||
const tabs = [
|
||||
{
|
||||
title: 'Data Set',
|
||||
content: <TabContent values={values} publishType={publishType} />
|
||||
},
|
||||
{
|
||||
title: 'Algorithm',
|
||||
content: <TabContent values={values} publishType={publishType} />
|
||||
}
|
||||
]
|
||||
|
||||
{hasFeedback ? (
|
||||
<MetadataFeedback
|
||||
title="Publishing Data Set"
|
||||
error={error}
|
||||
success={success}
|
||||
loading={publishStepText}
|
||||
setError={setError}
|
||||
successAction={{
|
||||
name: 'Go to data set →',
|
||||
to: `/asset/${did}`
|
||||
}}
|
||||
return (
|
||||
<>
|
||||
<Persist
|
||||
name={
|
||||
publishType === 'dataset'
|
||||
? formNameDatasets
|
||||
: formNameAlgorithms
|
||||
}
|
||||
ignoreFields={['isSubmitting']}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Alert
|
||||
text={content.warning}
|
||||
state="info"
|
||||
className={styles.alert}
|
||||
|
||||
{hasFeedback ? (
|
||||
<MetadataFeedback
|
||||
title={title}
|
||||
error={error}
|
||||
success={success}
|
||||
loading={publishStepText}
|
||||
setError={setError}
|
||||
successAction={{
|
||||
name: `Go to ${
|
||||
publishType === 'dataset' ? 'data set' : 'algorithm'
|
||||
} →`,
|
||||
to: `/asset/${did}`
|
||||
}}
|
||||
/>
|
||||
<article className={styles.grid}>
|
||||
<FormPublish content={content.form} />
|
||||
) : (
|
||||
<>
|
||||
<Alert
|
||||
text={content.warning}
|
||||
state="info"
|
||||
className={styles.alert}
|
||||
/>
|
||||
|
||||
<aside>
|
||||
<div className={styles.sticky}>
|
||||
<MetadataPreview values={values} />
|
||||
<Web3Feedback />
|
||||
</div>
|
||||
</aside>
|
||||
</article>
|
||||
</>
|
||||
)}
|
||||
<Tabs
|
||||
className={styles.tabs}
|
||||
items={tabs}
|
||||
handleTabChange={(title) => {
|
||||
setPublishType(title.toLowerCase().replace(' ', '') as any)
|
||||
title === 'Algorithm'
|
||||
? setdatasetInitialValues(values)
|
||||
: setAlgoInitialValues(values)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{debug === true && <Debug values={values} />}
|
||||
</>
|
||||
)}
|
||||
{debug === true && <Debug values={values} />}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Formik>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
/* .filterList {
|
||||
display: inline-flex;
|
||||
float: left;
|
||||
} */
|
||||
.filterList,
|
||||
div.filterList {
|
||||
white-space: normal;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.filter,
|
||||
button.filter,
|
||||
|
@ -9,14 +14,14 @@ button.filter,
|
|||
.filter:active,
|
||||
.filter:focus {
|
||||
border: 1px solid var(--border-color);
|
||||
text-transform: uppercase;
|
||||
border-radius: var(--border-radius);
|
||||
margin-right: calc(var(--spacer) / 6);
|
||||
margin-bottom: calc(var(--spacer) / 6);
|
||||
color: var(--color-secondary);
|
||||
background: var(--background-body);
|
||||
background: var(--background-content);
|
||||
|
||||
/* the only thing not possible to overwrite button style="text" with more specifity of selectors, so sledgehammer */
|
||||
padding: calc(var(--spacer) / 5) !important;
|
||||
padding: calc(var(--spacer) / 6) !important;
|
||||
}
|
||||
|
||||
.filter:hover,
|
||||
|
@ -31,3 +36,25 @@ button.filter,
|
|||
background: var(--font-color-text);
|
||||
border-color: var(--background-body);
|
||||
}
|
||||
|
||||
.filter.selected::after {
|
||||
content: '✕';
|
||||
margin-left: calc(var(--spacer) / 6);
|
||||
color: var(--background-body);
|
||||
}
|
||||
|
||||
.filterList:first-of-type {
|
||||
margin-bottom: calc(var(--spacer) / 6);
|
||||
}
|
||||
|
||||
.showClear {
|
||||
display: inline-flex;
|
||||
text-transform: capitalize;
|
||||
color: var(--color-secondary);
|
||||
font-weight: var(--font-weight-base);
|
||||
margin-left: calc(var(--spacer) / 6);
|
||||
}
|
||||
|
||||
.hideClear {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -1,28 +1,45 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { useNavigate } from '@reach/router'
|
||||
import styles from './filterPrice.module.css'
|
||||
import classNames from 'classnames/bind'
|
||||
import { addExistingParamsToUrl, FilterByPriceOptions } from './utils'
|
||||
import {
|
||||
addExistingParamsToUrl,
|
||||
FilterByPriceOptions,
|
||||
FilterByTypeOptions
|
||||
} from './utils'
|
||||
import Button from '../../atoms/Button'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
const filterItems = [
|
||||
{ display: 'all', value: undefined },
|
||||
const clearFilters = [{ display: 'Clear', value: '' }]
|
||||
|
||||
const priceFilterItems = [
|
||||
{ display: 'fixed price', value: FilterByPriceOptions.Fixed },
|
||||
{ display: 'dynamic price', value: FilterByPriceOptions.Dynamic }
|
||||
]
|
||||
|
||||
const serviceFilterItems = [
|
||||
{ display: 'data sets', value: FilterByTypeOptions.Data },
|
||||
{ display: 'algorithms', value: FilterByTypeOptions.Algorithm }
|
||||
]
|
||||
|
||||
export default function FilterPrice({
|
||||
priceType,
|
||||
setPriceType
|
||||
serviceType,
|
||||
setPriceType,
|
||||
setServiceType
|
||||
}: {
|
||||
priceType: string
|
||||
setPriceType: React.Dispatch<React.SetStateAction<string>>
|
||||
serviceType: string
|
||||
setServiceType: React.Dispatch<React.SetStateAction<string>>
|
||||
}): ReactElement {
|
||||
const navigate = useNavigate()
|
||||
|
||||
async function applyFilter(filterBy: string) {
|
||||
const [priceSelections, setPriceSelections] = useState<string[]>([])
|
||||
const [serviceSelections, setServiceSelections] = useState<string[]>([])
|
||||
|
||||
async function applyPriceFilter(filterBy: string) {
|
||||
let urlLocation = await addExistingParamsToUrl(location, 'priceType')
|
||||
if (filterBy) {
|
||||
urlLocation = `${urlLocation}&priceType=${filterBy}`
|
||||
|
@ -31,11 +48,87 @@ export default function FilterPrice({
|
|||
navigate(urlLocation)
|
||||
}
|
||||
|
||||
async function applyServiceFilter(filterBy: string) {
|
||||
let urlLocation = await addExistingParamsToUrl(location, 'serviceType')
|
||||
if (filterBy && location.search.indexOf('&serviceType') === -1) {
|
||||
urlLocation = `${urlLocation}&serviceType=${filterBy}`
|
||||
}
|
||||
setServiceType(filterBy)
|
||||
navigate(urlLocation)
|
||||
}
|
||||
|
||||
async function handleSelectedFilter(isSelected: boolean, value: string) {
|
||||
if (
|
||||
value === FilterByPriceOptions.Fixed ||
|
||||
value === FilterByPriceOptions.Dynamic
|
||||
) {
|
||||
if (isSelected) {
|
||||
if (priceSelections.length > 1) {
|
||||
// both selected -> select the other one
|
||||
const otherValue = priceFilterItems.find((p) => p.value !== value)
|
||||
.value
|
||||
await applyPriceFilter(otherValue)
|
||||
} else {
|
||||
// only the current one selected -> deselect it
|
||||
await applyPriceFilter(undefined)
|
||||
}
|
||||
} else {
|
||||
if (priceSelections.length > 0) {
|
||||
// one already selected -> both selected
|
||||
await applyPriceFilter(FilterByPriceOptions.All)
|
||||
setPriceSelections(priceFilterItems.map((p) => p.value))
|
||||
} else {
|
||||
// none selected -> select
|
||||
await applyPriceFilter(value)
|
||||
setPriceSelections([value])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isSelected) {
|
||||
if (serviceSelections.length > 1) {
|
||||
const otherValue = serviceFilterItems.find((p) => p.value !== value)
|
||||
.value
|
||||
await applyServiceFilter(otherValue)
|
||||
setServiceSelections([otherValue])
|
||||
} else {
|
||||
await applyServiceFilter(undefined)
|
||||
}
|
||||
} else {
|
||||
if (serviceSelections.length) {
|
||||
await applyServiceFilter(undefined)
|
||||
setServiceSelections(serviceFilterItems.map((p) => p.value))
|
||||
} else {
|
||||
await applyServiceFilter(value)
|
||||
setServiceSelections([value])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function applyClearFilter() {
|
||||
let urlLocation = await addExistingParamsToUrl(
|
||||
location,
|
||||
'priceType',
|
||||
'serviceType'
|
||||
)
|
||||
|
||||
urlLocation = `${urlLocation}`
|
||||
|
||||
setServiceSelections([])
|
||||
setPriceSelections([])
|
||||
|
||||
setPriceType(undefined)
|
||||
setServiceType(undefined)
|
||||
navigate(urlLocation)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{filterItems.map((e, index) => {
|
||||
const filter = cx({
|
||||
[styles.selected]: e.value === priceType,
|
||||
<div className={styles.filterList}>
|
||||
{priceFilterItems.map((e, index) => {
|
||||
const isPriceSelected =
|
||||
e.value === priceType || priceSelections.includes(e.value)
|
||||
const selectFilter = cx({
|
||||
[styles.selected]: isPriceSelected,
|
||||
[styles.filter]: true
|
||||
})
|
||||
return (
|
||||
|
@ -43,9 +136,47 @@ export default function FilterPrice({
|
|||
size="small"
|
||||
style="text"
|
||||
key={index}
|
||||
className={filter}
|
||||
className={selectFilter}
|
||||
onClick={async () => {
|
||||
await applyFilter(e.value)
|
||||
handleSelectedFilter(isPriceSelected, e.value)
|
||||
}}
|
||||
>
|
||||
{e.display}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
{serviceFilterItems.map((e, index) => {
|
||||
const isServiceSelected =
|
||||
e.value === serviceType || serviceSelections.includes(e.value)
|
||||
const selectFilter = cx({
|
||||
[styles.selected]: isServiceSelected,
|
||||
[styles.filter]: true
|
||||
})
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
style="text"
|
||||
key={index}
|
||||
className={selectFilter}
|
||||
onClick={async () => {
|
||||
handleSelectedFilter(isServiceSelected, e.value)
|
||||
}}
|
||||
>
|
||||
{e.display}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
{clearFilters.map((e, index) => {
|
||||
const showClear =
|
||||
priceSelections.length > 0 || serviceSelections.length > 0
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
style="text"
|
||||
key={index}
|
||||
className={showClear ? styles.showClear : styles.hideClear}
|
||||
onClick={async () => {
|
||||
applyClearFilter()
|
||||
}}
|
||||
>
|
||||
{e.display}
|
||||
|
|
|
@ -21,10 +21,20 @@ export default function SearchPage({
|
|||
}): ReactElement {
|
||||
const { config } = useOcean()
|
||||
const parsed = queryString.parse(location.search)
|
||||
const { text, owner, tags, page, sort, sortOrder, priceType } = parsed
|
||||
const {
|
||||
text,
|
||||
owner,
|
||||
tags,
|
||||
page,
|
||||
sort,
|
||||
sortOrder,
|
||||
priceType,
|
||||
serviceType
|
||||
} = parsed
|
||||
const [queryResult, setQueryResult] = useState<QueryResult>()
|
||||
const [loading, setLoading] = useState<boolean>()
|
||||
const [price, setPriceType] = useState<string>(priceType as string)
|
||||
const [service, setServiceType] = useState<string>(serviceType as string)
|
||||
const [sortType, setSortType] = useState<string>(sort as string)
|
||||
const [sortDirection, setSortDirection] = useState<string>(
|
||||
sortOrder as string
|
||||
|
@ -49,6 +59,7 @@ export default function SearchPage({
|
|||
sort,
|
||||
page,
|
||||
priceType,
|
||||
serviceType,
|
||||
sortOrder,
|
||||
config.metadataCacheUri
|
||||
])
|
||||
|
@ -69,13 +80,19 @@ export default function SearchPage({
|
|||
<SearchBar initialValue={(text || owner) as string} />
|
||||
)}
|
||||
<div className={styles.row}>
|
||||
<PriceFilter priceType={price} setPriceType={setPriceType} />
|
||||
<PriceFilter
|
||||
priceType={price}
|
||||
serviceType={service}
|
||||
setPriceType={setPriceType}
|
||||
setServiceType={setServiceType}
|
||||
/>
|
||||
<Sort
|
||||
sortType={sortType}
|
||||
sortDirection={sortDirection}
|
||||
setSortType={setSortType}
|
||||
setSortDirection={setSortDirection}
|
||||
setPriceType={setPriceType}
|
||||
setServiceType={setServiceType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
.sortList {
|
||||
padding: 0 calc(var(--spacer) / 10);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--background-body);
|
||||
background: var(--background-content);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
.sortList {
|
||||
align-self: flex-end;
|
||||
overflow-y: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.sortLabel {
|
||||
|
@ -13,6 +22,7 @@
|
|||
margin-right: calc(var(--spacer) / 1.5);
|
||||
text-transform: uppercase;
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.sorted {
|
||||
|
@ -23,7 +33,7 @@
|
|||
text-transform: capitalize;
|
||||
border-radius: 0;
|
||||
font-weight: var(--font-weight-base);
|
||||
background: var(--background-body);
|
||||
background: var(--background-content);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@ import {
|
|||
addExistingParamsToUrl,
|
||||
SortTermOptions,
|
||||
SortValueOptions,
|
||||
FilterByPriceOptions
|
||||
FilterByPriceOptions,
|
||||
FilterByTypeOptions
|
||||
} from './utils'
|
||||
import Button from '../../atoms/Button'
|
||||
import styles from './sort.module.css'
|
||||
|
@ -23,13 +24,15 @@ export default function Sort({
|
|||
setSortType,
|
||||
sortDirection,
|
||||
setSortDirection,
|
||||
setPriceType
|
||||
setPriceType,
|
||||
setServiceType
|
||||
}: {
|
||||
sortType: string
|
||||
setSortType: React.Dispatch<React.SetStateAction<string>>
|
||||
sortDirection: string
|
||||
setSortDirection: React.Dispatch<React.SetStateAction<string>>
|
||||
setPriceType: React.Dispatch<React.SetStateAction<string>>
|
||||
setServiceType: React.Dispatch<React.SetStateAction<string>>
|
||||
}): ReactElement {
|
||||
const navigate = useNavigate()
|
||||
const directionArrow = String.fromCharCode(
|
||||
|
@ -43,8 +46,6 @@ export default function Sort({
|
|||
if (sortBy === SortTermOptions.Liquidity) {
|
||||
urlLocation = `${urlLocation}&priceType=${FilterByPriceOptions.Dynamic}`
|
||||
setPriceType(FilterByPriceOptions.Dynamic)
|
||||
} else {
|
||||
setPriceType(undefined)
|
||||
}
|
||||
setSortType(sortBy)
|
||||
} else if (direction) {
|
||||
|
|
|
@ -27,15 +27,39 @@ type SortValueOptions = typeof SortValueOptions[keyof typeof SortValueOptions]
|
|||
|
||||
export const FilterByPriceOptions = {
|
||||
Fixed: 'exchange',
|
||||
Dynamic: 'pool'
|
||||
Dynamic: 'pool',
|
||||
All: 'all'
|
||||
} as const
|
||||
type FilterByPriceOptions = typeof FilterByPriceOptions[keyof typeof FilterByPriceOptions]
|
||||
|
||||
function addPriceFilterToQuerry(sortTerm: string, priceFilter: string): string {
|
||||
sortTerm = priceFilter
|
||||
? /\S/.test(sortTerm)
|
||||
? `${sortTerm} AND price.type:${priceFilter}`
|
||||
: `price.type:${priceFilter}`
|
||||
export const FilterByTypeOptions = {
|
||||
Data: 'dataset',
|
||||
Algorithm: 'algorithm'
|
||||
} as const
|
||||
type FilterByTypeOptions = typeof FilterByTypeOptions[keyof typeof FilterByTypeOptions]
|
||||
|
||||
function addPriceFilterToQuery(sortTerm: string, priceFilter: string): string {
|
||||
if (priceFilter === FilterByPriceOptions.All) {
|
||||
sortTerm = priceFilter
|
||||
? sortTerm === ''
|
||||
? `(price.type:${FilterByPriceOptions.Fixed} OR price.type:${FilterByPriceOptions.Dynamic})`
|
||||
: `${sortTerm} AND (price.type:${FilterByPriceOptions.Dynamic} OR price.type:${FilterByPriceOptions.Fixed})`
|
||||
: sortTerm
|
||||
} else {
|
||||
sortTerm = priceFilter
|
||||
? sortTerm === ''
|
||||
? `price.type:${priceFilter}`
|
||||
: `${sortTerm} AND price.type:${priceFilter}`
|
||||
: sortTerm
|
||||
}
|
||||
return sortTerm
|
||||
}
|
||||
|
||||
function addTypeFilterToQuery(sortTerm: string, typeFilter: string): string {
|
||||
sortTerm = typeFilter
|
||||
? sortTerm === ''
|
||||
? `service.attributes.main.type:${typeFilter}`
|
||||
: `${sortTerm} AND service.attributes.main.type:${typeFilter}`
|
||||
: sortTerm
|
||||
return sortTerm
|
||||
}
|
||||
|
@ -59,7 +83,8 @@ export function getSearchQuery(
|
|||
offset?: string,
|
||||
sort?: string,
|
||||
sortOrder?: string,
|
||||
priceType?: string
|
||||
priceType?: string,
|
||||
serviceType?: string
|
||||
): SearchQuery {
|
||||
const sortTerm = getSortType(sort)
|
||||
const sortValue = sortOrder === SortValueOptions.Ascending ? 1 : -1
|
||||
|
@ -72,7 +97,8 @@ export function getSearchQuery(
|
|||
? // eslint-disable-next-line no-useless-escape
|
||||
`(service.attributes.additionalInformation.categories:\"${categories}\")`
|
||||
: text || ''
|
||||
searchTerm = addPriceFilterToQuerry(searchTerm, priceType)
|
||||
searchTerm = addTypeFilterToQuery(searchTerm, serviceType)
|
||||
searchTerm = addPriceFilterToQuery(searchTerm, priceType)
|
||||
|
||||
return {
|
||||
page: Number(page) || 1,
|
||||
|
@ -111,6 +137,7 @@ export async function getResults(
|
|||
sort?: string
|
||||
sortOrder?: string
|
||||
priceType?: string
|
||||
serviceType?: string
|
||||
},
|
||||
metadataCacheUri: string
|
||||
): Promise<QueryResult> {
|
||||
|
@ -123,7 +150,8 @@ export async function getResults(
|
|||
categories,
|
||||
sort,
|
||||
sortOrder,
|
||||
priceType
|
||||
priceType,
|
||||
serviceType
|
||||
} = params
|
||||
const metadataCache = new MetadataCache(metadataCacheUri, Logger)
|
||||
const searchQuery = getSearchQuery(
|
||||
|
@ -135,7 +163,8 @@ export async function getResults(
|
|||
offset,
|
||||
sort,
|
||||
sortOrder,
|
||||
priceType
|
||||
priceType,
|
||||
serviceType
|
||||
)
|
||||
const queryResult = await metadataCache.queryMetadata(searchQuery)
|
||||
|
||||
|
|
|
@ -25,9 +25,10 @@
|
|||
/* Only use these vars for most color referencing for easy light/dark mode */
|
||||
--font-color-text: #41474e;
|
||||
--font-color-heading: #141414;
|
||||
--background-body: #fff;
|
||||
--background-body: #fcfcfc;
|
||||
--background-content: #fff;
|
||||
--background-body-transparent: rgba(255, 255, 255, 0.8);
|
||||
--background-content: #fff;
|
||||
--background-highlight: #f7f7f7;
|
||||
--border-color: #e2e2e2;
|
||||
--box-shadow-color: rgba(0, 0, 0, 0.05);
|
||||
|
@ -71,9 +72,9 @@
|
|||
.dark {
|
||||
--font-color-text: #e2e2e2;
|
||||
--font-color-heading: #f7f7f7;
|
||||
--background-body: rgb(10, 10, 10);
|
||||
--background-body-transparent: rgba(10, 10, 10, 0.9);
|
||||
--background-content: #141414;
|
||||
--background-body: #141414;
|
||||
--background-body-transparent: rgba(20, 20, 20, 0.9);
|
||||
--background-highlight: #201f1f;
|
||||
--border-color: #303030;
|
||||
--box-shadow-color: rgba(0, 0, 0, 0.2);
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
import { Logger, ServiceCompute } from '@oceanprotocol/lib'
|
||||
import { MetadataAlgorithm } from '@oceanprotocol/lib/dist/node/ddo/interfaces/MetadataAlgorithm'
|
||||
import { ComputeJob } from '@oceanprotocol/lib/dist/node/ocean/interfaces/ComputeJob'
|
||||
import { computeFeedback } from '../utils/feedback'
|
||||
import { useOcean } from '../providers/Ocean'
|
||||
import { useWeb3 } from '../providers/Web3'
|
||||
|
||||
interface ComputeValue {
|
||||
entrypoint: string
|
||||
image: string
|
||||
tag: string
|
||||
}
|
||||
interface ComputeOption {
|
||||
name: string
|
||||
value: ComputeValue
|
||||
}
|
||||
|
||||
const computeOptions: ComputeOption[] = [
|
||||
{
|
||||
name: 'nodejs',
|
||||
value: {
|
||||
entrypoint: 'node $ALGO',
|
||||
image: 'node',
|
||||
tag: '10'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'python3.7',
|
||||
value: {
|
||||
entrypoint: 'python $ALGO',
|
||||
image: 'oceanprotocol/algo_dockers',
|
||||
tag: 'python-panda'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
interface UseCompute {
|
||||
compute: (
|
||||
did: string,
|
||||
computeService: ServiceCompute,
|
||||
dataTokenAddress: string,
|
||||
algorithmRawCode: string,
|
||||
computeContainer: ComputeValue,
|
||||
marketFeeAddress?: string,
|
||||
orderId?: string
|
||||
) => Promise<ComputeJob | void>
|
||||
computeStep?: number
|
||||
computeStepText?: string
|
||||
computeError?: string
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const rawAlgorithmMeta: MetadataAlgorithm = {
|
||||
rawcode: `console.log('Hello world'!)`,
|
||||
format: 'docker-image',
|
||||
version: '0.1',
|
||||
container: {
|
||||
entrypoint: '',
|
||||
image: '',
|
||||
tag: ''
|
||||
}
|
||||
}
|
||||
|
||||
function useCompute(): UseCompute {
|
||||
const { accountId } = useWeb3()
|
||||
const { ocean, account } = useOcean()
|
||||
const [computeStep, setComputeStep] = useState<number | undefined>()
|
||||
const [computeStepText, setComputeStepText] = useState<string | undefined>()
|
||||
const [computeError, setComputeError] = useState<string | undefined>()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
function setStep(index?: number) {
|
||||
if (!index) {
|
||||
setComputeStep(undefined)
|
||||
setComputeStepText(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
setComputeStep(index)
|
||||
setComputeStepText(computeFeedback[index])
|
||||
}
|
||||
|
||||
async function compute(
|
||||
did: string,
|
||||
computeService: ServiceCompute,
|
||||
dataTokenAddress: string,
|
||||
algorithmRawCode: string,
|
||||
computeContainer: ComputeValue,
|
||||
marketFeeAddress?: string,
|
||||
orderId?: string
|
||||
): Promise<ComputeJob | void> {
|
||||
if (!ocean || !account) return
|
||||
setComputeError(undefined)
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setStep(0)
|
||||
rawAlgorithmMeta.container = computeContainer
|
||||
rawAlgorithmMeta.rawcode = algorithmRawCode
|
||||
const output = {}
|
||||
if (!orderId) {
|
||||
const userOwnedTokens = await ocean.accounts.getTokenBalance(
|
||||
dataTokenAddress,
|
||||
account
|
||||
)
|
||||
if (parseFloat(userOwnedTokens) < 1) {
|
||||
setComputeError('Not enough datatokens')
|
||||
} else {
|
||||
Logger.log(
|
||||
'compute order',
|
||||
accountId,
|
||||
did,
|
||||
computeService,
|
||||
rawAlgorithmMeta,
|
||||
marketFeeAddress
|
||||
)
|
||||
orderId = await ocean.compute.orderAsset(
|
||||
accountId,
|
||||
did,
|
||||
computeService.index,
|
||||
undefined,
|
||||
rawAlgorithmMeta,
|
||||
marketFeeAddress
|
||||
)
|
||||
setStep(1)
|
||||
}
|
||||
}
|
||||
setStep(2)
|
||||
if (orderId) {
|
||||
const response = await ocean.compute.start(
|
||||
did,
|
||||
orderId,
|
||||
dataTokenAddress,
|
||||
account,
|
||||
undefined,
|
||||
rawAlgorithmMeta,
|
||||
output,
|
||||
`${computeService.index}`,
|
||||
computeService.type
|
||||
)
|
||||
return response
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(error)
|
||||
setComputeError(error.message)
|
||||
} finally {
|
||||
setStep(undefined)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return { compute, computeStep, computeStepText, computeError, isLoading }
|
||||
}
|
||||
|
||||
export { useCompute, UseCompute, ComputeValue, ComputeOption, computeOptions }
|
||||
export default UseCompute
|
|
@ -1,5 +1,5 @@
|
|||
import { DDO, Logger, BestPrice } from '@oceanprotocol/lib'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { TransactionReceipt } from 'web3-core'
|
||||
import { Decimal } from 'decimal.js'
|
||||
import {
|
||||
|
@ -22,15 +22,17 @@ interface PriceOptions {
|
|||
}
|
||||
|
||||
interface UsePricing {
|
||||
dtSymbol?: string
|
||||
dtName?: string
|
||||
getDTSymbol: (ddo: DDO) => Promise<string>
|
||||
getDTName: (ddo: DDO) => Promise<string>
|
||||
createPricing: (
|
||||
priceOptions: PriceOptions
|
||||
priceOptions: PriceOptions,
|
||||
ddo: DDO
|
||||
) => Promise<TransactionReceipt | string | void>
|
||||
mint: (tokensToMint: string) => Promise<TransactionReceipt | void>
|
||||
mint: (tokensToMint: string, ddo: DDO) => Promise<TransactionReceipt | void>
|
||||
buyDT: (
|
||||
dtAmount: number | string,
|
||||
price: BestPrice
|
||||
price: BestPrice,
|
||||
ddo: DDO
|
||||
) => Promise<TransactionReceipt | void>
|
||||
pricingStep?: number
|
||||
pricingStepText?: string
|
||||
|
@ -38,38 +40,38 @@ interface UsePricing {
|
|||
pricingIsLoading: boolean
|
||||
}
|
||||
|
||||
function usePricing(ddo: DDO): UsePricing {
|
||||
function usePricing(): UsePricing {
|
||||
const { accountId } = useWeb3()
|
||||
const { ocean, config } = useOcean()
|
||||
const [pricingIsLoading, setPricingIsLoading] = useState(false)
|
||||
const [pricingStep, setPricingStep] = useState<number>()
|
||||
const [pricingStepText, setPricingStepText] = useState<string>()
|
||||
const [pricingError, setPricingError] = useState<string>()
|
||||
const [dtSymbol, setDtSymbol] = useState<string>()
|
||||
const [dtName, setDtName] = useState<string>()
|
||||
|
||||
const { dataToken, dataTokenInfo } = ddo
|
||||
async function getDTSymbol(ddo: DDO): Promise<string> {
|
||||
if (!ocean || !accountId) return
|
||||
|
||||
// Get Datatoken info, from DDO first, then from chain
|
||||
useEffect(() => {
|
||||
if (!dataToken) return
|
||||
const { dataToken, dataTokenInfo } = ddo
|
||||
return dataTokenInfo
|
||||
? dataTokenInfo.symbol
|
||||
: await ocean?.datatokens.getSymbol(dataToken)
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const dtSymbol = dataTokenInfo
|
||||
? dataTokenInfo.symbol
|
||||
: await ocean?.datatokens.getSymbol(dataToken)
|
||||
setDtSymbol(dtSymbol)
|
||||
|
||||
const dtName = dataTokenInfo
|
||||
? dataTokenInfo.name
|
||||
: await ocean?.datatokens.getName(dataToken)
|
||||
setDtName(dtName)
|
||||
}
|
||||
init()
|
||||
}, [ocean, dataToken, dataTokenInfo])
|
||||
async function getDTName(ddo: DDO): Promise<string> {
|
||||
if (!ocean || !accountId) return
|
||||
const { dataToken, dataTokenInfo } = ddo
|
||||
return dataTokenInfo
|
||||
? dataTokenInfo.name
|
||||
: await ocean?.datatokens.getName(dataToken)
|
||||
}
|
||||
|
||||
// Helper for setting steps & feedback for all flows
|
||||
function setStep(index: number, type: 'pool' | 'exchange' | 'buy') {
|
||||
async function setStep(
|
||||
index: number,
|
||||
type: 'pool' | 'exchange' | 'buy',
|
||||
ddo: DDO
|
||||
) {
|
||||
const dtSymbol = await getDTSymbol(ddo)
|
||||
setPricingStep(index)
|
||||
if (!dtSymbol) return
|
||||
|
||||
|
@ -91,8 +93,10 @@ function usePricing(ddo: DDO): UsePricing {
|
|||
}
|
||||
|
||||
async function mint(
|
||||
tokensToMint: string
|
||||
tokensToMint: string,
|
||||
ddo: DDO
|
||||
): Promise<TransactionReceipt | void> {
|
||||
const { dataToken } = ddo
|
||||
Logger.log('mint function', dataToken, accountId)
|
||||
const balance = new Decimal(
|
||||
await ocean.datatokens.balance(dataToken, accountId)
|
||||
|
@ -111,7 +115,8 @@ function usePricing(ddo: DDO): UsePricing {
|
|||
|
||||
async function buyDT(
|
||||
dtAmount: number | string,
|
||||
price: BestPrice
|
||||
price: BestPrice,
|
||||
ddo: DDO
|
||||
): Promise<TransactionReceipt | void> {
|
||||
if (!ocean || !accountId) return
|
||||
|
||||
|
@ -120,7 +125,7 @@ function usePricing(ddo: DDO): UsePricing {
|
|||
try {
|
||||
setPricingIsLoading(true)
|
||||
setPricingError(undefined)
|
||||
setStep(1, 'buy')
|
||||
setStep(1, 'buy', ddo)
|
||||
|
||||
Logger.log('Price found for buying', price)
|
||||
Decimal.set({ precision: 18 })
|
||||
|
@ -129,7 +134,8 @@ function usePricing(ddo: DDO): UsePricing {
|
|||
case 'pool': {
|
||||
const oceanAmmount = new Decimal(price.value).times(1.05).toString()
|
||||
const maxPrice = new Decimal(price.value).times(2).toString()
|
||||
setStep(2, 'buy')
|
||||
|
||||
setStep(2, 'buy', ddo)
|
||||
Logger.log(
|
||||
'Buying token from pool',
|
||||
price,
|
||||
|
@ -144,7 +150,7 @@ function usePricing(ddo: DDO): UsePricing {
|
|||
oceanAmmount,
|
||||
maxPrice
|
||||
)
|
||||
setStep(3, 'buy')
|
||||
setStep(3, 'buy', ddo)
|
||||
Logger.log('DT buy response', tx)
|
||||
break
|
||||
}
|
||||
|
@ -164,13 +170,13 @@ function usePricing(ddo: DDO): UsePricing {
|
|||
`${price.value}`,
|
||||
accountId
|
||||
)
|
||||
setStep(2, 'buy')
|
||||
setStep(2, 'buy', ddo)
|
||||
tx = await ocean.fixedRateExchange.buyDT(
|
||||
price.address,
|
||||
`${dtAmount}`,
|
||||
accountId
|
||||
)
|
||||
setStep(3, 'buy')
|
||||
setStep(3, 'buy', ddo)
|
||||
Logger.log('DT exchange buy response', tx)
|
||||
break
|
||||
}
|
||||
|
@ -179,7 +185,7 @@ function usePricing(ddo: DDO): UsePricing {
|
|||
setPricingError(error.message)
|
||||
Logger.error(error)
|
||||
} finally {
|
||||
setStep(0, 'buy')
|
||||
setStep(0, 'buy', ddo)
|
||||
setPricingStepText(undefined)
|
||||
setPricingIsLoading(false)
|
||||
}
|
||||
|
@ -188,8 +194,12 @@ function usePricing(ddo: DDO): UsePricing {
|
|||
}
|
||||
|
||||
async function createPricing(
|
||||
priceOptions: PriceOptions
|
||||
priceOptions: PriceOptions,
|
||||
ddo: DDO
|
||||
): Promise<TransactionReceipt | void> {
|
||||
const { dataToken } = ddo
|
||||
const dtSymbol = await getDTSymbol(ddo)
|
||||
|
||||
if (!ocean || !accountId || !dtSymbol) return
|
||||
|
||||
const {
|
||||
|
@ -211,12 +221,12 @@ function usePricing(ddo: DDO): UsePricing {
|
|||
setPricingIsLoading(true)
|
||||
setPricingError(undefined)
|
||||
|
||||
setStep(99, 'pool')
|
||||
setStep(99, 'pool', ddo)
|
||||
|
||||
try {
|
||||
// if fixedPrice set dt to max amount
|
||||
if (!isPool) dtAmount = 1000
|
||||
await mint(`${dtAmount}`)
|
||||
await mint(`${dtAmount}`, ddo)
|
||||
|
||||
// dtAmount for fixed price is set to max
|
||||
const tx = isPool
|
||||
|
@ -229,10 +239,10 @@ function usePricing(ddo: DDO): UsePricing {
|
|||
`${oceanAmount}`,
|
||||
swapFee
|
||||
)
|
||||
.next((step: number) => setStep(step, 'pool'))
|
||||
.next((step: number) => setStep(step, 'pool', ddo))
|
||||
: await ocean.fixedRateExchange
|
||||
.create(dataToken, `${price}`, accountId, `${dtAmount}`)
|
||||
.next((step: number) => setStep(step, 'exchange'))
|
||||
.next((step: number) => setStep(step, 'exchange', ddo))
|
||||
await sleep(20000)
|
||||
return tx
|
||||
} catch (error) {
|
||||
|
@ -246,8 +256,8 @@ function usePricing(ddo: DDO): UsePricing {
|
|||
}
|
||||
|
||||
return {
|
||||
dtSymbol,
|
||||
dtName,
|
||||
getDTSymbol,
|
||||
getDTName,
|
||||
createPricing,
|
||||
buyDT,
|
||||
mint,
|
||||
|
|
|
@ -82,36 +82,7 @@ function usePublish(): UsePublish {
|
|||
}
|
||||
case 'compute': {
|
||||
if (!timeout) timeout = 3600
|
||||
const cluster = ocean.compute.createClusterAttributes(
|
||||
'Kubernetes',
|
||||
'http://10.0.0.17/xxx'
|
||||
)
|
||||
const servers = [
|
||||
ocean.compute.createServerAttributes(
|
||||
'1',
|
||||
'xlsize',
|
||||
'50',
|
||||
'16',
|
||||
'0',
|
||||
'128gb',
|
||||
'160gb',
|
||||
timeout
|
||||
)
|
||||
]
|
||||
const containers = [
|
||||
ocean.compute.createContainerAttributes(
|
||||
'tensorflow/tensorflow',
|
||||
'latest',
|
||||
'sha256:cb57ecfa6ebbefd8ffc7f75c0f00e57a7fa739578a429b6f72a0df19315deadc'
|
||||
)
|
||||
]
|
||||
const provider = ocean.compute.createProviderAttributes(
|
||||
'Azure',
|
||||
'Compute service with 16gb ram for each node.',
|
||||
cluster,
|
||||
containers,
|
||||
servers
|
||||
)
|
||||
const provider = {}
|
||||
const origComputePrivacy: ServiceComputePrivacy = {
|
||||
allowRawAlgorithm: false,
|
||||
allowNetworkAccess: false,
|
||||
|
|
5
src/images/compute.svg
Normal file
5
src/images/compute.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="19" height="19" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 0H5V9H14V0ZM7 2H12V7H7V2Z" />
|
||||
<path d="M9 10V19H0V10H9ZM7 12H2V17H7V12Z" />
|
||||
<path d="M19 10V19H10V10H19ZM17 12H12V17H17V12Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 232 B |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user