diff --git a/content/publish/form.json b/content/publish/form.json index 69daabd8e..e625624ac 100644 --- a/content/publish/form.json +++ b/content/publish/form.json @@ -54,21 +54,22 @@ }, { "name": "dockerImageCustom", - "label": "Docker 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", + "label": "Custom Docker Image", + "placeholder": "e.g. oceanprotocol/algo_dockers:node-vibrant or quay.io/startx/mariadb", + "help": "Provide the name and the tag of a public Docker hub image or the custom image if you have it hosted in a 3rd party repository", + "type": "container", "required": true }, { - "name": "dockerImageCustomTag", - "label": "Docker Image Tag", - "placeholder": "e.g. latest", - "help": "Provide the tag for your Docker image.", + "name": "dockerImageChecksum", + "label": "Docker Image Checksum", + "placeholder": "e.g. sha256:xiXqb7Vet0FbN9q0GFMgUdi5C22wjJT0i2G6lYKC2jl6QxkKzVz7KaPDgqfTMjNF", + "help": "Provide the checksum(DIGEST) of your docker image.", "required": true }, { "name": "dockerImageCustomEntrypoint", - "label": "Docker Entrypoint", + "label": "Docker Image Entrypoint", "placeholder": "e.g. python $ALGO", "help": "Provide the entrypoint for your algorithm.", "required": true diff --git a/src/@utils/aquarius.ts b/src/@utils/aquarius.ts index 90b1e5a27..c0f7697ea 100644 --- a/src/@utils/aquarius.ts +++ b/src/@utils/aquarius.ts @@ -268,7 +268,7 @@ export async function getAlgorithmDatasetsForCompute( const query = generateBaseQuery(baseQueryParams) const computeDatasets = await queryMetadata(query, cancelToken) - if (computeDatasets.totalResults === 0) return [] + if (computeDatasets?.totalResults === 0) return [] const datasets = await transformAssetToAssetSelection( datasetProviderUri, diff --git a/src/@utils/docker.ts b/src/@utils/docker.ts index 9453f5447..ee917828d 100644 --- a/src/@utils/docker.ts +++ b/src/@utils/docker.ts @@ -1,16 +1,27 @@ import { LoggerInstance } from '@oceanprotocol/lib' import axios from 'axios' -import isUrl from 'is-url-superb' import { toast } from 'react-toastify' -async function isDockerHubImageValid( +export interface dockerContainerInfo { + exists: boolean + checksum: string +} + +export async function getContainerChecksum( image: string, tag: string -): Promise { +): Promise { + const containerInfo: dockerContainerInfo = { + exists: false, + checksum: null + } try { const response = await axios.post( `https://dockerhub-proxy.oceanprotocol.com`, - { image, tag } + { + image, + tag + } ) if ( !response || @@ -18,46 +29,18 @@ async function isDockerHubImageValid( response.data.status !== 'success' ) { toast.error( - 'Could not fetch docker hub image info. Please check image name and tag and try again' + 'Could not fetch docker hub image informations. If you have it hosted in a 3rd party repository please fill in the container checksum manually.' ) - return false + return containerInfo } - - return true + containerInfo.exists = true + containerInfo.checksum = response.data.result.checksum + return containerInfo } catch (error) { LoggerInstance.error(error.message) toast.error( - 'Could not fetch docker hub image info. Please check image name and tag and try again' + 'Could not fetch docker hub image informations. If you have it hosted in a 3rd party repository please fill in the container checksum manually.' ) - return false + return containerInfo } } - -async function is3rdPartyImageValid(imageURL: string): Promise { - 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) { - LoggerInstance.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 { - const isValid = isUrl(dockerImage) - ? await is3rdPartyImageValid(dockerImage) - : await isDockerHubImageValid(dockerImage, tag) - return isValid -} diff --git a/src/components/@shared/FormFields/ContainerInput/Info.module.css b/src/components/@shared/FormFields/ContainerInput/Info.module.css new file mode 100644 index 000000000..ed25b655f --- /dev/null +++ b/src/components/@shared/FormFields/ContainerInput/Info.module.css @@ -0,0 +1,48 @@ +.info { + border-radius: var(--border-radius); + padding: calc(var(--spacer) / 2); + border: 1px solid var(--border-color); + background-color: var(--background-highlight); + position: relative; +} + +.info ul { + margin: 0; +} + +.info li { + display: inline-block; + font-size: var(--font-size-small); + margin-right: calc(var(--spacer) / 2); + color: var(--color-secondary); +} + +.info li.success { + color: var(--brand-alert-green); +} + +.info li.error { + color: var(--brand-alert-red); +} + +.contianer { + margin: 0; + font-size: var(--font-size-base); + line-height: var(--line-height); + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-all; + padding-right: calc(var(--spacer) / 2); +} + +.removeButton { + cursor: pointer; + border: none; + position: absolute; + top: -0.2rem; + right: 0; + font-size: var(--font-size-h3); + cursor: pointer; + color: var(--font-color-text); + background-color: transparent; +} diff --git a/src/components/@shared/FormFields/ContainerInput/Info.tsx b/src/components/@shared/FormFields/ContainerInput/Info.tsx new file mode 100644 index 000000000..a24332273 --- /dev/null +++ b/src/components/@shared/FormFields/ContainerInput/Info.tsx @@ -0,0 +1,29 @@ +import React, { ReactElement } from 'react' +import styles from './Info.module.css' + +export default function ImageInfo({ + image, + tag, + valid, + handleClose +}: { + image: string + tag: string + valid: boolean + handleClose(): void +}): ReactElement { + const displayText = valid + ? '✓ Image found, container checksum automatically added!' + : 'x Container checksum could not be fetched automatically, please add it manually' + return ( +
+

{`Image: ${image} Tag: ${tag}`}

+
    +
  • {displayText}
  • +
+ +
+ ) +} diff --git a/src/components/@shared/FormFields/ContainerInput/index.tsx b/src/components/@shared/FormFields/ContainerInput/index.tsx new file mode 100644 index 000000000..400088ae1 --- /dev/null +++ b/src/components/@shared/FormFields/ContainerInput/index.tsx @@ -0,0 +1,83 @@ +import React, { ReactElement, useState } from 'react' +import { useField, useFormikContext } from 'formik' +import UrlInput from '../URLInput' +import { InputProps } from '@shared/FormInput' +import { FormPublishData } from 'src/components/Publish/_types' +import { LoggerInstance } from '@oceanprotocol/lib' +import ImageInfo from './Info' +import { getContainerChecksum } from '@utils/docker' + +export default function ContainerInput(props: InputProps): ReactElement { + const [field] = useField(props.name) + const [fieldChecksum, metaChecksum, helpersChecksum] = useField( + 'metadata.dockerImageCustomChecksum' + ) + + const { values, setFieldError, setFieldValue } = + useFormikContext() + const [isLoading, setIsLoading] = useState(false) + const [isValid, setIsValid] = useState(false) + const [checked, setChecked] = useState(false) + + async function handleValidation(e: React.SyntheticEvent, container: string) { + e.preventDefault() + try { + setIsLoading(true) + const parsedContainerValue = container?.split(':') + const imageName = + parsedContainerValue?.length > 1 + ? parsedContainerValue?.slice(0, -1).join(':') + : parsedContainerValue[0] + const tag = + parsedContainerValue?.length > 1 ? parsedContainerValue?.at(-1) : '' + const containerInfo = await getContainerChecksum(imageName, tag) + setFieldValue('metadata.dockerImageCustom', imageName) + setFieldValue('metadata.dockerImageCustomTag', tag) + setChecked(true) + if (containerInfo.checksum) { + setFieldValue( + 'metadata.dockerImageCustomChecksum', + containerInfo.checksum + ) + helpersChecksum.setTouched(false) + setIsValid(true) + } + } catch (error) { + setFieldError(`${field.name}[0].url`, error.message) + LoggerInstance.error(error.message) + } finally { + setIsLoading(false) + } + } + + function handleClose() { + setFieldValue('metadata.dockerImageCustom', '') + setFieldValue('metadata.dockerImageCustomTag', '') + setFieldValue('metadata.dockerImageCustomChecksum', '') + setChecked(false) + setIsValid(false) + helpersChecksum.setTouched(true) + } + + return ( + <> + {checked ? ( + + ) : ( + + )} + + ) +} diff --git a/src/components/@shared/FormFields/URLInput/index.tsx b/src/components/@shared/FormFields/URLInput/index.tsx index 3890032c9..af7d0fdf3 100644 --- a/src/components/@shared/FormFields/URLInput/index.tsx +++ b/src/components/@shared/FormFields/URLInput/index.tsx @@ -12,12 +12,14 @@ export default function URLInput({ handleButtonClick, isLoading, name, + checkUrl, ...props }: { submitText: string handleButtonClick(e: React.SyntheticEvent, data: string): void isLoading: boolean name: string + checkUrl?: boolean }): ReactElement { const [field, meta] = useField(name) const [isButtonDisabled, setIsButtonDisabled] = useState(true) @@ -28,7 +30,7 @@ export default function URLInput({ setIsButtonDisabled( !field?.value || field.value === '' || - !isUrl(field.value) || + (checkUrl && !isUrl(field.value)) || field.value.includes('javascript:') || meta?.error ) diff --git a/src/components/@shared/FormInput/InputElement.tsx b/src/components/@shared/FormInput/InputElement.tsx index 5d8d57f82..79e846f54 100644 --- a/src/components/@shared/FormInput/InputElement.tsx +++ b/src/components/@shared/FormInput/InputElement.tsx @@ -11,6 +11,7 @@ import AssetSelection, { } from '../FormFields/AssetSelection' import Nft from '../FormFields/Nft' import InputRadio from './InputRadio' +import ContainerInput from '@shared/FormFields/ContainerInput' import TagsAutoComplete from './TagsAutoComplete' const cx = classNames.bind(styles) @@ -108,6 +109,8 @@ export default function InputElement({ ) case 'files': return + case 'container': + return case 'providerUrl': return case 'nft': diff --git a/src/components/Publish/Metadata/index.tsx b/src/components/Publish/Metadata/index.tsx index 2be4852bd..775246ab9 100644 --- a/src/components/Publish/Metadata/index.tsx +++ b/src/components/Publish/Metadata/index.tsx @@ -1,6 +1,6 @@ import { BoxSelectionOption } from '@shared/FormFields/BoxSelection' import Input from '@shared/FormInput' -import { Field, useFormikContext } from 'formik' +import { Field, useField, useFormikContext } from 'formik' import React, { ReactElement, useEffect } from 'react' import content from '../../../../content/publish/form.json' import { FormPublishData } from '../_types' @@ -23,6 +23,8 @@ export default function MetadataFields(): ReactElement { // connect with Form state, use for conditional field rendering const { values, setFieldValue } = useFormikContext() + const [field, meta] = useField('metadata.dockerImageCustomChecksum') + // BoxSelection component is not a Formik component // so we need to handle checked state manually. const assetTypeOptions: BoxSelectionOption[] = [ @@ -124,11 +126,14 @@ export default function MetadataFields(): ReactElement { /> { if (dockerImage === '') return const preset = algorithmContainerPresets.find( (preset) => `${preset.image}:${preset.tag}` === dockerImage ) + preset.checksum = await ( + await getContainerChecksum(preset.image, preset.tag) + ).checksum return preset } @@ -80,6 +84,11 @@ export async function transformPublishFormToDdo( const currentTime = dateToStringNoMS(new Date()) const isPreview = !datatokenAddress && !nftAddress + const algorithmContainerPresets = + type === 'algorithm' && dockerImage !== '' && dockerImage !== 'custom' + ? await getAlgorithmContainerPreset(dockerImage) + : null + // Transform from files[0].url to string[] assuming only 1 file const filesTransformed = files?.length && files[0].valid && [sanitizeUrl(files[0].url)] @@ -110,20 +119,19 @@ export async function transformPublishFormToDdo( entrypoint: dockerImage === 'custom' ? dockerImageCustomEntrypoint - : getAlgorithmContainerPreset(dockerImage).entrypoint, + : algorithmContainerPresets.entrypoint, image: dockerImage === 'custom' ? dockerImageCustom - : getAlgorithmContainerPreset(dockerImage).image, + : algorithmContainerPresets.image, tag: dockerImage === 'custom' ? dockerImageCustomTag - : getAlgorithmContainerPreset(dockerImage).tag, + : algorithmContainerPresets.tag, checksum: dockerImage === 'custom' - ? // ? dockerImageCustomChecksum - '' - : getAlgorithmContainerPreset(dockerImage).checksum + ? dockerImageCustomChecksum + : algorithmContainerPresets.checksum } } })