From 18cd6b6f0176e61c3cac2fd042c5da63fedbd77c Mon Sep 17 00:00:00 2001 From: Bogdan Fazakas Date: Tue, 16 Feb 2021 11:27:02 +0200 Subject: [PATCH] WIP publish algorithm --- content/pages/publishAlgo.json | 62 +++++++++++++ src/@types/MetaData.d.ts | 12 +++ src/components/molecules/MetadataPreview.tsx | 36 +++++++- .../organisms/AssetActions/Edit/index.tsx | 2 +- .../pages/Publish/FormAlgoPublish.tsx | 86 ++++++++++++++++++ src/components/pages/Publish/index.tsx | 91 ++++++++++++++++--- src/models/FormAlgoPublish.ts | 32 +++++++ src/pages/publish.tsx | 31 ++++++- src/utils/metadata.ts | 59 +++++++++++- tests/unit/pages/publish.test.tsx | 5 +- 10 files changed, 395 insertions(+), 21 deletions(-) create mode 100644 content/pages/publishAlgo.json create mode 100644 src/components/pages/Publish/FormAlgoPublish.tsx create mode 100644 src/models/FormAlgoPublish.ts diff --git a/content/pages/publishAlgo.json b/content/pages/publishAlgo.json new file mode 100644 index 000000000..4132cd889 --- /dev/null +++ b/content/pages/publishAlgo.json @@ -0,0 +1,62 @@ +{ + "title": "Publish", + "description": "Highlight the important features of your algorith to make it more discoverable and catch the interest of data consumers.", + "warning": "Given the beta status, publishing on Ropsten or Rinkeby first is strongly recommended. Please familiarize yourself with [the market](https://oceanprotocol.com/technology/marketplaces), [the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13), and the [Terms of Use](/terms).", + "form": { + "title": "Publish", + "data": [ + { + "name": "name", + "label": "Title", + "placeholder": "e.g. Shapes of Desert Plants", + "help": "Enter a concise title.", + "required": true + }, + { + "name": "description", + "label": "Description", + "help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics).", + "type": "textarea", + "required": true + }, + { + "name": "files", + "label": "File", + "placeholder": "e.g. https://file.com/file.json", + "help": "Please provide a URL to your algorith file. This URL will be stored encrypted after publishing.", + "type": "files", + "required": true + }, + { + "name": "dockerImage", + "label": "Docker Image", + "placeholder": "e.g. python3.7", + "help": "Please select a predefined image to run your algorithm.", + "type": "select", + "options": ["NodeJS", "Python 3.7"], + "required": true + }, + { + "name": "author", + "label": "Author", + "placeholder": "e.g. Jelly McJellyfish", + "help": "Give proper attribution for your algorith.", + "required": true + }, + { + "name": "tags", + "label": "Tags", + "placeholder": "e.g. logistics, ai", + "help": "Separate tags with comma." + }, + { + "name": "termsAndConditions", + "label": "Terms & Conditions", + "type": "terms", + "options": ["I agree to these Terms and Conditions"], + "required": true + } + ], + "success": "Algorithm Published!" + } +} diff --git a/src/@types/MetaData.d.ts b/src/@types/MetaData.d.ts index 6e3a48dea..80a585a28 100644 --- a/src/@types/MetaData.d.ts +++ b/src/@types/MetaData.d.ts @@ -39,6 +39,18 @@ export interface MetadataPublishForm { links?: string | File[] } +export interface AlgorithmPublishForm { + // ---- required fields ---- + name: string + description: string + files: string | File[] + author: string + dockerImage: string + termsAndConditions: boolean + // ---- optional fields ---- + tags?: string +} + export interface ServiceMetadataMarket extends ServiceMetadata { attributes: MetadataMarket } diff --git a/src/components/molecules/MetadataPreview.tsx b/src/components/molecules/MetadataPreview.tsx index cf448987e..6b72f338f 100644 --- a/src/components/molecules/MetadataPreview.tsx +++ b/src/components/molecules/MetadataPreview.tsx @@ -5,7 +5,10 @@ import Tags from '../atoms/Tags' import MetaItem from '../organisms/AssetContent/MetaItem' import styles from './MetadataPreview.module.css' import File from '../atoms/File' -import { MetadataPublishForm } from '../../@types/MetaData' +import { + MetadataPublishForm, + AlgorithmPublishForm +} from '../../@types/MetaData' import Button from '../atoms/Button' import { transformTags } from '../../utils/metadata' @@ -82,7 +85,7 @@ function Sample({ url }: { url: string }) { ) } -export default function MetadataPreview({ +export function MetadataPreview({ values }: { values: Partial @@ -119,3 +122,32 @@ export default function MetadataPreview({ ) } + +export function MetadataAlgorithmPreview({ + values +}: { + values: Partial +}): ReactElement { + return ( +
+

Preview

+
+ {values.name &&

{values.name}

} + {values.description && } + +
+ {values.files?.length > 0 && typeof values.files !== 'string' && ( + + )} +
+ {values.tags && } +
+ + +
+ ) +} diff --git a/src/components/organisms/AssetActions/Edit/index.tsx b/src/components/organisms/AssetActions/Edit/index.tsx index feb609469..d70a1343b 100644 --- a/src/components/organisms/AssetActions/Edit/index.tsx +++ b/src/components/organisms/AssetActions/Edit/index.tsx @@ -8,7 +8,7 @@ import { } from '../../../../models/FormEditMetadata' import { useAsset } from '../../../../providers/Asset' import { useUserPreferences } from '../../../../providers/UserPreferences' -import MetadataPreview from '../../../molecules/MetadataPreview' +import { MetadataPreview } from '../../../molecules/MetadataPreview' import Debug from './Debug' import Web3Feedback from '../../../molecules/Wallet/Feedback' import FormEditMetadata from './FormEditMetadata' diff --git a/src/components/pages/Publish/FormAlgoPublish.tsx b/src/components/pages/Publish/FormAlgoPublish.tsx new file mode 100644 index 000000000..5307c6ff9 --- /dev/null +++ b/src/components/pages/Publish/FormAlgoPublish.tsx @@ -0,0 +1,86 @@ +import React, { ReactElement, useEffect, FormEvent, ChangeEvent } from 'react' +import styles from './FormPublish.module.css' +import { useOcean } from '@oceanprotocol/react' +import { useFormikContext, Field, Form, FormikContextType } from 'formik' +import Input from '../../atoms/Input' +import Button from '../../atoms/Button' +import { FormContent, FormFieldProps } from '../../../@types/Form' +import { AlgorithmPublishForm } from '../../../@types/MetaData' + +export default function FormPublish({ + content +}: { + content: FormContent +}): ReactElement { + const { ocean, account } = useOcean() + const { + status, + setStatus, + isValid, + setErrors, + setTouched, + resetForm, + initialValues, + validateField, + setFieldValue + }: FormikContextType = useFormikContext() + + // reset form validation on every mount + useEffect(() => { + setErrors({}) + setTouched({}) + resetForm({ values: initialValues, status: 'empty' }) + // setSubmitting(false) + }, [setErrors, setTouched]) + + // Manually handle change events instead of using `handleChange` from Formik. + // Workaround for default `validateOnChange` not kicking in + function handleFieldChange( + e: ChangeEvent, + field: FormFieldProps + ) { + validateField(field.name) + setFieldValue(field.name, e.target.value) + } + + const resetFormAndClearStorage = (e: FormEvent) => { + e.preventDefault() + resetForm({ values: initialValues, status: 'empty' }) + setStatus('empty') + } + + return ( +
status === 'empty' && setStatus(null)} + > + {content.data.map((field: FormFieldProps) => ( + ) => + handleFieldChange(e, field) + } + /> + ))} + +
+ + + {status !== 'empty' && ( + + )} +
+ + ) +} diff --git a/src/components/pages/Publish/index.tsx b/src/components/pages/Publish/index.tsx index 5995acd9b..b78f9fc84 100644 --- a/src/components/pages/Publish/index.tsx +++ b/src/components/pages/Publish/index.tsx @@ -1,18 +1,31 @@ -import React, { ReactElement, useState } from 'react' +import React, { ReactElement, useState, useEffect } from 'react' import { Formik } from 'formik' import { usePublish, useOcean } from '@oceanprotocol/react' import styles from './index.module.css' import FormPublish from './FormPublish' +import FormAlgoPublish from './FormAlgoPublish' import PublishType from './PublishType' import Web3Feedback from '../../molecules/Wallet/Feedback' import { FormContent } from '../../../@types/Form' import { initialValues, validationSchema } from '../../../models/FormPublish' +import { + initialValues as initialValuesAlgorithm, + validationSchema as validationSchemaAlgorithm +} from '../../../models/FormAlgoPublish' + import { transformPublishFormToMetadata, + transformPublishAlgorithmFormToMetadata, mapTimeoutStringToSeconds } from '../../../utils/metadata' -import MetadataPreview from '../../molecules/MetadataPreview' -import { MetadataPublishForm } from '../../../@types/MetaData' +import { + MetadataPreview, + MetadataAlgorithmPreview +} from '../../molecules/MetadataPreview' +import { + MetadataPublishForm, + AlgorithmPublishForm +} from '../../../@types/MetaData' import { useUserPreferences } from '../../../providers/UserPreferences' import { Logger, Metadata } from '@oceanprotocol/lib' import { Persist } from '../../atoms/FormikPersist' @@ -24,20 +37,29 @@ import Button from '../../atoms/Button' const formName = 'ocean-publish-form' export default function PublishPage({ - content + content, + contentAlgoPublish }: { content: { warning: string; form: FormContent } + contentAlgoPublish: { warning: string; form: FormContent } }): ReactElement { const { debug } = useUserPreferences() const { publish, publishError, isLoading, publishStepText } = usePublish() const { isInPurgatory, purgatoryData } = useOcean() const [success, setSuccess] = useState() const [error, setError] = useState() + const [title, setTitle] = useState() const [did, setDid] = useState() const [publishType, setPublishType] = useState() const hasFeedback = isLoading || error || success + useEffect(() => { + publishType === 'data' + ? setTitle('Publishing Data Set') + : setTitle('Publishing Algorithm') + }, [publishType]) + async function handleSubmit( values: Partial, resetForm: () => void @@ -81,16 +103,52 @@ export default function PublishPage({ } } + async function handleAlgorithmSubmit( + values: Partial, + resetForm: () => void + ): Promise { + const metadata = transformPublishAlgorithmFormToMetadata(values) + + try { + Logger.log('Publish Algorithm with ', metadata) + + const ddo = await publish((metadata as unknown) as Metadata, 'access') + + // Publish failed + if (!ddo || publishError) { + setError(publishError || 'Publishing DDO failed.') + Logger.error(publishError || 'Publishing DDO failed.') + return + } + + // Publish succeeded + setDid(ddo.id) + setSuccess( + '🎉 Successfully published. 🎉 Now create a price for your algorithm.' + ) + resetForm() + } catch (error) { + setError(error.message) + Logger.error(error.message) + } + } + return isInPurgatory && purgatoryData ? null : ( { // move user's focus to top of screen window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) // kick off publishing - await handleSubmit(values, resetForm) + publishType === 'data' + ? await handleSubmit(values, resetForm) + : await handleAlgorithmSubmit(values, resetForm) }} > {({ values }) => ( @@ -99,7 +157,7 @@ export default function PublishPage({ {hasFeedback ? ( ) : ( <> - +
- + {publishType === 'data' ? ( + + ) : ( + + )} diff --git a/src/models/FormAlgoPublish.ts b/src/models/FormAlgoPublish.ts new file mode 100644 index 000000000..04c31d53b --- /dev/null +++ b/src/models/FormAlgoPublish.ts @@ -0,0 +1,32 @@ +import { AlgorithmPublishForm } from '../@types/MetaData' +import { File as FileMetadata } from '@oceanprotocol/lib' +import * as Yup from 'yup' + +export const validationSchema: Yup.SchemaOf = Yup.object() + .shape({ + // ---- required fields ---- + name: Yup.string() + .min(4, (param) => `Title must be at least ${param.min} characters`) + .required('Required'), + description: Yup.string().min(10).required('Required'), + files: Yup.array().required('Required').nullable(), + dockerImage: Yup.string() + .matches(/NodeJS|Python 3.7/g, { excludeEmptyString: true }) + .required('Required'), + author: Yup.string().required('Required'), + termsAndConditions: Yup.boolean().required('Required'), + // ---- optional fields ---- + tags: Yup.string().nullable(), + links: Yup.array().nullable() + }) + .defined() + +export const initialValues: Partial = { + name: '', + author: '', + dockerImage: '', + files: '', + description: '', + termsAndConditions: false, + tags: '' +} diff --git a/src/pages/publish.tsx b/src/pages/publish.tsx index 97a4c358e..365cf0ea5 100644 --- a/src/pages/publish.tsx +++ b/src/pages/publish.tsx @@ -5,11 +5,13 @@ import { graphql, PageProps } from 'gatsby' export default function PageGatsbyPublish(props: PageProps): ReactElement { const content = (props.data as any).content.edges[0].node.childPagesJson + const contentAlgoPublish = (props.data as any).contentAlgoPublish.edges[0] + .node.childPagesJson const { title, description } = content return ( - + ) } @@ -41,5 +43,32 @@ export const contentQuery = graphql` } } } + contentAlgoPublish: allFile( + filter: { relativePath: { eq: "pages/publishAlgo.json" } } + ) { + edges { + node { + childPagesJson { + title + description + warning + form { + title + data { + name + placeholder + label + help + type + required + sortOptions + options + } + success + } + } + } + } + } } ` diff --git a/src/utils/metadata.ts b/src/utils/metadata.ts index 23bf00406..58251e8a3 100644 --- a/src/utils/metadata.ts +++ b/src/utils/metadata.ts @@ -1,8 +1,12 @@ -import { MetadataMarket, MetadataPublishForm } from '../@types/MetaData' +import { + MetadataMarket, + MetadataPublishForm, + AlgorithmPublishForm +} from '../@types/MetaData' import { toStringNoMS } from '.' import AssetModel from '../models/Asset' import slugify from '@sindresorhus/slugify' -import { DDO } from '@oceanprotocol/lib' +import { DDO, MetadataAlgorithm } from '@oceanprotocol/lib' export function transformTags(value: string): string[] { const originalTags = value?.split(',') @@ -66,6 +70,21 @@ export function checkIfTimeoutInPredefinedValues( return false } +function getAlgoithComponent(selectedAlgorithm: string): MetadataAlgorithm { + return { + language: selectedAlgorithm === 'NodeJS' ? 'js' : 'py', + format: 'docker-image', + version: '0.1', + container: { + entrypoint: + selectedAlgorithm === 'NodeJS' ? 'node $ALGO' : 'python $ALGO', + image: + selectedAlgorithm === 'NodeJS' ? 'node' : 'oceanprotocol/algo_dockers', + tag: selectedAlgorithm === 'NodeJS' ? '10' : 'python-panda' + } + } +} + export function transformPublishFormToMetadata( { name, @@ -100,3 +119,39 @@ export function transformPublishFormToMetadata( return metadata } + +export function transformPublishAlgorithmFormToMetadata( + { + name, + author, + description, + tags, + dockerImage, + termsAndConditions, + files + }: Partial, + ddo?: DDO +): MetadataMarket { + const currentTime = toStringNoMS(new Date()) + const algorithm = getAlgoithComponent(dockerImage) + const metadata: MetadataMarket = { + main: { + ...AssetModel.main, + name, + type: 'algorithm', + author, + dateCreated: ddo ? ddo.created : currentTime, + files: typeof files !== 'string' && files, + license: 'https://market.oceanprotocol.com/terms', + algorithm: algorithm + }, + additionalInformation: { + ...AssetModel.additionalInformation, + description, + tags: transformTags(tags), + termsAndConditions + } + } + + return metadata +} diff --git a/tests/unit/pages/publish.test.tsx b/tests/unit/pages/publish.test.tsx index b2fcd1ee2..9accdb737 100644 --- a/tests/unit/pages/publish.test.tsx +++ b/tests/unit/pages/publish.test.tsx @@ -2,10 +2,13 @@ import React from 'react' import { render } from '@testing-library/react' import Publish from '../../../src/components/pages/Publish' import content from '../../../content/pages/publish.json' +import contentAlgo from '../../../content/pages/publishAlgo.json' describe('Home', () => { it('renders without crashing', () => { - const { container } = render() + const { container } = render( + + ) expect(container.firstChild).toBeInTheDocument() }) })