1
0
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:
Matthias Kretschmann 2021-04-29 12:02:47 +02:00 committed by GitHub
commit 020d4fa05a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
119 changed files with 6784 additions and 2453 deletions

2
.gitignore vendored
View File

@ -12,5 +12,5 @@ public/storybook
.artifacts
.vercel
repo-metadata.json
networks-metadata.json
content/networks-metadata.json
src/@types/apollo

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

View File

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

View File

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

View 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!"
}

View 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!"
}

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

View 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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,4 @@
export interface AlgorithmOption {
did: string
name: string
}

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
.action {
text-align: center;
display: block;
margin-top: calc(var(--spacer) / 2);
}

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -14,5 +14,5 @@
}
.info {
width: .85rem
}
width: 0.85rem;
}

View File

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

View File

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

View File

@ -11,3 +11,7 @@
.form label {
display: none;
}
.form input {
background-color: var(--background-content);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,3 +22,7 @@
margin-left: calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 2);
}
select[multiple] {
height: 130px;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,3 +10,8 @@
color: var(--color-secondary);
text-transform: uppercase;
}
.content {
word-wrap: break-word;
white-space: normal;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,3 +42,7 @@
margin-left: calc(var(--spacer) / 4);
margin-right: calc(var(--spacer) / 4);
}
.separator {
color: var(--color-secondary);
}

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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