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

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 e3d916fe61.

* 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 e3bf6b4a2a.

Co-authored-by: KY Lau <kian_yee.lau@daimler.com>
This commit is contained in:
Kris Liew 2021-06-14 15:47:31 +08:00 committed by GitHub
parent e26ed0e81a
commit 4fc5862654
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 536 additions and 2 deletions

View File

@ -10,4 +10,6 @@ GATSBY_NETWORK="rinkeby"
#GATSBY_ANALYTICS_ID="xxx" #GATSBY_ANALYTICS_ID="xxx"
#GATSBY_PORTIS_ID="xxx" #GATSBY_PORTIS_ID="xxx"
#GATSBY_ALLOW_FIXED_PRICING="true" #GATSBY_ALLOW_FIXED_PRICING="true"
#GATSBY_ALLOW_DYNAMIC_PRICING="true" #GATSBY_ALLOW_DYNAMIC_PRICING="true"
#GATSBY_ALLOW_ADVANCED_SETTINGS="true"
#GATSBY_CREDENTIAL_TYPE="address"

View File

@ -44,5 +44,9 @@ module.exports = {
// Used to show or hide the fixed and dynamic price options // Used to show or hide the fixed and dynamic price options
// tab to publishers during the price creation. // tab to publishers during the price creation.
allowFixedPricing: process.env.GATSBY_ALLOW_FIXED_PRICING || 'true', 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'
} }

View File

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

View File

@ -12,6 +12,7 @@ import classNames from 'classnames/bind'
import AssetSelection, { import AssetSelection, {
AssetSelectionAsset AssetSelectionAsset
} from '../../molecules/FormFields/AssetSelection' } from '../../molecules/FormFields/AssetSelection'
import Credentials from '../../molecules/FormFields/Credential'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -137,6 +138,8 @@ export default function InputElement({
{...props} {...props}
/> />
) )
case 'credentials':
return <Credentials name={name} {...field} {...props} />
default: default:
return prefix || postfix ? ( return prefix || postfix ? (
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}> <div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>

View File

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

View File

@ -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<string[]>(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<HTMLButtonElement>) {
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 (
<div className={styles.credential}>
<InputGroup>
<InputElement
type="text"
name="address"
size="default"
placeholder={props.placeholder}
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setValue(e.target.value)
}
/>
<Button
onClick={(e: FormEvent<HTMLButtonElement>) => handleAddValue(e)}
>
Add
</Button>
</InputGroup>
<div className={styles.scroll}>
{arrayInput &&
arrayInput.map((value) => {
return (
<div className={styles.chip} key={value}>
<code>{value}</code>
<span className={styles.buttonWrapper}>
<Button
className={styles.crossButton}
style="text"
onClick={(even) => handleDeleteChip(value)}
>
<Cross />
</Button>
</span>
</div>
)
})}
</div>
</div>
)
}

View File

@ -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<AdvancedSettings>()
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 (
<>
<DebugOutput title="Collected Form Values" output={values} />
<DebugOutput title="Transformed Form Values" output={advancedSettings} />
</>
)
}

View File

@ -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<string>()
const [error, setError] = useState<string>()
const { appConfig } = useSiteMetadata()
const hasFeedback = error || success
const credentialType = getDefaultCredentialType(appConfig.credentialType)
async function handleSubmit(
values: Partial<AdvancedSettingsForm>,
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 (
<Formik
initialValues={getInitialValues(ddo, credentialType)}
validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
await handleSubmit(values, resetForm)
}}
>
{({ isSubmitting, values }) =>
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}>
<FormAdvancedSettings
data={content.form.data}
setShowEdit={setShowEdit}
/>
</article>
{debug === true && (
<div className={styles.grid}>
<DebugEditCredential
values={values}
ddo={ddo}
credentialType={credentialType}
/>
</div>
)}
</>
)
}
</Formik>
)
}

View File

@ -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<Partial<AdvancedSettingsForm>> = useFormikContext()
function handleFieldChange(
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps
) {
validateField(field.name)
if (e.target.type === 'checkbox')
setFieldValue(field.name, e.target.checked)
else setFieldValue(field.name, e.target.value)
}
return (
<Form className={styles.form}>
{data.map((field: FormFieldProps) => (
<Field
key={field.name}
{...field}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
))}
<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

@ -17,6 +17,8 @@ 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' import styles from './index.module.css'
import EditAdvancedSettings from '../AssetActions/Edit/EditAdvancedSettings'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
export interface AssetContentProps { export interface AssetContentProps {
path?: string path?: string
@ -48,8 +50,11 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
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 [showEditCompute, setShowEditCompute] = useState<boolean>()
const [showEditAdvancedSettings, setShowEditAdvancedSettings] =
useState<boolean>()
const [isOwner, setIsOwner] = useState(false) const [isOwner, setIsOwner] = useState(false)
const { ddo, price, metadata, type } = useAsset() const { ddo, price, metadata, type } = useAsset()
const { appConfig } = useSiteMetadata()
useEffect(() => { useEffect(() => {
if (!accountId || !owner) return if (!accountId || !owner) return
@ -70,10 +75,17 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
setShowEditCompute(true) setShowEditCompute(true)
} }
function handleEditAdvancedSettingsButton() {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
setShowEditAdvancedSettings(true)
}
return showEdit ? ( return showEdit ? (
<Edit setShowEdit={setShowEdit} /> <Edit setShowEdit={setShowEdit} />
) : showEditCompute ? ( ) : showEditCompute ? (
<EditComputeDataset setShowEdit={setShowEditCompute} /> <EditComputeDataset setShowEdit={setShowEditCompute} />
) : showEditAdvancedSettings ? (
<EditAdvancedSettings setShowEdit={setShowEditAdvancedSettings} />
) : ( ) : (
<article className={styles.grid}> <article className={styles.grid}>
<div> <div>
@ -103,6 +115,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>
{appConfig.allowAdvancedSettings === 'true' && (
<>
<span className={styles.separator}>|</span>
<Button
style="text"
size="small"
onClick={handleEditAdvancedSettingsButton}
>
Edit Advanced Settings
</Button>
</>
)}
{ddo.findServiceByType('compute') && type === 'dataset' && ( {ddo.findServiceByType('compute') && type === 'dataset' && (
<> <>
<span className={styles.separator}>|</span> <span className={styles.separator}>|</span>

View File

@ -27,6 +27,8 @@ interface UseSiteMetadata {
portisId: string portisId: string
allowFixedPricing: string allowFixedPricing: string
allowDynamicPricing: string allowDynamicPricing: string
allowAdvancedSettings: string
credentialType: string
} }
} }
@ -59,6 +61,8 @@ const query = graphql`
portisId portisId
allowFixedPricing allowFixedPricing
allowDynamicPricing allowDynamicPricing
allowAdvancedSettings
credentialType
} }
} }
} }

3
src/images/cross.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg>

After

Width:  |  Height:  |  Size: 364 B

View File

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