From 977a38e118db7054b7cc4649b875acdc22c51390 Mon Sep 17 00:00:00 2001 From: Norbi <37236152+KatunaNorbert@users.noreply.github.com> Date: Thu, 25 Mar 2021 09:34:07 +0200 Subject: [PATCH] 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 --- content/pages/editComputeDataset.json | 27 +++ package-lock.json | 8 +- package.json | 2 +- src/@types/ComputeDataset.ts | 4 + src/@types/Form.d.ts | 5 +- src/components/atoms/Input/InputElement.tsx | 16 +- src/components/atoms/Input/index.tsx | 1 + .../FormFields/AssetSelection.module.css | 72 ++++++-- .../molecules/FormFields/AssetSelection.tsx | 117 +++++++++---- .../AssetActions/Edit/DebugEditCompute.tsx | 40 +++++ .../Edit/{Debug.tsx => DebugEditMetadata.tsx} | 0 .../AssetActions/Edit/EditComputeDataset.tsx | 161 ++++++++++++++++++ .../Edit/FormEditComputeDataset.tsx | 76 +++++++++ .../Edit/FormEditMetadata.module.css | 4 + .../AssetActions/Edit/index.module.css | 11 ++ .../organisms/AssetActions/Edit/index.tsx | 10 +- .../organisms/AssetContent/index.module.css | 4 + .../organisms/AssetContent/index.tsx | 24 ++- src/hooks/usePublish.ts | 3 +- src/models/FormEditComputeDataset.ts | 30 ++++ src/utils/aquarius.ts | 65 ++++++- src/utils/compute.ts | 41 +++++ 22 files changed, 658 insertions(+), 63 deletions(-) create mode 100644 content/pages/editComputeDataset.json create mode 100644 src/@types/ComputeDataset.ts create mode 100644 src/components/organisms/AssetActions/Edit/DebugEditCompute.tsx rename src/components/organisms/AssetActions/Edit/{Debug.tsx => DebugEditMetadata.tsx} (100%) create mode 100644 src/components/organisms/AssetActions/Edit/EditComputeDataset.tsx create mode 100644 src/components/organisms/AssetActions/Edit/FormEditComputeDataset.tsx create mode 100644 src/models/FormEditComputeDataset.ts create mode 100644 src/utils/compute.ts diff --git a/content/pages/editComputeDataset.json b/content/pages/editComputeDataset.json new file mode 100644 index 000000000..17c3c6d50 --- /dev/null +++ b/content/pages/editComputeDataset.json @@ -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 + } + ] + } +} diff --git a/package-lock.json b/package-lock.json index 5c5156dda..14624660c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3625,9 +3625,9 @@ "integrity": "sha512-6GrBk1jy+zxjDjh2SPra06etrqdp8CB6RaZaTq2OQpK8dA2Dq91hqCbj+6eb21MlU8bDY3/atnxax9rgPgsxkA==" }, "@oceanprotocol/lib": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.12.0.tgz", - "integrity": "sha512-bREJhiyQ1LlFdLY0WoZbelfH27R7PLi0pY+c3TiX3fYvDShfp5NCYkq0B8Wf4FjxUxd4BMJREwRNdOS416RYVA==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.12.1.tgz", + "integrity": "sha512-Cw4d4Di6GnL1gfZcYqemnKh1Agpc66AemIJANfbJpO08KkgiLEZhhMYsYQZzha+zQD9Jr4ZuzE/ZlYDr6S0o6w==", "requires": { "@ethereum-navigator/navigator": "^0.5.2", "@oceanprotocol/contracts": "^0.5.10", @@ -16410,7 +16410,7 @@ } }, "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", "requires": { "bn.js": "^4.11.8", diff --git a/package.json b/package.json index 5c5c9e804..01474a108 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@coingecko/cryptoformat": "^0.4.2", "@loadable/component": "^5.14.1", "@oceanprotocol/art": "^3.0.0", - "@oceanprotocol/lib": "^0.12.0", + "@oceanprotocol/lib": "^0.12.1", "@oceanprotocol/typographies": "^0.1.0", "@portis/web3": "^3.0.3", "@sindresorhus/slugify": "^1.0.0", diff --git a/src/@types/ComputeDataset.ts b/src/@types/ComputeDataset.ts new file mode 100644 index 000000000..26e5caf96 --- /dev/null +++ b/src/@types/ComputeDataset.ts @@ -0,0 +1,4 @@ +export interface AlgorithmOption { + did: string + name: string +} diff --git a/src/@types/Form.d.ts b/src/@types/Form.d.ts index 5e757c86c..929061e77 100644 --- a/src/@types/Form.d.ts +++ b/src/@types/Form.d.ts @@ -1,10 +1,13 @@ +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 diff --git a/src/components/atoms/Input/InputElement.tsx b/src/components/atoms/Input/InputElement.tsx index d1842e0e5..71f515bc2 100644 --- a/src/components/atoms/Input/InputElement.tsx +++ b/src/components/atoms/Input/InputElement.tsx @@ -14,13 +14,14 @@ const cx = classNames.bind(styles) const DefaultInput = ({ size, + className, prefix, postfix, additionalComponent, ...props }: InputProps) => ( @@ -36,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 = @@ -50,7 +52,12 @@ export default function InputElement({ ? options : options.sort((a: string, b: string) => a.localeCompare(b)) return ( - {field !== undefined && field.value === '' && ( )} @@ -106,6 +113,7 @@ export default function InputElement({ @@ -126,6 +134,7 @@ export default function InputElement({ name={name} type={type || 'text'} size={size} + disabled={disabled} {...props} /> {postfix && ( @@ -137,6 +146,7 @@ export default function InputElement({ name={name} type={type || 'text'} size={size} + disabled={disabled} {...props} /> ) diff --git a/src/components/atoms/Input/index.tsx b/src/components/atoms/Input/index.tsx index 55228fb33..0aead33ab 100644 --- a/src/components/atoms/Input/index.tsx +++ b/src/components/atoms/Input/index.tsx @@ -41,6 +41,7 @@ export interface InputProps { step?: string defaultChecked?: boolean size?: 'mini' | 'small' | 'large' | 'default' + className?: string } export default function Input(props: Partial): ReactElement { diff --git a/src/components/molecules/FormFields/AssetSelection.module.css b/src/components/molecules/FormFields/AssetSelection.module.css index f4d24b250..43a78cd2c 100644 --- a/src/components/molecules/FormFields/AssetSelection.module.css +++ b/src/components/molecules/FormFields/AssetSelection.module.css @@ -1,12 +1,26 @@ .selection { - padding: calc(var(--spacer) / 4); + 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); +} +.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; + position: relative; /* smooth overflow scrolling for pre-iOS 13 */ overflow: auto; -webkit-overflow-scrolling: touch; @@ -14,44 +28,78 @@ .row { display: flex; - align-items: flex-start; + align-items: center; border-bottom: 1px solid var(--border-color); - padding-top: calc(var(--spacer) / 4); - padding-bottom: calc(var(--spacer) / 4); -} - -.row > div { - width: 100%; + padding: calc(var(--spacer) / 3) calc(var(--spacer) / 4); } .content { display: flex; - align-items: baseline; + align-items: center; + width: 100%; + margin-top: calc(var(--spacer) / 10); +} + +.label { + display: block; width: 100%; } .input { + align-self: flex-start; + min-width: 1.2rem; margin-top: 0; margin-left: 0; 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 { font-size: var(--font-size-small); - margin: 0; + margin-top: calc(var(--spacer) / 12); + margin-bottom: calc(var(--spacer) / 12); } .link { - margin-left: calc(var(--spacer) / 4); + 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 { - display: inline-block; + white-space: pre; 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); +} diff --git a/src/components/molecules/FormFields/AssetSelection.tsx b/src/components/molecules/FormFields/AssetSelection.tsx index a6813376f..aad3e00c3 100644 --- a/src/components/molecules/FormFields/AssetSelection.tsx +++ b/src/components/molecules/FormFields/AssetSelection.tsx @@ -1,58 +1,107 @@ -import React from 'react' +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 styles from './AssetSelection.module.css' +import InputElement from '../../atoms/Input/InputElement' +import Loader from '../../atoms/Loader' + +const cx = classNames.bind(styles) export interface AssetSelectionAsset { did: string name: string price: string + checked: boolean } export default function AssetSelection({ assets, multiple, + disabled, ...props }: { assets: AssetSelectionAsset[] multiple?: boolean + disabled?: boolean }): JSX.Element { - return ( -
- {assets.map((asset: AssetSelectionAsset) => ( -
- -
- + const [searchValue, setSearchValue] = useState('') - - - -
-
- ))} + const styleClassesInput = cx({ + input: true, + [styles.checkbox]: multiple, + [styles.radio]: !multiple + }) + + function handleSearchInput(e: ChangeEvent) { + setSearchValue(e.target.value) + } + + return ( +
+ +
+ {assets ? ( + assets + .filter((asset: AssetSelectionAsset) => + searchValue !== '' + ? asset.name.toLowerCase().includes(searchValue) || + asset.did.includes(searchValue) + : asset + ) + .map((asset: AssetSelectionAsset) => ( +
+ + + + +
+ )) + ) : ( + + )} +
) } diff --git a/src/components/organisms/AssetActions/Edit/DebugEditCompute.tsx b/src/components/organisms/AssetActions/Edit/DebugEditCompute.tsx new file mode 100644 index 000000000..7f5698776 --- /dev/null +++ b/src/components/organisms/AssetActions/Edit/DebugEditCompute.tsx @@ -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() + + useEffect(() => { + if (!ocean) return + + async function transformValues() { + const privacy = await transformComputeFormToServiceComputePrivacy( + values, + ocean + ) + setFormTransformed(privacy) + } + transformValues() + }, [values, ddo, ocean]) + + return ( + <> + + + + ) +} diff --git a/src/components/organisms/AssetActions/Edit/Debug.tsx b/src/components/organisms/AssetActions/Edit/DebugEditMetadata.tsx similarity index 100% rename from src/components/organisms/AssetActions/Edit/Debug.tsx rename to src/components/organisms/AssetActions/Edit/DebugEditMetadata.tsx diff --git a/src/components/organisms/AssetActions/Edit/EditComputeDataset.tsx b/src/components/organisms/AssetActions/Edit/EditComputeDataset.tsx new file mode 100644 index 000000000..8af2bbe37 --- /dev/null +++ b/src/components/organisms/AssetActions/Edit/EditComputeDataset.tsx @@ -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() + const [error, setError] = useState() + + 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 ( + { + // 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 ? ( + { + await refreshDdo() + setShowEdit(false) + } + }} + /> + ) : ( + <> +

{content.description}

+
+ +
+ + {debug === true && ( +
+ +
+ )} + + ) + } +
+ ) +} diff --git a/src/components/organisms/AssetActions/Edit/FormEditComputeDataset.tsx b/src/components/organisms/AssetActions/Edit/FormEditComputeDataset.tsx new file mode 100644 index 000000000..40409fcfa --- /dev/null +++ b/src/components/organisms/AssetActions/Edit/FormEditComputeDataset.tsx @@ -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 = useFormikContext() + const [allAlgorithms, setAllAlgorithms] = useState() + + const { publisherTrustedAlgorithms } = ddo?.findServiceByType( + 'compute' + ).attributes.main.privacy + + useEffect(() => { + getAlgorithmsForAssetSelection( + config.metadataCacheUri, + publisherTrustedAlgorithms + ).then((algorithms) => { + setAllAlgorithms(algorithms) + }) + }, [config.metadataCacheUri, publisherTrustedAlgorithms]) + + return ( +
+

{title}

+ {data.map((field: FormFieldProps) => ( + + ))} +
+ + +
+ + ) +} diff --git a/src/components/organisms/AssetActions/Edit/FormEditMetadata.module.css b/src/components/organisms/AssetActions/Edit/FormEditMetadata.module.css index f42d3a152..4bb393ac4 100644 --- a/src/components/organisms/AssetActions/Edit/FormEditMetadata.module.css +++ b/src/components/organisms/AssetActions/Edit/FormEditMetadata.module.css @@ -22,3 +22,7 @@ margin-left: calc(var(--spacer) / 2); margin-right: calc(var(--spacer) / 2); } + +select[multiple] { + height: 130px; +} diff --git a/src/components/organisms/AssetActions/Edit/index.module.css b/src/components/organisms/AssetActions/Edit/index.module.css index ea4143cbe..740b8e04b 100644 --- a/src/components/organisms/AssetActions/Edit/index.module.css +++ b/src/components/organisms/AssetActions/Edit/index.module.css @@ -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; +} diff --git a/src/components/organisms/AssetActions/Edit/index.tsx b/src/components/organisms/AssetActions/Edit/index.tsx index 9ad4257f6..6f526469c 100644 --- a/src/components/organisms/AssetActions/Edit/index.tsx +++ b/src/components/organisms/AssetActions/Edit/index.tsx @@ -8,7 +8,7 @@ import { import { useAsset } from '../../../../providers/Asset' import { useUserPreferences } from '../../../../providers/UserPreferences' import { MetadataPreview } from '../../../molecules/MetadataPreview' -import Debug from './Debug' +import Debug from './DebugEditMetadata' import Web3Feedback from '../../../molecules/Wallet/Feedback' import FormEditMetadata from './FormEditMetadata' import { mapTimeoutStringToSeconds } from '../../../../utils/metadata' @@ -64,6 +64,9 @@ export default function Edit({ const [success, setSuccess] = useState() const [error, setError] = useState() const [timeoutStringValue, setTimeoutStringValue] = useState() + const timeout = ddo.findServiceByType('access') + ? ddo.findServiceByType('access').attributes.main.timeout + : ddo.findServiceByType('compute').attributes.main.timeout const hasFeedback = error || success @@ -122,10 +125,7 @@ export default function Edit({ return ( { // move user's focus to top of screen diff --git a/src/components/organisms/AssetContent/index.module.css b/src/components/organisms/AssetContent/index.module.css index bc9ef4f35..b54048e13 100644 --- a/src/components/organisms/AssetContent/index.module.css +++ b/src/components/organisms/AssetContent/index.module.css @@ -42,3 +42,7 @@ margin-left: calc(var(--spacer) / 4); margin-right: calc(var(--spacer) / 4); } + +.separator { + color: var(--color-secondary); +} diff --git a/src/components/organisms/AssetContent/index.tsx b/src/components/organisms/AssetContent/index.tsx index 7a6e7ff1e..1e6e88d9c 100644 --- a/src/components/organisms/AssetContent/index.tsx +++ b/src/components/organisms/AssetContent/index.tsx @@ -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,7 +47,9 @@ export default function AssetContent(props: AssetContentProps): ReactElement { const { owner, isInPurgatory, purgatoryData } = useAsset() const [showPricing, setShowPricing] = useState(false) const [showEdit, setShowEdit] = useState() + const [showEditCompute, setShowEditCompute] = useState() const { ddo, price, metadata } = useAsset() + const isOwner = accountId === owner useEffect(() => { @@ -60,8 +63,15 @@ export default function AssetContent(props: AssetContentProps): ReactElement { setShowEdit(true) } + function handleEditComputeButton() { + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) + setShowEditCompute(true) + } + return showEdit ? ( + ) : showEditCompute ? ( + ) : (
@@ -91,6 +101,18 @@ export default function AssetContent(props: AssetContentProps): ReactElement { + {ddo.findServiceByType('compute') && ( + <> + | + + + )}
)} diff --git a/src/hooks/usePublish.ts b/src/hooks/usePublish.ts index 6ef5cbda8..bf0192b3b 100644 --- a/src/hooks/usePublish.ts +++ b/src/hooks/usePublish.ts @@ -113,8 +113,9 @@ function usePublish(): UsePublish { servers ) const origComputePrivacy: ServiceComputePrivacy = { - allowRawAlgorithm: true, + allowRawAlgorithm: false, allowNetworkAccess: false, + allowAllPublishedAlgorithms: false, publisherTrustedAlgorithms: [] } const computeService = ocean.compute.createComputeService( diff --git a/src/models/FormEditComputeDataset.ts b/src/models/FormEditComputeDataset.ts new file mode 100644 index 000000000..6d517def8 --- /dev/null +++ b/src/models/FormEditComputeDataset.ts @@ -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 = 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 + } +} diff --git a/src/utils/aquarius.ts b/src/utils/aquarius.ts index 0e88a7949..2aa4f277d 100644 --- a/src/utils/aquarius.ts +++ b/src/utils/aquarius.ts @@ -1,9 +1,18 @@ -import { DDO, DID, Logger } from '@oceanprotocol/lib' +import { + Config, + DDO, + DID, + Logger, + publisherTrustedAlgorithm as PublisherTrustedAlgorithm +} from '@oceanprotocol/lib/' import { QueryResult, SearchQuery } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache' +import { AssetSelectionAsset } from '../components/molecules/FormFields/AssetSelection' 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. // 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 { + 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 +} diff --git a/src/utils/compute.ts b/src/utils/compute.ts new file mode 100644 index 000000000..c3483d916 --- /dev/null +++ b/src/utils/compute.ts @@ -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 { + 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 { + const { allowAllPublishedAlgorithms } = values + const publisherTrustedAlgorithms = values.allowAllPublishedAlgorithms + ? [] + : await createTrustedAlgorithmList(values.publisherTrustedAlgorithms, ocean) + + const privacy: ServiceComputePrivacy = { + allowNetworkAccess: false, + allowRawAlgorithm: false, + allowAllPublishedAlgorithms, + publisherTrustedAlgorithms + } + + return privacy +}