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

Edit compute dataset (#417)

* WIP

* created form for editing compute privacy

* used editComputePrivacy method

* select and update trusted algorithm

* display and select multiple trusted algorithms

* fixed update when trusted algorithm list not changed

* code refactoring

* moved separator inside condition

* moved functions and interface from EditComputeDataset component

* moved algorithmOptions to parent component

* used AssetSelection to display algorithms

* use AssetSelection to select trusted algorithms

* getAlgorithmsOptions function review

* review fixes

* removed unused imports

* merge fixes

* AssetSelection style & usability tweaks

* use custom radio & checkbox styles
* add simple search for name & DID
* spacing adjustments

* copy updates, remove raw algo input, hardcode allowRawAlgorithm

* copy

* AssetSelection usability tweaks

* make rows clickable
* tweak layout, style and markup

* use formik set function to update values

* sorted algorithm list, added checked field

* sort assetSelection list on user select

* fix getAlgorithmsForAssetSelection breaking on empty responses

* form debug output

* another empty publisherTrustedAlgorithms fix

* created separate algorithms state for the form, sort list on edit

* refactor

* use Formik functionality wherever possible
* unify transforming form data to final data

* fix form debug transformation

* fix form submit, fix defaultChecked

* refactor

* use Formik functionality wherever possible
* unify transforming form data to final data

* fix form debug transformation

* fix form submit, fix defaultChecked

* disable assetSelection when allowAllAlgorithms is true

* added loader to AssetSelection

* changed allowAllAlgorithms to allowAllPublishedAlgorithms

* fixed lint error

* updated transformComputeFormToServiceComputePrivacy

* lint fix

* modify publish defaults

Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
Norbi 2021-03-25 09:34:07 +02:00 committed by GitHub
parent 2e9db9d170
commit 977a38e118
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 658 additions and 63 deletions

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": "allowAllPublishedAlgorithms",
"label": "All Algorithms",
"help": "Allow any published algorithm to run on this data set.",
"type": "checkbox",
"options": ["Allow any published algorithm"]
},
{
"name": "publisherTrustedAlgorithms",
"label": "Selected Algorithms",
"help": "Choose one or multiple algorithms you trust to allow them to run on this data set.",
"type": "assetSelectionMultiple",
"multiple": true,
"options": [],
"sortOptions": false
}
]
}
}

8
package-lock.json generated
View File

