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

Merge branch 'main' into fix/issue497-fetch-price-subgraph, fixed merge conflicts

This commit is contained in:
Bogdan Fazakas 2021-05-10 16:27:32 +03:00
commit ff1d987b5d
41 changed files with 308 additions and 320 deletions

View File

@ -1,5 +1,5 @@
# Default network, possible values: # Default network, possible values:
# "development", "ropsten", "rinkeby", "mainnet", "polygon" # "development", "ropsten", "rinkeby", "mainnet", "polygon", "moonbeamalpha"
GATSBY_NETWORK="rinkeby" GATSBY_NETWORK="rinkeby"
#GATSBY_INFURA_PROJECT_ID="xxx" #GATSBY_INFURA_PROJECT_ID="xxx"

View File

@ -20,6 +20,15 @@
"rows": 10, "rows": 10,
"required": true "required": true
}, },
{
"name": "price",
"label": "New Price",
"type": "number",
"min": "1",
"placeholder": "0",
"help": "Enter a new price.",
"required": true
},
{ {
"name": "links", "name": "links",
"label": "Sample file", "label": "Sample file",

View File

@ -6,13 +6,6 @@
"successAction": "Close", "successAction": "Close",
"error": "Updating DDO failed.", "error": "Updating DDO failed.",
"data": [ "data": [
{
"name": "allowAllPublishedAlgorithms",
"label": "All Algorithms",
"help": "Allow any published algorithm to run on this data set.",
"type": "checkbox",
"options": ["Allow any published algorithm"]
},
{ {
"name": "publisherTrustedAlgorithms", "name": "publisherTrustedAlgorithms",
"label": "Selected Algorithms", "label": "Selected Algorithms",
@ -21,6 +14,13 @@
"multiple": true, "multiple": true,
"options": [], "options": [],
"sortOptions": false "sortOptions": false
},
{
"name": "allowAllPublishedAlgorithms",
"label": "All Algorithms",
"help": "Allow any published algorithm to run on this data set.",
"type": "checkbox",
"options": ["Allow any published algorithm"]
} }
] ]
} }

View File

@ -1,4 +1,7 @@
{ {
"title": "History", "title": "History",
"description": "Find the data sets and jobs that you previously accessed." "description": "Find the data sets and jobs that you previously accessed.",
"compute": {
"storage": "Results are stored for 30 days."
}
} }

View File

@ -19,7 +19,7 @@
"name": "files", "name": "files",
"label": "File", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "placeholder": "e.g. https://file.com/file.json",
"help": "Please enter the URL to your algorithm file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing.", "help": "Please enter the URL to your algorithm file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing. Some restrictions apply:\n\n- max. running time: 1 min.\n- [Writing Algorithms for Compute to Data](https://docs.oceanprotocol.com/tutorials/compute-to-data-algorithms/)",
"type": "files", "type": "files",
"required": true "required": true
}, },
@ -56,6 +56,13 @@
"sortOptions": false, "sortOptions": false,
"required": true "required": true
}, },
{
"name": "dataTokenOptions",
"label": "Datatoken Name & Symbol",
"type": "datatoken",
"help": "The datatoken for this algorithm will be created with this name & symbol.",
"required": true
},
{ {
"name": "entrypoint", "name": "entrypoint",
"label": "Entrypoint", "label": "Entrypoint",

View File

@ -19,7 +19,7 @@
"name": "files", "name": "files",
"label": "File", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "placeholder": "e.g. https://file.com/file.json",
"help": "Please enter the URL to your data set file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing.", "help": "Please enter the URL to your data set file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing. For a compute data set, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size.",
"type": "files", "type": "files",
"required": true "required": true
}, },

36
package-lock.json generated
View File

