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

Merge pull request #384 from oceanprotocol/publish-algo

Compute: publish algorithm
This commit is contained in:
Bogdan Fazakas 2021-03-05 13:49:08 +02:00 committed by GitHub
commit e74d848397
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 884 additions and 276 deletions

View File

@ -1,84 +0,0 @@
{
"title": "Publish",
"description": "Highlight the important features of your data set 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 data set file. This URL will be stored encrypted after publishing.",
"type": "files",
"required": true
},
{
"name": "links",
"label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json",
"help": "Please provide a URL to a sample of your data set file. This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing.",
"type": "files"
},
{
"name": "access",
"label": "Access Type",
"help": "Choose how you want your files to be accessible for the specified price.",
"type": "select",
"options": ["Download"],
"required": true
},
{
"name": "timeout",
"label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
"type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false,
"required": true
},
{
"name": "dataTokenOptions",
"label": "Datatoken Name & Symbol",
"type": "datatoken",
"help": "The datatoken for this data set will be created with this name & symbol.",
"required": true
},
{
"name": "author",
"label": "Author",
"placeholder": "e.g. Jelly McJellyfish",
"help": "Give proper attribution for your data set.",
"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": "Asset Created!"
}
}

View File

@ -0,0 +1,85 @@
{
"title": "Publish an Algorithm",
"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 image to run your algorithm.",
"type": "select",
"options": ["node:pre-defined", "python:pre-defined", "custom image"],
"required": true
},
{
"name": "image",
"label": "Image URL",
"placeholder": "e.g. oceanprotocol/algo_dockers or https://example.com/image_path",
"help": "Provide the name of a public docker image or the full url if you have it hosted in a 3rd party repo",
"required": false
},
{
"name": "containerTag",
"label": "Docker Image Tag",
"placeholder": "e.g. latest",
"help": "Provide the tag for your docker image.",
"required": false
},
{
"name": "entrypoint",
"label": "Entrypoint",
"placeholder": "e.g. python $ALGO",
"help": "Provide the entrypoint for your algorithm.",
"required": false
},
{
"name": "algorithmPrivacy",
"label": "Algorithm Privacy",
"type": "checkbox",
"options": ["Keep my algorithm private"],
"required": false
},
{
"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

@ -0,0 +1,79 @@
{
"title": "Publish a Data Set",
"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 data set file. This URL will be stored encrypted after publishing.",
"type": "files",
"required": true
},
{
"name": "links",
"label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json",
"help": "Please provide a URL to a sample of your data set file. This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing.",
"type": "files"
},
{
"name": "access",
"label": "Access Type",
"help": "Choose how you want your files to be accessible for the specified price.",
"type": "select",
"options": ["Download"],
"required": true
},
{
"name": "timeout",
"label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
"type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false,
"required": true
},
{
"name": "dataTokenOptions",
"label": "Datatoken Name & Symbol",
"type": "datatoken",
"help": "The datatoken for this data set will be created with this name & symbol.",
"required": true
},
{
"name": "author",
"label": "Author",
"placeholder": "e.g. Jelly McJellyfish",
"help": "Give proper attribution for your data set.",
"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": "Asset Created!"
}

View File

@ -0,0 +1,5 @@
{
"title": "Publish",
"description": "Highlight the important features of your data set or algorithm 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)."
}

View File

@ -5,6 +5,7 @@ export interface FormFieldProps {
options?: string[]
sortOptions?: boolean
required?: boolean
disabled?: boolean
help?: string
placeholder?: string
pattern?: string

View File

@ -24,7 +24,7 @@ export interface PriceOptionsMarket extends PriceOptions {
swapFee: number
}
export interface MetadataPublishForm {
export interface MetadataPublishFormDataset {
// ---- required fields ----
name: string
description: string
@ -39,6 +39,22 @@ export interface MetadataPublishForm {
links?: string | File[]
}
export interface MetadataPublishFormAlgorithm {
// ---- required fields ----
name: string
description: string
files: string | File[]
author: string
dockerImage: string
algorithmPrivacy: boolean
termsAndConditions: boolean
// ---- optional fields ----
image: string
containerTag: string
entrypoint: string
tags?: string
}
export interface ServiceMetadataMarket extends ServiceMetadata {
attributes: MetadataMarket
}

View File

@ -13,6 +13,7 @@
text-transform: uppercase;
cursor: pointer;
color: var(--color-secondary);
background-color: var(--background-body);
border: 1px solid var(--border-color);
margin-right: -1px;
min-width: 100px;
@ -29,7 +30,7 @@
}
.tab[aria-selected='true'] {
background: var(--font-color-heading);
background-color: var(--font-color-heading);
color: var(--background-body);
border-color: var(--font-color-heading);
}

View File

@ -14,5 +14,5 @@
}
.info {
width: .85rem
}
width: 0.85rem;
}

View File

@ -1,5 +1,6 @@
.preview {
font-size: var(--font-size-small);
margin-top: calc(var(--spacer) / 2);
margin-bottom: var(--spacer);
}
@ -10,7 +11,14 @@
.metaFull {
display: grid;
gap: var(--spacer);
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
}
.metaAlgorithm {
display: grid;
gap: var(--spacer);
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
margin-bottom: var(--spacer);
}
.previewTitle {

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 {
MetadataPublishFormDataset,
MetadataPublishFormAlgorithm
} from '../../@types/MetaData'
import Button from '../atoms/Button'
import { transformTags } from '../../utils/metadata'
@ -42,7 +45,7 @@ function Description({ description }: { description: string }) {
)
}
function MetaFull({ values }: { values: Partial<MetadataPublishForm> }) {
function MetaFull({ values }: { values: Partial<MetadataPublishFormDataset> }) {
return (
<div className={styles.metaFull}>
{Object.entries(values)
@ -56,6 +59,8 @@ function MetaFull({ values }: { values: Partial<MetadataPublishForm> }) {
key.includes('links') ||
key.includes('termsAndConditions') ||
key.includes('dataTokenOptions') ||
key.includes('dockerImage') ||
key.includes('algorithmPrivacy') ||
value === undefined ||
value === ''
)
@ -82,10 +87,10 @@ function Sample({ url }: { url: string }) {
)
}
export default function MetadataPreview({
export function MetadataPreview({
values
}: {
values: Partial<MetadataPublishForm>
values: Partial<MetadataPublishFormDataset>
}): ReactElement {
return (
<div className={styles.preview}>
@ -119,3 +124,47 @@ export default function MetadataPreview({
</div>
)
}
export function MetadataAlgorithmPreview({
values
}: {
values: Partial<MetadataPublishFormAlgorithm>
}): 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>
<div className={styles.metaAlgorithm}>
{values.dockerImage && (
<MetaItem
key="dockerImage"
title="Docker Image"
content={values.dockerImage}
/>
)}
{values.algorithmPrivacy && (
<MetaItem
key="privateAlgorithm"
title="Private Algorithm"
content="Yes"
/>
)}
</div>
<MetaFull values={values} />
</div>
)
}

View File

@ -1,6 +1,6 @@
import { DDO } from '@oceanprotocol/lib'
import React, { ReactElement } from 'react'
import { MetadataPublishForm } from '../../../../@types/MetaData'
import { MetadataPublishFormDataset } from '../../../../@types/MetaData'
import { transformPublishFormToMetadata } from '../../../../utils/metadata'
import DebugOutput from '../../../atoms/DebugOutput'
@ -8,7 +8,7 @@ export default function Debug({
values,
ddo
}: {
values: Partial<MetadataPublishForm>
values: Partial<MetadataPublishFormDataset>
ddo: DDO
}): ReactElement {
const newDdo = {

View File

@ -5,12 +5,12 @@ import Button from '../../../atoms/Button'
import Input from '../../../atoms/Input'
import { useOcean } from '@oceanprotocol/react'
import { FormFieldProps } from '../../../../@types/Form'
import { MetadataPublishForm } from '../../../../@types/MetaData'
import { MetadataPublishFormDataset } from '../../../../@types/MetaData'
import { checkIfTimeoutInPredefinedValues } from '../../../../utils/metadata'
function handleTimeoutCustomOption(
data: FormFieldProps[],
values: Partial<MetadataPublishForm>
values: Partial<MetadataPublishFormDataset>
) {
const timeoutFieldContent = data.filter(
(field) => field.name === 'timeout'
@ -51,14 +51,14 @@ export default function FormEditMetadata({
data: FormFieldProps[]
setShowEdit: (show: boolean) => void
setTimeoutStringValue: (value: string) => void
values: Partial<MetadataPublishForm>
values: Partial<MetadataPublishFormDataset>
}): ReactElement {
const { ocean, accountId } = useOcean()
const {
isValid,
validateField,
setFieldValue
}: FormikContextType<Partial<MetadataPublishForm>> = useFormikContext()
}: FormikContextType<Partial<MetadataPublishFormDataset>> = useFormikContext()
// Manually handle change events instead of using `handleChange` from Formik.
// Workaround for default `validateOnChange` not kicking in

View File

@ -1,14 +1,14 @@
import { useOcean } from '@oceanprotocol/react'
import { Formik } from 'formik'
import React, { ReactElement, useState } from 'react'
import { MetadataPublishForm } from '../../../../@types/MetaData'
import { MetadataPublishFormDataset } from '../../../../@types/MetaData'
import {
validationSchema,
getInitialValues
} 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'
@ -66,7 +66,7 @@ export default function Edit({
const hasFeedback = error || success
async function handleSubmit(
values: Partial<MetadataPublishForm>,
values: Partial<MetadataPublishFormDataset>,
resetForm: () => void
) {
try {

View File

@ -3,7 +3,6 @@ import styles from './index.module.css'
import Compute from './Compute'
import Consume from './Consume'
import { Logger } from '@oceanprotocol/lib'
import { ConfigHelperConfig } from '@oceanprotocol/lib/dist/node/utils/ConfigHelper'
import Tabs from '../../atoms/Tabs'
import { useOcean } from '@oceanprotocol/react'
import compareAsBN from '../../../utils/compareAsBN'

View File

@ -2,7 +2,7 @@
margin-top: var(--spacer);
display: grid;
gap: var(--spacer);
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
}
.metaFull code {

View File

@ -10,3 +10,8 @@
color: var(--color-secondary);
text-transform: uppercase;
}
.content {
word-wrap: break-word;
white-space: normal;
}

View File

@ -11,7 +11,7 @@ export default function MetaItem({
return (
<div className={styles.metaItem}>
<h3 className={styles.title}>{title}</h3>
{content}
<div className={styles.content}>{content}</div>
</div>
)
}

View File

@ -1,5 +1,5 @@
import React, { ReactElement } from 'react'
import { MetadataPublishForm } from '../../../@types/MetaData'
import { MetadataPublishFormDataset } from '../../../@types/MetaData'
import DebugOutput from '../../atoms/DebugOutput'
import styles from './index.module.css'
import { transformPublishFormToMetadata } from '../../../utils/metadata'
@ -7,7 +7,7 @@ import { transformPublishFormToMetadata } from '../../../utils/metadata'
export default function Debug({
values
}: {
values: Partial<MetadataPublishForm>
values: Partial<MetadataPublishFormDataset>
}): ReactElement {
const ddo = {
'@context': 'https://w3id.org/did/v1',

View File

@ -0,0 +1,162 @@
import React, {
ReactElement,
useEffect,
useState,
FormEvent,
ChangeEvent
} from 'react'
import { useStaticQuery, graphql } from 'gatsby'
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 { MetadataPublishFormAlgorithm } from '../../../@types/MetaData'
import { initialValues as initialValuesAlgorithm } from '../../../models/FormAlgoPublish'
import stylesIndex from './index.module.css'
const query = graphql`
query {
content: allFile(
filter: { relativePath: { eq: "pages/publish/form-algorithm.json" } }
) {
edges {
node {
childPublishJson {
title
data {
name
placeholder
label
help
type
required
sortOptions
options
}
warning
}
}
}
}
}
`
export default function FormPublish(): ReactElement {
const data = useStaticQuery(query)
const content: FormContent = data.content.edges[0].node.childPublishJson
const { ocean, account } = useOcean()
const {
status,
setStatus,
isValid,
setErrors,
setTouched,
resetForm,
initialValues,
validateField,
setFieldValue
}: FormikContextType<MetadataPublishFormAlgorithm> = useFormikContext()
const [selectedDockerImage, setSelectedDockerImage] = useState<string>(
initialValues.dockerImage
)
// reset form validation on every mount
useEffect(() => {
setErrors({})
setTouched({})
// setSubmitting(false)
}, [setErrors, setTouched])
function handleImageSelectChange(imageSelected: string) {
switch (imageSelected) {
case 'node:pre-defined': {
setFieldValue('image', 'node')
setFieldValue('containerTag', '10')
setFieldValue('entrypoint', 'node $ALGO')
break
}
case 'python:pre-defined': {
setFieldValue('image', 'oceanprotocol/algo_dockers')
setFieldValue('containerTag', 'python-panda')
setFieldValue('entrypoint', 'python $ALGO')
break
}
default: {
setFieldValue('image', '')
setFieldValue('containerTag', '')
setFieldValue('entrypoint', '')
break
}
}
}
// Manually handle change events instead of using `handleChange` from Formik.
// Workaround for default `validateOnChange` not kicking in
function handleFieldChange(
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps
) {
const value =
field.type === 'checkbox' ? !JSON.parse(e.target.value) : e.target.value
if (field.name === 'dockerImage') {
setSelectedDockerImage(e.target.value)
handleImageSelectChange(e.target.value)
}
validateField(field.name)
setFieldValue(field.name, value)
}
const resetFormAndClearStorage = (e: FormEvent<Element>) => {
e.preventDefault()
resetForm({
values: initialValuesAlgorithm as MetadataPublishFormAlgorithm,
status: 'empty'
})
setStatus('empty')
}
return (
<Form
className={styles.form}
// do we need this?
onChange={() => status === 'empty' && setStatus(null)}
>
<h2 className={stylesIndex.formTitle}>{content.title}</h2>
{content.data.map(
(field: FormFieldProps) =>
((field.name !== 'entrypoint' &&
field.name !== 'image' &&
field.name !== 'containerTag') ||
selectedDockerImage === 'custom image') && (
<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,6 +1,9 @@
.form {
composes: box from '../../atoms/Box.module.css';
margin-bottom: var(--spacer);
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.actions {

View File

@ -1,17 +1,45 @@
import React, { ReactElement, useEffect, FormEvent, ChangeEvent } from 'react'
import { useStaticQuery, graphql } from 'gatsby'
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 { MetadataPublishForm } from '../../../@types/MetaData'
import { MetadataPublishFormDataset } from '../../../@types/MetaData'
import { initialValues as initialValuesDataset } from '../../../models/FormAlgoPublish'
import stylesIndex from './index.module.css'
export default function FormPublish({
content
}: {
content: FormContent
}): ReactElement {
const query = graphql`
query {
content: allFile(
filter: { relativePath: { eq: "pages/publish/form-dataset.json" } }
) {
edges {
node {
childPublishJson {
title
data {
name
placeholder
label
help
type
required
sortOptions
options
}
warning
}
}
}
}
}
`
export default function FormPublish(): ReactElement {
const data = useStaticQuery(query)
const content: FormContent = data.content.edges[0].node.childPublishJson
const { ocean, account } = useOcean()
const {
status,
@ -23,7 +51,7 @@ export default function FormPublish({
initialValues,
validateField,
setFieldValue
}: FormikContextType<MetadataPublishForm> = useFormikContext()
}: FormikContextType<MetadataPublishFormDataset> = useFormikContext()
// reset form validation on every mount
useEffect(() => {
@ -45,7 +73,10 @@ export default function FormPublish({
const resetFormAndClearStorage = (e: FormEvent<Element>) => {
e.preventDefault()
resetForm({ values: initialValues, status: 'empty' })
resetForm({
values: initialValuesDataset as MetadataPublishFormDataset,
status: 'empty'
})
setStatus('empty')
}
@ -55,6 +86,7 @@ export default function FormPublish({
// do we need this?
onChange={() => status === 'empty' && setStatus(null)}
>
<h2 className={stylesIndex.formTitle}>{content.title}</h2>
{content.data.map((field: FormFieldProps) => (
<Field
key={field.name}

View File

@ -1,29 +0,0 @@
.tabElement,
button.tabElement,
.tabElement:hover,
.tabElement:active,
.tabElement:focus {
border: 1px solid var(--border-color);
text-transform: uppercase;
border-radius: var(--border-radius);
margin-right: calc(var(--spacer) / 6);
margin-bottom: calc(var(--spacer) / 6);
color: var(--color-secondary);
background: var(--background-body);
/* the only thing not possible to overwrite button style="text" with more specifity of selectors, so sledgehammer */
padding: calc(var(--spacer) / 5) !important;
}
.tabElement:hover,
.tabElement:focus {
color: var(--font-color-text);
background: inherit;
transform: none;
}
.tabElement.selected {
color: var(--background-body);
background: var(--font-color-text);
border-color: var(--background-body);
}

View File

@ -1,47 +0,0 @@
import React, { ReactElement, useEffect } from 'react'
import styles from './PublishType.module.css'
import classNames from 'classnames/bind'
import Button from '../../atoms/Button'
const cx = classNames.bind(styles)
const publishTypes = [
{ display: 'data', value: 'data' },
{ display: 'algorithms', value: 'algorithms' }
]
export default function PublishType({
type,
setType
}: {
type: string
setType: React.Dispatch<React.SetStateAction<string>>
}): ReactElement {
useEffect(() => {
setType(publishTypes[0].value)
}, [])
return (
<div>
{publishTypes.map((e, index) => {
const tabElement = cx({
[styles.selected]: e.value === type,
[styles.tabElement]: true
})
return (
<Button
size="small"
style="text"
key={index}
className={tabElement}
onClick={async () => {
setType(e.value)
}}
>
{e.display}
</Button>
)
})}
</div>
)
}

View File

@ -1,3 +1,16 @@
.tabs ul[class*='tabList'] {
background-color: var(--background-content);
border: 1px solid var(--border-color);
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
.tabs div[class*='tabContent'] {
padding-left: 0;
padding-right: 0;
padding-top: 0;
}
.grid {
display: grid;
gap: calc(var(--spacer) * 1.5);
@ -16,8 +29,17 @@ div.alert {
grid-template-columns: 1.618fr 1fr;
}
.tabs ul[class*='tabList'] {
/* fake the above 1.618fr column */
max-width: calc((100% / 1.618) - calc(var(--spacer) / 1.075));
}
.sticky {
position: sticky;
top: calc(var(--spacer) / 2);
}
}
.formTitle {
font-size: var(--font-size-h4);
}

View File

@ -1,46 +1,111 @@
import React, { ReactElement, useState } from 'react'
import { Formik } from 'formik'
import React, { ReactElement, useState, useEffect } from 'react'
import { Formik, FormikState } from 'formik'
import { usePublish, useOcean } from '@oceanprotocol/react'
import styles from './index.module.css'
import FormPublish from './FormPublish'
import PublishType from './PublishType'
import FormAlgoPublish from './FormAlgoPublish'
import Web3Feedback from '../../molecules/Wallet/Feedback'
import { FormContent } from '../../../@types/Form'
import Tabs from '../../atoms/Tabs'
import { initialValues, validationSchema } from '../../../models/FormPublish'
import {
initialValues as initialValuesAlgorithm,
validationSchema as validationSchemaAlgorithm
} from '../../../models/FormAlgoPublish'
import {
transformPublishFormToMetadata,
mapTimeoutStringToSeconds
transformPublishAlgorithmFormToMetadata,
mapTimeoutStringToSeconds,
validateDockerImage
} from '../../../utils/metadata'
import MetadataPreview from '../../molecules/MetadataPreview'
import { MetadataPublishForm } from '../../../@types/MetaData'
import {
MetadataPreview,
MetadataAlgorithmPreview
} from '../../molecules/MetadataPreview'
import {
MetadataPublishFormDataset,
MetadataPublishFormAlgorithm
} from '../../../@types/MetaData'
import { useUserPreferences } from '../../../providers/UserPreferences'
import { Logger, Metadata } from '@oceanprotocol/lib'
import { Logger, Metadata, MetadataMain } from '@oceanprotocol/lib'
import { Persist } from '../../atoms/FormikPersist'
import Debug from './Debug'
import Alert from '../../atoms/Alert'
import MetadataFeedback from '../../molecules/MetadataFeedback'
import Button from '../../atoms/Button'
const formName = 'ocean-publish-form'
const formNameDatasets = 'ocean-publish-form-datasets'
const formNameAlgorithms = 'ocean-publish-form-algorithms'
function TabContent({
publishType,
values
}: {
publishType: MetadataMain['type']
values: Partial<MetadataPublishFormAlgorithm | MetadataPublishFormDataset>
}) {
return (
<article className={styles.grid}>
{publishType === 'dataset' ? <FormPublish /> : <FormAlgoPublish />}
<aside>
<div className={styles.sticky}>
{publishType === 'dataset' ? (
<MetadataPreview values={values} />
) : (
<MetadataAlgorithmPreview values={values} />
)}
<Web3Feedback />
</div>
</aside>
</article>
)
}
export default function PublishPage({
content
}: {
content: { warning: string; form: FormContent }
content: { warning: string }
}): 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 [algoInitialValues, setAlgoInitialValues] = useState<
Partial<MetadataPublishFormAlgorithm>
>(
(localStorage.getItem('ocean-publish-form-algorithms') &&
(JSON.parse(localStorage.getItem('ocean-publish-form-algorithms'))
.initialValues as MetadataPublishFormAlgorithm)) ||
initialValuesAlgorithm
)
const [datasetInitialValues, setdatasetInitialValues] = useState<
Partial<MetadataPublishFormDataset>
>(
(localStorage.getItem('ocean-publish-form-datasets') &&
(JSON.parse(localStorage.getItem('ocean-publish-form-datasets'))
.initialValues as MetadataPublishFormDataset)) ||
initialValues
)
const [publishType, setPublishType] = useState<MetadataMain['type']>(
'dataset'
)
const hasFeedback = isLoading || error || success
useEffect(() => {
publishType === 'dataset'
? setTitle('Publishing Data Set')
: setTitle('Publishing Algorithm')
}, [publishType])
async function handleSubmit(
values: Partial<MetadataPublishForm>,
resetForm: () => void
values: Partial<MetadataPublishFormDataset>,
resetForm: (
nextState?: Partial<FormikState<Partial<MetadataPublishFormDataset>>>
) => void
): Promise<void> {
const metadata = transformPublishFormToMetadata(values)
const timeout = mapTimeoutStringToSeconds(values.timeout)
@ -74,7 +139,53 @@ export default function PublishPage({
setSuccess(
'🎉 Successfully published. 🎉 Now create a price on your data set.'
)
resetForm()
resetForm({
values: initialValues as MetadataPublishFormDataset,
status: 'empty'
})
} catch (error) {
setError(error.message)
Logger.error(error.message)
}
}
async function handleAlgorithmSubmit(
values: Partial<MetadataPublishFormAlgorithm>,
resetForm: (
nextState?: Partial<FormikState<Partial<MetadataPublishFormAlgorithm>>>
) => void
): Promise<void> {
const metadata = transformPublishAlgorithmFormToMetadata(values)
const validDockerImage =
values.dockerImage === 'custom image'
? await validateDockerImage(values.image, values.containerTag)
: true
try {
if (validDockerImage) {
Logger.log('Publish Algorithm with ', metadata)
const ddo = await publish(
(metadata as unknown) as Metadata,
values.algorithmPrivacy === true ? 'compute' : '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({
values: initialValuesAlgorithm as MetadataPublishFormAlgorithm,
status: 'empty'
})
}
} catch (error) {
setError(error.message)
Logger.error(error.message)
@ -83,56 +194,83 @@ export default function PublishPage({
return isInPurgatory && purgatoryData ? null : (
<Formik
initialValues={initialValues}
initialValues={
publishType === 'dataset' ? datasetInitialValues : algoInitialValues
}
initialStatus="empty"
validationSchema={validationSchema}
validationSchema={
publishType === 'dataset' ? 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 === 'dataset'
? await handleSubmit(values, resetForm)
: await handleAlgorithmSubmit(values, resetForm)
}}
enableReinitialize
>
{({ values }) => (
<>
<Persist name={formName} ignoreFields={['isSubmitting']} />
{({ values }) => {
const tabs = [
{
title: 'Data Set',
content: <TabContent values={values} publishType={publishType} />
},
{
title: 'Algorithm',
content: <TabContent values={values} publishType={publishType} />
}
]
{hasFeedback ? (
<MetadataFeedback
title="Publishing Data Set"
error={error}
success={success}
loading={publishStepText}
setError={setError}
successAction={{
name: 'Go to data set →',
to: `/asset/${did}`
}}
return (
<>
<Persist
name={
publishType === 'dataset'
? formNameDatasets
: formNameAlgorithms
}
ignoreFields={['isSubmitting']}
/>
) : (
<>
<PublishType type={publishType} setType={setPublishType} />
<Alert
text={content.warning}
state="info"
className={styles.alert}
{hasFeedback ? (
<MetadataFeedback
title={title}
error={error}
success={success}
loading={publishStepText}
setError={setError}
successAction={{
name: 'Go to data set →',
to: `/asset/${did}`
}}
/>
<article className={styles.grid}>
<FormPublish content={content.form} />
) : (
<>
<Alert
text={content.warning}
state="info"
className={styles.alert}
/>
<aside>
<div className={styles.sticky}>
<MetadataPreview values={values} />
<Web3Feedback />
</div>
</aside>
</article>
</>
)}
<Tabs
className={styles.tabs}
items={tabs}
handleTabChange={(title) => {
setPublishType(title.toLowerCase().replace(' ', '') as any)
title === 'Algorithm'
? setdatasetInitialValues(values)
: setAlgoInitialValues(values)
}}
/>
</>
)}
{debug === true && <Debug values={values} />}
</>
)}
{debug === true && <Debug values={values} />}
</>
)
}}
</Formik>
)
}

View File

@ -43,6 +43,10 @@ button.filter,
color: var(--background-body);
}
.filterList:first-of-type {
margin-bottom: calc(var(--spacer) / 6);
}
.showClear {
display: inline-flex;
text-transform: capitalize;

View File

@ -0,0 +1,42 @@
import { MetadataPublishFormAlgorithm } from '../@types/MetaData'
import { File as FileMetadata } from '@oceanprotocol/lib'
import * as Yup from 'yup'
export const validationSchema: Yup.SchemaOf<MetadataPublishFormAlgorithm> = 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(/node:pre-defined|python:pre-defined|custom image/g, {
excludeEmptyString: true
})
.required('Required'),
image: Yup.string().required('Required'),
containerTag: Yup.string().required('Required'),
entrypoint: Yup.string().required('Required'),
author: Yup.string().required('Required'),
termsAndConditions: Yup.boolean().required('Required'),
// ---- optional fields ----
algorithmPrivacy: Yup.boolean().nullable(),
tags: Yup.string().nullable(),
links: Yup.array<FileMetadata[]>().nullable()
})
.defined()
export const initialValues: Partial<MetadataPublishFormAlgorithm> = {
name: '',
author: '',
dockerImage: 'node:pre-defined',
image: 'node',
containerTag: '10',
entrypoint: 'node $ALGO',
files: '',
description: '',
algorithmPrivacy: false,
termsAndConditions: false,
tags: ''
}

View File

@ -1,4 +1,4 @@
import { MetadataMarket, MetadataPublishForm } from '../@types/MetaData'
import { MetadataMarket, MetadataPublishFormDataset } from '../@types/MetaData'
import { secondsToString } from '../utils/metadata'
import * as Yup from 'yup'
@ -13,7 +13,7 @@ export const validationSchema = Yup.object().shape({
export function getInitialValues(
metadata: MetadataMarket,
timeout: number
): Partial<MetadataPublishForm> {
): Partial<MetadataPublishFormDataset> {
return {
name: metadata.main.name,
description: metadata.additionalInformation.description,

View File

@ -1,8 +1,8 @@
import { MetadataPublishForm } from '../@types/MetaData'
import { MetadataPublishFormDataset } from '../@types/MetaData'
import { File as FileMetadata } from '@oceanprotocol/lib'
import * as Yup from 'yup'
export const validationSchema: Yup.SchemaOf<MetadataPublishForm> = Yup.object()
export const validationSchema: Yup.SchemaOf<MetadataPublishFormDataset> = Yup.object()
.shape({
// ---- required fields ----
name: Yup.string()
@ -29,7 +29,7 @@ export const validationSchema: Yup.SchemaOf<MetadataPublishForm> = Yup.object()
})
.defined()
export const initialValues: Partial<MetadataPublishForm> = {
export const initialValues: Partial<MetadataPublishFormDataset> = {
name: '',
author: '',
dataTokenOptions: {

View File

@ -4,7 +4,7 @@ import Page from '../components/templates/Page'
import { graphql, PageProps } from 'gatsby'
export default function PageGatsbyPublish(props: PageProps): ReactElement {
const content = (props.data as any).content.edges[0].node.childPagesJson
const content = (props.data as any).content.edges[0].node.childPublishJson
const { title, description } = content
return (
@ -16,27 +16,15 @@ export default function PageGatsbyPublish(props: PageProps): ReactElement {
export const contentQuery = graphql`
query PublishPageQuery {
content: allFile(filter: { relativePath: { eq: "pages/publish.json" } }) {
content: allFile(
filter: { relativePath: { eq: "pages/publish/index.json" } }
) {
edges {
node {
childPagesJson {
childPublishJson {
title
description
warning
form {
title
data {
name
placeholder
label
help
type
required
sortOptions
options
}
success
}
}
}
}

View File

@ -1,8 +1,15 @@
import { MetadataMarket, MetadataPublishForm } from '../@types/MetaData'
import axios, { CancelToken, AxiosResponse } from 'axios'
import { toast } from 'react-toastify'
import isUrl from 'is-url-superb'
import {
MetadataMarket,
MetadataPublishFormDataset,
MetadataPublishFormAlgorithm
} from '../@types/MetaData'
import { toStringNoMS } from '.'
import AssetModel from '../models/Asset'
import slugify from '@sindresorhus/slugify'
import { DDO } from '@oceanprotocol/lib'
import { DDO, MetadataAlgorithm, Logger } from '@oceanprotocol/lib'
export function transformTags(value: string): string[] {
const originalTags = value?.split(',')
@ -66,6 +73,29 @@ export function checkIfTimeoutInPredefinedValues(
return false
}
function getAlgoithComponent(
image: string,
containerTag: string,
entrypoint: string,
algorithmLanguace: string
): MetadataAlgorithm {
return {
language: algorithmLanguace,
format: 'docker-image',
version: '0.1',
container: {
entrypoint: entrypoint,
image: image,
tag: containerTag
}
}
}
function getAlgoithFileExtension(fileUrl: string): string {
const splitedFileUrl = fileUrl.split('.')
return splitedFileUrl[splitedFileUrl.length - 1]
}
export function transformPublishFormToMetadata(
{
name,
@ -75,7 +105,7 @@ export function transformPublishFormToMetadata(
links,
termsAndConditions,
files
}: Partial<MetadataPublishForm>,
}: Partial<MetadataPublishFormDataset>,
ddo?: DDO
): MetadataMarket {
const currentTime = toStringNoMS(new Date())
@ -100,3 +130,103 @@ export function transformPublishFormToMetadata(
return metadata
}
async function isDockerHubImageValid(
image: string,
tag: string
): Promise<boolean> {
try {
const response = await axios.get(
`https://hub.docker.com/v2/repositories/${image}/tags/${tag}`
)
if (!response || response.status !== 200 || !response.data) {
toast.error(
'Could not fetch docker hub image info. Please check image name and tag and try again'
)
return false
}
return true
} catch (error) {
Logger.error(error.message)
toast.error(
'Could not fetch docker hub image info. Please check image name and tag and try again'
)
return false
}
}
async function is3rdPartyImageValid(imageURL: string): Promise<boolean> {
try {
const response = await axios.head(imageURL)
if (!response || response.status !== 200) {
toast.error(
'Could not fetch docker image info. Please check URL and try again'
)
return false
}
return true
} catch (error) {
Logger.error(error.message)
toast.error(
'Could not fetch docker image info. Please check URL and try again'
)
return false
}
}
export async function validateDockerImage(
dockerImage: string,
tag: string
): Promise<boolean> {
const isValid = isUrl(dockerImage)
? await is3rdPartyImageValid(dockerImage)
: await isDockerHubImageValid(dockerImage, tag)
return isValid
}
export function transformPublishAlgorithmFormToMetadata(
{
name,
author,
description,
tags,
dockerImage,
image,
containerTag,
entrypoint,
termsAndConditions,
files
}: Partial<MetadataPublishFormAlgorithm>,
ddo?: DDO
): MetadataMarket {
const currentTime = toStringNoMS(new Date())
const fileUrl = typeof files !== 'string' && files[0].url
const algorithmLanguace = getAlgoithFileExtension(fileUrl)
const algorithm = getAlgoithComponent(
image,
containerTag,
entrypoint,
algorithmLanguace
)
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

@ -1,6 +1,6 @@
import { MetadataPublishForm } from '../../../src/@types/MetaData'
import { MetadataPublishFormDataset } from '../../../src/@types/MetaData'
const testFormData: MetadataPublishForm = {
const testFormData: MetadataPublishFormDataset = {
author: '',
files: [],
dataTokenOptions: {

View File

@ -3,20 +3,19 @@ import { render } from '@testing-library/react'
import { transformPublishFormToMetadata } from '../../../src/utils/metadata'
import {
MetadataMarket,
MetadataPublishForm
MetadataPublishFormDataset
} from '../../../src/@types/MetaData'
import PublishForm from '../../../src/components/pages/Publish/FormPublish'
import publishFormData from '../__fixtures__/testFormData'
import content from '../../../content/pages/publish.json'
describe('PublishForm', () => {
it('renders without crashing', async () => {
const { container } = render(<PublishForm content={content.form} />)
const { container } = render(<PublishForm />)
expect(container.firstChild).toBeInTheDocument()
})
// it('Form data is correctly transformed to asset Metadata', () => {
// const data: MetadataPublishForm = publishFormData
// const data: MetadataPublishFormDataset = publishFormData
// let metadata: MetadataMarket = transformPublishFormToMetadata(data)
// expect(metadata.additionalInformation).toBeDefined()

View File

@ -1,7 +1,7 @@
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 content from '../../../content/pages/publish/index.json'
describe('Home', () => {
it('renders without crashing', () => {