diff --git a/.env.example b/.env.example index 12eac2c37..91f93b378 100644 --- a/.env.example +++ b/.env.example @@ -10,4 +10,6 @@ GATSBY_NETWORK="rinkeby" #GATSBY_ANALYTICS_ID="xxx" #GATSBY_PORTIS_ID="xxx" #GATSBY_ALLOW_FIXED_PRICING="true" -#GATSBY_ALLOW_DYNAMIC_PRICING="true" \ No newline at end of file +#GATSBY_ALLOW_DYNAMIC_PRICING="true" +#GATSBY_ALLOW_ADVANCED_SETTINGS="true" +#GATSBY_CREDENTIAL_TYPE="address" diff --git a/app.config.js b/app.config.js index ba3a87393..129d2076f 100644 --- a/app.config.js +++ b/app.config.js @@ -44,5 +44,9 @@ module.exports = { // Used to show or hide the fixed and dynamic price options // tab to publishers during the price creation. allowFixedPricing: process.env.GATSBY_ALLOW_FIXED_PRICING || 'true', - allowDynamicPricing: process.env.GATSBY_ALLOW_DYNAMIC_PRICING || 'true' + allowDynamicPricing: process.env.GATSBY_ALLOW_DYNAMIC_PRICING || 'true', + + // Used to show or hide advanced settings button in asset details page + allowAdvancedSettings: process.env.GATSBY_ALLOW_ADVANCED_SETTINGS || 'false', + credentialType: process.env.GATSBY_CREDENTIAL_TYPE || 'address' } diff --git a/content/pages/editAdvancedSettings.json b/content/pages/editAdvancedSettings.json new file mode 100644 index 000000000..1e2282a30 --- /dev/null +++ b/content/pages/editAdvancedSettings.json @@ -0,0 +1,31 @@ +{ + "description": "Update advanced settings of this data set. Updating these settings will create an on-chain transaction you have to approve in your wallet.", + "form": { + "success": "🎉 Successfully updated. 🎉", + "successAction": "Close", + "error": "Updating DDO failed.", + "data": [ + { + "name": "allow", + "label": "Allow ETH Address", + "placeholder": "e.g. 0x12345678901234567890abcd", + "help": "Enter ETH address and click ADD button to append the list. Only ETH address in allow list can consume this asset. If the list is empty means anyone can download or compute this asset", + "type": "credentials" + }, + { + "name": "deny", + "label": "Deny ETH Address", + "placeholder": "e.g. 0x12345678901234567890abcd", + "help": "Enter ETH address and click ADD button to append the list. If ETH address is fall under deny list, download or compute of this asset is denied", + "type": "credentials" + }, + { + "name": "isOrderDisabled", + "label": "Disable Consumption", + "help": "Disable dataset being download or compute when dataset undergoing maintenance.", + "type": "checkbox", + "options": ["Disable"] + } + ] + } +} diff --git a/src/components/atoms/Input/InputElement.tsx b/src/components/atoms/Input/InputElement.tsx index 39637a8ff..966407381 100644 --- a/src/components/atoms/Input/InputElement.tsx +++ b/src/components/atoms/Input/InputElement.tsx @@ -12,6 +12,7 @@ import classNames from 'classnames/bind' import AssetSelection, { AssetSelectionAsset } from '../../molecules/FormFields/AssetSelection' +import Credentials from '../../molecules/FormFields/Credential' const cx = classNames.bind(styles) @@ -137,6 +138,8 @@ export default function InputElement({ {...props} /> ) + case 'credentials': + return default: return prefix || postfix ? (
diff --git a/src/components/molecules/FormFields/Credential/Credential.module.css b/src/components/molecules/FormFields/Credential/Credential.module.css new file mode 100644 index 000000000..1a76ef143 --- /dev/null +++ b/src/components/molecules/FormFields/Credential/Credential.module.css @@ -0,0 +1,40 @@ +.chip { + border: 1px solid var(--border-color); + display: flex; + padding-left: 10px; +} + +.buttonWrapper { + width: 100%; + text-align: right; +} + +.crossButton { + min-width: 0; +} + +.crossButton svg { + display: inline-block; + width: var(--font-size-large); + height: var(--font-size-large); + fill: var(--brand-pink); + vertical-align: middle; +} + +.scroll { + border-top: 1px solid var(--border-color); + min-height: fit-content; + max-height: 200px; + position: relative; + overflow: auto; + -webkit-overflow-scrolling: touch; +} + +.credential { + padding: 0; + border: 1px solid var(--border-color); + background-color: var(--background-highlight); + border-radius: var(--border-radius); + font-size: var(--font-size-small); + min-height: 200px; +} diff --git a/src/components/molecules/FormFields/Credential/index.tsx b/src/components/molecules/FormFields/Credential/index.tsx new file mode 100644 index 000000000..3aa21085d --- /dev/null +++ b/src/components/molecules/FormFields/Credential/index.tsx @@ -0,0 +1,81 @@ +import { useField } from 'formik' +import { InputProps } from '../../../atoms/Input' +import React, { useState, ChangeEvent, FormEvent, useEffect } from 'react' +import InputGroup from '../../../atoms/Input/InputGroup' +import Button from '../../../atoms/Button' +import styles from './Credential.module.css' +import { isAddress } from 'web3-utils' +import { toast } from 'react-toastify' +import { ReactComponent as Cross } from '../../../../images/cross.svg' +import InputElement from '../../../atoms/Input/InputElement' + +export default function Credentials(props: InputProps) { + const [field, meta, helpers] = useField(props.name) + const [arrayInput, setArrayInput] = useState(field.value || []) + const [value, setValue] = useState('') + + useEffect(() => { + helpers.setValue(arrayInput) + }, [arrayInput]) + + function handleDeleteChip(value: string) { + const newInput = arrayInput.filter((input) => input !== value) + setArrayInput(newInput) + helpers.setValue(newInput) + } + + function handleAddValue(e: FormEvent) { + e.preventDefault() + if (!isAddress(value)) { + toast.error('Wallet address is invalid') + return + } + if (arrayInput.includes(value)) { + toast.error('Wallet address already added into list') + return + } + setArrayInput((arrayInput) => [...arrayInput, value]) + setValue('') + } + + return ( +
+ + ) => + setValue(e.target.value) + } + /> + + +
+ {arrayInput && + arrayInput.map((value) => { + return ( +
+ {value} + + + +
+ ) + })} +
+
+ ) +} diff --git a/src/components/organisms/AssetActions/Edit/DebugEditAdvancedSettings.tsx b/src/components/organisms/AssetActions/Edit/DebugEditAdvancedSettings.tsx new file mode 100644 index 000000000..5c1f03a23 --- /dev/null +++ b/src/components/organisms/AssetActions/Edit/DebugEditAdvancedSettings.tsx @@ -0,0 +1,48 @@ +import { DDO, Credentials, CredentialType } from '@oceanprotocol/lib' +import React, { ReactElement, useEffect, useState } from 'react' +import { AdvancedSettingsForm } from '../../../../models/FormEditCredential' +import { useOcean } from '../../../../providers/Ocean' +import DebugOutput from '../../../atoms/DebugOutput' + +export interface AdvancedSettings { + credentail: Credentials + isOrderDisabled: boolean +} + +export default function DebugEditCredential({ + values, + ddo, + credentialType +}: { + values: AdvancedSettingsForm + ddo: DDO + credentialType: CredentialType +}): ReactElement { + const { ocean } = useOcean() + const [advancedSettings, setAdvancedSettings] = useState() + + useEffect(() => { + if (!ocean) return + + async function transformValues() { + const newDdo = await ocean.assets.updateCredentials( + ddo, + credentialType, + values.allow, + values.deny + ) + setAdvancedSettings({ + credentail: newDdo.credentials, + isOrderDisabled: values.isOrderDisabled + }) + } + transformValues() + }, [values, ddo, ocean]) + + return ( + <> + + + + ) +} diff --git a/src/components/organisms/AssetActions/Edit/EditAdvancedSettings.tsx b/src/components/organisms/AssetActions/Edit/EditAdvancedSettings.tsx new file mode 100644 index 000000000..6c1bd2d9b --- /dev/null +++ b/src/components/organisms/AssetActions/Edit/EditAdvancedSettings.tsx @@ -0,0 +1,163 @@ +import { Formik } from 'formik' +import React, { ReactElement, useState } from 'react' +import { useAsset } from '../../../../providers/Asset' +import { useUserPreferences } from '../../../../providers/UserPreferences' +import styles from './index.module.css' +import { Logger, CredentialType, DDO } from '@oceanprotocol/lib' +import MetadataFeedback from '../../../molecules/MetadataFeedback' +import { graphql, useStaticQuery } from 'gatsby' +import { useWeb3 } from '../../../../providers/Web3' +import { useOcean } from '../../../../providers/Ocean' +import FormAdvancedSettings from './FormAdvancedSettings' +import { + AdvancedSettingsForm, + getInitialValues, + validationSchema +} from '../../../../models/FormEditCredential' +import DebugEditCredential from './DebugEditAdvancedSettings' +import { useSiteMetadata } from '../../../../hooks/useSiteMetadata' + +const contentQuery = graphql` + query EditAvanceSettingsQuery { + content: allFile( + filter: { relativePath: { eq: "pages/editAdvancedSettings.json" } } + ) { + edges { + node { + childPagesJson { + description + form { + success + successAction + error + data { + name + placeholder + label + help + type + options + } + } + } + } + } + } + } +` + +function getDefaultCredentialType(credentialType: string): CredentialType { + switch (credentialType) { + case 'address': + return CredentialType.address + case 'credential3Box': + return CredentialType.credential3Box + default: + return CredentialType.address + } +} + +export default function EditAdvancedSettings({ + setShowEdit +}: { + setShowEdit: (show: boolean) => void +}): ReactElement { + const data = useStaticQuery(contentQuery) + const content = data.content.edges[0].node.childPagesJson + + const { debug } = useUserPreferences() + const { accountId } = useWeb3() + const { ocean } = useOcean() + const { metadata, ddo, refreshDdo } = useAsset() + const [success, setSuccess] = useState() + const [error, setError] = useState() + const { appConfig } = useSiteMetadata() + + const hasFeedback = error || success + + const credentialType = getDefaultCredentialType(appConfig.credentialType) + + async function handleSubmit( + values: Partial, + resetForm: () => void + ) { + try { + let newDdo: DDO + newDdo = await ocean.assets.updateCredentials( + ddo, + credentialType, + values.allow, + values.deny + ) + + newDdo = await ocean.assets.editMetadata(newDdo, { + status: { + isOrderDisabled: values.isOrderDisabled + } + }) + + const storedddo = await ocean.assets.updateMetadata(newDdo, accountId) + + if (!storedddo) { + setError(content.form.error) + Logger.error(content.form.error) + return + } else { + setSuccess(content.form.success) + resetForm() + } + } catch (error) { + Logger.error(error.message) + setError(error.message) + } + } + + return ( + { + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) + await handleSubmit(values, resetForm) + }} + > + {({ isSubmitting, values }) => + isSubmitting || hasFeedback ? ( + { + await refreshDdo() + setShowEdit(false) + } + }} + /> + ) : ( + <> +

{content.description}

+
+ +
+ + {debug === true && ( +
+ +
+ )} + + ) + } +
+ ) +} diff --git a/src/components/organisms/AssetActions/Edit/FormAdvancedSettings.tsx b/src/components/organisms/AssetActions/Edit/FormAdvancedSettings.tsx new file mode 100644 index 000000000..a67186abf --- /dev/null +++ b/src/components/organisms/AssetActions/Edit/FormAdvancedSettings.tsx @@ -0,0 +1,59 @@ +import React, { ChangeEvent, ReactElement } from 'react' +import styles from './FormEditMetadata.module.css' +import { Field, Form, FormikContextType, useFormikContext } from 'formik' +import Button from '../../../atoms/Button' +import Input from '../../../atoms/Input' +import { FormFieldProps } from '../../../../@types/Form' +import { useOcean } from '../../../../providers/Ocean' +import { useWeb3 } from '../../../../providers/Web3' +import { AdvancedSettingsForm } from '../../../../models/FormEditCredential' + +export default function FormAdvancedSettings({ + data, + setShowEdit +}: { + data: FormFieldProps[] + setShowEdit: (show: boolean) => void +}): ReactElement { + const { accountId } = useWeb3() + const { ocean, config } = useOcean() + const { + isValid, + validateField, + setFieldValue + }: FormikContextType> = useFormikContext() + + function handleFieldChange( + e: ChangeEvent, + field: FormFieldProps + ) { + validateField(field.name) + if (e.target.type === 'checkbox') + setFieldValue(field.name, e.target.checked) + else setFieldValue(field.name, e.target.value) + } + + return ( +
+ {data.map((field: FormFieldProps) => ( + ) => + handleFieldChange(e, field) + } + /> + ))} + +
+ + +
+ + ) +} diff --git a/src/components/organisms/AssetContent/index.tsx b/src/components/organisms/AssetContent/index.tsx index abd48cbe6..3f695b864 100644 --- a/src/components/organisms/AssetContent/index.tsx +++ b/src/components/organisms/AssetContent/index.tsx @@ -17,6 +17,8 @@ import MetaMain from './MetaMain' import EditHistory from './EditHistory' import { useWeb3 } from '../../../providers/Web3' import styles from './index.module.css' +import EditAdvancedSettings from '../AssetActions/Edit/EditAdvancedSettings' +import { useSiteMetadata } from '../../../hooks/useSiteMetadata' export interface AssetContentProps { path?: string @@ -48,8 +50,11 @@ export default function AssetContent(props: AssetContentProps): ReactElement { const [showPricing, setShowPricing] = useState(false) const [showEdit, setShowEdit] = useState() const [showEditCompute, setShowEditCompute] = useState() + const [showEditAdvancedSettings, setShowEditAdvancedSettings] = + useState() const [isOwner, setIsOwner] = useState(false) const { ddo, price, metadata, type } = useAsset() + const { appConfig } = useSiteMetadata() useEffect(() => { if (!accountId || !owner) return @@ -70,10 +75,17 @@ export default function AssetContent(props: AssetContentProps): ReactElement { setShowEditCompute(true) } + function handleEditAdvancedSettingsButton() { + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) + setShowEditAdvancedSettings(true) + } + return showEdit ? ( ) : showEditCompute ? ( + ) : showEditAdvancedSettings ? ( + ) : (
@@ -103,6 +115,18 @@ export default function AssetContent(props: AssetContentProps): ReactElement { + {appConfig.allowAdvancedSettings === 'true' && ( + <> + | + + + )} {ddo.findServiceByType('compute') && type === 'dataset' && ( <> | diff --git a/src/hooks/useSiteMetadata.ts b/src/hooks/useSiteMetadata.ts index 0db181ea0..30ed596bd 100644 --- a/src/hooks/useSiteMetadata.ts +++ b/src/hooks/useSiteMetadata.ts @@ -27,6 +27,8 @@ interface UseSiteMetadata { portisId: string allowFixedPricing: string allowDynamicPricing: string + allowAdvancedSettings: string + credentialType: string } } @@ -59,6 +61,8 @@ const query = graphql` portisId allowFixedPricing allowDynamicPricing + allowAdvancedSettings + credentialType } } } diff --git a/src/images/cross.svg b/src/images/cross.svg new file mode 100644 index 000000000..448fdee46 --- /dev/null +++ b/src/images/cross.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/models/FormEditCredential.ts b/src/models/FormEditCredential.ts new file mode 100644 index 000000000..c8da10d3a --- /dev/null +++ b/src/models/FormEditCredential.ts @@ -0,0 +1,72 @@ +import { + CredentialAction, + Credential, + Credentials, + CredentialType, + DDO +} from '@oceanprotocol/lib' +import * as Yup from 'yup' + +export interface AdvancedSettingsForm { + allow: string[] + deny: string[] + isOrderDisabled: boolean +} + +export const validationSchema: Yup.SchemaOf = + Yup.object().shape({ + allow: Yup.array().nullable(), + deny: Yup.array().nullable(), + isOrderDisabled: Yup.boolean().nullable() + }) + +function getCredentialList( + credential: Credential[], + credentialType: CredentialType +): string[] { + const credentialByType = credential.find( + (credential) => credential.type === credentialType + ) + return credentialByType.value && credentialByType.value.length > 0 + ? credentialByType.value + : [] +} + +function getAssetCredentials( + credentials: Credentials, + credentialType: CredentialType, + credentialAction: CredentialAction +): string[] { + if (!credentials) return [] + + if (credentialAction === 'allow') { + return credentials.allow + ? getCredentialList(credentials.allow, credentialType) + : [] + } + return credentials.deny + ? getCredentialList(credentials.deny, credentialType) + : [] +} + +export function getInitialValues( + ddo: DDO, + credentailType: CredentialType +): AdvancedSettingsForm { + const allowCredential = getAssetCredentials( + ddo.credentials, + credentailType, + 'allow' + ) + const denyCredential = getAssetCredentials( + ddo.credentials, + credentailType, + 'deny' + ) + const metadata = ddo.findServiceByType('metadata') + return { + allow: allowCredential, + deny: denyCredential, + isOrderDisabled: metadata.attributes?.status?.isOrderDisabled || false + } +}