From 4fc5862654b450f1462a1a508c80ae07395a68e6 Mon Sep 17 00:00:00 2001 From: Kris Liew <39853992+krisliew@users.noreply.github.com> Date: Mon, 14 Jun 2021 15:47:31 +0800 Subject: [PATCH] Edit Advance Settings form for Edit Credentials & edit isOrderDisabled Flag (#655) * #638 Initial design * #638 fix import and get value from env file * #638 Fix UI * #638 Improved UI * #638 Add deny credential and isOrderDisabled * UI update * Fix lint * Attempt fix issue * Revert "Attempt fix issue" This reverts commit e3d916fe6142a30ad95c8199fa9f690c9e0319be. * Extract default credential type * Fix complexity issue * Fix complexity issue * Fix typo error * Enhance UI * Enhance Credentials Component UI * Reduce duplication * Revert "Reduce duplication" This reverts commit e3bf6b4a2a73356490c954078f899d175bcd3821. Co-authored-by: KY Lau --- .env.example | 4 +- app.config.js | 6 +- content/pages/editAdvancedSettings.json | 31 ++++ src/components/atoms/Input/InputElement.tsx | 3 + .../Credential/Credential.module.css | 40 +++++ .../molecules/FormFields/Credential/index.tsx | 81 +++++++++ .../Edit/DebugEditAdvancedSettings.tsx | 48 ++++++ .../Edit/EditAdvancedSettings.tsx | 163 ++++++++++++++++++ .../Edit/FormAdvancedSettings.tsx | 59 +++++++ .../organisms/AssetContent/index.tsx | 24 +++ src/hooks/useSiteMetadata.ts | 4 + src/images/cross.svg | 3 + src/models/FormEditCredential.ts | 72 ++++++++ 13 files changed, 536 insertions(+), 2 deletions(-) create mode 100644 content/pages/editAdvancedSettings.json create mode 100644 src/components/molecules/FormFields/Credential/Credential.module.css create mode 100644 src/components/molecules/FormFields/Credential/index.tsx create mode 100644 src/components/organisms/AssetActions/Edit/DebugEditAdvancedSettings.tsx create mode 100644 src/components/organisms/AssetActions/Edit/EditAdvancedSettings.tsx create mode 100644 src/components/organisms/AssetActions/Edit/FormAdvancedSettings.tsx create mode 100644 src/images/cross.svg create mode 100644 src/models/FormEditCredential.ts 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 + } +}