@ -3625,9 +3625,9 @@
"integrity": "sha512-6GrBk1jy+zxjDjh2SPra06etrqdp8CB6RaZaTq2OQpK8dA2Dq91hqCbj+6eb21MlU8bDY3/atnxax9rgPgsxkA==" "integrity": "sha512-6GrBk1jy+zxjDjh2SPra06etrqdp8CB6RaZaTq2OQpK8dA2Dq91hqCbj+6eb21MlU8bDY3/atnxax9rgPgsxkA=="
}, },
"@oceanprotocol/lib": { "@oceanprotocol/lib": {
"version": "0.12.0", "version": "0.12.1",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.12.0.tgz", "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.12.1.tgz",
"integrity": "sha512-bREJhiyQ1LlFdLY0WoZbelfH27R7PLi0pY+c3TiX3fYvDShfp5NCYkq0B8Wf4FjxUxd4BMJREwRNdOS416RYVA==", "integrity": "sha512-Cw4d4Di6GnL1gfZcYqemnKh1Agpc66AemIJANfbJpO08KkgiLEZhhMYsYQZzha+zQD9Jr4ZuzE/ZlYDr6S0o6w==",
"requires": { "requires": {
"@ethereum-navigator/navigator": "^0.5.2", "@ethereum-navigator/navigator": "^0.5.2",
"@oceanprotocol/contracts": "^0.5.10", "@oceanprotocol/contracts": "^0.5.10",
@ -16410,7 +16410,7 @@
} }
}, },
"ethereumjs-abi": { "ethereumjs-abi": {
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#1ce6a1d64235fabe2aaf827fd606def55693508f", "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#1a27c59c15ab1e95ee8e5c4ed6ad814c49cc439e",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": { "requires": {
"bn.js": "^4.11.8", "bn.js": "^4.11.8",

View File

@ -27,7 +27,7 @@
"@coingecko/cryptoformat": "^0.4.2", "@coingecko/cryptoformat": "^0.4.2",
"@loadable/component": "^5.14.1", "@loadable/component": "^5.14.1",
"@oceanprotocol/art": "^3.0.0", "@oceanprotocol/art": "^3.0.0",
"@oceanprotocol/lib": "^0.12.0", "@oceanprotocol/lib": "^0.12.1",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@portis/web3": "^3.0.3", "@portis/web3": "^3.0.3",
"@sindresorhus/slugify": "^1.0.0", "@sindresorhus/slugify": "^1.0.0",

View File

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

View File

@ -1,10 +1,13 @@
import { AssetSelectionAsset } from '../../molecules/FormFields/AssetSelection'
export interface FormFieldProps { export interface FormFieldProps {
label: string label: string
name: string name: string
type?: string type?: string
options?: string[] options?: string[] | AssetSelectionAsset[]
sortOptions?: boolean sortOptions?: boolean
required?: boolean required?: boolean
multiple?: boolean
disabled?: boolean disabled?: boolean
help?: string help?: string
placeholder?: string placeholder?: string

View File

@ -14,13 +14,14 @@ const cx = classNames.bind(styles)
const DefaultInput = ({ const DefaultInput = ({
size, size,
className,
prefix, prefix,
postfix, postfix,
additionalComponent, additionalComponent,
...props ...props
}: InputProps) => ( }: InputProps) => (
<input <input
className={cx({ input: true, [size]: size })} className={cx({ input: true, [size]: size, [className]: className })}
id={props.name} id={props.name}
{...props} {...props}
/> />
@ -36,13 +37,14 @@ export default function InputElement({
size, size,
field, field,
label, label,
multiple,
disabled,
help, help,
form, form,
additionalComponent, additionalComponent,
...props ...props
}: InputProps): ReactElement { }: InputProps): ReactElement {
const styleClasses = cx({ select: true, [size]: size }) const styleClasses = cx({ select: true, [size]: size })
switch (type) { switch (type) {
case 'select': { case 'select': {
const sortedOptions = const sortedOptions =
@ -50,7 +52,12 @@ export default function InputElement({
? options ? options
: options.sort((a: string, b: string) => a.localeCompare(b)) : options.sort((a: string, b: string) => a.localeCompare(b))
return ( return (
<select id={name} className={styleClasses} {...props}> <select
id={name}
className={styleClasses}
{...props}
multiple={multiple}
>
{field !== undefined && field.value === '' && ( {field !== undefined && field.value === '' && (
<option value="">---</option> <option value="">---</option>
)} )}
@ -106,6 +113,7 @@ export default function InputElement({
<AssetSelection <AssetSelection
assets={(options as unknown) as AssetSelectionAsset[]} assets={(options as unknown) as AssetSelectionAsset[]}
multiple multiple
disabled={disabled}
{...field} {...field}
{...props} {...props}
/> />
@ -126,6 +134,7 @@ export default function InputElement({
name={name} name={name}
type={type || 'text'} type={type || 'text'}
size={size} size={size}
disabled={disabled}
{...props} {...props}
/> />
{postfix && ( {postfix && (
@ -137,6 +146,7 @@ export default function InputElement({
name={name} name={name}
type={type || 'text'} type={type || 'text'}
size={size} size={size}
disabled={disabled}
{...props} {...props}
/> />
) )

View File

@ -41,6 +41,7 @@ export interface InputProps {
step?: string step?: string
defaultChecked?: boolean defaultChecked?: boolean
size?: 'mini' | 'small' | 'large' | 'default' size?: 'mini' | 'small' | 'large' | 'default'
className?: string
} }
export default function Input(props: Partial<InputProps>): ReactElement { export default function Input(props: Partial<InputProps>): ReactElement {

View File

@ -1,12 +1,26 @@
.selection { .selection {
padding: calc(var(--spacer) / 4); padding: 0;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
background-color: var(--background-highlight); background-color: var(--background-highlight);
border-radius: var(--border-radius); border-radius: var(--border-radius);
margin-bottom: calc(var(--spacer) / 2); margin-bottom: calc(var(--spacer) / 2);
font-size: var(--font-size-small); font-size: var(--font-size-small);
}
.disabled {
opacity: 0.5;
}
div [class*='loaderWrap'] {
margin: calc(var(--spacer) / 3);
}
.scroll {
border-top: 1px solid var(--border-color);
margin-top: calc(var(--spacer) / 4);
min-height: 200px;
max-height: 300px; max-height: 300px;
position: relative;
/* smooth overflow scrolling for pre-iOS 13 */ /* smooth overflow scrolling for pre-iOS 13 */
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
@ -14,44 +28,78 @@
.row { .row {
display: flex; display: flex;
align-items: flex-start; align-items: center;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding-top: calc(var(--spacer) / 4); padding: calc(var(--spacer) / 3) calc(var(--spacer) / 4);
padding-bottom: calc(var(--spacer) / 4);
}
.row > div {
width: 100%;
} }
.content { .content {
display: flex; display: flex;
align-items: baseline; align-items: center;
width: 100%;
margin-top: calc(var(--spacer) / 10);
}
.label {
display: block;
width: 100%; width: 100%;
} }
.input { .input {
align-self: flex-start;
min-width: 1.2rem;
margin-top: 0; margin-top: 0;
margin-left: 0; margin-left: 0;
margin-right: calc(var(--spacer) / 4); margin-right: calc(var(--spacer) / 4);
} }
.radio {
composes: radio from '../../atoms/Input/InputElement.module.css';
}
.checkbox {
composes: checkbox from '../../atoms/Input/InputElement.module.css';
}
.title { .title {
font-size: var(--font-size-small); font-size: var(--font-size-small);
margin: 0; margin-top: calc(var(--spacer) / 12);
margin-bottom: calc(var(--spacer) / 12);
} }
.link { .link {
margin-left: calc(var(--spacer) / 4); display: inline-block;
margin-left: calc(var(--spacer) / 8);
} }
.link svg { .link svg {
margin: 0;
fill: var(--color-primary); fill: var(--color-primary);
width: 0.7em; width: 0.7em;
height: 0.7em; height: 0.7em;
} }
.footer {
display: flex;
gap: var(--spacer);
justify-content: space-between;
margin-top: calc(var(--spacer) / 12);
}
.price { .price {
display: inline-block; white-space: pre;
font-size: var(--font-size-small) !important; font-size: var(--font-size-small) !important;
} }
.search {
margin: calc(var(--spacer) / 4);
width: calc(100% - calc(var(--spacer) / 2));
}
.did {
padding: 0;
font-size: var(--font-size-mini);
display: block;
text-align: left;
color: var(--color-secondary);
}

View File

@ -1,47 +1,85 @@
import React from 'react' import React, { ChangeEvent, useState } from 'react'
import Dotdotdot from 'react-dotdotdot' import Dotdotdot from 'react-dotdotdot'
import slugify from 'slugify' import slugify from 'slugify'
import classNames from 'classnames/bind'
import PriceUnit from '../../atoms/Price/PriceUnit' import PriceUnit from '../../atoms/Price/PriceUnit'
import { ReactComponent as External } from '../../../images/external.svg' import { ReactComponent as External } from '../../../images/external.svg'
import styles from './AssetSelection.module.css' import styles from './AssetSelection.module.css'
import InputElement from '../../atoms/Input/InputElement'
import Loader from '../../atoms/Loader'
const cx = classNames.bind(styles)
export interface AssetSelectionAsset { export interface AssetSelectionAsset {
did: string did: string
name: string name: string
price: string price: string
checked: boolean
} }
export default function AssetSelection({ export default function AssetSelection({
assets, assets,
multiple, multiple,
disabled,
...props ...props
}: { }: {
assets: AssetSelectionAsset[] assets: AssetSelectionAsset[]
multiple?: boolean multiple?: boolean
disabled?: boolean
}): JSX.Element { }): 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 ( return (
<div className={styles.selection}> <div className={`${styles.selection} ${disabled ? styles.disabled : ''}`}>
{assets.map((asset: AssetSelectionAsset) => ( <InputElement
type="search"
name="search"
size="small"
placeholder="Search by title or DID..."
value={searchValue}
onChange={handleSearchInput}
className={styles.search}
disabled={disabled}
/>
<div className={styles.scroll}>
{assets ? (
assets
.filter((asset: AssetSelectionAsset) =>
searchValue !== ''
? asset.name.toLowerCase().includes(searchValue) ||
asset.did.includes(searchValue)
: asset
)
.map((asset: AssetSelectionAsset) => (
<div className={styles.row} key={asset.did}> <div className={styles.row} key={asset.did}>
<input <input
id={slugify(asset.name)} id={slugify(asset.did)}
type={multiple ? 'checkbox' : 'radio'} type={multiple ? 'checkbox' : 'radio'}
value={asset.did} className={styleClassesInput}
className={styles.input} defaultChecked={asset.checked}
{...props} {...props}
disabled={disabled}
value={asset.did}
/> />
<div className={styles.content}>
<label <label
className={styles.label} className={styles.label}
htmlFor={slugify(asset.name)} htmlFor={slugify(asset.did)}
title={asset.name} title={asset.name}
> >
<Dotdotdot clamp={1} tagName="h3" className={styles.title}> <h3 className={styles.title}>
<Dotdotdot clamp={1} tagName="span">
{asset.name} {asset.name}
</Dotdotdot> </Dotdotdot>
<PriceUnit price={asset.price} small className={styles.price} />
</label>
<a <a
className={styles.link} className={styles.link}
href={`/asset/${asset.did}`} href={`/asset/${asset.did}`}
@ -50,9 +88,20 @@ export default function AssetSelection({
> >
<External /> <External />
</a> </a>
</h3>
<Dotdotdot clamp={1} tagName="code" className={styles.did}>
{asset.did}
</Dotdotdot>
</label>
<PriceUnit price={asset.price} small className={styles.price} />
</div> </div>
))
) : (
<Loader />
)}
</div> </div>
))}
</div> </div>
) )
} }

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

@ -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,76 @@
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 { getAlgorithmsForAssetSelection } from '../../../../utils/aquarius'
import { useAsset } from '../../../../providers/Asset'
import { ComputePrivacyForm } from '../../../../models/FormEditComputeDataset'
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
useEffect(() => {
getAlgorithmsForAssetSelection(
config.metadataCacheUri,
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-left: calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 2); margin-right: calc(var(--spacer) / 2);
} }
select[multiple] {
height: 130px;
}

View File

@ -8,3 +8,14 @@
margin-top: -1.5rem; margin-top: -1.5rem;
max-width: 50rem; 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

@ -8,7 +8,7 @@ import {
import { useAsset } from '../../../../providers/Asset' import { useAsset } from '../../../../providers/Asset'
import { useUserPreferences } from '../../../../providers/UserPreferences' import { useUserPreferences } from '../../../../providers/UserPreferences'
import { MetadataPreview } from '../../../molecules/MetadataPreview' import { MetadataPreview } from '../../../molecules/MetadataPreview'
import Debug from './Debug' import Debug from './DebugEditMetadata'
import Web3Feedback from '../../../molecules/Wallet/Feedback' import Web3Feedback from '../../../molecules/Wallet/Feedback'
import FormEditMetadata from './FormEditMetadata' import FormEditMetadata from './FormEditMetadata'
import { mapTimeoutStringToSeconds } from '../../../../utils/metadata' import { mapTimeoutStringToSeconds } from '../../../../utils/metadata'
@ -64,6 +64,9 @@ export default function Edit({
const [success, setSuccess] = useState<string>() const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const [timeoutStringValue, setTimeoutStringValue] = useState<string>() const [timeoutStringValue, setTimeoutStringValue] = useState<string>()
const timeout = ddo.findServiceByType('access')
? ddo.findServiceByType('access').attributes.main.timeout
: ddo.findServiceByType('compute').attributes.main.timeout
const hasFeedback = error || success const hasFeedback = error || success
@ -122,10 +125,7 @@ export default function Edit({
return ( return (
<Formik <Formik
initialValues={getInitialValues( initialValues={getInitialValues(metadata, timeout)}
metadata,
ddo.findServiceByType('access').attributes.main.timeout
)}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => { onSubmit={async (values, { resetForm }) => {
// move user's focus to top of screen // move user's focus to top of screen

View File

@ -42,3 +42,7 @@
margin-left: calc(var(--spacer) / 4); margin-left: calc(var(--spacer) / 4);
margin-right: 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 Markdown from '../../atoms/Markdown'
import MetaFull from './MetaFull' import MetaFull from './MetaFull'
import MetaSecondary from './MetaSecondary' import MetaSecondary from './MetaSecondary'
import styles from './index.module.css'
import AssetActions from '../AssetActions' import AssetActions from '../AssetActions'
import { useUserPreferences } from '../../../providers/UserPreferences' import { useUserPreferences } from '../../../providers/UserPreferences'
import Pricing from './Pricing' import Pricing from './Pricing'
@ -12,10 +11,12 @@ import { useAsset } from '../../../providers/Asset'
import Alert from '../../atoms/Alert' import Alert from '../../atoms/Alert'
import Button from '../../atoms/Button' import Button from '../../atoms/Button'
import Edit from '../AssetActions/Edit' import Edit from '../AssetActions/Edit'
import EditComputeDataset from '../AssetActions/Edit/EditComputeDataset'
import DebugOutput from '../../atoms/DebugOutput' import DebugOutput from '../../atoms/DebugOutput'
import MetaMain from './MetaMain' import MetaMain from './MetaMain'
import EditHistory from './EditHistory' import EditHistory from './EditHistory'
import { useWeb3 } from '../../../providers/Web3' import { useWeb3 } from '../../../providers/Web3'
import styles from './index.module.css'
export interface AssetContentProps { export interface AssetContentProps {
path?: string path?: string
@ -46,7 +47,9 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
const { owner, isInPurgatory, purgatoryData } = useAsset() const { owner, isInPurgatory, purgatoryData } = useAsset()
const [showPricing, setShowPricing] = useState(false) const [showPricing, setShowPricing] = useState(false)
const [showEdit, setShowEdit] = useState<boolean>() const [showEdit, setShowEdit] = useState<boolean>()
const [showEditCompute, setShowEditCompute] = useState<boolean>()
const { ddo, price, metadata } = useAsset() const { ddo, price, metadata } = useAsset()
const isOwner = accountId === owner const isOwner = accountId === owner
useEffect(() => { useEffect(() => {
@ -60,8 +63,15 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
setShowEdit(true) setShowEdit(true)
} }
function handleEditComputeButton() {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
setShowEditCompute(true)
}
return showEdit ? ( return showEdit ? (
<Edit setShowEdit={setShowEdit} /> <Edit setShowEdit={setShowEdit} />
) : showEditCompute ? (
<EditComputeDataset setShowEdit={setShowEditCompute} />
) : ( ) : (
<article className={styles.grid}> <article className={styles.grid}>
<div> <div>
@ -91,6 +101,18 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
<Button style="text" size="small" onClick={handleEditButton}> <Button style="text" size="small" onClick={handleEditButton}>
Edit Metadata Edit Metadata
</Button> </Button>
{ddo.findServiceByType('compute') && (
<>
<span className={styles.separator}>|</span>
<Button
style="text"
size="small"
onClick={handleEditComputeButton}
>
Edit Compute Settings
</Button>
</>
)}
</div> </div>
)} )}
</> </>

View File

@ -113,8 +113,9 @@ function usePublish(): UsePublish {
servers servers
) )
const origComputePrivacy: ServiceComputePrivacy = { const origComputePrivacy: ServiceComputePrivacy = {
allowRawAlgorithm: true, allowRawAlgorithm: false,
allowNetworkAccess: false, allowNetworkAccess: false,
allowAllPublishedAlgorithms: false,
publisherTrustedAlgorithms: [] publisherTrustedAlgorithms: []
} }
const computeService = ocean.compute.createComputeService( const computeService = ocean.compute.createComputeService(

View File

@ -0,0 +1,30 @@
import { ServiceComputePrivacy } from '@oceanprotocol/lib'
import * as Yup from 'yup'
export interface ComputePrivacyForm {
allowAllPublishedAlgorithms: boolean
publisherTrustedAlgorithms: string[]
}
export const validationSchema: Yup.SchemaOf<ComputePrivacyForm> = Yup.object().shape(
{
allowAllPublishedAlgorithms: Yup.boolean().nullable(),
publisherTrustedAlgorithms: Yup.array().nullable()
}
)
export function getInitialValues(
compute: ServiceComputePrivacy
): ComputePrivacyForm {
// TODO: ocean.js needs allowAllAlgoritms setting
const { allowAllPublishedAlgorithms, publisherTrustedAlgorithms } = compute
const publisherTrustedAlgorithmsForForm = (
publisherTrustedAlgorithms || []
).map((algo) => algo.did)
return {
allowAllPublishedAlgorithms,
publisherTrustedAlgorithms: publisherTrustedAlgorithmsForForm
}
}

View File

@ -1,9 +1,18 @@
import { DDO, DID, Logger } from '@oceanprotocol/lib' import {
Config,
DDO,
DID,
Logger,
publisherTrustedAlgorithm as PublisherTrustedAlgorithm
} from '@oceanprotocol/lib/'
import { import {
QueryResult, QueryResult,
SearchQuery SearchQuery
} from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache' } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import { AssetSelectionAsset } from '../components/molecules/FormFields/AssetSelection'
import axios, { CancelToken, AxiosResponse } from 'axios' import axios, { CancelToken, AxiosResponse } from 'axios'
import web3 from 'web3'
import { ConfigHelperConfig } from '@oceanprotocol/lib/dist/node/utils/ConfigHelper'
// TODO: import directly from ocean.js somehow. // TODO: import directly from ocean.js somehow.
// Transforming Aquarius' direct response is needed for getting actual DDOs // Transforming Aquarius' direct response is needed for getting actual DDOs
@ -97,3 +106,57 @@ export async function getAssetsNames(
} }
} }
} }
export async function getAlgorithmsForAssetSelection(
metadataCacheUri: string,
selectedAlgorithms?: PublisherTrustedAlgorithm[]
): Promise<AssetSelectionAsset[]> {
const query = {
page: 1,
query: {
query_string: {
query: `(service.attributes.main.type:algorithm) -isInPurgatory:true`
}
},
sort: { created: -1 }
}
const source = axios.CancelToken.source()
const didList: string[] = []
const priceList: any = {}
const result = await queryMetadata(
query as any,
metadataCacheUri,
source.token
)
result?.results?.forEach((ddo: DDO) => {
const did: string = web3.utils
.toChecksumAddress(ddo.dataToken)
.replace('0x', 'did:op:')
didList.push(did)
priceList[did] = ddo.price.value
})
const ddoNames = await getAssetsNames(didList, metadataCacheUri, source.token)
const algorithmList: AssetSelectionAsset[] = []
didList?.forEach((did: string) => {
let selected = false
selectedAlgorithms?.forEach((algorithm: PublisherTrustedAlgorithm) => {
if (algorithm.did === did) {
selected = true
}
})
selected
? algorithmList.unshift({
did: did,
name: ddoNames[did],
price: priceList[did],
checked: selected
})
: algorithmList.push({
did: did,
name: ddoNames[did],
price: priceList[did],
checked: selected
})
})
return algorithmList
}

41
src/utils/compute.ts Normal file
View File

@ -0,0 +1,41 @@
import {
DDO,
Ocean,
ServiceComputePrivacy,
publisherTrustedAlgorithm as PublisherTrustedAlgorithm
} from '@oceanprotocol/lib'
import { ComputePrivacyForm } from '../models/FormEditComputeDataset'
export async function createTrustedAlgorithmList(
selectedAlgorithms: string[], // list of DIDs
ocean: Ocean
): Promise<PublisherTrustedAlgorithm[]> {
const trustedAlgorithms = []
for (const selectedAlgorithm of selectedAlgorithms) {
const trustedAlgorithm = await ocean.compute.createPublisherTrustedAlgorithmfromDID(
selectedAlgorithm
)
trustedAlgorithms.push(trustedAlgorithm)
}
return trustedAlgorithms
}
export async function transformComputeFormToServiceComputePrivacy(
values: ComputePrivacyForm,
ocean: Ocean
): Promise<ServiceComputePrivacy> {
const { allowAllPublishedAlgorithms } = values
const publisherTrustedAlgorithms = values.allowAllPublishedAlgorithms
? []
: await createTrustedAlgorithmList(values.publisherTrustedAlgorithms, ocean)
const privacy: ServiceComputePrivacy = {
allowNetworkAccess: false,
allowRawAlgorithm: false,
allowAllPublishedAlgorithms,
publisherTrustedAlgorithms
}
return privacy
}