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",
|
"placeholder": "e.g. logistics",
|
||||||
"required": false
|
"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",
|
"name": "paymentCollector",
|
||||||
"label": "Payment Collector Address",
|
"label": "Payment Collector Address",
|
||||||
@ -199,6 +207,14 @@
|
|||||||
],
|
],
|
||||||
"sortOptions": false,
|
"sortOptions": false,
|
||||||
"required": 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.",
|
"help": "Provide the entrypoint for your algorithm.",
|
||||||
"required": true
|
"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",
|
"name": "termsAndConditions",
|
||||||
"label": "Terms & Conditions",
|
"label": "Terms & Conditions",
|
||||||
@ -255,6 +263,14 @@
|
|||||||
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
|
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
|
||||||
"sortOptions": false,
|
"sortOptions": false,
|
||||||
"required": true
|
"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 {
|
interface AssetExtended extends Asset {
|
||||||
accessDetails?: AccessDetails
|
accessDetails?: AccessDetails
|
||||||
views?: number
|
views?: number
|
||||||
|
metadata: MetadataExtended
|
||||||
|
services: ServiceExtended[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,14 @@ import {
|
|||||||
ComputeEditForm,
|
ComputeEditForm,
|
||||||
MetadataEditForm
|
MetadataEditForm
|
||||||
} from '@components/Asset/Edit/_types'
|
} from '@components/Asset/Edit/_types'
|
||||||
import { FormPublishData } from '@components/Publish/_types'
|
import {
|
||||||
|
FormConsumerParameter,
|
||||||
|
FormPublishData
|
||||||
|
} from '@components/Publish/_types'
|
||||||
import {
|
import {
|
||||||
Arweave,
|
Arweave,
|
||||||
Asset,
|
Asset,
|
||||||
|
ConsumerParameter,
|
||||||
DDO,
|
DDO,
|
||||||
FileInfo,
|
FileInfo,
|
||||||
GraphqlQuery,
|
GraphqlQuery,
|
||||||
@ -188,3 +192,30 @@ export function previewDebugPatch(
|
|||||||
|
|
||||||
return buildValuesPreview
|
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
|
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,
|
ProviderInstance,
|
||||||
UrlFile,
|
UrlFile,
|
||||||
AbiItem,
|
AbiItem,
|
||||||
|
UserCustomParameters,
|
||||||
getErrorMessage
|
getErrorMessage
|
||||||
} from '@oceanprotocol/lib'
|
} from '@oceanprotocol/lib'
|
||||||
// if customProviderUrl is set, we need to call provider using this custom endpoint
|
// if customProviderUrl is set, we need to call provider using this custom endpoint
|
||||||
import { customProviderUrl } from '../../app.config'
|
import { customProviderUrl } from '../../app.config'
|
||||||
import { QueryHeader } from '@shared/FormInput/InputElement/Headers'
|
import { KeyValuePair } from '@shared/FormInput/InputElement/KeyValueInput'
|
||||||
import { Signer } from 'ethers'
|
import { Signer } from 'ethers'
|
||||||
import { getValidUntilTime } from './compute'
|
import { getValidUntilTime } from './compute'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
@ -110,7 +111,7 @@ export async function getFileInfo(
|
|||||||
providerUrl: string,
|
providerUrl: string,
|
||||||
storageType: string,
|
storageType: string,
|
||||||
query?: string,
|
query?: string,
|
||||||
headers?: QueryHeader[],
|
headers?: KeyValuePair[],
|
||||||
abi?: string,
|
abi?: string,
|
||||||
chainId?: number,
|
chainId?: number,
|
||||||
method?: string
|
method?: string
|
||||||
@ -226,7 +227,8 @@ export async function downloadFile(
|
|||||||
signer: Signer,
|
signer: Signer,
|
||||||
asset: AssetExtended,
|
asset: AssetExtended,
|
||||||
accountId: string,
|
accountId: string,
|
||||||
validOrderTx?: string
|
validOrderTx?: string,
|
||||||
|
userCustomParameters?: UserCustomParameters
|
||||||
) {
|
) {
|
||||||
let downloadUrl
|
let downloadUrl
|
||||||
try {
|
try {
|
||||||
@ -236,7 +238,8 @@ export async function downloadFile(
|
|||||||
0,
|
0,
|
||||||
validOrderTx || asset.accessDetails.validOrderTx,
|
validOrderTx || asset.accessDetails.validOrderTx,
|
||||||
customProviderUrl || asset.services[0].serviceEndpoint,
|
customProviderUrl || asset.services[0].serviceEndpoint,
|
||||||
signer
|
signer,
|
||||||
|
userCustomParameters
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(JSON.parse(error.message))
|
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 { useAsset } from '@context/Asset'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import { useNetwork } from 'wagmi'
|
import { useNetwork } from 'wagmi'
|
||||||
import InputHeaders from '../Headers'
|
import InputKeyValue from '../KeyValueInput'
|
||||||
import Button from '@shared/atoms/Button'
|
import Button from '@shared/atoms/Button'
|
||||||
import Loader from '@shared/atoms/Loader'
|
import Loader from '@shared/atoms/Loader'
|
||||||
import { checkJson } from '@utils/codemirror'
|
import { checkJson } from '@utils/codemirror'
|
||||||
@ -161,7 +161,9 @@ export default function FilesInput(props: InputProps): ReactElement {
|
|||||||
<Field
|
<Field
|
||||||
key={i}
|
key={i}
|
||||||
component={
|
component={
|
||||||
innerField.type === 'headers' ? InputHeaders : Input
|
innerField.type === 'headers'
|
||||||
|
? InputKeyValue
|
||||||
|
: Input
|
||||||
}
|
}
|
||||||
{...innerField}
|
{...innerField}
|
||||||
name={`${field.name}[0].${innerField.value}`}
|
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 React, {
|
||||||
import InputElement from '../../InputElement'
|
ChangeEvent,
|
||||||
|
ReactElement,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState
|
||||||
|
} from 'react'
|
||||||
|
import InputElement from '..'
|
||||||
import Label from '../../Label'
|
import Label from '../../Label'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import Tooltip from '@shared/atoms/Tooltip'
|
import Tooltip from '@shared/atoms/Tooltip'
|
||||||
import Markdown from '@shared/Markdown'
|
import Markdown from '@shared/Markdown'
|
||||||
import Button from '@shared/atoms/Button'
|
import Button from '@shared/atoms/Button'
|
||||||
import { InputProps } from '@shared/FormInput'
|
import { InputProps } from '@shared/FormInput'
|
||||||
|
import classNames from 'classnames/bind'
|
||||||
|
|
||||||
export interface QueryHeader {
|
const cx = classNames.bind(styles)
|
||||||
|
|
||||||
|
export interface KeyValuePair {
|
||||||
key: string
|
key: string
|
||||||
value: 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 { label, help, prominentHelp, form, field } = props
|
||||||
|
|
||||||
const [currentKey, setCurrentKey] = useState('')
|
const [currentKey, setCurrentKey] = useState('')
|
||||||
const [currentValue, setCurrentValue] = useState('')
|
const [currentValue, setCurrentValue] = useState('')
|
||||||
const [disabledButton, setDisabledButton] = useState(true)
|
const [disabledButton, setDisabledButton] = useState(true)
|
||||||
|
const [hasOnlyUniqueKeys, setHasOnlyUniqueKeys] = useState(true)
|
||||||
|
|
||||||
const [headers, setHeaders] = useState([] as QueryHeader[])
|
const [pairs, setPairs] = useState(value || [])
|
||||||
|
|
||||||
const addHeader = () => {
|
const currentKeyExists = useCallback(() => {
|
||||||
setHeaders((prev) => [
|
return pairs.some((pair) => pair.key === currentKey)
|
||||||
|
}, [currentKey, pairs])
|
||||||
|
|
||||||
|
const addPair = () => {
|
||||||
|
if (currentKeyExists()) {
|
||||||
|
setHasOnlyUniqueKeys(false)
|
||||||
|
if (uniqueKeys) return
|
||||||
|
}
|
||||||
|
|
||||||
|
setPairs((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
key: currentKey,
|
key: currentKey,
|
||||||
@ -33,9 +65,9 @@ export default function InputHeaders(props: InputProps): ReactElement {
|
|||||||
setCurrentValue('')
|
setCurrentValue('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeHeader = (i: number) => {
|
const removePair = (index: number) => {
|
||||||
const newHeaders = headers.filter((header, index) => index !== i)
|
const newPairs = pairs.filter((pair, pairIndex) => pairIndex !== index)
|
||||||
setHeaders(newHeaders)
|
setPairs(newPairs)
|
||||||
setCurrentKey('')
|
setCurrentKey('')
|
||||||
setCurrentValue('')
|
setCurrentValue('')
|
||||||
}
|
}
|
||||||
@ -50,15 +82,22 @@ export default function InputHeaders(props: InputProps): ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.setFieldValue(`${field.name}`, headers)
|
form.setFieldValue(`${field.name}`, pairs)
|
||||||
}, [headers])
|
}, [pairs])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDisabledButton(!currentKey || !currentValue)
|
setDisabledButton(
|
||||||
}, [currentKey, currentValue])
|
!currentKey || !currentValue || (uniqueKeys && currentKeyExists())
|
||||||
|
)
|
||||||
|
setHasOnlyUniqueKeys(!currentKeyExists())
|
||||||
|
}, [currentKey, currentValue, uniqueKeys, currentKeyExists])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
|
className={cx({
|
||||||
|
hasError: uniqueKeys && !hasOnlyUniqueKeys
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Label htmlFor={props.name}>
|
<Label htmlFor={props.name}>
|
||||||
{label}
|
{label}
|
||||||
{props.required && (
|
{props.required && (
|
||||||
@ -71,18 +110,19 @@ export default function InputHeaders(props: InputProps): ReactElement {
|
|||||||
)}
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<div className={styles.headersContainer}>
|
<div className={styles.pairsContainer}>
|
||||||
<InputElement
|
<InputElement
|
||||||
|
className={styles.keyInput}
|
||||||
name={`${field.name}.key`}
|
name={`${field.name}.key`}
|
||||||
placeholder={'key'}
|
placeholder={keyPlaceholder}
|
||||||
value={`${currentKey}`}
|
value={`${currentKey}`}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputElement
|
<InputElement
|
||||||
className={`${styles.input}`}
|
className={styles.input}
|
||||||
name={`${field.name}.value`}
|
name={`${field.name}.value`}
|
||||||
placeholder={'value'}
|
placeholder={valuePlaceholder}
|
||||||
value={`${currentValue}`}
|
value={`${currentValue}`}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
@ -92,26 +132,32 @@ export default function InputHeaders(props: InputProps): ReactElement {
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={(e: React.SyntheticEvent) => {
|
onClick={(e: React.SyntheticEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
addHeader()
|
addPair()
|
||||||
}}
|
}}
|
||||||
disabled={disabledButton}
|
disabled={disabledButton}
|
||||||
>
|
>
|
||||||
add
|
add
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{uniqueKeys && !hasOnlyUniqueKeys && (
|
||||||
|
<p
|
||||||
|
className={styles.error}
|
||||||
|
>{`The ${keyPlaceholder} field must be unique`}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{headers.length > 0 &&
|
{pairs.length > 0 &&
|
||||||
headers.map((header, i) => {
|
pairs.map((header, i) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.headersAddedContainer} key={`header_${i}`}>
|
<div className={styles.pairsAddedContainer} key={`pair_${i}`}>
|
||||||
<InputElement
|
<InputElement
|
||||||
name={`header[${i}].key`}
|
name={`pair[${i}].key`}
|
||||||
value={`${header.key}`}
|
value={`${header.key}`}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputElement
|
<InputElement
|
||||||
name={`header[${i}].key`}
|
name={`pair[${i}].value`}
|
||||||
value={`${header.value}`}
|
value={`${header.value}`}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
@ -121,7 +167,7 @@ export default function InputHeaders(props: InputProps): ReactElement {
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={(e: React.SyntheticEvent) => {
|
onClick={(e: React.SyntheticEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
removeHeader(i)
|
removePair(i)
|
||||||
}}
|
}}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
>
|
>
|
@ -16,6 +16,7 @@ import TabsFile from '@shared/atoms/TabsFile'
|
|||||||
import useDarkMode from '@oceanprotocol/use-dark-mode'
|
import useDarkMode from '@oceanprotocol/use-dark-mode'
|
||||||
import appConfig from '../../../../../app.config'
|
import appConfig from '../../../../../app.config'
|
||||||
import { extensions, oceanTheme } from '@utils/codemirror'
|
import { extensions, oceanTheme } from '@utils/codemirror'
|
||||||
|
import { ConsumerParameters } from './ConsumerParameters'
|
||||||
|
|
||||||
const cx = classNames.bind(styles)
|
const cx = classNames.bind(styles)
|
||||||
|
|
||||||
@ -130,6 +131,9 @@ export default function InputElement({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case 'consumerParameters':
|
||||||
|
return <ConsumerParameters {...field} form={form} {...props} />
|
||||||
|
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
return <textarea id={props.name} className={styles.textarea} {...props} />
|
return <textarea id={props.name} className={styles.textarea} {...props} />
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import React, {
|
|||||||
import InputElement from './InputElement'
|
import InputElement from './InputElement'
|
||||||
import Label from './Label'
|
import Label from './Label'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import { ErrorMessage, FieldInputProps } from 'formik'
|
import { ErrorMessage, FieldInputProps, useField } from 'formik'
|
||||||
import classNames from 'classnames/bind'
|
import classNames from 'classnames/bind'
|
||||||
import Disclaimer from './Disclaimer'
|
import Disclaimer from './Disclaimer'
|
||||||
import Tooltip from '@shared/atoms/Tooltip'
|
import Tooltip from '@shared/atoms/Tooltip'
|
||||||
@ -18,6 +18,7 @@ import Markdown from '@shared/Markdown'
|
|||||||
import FormHelp from './Help'
|
import FormHelp from './Help'
|
||||||
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
|
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
|
||||||
import { BoxSelectionOption } from '@shared/FormInput/InputElement/BoxSelection'
|
import { BoxSelectionOption } from '@shared/FormInput/InputElement/BoxSelection'
|
||||||
|
import { getObjectPropertyByPath } from '@utils/index'
|
||||||
|
|
||||||
const cx = classNames.bind(styles)
|
const cx = classNames.bind(styles)
|
||||||
|
|
||||||
@ -71,21 +72,17 @@ export interface InputProps {
|
|||||||
disclaimerValues?: string[]
|
disclaimerValues?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkError(
|
function checkError(form: any, field: FieldInputProps<any>) {
|
||||||
form: any,
|
const touched = getObjectPropertyByPath(form?.touched, field?.name)
|
||||||
parsedFieldName: string[],
|
const errors = getObjectPropertyByPath(form?.errors, field?.name)
|
||||||
field: FieldInputProps<any>
|
|
||||||
) {
|
return (
|
||||||
if (
|
touched &&
|
||||||
(form?.touched?.[parsedFieldName[0]]?.[parsedFieldName[1]] &&
|
errors &&
|
||||||
form?.errors?.[parsedFieldName[0]]?.[parsedFieldName[1]]) ||
|
!field.name.endsWith('.files') &&
|
||||||
(form?.touched[field?.name] &&
|
!field.name.endsWith('.links') &&
|
||||||
form?.errors[field?.name] &&
|
!field.name.endsWith('consumerParameters')
|
||||||
field.name !== 'files' &&
|
)
|
||||||
field.name !== 'links')
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Input(props: Partial<InputProps>): ReactElement {
|
export default function Input(props: Partial<InputProps>): ReactElement {
|
||||||
@ -102,13 +99,10 @@ export default function Input(props: Partial<InputProps>): ReactElement {
|
|||||||
} = props
|
} = props
|
||||||
|
|
||||||
const isFormikField = typeof field !== 'undefined'
|
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
|
// 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?
|
// `useField()` hook in here to get `meta.error` so we have to match against form?.errors?
|
||||||
// handling flat and nested data at same time.
|
// handling flat and nested data at same time.
|
||||||
const parsedFieldName =
|
const hasFormikError = checkError(form, field)
|
||||||
isFormikField && (isNestedField ? field?.name.split('.') : [field?.name])
|
|
||||||
const hasFormikError = checkError(form, parsedFieldName, field)
|
|
||||||
|
|
||||||
const styleClasses = cx({
|
const styleClasses = cx({
|
||||||
field: true,
|
field: true,
|
||||||
@ -123,7 +117,7 @@ export default function Input(props: Partial<InputProps>): ReactElement {
|
|||||||
if (disclaimer && disclaimerValues) {
|
if (disclaimer && disclaimerValues) {
|
||||||
setDisclaimerVisible(
|
setDisclaimerVisible(
|
||||||
disclaimerValues.includes(
|
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 React, { ReactElement, ReactNode } from 'react'
|
||||||
import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs'
|
import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs'
|
||||||
import styles from './index.module.css'
|
|
||||||
import InputRadio from '@shared/FormInput/InputElement/Radio'
|
import InputRadio from '@shared/FormInput/InputElement/Radio'
|
||||||
|
import styles from './index.module.css'
|
||||||
export interface TabsItem {
|
export interface TabsItem {
|
||||||
title: string
|
title: string
|
||||||
content: ReactNode
|
content: ReactNode
|
||||||
@ -15,6 +14,8 @@ export interface TabsProps {
|
|||||||
handleTabChange?: (tabName: string) => void
|
handleTabChange?: (tabName: string) => void
|
||||||
defaultIndex?: number
|
defaultIndex?: number
|
||||||
showRadio?: boolean
|
showRadio?: boolean
|
||||||
|
selectedIndex?: number
|
||||||
|
onIndexSelected?: (index: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Tabs({
|
export default function Tabs({
|
||||||
@ -22,10 +23,17 @@ export default function Tabs({
|
|||||||
className,
|
className,
|
||||||
handleTabChange,
|
handleTabChange,
|
||||||
defaultIndex,
|
defaultIndex,
|
||||||
showRadio
|
showRadio,
|
||||||
|
selectedIndex,
|
||||||
|
onIndexSelected
|
||||||
}: TabsProps): ReactElement {
|
}: TabsProps): ReactElement {
|
||||||
return (
|
return (
|
||||||
<ReactTabs className={`${className || ''}`} defaultIndex={defaultIndex}>
|
<ReactTabs
|
||||||
|
className={`${className || ''}`}
|
||||||
|
selectedIndex={selectedIndex}
|
||||||
|
onSelect={onIndexSelected}
|
||||||
|
defaultIndex={selectedIndex ? undefined : defaultIndex}
|
||||||
|
>
|
||||||
<div className={styles.tabListContainer}>
|
<div className={styles.tabListContainer}>
|
||||||
<TabList className={styles.tabList}>
|
<TabList className={styles.tabList}>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
|
@ -10,14 +10,14 @@ import { useAsset } from '@context/Asset'
|
|||||||
import content from '../../../../../content/pages/startComputeDataset.json'
|
import content from '../../../../../content/pages/startComputeDataset.json'
|
||||||
import { Asset, ZERO_ADDRESS } from '@oceanprotocol/lib'
|
import { Asset, ZERO_ADDRESS } from '@oceanprotocol/lib'
|
||||||
import { getAccessDetails } from '@utils/accessDetailsAndPricing'
|
import { getAccessDetails } from '@utils/accessDetailsAndPricing'
|
||||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
|
||||||
import Alert from '@shared/atoms/Alert'
|
|
||||||
import { getTokenBalanceFromSymbol } from '@utils/wallet'
|
import { getTokenBalanceFromSymbol } from '@utils/wallet'
|
||||||
import { MAX_DECIMALS } from '@utils/constants'
|
import { MAX_DECIMALS } from '@utils/constants'
|
||||||
import Decimal from 'decimal.js'
|
import Decimal from 'decimal.js'
|
||||||
import { useAccount } from 'wagmi'
|
import { useAccount } from 'wagmi'
|
||||||
import useBalance from '@hooks/useBalance'
|
import useBalance from '@hooks/useBalance'
|
||||||
import useNetworkMetadata from '@hooks/useNetworkMetadata'
|
import useNetworkMetadata from '@hooks/useNetworkMetadata'
|
||||||
|
import ConsumerParameters from '../ConsumerParameters'
|
||||||
|
import { FormConsumerParameter } from '@components/Publish/_types'
|
||||||
|
|
||||||
export default function FormStartCompute({
|
export default function FormStartCompute({
|
||||||
algorithms,
|
algorithms,
|
||||||
@ -78,12 +78,18 @@ export default function FormStartCompute({
|
|||||||
validUntil?: string
|
validUntil?: string
|
||||||
retry: boolean
|
retry: boolean
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const { siteContent } = useMarketMetadata()
|
|
||||||
const { address: accountId, isConnected } = useAccount()
|
const { address: accountId, isConnected } = useAccount()
|
||||||
const { balance } = useBalance()
|
const { balance } = useBalance()
|
||||||
const { isSupportedOceanNetwork } = useNetworkMetadata()
|
const { isSupportedOceanNetwork } = useNetworkMetadata()
|
||||||
const { isValid, values }: FormikContextType<{ algorithm: string }> =
|
const {
|
||||||
useFormikContext()
|
isValid,
|
||||||
|
values
|
||||||
|
}: FormikContextType<{
|
||||||
|
algorithm: string
|
||||||
|
dataServiceParams: FormConsumerParameter[]
|
||||||
|
algoServiceParams: FormConsumerParameter[]
|
||||||
|
algoParams: FormConsumerParameter[]
|
||||||
|
}> = useFormikContext()
|
||||||
const { asset, isAssetNetwork } = useAsset()
|
const { asset, isAssetNetwork } = useAsset()
|
||||||
|
|
||||||
const [datasetOrderPrice, setDatasetOrderPrice] = useState(
|
const [datasetOrderPrice, setDatasetOrderPrice] = useState(
|
||||||
@ -249,7 +255,12 @@ export default function FormStartCompute({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
{asset && selectedAlgorithmAsset && (
|
||||||
|
<ConsumerParameters
|
||||||
|
asset={asset}
|
||||||
|
selectedAlgorithmAsset={selectedAlgorithmAsset}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<PriceOutput
|
<PriceOutput
|
||||||
hasPreviousOrder={hasPreviousOrder}
|
hasPreviousOrder={hasPreviousOrder}
|
||||||
assetTimeout={assetTimeout}
|
assetTimeout={assetTimeout}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
.priceComponent {
|
.priceComponent {
|
||||||
margin-left: -2rem;
|
margin-left: -2rem;
|
||||||
margin-right: -2rem;
|
margin-right: -2rem;
|
||||||
margin-top: -1rem;
|
|
||||||
margin-bottom: calc(var(--spacer) / 1.5);
|
margin-bottom: calc(var(--spacer) / 1.5);
|
||||||
padding-left: calc(var(--spacer) / 2);
|
padding-left: calc(var(--spacer) / 2);
|
||||||
padding-right: 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 * 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
|
algorithm: string
|
||||||
}> = Yup.object().shape({
|
dataServiceParams: any
|
||||||
algorithm: Yup.string().required('Required')
|
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 {
|
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,
|
ComputeOutput,
|
||||||
ProviderComputeInitializeResults,
|
ProviderComputeInitializeResults,
|
||||||
unitsToAmount,
|
unitsToAmount,
|
||||||
minAbi,
|
|
||||||
ProviderFees,
|
ProviderFees,
|
||||||
|
UserCustomParameters,
|
||||||
getErrorMessage
|
getErrorMessage
|
||||||
} from '@oceanprotocol/lib'
|
} from '@oceanprotocol/lib'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
@ -22,7 +22,7 @@ import Price from '@shared/Price'
|
|||||||
import FileIcon from '@shared/FileIcon'
|
import FileIcon from '@shared/FileIcon'
|
||||||
import Alert from '@shared/atoms/Alert'
|
import Alert from '@shared/atoms/Alert'
|
||||||
import { Formik } from 'formik'
|
import { Formik } from 'formik'
|
||||||
import { getInitialValues, validationSchema } from './_constants'
|
import { getComputeValidationSchema, getInitialValues } from './_constants'
|
||||||
import FormStartComputeDataset from './FormComputeDataset'
|
import FormStartComputeDataset from './FormComputeDataset'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import SuccessConfetti from '@shared/SuccessConfetti'
|
import SuccessConfetti from '@shared/SuccessConfetti'
|
||||||
@ -50,6 +50,7 @@ import { useAccount, useSigner } from 'wagmi'
|
|||||||
import { getDummySigner } from '@utils/wallet'
|
import { getDummySigner } from '@utils/wallet'
|
||||||
import useNetworkMetadata from '@hooks/useNetworkMetadata'
|
import useNetworkMetadata from '@hooks/useNetworkMetadata'
|
||||||
import { useAsset } from '@context/Asset'
|
import { useAsset } from '@context/Asset'
|
||||||
|
import { parseConsumerParameterValues } from '../ConsumerParameters'
|
||||||
|
|
||||||
const refreshInterval = 10000 // 10 sec.
|
const refreshInterval = 10000 // 10 sec.
|
||||||
|
|
||||||
@ -339,7 +340,11 @@ export default function Compute({
|
|||||||
toast.error(errorMsg)
|
toast.error(errorMsg)
|
||||||
}, [error])
|
}, [error])
|
||||||
|
|
||||||
async function startJob(): Promise<void> {
|
async function startJob(userCustomParameters: {
|
||||||
|
dataServiceParams?: UserCustomParameters
|
||||||
|
algoServiceParams?: UserCustomParameters
|
||||||
|
algoParams?: UserCustomParameters
|
||||||
|
}): Promise<void> {
|
||||||
try {
|
try {
|
||||||
setIsOrdering(true)
|
setIsOrdering(true)
|
||||||
setIsOrdered(false)
|
setIsOrdered(false)
|
||||||
@ -347,7 +352,9 @@ export default function Compute({
|
|||||||
const computeService = getServiceByName(asset, 'compute')
|
const computeService = getServiceByName(asset, 'compute')
|
||||||
const computeAlgorithm: ComputeAlgorithm = {
|
const computeAlgorithm: ComputeAlgorithm = {
|
||||||
documentId: selectedAlgorithmAsset.id,
|
documentId: selectedAlgorithmAsset.id,
|
||||||
serviceId: selectedAlgorithmAsset.services[0].id
|
serviceId: selectedAlgorithmAsset.services[0].id,
|
||||||
|
algocustomdata: userCustomParameters?.algoParams,
|
||||||
|
userdata: userCustomParameters?.algoServiceParams
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowed = await isOrderable(
|
const allowed = await isOrderable(
|
||||||
@ -406,7 +413,8 @@ export default function Compute({
|
|||||||
const computeAsset: ComputeAsset = {
|
const computeAsset: ComputeAsset = {
|
||||||
documentId: asset.id,
|
documentId: asset.id,
|
||||||
serviceId: asset.services[0].id,
|
serviceId: asset.services[0].id,
|
||||||
transferTxId: datasetOrderTx
|
transferTxId: datasetOrderTx,
|
||||||
|
userdata: userCustomParameters?.dataServiceParams
|
||||||
}
|
}
|
||||||
computeAlgorithm.transferTxId = algorithmOrderTx
|
computeAlgorithm.transferTxId = algorithmOrderTx
|
||||||
const output: ComputeOutput = {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@ -480,13 +514,15 @@ export default function Compute({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={getInitialValues()}
|
initialValues={getInitialValues(asset, selectedAlgorithmAsset)}
|
||||||
validateOnMount
|
validateOnMount
|
||||||
validationSchema={validationSchema}
|
validationSchema={getComputeValidationSchema(
|
||||||
onSubmit={async (values) => {
|
asset.services[0].consumerParameters,
|
||||||
if (!values.algorithm) return
|
selectedAlgorithmAsset?.services[0].consumerParameters,
|
||||||
await startJob()
|
selectedAlgorithmAsset?.metadata?.algorithm?.consumerParameters
|
||||||
}}
|
)}
|
||||||
|
enableReinitialize
|
||||||
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
<FormStartComputeDataset
|
<FormStartComputeDataset
|
||||||
algorithms={algorithmList}
|
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;
|
display: flex;
|
||||||
width: auto;
|
width: auto;
|
||||||
padding: 0 calc(var(--spacer) / 2) 0 calc(var(--spacer) * 1.5);
|
padding: 0 calc(var(--spacer) / 2) 0 calc(var(--spacer) * 1.5);
|
||||||
|
margin-bottom: var(--spacer);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filewrapper {
|
.filewrapper {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
.feedback {
|
.feedback {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: calc(var(--spacer));
|
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 FileIcon from '@shared/FileIcon'
|
||||||
import Price from '@shared/Price'
|
import Price from '@shared/Price'
|
||||||
import { useAsset } from '@context/Asset'
|
import { useAsset } from '@context/Asset'
|
||||||
import ButtonBuy from './ButtonBuy'
|
import ButtonBuy from '../ButtonBuy'
|
||||||
import { secondsToString } from '@utils/ddo'
|
import { secondsToString } from '@utils/ddo'
|
||||||
import AlgorithmDatasetsListForCompute from './Compute/AlgorithmDatasetsListForCompute'
|
import AlgorithmDatasetsListForCompute from '../Compute/AlgorithmDatasetsListForCompute'
|
||||||
import styles from './Download.module.css'
|
import styles from './index.module.css'
|
||||||
import { FileInfo, LoggerInstance, ZERO_ADDRESS } from '@oceanprotocol/lib'
|
import {
|
||||||
|
FileInfo,
|
||||||
|
LoggerInstance,
|
||||||
|
UserCustomParameters,
|
||||||
|
ZERO_ADDRESS
|
||||||
|
} from '@oceanprotocol/lib'
|
||||||
import { order } from '@utils/order'
|
import { order } from '@utils/order'
|
||||||
import { downloadFile } from '@utils/provider'
|
import { downloadFile } from '@utils/provider'
|
||||||
import { getOrderFeedback } from '@utils/feedback'
|
import { getOrderFeedback } from '@utils/feedback'
|
||||||
@ -18,6 +23,12 @@ import Alert from '@shared/atoms/Alert'
|
|||||||
import Loader from '@shared/atoms/Loader'
|
import Loader from '@shared/atoms/Loader'
|
||||||
import { useAccount, useSigner } from 'wagmi'
|
import { useAccount, useSigner } from 'wagmi'
|
||||||
import useNetworkMetadata from '@hooks/useNetworkMetadata'
|
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({
|
export default function Download({
|
||||||
asset,
|
asset,
|
||||||
@ -142,7 +153,7 @@ export default function Download({
|
|||||||
orderPriceAndFees
|
orderPriceAndFees
|
||||||
])
|
])
|
||||||
|
|
||||||
async function handleOrderOrDownload() {
|
async function handleOrderOrDownload(dataParams?: UserCustomParameters) {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -154,7 +165,7 @@ export default function Download({
|
|||||||
)[3]
|
)[3]
|
||||||
)
|
)
|
||||||
|
|
||||||
await downloadFile(signer, asset, accountId, validOrderTx)
|
await downloadFile(signer, asset, accountId, validOrderTx, dataParams)
|
||||||
} else {
|
} else {
|
||||||
setStatusText(
|
setStatusText(
|
||||||
getOrderFeedback(
|
getOrderFeedback(
|
||||||
@ -174,7 +185,7 @@ export default function Download({
|
|||||||
throw new Error()
|
throw new Error()
|
||||||
}
|
}
|
||||||
setIsOwned(true)
|
setIsOwned(true)
|
||||||
setValidOrderTx(tx?.transactionHash)
|
setValidOrderTx(tx.transactionHash)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LoggerInstance.error(error)
|
LoggerInstance.error(error)
|
||||||
@ -187,16 +198,16 @@ export default function Download({
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const PurchaseButton = () => (
|
const PurchaseButton = ({ isValid }: { isValid?: boolean }) => (
|
||||||
<ButtonBuy
|
<ButtonBuy
|
||||||
action="download"
|
action="download"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled || !isValid}
|
||||||
hasPreviousOrder={isOwned}
|
hasPreviousOrder={isOwned}
|
||||||
hasDatatoken={hasDatatoken}
|
hasDatatoken={hasDatatoken}
|
||||||
btSymbol={asset?.accessDetails?.baseToken?.symbol}
|
btSymbol={asset?.accessDetails?.baseToken?.symbol}
|
||||||
dtSymbol={asset?.datatokens[0]?.symbol}
|
dtSymbol={asset?.datatokens[0]?.symbol}
|
||||||
dtBalance={dtBalance}
|
dtBalance={dtBalance}
|
||||||
onClick={handleOrderOrDownload}
|
type="submit"
|
||||||
assetTimeout={secondsToString(asset?.services?.[0]?.timeout)}
|
assetTimeout={secondsToString(asset?.services?.[0]?.timeout)}
|
||||||
assetType={asset?.metadata?.type}
|
assetType={asset?.metadata?.type}
|
||||||
stepText={statusText}
|
stepText={statusText}
|
||||||
@ -212,6 +223,8 @@ export default function Download({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const AssetAction = ({ asset }: { asset: AssetExtended }) => {
|
const AssetAction = ({ asset }: { asset: AssetExtended }) => {
|
||||||
|
const { isValid } = useFormikContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isOrderDisabled ? (
|
{isOrderDisabled ? (
|
||||||
@ -230,18 +243,12 @@ export default function Download({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{isPriceLoading ? (
|
{asset && <ConsumerParameters asset={asset} />}
|
||||||
<Loader message="Calculating full price (including fees)" />
|
{!isInPurgatory && (
|
||||||
) : (
|
<div className={styles.buttonBuy}>
|
||||||
<Price
|
<PurchaseButton isValid={isValid} />
|
||||||
price={asset.stats?.price}
|
</div>
|
||||||
orderPriceAndFees={orderPriceAndFees}
|
|
||||||
conversion
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isInPurgatory && <PurchaseButton />}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -251,20 +258,52 @@ export default function Download({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={styles.consume}>
|
<Formik
|
||||||
<div className={styles.info}>
|
initialValues={{
|
||||||
<div className={styles.filewrapper}>
|
dataServiceParams: getDefaultValues(
|
||||||
<FileIcon file={file} isLoading={fileIsLoading} small />
|
asset?.services[0].consumerParameters
|
||||||
</div>
|
)
|
||||||
<AssetAction asset={asset} />
|
}}
|
||||||
</div>
|
validationSchema={getDownloadValidationSchema(
|
||||||
|
asset?.services[0].consumerParameters
|
||||||
{asset?.metadata?.type === 'algorithm' && (
|
|
||||||
<AlgorithmDatasetsListForCompute
|
|
||||||
algorithmDid={asset.id}
|
|
||||||
asset={asset}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</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 { FileInfo, LoggerInstance, Datatoken } from '@oceanprotocol/lib'
|
||||||
import { compareAsBN } from '@utils/numbers'
|
import { compareAsBN } from '@utils/numbers'
|
||||||
import { useAsset } from '@context/Asset'
|
import { useAsset } from '@context/Asset'
|
||||||
import Web3Feedback from '@shared/Web3Feedback'
|
|
||||||
import { getFileDidInfo, getFileInfo } from '@utils/provider'
|
import { getFileDidInfo, getFileInfo } from '@utils/provider'
|
||||||
import { getOceanConfig } from '@utils/ocean'
|
import { getOceanConfig } from '@utils/ocean'
|
||||||
import { useCancelToken } from '@hooks/useCancelToken'
|
import { useCancelToken } from '@hooks/useCancelToken'
|
||||||
|
@ -2,12 +2,12 @@ import React, { ReactElement, useState, useEffect } from 'react'
|
|||||||
import { Formik } from 'formik'
|
import { Formik } from 'formik'
|
||||||
import {
|
import {
|
||||||
LoggerInstance,
|
LoggerInstance,
|
||||||
Metadata,
|
|
||||||
FixedRateExchange,
|
FixedRateExchange,
|
||||||
Asset,
|
Asset,
|
||||||
Service,
|
|
||||||
Datatoken,
|
Datatoken,
|
||||||
Nft
|
Nft,
|
||||||
|
Metadata,
|
||||||
|
Service
|
||||||
} from '@oceanprotocol/lib'
|
} from '@oceanprotocol/lib'
|
||||||
import { validationSchema } from './_validation'
|
import { validationSchema } from './_validation'
|
||||||
import { getInitialValues } from './_constants'
|
import { getInitialValues } from './_constants'
|
||||||
@ -28,6 +28,7 @@ import { sanitizeUrl } from '@utils/url'
|
|||||||
import { getEncryptedFiles } from '@utils/provider'
|
import { getEncryptedFiles } from '@utils/provider'
|
||||||
import { assetStateToNumber } from '@utils/assetState'
|
import { assetStateToNumber } from '@utils/assetState'
|
||||||
import { useAccount, useProvider, useNetwork, useSigner } from 'wagmi'
|
import { useAccount, useProvider, useNetwork, useSigner } from 'wagmi'
|
||||||
|
import { transformConsumerParameters } from '@components/Publish/_utils'
|
||||||
|
|
||||||
export default function Edit({
|
export default function Edit({
|
||||||
asset
|
asset
|
||||||
@ -104,6 +105,13 @@ export default function Edit({
|
|||||||
tags: values.tags
|
tags: values.tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (asset.metadata.type === 'algorithm') {
|
||||||
|
updatedMetadata.algorithm.consumerParameters =
|
||||||
|
!values.usesConsumerParameters
|
||||||
|
? undefined
|
||||||
|
: transformConsumerParameters(values.consumerParameters)
|
||||||
|
}
|
||||||
|
|
||||||
asset?.accessDetails?.type === 'fixed' &&
|
asset?.accessDetails?.type === 'fixed' &&
|
||||||
values.price !== asset.accessDetails.price &&
|
values.price !== asset.accessDetails.price &&
|
||||||
(await updateFixedPrice(values.price))
|
(await updateFixedPrice(values.price))
|
||||||
@ -138,6 +146,11 @@ export default function Edit({
|
|||||||
timeout: mapTimeoutStringToSeconds(values.timeout),
|
timeout: mapTimeoutStringToSeconds(values.timeout),
|
||||||
files: updatedFiles
|
files: updatedFiles
|
||||||
}
|
}
|
||||||
|
if (values?.service?.consumerParameters) {
|
||||||
|
updatedService.consumerParameters = transformConsumerParameters(
|
||||||
|
values.service.consumerParameters
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove version update at a later time
|
// TODO: remove version update at a later time
|
||||||
const updatedAsset: Asset = {
|
const updatedAsset: Asset = {
|
||||||
@ -189,7 +202,7 @@ export default function Edit({
|
|||||||
enableReinitialize
|
enableReinitialize
|
||||||
initialValues={getInitialValues(
|
initialValues={getInitialValues(
|
||||||
asset?.metadata,
|
asset?.metadata,
|
||||||
asset?.services[0]?.timeout,
|
asset?.services[0],
|
||||||
asset?.accessDetails?.price || '0',
|
asset?.accessDetails?.price || '0',
|
||||||
paymentCollector,
|
paymentCollector,
|
||||||
assetState
|
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 { getFileInfo } from '@utils/provider'
|
||||||
import { getFieldContent } from '@utils/form'
|
import { getFieldContent } from '@utils/form'
|
||||||
import { isGoogleUrl } from '@utils/url'
|
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(
|
export function checkIfTimeoutInPredefinedValues(
|
||||||
timeout: string,
|
timeout: string,
|
||||||
@ -130,6 +133,27 @@ export default function FormEditMetadata({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Field {...getFieldContent('tags', data)} component={Input} name="tags" />
|
<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
|
<Field
|
||||||
{...getFieldContent('paymentCollector', data)}
|
{...getFieldContent('paymentCollector', data)}
|
||||||
component={Input}
|
component={Input}
|
||||||
@ -140,6 +164,26 @@ export default function FormEditMetadata({
|
|||||||
component={Input}
|
component={Input}
|
||||||
name="assetState"
|
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 />
|
<FormActions />
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Metadata, ServiceComputeOptions } from '@oceanprotocol/lib'
|
import { Metadata, Service, ServiceComputeOptions } from '@oceanprotocol/lib'
|
||||||
import { secondsToString } from '@utils/ddo'
|
import { parseConsumerParameters, secondsToString } from '@utils/ddo'
|
||||||
import { ComputeEditForm, MetadataEditForm } from './_types'
|
import { ComputeEditForm, MetadataEditForm } from './_types'
|
||||||
|
|
||||||
export function getInitialValues(
|
export function getInitialValues(
|
||||||
metadata: Metadata,
|
metadata: Metadata,
|
||||||
timeout: number,
|
service: Service,
|
||||||
price: string,
|
price: string,
|
||||||
paymentCollector: string,
|
paymentCollector: string,
|
||||||
assetState: string
|
assetState: string
|
||||||
@ -15,11 +15,19 @@ export function getInitialValues(
|
|||||||
price,
|
price,
|
||||||
links: [{ url: '', type: 'url' }],
|
links: [{ url: '', type: 'url' }],
|
||||||
files: [{ url: '', type: 'ipfs' }],
|
files: [{ url: '', type: 'ipfs' }],
|
||||||
timeout: secondsToString(timeout),
|
timeout: secondsToString(service?.timeout),
|
||||||
author: metadata?.author,
|
author: metadata?.author,
|
||||||
tags: metadata?.tags,
|
tags: metadata?.tags,
|
||||||
|
usesConsumerParameters: metadata?.algorithm?.consumerParameters?.length > 0,
|
||||||
|
consumerParameters: parseConsumerParameters(
|
||||||
|
metadata?.algorithm?.consumerParameters
|
||||||
|
),
|
||||||
paymentCollector,
|
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'
|
import { FileInfo } from '@oceanprotocol/lib'
|
||||||
export interface MetadataEditForm {
|
export interface MetadataEditForm {
|
||||||
name: string
|
name: string
|
||||||
@ -9,7 +10,13 @@ export interface MetadataEditForm {
|
|||||||
links?: FileInfo[]
|
links?: FileInfo[]
|
||||||
author?: string
|
author?: string
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
|
usesConsumerParameters?: boolean
|
||||||
|
consumerParameters?: FormConsumerParameter[]
|
||||||
assetState?: string
|
assetState?: string
|
||||||
|
service?: {
|
||||||
|
usesConsumerParameters?: boolean
|
||||||
|
consumerParameters?: FormConsumerParameter[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComputeEditForm {
|
export interface ComputeEditForm {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { FileInfo } from '@oceanprotocol/lib'
|
import { FileInfo } from '@oceanprotocol/lib'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { isAddress } from 'ethers/lib/utils'
|
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({
|
export const validationSchema = Yup.object().shape({
|
||||||
name: Yup.string()
|
name: Yup.string()
|
||||||
@ -37,6 +38,16 @@ export const validationSchema = Yup.object().shape({
|
|||||||
timeout: Yup.string().required('Required'),
|
timeout: Yup.string().required('Required'),
|
||||||
author: Yup.string().nullable(),
|
author: Yup.string().nullable(),
|
||||||
tags: Yup.array<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(
|
paymentCollector: Yup.string().test(
|
||||||
'ValidAddress',
|
'ValidAddress',
|
||||||
'Must be a valid Ethereum Address.',
|
'Must be a valid Ethereum Address.',
|
||||||
@ -44,7 +55,19 @@ export const validationSchema = Yup.object().shape({
|
|||||||
return isAddress(value)
|
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({
|
export const computeSettingsValidationSchema = Yup.object().shape({
|
||||||
|
@ -3,6 +3,7 @@ import Input from '@shared/FormInput'
|
|||||||
import { Field, useField, useFormikContext } from 'formik'
|
import { Field, useField, useFormikContext } from 'formik'
|
||||||
import React, { ReactElement, useEffect } from 'react'
|
import React, { ReactElement, useEffect } from 'react'
|
||||||
import content from '../../../../content/publish/form.json'
|
import content from '../../../../content/publish/form.json'
|
||||||
|
import consumerParametersContent from '../../../../content/publish/consumerParameters.json'
|
||||||
import { FormPublishData } from '../_types'
|
import { FormPublishData } from '../_types'
|
||||||
import IconDataset from '@images/dataset.svg'
|
import IconDataset from '@images/dataset.svg'
|
||||||
import IconAlgorithm from '@images/algorithm.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 IconDownload from '@images/download.svg'
|
||||||
import IconCompute from '@images/compute.svg'
|
import IconCompute from '@images/compute.svg'
|
||||||
import content from '../../../../content/publish/form.json'
|
import content from '../../../../content/publish/form.json'
|
||||||
|
import consumerParametersContent from '../../../../content/publish/consumerParameters.json'
|
||||||
import { getFieldContent } from '@utils/form'
|
import { getFieldContent } from '@utils/form'
|
||||||
import { FormPublishData } from '../_types'
|
import { FormPublishData } from '../_types'
|
||||||
import Alert from '@shared/atoms/Alert'
|
import Alert from '@shared/atoms/Alert'
|
||||||
@ -101,6 +102,21 @@ export default function ServicesFields(): ReactElement {
|
|||||||
component={Input}
|
component={Input}
|
||||||
name="services[0].timeout"
|
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: '',
|
dockerImage: '',
|
||||||
dockerImageCustom: '',
|
dockerImageCustom: '',
|
||||||
dockerImageCustomTag: '',
|
dockerImageCustomTag: '',
|
||||||
dockerImageCustomEntrypoint: ''
|
dockerImageCustomEntrypoint: '',
|
||||||
|
usesConsumerParameters: false,
|
||||||
|
consumerParameters: []
|
||||||
},
|
},
|
||||||
services: [
|
services: [
|
||||||
{
|
{
|
||||||
@ -82,7 +84,9 @@ export const initialValues: FormPublishData = {
|
|||||||
valid: true,
|
valid: true,
|
||||||
custom: false
|
custom: false
|
||||||
},
|
},
|
||||||
computeOptions
|
computeOptions,
|
||||||
|
usesConsumerParameters: false,
|
||||||
|
consumerParameters: []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
pricing: {
|
pricing: {
|
||||||
|
@ -10,6 +10,8 @@ export interface FormPublishService {
|
|||||||
providerUrl: { url: string; valid: boolean; custom: boolean }
|
providerUrl: { url: string; valid: boolean; custom: boolean }
|
||||||
algorithmPrivacy?: boolean
|
algorithmPrivacy?: boolean
|
||||||
computeOptions?: ServiceComputeOptions
|
computeOptions?: ServiceComputeOptions
|
||||||
|
usesConsumerParameters?: boolean
|
||||||
|
consumerParameters?: FormConsumerParameter[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormPublishData {
|
export interface FormPublishData {
|
||||||
@ -32,6 +34,12 @@ export interface FormPublishData {
|
|||||||
dockerImageCustomTag?: string
|
dockerImageCustomTag?: string
|
||||||
dockerImageCustomEntrypoint?: string
|
dockerImageCustomEntrypoint?: string
|
||||||
dockerImageCustomChecksum?: string
|
dockerImageCustomChecksum?: string
|
||||||
|
usesConsumerParameters?: boolean
|
||||||
|
consumerParameters?: FormConsumerParameter[]
|
||||||
|
service?: {
|
||||||
|
usesConsumerParameters?: boolean
|
||||||
|
consumerParameters?: FormConsumerParameter[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
services: FormPublishService[]
|
services: FormPublishService[]
|
||||||
pricing: PricePublishOptions
|
pricing: PricePublishOptions
|
||||||
@ -61,3 +69,14 @@ export interface MetadataAlgorithmContainer {
|
|||||||
tag: string
|
tag: string
|
||||||
checksum: 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,
|
DispenserCreationParams,
|
||||||
getHash,
|
getHash,
|
||||||
LoggerInstance,
|
LoggerInstance,
|
||||||
Metadata,
|
|
||||||
NftCreateData,
|
NftCreateData,
|
||||||
NftFactory,
|
NftFactory,
|
||||||
Service,
|
|
||||||
ZERO_ADDRESS,
|
ZERO_ADDRESS,
|
||||||
getEventFromTx
|
getEventFromTx,
|
||||||
|
ConsumerParameter,
|
||||||
|
Metadata,
|
||||||
|
Service
|
||||||
} from '@oceanprotocol/lib'
|
} from '@oceanprotocol/lib'
|
||||||
import { mapTimeoutStringToSeconds, normalizeFile } from '@utils/ddo'
|
import { mapTimeoutStringToSeconds, normalizeFile } from '@utils/ddo'
|
||||||
import { generateNftCreateData } from '@utils/nft'
|
import { generateNftCreateData } from '@utils/nft'
|
||||||
import { getEncryptedFiles } from '@utils/provider'
|
import { getEncryptedFiles } from '@utils/provider'
|
||||||
import slugify from 'slugify'
|
import slugify from 'slugify'
|
||||||
import { algorithmContainerPresets } from './_constants'
|
import { algorithmContainerPresets } from './_constants'
|
||||||
import { FormPublishData, MetadataAlgorithmContainer } from './_types'
|
import {
|
||||||
|
FormConsumerParameter,
|
||||||
|
FormPublishData,
|
||||||
|
MetadataAlgorithmContainer
|
||||||
|
} from './_types'
|
||||||
import {
|
import {
|
||||||
marketFeeAddress,
|
marketFeeAddress,
|
||||||
publisherMarketOrderFee,
|
publisherMarketOrderFee,
|
||||||
@ -59,6 +64,33 @@ function transformTags(originalTags: string[]): string[] {
|
|||||||
return transformedTags
|
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(
|
export async function transformPublishFormToDdo(
|
||||||
values: FormPublishData,
|
values: FormPublishData,
|
||||||
// Those 2 are only passed during actual publishing process
|
// Those 2 are only passed during actual publishing process
|
||||||
@ -79,7 +111,9 @@ export async function transformPublishFormToDdo(
|
|||||||
dockerImageCustom,
|
dockerImageCustom,
|
||||||
dockerImageCustomTag,
|
dockerImageCustomTag,
|
||||||
dockerImageCustomEntrypoint,
|
dockerImageCustomEntrypoint,
|
||||||
dockerImageCustomChecksum
|
dockerImageCustomChecksum,
|
||||||
|
usesConsumerParameters,
|
||||||
|
consumerParameters
|
||||||
} = metadata
|
} = metadata
|
||||||
const { access, files, links, providerUrl, timeout } = services[0]
|
const { access, files, links, providerUrl, timeout } = services[0]
|
||||||
|
|
||||||
@ -98,6 +132,10 @@ export async function transformPublishFormToDdo(
|
|||||||
const linksTransformed = links?.length &&
|
const linksTransformed = links?.length &&
|
||||||
links[0].valid && [sanitizeUrl(links[0].url)]
|
links[0].valid && [sanitizeUrl(links[0].url)]
|
||||||
|
|
||||||
|
const consumerParametersTransformed = usesConsumerParameters
|
||||||
|
? transformConsumerParameters(consumerParameters)
|
||||||
|
: undefined
|
||||||
|
|
||||||
const newMetadata: Metadata = {
|
const newMetadata: Metadata = {
|
||||||
created: currentTime,
|
created: currentTime,
|
||||||
updated: currentTime,
|
updated: currentTime,
|
||||||
@ -135,7 +173,8 @@ export async function transformPublishFormToDdo(
|
|||||||
dockerImage === 'custom'
|
dockerImage === 'custom'
|
||||||
? dockerImageCustomChecksum
|
? dockerImageCustomChecksum
|
||||||
: algorithmContainerPresets.checksum
|
: algorithmContainerPresets.checksum
|
||||||
}
|
},
|
||||||
|
consumerParameters: consumerParametersTransformed
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -161,7 +200,10 @@ export async function transformPublishFormToDdo(
|
|||||||
timeout: mapTimeoutStringToSeconds(timeout),
|
timeout: mapTimeoutStringToSeconds(timeout),
|
||||||
...(access === 'compute' && {
|
...(access === 'compute' && {
|
||||||
compute: values.services[0].computeOptions
|
compute: values.services[0].computeOptions
|
||||||
})
|
}),
|
||||||
|
consumerParameters: values.services[0].usesConsumerParameters
|
||||||
|
? transformConsumerParameters(values.services[0].consumerParameters)
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDdo: DDO = {
|
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 { 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
|
// TODO: conditional validation
|
||||||
// e.g. when algo is selected, Docker image is required
|
// e.g. when algo is selected, Docker image is required
|
||||||
@ -26,7 +27,17 @@ const validationMetadata = {
|
|||||||
tags: Yup.array<string[]>().nullable(),
|
tags: Yup.array<string[]>().nullable(),
|
||||||
termsAndConditions: Yup.boolean()
|
termsAndConditions: Yup.boolean()
|
||||||
.required('Required')
|
.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 = {
|
const validationService = {
|
||||||
@ -60,6 +71,16 @@ const validationService = {
|
|||||||
url: Yup.string().url('Must be a valid URL.').required('Required'),
|
url: Yup.string().url('Must be a valid URL.').required('Required'),
|
||||||
valid: Yup.boolean().isTrue().required('Valid Provider is required.'),
|
valid: Yup.boolean().isTrue().required('Valid Provider is required.'),
|
||||||
custom: Yup.boolean()
|
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