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

WIP publish algorithm

This commit is contained in:
Bogdan Fazakas 2021-02-16 11:27:02 +02:00
parent e4a826cc7e
commit 18cd6b6f01
10 changed files with 395 additions and 21 deletions

View File

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

View File

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

View File

@ -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<MetadataPublishForm>
@ -119,3 +122,32 @@ export default function MetadataPreview({
</div>
)
}
export function MetadataAlgorithmPreview({
values
}: {
values: Partial<AlgorithmPublishForm>
}): ReactElement {
return (
<div className={styles.preview}>
<h2 className={styles.previewTitle}>Preview</h2>
<header>
{values.name && <h3 className={styles.title}>{values.name}</h3>}
{values.description && <Description description={values.description} />}
<div className={styles.asset}>
{values.files?.length > 0 && typeof values.files !== 'string' && (
<File
file={values.files[0] as FileMetadata}
className={styles.file}
small
/>
)}
</div>
{values.tags && <Tags items={transformTags(values.tags)} />}
</header>
<MetaFull values={values} />
</div>
)
}

View File

@ -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'

View File

@ -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<AlgorithmPublishForm> = 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<HTMLInputElement>,
field: FormFieldProps
) {
validateField(field.name)
setFieldValue(field.name, e.target.value)
}
const resetFormAndClearStorage = (e: FormEvent<Element>) => {
e.preventDefault()
resetForm({ values: initialValues, status: 'empty' })
setStatus('empty')
}
return (
<Form
className={styles.form}
// do we need this?
onChange={() => status === 'empty' && setStatus(null)}
>
{content.data.map((field: FormFieldProps) => (
<Field
key={field.name}
{...field}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
))}
<footer className={styles.actions}>
<Button
style="primary"
type="submit"
disabled={!ocean || !account || !isValid || status === 'empty'}
>
Submit
</Button>
{status !== 'empty' && (
<Button style="text" size="small" onClick={resetFormAndClearStorage}>
Reset Form
</Button>
)}
</footer>
</Form>
)
}

View File

@ -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<string>()
const [error, setError] = useState<string>()
const [title, setTitle] = useState<string>()
const [did, setDid] = useState<string>()
const [publishType, setPublishType] = useState<string>()
const hasFeedback = isLoading || error || success
useEffect(() => {
publishType === 'data'
? setTitle('Publishing Data Set')
: setTitle('Publishing Algorithm')
}, [publishType])
async function handleSubmit(
values: Partial<MetadataPublishForm>,
resetForm: () => void
@ -81,16 +103,52 @@ export default function PublishPage({
}
}
async function handleAlgorithmSubmit(
values: Partial<AlgorithmPublishForm>,
resetForm: () => void
): Promise<void> {
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 : (
<Formik
initialValues={initialValues}
initialValues={
publishType === 'data' ? initialValues : initialValuesAlgorithm
}
initialStatus="empty"
validationSchema={validationSchema}
validationSchema={
publishType === 'data' ? validationSchema : validationSchemaAlgorithm
}
onSubmit={async (values, { resetForm }) => {
// 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 ? (
<MetadataFeedback
title="Publishing Data Set"
title={title}
error={error}
success={success}
loading={publishStepText}
@ -111,21 +169,26 @@ export default function PublishPage({
/>
) : (
<>
<PublishType
type={publishType}
setType={setPublishType}
></PublishType>
<PublishType type={publishType} setType={setPublishType} />
<Alert
text={content.warning}
state="info"
className={styles.alert}
/>
<article className={styles.grid}>
{publishType === 'data' ? (
<FormPublish content={content.form} />
) : (
<FormAlgoPublish content={contentAlgoPublish.form} />
)}
<aside>
<div className={styles.sticky}>
{publishType === 'data' ? (
<MetadataPreview values={values} />
) : (
<MetadataAlgorithmPreview values={values} />
)}
<Web3Feedback />
</div>
</aside>

View File

@ -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<AlgorithmPublishForm> = 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<FileMetadata>().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<FileMetadata[]>().nullable()
})
.defined()
export const initialValues: Partial<AlgorithmPublishForm> = {
name: '',
author: '',
dockerImage: '',
files: '',
description: '',
termsAndConditions: false,
tags: ''
}

View File

@ -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 (
<Page title={title} description={description} uri={props.uri}>
<PagePublish content={content} />
<PagePublish content={content} contentAlgoPublish={contentAlgoPublish} />
</Page>
)
}
@ -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
}
}
}
}
}
}
`

View File

@ -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<AlgorithmPublishForm>,
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
}

View File

@ -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(<Publish content={content} />)
const { container } = render(
<Publish content={content} contentAlgoPublish={contentAlgo} />
)
expect(container.firstChild).toBeInTheDocument()
})
})