mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Feat: Consumer Parameters (#1921)
* feat: add consumer parameters to publish * fix: publish form validation * feat: update consumer parameters form * feat: add consumer parameter types * feat: update consumer parameter validation * feat: add consumer parameters structure * feat: add InputOptions error handling * feat: update consumer parameters validation * feat: update transformPublishFromToDdo * feat: add default options to parameter options * fix: value handling for select and mutliselect * feat: update "add new parameter" button label * feat: remove unused publish form sections * chore: remove console.log * chore: remove comments * feat: remove multiselect * feat: add consumer parameters section label * feat: update types * feat: update edit form fields * feat: parse consumer parameters in edit form * feat: add consumer parameters field to edit * feat: transform consumer parameters values before edit * feat: update "required" type to select * feat: update "required" type to select on edit form * fix: error object access * fix: edit flow crash * fix: validation when consumer parameters are not selected * feat: update validation for default consumer parameters value * fix: types * feat: add service consumer parameters to publish form * chore: remove console.log * feat: add service consumer parameters edit * chore: remove comments * fix: form edit metadata types * feat: add consume algo parameter structure * feat: consumer parameter required default value condition * feat: add consumer parameter groups to assetActions * fix: consumer parameters grouping * feat: update consumer parameters alignment * feat: update types * feat: allow service consumer parameters on dataset asset type * feat: add consumer parameters to ddo updated values in edit form * feat: add "data service" consumer parameters to consumption form * feat: allow service parameters on all asset types * feat: update parameter consume form design * feat: update asset actions consume parameters location * feat: add service parameters to download assets * refactor: consumer parameters actions * refactor: form action name * refactor: consumer parameters default input * refactor: consumer parameters select input * refactor: consumer parameter input * fix: props name * refactor: variable naming * refactor: consumer parameters form validation * refactor: rename consumer parameters types * refactor: extract consumer parameters form data to separate file * refactor: rename type * feat: controlled tabs for consumer params * feat: restore default value as required * refactor: parse values before edit * feat: add form to handle consumer parameters consumption * feat: send consumer params with download request * feat: send consumer params with compute request * feat: handle compute form initialization * chore: remove unused dependency * feat: handle download form data initialization * chore: remove console.log * feat: update types * fix: consumer parameter value types * feat: update ConsumerParameter type * feat: update ConsumerParameter type * chore: add comments * refactor: consumer parameters inputs * refactor: rename data and algo service params * refactor: consumer parameters form * refactor: consumer parameter form styling * refactor: make headers input reusable as KeyValueInput * refactor: refactorings, reduce duplication (WIP) * refactor: usercustomparameters consumption form creation/validation * refactor: return undefined consistently if property path not found on object * refactor: reuse fieldType and fieldOptions in DefaultInput * fix: parse ddo consumer parameters for edit form * fix: asset view crash for assets w/o consumer params * fix: publish preview for assets with no consumer params * fix: revert accidential rename of algoService * fix: revert publish navigation padding change * feat: update consumer parameters' labels and helpers * fix: consumer parameters validation * feat: update consumer parameter helper wording * chore: merge conflicts * feat: update types * fix: validate form on consumer parameter deletion * feat: add key value input placeholder props * fix: handle boolean type consumer parameters in consume form (#38) * Update src/components/Publish/_validation.ts Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * Update src/components/Asset/AssetActions/ConsumerParameters/FormConsumerParameters.module.css Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * Update src/components/Asset/Edit/_validation.ts Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * Update src/components/Asset/AssetActions/ConsumerParameters/index.tsx Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * Update src/components/Asset/AssetActions/ConsumerParameters/FormConsumerParameters.tsx Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * Update src/components/@shared/FormInput/index.tsx Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * Update src/components/@shared/FormInput/InputElement/ConsumerParameters/index.tsx Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * Update src/components/@shared/FormInput/InputElement/ConsumerParameters/index.tsx Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * Update src/components/@shared/FormInput/InputElement/ConsumerParameters/OptionsInput.tsx Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * Update src/components/@shared/FormInput/InputElement/ConsumerParameters/FormActions.tsx Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * Update src/@utils/provider.ts Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * fix: import paths * refactor: add consumer parameters validation types * feat: add string length min/max to consumer parameters validation * feat: add cp description to consume form * feat: reduce character limit in CP tab title * feat: update CP fields' placeholders and helpers * fix: show only relevant CPs in asset actions --------- Co-authored-by: Luca Milanese <luca.milanese90@gmail.com> Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com>
This commit is contained in:
parent
b7a28df97e
commit
fefb42aa07
@ -179,6 +179,14 @@
|
||||
"placeholder": "e.g. logistics",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "usesConsumerParameters",
|
||||
"label": "Algorithm custom parameters",
|
||||
"help": "Algorithm custom parameters are used to define required consumer input before running the algorithm in a Compute-to-Data environment.",
|
||||
"type": "checkbox",
|
||||
"options": ["This asset uses algorithm custom parameters"],
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "paymentCollector",
|
||||
"label": "Payment Collector Address",
|
||||
@ -199,6 +207,14 @@
|
||||
],
|
||||
"sortOptions": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "usesServiceConsumerParameters",
|
||||
"label": "User defined parameters",
|
||||
"help": "User defined parameters are used to filter or query the published asset.",
|
||||
"type": "checkbox",
|
||||
"options": ["This asset uses user defined parameters"],
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
66
content/publish/consumerParameters.json
Normal file
66
content/publish/consumerParameters.json
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"consumerParameters": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "consumerParameters",
|
||||
"label": "Custom parameters",
|
||||
"type": "consumerParameters",
|
||||
"required": false,
|
||||
"fields": [
|
||||
{
|
||||
"name": "name",
|
||||
"label": "Parameter Name",
|
||||
"placeholder": "e.g. iterations",
|
||||
"help": "The parameter name (this is sent as HTTP param or key towards algo).",
|
||||
"type": "text",
|
||||
"required": true
|
||||
},
|
||||
|
||||
{
|
||||
"name": "label",
|
||||
"label": "Parameter Label",
|
||||
"placeholder": "e.g. Iterations",
|
||||
"help": "The field label which is displayed.",
|
||||
"type": "text",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"label": "Description",
|
||||
"placeholder": "e.g. How many iterations should the algorithm perform.",
|
||||
"type": "text",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"label": "Parameter Type",
|
||||
"help": "The field type (text, number, boolean, select). This influences how the parameter is displayed for the consumer before the asset is used.",
|
||||
"type": "select",
|
||||
"options": ["number", "text", "boolean", "select"],
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"label": "Select Options",
|
||||
"help": "For select types, a list of options.",
|
||||
"type": "creatableSelect",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "required",
|
||||
"label": "Required",
|
||||
"options": ["optional", "required"],
|
||||
"type": "select",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "default",
|
||||
"label": "Default Value",
|
||||
"placeholder": "e.g. 6",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -74,6 +74,14 @@
|
||||
"help": "Provide the entrypoint for your algorithm.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "usesConsumerParameters",
|
||||
"label": "Algorithm custom parameters",
|
||||
"help": "Algorithm custom parameters are used to define required consumer input before running the algorithm in a Compute-to-Data environment.",
|
||||
"type": "checkbox",
|
||||
"options": ["This asset uses algorithm custom parameters"],
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "termsAndConditions",
|
||||
"label": "Terms & Conditions",
|
||||
@ -255,6 +263,14 @@
|
||||
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
|
||||
"sortOptions": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "usesConsumerParameters",
|
||||
"label": "User defined parameters",
|
||||
"help": "User defined parameters are used to filter or query the published asset.",
|
||||
"type": "checkbox",
|
||||
"options": ["This asset uses user defined parameters"],
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
2
src/@types/AssetExtended.d.ts
vendored
2
src/@types/AssetExtended.d.ts
vendored
@ -6,5 +6,7 @@ declare global {
|
||||
interface AssetExtended extends Asset {
|
||||
accessDetails?: AccessDetails
|
||||
views?: number
|
||||
metadata: MetadataExtended
|
||||
services: ServiceExtended[]
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,14 @@ import {
|
||||
ComputeEditForm,
|
||||
MetadataEditForm
|
||||
} from '@components/Asset/Edit/_types'
|
||||
import { FormPublishData } from '@components/Publish/_types'
|
||||
import {
|
||||
FormConsumerParameter,
|
||||
FormPublishData
|
||||
} from '@components/Publish/_types'
|
||||
import {
|
||||
Arweave,
|
||||
Asset,
|
||||
ConsumerParameter,
|
||||
DDO,
|
||||
FileInfo,
|
||||
GraphqlQuery,
|
||||
@ -188,3 +192,30 @@ export function previewDebugPatch(
|
||||
|
||||
return buildValuesPreview
|
||||
}
|
||||
|
||||
export function parseConsumerParameters(
|
||||
consumerParameters: ConsumerParameter[]
|
||||
): FormConsumerParameter[] {
|
||||
if (!consumerParameters?.length) return []
|
||||
|
||||
return consumerParameters.map((param) => ({
|
||||
...param,
|
||||
required: param.required ? 'required' : 'optional',
|
||||
options:
|
||||
param.type === 'select'
|
||||
? JSON.parse(param.options)?.map((option) => {
|
||||
const key = Object.keys(option)[0]
|
||||
return {
|
||||
key,
|
||||
value: option[key]
|
||||
}
|
||||
})
|
||||
: [],
|
||||
default:
|
||||
param.type === 'boolean'
|
||||
? param.default === 'true'
|
||||
: param.type === 'number'
|
||||
? Number(param.default)
|
||||
: param.default
|
||||
}))
|
||||
}
|
||||
|
@ -17,3 +17,27 @@ export function sortAssets(items: Asset[], sorted: string[]) {
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
export const isPlainObject = (object: any) => {
|
||||
return object !== null && typeof object === 'object' && !Array.isArray(object)
|
||||
}
|
||||
|
||||
export const getObjectPropertyByPath = (object: any, path = '') => {
|
||||
if (!isPlainObject(object)) return undefined
|
||||
path = path.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties
|
||||
path = path.replace(/^\./, '') // strip a leading dot
|
||||
const pathArray = path.split('.')
|
||||
for (let i = 0, n = pathArray.length; i < n; ++i) {
|
||||
const key = pathArray[i]
|
||||
try {
|
||||
if (key in object) {
|
||||
object = object[key]
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
@ -13,11 +13,12 @@ import {
|
||||
ProviderInstance,
|
||||
UrlFile,
|
||||
AbiItem,
|
||||
UserCustomParameters,
|
||||
getErrorMessage
|
||||
} from '@oceanprotocol/lib'
|
||||
// if customProviderUrl is set, we need to call provider using this custom endpoint
|
||||
import { customProviderUrl } from '../../app.config'
|
||||
import { QueryHeader } from '@shared/FormInput/InputElement/Headers'
|
||||
import { KeyValuePair } from '@shared/FormInput/InputElement/KeyValueInput'
|
||||
import { Signer } from 'ethers'
|
||||
import { getValidUntilTime } from './compute'
|
||||
import { toast } from 'react-toastify'
|
||||
@ -110,7 +111,7 @@ export async function getFileInfo(
|
||||
providerUrl: string,
|
||||
storageType: string,
|
||||
query?: string,
|
||||
headers?: QueryHeader[],
|
||||
headers?: KeyValuePair[],
|
||||
abi?: string,
|
||||
chainId?: number,
|
||||
method?: string
|
||||
@ -226,7 +227,8 @@ export async function downloadFile(
|
||||
signer: Signer,
|
||||
asset: AssetExtended,
|
||||
accountId: string,
|
||||
validOrderTx?: string
|
||||
validOrderTx?: string,
|
||||
userCustomParameters?: UserCustomParameters
|
||||
) {
|
||||
let downloadUrl
|
||||
try {
|
||||
@ -236,7 +238,8 @@ export async function downloadFile(
|
||||
0,
|
||||
validOrderTx || asset.accessDetails.validOrderTx,
|
||||
customProviderUrl || asset.services[0].serviceEndpoint,
|
||||
signer
|
||||
signer,
|
||||
userCustomParameters
|
||||
)
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(JSON.parse(error.message))
|
||||
|
@ -0,0 +1,40 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import Input, { InputProps } from '../..'
|
||||
import { Field, useField } from 'formik'
|
||||
import { FormConsumerParameter } from '@components/Publish/_types'
|
||||
|
||||
export default function DefaultInput({
|
||||
index,
|
||||
inputName,
|
||||
...props
|
||||
}: InputProps & {
|
||||
index: number
|
||||
inputName: string
|
||||
}): ReactElement {
|
||||
const [field] = useField<FormConsumerParameter[]>(inputName)
|
||||
const fieldType = field.value[index]?.type
|
||||
const fieldOptions = field.value[index]?.options
|
||||
|
||||
const getStringOptions = (
|
||||
options: { key: string; value: string }[]
|
||||
): string[] => {
|
||||
if (!options?.length) return []
|
||||
|
||||
return options.map((option) => option.key)
|
||||
}
|
||||
|
||||
return (
|
||||
<Field
|
||||
{...props}
|
||||
component={Input}
|
||||
type={fieldType === 'boolean' ? 'select' : fieldType}
|
||||
options={
|
||||
fieldType === 'boolean'
|
||||
? ['true', 'false']
|
||||
: fieldType === 'select'
|
||||
? getStringOptions(fieldOptions)
|
||||
: fieldOptions
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--spacer);
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import styles from './FormActions.module.css'
|
||||
import { useField, useFormikContext } from 'formik'
|
||||
import {
|
||||
FormConsumerParameter,
|
||||
FormPublishData
|
||||
} from '@components/Publish/_types'
|
||||
import { getObjectPropertyByPath } from '@utils/index'
|
||||
import { defaultConsumerParam } from '.'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
export default function FormActions({
|
||||
fieldName,
|
||||
index,
|
||||
onParameterAdded,
|
||||
onParameterDeleted
|
||||
}: {
|
||||
fieldName: string
|
||||
index: number
|
||||
onParameterAdded?: (index: number) => void
|
||||
onParameterDeleted?: (previousIndex: number) => void
|
||||
}): ReactElement {
|
||||
const { errors, setFieldTouched, validateField } =
|
||||
useFormikContext<FormPublishData>()
|
||||
const [field, meta, helpers] = useField<FormConsumerParameter[]>(fieldName)
|
||||
|
||||
const setParamPropsTouched = (index: number, touched = true) => {
|
||||
Object.keys(defaultConsumerParam).forEach((param) => {
|
||||
setFieldTouched(`${field.name}[${index}].${param}`, touched)
|
||||
})
|
||||
}
|
||||
|
||||
const addParameter = (index: number) => {
|
||||
// validate parameter before allowing the creation of a new one
|
||||
validateField(field.name)
|
||||
setParamPropsTouched(index)
|
||||
|
||||
// Check errors on current tab before creating a new param
|
||||
if (getObjectPropertyByPath(errors, `${field.name}[${index}]`)) {
|
||||
toast.error(
|
||||
'Cannot add new parameter. Current parameter definition contains errors.'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
helpers.setValue([...field.value, { ...defaultConsumerParam }])
|
||||
|
||||
onParameterAdded && onParameterAdded(field.value.length)
|
||||
}
|
||||
|
||||
const deleteParameter = (index: number) => {
|
||||
helpers.setValue(field.value.filter((p, i) => i !== index))
|
||||
|
||||
const previousIndex = Math.max(0, index - 1)
|
||||
onParameterDeleted && onParameterDeleted(previousIndex)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
style="ghost"
|
||||
size="small"
|
||||
disabled={field.value.length === 1}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
deleteParameter(index)
|
||||
}}
|
||||
>
|
||||
Delete parameter
|
||||
</Button>
|
||||
<Button
|
||||
style="primary"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
addParameter(index)
|
||||
}}
|
||||
>
|
||||
Add new parameter
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container.hasError * {
|
||||
border-color: var(--brand-alert-red);
|
||||
}
|
||||
|
||||
.hasError label {
|
||||
color: var(--brand-alert-red);
|
||||
}
|
||||
|
||||
.error {
|
||||
composes: error from '../../index.module.css';
|
||||
top: 100%;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { FormPublishData } from '@components/Publish/_types'
|
||||
import classNames from 'classnames/bind'
|
||||
import { ErrorMessage, Field, useFormikContext } from 'formik'
|
||||
import { getObjectPropertyByPath } from '@utils/index'
|
||||
import InputKeyValue, { KeyValueInputProps } from '../KeyValueInput'
|
||||
import styles from './OptionsInput.module.css'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
export default function OptionsInput(props: KeyValueInputProps): ReactElement {
|
||||
const { errors, touched } = useFormikContext<FormPublishData>()
|
||||
|
||||
const hasError = (): boolean =>
|
||||
[errors, touched].every(
|
||||
(object) => !!getObjectPropertyByPath(object, props.name)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cx({ container: true, hasError: hasError() })}>
|
||||
<Field
|
||||
{...props}
|
||||
component={InputKeyValue}
|
||||
uniqueKeys
|
||||
keyPlaceholder="value"
|
||||
valuePlaceholder="label"
|
||||
/>
|
||||
|
||||
{hasError() && (
|
||||
<div className={styles.error}>
|
||||
<ErrorMessage name={props.name} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import Input, { InputProps } from '../..'
|
||||
import { Field, useField, useFormikContext } from 'formik'
|
||||
import {
|
||||
FormConsumerParameter,
|
||||
FormPublishData
|
||||
} from '@components/Publish/_types'
|
||||
import { defaultConsumerParam } from '.'
|
||||
|
||||
export default function TypeInput({
|
||||
index,
|
||||
inputName,
|
||||
...props
|
||||
}: InputProps & {
|
||||
index: number
|
||||
inputName: string
|
||||
}): ReactElement {
|
||||
const { setFieldTouched } = useFormikContext<FormPublishData>()
|
||||
const [field, meta, helpers] = useField<FormConsumerParameter[]>(inputName)
|
||||
|
||||
const resetDefaultValue = (
|
||||
parameterName: string,
|
||||
parameterType: FormConsumerParameter['type'],
|
||||
index: number
|
||||
) => {
|
||||
if (parameterName !== 'type') return
|
||||
if (field.value[index].type === parameterType) return
|
||||
|
||||
setFieldTouched(`${field.name}[${index}].default`, false)
|
||||
helpers.setValue(
|
||||
field.value.map((currentParam, i) => {
|
||||
if (i !== index) return currentParam
|
||||
|
||||
return {
|
||||
...defaultConsumerParam,
|
||||
...currentParam,
|
||||
default: defaultConsumerParam.default
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Field
|
||||
{...props}
|
||||
component={Input}
|
||||
onChange={(e) => {
|
||||
resetDefaultValue(props.name, e.target.value, index)
|
||||
field.onChange(e)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import {
|
||||
FormConsumerParameter,
|
||||
FormPublishData,
|
||||
FormPublishService
|
||||
} from '@components/Publish/_types'
|
||||
import * as Yup from 'yup'
|
||||
import { SchemaLike } from 'yup/lib/types'
|
||||
import { paramTypes } from '.'
|
||||
|
||||
interface TestContextExtended extends Yup.TestContext {
|
||||
from: {
|
||||
value: FormPublishData['metadata'] | FormPublishService
|
||||
}[]
|
||||
}
|
||||
|
||||
export const validationConsumerParameters: {
|
||||
[key in keyof FormConsumerParameter]: SchemaLike
|
||||
} = {
|
||||
name: Yup.string()
|
||||
.test('unique', 'Parameter names must be unique', (name, context) => {
|
||||
const [parentFormObj, nextParentFormObj] = (
|
||||
context as TestContextExtended
|
||||
).from
|
||||
|
||||
if (
|
||||
!nextParentFormObj?.value?.consumerParameters ||
|
||||
nextParentFormObj.value.consumerParameters.length === 1
|
||||
)
|
||||
return true
|
||||
|
||||
const { consumerParameters } = nextParentFormObj.value
|
||||
const occasions = consumerParameters.filter(
|
||||
(params) => params.name === name
|
||||
)
|
||||
return occasions.length === 1
|
||||
})
|
||||
.min(4, (param) => `Name must be at least ${param.min} characters`)
|
||||
.max(50, (param) => `Name must have maximum ${param.max} characters`)
|
||||
.required('Required'),
|
||||
type: Yup.string().oneOf(paramTypes).required('Required'),
|
||||
description: Yup.string()
|
||||
.min(10, (param) => `Description must be at least ${param.min} characters`)
|
||||
.max(
|
||||
500,
|
||||
(param) => `Description must have maximum ${param.max} characters`
|
||||
)
|
||||
.required('Required'),
|
||||
label: Yup.string()
|
||||
.min(4, (param) => `Label must be at least ${param.min} characters`)
|
||||
.max(50, (param) => `Label must have maximum ${param.max} characters`)
|
||||
.required('Required'),
|
||||
required: Yup.string().oneOf(['optional', 'required']).required('Required'),
|
||||
default: Yup.mixed().required('Required'),
|
||||
options: Yup.array().when('type', {
|
||||
is: 'select',
|
||||
then: Yup.array()
|
||||
.of(Yup.object())
|
||||
.min(2, 'At least two options are required')
|
||||
.required('Required'),
|
||||
otherwise: Yup.array()
|
||||
})
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
.container {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { Field, useField } from 'formik'
|
||||
import Input, { InputProps } from '../..'
|
||||
import { FormConsumerParameter } from '@components/Publish/_types'
|
||||
import Tabs from '@shared/atoms/Tabs'
|
||||
import FormActions from './FormActions'
|
||||
import DefaultInput from './DefaultInput'
|
||||
import OptionsInput from './OptionsInput'
|
||||
import styles from './index.module.css'
|
||||
import TypeInput from './TypeInput'
|
||||
|
||||
export const defaultConsumerParam: FormConsumerParameter = {
|
||||
name: '',
|
||||
label: '',
|
||||
description: '',
|
||||
type: 'text',
|
||||
options: undefined,
|
||||
default: '',
|
||||
required: 'optional'
|
||||
}
|
||||
|
||||
export const paramTypes: FormConsumerParameter['type'][] = [
|
||||
'number',
|
||||
'text',
|
||||
'boolean',
|
||||
'select'
|
||||
]
|
||||
|
||||
export function ConsumerParameters(props: InputProps): ReactElement {
|
||||
const [field, meta, helpers] = useField<FormConsumerParameter[]>(props.name)
|
||||
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (field.value.length === 0)
|
||||
helpers.setValue([{ ...defaultConsumerParam }])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Tabs
|
||||
selectedIndex={tabIndex}
|
||||
onIndexSelected={setTabIndex}
|
||||
items={field.value.map((param, index) => {
|
||||
return {
|
||||
title:
|
||||
param?.name?.length > 15
|
||||
? `${param?.name?.slice(0, 15)}...`
|
||||
: param?.name || 'New parameter',
|
||||
content: (
|
||||
<div>
|
||||
{props.fields?.map((subField: InputProps) => {
|
||||
if (subField.name === 'options') {
|
||||
return field.value[index]?.type === 'select' ? (
|
||||
<OptionsInput
|
||||
key={`${field.name}[${index}].${props.name}`}
|
||||
{...subField}
|
||||
name={`${field.name}[${index}].${subField.name}`}
|
||||
value={field.value[index][subField.name]}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
if (subField.name === 'default') {
|
||||
return (
|
||||
<DefaultInput
|
||||
key={`${field.name}[${index}].${subField.name}`}
|
||||
{...subField}
|
||||
name={`${field.name}[${index}].${subField.name}`}
|
||||
index={index}
|
||||
inputName={props.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (subField.name === 'type') {
|
||||
return (
|
||||
<TypeInput
|
||||
key={`${field.name}[${index}].${subField.name}`}
|
||||
{...subField}
|
||||
name={`${field.name}[${index}].${subField.name}`}
|
||||
index={index}
|
||||
inputName={props.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Field
|
||||
key={`${field.name}[${index}].${subField.name}`}
|
||||
{...subField}
|
||||
name={`${field.name}[${index}].${subField.name}`}
|
||||
component={Input}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<FormActions
|
||||
fieldName={props.name}
|
||||
index={index}
|
||||
onParameterAdded={setTabIndex}
|
||||
onParameterDeleted={setTabIndex}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -8,7 +8,7 @@ import { LoggerInstance, FileInfo } from '@oceanprotocol/lib'
|
||||
import { useAsset } from '@context/Asset'
|
||||
import styles from './index.module.css'
|
||||
import { useNetwork } from 'wagmi'
|
||||
import InputHeaders from '../Headers'
|
||||
import InputKeyValue from '../KeyValueInput'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import Loader from '@shared/atoms/Loader'
|
||||
import { checkJson } from '@utils/codemirror'
|
||||
@ -161,7 +161,9 @@ export default function FilesInput(props: InputProps): ReactElement {
|
||||
<Field
|
||||
key={i}
|
||||
component={
|
||||
innerField.type === 'headers' ? InputHeaders : Input
|
||||
innerField.type === 'headers'
|
||||
? InputKeyValue
|
||||
: Input
|
||||
}
|
||||
{...innerField}
|
||||
name={`${field.name}[0].${innerField.value}`}
|
||||
|
@ -1,13 +0,0 @@
|
||||
.headersContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0 calc(var(--spacer) / 4);
|
||||
margin-bottom: var(--spacer);
|
||||
}
|
||||
|
||||
.headersAddedContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0 calc(var(--spacer) / 4);
|
||||
margin-bottom: var(--spacer);
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
.pairsContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0 calc(var(--spacer) / 4);
|
||||
margin-bottom: var(--spacer);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pairsAddedContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0 calc(var(--spacer) / 4);
|
||||
margin-bottom: var(--spacer);
|
||||
}
|
||||
|
||||
.hasError label {
|
||||
color: var(--brand-alert-red);
|
||||
}
|
||||
|
||||
.hasError .pairsContainer .keyInput,
|
||||
.hasError .pairsContainer .keyInput:focus,
|
||||
.hasError .pairsContainer [class*='prefix'],
|
||||
.hasError .pairsContainer [class*='postfix'] {
|
||||
color: var(--brand-alert-red);
|
||||
border-color: var(--brand-alert-red);
|
||||
}
|
||||
|
||||
.required {
|
||||
composes: required from '../../index.module.css';
|
||||
}
|
||||
|
||||
.error {
|
||||
composes: error from '../../index.module.css';
|
||||
top: 100%;
|
||||
}
|
@ -1,28 +1,60 @@
|
||||
import React, { ChangeEvent, ReactElement, useEffect, useState } from 'react'
|
||||
import InputElement from '../../InputElement'
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState
|
||||
} from 'react'
|
||||
import InputElement from '..'
|
||||
import Label from '../../Label'
|
||||
import styles from './index.module.css'
|
||||
import Tooltip from '@shared/atoms/Tooltip'
|
||||
import Markdown from '@shared/Markdown'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import { InputProps } from '@shared/FormInput'
|
||||
import classNames from 'classnames/bind'
|
||||
|
||||
export interface QueryHeader {
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
export interface KeyValuePair {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export default function InputHeaders(props: InputProps): ReactElement {
|
||||
export interface KeyValueInputProps extends Omit<InputProps, 'value'> {
|
||||
value: KeyValuePair[]
|
||||
uniqueKeys?: boolean
|
||||
keyPlaceholder?: string
|
||||
valuePlaceholder?: string
|
||||
}
|
||||
|
||||
export default function InputKeyValue({
|
||||
uniqueKeys = false,
|
||||
value,
|
||||
keyPlaceholder = 'key',
|
||||
valuePlaceholder = 'value',
|
||||
...props
|
||||
}: KeyValueInputProps): ReactElement {
|
||||
const { label, help, prominentHelp, form, field } = props
|
||||
|
||||
const [currentKey, setCurrentKey] = useState('')
|
||||
const [currentValue, setCurrentValue] = useState('')
|
||||
const [disabledButton, setDisabledButton] = useState(true)
|
||||
const [hasOnlyUniqueKeys, setHasOnlyUniqueKeys] = useState(true)
|
||||
|
||||
const [headers, setHeaders] = useState([] as QueryHeader[])
|
||||
const [pairs, setPairs] = useState(value || [])
|
||||
|
||||
const addHeader = () => {
|
||||
setHeaders((prev) => [
|
||||
const currentKeyExists = useCallback(() => {
|
||||
return pairs.some((pair) => pair.key === currentKey)
|
||||
}, [currentKey, pairs])
|
||||
|
||||
const addPair = () => {
|
||||
if (currentKeyExists()) {
|
||||
setHasOnlyUniqueKeys(false)
|
||||
if (uniqueKeys) return
|
||||
}
|
||||
|
||||
setPairs((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentKey,
|
||||
@ -33,9 +65,9 @@ export default function InputHeaders(props: InputProps): ReactElement {
|
||||
setCurrentValue('')
|
||||
}
|
||||
|
||||
const removeHeader = (i: number) => {
|
||||
const newHeaders = headers.filter((header, index) => index !== i)
|
||||
setHeaders(newHeaders)
|
||||
const removePair = (index: number) => {
|
||||
const newPairs = pairs.filter((pair, pairIndex) => pairIndex !== index)
|
||||
setPairs(newPairs)
|
||||
setCurrentKey('')
|
||||
setCurrentValue('')
|
||||
}
|
||||
@ -50,15 +82,22 @@ export default function InputHeaders(props: InputProps): ReactElement {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldValue(`${field.name}`, headers)
|
||||
}, [headers])
|
||||
form.setFieldValue(`${field.name}`, pairs)
|
||||
}, [pairs])
|
||||
|
||||
useEffect(() => {
|
||||
setDisabledButton(!currentKey || !currentValue)
|
||||
}, [currentKey, currentValue])
|
||||
setDisabledButton(
|
||||
!currentKey || !currentValue || (uniqueKeys && currentKeyExists())
|
||||
)
|
||||
setHasOnlyUniqueKeys(!currentKeyExists())
|
||||
}, [currentKey, currentValue, uniqueKeys, currentKeyExists])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={cx({
|
||||
hasError: uniqueKeys && !hasOnlyUniqueKeys
|
||||
})}
|
||||
>
|
||||
<Label htmlFor={props.name}>
|
||||
{label}
|
||||
{props.required && (
|
||||
@ -71,18 +110,19 @@ export default function InputHeaders(props: InputProps): ReactElement {
|
||||
)}
|
||||
</Label>
|
||||
|
||||
<div className={styles.headersContainer}>
|
||||
<div className={styles.pairsContainer}>
|
||||
<InputElement
|
||||
className={styles.keyInput}
|
||||
name={`${field.name}.key`}
|
||||
placeholder={'key'}
|
||||
placeholder={keyPlaceholder}
|
||||
value={`${currentKey}`}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<InputElement
|
||||
className={`${styles.input}`}
|
||||
className={styles.input}
|
||||
name={`${field.name}.value`}
|
||||
placeholder={'value'}
|
||||
placeholder={valuePlaceholder}
|
||||
value={`${currentValue}`}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
@ -92,26 +132,32 @@ export default function InputHeaders(props: InputProps): ReactElement {
|
||||
size="small"
|
||||
onClick={(e: React.SyntheticEvent) => {
|
||||
e.preventDefault()
|
||||
addHeader()
|
||||
addPair()
|
||||
}}
|
||||
disabled={disabledButton}
|
||||
>
|
||||
add
|
||||
</Button>
|
||||
|
||||
{uniqueKeys && !hasOnlyUniqueKeys && (
|
||||
<p
|
||||
className={styles.error}
|
||||
>{`The ${keyPlaceholder} field must be unique`}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{headers.length > 0 &&
|
||||
headers.map((header, i) => {
|
||||
{pairs.length > 0 &&
|
||||
pairs.map((header, i) => {
|
||||
return (
|
||||
<div className={styles.headersAddedContainer} key={`header_${i}`}>
|
||||
<div className={styles.pairsAddedContainer} key={`pair_${i}`}>
|
||||
<InputElement
|
||||
name={`header[${i}].key`}
|
||||
name={`pair[${i}].key`}
|
||||
value={`${header.key}`}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<InputElement
|
||||
name={`header[${i}].key`}
|
||||
name={`pair[${i}].value`}
|
||||
value={`${header.value}`}
|
||||
disabled
|
||||
/>
|
||||
@ -121,7 +167,7 @@ export default function InputHeaders(props: InputProps): ReactElement {
|
||||
size="small"
|
||||
onClick={(e: React.SyntheticEvent) => {
|
||||
e.preventDefault()
|
||||
removeHeader(i)
|
||||
removePair(i)
|
||||
}}
|
||||
disabled={false}
|
||||
>
|
@ -16,6 +16,7 @@ import TabsFile from '@shared/atoms/TabsFile'
|
||||
import useDarkMode from '@oceanprotocol/use-dark-mode'
|
||||
import appConfig from '../../../../../app.config'
|
||||
import { extensions, oceanTheme } from '@utils/codemirror'
|
||||
import { ConsumerParameters } from './ConsumerParameters'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
@ -130,6 +131,9 @@ export default function InputElement({
|
||||
/>
|
||||
)
|
||||
|
||||
case 'consumerParameters':
|
||||
return <ConsumerParameters {...field} form={form} {...props} />
|
||||
|
||||
case 'textarea':
|
||||
return <textarea id={props.name} className={styles.textarea} {...props} />
|
||||
|
||||
|
@ -10,7 +10,7 @@ import React, {
|
||||
import InputElement from './InputElement'
|
||||
import Label from './Label'
|
||||
import styles from './index.module.css'
|
||||
import { ErrorMessage, FieldInputProps } from 'formik'
|
||||
import { ErrorMessage, FieldInputProps, useField } from 'formik'
|
||||
import classNames from 'classnames/bind'
|
||||
import Disclaimer from './Disclaimer'
|
||||
import Tooltip from '@shared/atoms/Tooltip'
|
||||
@ -18,6 +18,7 @@ import Markdown from '@shared/Markdown'
|
||||
import FormHelp from './Help'
|
||||
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
|
||||
import { BoxSelectionOption } from '@shared/FormInput/InputElement/BoxSelection'
|
||||
import { getObjectPropertyByPath } from '@utils/index'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
@ -71,21 +72,17 @@ export interface InputProps {
|
||||
disclaimerValues?: string[]
|
||||
}
|
||||
|
||||
function checkError(
|
||||
form: any,
|
||||
parsedFieldName: string[],
|
||||
field: FieldInputProps<any>
|
||||
) {
|
||||
if (
|
||||
(form?.touched?.[parsedFieldName[0]]?.[parsedFieldName[1]] &&
|
||||
form?.errors?.[parsedFieldName[0]]?.[parsedFieldName[1]]) ||
|
||||
(form?.touched[field?.name] &&
|
||||
form?.errors[field?.name] &&
|
||||
field.name !== 'files' &&
|
||||
field.name !== 'links')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
function checkError(form: any, field: FieldInputProps<any>) {
|
||||
const touched = getObjectPropertyByPath(form?.touched, field?.name)
|
||||
const errors = getObjectPropertyByPath(form?.errors, field?.name)
|
||||
|
||||
return (
|
||||
touched &&
|
||||
errors &&
|
||||
!field.name.endsWith('.files') &&
|
||||
!field.name.endsWith('.links') &&
|
||||
!field.name.endsWith('consumerParameters')
|
||||
)
|
||||
}
|
||||
|
||||
export default function Input(props: Partial<InputProps>): ReactElement {
|
||||
@ -102,13 +99,10 @@ export default function Input(props: Partial<InputProps>): ReactElement {
|
||||
} = props
|
||||
|
||||
const isFormikField = typeof field !== 'undefined'
|
||||
const isNestedField = field?.name?.includes('.')
|
||||
// TODO: this feels hacky as it assumes nested `values` store. But we can't use the
|
||||
// `useField()` hook in here to get `meta.error` so we have to match against form?.errors?
|
||||
// handling flat and nested data at same time.
|
||||
const parsedFieldName =
|
||||
isFormikField && (isNestedField ? field?.name.split('.') : [field?.name])
|
||||
const hasFormikError = checkError(form, parsedFieldName, field)
|
||||
const hasFormikError = checkError(form, field)
|
||||
|
||||
const styleClasses = cx({
|
||||
field: true,
|
||||
@ -123,7 +117,7 @@ export default function Input(props: Partial<InputProps>): ReactElement {
|
||||
if (disclaimer && disclaimerValues) {
|
||||
setDisclaimerVisible(
|
||||
disclaimerValues.includes(
|
||||
props.form?.values[parsedFieldName[0]]?.[parsedFieldName[1]]
|
||||
getObjectPropertyByPath(props.form?.values, field?.name)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { ReactElement, ReactNode } from 'react'
|
||||
import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs'
|
||||
import styles from './index.module.css'
|
||||
import InputRadio from '@shared/FormInput/InputElement/Radio'
|
||||
|
||||
import styles from './index.module.css'
|
||||
export interface TabsItem {
|
||||
title: string
|
||||
content: ReactNode
|
||||
@ -15,6 +14,8 @@ export interface TabsProps {
|
||||
handleTabChange?: (tabName: string) => void
|
||||
defaultIndex?: number
|
||||
showRadio?: boolean
|
||||
selectedIndex?: number
|
||||
onIndexSelected?: (index: number) => void
|
||||
}
|
||||
|
||||
export default function Tabs({
|
||||
@ -22,10 +23,17 @@ export default function Tabs({
|
||||
className,
|
||||
handleTabChange,
|
||||
defaultIndex,
|
||||
showRadio
|
||||
showRadio,
|
||||
selectedIndex,
|
||||
onIndexSelected
|
||||
}: TabsProps): ReactElement {
|
||||
return (
|
||||
<ReactTabs className={`${className || ''}`} defaultIndex={defaultIndex}>
|
||||
<ReactTabs
|
||||
className={`${className || ''}`}
|
||||
selectedIndex={selectedIndex}
|
||||
onSelect={onIndexSelected}
|
||||
defaultIndex={selectedIndex ? undefined : defaultIndex}
|
||||
>
|
||||
<div className={styles.tabListContainer}>
|
||||
<TabList className={styles.tabList}>
|
||||
{items.map((item, index) => (
|
||||
|
@ -10,14 +10,14 @@ import { useAsset } from '@context/Asset'
|
||||
import content from '../../../../../content/pages/startComputeDataset.json'
|
||||
import { Asset, ZERO_ADDRESS } from '@oceanprotocol/lib'
|
||||
import { getAccessDetails } from '@utils/accessDetailsAndPricing'
|
||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||
import Alert from '@shared/atoms/Alert'
|
||||
import { getTokenBalanceFromSymbol } from '@utils/wallet'
|
||||
import { MAX_DECIMALS } from '@utils/constants'
|
||||
import Decimal from 'decimal.js'
|
||||
import { useAccount } from 'wagmi'
|
||||
import useBalance from '@hooks/useBalance'
|
||||
import useNetworkMetadata from '@hooks/useNetworkMetadata'
|
||||
import ConsumerParameters from '../ConsumerParameters'
|
||||
import { FormConsumerParameter } from '@components/Publish/_types'
|
||||
|
||||
export default function FormStartCompute({
|
||||
algorithms,
|
||||
@ -78,12 +78,18 @@ export default function FormStartCompute({
|
||||
validUntil?: string
|
||||
retry: boolean
|
||||
}): ReactElement {
|
||||
const { siteContent } = useMarketMetadata()
|
||||
const { address: accountId, isConnected } = useAccount()
|
||||
const { balance } = useBalance()
|
||||
const { isSupportedOceanNetwork } = useNetworkMetadata()
|
||||
const { isValid, values }: FormikContextType<{ algorithm: string }> =
|
||||
useFormikContext()
|
||||
const {
|
||||
isValid,
|
||||
values
|
||||
}: FormikContextType<{
|
||||
algorithm: string
|
||||
dataServiceParams: FormConsumerParameter[]
|
||||
algoServiceParams: FormConsumerParameter[]
|
||||
algoParams: FormConsumerParameter[]
|
||||
}> = useFormikContext()
|
||||
const { asset, isAssetNetwork } = useAsset()
|
||||
|
||||
const [datasetOrderPrice, setDatasetOrderPrice] = useState(
|
||||
@ -249,7 +255,12 @@ export default function FormStartCompute({
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{asset && selectedAlgorithmAsset && (
|
||||
<ConsumerParameters
|
||||
asset={asset}
|
||||
selectedAlgorithmAsset={selectedAlgorithmAsset}
|
||||
/>
|
||||
)}
|
||||
<PriceOutput
|
||||
hasPreviousOrder={hasPreviousOrder}
|
||||
assetTimeout={assetTimeout}
|
||||
|
@ -1,7 +1,6 @@
|
||||
.priceComponent {
|
||||
margin-left: -2rem;
|
||||
margin-right: -2rem;
|
||||
margin-top: -1rem;
|
||||
margin-bottom: calc(var(--spacer) / 1.5);
|
||||
padding-left: calc(var(--spacer) / 2);
|
||||
padding-right: calc(var(--spacer) / 2);
|
||||
|
@ -1,13 +1,45 @@
|
||||
import { ConsumerParameter, UserCustomParameters } from '@oceanprotocol/lib'
|
||||
import * as Yup from 'yup'
|
||||
import { getDefaultValues } from '../ConsumerParameters/FormConsumerParameters'
|
||||
import { getUserCustomParameterValidationSchema } from '../ConsumerParameters/_validation'
|
||||
|
||||
export const validationSchema: Yup.SchemaOf<{
|
||||
export function getComputeValidationSchema(
|
||||
dataServiceParams: ConsumerParameter[],
|
||||
algoServiceParams: ConsumerParameter[],
|
||||
algoParams: ConsumerParameter[]
|
||||
): Yup.SchemaOf<{
|
||||
algorithm: string
|
||||
}> = Yup.object().shape({
|
||||
algorithm: Yup.string().required('Required')
|
||||
})
|
||||
dataServiceParams: any
|
||||
algoServiceParams: any
|
||||
algoParams: any
|
||||
}> {
|
||||
return Yup.object().shape({
|
||||
algorithm: Yup.string().required('Required'),
|
||||
dataServiceParams:
|
||||
getUserCustomParameterValidationSchema(dataServiceParams),
|
||||
algoServiceParams:
|
||||
getUserCustomParameterValidationSchema(algoServiceParams),
|
||||
algoParams: getUserCustomParameterValidationSchema(algoParams)
|
||||
})
|
||||
}
|
||||
|
||||
export function getInitialValues(): { algorithm: string } {
|
||||
export function getInitialValues(
|
||||
asset?: AssetExtended,
|
||||
selectedAlgorithmAsset?: AssetExtended
|
||||
): {
|
||||
algorithm: string
|
||||
dataServiceParams?: UserCustomParameters
|
||||
algoServiceParams?: UserCustomParameters
|
||||
algoParams?: UserCustomParameters
|
||||
} {
|
||||
return {
|
||||
algorithm: undefined
|
||||
algorithm: selectedAlgorithmAsset?.id,
|
||||
dataServiceParams: getDefaultValues(asset?.services[0].consumerParameters),
|
||||
algoServiceParams: getDefaultValues(
|
||||
selectedAlgorithmAsset?.services[0].consumerParameters
|
||||
),
|
||||
algoParams: getDefaultValues(
|
||||
selectedAlgorithmAsset?.metadata?.algorithm.consumerParameters
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ import {
|
||||
ComputeOutput,
|
||||
ProviderComputeInitializeResults,
|
||||
unitsToAmount,
|
||||
minAbi,
|
||||
ProviderFees,
|
||||
UserCustomParameters,
|
||||
getErrorMessage
|
||||
} from '@oceanprotocol/lib'
|
||||
import { toast } from 'react-toastify'
|
||||
@ -22,7 +22,7 @@ import Price from '@shared/Price'
|
||||
import FileIcon from '@shared/FileIcon'
|
||||
import Alert from '@shared/atoms/Alert'
|
||||
import { Formik } from 'formik'
|
||||
import { getInitialValues, validationSchema } from './_constants'
|
||||
import { getComputeValidationSchema, getInitialValues } from './_constants'
|
||||
import FormStartComputeDataset from './FormComputeDataset'
|
||||
import styles from './index.module.css'
|
||||
import SuccessConfetti from '@shared/SuccessConfetti'
|
||||
@ -50,6 +50,7 @@ import { useAccount, useSigner } from 'wagmi'
|
||||
import { getDummySigner } from '@utils/wallet'
|
||||
import useNetworkMetadata from '@hooks/useNetworkMetadata'
|
||||
import { useAsset } from '@context/Asset'
|
||||
import { parseConsumerParameterValues } from '../ConsumerParameters'
|
||||
|
||||
const refreshInterval = 10000 // 10 sec.
|
||||
|
||||
@ -339,7 +340,11 @@ export default function Compute({
|
||||
toast.error(errorMsg)
|
||||
}, [error])
|
||||
|
||||
async function startJob(): Promise<void> {
|
||||
async function startJob(userCustomParameters: {
|
||||
dataServiceParams?: UserCustomParameters
|
||||
algoServiceParams?: UserCustomParameters
|
||||
algoParams?: UserCustomParameters
|
||||
}): Promise<void> {
|
||||
try {
|
||||
setIsOrdering(true)
|
||||
setIsOrdered(false)
|
||||
@ -347,7 +352,9 @@ export default function Compute({
|
||||
const computeService = getServiceByName(asset, 'compute')
|
||||
const computeAlgorithm: ComputeAlgorithm = {
|
||||
documentId: selectedAlgorithmAsset.id,
|
||||
serviceId: selectedAlgorithmAsset.services[0].id
|
||||
serviceId: selectedAlgorithmAsset.services[0].id,
|
||||
algocustomdata: userCustomParameters?.algoParams,
|
||||
userdata: userCustomParameters?.algoServiceParams
|
||||
}
|
||||
|
||||
const allowed = await isOrderable(
|
||||
@ -406,7 +413,8 @@ export default function Compute({
|
||||
const computeAsset: ComputeAsset = {
|
||||
documentId: asset.id,
|
||||
serviceId: asset.services[0].id,
|
||||
transferTxId: datasetOrderTx
|
||||
transferTxId: datasetOrderTx,
|
||||
userdata: userCustomParameters?.dataServiceParams
|
||||
}
|
||||
computeAlgorithm.transferTxId = algorithmOrderTx
|
||||
const output: ComputeOutput = {
|
||||
@ -440,6 +448,32 @@ export default function Compute({
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (values: {
|
||||
algorithm: string
|
||||
dataServiceParams?: UserCustomParameters
|
||||
algoServiceParams?: UserCustomParameters
|
||||
algoParams?: UserCustomParameters
|
||||
}) => {
|
||||
if (!values.algorithm) return
|
||||
|
||||
const userCustomParameters = {
|
||||
dataServiceParams: parseConsumerParameterValues(
|
||||
values?.dataServiceParams,
|
||||
asset.services[0].consumerParameters
|
||||
),
|
||||
algoServiceParams: parseConsumerParameterValues(
|
||||
values?.algoServiceParams,
|
||||
selectedAlgorithmAsset?.services[0].consumerParameters
|
||||
),
|
||||
algoParams: parseConsumerParameterValues(
|
||||
values?.algoParams,
|
||||
selectedAlgorithmAsset?.metadata?.algorithm?.consumerParameters
|
||||
)
|
||||
}
|
||||
|
||||
await startJob(userCustomParameters)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -480,13 +514,15 @@ export default function Compute({
|
||||
</>
|
||||
) : (
|
||||
<Formik
|
||||
initialValues={getInitialValues()}
|
||||
initialValues={getInitialValues(asset, selectedAlgorithmAsset)}
|
||||
validateOnMount
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={async (values) => {
|
||||
if (!values.algorithm) return
|
||||
await startJob()
|
||||
}}
|
||||
validationSchema={getComputeValidationSchema(
|
||||
asset.services[0].consumerParameters,
|
||||
selectedAlgorithmAsset?.services[0].consumerParameters,
|
||||
selectedAlgorithmAsset?.metadata?.algorithm?.consumerParameters
|
||||
)}
|
||||
enableReinitialize
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<FormStartComputeDataset
|
||||
algorithms={algorithmList}
|
||||
|
@ -0,0 +1,39 @@
|
||||
.container > label {
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.parametersContainer {
|
||||
composes: selection from '@shared/FormInput/InputElement/AssetSelection/index.module.css';
|
||||
|
||||
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2);
|
||||
max-height: 50vh;
|
||||
|
||||
/* smooth overflow scrolling for pre-iOS 13 */
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.parameter > div {
|
||||
margin-bottom: 0;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.parameter {
|
||||
position: relative;
|
||||
margin-left: -1rem;
|
||||
margin-right: -1rem;
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
padding-bottom: calc(var(--spacer) / 2);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.parameter:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.parameter > div > div {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import Input from '@shared/FormInput'
|
||||
import Label from '@shared/FormInput/Label'
|
||||
import { Field, useField } from 'formik'
|
||||
import styles from './FormConsumerParameters.module.css'
|
||||
import { ConsumerParameter, UserCustomParameters } from '@oceanprotocol/lib'
|
||||
|
||||
export function getDefaultValues(
|
||||
parameters: ConsumerParameter[]
|
||||
): UserCustomParameters {
|
||||
const defaults = {}
|
||||
parameters?.forEach((param) => {
|
||||
Object.assign(defaults, {
|
||||
[param.name]:
|
||||
param.type === 'number'
|
||||
? Number(param.default)
|
||||
: param.type === 'boolean'
|
||||
? param.default.toString()
|
||||
: param.default
|
||||
})
|
||||
})
|
||||
|
||||
return defaults
|
||||
}
|
||||
|
||||
export default function FormConsumerParameters({
|
||||
name,
|
||||
parameters
|
||||
}: {
|
||||
name: string
|
||||
parameters: ConsumerParameter[]
|
||||
}): ReactElement {
|
||||
const [field] = useField<UserCustomParameters[]>(name)
|
||||
|
||||
const getParameterOptions = (parameter: ConsumerParameter): string[] => {
|
||||
if (!parameter.options && parameter.type !== 'boolean') return []
|
||||
|
||||
const updatedOptions =
|
||||
parameter.type === 'boolean'
|
||||
? ['true', 'false']
|
||||
: parameter.type === 'select'
|
||||
? JSON.parse(parameter.options)?.map((option) => Object.keys(option)[0])
|
||||
: []
|
||||
|
||||
// add empty option, if parameter is optional
|
||||
if (!parameter.required) updatedOptions.unshift('')
|
||||
|
||||
return updatedOptions
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Label htmlFor="Input the consumer parameters">
|
||||
Input the consumer parameters
|
||||
</Label>
|
||||
<div className={styles.parametersContainer}>
|
||||
{parameters?.map((param) => {
|
||||
const { default: paramDefault, ...rest } = param
|
||||
|
||||
return (
|
||||
<div key={param.name} className={styles.parameter}>
|
||||
<Field
|
||||
{...rest}
|
||||
component={Input}
|
||||
help={param.description}
|
||||
name={`${name}.${param.name}`}
|
||||
options={getParameterOptions(param)}
|
||||
size="small"
|
||||
type={param.type === 'boolean' ? 'select' : param.type}
|
||||
value={field.value[param.name]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { ConsumerParameter } from '@oceanprotocol/lib'
|
||||
import * as Yup from 'yup'
|
||||
import { SchemaLike } from 'yup/lib/types'
|
||||
|
||||
export function getUserCustomParameterValidationSchema(
|
||||
parameters: ConsumerParameter[]
|
||||
): SchemaLike {
|
||||
const shape = {}
|
||||
|
||||
parameters?.forEach((parameter) => {
|
||||
const schemaBase =
|
||||
parameter.type === 'number'
|
||||
? Yup.number()
|
||||
: parameter.type === 'boolean'
|
||||
? Yup.boolean()
|
||||
: Yup.string()
|
||||
|
||||
Object.assign(shape, {
|
||||
[parameter.name]: parameter.required
|
||||
? schemaBase.required('required')
|
||||
: schemaBase.nullable().transform((value) => value || null)
|
||||
})
|
||||
})
|
||||
|
||||
const schema = Yup.object(shape)
|
||||
|
||||
return schema
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
.container {
|
||||
margin-top: -1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-left: -2rem;
|
||||
margin-right: -2rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react'
|
||||
import FormConsumerParameters from './FormConsumerParameters'
|
||||
import styles from './index.module.css'
|
||||
import Tabs, { TabsItem } from '@shared/atoms/Tabs'
|
||||
import { ConsumerParameter, UserCustomParameters } from '@oceanprotocol/lib'
|
||||
|
||||
export function parseConsumerParameterValues(
|
||||
formValues?: UserCustomParameters,
|
||||
parameters?: ConsumerParameter[]
|
||||
): UserCustomParameters {
|
||||
if (!formValues) return
|
||||
|
||||
const parsedValues = {}
|
||||
Object.entries(formValues)?.forEach((userCustomParameter) => {
|
||||
const [userCustomParameterKey, userCustomParameterValue] =
|
||||
userCustomParameter
|
||||
|
||||
const { type } = parameters.find(
|
||||
(param) => param.name === userCustomParameterKey
|
||||
)
|
||||
|
||||
Object.assign(parsedValues, {
|
||||
[userCustomParameterKey]:
|
||||
type === 'select' && userCustomParameterValue === ''
|
||||
? undefined
|
||||
: type === 'boolean'
|
||||
? userCustomParameterValue === 'true'
|
||||
: userCustomParameterValue
|
||||
})
|
||||
})
|
||||
|
||||
return parsedValues
|
||||
}
|
||||
|
||||
export default function ConsumerParameters({
|
||||
asset,
|
||||
selectedAlgorithmAsset
|
||||
}: {
|
||||
asset: AssetExtended
|
||||
selectedAlgorithmAsset?: AssetExtended
|
||||
}): ReactElement {
|
||||
const [tabs, setTabs] = useState<TabsItem[]>([])
|
||||
|
||||
const updateTabs = useCallback(() => {
|
||||
const tabs = []
|
||||
if (asset?.services[0]?.consumerParameters?.length > 0) {
|
||||
tabs.push({
|
||||
title: 'Data Service',
|
||||
content: (
|
||||
<FormConsumerParameters
|
||||
name="dataServiceParams"
|
||||
parameters={asset.services[0].consumerParameters}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
if (selectedAlgorithmAsset?.services[0]?.consumerParameters?.length > 0) {
|
||||
tabs.push({
|
||||
title: 'Algo Service',
|
||||
content: (
|
||||
<FormConsumerParameters
|
||||
name="algoServiceParams"
|
||||
parameters={selectedAlgorithmAsset.services[0].consumerParameters}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
if (
|
||||
selectedAlgorithmAsset?.metadata?.algorithm?.consumerParameters?.length >
|
||||
0
|
||||
) {
|
||||
tabs.push({
|
||||
title: 'Algo Params',
|
||||
content: (
|
||||
<FormConsumerParameters
|
||||
name="algoParams"
|
||||
parameters={
|
||||
selectedAlgorithmAsset.metadata?.algorithm.consumerParameters
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return tabs
|
||||
}, [asset, selectedAlgorithmAsset])
|
||||
|
||||
useEffect(() => {
|
||||
setTabs(updateTabs())
|
||||
}, [updateTabs])
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{tabs.length > 0 && <Tabs items={tabs} />}
|
||||
</div>
|
||||
)
|
||||
}
|
13
src/components/Asset/AssetActions/Download/_validation.ts
Normal file
13
src/components/Asset/AssetActions/Download/_validation.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as Yup from 'yup'
|
||||
import { getUserCustomParameterValidationSchema } from '../ConsumerParameters/_validation'
|
||||
import { ConsumerParameter } from '@oceanprotocol/lib'
|
||||
|
||||
export function getDownloadValidationSchema(
|
||||
parameters: ConsumerParameter[]
|
||||
): Yup.SchemaOf<{
|
||||
dataServiceParams: any
|
||||
}> {
|
||||
return Yup.object().shape({
|
||||
dataServiceParams: getUserCustomParameterValidationSchema(parameters)
|
||||
})
|
||||
}
|
@ -8,13 +8,29 @@
|
||||
display: flex;
|
||||
width: auto;
|
||||
padding: 0 calc(var(--spacer) / 2) 0 calc(var(--spacer) * 1.5);
|
||||
margin-bottom: var(--spacer);
|
||||
}
|
||||
|
||||
.filewrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.price {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.feedback {
|
||||
width: 100%;
|
||||
margin-top: calc(var(--spacer));
|
||||
}
|
||||
|
||||
.buttonBuy > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.buttonBuy > div > button {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
@ -2,11 +2,16 @@ import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import FileIcon from '@shared/FileIcon'
|
||||
import Price from '@shared/Price'
|
||||
import { useAsset } from '@context/Asset'
|
||||
import ButtonBuy from './ButtonBuy'
|
||||
import ButtonBuy from '../ButtonBuy'
|
||||
import { secondsToString } from '@utils/ddo'
|
||||
import AlgorithmDatasetsListForCompute from './Compute/AlgorithmDatasetsListForCompute'
|
||||
import styles from './Download.module.css'
|
||||
import { FileInfo, LoggerInstance, ZERO_ADDRESS } from '@oceanprotocol/lib'
|
||||
import AlgorithmDatasetsListForCompute from '../Compute/AlgorithmDatasetsListForCompute'
|
||||
import styles from './index.module.css'
|
||||
import {
|
||||
FileInfo,
|
||||
LoggerInstance,
|
||||
UserCustomParameters,
|
||||
ZERO_ADDRESS
|
||||
} from '@oceanprotocol/lib'
|
||||
import { order } from '@utils/order'
|
||||
import { downloadFile } from '@utils/provider'
|
||||
import { getOrderFeedback } from '@utils/feedback'
|
||||
@ -18,6 +23,12 @@ import Alert from '@shared/atoms/Alert'
|
||||
import Loader from '@shared/atoms/Loader'
|
||||
import { useAccount, useSigner } from 'wagmi'
|
||||
import useNetworkMetadata from '@hooks/useNetworkMetadata'
|
||||
import ConsumerParameters, {
|
||||
parseConsumerParameterValues
|
||||
} from '../ConsumerParameters'
|
||||
import { Form, Formik, useFormikContext } from 'formik'
|
||||
import { getDownloadValidationSchema } from './_validation'
|
||||
import { getDefaultValues } from '../ConsumerParameters/FormConsumerParameters'
|
||||
|
||||
export default function Download({
|
||||
asset,
|
||||
@ -142,7 +153,7 @@ export default function Download({
|
||||
orderPriceAndFees
|
||||
])
|
||||
|
||||
async function handleOrderOrDownload() {
|
||||
async function handleOrderOrDownload(dataParams?: UserCustomParameters) {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
@ -154,7 +165,7 @@ export default function Download({
|
||||
)[3]
|
||||
)
|
||||
|
||||
await downloadFile(signer, asset, accountId, validOrderTx)
|
||||
await downloadFile(signer, asset, accountId, validOrderTx, dataParams)
|
||||
} else {
|
||||
setStatusText(
|
||||
getOrderFeedback(
|
||||
@ -174,7 +185,7 @@ export default function Download({
|
||||
throw new Error()
|
||||
}
|
||||
setIsOwned(true)
|
||||
setValidOrderTx(tx?.transactionHash)
|
||||
setValidOrderTx(tx.transactionHash)
|
||||
}
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error)
|
||||
@ -187,16 +198,16 @@ export default function Download({
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const PurchaseButton = () => (
|
||||
const PurchaseButton = ({ isValid }: { isValid?: boolean }) => (
|
||||
<ButtonBuy
|
||||
action="download"
|
||||
disabled={isDisabled}
|
||||
disabled={isDisabled || !isValid}
|
||||
hasPreviousOrder={isOwned}
|
||||
hasDatatoken={hasDatatoken}
|
||||
btSymbol={asset?.accessDetails?.baseToken?.symbol}
|
||||
dtSymbol={asset?.datatokens[0]?.symbol}
|
||||
dtBalance={dtBalance}
|
||||
onClick={handleOrderOrDownload}
|
||||
type="submit"
|
||||
assetTimeout={secondsToString(asset?.services?.[0]?.timeout)}
|
||||
assetType={asset?.metadata?.type}
|
||||
stepText={statusText}
|
||||
@ -212,6 +223,8 @@ export default function Download({
|
||||
)
|
||||
|
||||
const AssetAction = ({ asset }: { asset: AssetExtended }) => {
|
||||
const { isValid } = useFormikContext()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isOrderDisabled ? (
|
||||
@ -230,18 +243,12 @@ export default function Download({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{isPriceLoading ? (
|
||||
<Loader message="Calculating full price (including fees)" />
|
||||
) : (
|
||||
<Price
|
||||
price={asset.stats?.price}
|
||||
orderPriceAndFees={orderPriceAndFees}
|
||||
conversion
|
||||
size="large"
|
||||
/>
|
||||
{asset && <ConsumerParameters asset={asset} />}
|
||||
{!isInPurgatory && (
|
||||
<div className={styles.buttonBuy}>
|
||||
<PurchaseButton isValid={isValid} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isInPurgatory && <PurchaseButton />}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -251,20 +258,52 @@ export default function Download({
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className={styles.consume}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.filewrapper}>
|
||||
<FileIcon file={file} isLoading={fileIsLoading} small />
|
||||
</div>
|
||||
<AssetAction asset={asset} />
|
||||
</div>
|
||||
|
||||
{asset?.metadata?.type === 'algorithm' && (
|
||||
<AlgorithmDatasetsListForCompute
|
||||
algorithmDid={asset.id}
|
||||
asset={asset}
|
||||
/>
|
||||
<Formik
|
||||
initialValues={{
|
||||
dataServiceParams: getDefaultValues(
|
||||
asset?.services[0].consumerParameters
|
||||
)
|
||||
}}
|
||||
validationSchema={getDownloadValidationSchema(
|
||||
asset?.services[0].consumerParameters
|
||||
)}
|
||||
</aside>
|
||||
onSubmit={async (values) => {
|
||||
const dataServiceParams = parseConsumerParameterValues(
|
||||
values?.dataServiceParams,
|
||||
asset.services[0].consumerParameters
|
||||
)
|
||||
|
||||
await handleOrderOrDownload(dataServiceParams)
|
||||
}}
|
||||
>
|
||||
<Form>
|
||||
<aside className={styles.consume}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.filewrapper}>
|
||||
<FileIcon file={file} isLoading={fileIsLoading} small />
|
||||
</div>
|
||||
{isPriceLoading ? (
|
||||
<Loader message="Calculating full price (including fees)" />
|
||||
) : (
|
||||
<Price
|
||||
className={styles.price}
|
||||
price={asset.stats?.price}
|
||||
orderPriceAndFees={orderPriceAndFees}
|
||||
conversion
|
||||
size="large"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<AssetAction asset={asset} />
|
||||
|
||||
{asset?.metadata?.type === 'algorithm' && (
|
||||
<AlgorithmDatasetsListForCompute
|
||||
algorithmDid={asset.id}
|
||||
asset={asset}
|
||||
/>
|
||||
)}
|
||||
</aside>
|
||||
</Form>
|
||||
</Formik>
|
||||
)
|
||||
}
|
@ -4,7 +4,6 @@ import Download from './Download'
|
||||
import { FileInfo, LoggerInstance, Datatoken } from '@oceanprotocol/lib'
|
||||
import { compareAsBN } from '@utils/numbers'
|
||||
import { useAsset } from '@context/Asset'
|
||||
import Web3Feedback from '@shared/Web3Feedback'
|
||||
import { getFileDidInfo, getFileInfo } from '@utils/provider'
|
||||
import { getOceanConfig } from '@utils/ocean'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
|
@ -2,12 +2,12 @@ import React, { ReactElement, useState, useEffect } from 'react'
|
||||
import { Formik } from 'formik'
|
||||
import {
|
||||
LoggerInstance,
|
||||
Metadata,
|
||||
FixedRateExchange,
|
||||
Asset,
|
||||
Service,
|
||||
Datatoken,
|
||||
Nft
|
||||
Nft,
|
||||
Metadata,
|
||||
Service
|
||||
} from '@oceanprotocol/lib'
|
||||
import { validationSchema } from './_validation'
|
||||
import { getInitialValues } from './_constants'
|
||||
@ -28,6 +28,7 @@ import { sanitizeUrl } from '@utils/url'
|
||||
import { getEncryptedFiles } from '@utils/provider'
|
||||
import { assetStateToNumber } from '@utils/assetState'
|
||||
import { useAccount, useProvider, useNetwork, useSigner } from 'wagmi'
|
||||
import { transformConsumerParameters } from '@components/Publish/_utils'
|
||||
|
||||
export default function Edit({
|
||||
asset
|
||||
@ -104,6 +105,13 @@ export default function Edit({
|
||||
tags: values.tags
|
||||
}
|
||||
|
||||
if (asset.metadata.type === 'algorithm') {
|
||||
updatedMetadata.algorithm.consumerParameters =
|
||||
!values.usesConsumerParameters
|
||||
? undefined
|
||||
: transformConsumerParameters(values.consumerParameters)
|
||||
}
|
||||
|
||||
asset?.accessDetails?.type === 'fixed' &&
|
||||
values.price !== asset.accessDetails.price &&
|
||||
(await updateFixedPrice(values.price))
|
||||
@ -138,6 +146,11 @@ export default function Edit({
|
||||
timeout: mapTimeoutStringToSeconds(values.timeout),
|
||||
files: updatedFiles
|
||||
}
|
||||
if (values?.service?.consumerParameters) {
|
||||
updatedService.consumerParameters = transformConsumerParameters(
|
||||
values.service.consumerParameters
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: remove version update at a later time
|
||||
const updatedAsset: Asset = {
|
||||
@ -189,7 +202,7 @@ export default function Edit({
|
||||
enableReinitialize
|
||||
initialValues={getInitialValues(
|
||||
asset?.metadata,
|
||||
asset?.services[0]?.timeout,
|
||||
asset?.services[0],
|
||||
asset?.accessDetails?.price || '0',
|
||||
paymentCollector,
|
||||
assetState
|
||||
|
4
src/components/Asset/Edit/FormEditMetadata.module.css
Normal file
4
src/components/Asset/Edit/FormEditMetadata.module.css
Normal file
@ -0,0 +1,4 @@
|
||||
.serviceContainer {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: var(--spacer);
|
||||
}
|
@ -7,6 +7,9 @@ import { FormPublishData } from '@components/Publish/_types'
|
||||
import { getFileInfo } from '@utils/provider'
|
||||
import { getFieldContent } from '@utils/form'
|
||||
import { isGoogleUrl } from '@utils/url'
|
||||
import styles from './FormEditMetadata.module.css'
|
||||
import { MetadataEditForm } from './_types'
|
||||
import consumerParametersContent from '../../../../content/publish/consumerParameters.json'
|
||||
|
||||
export function checkIfTimeoutInPredefinedValues(
|
||||
timeout: string,
|
||||
@ -130,6 +133,27 @@ export default function FormEditMetadata({
|
||||
/>
|
||||
|
||||
<Field {...getFieldContent('tags', data)} component={Input} name="tags" />
|
||||
|
||||
{asset.metadata.type === 'algorithm' && (
|
||||
<>
|
||||
<Field
|
||||
{...getFieldContent('usesConsumerParameters', data)}
|
||||
component={Input}
|
||||
name="usesConsumerParameters"
|
||||
/>
|
||||
{(values as unknown as MetadataEditForm).usesConsumerParameters && (
|
||||
<Field
|
||||
{...getFieldContent(
|
||||
'consumerParameters',
|
||||
consumerParametersContent.consumerParameters.fields
|
||||
)}
|
||||
component={Input}
|
||||
name="consumerParameters"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Field
|
||||
{...getFieldContent('paymentCollector', data)}
|
||||
component={Input}
|
||||
@ -140,6 +164,26 @@ export default function FormEditMetadata({
|
||||
component={Input}
|
||||
name="assetState"
|
||||
/>
|
||||
<div className={styles.serviceContainer}>
|
||||
<h4>Service</h4>
|
||||
|
||||
<Field
|
||||
{...getFieldContent('usesServiceConsumerParameters', data)}
|
||||
component={Input}
|
||||
name="service.usesConsumerParameters"
|
||||
/>
|
||||
{(values as unknown as MetadataEditForm).service
|
||||
.usesConsumerParameters && (
|
||||
<Field
|
||||
{...getFieldContent(
|
||||
'consumerParameters',
|
||||
consumerParametersContent.consumerParameters.fields
|
||||
)}
|
||||
component={Input}
|
||||
name="service.consumerParameters"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FormActions />
|
||||
</Form>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Metadata, ServiceComputeOptions } from '@oceanprotocol/lib'
|
||||
import { secondsToString } from '@utils/ddo'
|
||||
import { Metadata, Service, ServiceComputeOptions } from '@oceanprotocol/lib'
|
||||
import { parseConsumerParameters, secondsToString } from '@utils/ddo'
|
||||
import { ComputeEditForm, MetadataEditForm } from './_types'
|
||||
|
||||
export function getInitialValues(
|
||||
metadata: Metadata,
|
||||
timeout: number,
|
||||
service: Service,
|
||||
price: string,
|
||||
paymentCollector: string,
|
||||
assetState: string
|
||||
@ -15,11 +15,19 @@ export function getInitialValues(
|
||||
price,
|
||||
links: [{ url: '', type: 'url' }],
|
||||
files: [{ url: '', type: 'ipfs' }],
|
||||
timeout: secondsToString(timeout),
|
||||
timeout: secondsToString(service?.timeout),
|
||||
author: metadata?.author,
|
||||
tags: metadata?.tags,
|
||||
usesConsumerParameters: metadata?.algorithm?.consumerParameters?.length > 0,
|
||||
consumerParameters: parseConsumerParameters(
|
||||
metadata?.algorithm?.consumerParameters
|
||||
),
|
||||
paymentCollector,
|
||||
assetState
|
||||
assetState,
|
||||
service: {
|
||||
usesConsumerParameters: service?.consumerParameters?.length > 0,
|
||||
consumerParameters: parseConsumerParameters(service?.consumerParameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { FormConsumerParameter } from '@components/Publish/_types'
|
||||
import { FileInfo } from '@oceanprotocol/lib'
|
||||
export interface MetadataEditForm {
|
||||
name: string
|
||||
@ -9,7 +10,13 @@ export interface MetadataEditForm {
|
||||
links?: FileInfo[]
|
||||
author?: string
|
||||
tags?: string[]
|
||||
usesConsumerParameters?: boolean
|
||||
consumerParameters?: FormConsumerParameter[]
|
||||
assetState?: string
|
||||
service?: {
|
||||
usesConsumerParameters?: boolean
|
||||
consumerParameters?: FormConsumerParameter[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComputeEditForm {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { FileInfo } from '@oceanprotocol/lib'
|
||||
import * as Yup from 'yup'
|
||||
import { isAddress } from 'ethers/lib/utils'
|
||||
import { testLinks } from '../../../@utils/yup'
|
||||
import { testLinks } from '@utils/yup'
|
||||
import { validationConsumerParameters } from '@shared/FormInput/InputElement/ConsumerParameters/_validation'
|
||||
|
||||
export const validationSchema = Yup.object().shape({
|
||||
name: Yup.string()
|
||||
@ -37,6 +38,16 @@ export const validationSchema = Yup.object().shape({
|
||||
timeout: Yup.string().required('Required'),
|
||||
author: Yup.string().nullable(),
|
||||
tags: Yup.array<string[]>().nullable(),
|
||||
usesConsumerParameters: Yup.boolean(),
|
||||
consumerParameters: Yup.array().when('usesConsumerParameters', {
|
||||
is: true,
|
||||
then: Yup.array()
|
||||
.of(Yup.object().shape(validationConsumerParameters))
|
||||
.required('Required'),
|
||||
otherwise: Yup.array()
|
||||
.nullable()
|
||||
.transform((value) => value || null)
|
||||
}),
|
||||
paymentCollector: Yup.string().test(
|
||||
'ValidAddress',
|
||||
'Must be a valid Ethereum Address.',
|
||||
@ -44,7 +55,19 @@ export const validationSchema = Yup.object().shape({
|
||||
return isAddress(value)
|
||||
}
|
||||
),
|
||||
retireAsset: Yup.string()
|
||||
retireAsset: Yup.string(),
|
||||
service: Yup.object().shape({
|
||||
usesConsumerParameters: Yup.boolean(),
|
||||
consumerParameters: Yup.array().when('usesConsumerParameters', {
|
||||
is: true,
|
||||
then: Yup.array()
|
||||
.of(Yup.object().shape(validationConsumerParameters))
|
||||
.required('Required'),
|
||||
otherwise: Yup.array()
|
||||
.nullable()
|
||||
.transform((value) => value || null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
export const computeSettingsValidationSchema = Yup.object().shape({
|
||||
|
@ -3,6 +3,7 @@ import Input from '@shared/FormInput'
|
||||
import { Field, useField, useFormikContext } from 'formik'
|
||||
import React, { ReactElement, useEffect } from 'react'
|
||||
import content from '../../../../content/publish/form.json'
|
||||
import consumerParametersContent from '../../../../content/publish/consumerParameters.json'
|
||||
import { FormPublishData } from '../_types'
|
||||
import IconDataset from '@images/dataset.svg'
|
||||
import IconAlgorithm from '@images/algorithm.svg'
|
||||
@ -138,6 +139,24 @@ export default function MetadataFields(): ReactElement {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Field
|
||||
{...getFieldContent(
|
||||
'usesConsumerParameters',
|
||||
content.metadata.fields
|
||||
)}
|
||||
component={Input}
|
||||
name="metadata.usesConsumerParameters"
|
||||
/>
|
||||
{values.metadata.usesConsumerParameters && (
|
||||
<Field
|
||||
{...getFieldContent(
|
||||
'consumerParameters',
|
||||
consumerParametersContent.consumerParameters.fields
|
||||
)}
|
||||
component={Input}
|
||||
name="metadata.consumerParameters"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import React, { ReactElement, useEffect } from 'react'
|
||||
import IconDownload from '@images/download.svg'
|
||||
import IconCompute from '@images/compute.svg'
|
||||
import content from '../../../../content/publish/form.json'
|
||||
import consumerParametersContent from '../../../../content/publish/consumerParameters.json'
|
||||
import { getFieldContent } from '@utils/form'
|
||||
import { FormPublishData } from '../_types'
|
||||
import Alert from '@shared/atoms/Alert'
|
||||
@ -101,6 +102,21 @@ export default function ServicesFields(): ReactElement {
|
||||
component={Input}
|
||||
name="services[0].timeout"
|
||||
/>
|
||||
<Field
|
||||
{...getFieldContent('usesConsumerParameters', content.services.fields)}
|
||||
component={Input}
|
||||
name="services[0].usesConsumerParameters"
|
||||
/>
|
||||
{values.services[0].usesConsumerParameters && (
|
||||
<Field
|
||||
{...getFieldContent(
|
||||
'consumerParameters',
|
||||
consumerParametersContent.consumerParameters.fields
|
||||
)}
|
||||
component={Input}
|
||||
name="services[0].consumerParameters"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -68,7 +68,9 @@ export const initialValues: FormPublishData = {
|
||||
dockerImage: '',
|
||||
dockerImageCustom: '',
|
||||
dockerImageCustomTag: '',
|
||||
dockerImageCustomEntrypoint: ''
|
||||
dockerImageCustomEntrypoint: '',
|
||||
usesConsumerParameters: false,
|
||||
consumerParameters: []
|
||||
},
|
||||
services: [
|
||||
{
|
||||
@ -82,7 +84,9 @@ export const initialValues: FormPublishData = {
|
||||
valid: true,
|
||||
custom: false
|
||||
},
|
||||
computeOptions
|
||||
computeOptions,
|
||||
usesConsumerParameters: false,
|
||||
consumerParameters: []
|
||||
}
|
||||
],
|
||||
pricing: {
|
||||
|
@ -10,6 +10,8 @@ export interface FormPublishService {
|
||||
providerUrl: { url: string; valid: boolean; custom: boolean }
|
||||
algorithmPrivacy?: boolean
|
||||
computeOptions?: ServiceComputeOptions
|
||||
usesConsumerParameters?: boolean
|
||||
consumerParameters?: FormConsumerParameter[]
|
||||
}
|
||||
|
||||
export interface FormPublishData {
|
||||
@ -32,6 +34,12 @@ export interface FormPublishData {
|
||||
dockerImageCustomTag?: string
|
||||
dockerImageCustomEntrypoint?: string
|
||||
dockerImageCustomChecksum?: string
|
||||
usesConsumerParameters?: boolean
|
||||
consumerParameters?: FormConsumerParameter[]
|
||||
service?: {
|
||||
usesConsumerParameters?: boolean
|
||||
consumerParameters?: FormConsumerParameter[]
|
||||
}
|
||||
}
|
||||
services: FormPublishService[]
|
||||
pricing: PricePublishOptions
|
||||
@ -61,3 +69,14 @@ export interface MetadataAlgorithmContainer {
|
||||
tag: string
|
||||
checksum: string
|
||||
}
|
||||
|
||||
export interface FormConsumerParameter {
|
||||
name: string
|
||||
type: 'text' | 'number' | 'boolean' | 'select'
|
||||
label: string
|
||||
required: string
|
||||
description: string
|
||||
default: string | boolean | number
|
||||
options?: { key: string; value: string }[]
|
||||
value?: string | boolean | number
|
||||
}
|
||||
|
@ -7,19 +7,24 @@ import {
|
||||
DispenserCreationParams,
|
||||
getHash,
|
||||
LoggerInstance,
|
||||
Metadata,
|
||||
NftCreateData,
|
||||
NftFactory,
|
||||
Service,
|
||||
ZERO_ADDRESS,
|
||||
getEventFromTx
|
||||
getEventFromTx,
|
||||
ConsumerParameter,
|
||||
Metadata,
|
||||
Service
|
||||
} from '@oceanprotocol/lib'
|
||||
import { mapTimeoutStringToSeconds, normalizeFile } from '@utils/ddo'
|
||||
import { generateNftCreateData } from '@utils/nft'
|
||||
import { getEncryptedFiles } from '@utils/provider'
|
||||
import slugify from 'slugify'
|
||||
import { algorithmContainerPresets } from './_constants'
|
||||
import { FormPublishData, MetadataAlgorithmContainer } from './_types'
|
||||
import {
|
||||
FormConsumerParameter,
|
||||
FormPublishData,
|
||||
MetadataAlgorithmContainer
|
||||
} from './_types'
|
||||
import {
|
||||
marketFeeAddress,
|
||||
publisherMarketOrderFee,
|
||||
@ -59,6 +64,33 @@ function transformTags(originalTags: string[]): string[] {
|
||||
return transformedTags
|
||||
}
|
||||
|
||||
export function transformConsumerParameters(
|
||||
parameters: FormConsumerParameter[]
|
||||
): ConsumerParameter[] {
|
||||
if (!parameters?.length) return
|
||||
|
||||
const transformedValues = parameters.map((param) => {
|
||||
const options =
|
||||
param.type === 'select'
|
||||
? // Transform from { key: string, value: string } into { key: value }
|
||||
JSON.stringify(
|
||||
param.options?.map((opt) => ({ [opt.key]: opt.value }))
|
||||
)
|
||||
: undefined
|
||||
|
||||
const required = param.required === 'required'
|
||||
|
||||
return {
|
||||
...param,
|
||||
options,
|
||||
required,
|
||||
default: param.default.toString()
|
||||
}
|
||||
})
|
||||
|
||||
return transformedValues as ConsumerParameter[]
|
||||
}
|
||||
|
||||
export async function transformPublishFormToDdo(
|
||||
values: FormPublishData,
|
||||
// Those 2 are only passed during actual publishing process
|
||||
@ -79,7 +111,9 @@ export async function transformPublishFormToDdo(
|
||||
dockerImageCustom,
|
||||
dockerImageCustomTag,
|
||||
dockerImageCustomEntrypoint,
|
||||
dockerImageCustomChecksum
|
||||
dockerImageCustomChecksum,
|
||||
usesConsumerParameters,
|
||||
consumerParameters
|
||||
} = metadata
|
||||
const { access, files, links, providerUrl, timeout } = services[0]
|
||||
|
||||
@ -98,6 +132,10 @@ export async function transformPublishFormToDdo(
|
||||
const linksTransformed = links?.length &&
|
||||
links[0].valid && [sanitizeUrl(links[0].url)]
|
||||
|
||||
const consumerParametersTransformed = usesConsumerParameters
|
||||
? transformConsumerParameters(consumerParameters)
|
||||
: undefined
|
||||
|
||||
const newMetadata: Metadata = {
|
||||
created: currentTime,
|
||||
updated: currentTime,
|
||||
@ -135,7 +173,8 @@ export async function transformPublishFormToDdo(
|
||||
dockerImage === 'custom'
|
||||
? dockerImageCustomChecksum
|
||||
: algorithmContainerPresets.checksum
|
||||
}
|
||||
},
|
||||
consumerParameters: consumerParametersTransformed
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -161,7 +200,10 @@ export async function transformPublishFormToDdo(
|
||||
timeout: mapTimeoutStringToSeconds(timeout),
|
||||
...(access === 'compute' && {
|
||||
compute: values.services[0].computeOptions
|
||||
})
|
||||
}),
|
||||
consumerParameters: values.services[0].usesConsumerParameters
|
||||
? transformConsumerParameters(values.services[0].consumerParameters)
|
||||
: undefined
|
||||
}
|
||||
|
||||
const newDdo: DDO = {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { MAX_DECIMALS } from '@utils/constants'
|
||||
import * as Yup from 'yup'
|
||||
import { getMaxDecimalsValidation } from '@utils/numbers'
|
||||
import { FileInfo } from '@oceanprotocol/lib'
|
||||
import { testLinks } from '../../@utils/yup'
|
||||
import { MAX_DECIMALS } from '@utils/constants'
|
||||
import { getMaxDecimalsValidation } from '@utils/numbers'
|
||||
import * as Yup from 'yup'
|
||||
import { testLinks } from '@utils/yup'
|
||||
import { validationConsumerParameters } from '@components/@shared/FormInput/InputElement/ConsumerParameters/_validation'
|
||||
|
||||
// TODO: conditional validation
|
||||
// e.g. when algo is selected, Docker image is required
|
||||
@ -26,7 +27,17 @@ const validationMetadata = {
|
||||
tags: Yup.array<string[]>().nullable(),
|
||||
termsAndConditions: Yup.boolean()
|
||||
.required('Required')
|
||||
.isTrue('Please agree to the Terms and Conditions.')
|
||||
.isTrue('Please agree to the Terms and Conditions.'),
|
||||
usesConsumerParameters: Yup.boolean(),
|
||||
consumerParameters: Yup.array().when('usesConsumerParameters', {
|
||||
is: true,
|
||||
then: Yup.array()
|
||||
.of(Yup.object().shape(validationConsumerParameters))
|
||||
.required('Required'),
|
||||
otherwise: Yup.array()
|
||||
.nullable()
|
||||
.transform((value) => value || null)
|
||||
})
|
||||
}
|
||||
|
||||
const validationService = {
|
||||
@ -60,6 +71,16 @@ const validationService = {
|
||||
url: Yup.string().url('Must be a valid URL.').required('Required'),
|
||||
valid: Yup.boolean().isTrue().required('Valid Provider is required.'),
|
||||
custom: Yup.boolean()
|
||||
}),
|
||||
usesConsumerParameters: Yup.boolean(),
|
||||
consumerParameters: Yup.array().when('usesConsumerParameters', {
|
||||
is: true,
|
||||
then: Yup.array()
|
||||
.of(Yup.object().shape(validationConsumerParameters))
|
||||
.required('Required'),
|
||||
otherwise: Yup.array()
|
||||
.nullable()
|
||||
.transform((value) => value || null)
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user