@ -1573,16 +1573,16 @@
} }
}, },
"@ethereum-navigator/atlas": { "@ethereum-navigator/atlas": {
"version": "0.7.1", "version": "0.7.3",
"resolved": "https://registry.npmjs.org/@ethereum-navigator/atlas/-/atlas-0.7.1.tgz", "resolved": "https://registry.npmjs.org/@ethereum-navigator/atlas/-/atlas-0.7.3.tgz",
"integrity": "sha512-YV7tMVwpRcJbc+Kj/Rr0RzNV/2hHBEEM1/tMWDVLB15dGJfoQuRfPJpFt6uq+Ji6s3EkldIt9kZylEeG5ALKAA==" "integrity": "sha512-kCyV/8wOqSU/gn+H7uSaR/Xc+ZogXrW2QmF5MfZL2+NUS4+y0emlfBphTGwP5bOB0Cg2goJTcI7Y6+0LNcJYzg=="
}, },
"@ethereum-navigator/navigator": { "@ethereum-navigator/navigator": {
"version": "0.5.2", "version": "0.5.3",
"resolved": "https://registry.npmjs.org/@ethereum-navigator/navigator/-/navigator-0.5.2.tgz", "resolved": "https://registry.npmjs.org/@ethereum-navigator/navigator/-/navigator-0.5.3.tgz",
"integrity": "sha512-agSE2xzLxOKKid8QiS4v8jPhnFXW5uSXsICZ4JmS437aCZ8L3SUAy3cDQKikHb2PPZ3AazJO05k8m8i6u77peQ==", "integrity": "sha512-AOhS1EXPrVeWbLvW3fVbw5AZ2mPYXDKOqMojgVz602U/tEjRXAsw/Gwa+oSOssAyU37SNKBaIorEhBiEY+RYdA==",
"requires": { "requires": {
"@ethereum-navigator/atlas": "^0.7.1", "@ethereum-navigator/atlas": "^0.7.2",
"web3": "^1.2.7" "web3": "^1.2.7"
} }
}, },
@ -3622,24 +3622,24 @@
"integrity": "sha512-j4PEZSVtKSqxDYMVh/hd5vk088Bg6a6QkrUMTXN9Q6OIFAMfHM235f1AxaakNrEyK0FKMD908KuJEdfFLRn9Hw==" "integrity": "sha512-j4PEZSVtKSqxDYMVh/hd5vk088Bg6a6QkrUMTXN9Q6OIFAMfHM235f1AxaakNrEyK0FKMD908KuJEdfFLRn9Hw=="
}, },
"@oceanprotocol/contracts": { "@oceanprotocol/contracts": {
"version": "0.5.15", "version": "0.5.16",
"resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-0.5.16.tgz",
"integrity": "sha512-16edzNeO2v5WLW9ClsZ9VvMH1w24fZZutCmDhZWPirCAIdojGWRHraompfICjY1fovDljnZ5MpJpffuQ6kgxOA==" "integrity": "sha512-p7aFIUT8RVoMzdPP7ML8G08BnQ09syywKjOT16hqJm0GmofunEuVffUXbryG4EkQ+qRbf/zeoxSmesi79kQXlA=="
}, },
"@oceanprotocol/lib": { "@oceanprotocol/lib": {
"version": "0.14.4", "version": "0.14.8",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.14.4.tgz", "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.14.8.tgz",
"integrity": "sha512-f6Wj6FLpYmuFSGtnDw0lex0Vru1tGgvZqtwrdBYtMD+hW6Bn3B51+F/9ACVYAiSRDADrqPTDZiZetT2Ji3QY7Q==", "integrity": "sha512-eqab5iEgowyIM/LcDDs6xhZo/KToOmVw0betjXLG0+g70zS8R6XL2RHzCpFyutSdf/cH0w/ltPUfR8ZBElIyhQ==",
"requires": { "requires": {
"@ethereum-navigator/navigator": "^0.5.2", "@ethereum-navigator/navigator": "^0.5.2",
"@oceanprotocol/contracts": "^0.5.10", "@oceanprotocol/contracts": "0.5.16",
"@types/crypto-js": "^4.0.1", "@types/crypto-js": "^4.0.1",
"cross-fetch": "^3.1.2", "cross-fetch": "^3.1.2",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"decimal.js": "^10.2.1", "decimal.js": "^10.2.1",
"fs": "0.0.1-security", "fs": "0.0.1-security",
"lzma": "^2.3.2", "lzma": "^2.3.2",
"node-abort-controller": "^1.2.0", "node-abort-controller": "^2.0.0",
"save-file": "^2.3.1", "save-file": "^2.3.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"web3": "^1.3.5", "web3": "^1.3.5",
@ -28882,9 +28882,9 @@
} }
}, },
"node-abort-controller": { "node-abort-controller": {
"version": "1.2.1", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-1.2.1.tgz", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-2.0.0.tgz",
"integrity": "sha512-79PYeJuj6S9+yOHirR0JBLFOgjB6sQCir10uN6xRx25iD+ZD4ULqgRn3MwWBRaQGB0vEgReJzWwJo42T1R6YbQ==" "integrity": "sha512-L8RfEgjBTHAISTuagw51PprVAqNZoG6KSB6LQ6H1bskMVkFs5E71IyjauLBv3XbuomJlguWF/VnRHdJ1gqiAqA=="
}, },
"node-addon-api": { "node-addon-api": {
"version": "2.0.2", "version": "2.0.2",

View File

@ -27,7 +27,7 @@
"@coingecko/cryptoformat": "^0.4.2", "@coingecko/cryptoformat": "^0.4.2",
"@loadable/component": "^5.14.1", "@loadable/component": "^5.14.1",
"@oceanprotocol/art": "^3.0.0", "@oceanprotocol/art": "^3.0.0",
"@oceanprotocol/lib": "^0.14.4", "@oceanprotocol/lib": "^0.14.8",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@portis/web3": "^3.0.3", "@portis/web3": "^3.0.3",
"@sindresorhus/slugify": "^1.0.0", "@sindresorhus/slugify": "^1.0.0",

View File

@ -49,6 +49,7 @@ export interface MetadataPublishFormAlgorithm {
dockerImage: string dockerImage: string
algorithmPrivacy: boolean algorithmPrivacy: boolean
timeout: string timeout: string
dataTokenOptions: DataTokenOptions
termsAndConditions: boolean termsAndConditions: boolean
// ---- optional fields ---- // ---- optional fields ----
image: string image: string
@ -61,6 +62,7 @@ export interface MetadataEditForm {
name: string name: string
description: string description: string
timeout: string timeout: string
price?: number
links?: string | EditableMetadataLinks[] links?: string | EditableMetadataLinks[]
} }

View File

@ -91,6 +91,7 @@ export default function InputElement({
id={slugify(option)} id={slugify(option)}
type={type} type={type}
name={name} name={name}
checked={props.defaultChecked}
{...props} {...props}
/> />
<label className={styles.radioLabel} htmlFor={slugify(option)}> <label className={styles.radioLabel} htmlFor={slugify(option)}>

View File

@ -107,7 +107,9 @@
.did { .did {
padding: 0; padding: 0;
font-size: var(--font-size-mini); /* font-size: var(--font-size-mini); */
/* hack to make DotDotDot clamp work in Safari*/
font-size: 0.63rem;
display: block; display: block;
text-align: left; text-align: left;
color: var(--color-secondary); color: var(--color-secondary);

View File

@ -27,7 +27,7 @@ export default function FileInput({
<Button <Button
style="primary" style="primary"
size="small" size="small"
onClick={(e: React.SyntheticEvent) => handleButtonClick(e, field.value)} onClick={(e: React.SyntheticEvent) => e.preventDefault()}
disabled={!field.value} disabled={!field.value}
> >
{isLoading ? <Loader /> : 'Add File'} {isLoading ? <Loader /> : 'Add File'}

View File

@ -14,7 +14,7 @@ export default function FilesInput(props: InputProps): ReactElement {
const [fileUrl, setFileUrl] = useState<string>() const [fileUrl, setFileUrl] = useState<string>()
const { config } = useOcean() const { config } = useOcean()
useEffect(() => { function loadFileInfo() {
const source = axios.CancelToken.source() const source = axios.CancelToken.source()
async function validateUrl() { async function validateUrl() {
@ -33,11 +33,16 @@ export default function FilesInput(props: InputProps): ReactElement {
setIsLoading(false) setIsLoading(false)
} }
} }
fileUrl && validateUrl() fileUrl && validateUrl()
return () => { return () => {
source.cancel() source.cancel()
} }
}
useEffect(() => {
loadFileInfo()
}, [fileUrl, config.providerUri]) }, [fileUrl, config.providerUri])
async function handleButtonClick(e: React.SyntheticEvent, url: string) { async function handleButtonClick(e: React.SyntheticEvent, url: string) {
@ -48,6 +53,11 @@ export default function FilesInput(props: InputProps): ReactElement {
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf' // File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
e.preventDefault() e.preventDefault()
// In the case when the user re-add the same URL after it was removed (by accident or intentionally)
if (fileUrl === url) {
loadFileInfo()
}
setFileUrl(url) setFileUrl(url)
} }

View File

@ -14,6 +14,10 @@ const query = graphql`
export default function Terms(props: InputProps): ReactElement { export default function Terms(props: InputProps): ReactElement {
const data = useStaticQuery(query) const data = useStaticQuery(query)
const termsProps: InputProps = {
...props,
defaultChecked: props.value.toString() === 'true'
}
return ( return (
<> <>
@ -21,7 +25,7 @@ export default function Terms(props: InputProps): ReactElement {
className={styles.terms} className={styles.terms}
dangerouslySetInnerHTML={{ __html: data.terms.html }} dangerouslySetInnerHTML={{ __html: data.terms.html }}
/> />
<InputElement {...props} type="checkbox" /> <InputElement {...termsProps} type="checkbox" />
</> </>
) )
} }

View File

@ -135,6 +135,11 @@ export function MetadataAlgorithmPreview({
<h2 className={styles.previewTitle}>Preview</h2> <h2 className={styles.previewTitle}>Preview</h2>
<header> <header>
{values.name && <h3 className={styles.title}>{values.name}</h3>} {values.name && <h3 className={styles.title}>{values.name}</h3>}
{values.dataTokenOptions?.name && (
<p
className={styles.datatoken}
>{`${values.dataTokenOptions.name}${values.dataTokenOptions.symbol}`}</p>
)}
{values.description && <Description description={values.description} />} {values.description && <Description description={values.description} />}
<div className={styles.asset}> <div className={styles.asset}>

View File

@ -17,20 +17,30 @@ export default function SearchBar({
filters?: boolean filters?: boolean
size?: 'small' | 'large' size?: 'small' | 'large'
}): ReactElement { }): ReactElement {
const [value, setValue] = useState(initialValue || '') let [value, setValue] = useState(initialValue || '')
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
}
async function startSearch(e: FormEvent<HTMLButtonElement>) { async function startSearch(e: FormEvent<HTMLButtonElement>) {
e.preventDefault() e.preventDefault()
if (value === '') return if (value === '') value = ' '
const urlEncodedValue = encodeURIComponent(value) const urlEncodedValue = encodeURIComponent(value)
const url = await addExistingParamsToUrl(location, 'text') const url = await addExistingParamsToUrl(location, 'text')
navigate(`${url}&text=${urlEncodedValue}`) navigate(`${url}&text=${urlEncodedValue}`)
} }
async function emptySearch() {
const searchParams = new URLSearchParams(window.location.href)
const text = searchParams.get('text')
if (text !== ('' || undefined || null)) {
const url = await addExistingParamsToUrl(location, 'text')
navigate(`${url}&text=%20`)
}
}
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
e.target.value === '' && emptySearch()
}
return ( return (
<form className={styles.form}> <form className={styles.form}>
<InputGroup> <InputGroup>

View File

@ -1,14 +1,13 @@
.buttons { .buttons {
display: flex; display: grid;
justify-content: space-between; gap: calc(var(--spacer) / 4);
grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
padding-bottom: calc(var(--spacer) / 8); padding-bottom: calc(var(--spacer) / 8);
} }
.button { .button {
display: block; width: auto;
flex: 0 0 48%; padding: calc(var(--spacer) / 3) calc(var(--spacer) / 4) !important;
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2)
calc(var(--spacer) / 4) calc(var(--spacer) / 2) !important;
border-radius: var(--border-radius); border-radius: var(--border-radius);
text-transform: none; text-transform: none;
} }

View File

@ -18,8 +18,9 @@ export default function Chain(): ReactElement {
} }
const chains = [ const chains = [
{ name: 'ETH', oceanConfig: 'mainnet' }, { name: 'ETH', oceanConfig: 'mainnet', label: 'Mainnet' },
{ name: 'Polygon/Matic', oceanConfig: 'polygon' } { name: 'Polygon/Matic', oceanConfig: 'polygon', label: 'Mainnet' },
{ name: 'Moonbase Alpha', oceanConfig: 'moonbeamalpha', label: 'Testnet' }
] ]
// TODO: to fully solve https://github.com/oceanprotocol/market/issues/432 // TODO: to fully solve https://github.com/oceanprotocol/market/issues/432
@ -42,7 +43,7 @@ export default function Chain(): ReactElement {
onClick={() => connectOcean(button.oceanConfig)} onClick={() => connectOcean(button.oceanConfig)}
> >
{button.name} {button.name}
<span>Mainnet</span> <span>{button.label}</span>
</Button> </Button>
) )
})} })}

View File

@ -25,7 +25,10 @@ import {
getInitialValues, getInitialValues,
validationSchema validationSchema
} from '../../../../models/FormStartComputeDataset' } from '../../../../models/FormStartComputeDataset'
import { ComputeAlgorithm } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute' import {
ComputeAlgorithm,
ComputeOutput
} from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection' import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
import { SearchQuery } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache' import { SearchQuery } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import axios from 'axios' import axios from 'axios'
@ -190,8 +193,16 @@ export default function Compute({
}, [ocean, ddo, accountId]) }, [ocean, ddo, accountId])
useEffect(() => { useEffect(() => {
if (!ocean || !accountId || !selectedAlgorithmAsset) return if (!selectedAlgorithmAsset) return
initMetadata(selectedAlgorithmAsset)
const { timeout } = (
ddo.findServiceByType('access') || ddo.findServiceByType('compute')
).attributes.main
setAlgorithmTimeout(secondsToString(timeout))
if (accountId) {
if (selectedAlgorithmAsset.findServiceByType('access')) { if (selectedAlgorithmAsset.findServiceByType('access')) {
checkPreviousOrders(selectedAlgorithmAsset).then(() => { checkPreviousOrders(selectedAlgorithmAsset).then(() => {
if ( if (
@ -204,12 +215,8 @@ export default function Compute({
} else if (selectedAlgorithmAsset.findServiceByType('compute')) { } else if (selectedAlgorithmAsset.findServiceByType('compute')) {
checkPreviousOrders(selectedAlgorithmAsset) checkPreviousOrders(selectedAlgorithmAsset)
} }
checkAssetDTBalance(selectedAlgorithmAsset) }
initMetadata(selectedAlgorithmAsset) ocean && checkAssetDTBalance(selectedAlgorithmAsset)
const { timeout } = (
ddo.findServiceByType('access') || ddo.findServiceByType('compute')
).attributes.main
setAlgorithmTimeout(secondsToString(timeout))
}, [selectedAlgorithmAsset, ocean, accountId, hasPreviousAlgorithmOrder]) }, [selectedAlgorithmAsset, ocean, accountId, hasPreviousAlgorithmOrder])
// Output errors in toast UI // Output errors in toast UI
@ -330,7 +337,10 @@ export default function Compute({
computeAlgorithm.transferTxId = algorithmAssetOrderId computeAlgorithm.transferTxId = algorithmAssetOrderId
Logger.log('[compute] Starting compute job.') Logger.log('[compute] Starting compute job.')
const output = {} const output: ComputeOutput = {
publishAlgorithmLog: true,
publishOutput: true
}
const response = await ocean.compute.start( const response = await ocean.compute.start(
ddo.id, ddo.id,
assetOrderId, assetOrderId,
@ -410,7 +420,9 @@ export default function Compute({
action={<SuccessAction />} action={<SuccessAction />}
/> />
)} )}
{type !== 'algorithm' && (
<Web3Feedback isBalanceSufficient={isBalanceSufficient} /> <Web3Feedback isBalanceSufficient={isBalanceSufficient} />
)}
</footer> </footer>
</> </>
) )

View File

@ -47,15 +47,17 @@ export default function FormEditMetadata({
data, data,
setShowEdit, setShowEdit,
setTimeoutStringValue, setTimeoutStringValue,
values values,
showPrice
}: { }: {
data: FormFieldProps[] data: FormFieldProps[]
setShowEdit: (show: boolean) => void setShowEdit: (show: boolean) => void
setTimeoutStringValue: (value: string) => void setTimeoutStringValue: (value: string) => void
values: Partial<MetadataPublishFormDataset> values: Partial<MetadataPublishFormDataset>
showPrice: boolean
}): ReactElement { }): ReactElement {
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { ocean } = useOcean() const { ocean, config } = useOcean()
const { const {
isValid, isValid,
validateField, validateField,
@ -79,16 +81,20 @@ export default function FormEditMetadata({
return ( return (
<Form className={styles.form}> <Form className={styles.form}>
{data.map((field: FormFieldProps) => ( {data.map(
(field: FormFieldProps) =>
(!showPrice && field.name === 'price') || (
<Field <Field
key={field.name} key={field.name}
{...field} {...field}
component={Input} component={Input}
prefix={field.name === 'price' && config.oceanTokenSymbol}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field) handleFieldChange(e, field)
} }
/> />
))} )
)}
<footer className={styles.actions}> <footer className={styles.actions}>
<Button <Button

View File

@ -36,6 +36,7 @@ const contentQuery = graphql`
label label
help help
type type
min
required required
sortOptions sortOptions
options options
@ -60,7 +61,7 @@ export default function Edit({
const { debug } = useUserPreferences() const { debug } = useUserPreferences()
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { ocean } = useOcean() const { ocean } = useOcean()
const { metadata, ddo, refreshDdo } = useAsset() const { metadata, ddo, refreshDdo, price } = useAsset()
const [success, setSuccess] = useState<string>() const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const [timeoutStringValue, setTimeoutStringValue] = useState<string>() const [timeoutStringValue, setTimeoutStringValue] = useState<string>()
@ -70,6 +71,18 @@ export default function Edit({
const hasFeedback = error || success const hasFeedback = error || success
async function updateFixedPrice(newPrice: number) {
const setPriceResp = await ocean.fixedRateExchange.setRate(
price.address,
newPrice,
accountId
)
if (!setPriceResp) {
setError(content.form.error)
Logger.error(content.form.error)
}
}
async function handleSubmit( async function handleSubmit(
values: Partial<MetadataEditForm>, values: Partial<MetadataEditForm>,
resetForm: () => void resetForm: () => void
@ -82,6 +95,10 @@ export default function Edit({
links: typeof values.links !== 'string' ? values.links : [] links: typeof values.links !== 'string' ? values.links : []
}) })
price.type === 'exchange' &&
values.price !== price.value &&
(await updateFixedPrice(values.price))
if (!ddoEditedMetdata) { if (!ddoEditedMetdata) {
setError(content.form.error) setError(content.form.error)
Logger.error(content.form.error) Logger.error(content.form.error)
@ -127,7 +144,7 @@ export default function Edit({
return ( return (
<Formik <Formik
initialValues={getInitialValues(metadata, timeout)} initialValues={getInitialValues(metadata, timeout, price.value)}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => { onSubmit={async (values, { resetForm }) => {
// move user's focus to top of screen // move user's focus to top of screen
@ -160,6 +177,7 @@ export default function Edit({
setShowEdit={setShowEdit} setShowEdit={setShowEdit}
setTimeoutStringValue={setTimeoutStringValue} setTimeoutStringValue={setTimeoutStringValue}
values={initialValues} values={initialValues}
showPrice={price.type === 'exchange'}
/> />
<aside> <aside>

View File

@ -174,7 +174,7 @@ export default function Add({
) : ( ) : (
<Alert <Alert
className={styles.warning} className={styles.warning}
text={content.warning.main} text={content.warning}
state="info" state="info"
action={{ action={{
name: 'I understand', name: 'I understand',

View File

@ -21,6 +21,7 @@ import UserLiquidity from '../../../atoms/UserLiquidity'
import InputElement from '../../../atoms/Input/InputElement' import InputElement from '../../../atoms/Input/InputElement'
import { useOcean } from '../../../../providers/Ocean' import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3' import { useWeb3 } from '../../../../providers/Web3'
import Decimal from 'decimal.js'
const contentQuery = graphql` const contentQuery = graphql`
query PoolRemoveQuery { query PoolRemoveQuery {
@ -155,14 +156,6 @@ export default function Remove({
totalPoolTokens totalPoolTokens
]) ])
async function calculateAmountOfOceansRemoved(amountPoolShares: string) {
const oceanAmount = await ocean.pool.getOceanRemovedforPoolShares(
poolAddress,
amountPoolShares
)
setAmountOcean(oceanAmount)
}
useEffect(() => { useEffect(() => {
const minOceanAmount = const minOceanAmount =
(Number(amountOcean) * (100 - Number(slippage))) / 100 (Number(amountOcean) * (100 - Number(slippage))) / 100
@ -177,19 +170,24 @@ export default function Remove({
setAmountPercent(e.target.value) setAmountPercent(e.target.value)
if (!poolTokens) return if (!poolTokens) return
const amountPoolShares = (Number(e.target.value) / 100) * Number(poolTokens) const amountPoolShares = new Decimal(e.target.value)
.dividedBy(100)
.mul(new Decimal(poolTokens))
.toPrecision(18) // in some cases the returned value contain more than 18 digits which break conversion to wei
setAmountPoolShares(`${amountPoolShares}`) setAmountPoolShares(`${amountPoolShares}`)
calculateAmountOfOceansRemoved(`${amountPoolShares}`)
} }
function handleMaxButton(e: ChangeEvent<HTMLInputElement>) { function handleMaxButton(e: ChangeEvent<HTMLInputElement>) {
e.preventDefault() e.preventDefault()
setAmountPercent(amountMaxPercent) setAmountPercent(amountMaxPercent)
const amountPoolShares = const amountPoolShares = new Decimal(amountMaxPercent)
(Number(amountMaxPercent) / 100) * Number(poolTokens) .dividedBy(100)
.mul(new Decimal(poolTokens))
.toPrecision(18)
setAmountPoolShares(`${amountPoolShares}`) setAmountPoolShares(`${amountPoolShares}`)
calculateAmountOfOceansRemoved(`${amountPoolShares}`)
} }
function handleAdvancedButton(e: FormEvent<HTMLButtonElement>) { function handleAdvancedButton(e: FormEvent<HTMLButtonElement>) {

View File

@ -44,6 +44,7 @@ const poolLiquidityQuery = gql`
id id
totalShares totalShares
swapFee swapFee
spotPrice
tokens { tokens {
tokenAddress tokenAddress
balance balance
@ -141,7 +142,8 @@ export default function Pool(): ReactElement {
setCreatorLiquidity(creatorLiquidity) setCreatorLiquidity(creatorLiquidity)
const totalCreatorLiquidityInOcean = const totalCreatorLiquidityInOcean =
creatorLiquidity?.ocean + creatorLiquidity?.datatoken * price?.value creatorLiquidity?.ocean +
creatorLiquidity?.datatoken * dataLiquidity.pool.spotPrice
setCreatorTotalLiquidityInOcean(totalCreatorLiquidityInOcean) setCreatorTotalLiquidityInOcean(totalCreatorLiquidityInOcean)
const creatorPoolShare = const creatorPoolShare =
price?.ocean && price?.ocean &&
@ -250,7 +252,7 @@ export default function Pool(): ReactElement {
<ExplorerLink <ExplorerLink
networkId={networkId} networkId={networkId}
path={ path={
networkId === 137 networkId === 137 || networkId === 1287
? `tokens/${ddo.dataToken}` ? `tokens/${ddo.dataToken}`
: `token/${ddo.dataToken}` : `token/${ddo.dataToken}`
} }

View File

@ -1,7 +1,7 @@
.bookmark { .bookmark {
position: absolute; position: absolute;
top: -10px; top: -10px;
right: calc(var(--spacer) / 4); right: calc(var(--spacer) / 8);
appearance: none; appearance: none;
background: none; background: none;
border: none; border: none;

View File

@ -4,12 +4,30 @@
font-size: var(--font-size-small); font-size: var(--font-size-small);
} }
.meta p { .asset {
margin-bottom: 0; margin-left: -2rem;
margin-right: -2rem;
padding-left: 2rem;
padding-right: 3rem;
border-bottom: 1px solid var(--border-color);
margin-bottom: calc(var(--spacer) / 1.5);
padding-bottom: calc(var(--spacer) / 1.75);
} }
.published { @media (min-width: 40rem) {
margin-top: calc(var(--spacer) / 2); .asset {
margin-top: -0.65rem;
}
}
.assetType {
display: inline-block;
border-right: 1px solid var(--border-color);
padding-right: calc(var(--spacer) / 3.5);
margin-right: calc(var(--spacer) / 4);
}
.byline {
font-size: var(--font-size-small); font-size: var(--font-size-small);
} }

View File

@ -15,21 +15,25 @@ export default function MetaMain(): ReactElement {
return ( return (
<aside className={styles.meta}> <aside className={styles.meta}>
<AssetType type={type} accessType={accessType} /> <header className={styles.asset}>
<p> <AssetType
type={type}
accessType={accessType}
className={styles.assetType}
/>
<ExplorerLink <ExplorerLink
networkId={networkId} networkId={networkId}
path={ path={
networkId === 137 networkId === 137 || networkId === 1287
? `tokens/${ddo?.dataToken}` ? `tokens/${ddo?.dataToken}`
: `token/${ddo?.dataToken}` : `token/${ddo?.dataToken}`
} }
> >
{`${ddo?.dataTokenInfo.name}${ddo?.dataTokenInfo.symbol}`} {`${ddo?.dataTokenInfo.name}${ddo?.dataTokenInfo.symbol}`}
</ExplorerLink> </ExplorerLink>
</p> </header>
<div className={styles.published}> <div className={styles.byline}>
Published By <Publisher account={owner} /> Published By <Publisher account={owner} />
<p> <p>
<Time date={ddo?.created} relative /> <Time date={ddo?.created} relative />

View File

@ -48,14 +48,16 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
const [showPricing, setShowPricing] = useState(false) const [showPricing, setShowPricing] = useState(false)
const [showEdit, setShowEdit] = useState<boolean>() const [showEdit, setShowEdit] = useState<boolean>()
const [showEditCompute, setShowEditCompute] = useState<boolean>() const [showEditCompute, setShowEditCompute] = useState<boolean>()
const { ddo, price, metadata } = useAsset() const [isOwner, setIsOwner] = useState(false)
const { ddo, price, metadata, type } = useAsset()
const isOwner = accountId === owner
useEffect(() => { useEffect(() => {
if (!price) return if (!accountId || !owner) return
const isOwner = accountId.toLowerCase() === owner.toLowerCase()
setIsOwner(isOwner)
setShowPricing(isOwner && price.type === '') setShowPricing(isOwner && price.type === '')
}, [isOwner, price]) }, [accountId, price, owner])
function handleEditButton() { function handleEditButton() {
// move user's focus to top of screen // move user's focus to top of screen
@ -101,7 +103,7 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
<Button style="text" size="small" onClick={handleEditButton}> <Button style="text" size="small" onClick={handleEditButton}>
Edit Metadata Edit Metadata
</Button> </Button>
{ddo.findServiceByType('compute') && ( {ddo.findServiceByType('compute') && type === 'dataset' && (
<> <>
<span className={styles.separator}>|</span> <span className={styles.separator}>|</span>
<Button <Button

View File

@ -2,3 +2,7 @@
composes: asset from './Details.module.css'; composes: asset from './Details.module.css';
border-bottom-left-radius: var(--border-radius) !important; border-bottom-left-radius: var(--border-radius) !important;
} }
.help {
margin-top: calc(var(--spacer) / 3);
}

View File

@ -6,12 +6,33 @@ import { ListItem } from '../../../atoms/Lists'
import Button from '../../../atoms/Button' import Button from '../../../atoms/Button'
import { useOcean } from '../../../../providers/Ocean' import { useOcean } from '../../../../providers/Ocean'
import styles from './Results.module.css' import styles from './Results.module.css'
import FormHelp from '../../../atoms/Input/Help'
import { graphql, useStaticQuery } from 'gatsby'
export const contentQuery = graphql`
query HistoryPageComputeResultsQuery {
content: allFile(filter: { relativePath: { eq: "pages/history.json" } }) {
edges {
node {
childPagesJson {
compute {
storage
}
}
}
}
}
}
`
export default function Results({ export default function Results({
job job
}: { }: {
job: ComputeJobMetaData job: ComputeJobMetaData
}): ReactElement { }): ReactElement {
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childPagesJson
const { ocean, account } = useOcean() const { ocean, account } = useOcean()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [hasFetched, setHasFetched] = useState(false) const [hasFetched, setHasFetched] = useState(false)
@ -85,6 +106,7 @@ export default function Results({
)} )}
</Button> </Button>
)} )}
<FormHelp className={styles.help}>{content.compute.storage}</FormHelp>
</div> </div>
) )
} }

View File

@ -62,12 +62,11 @@ export default function FormPublish(): ReactElement {
const [selectedDockerImage, setSelectedDockerImage] = useState<string>( const [selectedDockerImage, setSelectedDockerImage] = useState<string>(
initialValues.dockerImage initialValues.dockerImage
) )
// reset form validation on every mount // reset form validation on every mount
useEffect(() => { useEffect(() => {
setErrors({}) setErrors({})
setTouched({}) setTouched({})
// setSubmitting(false)
}, [setErrors, setTouched]) }, [setErrors, setTouched])
function handleImageSelectChange(imageSelected: string) { function handleImageSelectChange(imageSelected: string) {

View File

@ -67,8 +67,11 @@ export default function FormPublish(): ReactElement {
e: ChangeEvent<HTMLInputElement>, e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps field: FormFieldProps
) { ) {
const value =
field.type === 'terms' ? !JSON.parse(e.target.value) : e.target.value
validateField(field.name) validateField(field.name)
setFieldValue(field.name, e.target.value) setFieldValue(field.name, value)
} }
const resetFormAndClearStorage = (e: FormEvent<Element>) => { const resetFormAndClearStorage = (e: FormEvent<Element>) => {

View File

@ -95,9 +95,22 @@ export default function PublishPage({
const [publishType, setPublishType] = useState<MetadataMain['type']>( const [publishType, setPublishType] = useState<MetadataMain['type']>(
'dataset' 'dataset'
) )
const hasFeedback = isLoading || error || success const hasFeedback = isLoading || error || success
const emptyAlgoDT = Object.values(algoInitialValues.dataTokenOptions).every(
(value) => value === ''
)
const emptyDatasetDT = Object.values(
datasetInitialValues.dataTokenOptions
).every((value) => value === '')
if (emptyAlgoDT) {
algoInitialValues.dataTokenOptions = datasetInitialValues.dataTokenOptions
} else {
if (emptyDatasetDT)
datasetInitialValues.dataTokenOptions = algoInitialValues.dataTokenOptions
}
useEffect(() => { useEffect(() => {
publishType === 'dataset' publishType === 'dataset'
? setTitle('Publishing Data Set') ? setTitle('Publishing Data Set')
@ -160,18 +173,22 @@ export default function PublishPage({
): Promise<void> { ): Promise<void> {
const metadata = transformPublishAlgorithmFormToMetadata(values) const metadata = transformPublishAlgorithmFormToMetadata(values)
const timeout = mapTimeoutStringToSeconds(values.timeout) const timeout = mapTimeoutStringToSeconds(values.timeout)
const validDockerImage =
values.dockerImage === 'custom image' // TODO: put back check once #572 is resolved
? await validateDockerImage(values.image, values.containerTag) // https://github.com/oceanprotocol/market/issues/572
: true const validDockerImage = true
// const validDockerImage =
// values.dockerImage === 'custom image'
// ? await validateDockerImage(values.image, values.containerTag)
// : true
try { try {
if (validDockerImage) { if (validDockerImage) {
Logger.log('Publish Algorithm with ', metadata) Logger.log('Publish algorithm with ', metadata, values.dataTokenOptions)
const ddo = await publish( const ddo = await publish(
(metadata as unknown) as Metadata, (metadata as unknown) as Metadata,
values.algorithmPrivacy === true ? 'compute' : 'access', values.algorithmPrivacy === true ? 'compute' : 'access',
undefined, values.dataTokenOptions,
timeout timeout
) )
@ -248,7 +265,9 @@ export default function PublishPage({
loading={publishStepText} loading={publishStepText}
setError={setError} setError={setError}
successAction={{ successAction={{
name: 'Go to data set →', name: `Go to ${
publishType === 'dataset' ? 'data set' : 'algorithm'
} `,
to: `/asset/${did}` to: `/asset/${did}`
}} }}
/> />

View File

@ -1,160 +0,0 @@
import { useState } from 'react'
import { Logger, ServiceCompute } from '@oceanprotocol/lib'
import { MetadataAlgorithm } from '@oceanprotocol/lib/dist/node/ddo/interfaces/MetadataAlgorithm'
import {
ComputeJob,
ComputeAlgorithm
} from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
import { computeFeedback } from '../utils/feedback'
import { useOcean } from '../providers/Ocean'
import { useWeb3 } from '../providers/Web3'
interface ComputeValue {
entrypoint: string
image: string
tag: string
}
interface ComputeOption {
name: string
value: ComputeValue
}
const computeOptions: ComputeOption[] = [
{
name: 'nodejs',
value: {
entrypoint: 'node $ALGO',
image: 'node',
tag: '10'
}
},
{
name: 'python3.7',
value: {
entrypoint: 'python $ALGO',
image: 'oceanprotocol/algo_dockers',
tag: 'python-panda'
}
}
]
interface UseCompute {
compute: (
did: string,
computeService: ServiceCompute,
dataTokenAddress: string,
algorithmRawCode: string,
computeContainer: ComputeValue,
marketFeeAddress?: string,
orderId?: string
) => Promise<ComputeJob | void>
computeStep?: number
computeStepText?: string
computeError?: string
isLoading: boolean
}
const rawAlgorithmMeta: MetadataAlgorithm = {
rawcode: `console.log('Hello world'!)`,
format: 'docker-image',
version: '0.1',
container: {
entrypoint: '',
image: '',
tag: ''
}
}
function useCompute(): UseCompute {
const { accountId } = useWeb3()
const { ocean, account } = useOcean()
const [computeStep, setComputeStep] = useState<number | undefined>()
const [computeStepText, setComputeStepText] = useState<string | undefined>()
const [computeError, setComputeError] = useState<string | undefined>()
const [isLoading, setIsLoading] = useState(false)
function setStep(index?: number) {
if (!index) {
setComputeStep(undefined)
setComputeStepText(undefined)
return
}
setComputeStep(index)
setComputeStepText(computeFeedback[index])
}
async function compute(
did: string,
computeService: ServiceCompute,
dataTokenAddress: string,
algorithmRawCode: string,
computeContainer: ComputeValue,
marketFeeAddress?: string,
orderId?: string
): Promise<ComputeJob | void> {
if (!ocean || !account) return
setComputeError(undefined)
try {
setIsLoading(true)
setStep(0)
rawAlgorithmMeta.container = computeContainer
rawAlgorithmMeta.rawcode = algorithmRawCode
const computeAlgorithm: ComputeAlgorithm = {
meta: rawAlgorithmMeta
}
const output = {}
if (!orderId) {
const userOwnedTokens = await ocean.accounts.getTokenBalance(
dataTokenAddress,
account
)
if (parseFloat(userOwnedTokens) < 1) {
setComputeError('Not enough datatokens')
} else {
Logger.log(
'compute order',
accountId,
did,
computeService,
rawAlgorithmMeta,
marketFeeAddress
)
orderId = await ocean.compute.orderAsset(
accountId,
did,
computeService.index,
computeAlgorithm,
marketFeeAddress
)
setStep(1)
}
}
setStep(2)
if (orderId) {
const response = await ocean.compute.start(
did,
orderId,
dataTokenAddress,
account,
computeAlgorithm,
output,
`${computeService.index}`,
computeService.type
)
return response
}
} catch (error) {
Logger.error(error)
setComputeError(error.message)
} finally {
setStep(undefined)
setIsLoading(false)
}
}
return { compute, computeStep, computeStepText, computeError, isLoading }
}
export { useCompute, UseCompute, ComputeValue, ComputeOption, computeOptions }
export default UseCompute

View File

@ -77,6 +77,7 @@ export function useGraphSyncStatus(): UseGraphSyncStatus {
// Get and set subgraph block // Get and set subgraph block
useEffect(() => { useEffect(() => {
if (!config || !config.subgraphUri) return if (!config || !config.subgraphUri) return
async function initBlockSubgraph() { async function initBlockSubgraph() {
setSubgraphLoading(true) setSubgraphLoading(true)
const blockGraph = await getBlockSubgraph(config.subgraphUri) const blockGraph = await getBlockSubgraph(config.subgraphUri)
@ -98,7 +99,7 @@ export function useGraphSyncStatus(): UseGraphSyncStatus {
return return
} }
setIsGraphSynced(true) setIsGraphSynced(true)
}, [blockGraph, blockHead]) }, [blockGraph, blockHead, web3Loading, subgraphLoading])
return { blockHead, blockGraph, isGraphSynced } return { blockHead, blockGraph, isGraphSynced }
} }

View File

@ -82,36 +82,7 @@ function usePublish(): UsePublish {
} }
case 'compute': { case 'compute': {
if (!timeout) timeout = 3600 if (!timeout) timeout = 3600
const cluster = ocean.compute.createClusterAttributes( const provider = {}
'Kubernetes',
'http://10.0.0.17/xxx'
)
const servers = [
ocean.compute.createServerAttributes(
'1',
'xlsize',
'50',
'16',
'0',
'128gb',
'160gb',
timeout
)
]
const containers = [
ocean.compute.createContainerAttributes(
'tensorflow/tensorflow',
'latest',
'sha256:cb57ecfa6ebbefd8ffc7f75c0f00e57a7fa739578a429b6f72a0df19315deadc'
)
]
const provider = ocean.compute.createProviderAttributes(
'Azure',
'Compute service with 16gb ram for each node.',
cluster,
containers,
servers
)
const origComputePrivacy: ServiceComputePrivacy = { const origComputePrivacy: ServiceComputePrivacy = {
allowRawAlgorithm: false, allowRawAlgorithm: false,
allowNetworkAccess: false, allowNetworkAccess: false,

View File

@ -11,6 +11,12 @@ export const validationSchema: Yup.SchemaOf<MetadataPublishFormAlgorithm> = Yup.
description: Yup.string().min(10).required('Required'), description: Yup.string().min(10).required('Required'),
files: Yup.array<FileMetadata>().required('Required').nullable(), files: Yup.array<FileMetadata>().required('Required').nullable(),
timeout: Yup.string().required('Required'), timeout: Yup.string().required('Required'),
dataTokenOptions: Yup.object()
.shape({
name: Yup.string(),
symbol: Yup.string()
})
.required('Required'),
dockerImage: Yup.string() dockerImage: Yup.string()
.matches(/node:latest|python:latest|custom image/g, { .matches(/node:latest|python:latest|custom image/g, {
excludeEmptyString: true excludeEmptyString: true
@ -31,6 +37,10 @@ export const validationSchema: Yup.SchemaOf<MetadataPublishFormAlgorithm> = Yup.
export const initialValues: Partial<MetadataPublishFormAlgorithm> = { export const initialValues: Partial<MetadataPublishFormAlgorithm> = {
name: '', name: '',
author: '', author: '',
dataTokenOptions: {
name: '',
symbol: ''
},
dockerImage: 'node:latest', dockerImage: 'node:latest',
image: 'node', image: 'node',
containerTag: 'latest', containerTag: 'latest',

View File

@ -1,4 +1,4 @@
import { MetadataMarket, MetadataPublishFormDataset } from '../@types/MetaData' import { MetadataMarket, MetadataEditForm } from '../@types/MetaData'
import { secondsToString } from '../utils/metadata' import { secondsToString } from '../utils/metadata'
import { EditableMetadataLinks } from '@oceanprotocol/lib' import { EditableMetadataLinks } from '@oceanprotocol/lib'
import * as Yup from 'yup' import * as Yup from 'yup'
@ -8,17 +8,20 @@ export const validationSchema = Yup.object().shape({
.min(4, (param) => `Title must be at least ${param.min} characters`) .min(4, (param) => `Title must be at least ${param.min} characters`)
.required('Required'), .required('Required'),
description: Yup.string().required('Required').min(10), description: Yup.string().required('Required').min(10),
price: Yup.number().required('Required'),
links: Yup.array<EditableMetadataLinks[]>().nullable(), links: Yup.array<EditableMetadataLinks[]>().nullable(),
timeout: Yup.string().required('Required') timeout: Yup.string().required('Required')
}) })
export function getInitialValues( export function getInitialValues(
metadata: MetadataMarket, metadata: MetadataMarket,
timeout: number timeout: number,
): Partial<MetadataPublishFormDataset> { price: number
): Partial<MetadataEditForm> {
return { return {
name: metadata.main.name, name: metadata.main.name,
description: metadata.additionalInformation.description, description: metadata.additionalInformation.description,
price,
links: metadata.additionalInformation.links, links: metadata.additionalInformation.links,
timeout: secondsToString(timeout) timeout: secondsToString(timeout)
} }

View File

@ -20,7 +20,7 @@ export default function PageGatsbySearch(props: PageProps): ReactElement {
? `Published by ${accountTruncate(owner as string)}` ? `Published by ${accountTruncate(owner as string)}`
: `${ : `${
totalResults !== undefined totalResults !== undefined
? searchValue ? searchValue && searchValue !== ' '
? totalResults === 0 ? totalResults === 0
? 'No results' ? 'No results'
: totalResults + : totalResults +

View File

@ -16,7 +16,10 @@ export function getOceanConfig(
): ConfigHelperConfig { ): ConfigHelperConfig {
return new ConfigHelper().getConfig( return new ConfigHelper().getConfig(
network, network,
network === 'polygon' || network === 137 network === 'polygon' ||
network === 137 ||
network === 'moonbeamalpha' ||
network === 1287
? undefined ? undefined
: process.env.GATSBY_INFURA_PROJECT_ID : process.env.GATSBY_INFURA_PROJECT_ID
) as ConfigHelperConfig ) as ConfigHelperConfig

View File

@ -35,7 +35,7 @@ export async function fileinfo(
return return
} else { } else {
toast.dismiss() // Remove any existing error message toast.dismiss() // Remove any existing error message
toast.success('Great! That dataset looks good 🐳', { toast.success('Great! That file looks good. 🐳', {
position: 'bottom-right', position: 'bottom-right',
autoClose: 5000, autoClose: 5000,
hideProgressBar: false, hideProgressBar: false,