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

Merge branch 'main' into feature/multinetwork

This commit is contained in:
Matthias Kretschmann 2021-06-14 15:15:53 +02:00
commit 30174e1a84
Signed by: m
GPG Key ID: 606EEEF3C479A91F
28 changed files with 843 additions and 59 deletions

View File

@ -10,4 +10,6 @@ GATSBY_NETWORK="rinkeby"
#GATSBY_ANALYTICS_ID="xxx"
#GATSBY_PORTIS_ID="xxx"
#GATSBY_ALLOW_FIXED_PRICING="true"
#GATSBY_ALLOW_DYNAMIC_PRICING="true"
#GATSBY_ALLOW_DYNAMIC_PRICING="true"
#GATSBY_ALLOW_ADVANCED_SETTINGS="true"
#GATSBY_CREDENTIAL_TYPE="address"

View File

@ -50,5 +50,9 @@ module.exports = {
// Used to show or hide the fixed and dynamic price options
// tab to publishers during the price creation.
allowFixedPricing: process.env.GATSBY_ALLOW_FIXED_PRICING || 'true',
allowDynamicPricing: process.env.GATSBY_ALLOW_DYNAMIC_PRICING || 'true'
allowDynamicPricing: process.env.GATSBY_ALLOW_DYNAMIC_PRICING || 'true',
// Used to show or hide advanced settings button in asset details page
allowAdvancedSettings: process.env.GATSBY_ALLOW_ADVANCED_SETTINGS || 'false',
credentialType: process.env.GATSBY_CREDENTIAL_TYPE || 'address'
}

View File

@ -0,0 +1,31 @@
{
"description": "Update advanced settings of this data set. Updating these settings will create an on-chain transaction you have to approve in your wallet.",
"form": {
"success": "🎉 Successfully updated. 🎉",
"successAction": "Close",
"error": "Updating DDO failed.",
"data": [
{
"name": "allow",
"label": "Allow ETH Address",
"placeholder": "e.g. 0x12345678901234567890abcd",
"help": "Enter ETH address and click ADD button to append the list. Only ETH address in allow list can consume this asset. If the list is empty means anyone can download or compute this asset",
"type": "credentials"
},
{
"name": "deny",
"label": "Deny ETH Address",
"placeholder": "e.g. 0x12345678901234567890abcd",
"help": "Enter ETH address and click ADD button to append the list. If ETH address is fall under deny list, download or compute of this asset is denied",
"type": "credentials"
},
{
"name": "isOrderDisabled",
"label": "Disable Consumption",
"help": "Disable dataset being download or compute when dataset undergoing maintenance.",
"type": "checkbox",
"options": ["Disable"]
}
]
}
}

7
package-lock.json generated
View File

@ -43525,7 +43525,6 @@
"node-abort-controller": "^2.0.0",
"save-file": "^2.3.1",
"uuid": "^8.3.2",
"web3": "^1.3.5",
"web3-eth-contract": "^1.3.6"
}
},
@ -43600,7 +43599,6 @@
"integrity": "sha512-5vwpq6kbvwkQwKqAoOU3L72GZ3Ta8RRrewKj9OJRolx28KLJJ8Dg9Rf7obRwt5jQA9bkYd8gqzMTrI7H3xLfaw==",
"dev": true,
"requires": {
"@oclif/config": "^1.15.1",
"@oclif/errors": "^1.3.3",
"@oclif/parser": "^3.8.3",
"@oclif/plugin-help": "^3",
@ -65003,11 +65001,6 @@
"clsx": "^1.1.1"
}
},
"reactjs-popup": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.4.tgz",
"integrity": "sha512-G5jTXL2JkClKAYAdqedf+K9QvbNsFWvdbrXW1/vWiyanuCU/d7DtQzQux+uKOz2HeNVRsFQHvs7abs0Z7VLAhg=="
},
"read": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",

View File

@ -12,6 +12,7 @@ import classNames from 'classnames/bind'
import AssetSelection, {
AssetSelectionAsset
} from '../../molecules/FormFields/AssetSelection'
import Credentials from '../../molecules/FormFields/Credential'
const cx = classNames.bind(styles)
@ -137,6 +138,8 @@ export default function InputElement({
{...props}
/>
)
case 'credentials':
return <Credentials name={name} {...field} {...props} />
default:
return prefix || postfix ? (
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>

View File

@ -0,0 +1,59 @@
.display {
composes: selection from './FormFields/AssetSelection.module.css';
}
.display [class*='loaderWrap'] {
margin: calc(var(--spacer) / 3);
}
.scroll {
composes: scroll from './FormFields/AssetSelection.module.css';
margin-top: 0;
border-top: none;
width: 100%;
}
.row {
composes: row from './FormFields/AssetSelection.module.css';
}
.row:last-child {
border-bottom: none;
}
.row:first-child {
border-top: none;
}
.row:hover {
background-color: var(--background-content);
}
.info {
display: block;
width: 100%;
}
.title {
composes: title from './FormFields/AssetSelection.module.css';
}
.hover:hover {
color: var(--color-primary);
}
.price {
composes: price from './FormFields/AssetSelection.module.css';
}
.price [class*='symbol'] {
font-size: calc(var(--font-size-small) / 1.2) !important;
}
.did {
composes: did from './FormFields/AssetSelection.module.css';
}
.empty {
composes: empty from './FormFields/AssetSelection.module.css';
}

View File

@ -0,0 +1,49 @@
import React from 'react'
import Dotdotdot from 'react-dotdotdot'
import { Link } from 'gatsby'
import PriceUnit from '../atoms/Price/PriceUnit'
import Loader from '../atoms/Loader'
import styles from './AssetComputeList.module.css'
import { AssetSelectionAsset } from './FormFields/AssetSelection'
function Empty() {
return <div className={styles.empty}>No assets found.</div>
}
export default function AssetComputeSelection({
assets
}: {
assets: AssetSelectionAsset[]
}): JSX.Element {
return (
<div className={styles.display}>
<div className={styles.scroll}>
{!assets ? (
<Loader />
) : assets && !assets.length ? (
<Empty />
) : (
assets.map((asset: AssetSelectionAsset) => (
<Link
to={`/asset/${asset.did}`}
className={styles.row}
key={asset.did}
>
<div className={styles.info}>
<h3 className={styles.title}>
<Dotdotdot clamp={1} tagName="span">
{asset.name}
</Dotdotdot>
</h3>
<Dotdotdot clamp={1} tagName="code" className={styles.did}>
{asset.symbol} | {asset.did}
</Dotdotdot>
</div>
<PriceUnit price={asset.price} small className={styles.price} />
</Link>
))
)}
</div>
</div>
)
}

View File

@ -0,0 +1,40 @@
.chip {
border: 1px solid var(--border-color);
display: flex;
padding-left: 10px;
}
.buttonWrapper {
width: 100%;
text-align: right;
}
.crossButton {
min-width: 0;
}
.crossButton svg {
display: inline-block;
width: var(--font-size-large);
height: var(--font-size-large);
fill: var(--brand-pink);
vertical-align: middle;
}
.scroll {
border-top: 1px solid var(--border-color);
min-height: fit-content;
max-height: 200px;
position: relative;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.credential {
padding: 0;
border: 1px solid var(--border-color);
background-color: var(--background-highlight);
border-radius: var(--border-radius);
font-size: var(--font-size-small);
min-height: 200px;
}

View File

@ -0,0 +1,81 @@
import { useField } from 'formik'
import { InputProps } from '../../../atoms/Input'
import React, { useState, ChangeEvent, FormEvent, useEffect } from 'react'
import InputGroup from '../../../atoms/Input/InputGroup'
import Button from '../../../atoms/Button'
import styles from './Credential.module.css'
import { isAddress } from 'web3-utils'
import { toast } from 'react-toastify'
import { ReactComponent as Cross } from '../../../../images/cross.svg'
import InputElement from '../../../atoms/Input/InputElement'
export default function Credentials(props: InputProps) {
const [field, meta, helpers] = useField(props.name)
const [arrayInput, setArrayInput] = useState<string[]>(field.value || [])
const [value, setValue] = useState('')
useEffect(() => {
helpers.setValue(arrayInput)
}, [arrayInput])
function handleDeleteChip(value: string) {
const newInput = arrayInput.filter((input) => input !== value)
setArrayInput(newInput)
helpers.setValue(newInput)
}
function handleAddValue(e: FormEvent<HTMLButtonElement>) {
e.preventDefault()
if (!isAddress(value)) {
toast.error('Wallet address is invalid')
return
}
if (arrayInput.includes(value)) {
toast.error('Wallet address already added into list')
return
}
setArrayInput((arrayInput) => [...arrayInput, value])
setValue('')
}
return (
<div className={styles.credential}>
<InputGroup>
<InputElement
type="text"
name="address"
size="default"
placeholder={props.placeholder}
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setValue(e.target.value)
}
/>
<Button
onClick={(e: FormEvent<HTMLButtonElement>) => handleAddValue(e)}
>
Add
</Button>
</InputGroup>
<div className={styles.scroll}>
{arrayInput &&
arrayInput.map((value) => {
return (
<div className={styles.chip} key={value}>
<code>{value}</code>
<span className={styles.buttonWrapper}>
<Button
className={styles.crossButton}
style="text"
onClick={(even) => handleDeleteChip(value)}
>
<Cross />
</Button>
</span>
</div>
)
})}
</div>
</div>
)
}

View File

@ -28,7 +28,6 @@ import {
ComputeAlgorithm,
ComputeOutput
} from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
import { SearchQuery } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import axios from 'axios'
import FormStartComputeDataset from './FormComputeDataset'
@ -36,6 +35,8 @@ import styles from './index.module.css'
import SuccessConfetti from '../../../atoms/SuccessConfetti'
import Button from '../../../atoms/Button'
import { secondsToString } from '../../../../utils/metadata'
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
import AlgorithmDatasetsListForCompute from '../../AssetContent/AlgorithmDatasetsListForCompute'
import { getPreviousOrders, getPrice } from '../../../../utils/subgraph'
const SuccessAction = () => (
@ -361,6 +362,8 @@ export default function Compute({
await checkPreviousOrders(ddo)
setIsPublished(true)
} catch (error) {
await checkPreviousOrders(selectedAlgorithmAsset)
await checkPreviousOrders(ddo)
setError('Failed to start job!')
Logger.error('[compute] Failed to start job: ', error.message)
} finally {
@ -376,10 +379,13 @@ export default function Compute({
</div>
{type === 'algorithm' ? (
<Alert
text="This algorithm has been set to private by the publisher and can't be downloaded. You can run it against any allowed data sets though!"
state="info"
/>
<>
<Alert
text="This algorithm has been set to private by the publisher and can't be downloaded. You can run it against any allowed data sets though!"
state="info"
/>
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} />
</>
) : (
<Formik
initialValues={getInitialValues()}

View File

@ -5,7 +5,10 @@
.info {
display: flex;
width: 100%;
width: auto;
margin-left: -2rem;
margin-right: -2rem;
padding: 0 calc(var(--spacer)) 0 calc(var(--spacer));
}
.filewrapper {

View File

@ -3,10 +3,8 @@ import { toast } from 'react-toastify'
import { File as FileMetadata, DDO } from '@oceanprotocol/lib'
import File from '../../atoms/File'
import Price from '../../atoms/Price'
import styles from './Consume.module.css'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import { useAsset } from '../../../providers/Asset'
import { secondsToString } from '../../../utils/metadata'
import { gql, useQuery } from '@apollo/client'
import { OrdersData } from '../../../@types/apollo/OrdersData'
import BigNumber from 'bignumber.js'
@ -15,6 +13,9 @@ import { useWeb3 } from '../../../providers/Web3'
import { usePricing } from '../../../hooks/usePricing'
import { useConsume } from '../../../hooks/useConsume'
import ButtonBuy from '../../atoms/ButtonBuy'
import AlgorithmDatasetsListForCompute from '../AssetContent/AlgorithmDatasetsListForCompute'
import Web3Feedback from '../../molecules/Web3Feedback'
import styles from './Consume.module.css'
const previousOrderQuery = gql`
query PreviousOrder($id: String!, $account: String!) {
@ -51,12 +52,11 @@ export default function Consume({
const { isInPurgatory, price, type, isAssetNetwork } = useAsset()
const { buyDT, pricingStepText, pricingError, pricingIsLoading } =
usePricing()
const { consumeStepText, consume, consumeError } = useConsume()
const { consumeStepText, consume, consumeError, isLoading } = useConsume()
const [isDisabled, setIsDisabled] = useState(true)
const [hasDatatoken, setHasDatatoken] = useState(false)
const [isConsumable, setIsConsumable] = useState(true)
const [assetTimeout, setAssetTimeout] = useState('')
const { data } = useQuery<OrdersData>(previousOrderQuery, {
variables: {
id: ddo.dataToken?.toLowerCase(),
@ -87,7 +87,7 @@ export default function Consume({
useEffect(() => {
const { timeout } = ddo.findServiceByType('access').attributes.main
setAssetTimeout(secondsToString(timeout))
setAssetTimeout(timeout.toString())
}, [ddo])
useEffect(() => {
@ -125,22 +125,28 @@ export default function Consume({
])
async function handleConsume() {
!hasPreviousOrder && !hasDatatoken && (await buyDT('1', price, ddo))
await consume(
if (!hasPreviousOrder && !hasDatatoken) {
const tx = await buyDT('1', price, ddo)
if (tx === undefined) return
}
const error = await consume(
ddo.id,
ddo.dataToken,
'access',
appConfig.marketFeeAddress,
previousOrderId
)
setHasPreviousOrder(true)
error || setHasPreviousOrder(true)
}
// Output errors in UI
useEffect(() => {
consumeError && toast.error(consumeError)
}, [consumeError])
useEffect(() => {
pricingError && toast.error(pricingError)
}, [consumeError, pricingError])
}, [pricingError])
const PurchaseButton = () => (
<ButtonBuy
@ -154,7 +160,7 @@ export default function Consume({
assetTimeout={assetTimeout}
assetType={type}
stepText={consumeStepText || pricingStepText}
isLoading={pricingIsLoading}
isLoading={pricingIsLoading || isLoading}
/>
)
@ -169,6 +175,12 @@ export default function Consume({
{!isInPurgatory && <PurchaseButton />}
</div>
</div>
{type === 'algorithm' && (
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} />
)}
<footer className={styles.feedback}>
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
</footer>
</aside>
)
}

View File

@ -0,0 +1,48 @@
import { DDO, Credentials, CredentialType } from '@oceanprotocol/lib'
import React, { ReactElement, useEffect, useState } from 'react'
import { AdvancedSettingsForm } from '../../../../models/FormEditCredential'
import { useOcean } from '../../../../providers/Ocean'
import DebugOutput from '../../../atoms/DebugOutput'
export interface AdvancedSettings {
credentail: Credentials
isOrderDisabled: boolean
}
export default function DebugEditCredential({
values,
ddo,
credentialType
}: {
values: AdvancedSettingsForm
ddo: DDO
credentialType: CredentialType
}): ReactElement {
const { ocean } = useOcean()
const [advancedSettings, setAdvancedSettings] = useState<AdvancedSettings>()
useEffect(() => {
if (!ocean) return
async function transformValues() {
const newDdo = await ocean.assets.updateCredentials(
ddo,
credentialType,
values.allow,
values.deny
)
setAdvancedSettings({
credentail: newDdo.credentials,
isOrderDisabled: values.isOrderDisabled
})
}
transformValues()
}, [values, ddo, ocean])
return (
<>
<DebugOutput title="Collected Form Values" output={values} />
<DebugOutput title="Transformed Form Values" output={advancedSettings} />
</>
)
}

View File

@ -0,0 +1,163 @@
import { Formik } from 'formik'
import React, { ReactElement, useState } from 'react'
import { useAsset } from '../../../../providers/Asset'
import { useUserPreferences } from '../../../../providers/UserPreferences'
import styles from './index.module.css'
import { Logger, CredentialType, DDO } from '@oceanprotocol/lib'
import MetadataFeedback from '../../../molecules/MetadataFeedback'
import { graphql, useStaticQuery } from 'gatsby'
import { useWeb3 } from '../../../../providers/Web3'
import { useOcean } from '../../../../providers/Ocean'
import FormAdvancedSettings from './FormAdvancedSettings'
import {
AdvancedSettingsForm,
getInitialValues,
validationSchema
} from '../../../../models/FormEditCredential'
import DebugEditCredential from './DebugEditAdvancedSettings'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
const contentQuery = graphql`
query EditAvanceSettingsQuery {
content: allFile(
filter: { relativePath: { eq: "pages/editAdvancedSettings.json" } }
) {
edges {
node {
childPagesJson {
description
form {
success
successAction
error
data {
name
placeholder
label
help
type
options
}
}
}
}
}
}
}
`
function getDefaultCredentialType(credentialType: string): CredentialType {
switch (credentialType) {
case 'address':
return CredentialType.address
case 'credential3Box':
return CredentialType.credential3Box
default:
return CredentialType.address
}
}
export default function EditAdvancedSettings({
setShowEdit
}: {
setShowEdit: (show: boolean) => void
}): ReactElement {
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childPagesJson
const { debug } = useUserPreferences()
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { metadata, ddo, refreshDdo } = useAsset()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const { appConfig } = useSiteMetadata()
const hasFeedback = error || success
const credentialType = getDefaultCredentialType(appConfig.credentialType)
async function handleSubmit(
values: Partial<AdvancedSettingsForm>,
resetForm: () => void
) {
try {
let newDdo: DDO
newDdo = await ocean.assets.updateCredentials(
ddo,
credentialType,
values.allow,
values.deny
)
newDdo = await ocean.assets.editMetadata(newDdo, {
status: {
isOrderDisabled: values.isOrderDisabled
}
})
const storedddo = await ocean.assets.updateMetadata(newDdo, accountId)
if (!storedddo) {
setError(content.form.error)
Logger.error(content.form.error)
return
} else {
setSuccess(content.form.success)
resetForm()
}
} catch (error) {
Logger.error(error.message)
setError(error.message)
}
}
return (
<Formik
initialValues={getInitialValues(ddo, credentialType)}
validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
await handleSubmit(values, resetForm)
}}
>
{({ isSubmitting, values }) =>
isSubmitting || hasFeedback ? (
<MetadataFeedback
title="Updating Data Set"
error={error}
success={success}
setError={setError}
successAction={{
name: content.form.successAction,
onClick: async () => {
await refreshDdo()
setShowEdit(false)
}
}}
/>
) : (
<>
<p className={styles.description}>{content.description}</p>
<article className={styles.grid}>
<FormAdvancedSettings
data={content.form.data}
setShowEdit={setShowEdit}
/>
</article>
{debug === true && (
<div className={styles.grid}>
<DebugEditCredential
values={values}
ddo={ddo}
credentialType={credentialType}
/>
</div>
)}
</>
)
}
</Formik>
)
}

View File

@ -0,0 +1,59 @@
import React, { ChangeEvent, ReactElement } from 'react'
import styles from './FormEditMetadata.module.css'
import { Field, Form, FormikContextType, useFormikContext } from 'formik'
import Button from '../../../atoms/Button'
import Input from '../../../atoms/Input'
import { FormFieldProps } from '../../../../@types/Form'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import { AdvancedSettingsForm } from '../../../../models/FormEditCredential'
export default function FormAdvancedSettings({
data,
setShowEdit
}: {
data: FormFieldProps[]
setShowEdit: (show: boolean) => void
}): ReactElement {
const { accountId } = useWeb3()
const { ocean, config } = useOcean()
const {
isValid,
validateField,
setFieldValue
}: FormikContextType<Partial<AdvancedSettingsForm>> = useFormikContext()
function handleFieldChange(
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps
) {
validateField(field.name)
if (e.target.type === 'checkbox')
setFieldValue(field.name, e.target.checked)
else setFieldValue(field.name, e.target.value)
}
return (
<Form className={styles.form}>
{data.map((field: FormFieldProps) => (
<Field
key={field.name}
{...field}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
))}
<footer className={styles.actions}>
<Button style="primary" disabled={!ocean || !accountId || !isValid}>
Submit
</Button>
<Button style="text" onClick={() => setShowEdit(false)}>
Cancel
</Button>
</footer>
</Form>
)
}

View File

@ -0,0 +1,29 @@
.datasetsContainer {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
width: auto;
margin-left: -2rem;
margin-right: -2rem;
border-top: 1px solid var(--border-color);
margin-top: calc(var(--spacer) / 2);
}
.datasetsContainer div[class*='AssetSelection-module--selection'] {
width: 100%;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-left: 0;
border-right: 0;
padding: 0;
}
.datasetsContainer .text {
margin-bottom: calc(var(--spacer) / 2);
margin-top: calc(var(--spacer) / 2);
text-align: center;
color: var(--font-color-text);
font-size: var(--font-size-base);
font-family: var(--font-family-heading);
}

View File

@ -0,0 +1,36 @@
import React, { ReactElement, useEffect, useState } from 'react'
import styles from './AlgorithmDatasetsListForCompute.module.css'
import { getAlgorithmDatasetsForCompute } from '../../../utils/aquarius'
import { AssetSelectionAsset } from '../../molecules/FormFields/AssetSelection'
import AssetComputeList from '../../molecules/AssetComputeList'
import { useOcean } from '../../../providers/Ocean'
import { useAsset } from '../../../providers/Asset'
export default function AlgorithmDatasetsListForCompute({
algorithmDid
}: {
algorithmDid: string
}): ReactElement {
const { config } = useOcean()
const { type } = useAsset()
const [datasetsForCompute, setDatasetsForCompute] =
useState<AssetSelectionAsset[]>()
useEffect(() => {
async function getDatasetsAllowedForCompute() {
const datasets = await getAlgorithmDatasetsForCompute(
algorithmDid,
config.metadataCacheUri
)
setDatasetsForCompute(datasets)
}
type === 'algorithm' && getDatasetsAllowedForCompute()
}, [type])
return (
<div className={styles.datasetsContainer}>
<h3 className={styles.text}>Datasets algorithm is allowed to run on</h3>
<AssetComputeList assets={datasetsForCompute} />
</div>
)
}

View File

@ -6,12 +6,10 @@ import { useAsset } from '../../../providers/Asset'
export default function MetaFull(): ReactElement {
const { ddo, metadata, isInPurgatory, type } = useAsset()
const { algorithm } = ddo.findServiceByType('metadata').attributes.main
function DockerImage() {
const algorithmContainer =
ddo.findServiceByType('metadata').attributes.main.algorithm.container
const { image } = algorithmContainer
const { tag } = algorithmContainer
const { image, tag } = algorithm.container
return <span>{`${image}:${tag}`}</span>
}
@ -25,7 +23,7 @@ export default function MetaFull(): ReactElement {
content={<Publisher account={ddo?.publicKey[0].owner} />}
/>
{type === 'algorithm' && (
{type === 'algorithm' && algorithm && (
<MetaItem title="Docker Image" content={<DockerImage />} />
)}
<MetaItem title="DID" content={<code>{ddo?.id}</code>} />

View File

@ -17,6 +17,8 @@ import MetaMain from './MetaMain'
import EditHistory from './EditHistory'
import { useWeb3 } from '../../../providers/Web3'
import styles from './index.module.css'
import EditAdvancedSettings from '../AssetActions/Edit/EditAdvancedSettings'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
export interface AssetContentProps {
path?: string
@ -48,8 +50,11 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
const [showPricing, setShowPricing] = useState(false)
const [showEdit, setShowEdit] = useState<boolean>()
const [showEditCompute, setShowEditCompute] = useState<boolean>()
const [showEditAdvancedSettings, setShowEditAdvancedSettings] =
useState<boolean>()
const [isOwner, setIsOwner] = useState(false)
const { ddo, price, metadata, type } = useAsset()
const { appConfig } = useSiteMetadata()
useEffect(() => {
if (!accountId || !owner) return
@ -70,10 +75,17 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
setShowEditCompute(true)
}
function handleEditAdvancedSettingsButton() {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
setShowEditAdvancedSettings(true)
}
return showEdit ? (
<Edit setShowEdit={setShowEdit} />
) : showEditCompute ? (
<EditComputeDataset setShowEdit={setShowEditCompute} />
) : showEditAdvancedSettings ? (
<EditAdvancedSettings setShowEdit={setShowEditAdvancedSettings} />
) : (
<article className={styles.grid}>
<div>
@ -103,6 +115,18 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
<Button style="text" size="small" onClick={handleEditButton}>
Edit Metadata
</Button>
{appConfig.allowAdvancedSettings === 'true' && (
<>
<span className={styles.separator}>|</span>
<Button
style="text"
size="small"
onClick={handleEditAdvancedSettingsButton}
>
Edit Advanced Settings
</Button>
</>
)}
{ddo.findServiceByType('compute') && type === 'dataset' && (
<>
<span className={styles.separator}>|</span>

View File

@ -159,6 +159,8 @@ export default function PublishPage({
values: initialValues as MetadataPublishFormDataset,
status: 'empty'
})
// move user's focus to top of screen
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
} catch (error) {
setError(error.message)
Logger.error(error.message)
@ -173,14 +175,10 @@ export default function PublishPage({
): Promise<void> {
const metadata = transformPublishAlgorithmFormToMetadata(values)
const timeout = mapTimeoutStringToSeconds(values.timeout)
// TODO: put back check once #572 is resolved
// https://github.com/oceanprotocol/market/issues/572
const validDockerImage = true
// const validDockerImage =
// values.dockerImage === 'custom image'
// ? await validateDockerImage(values.image, values.containerTag)
// : true
const validDockerImage =
values.dockerImage === 'custom image'
? await validateDockerImage(values.image, values.containerTag)
: true
try {
if (validDockerImage) {
Logger.log('Publish algorithm with ', metadata, values.dataTokenOptions)
@ -208,6 +206,10 @@ export default function PublishPage({
values: initialValuesAlgorithm as MetadataPublishFormAlgorithm,
status: 'empty'
})
// move user's focus to top of screen
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
} else {
document.getElementById('image').scrollIntoView({ behavior: 'smooth' })
}
} catch (error) {
setError(error.message)
@ -228,8 +230,6 @@ export default function PublishPage({
: validationSchemaAlgorithm
}
onSubmit={async (values, { resetForm }) => {
// move user's focus to top of screen
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
// kick off publishing
publishType === 'dataset'
? await handleSubmit(values, resetForm)

View File

@ -11,7 +11,7 @@ export default function PageTemplateAssetDetails({
}: {
uri: string
}): ReactElement {
const { ddo, title, error, isInPurgatory } = useAsset()
const { ddo, title, error, isInPurgatory, loading } = useAsset()
const [pageTitle, setPageTitle] = useState<string>()
useEffect(() => {
@ -23,7 +23,7 @@ export default function PageTemplateAssetDetails({
setPageTitle(isInPurgatory ? '' : title)
}, [ddo, error, isInPurgatory, title])
return ddo && pageTitle !== undefined ? (
return ddo && pageTitle !== undefined && !loading ? (
<Page title={pageTitle} uri={uri}>
<Router basepath="/asset">
<AssetContent path=":did" />

View File

@ -11,7 +11,7 @@ interface UseConsume {
serviceType: ServiceType,
marketFeeAddress: string,
orderId?: string
) => Promise<void>
) => Promise<string>
consumeStep?: number
consumeStepText?: string
consumeError?: string
@ -37,7 +37,7 @@ function useConsume(): UseConsume {
serviceType: ServiceType = 'access',
marketFeeAddress: string,
orderId?: string
): Promise<void> {
): Promise<string> {
if (!ocean || !account || !accountId) return
setIsLoading(true)
@ -53,19 +53,25 @@ function useConsume(): UseConsume {
)
if (parseFloat(userOwnedTokens) < 1) {
setConsumeError('Not enough datatokens')
return 'Not enough datatokens'
} else {
setStep(1)
orderId = await ocean.assets.order(
did as string,
serviceType,
accountId,
undefined,
marketFeeAddress,
undefined,
false
)
Logger.log('order created', orderId)
setStep(2)
try {
orderId = await ocean.assets.order(
did as string,
serviceType,
accountId,
undefined,
marketFeeAddress,
undefined,
false
)
Logger.log('order created', orderId)
setStep(2)
} catch (error) {
setConsumeError(error.message)
return error.message
}
}
}
setStep(3)
@ -81,11 +87,13 @@ function useConsume(): UseConsume {
} catch (error) {
setConsumeError(error.message)
Logger.error(error)
return error.message
} finally {
setConsumeStep(undefined)
setConsumeStepText(undefined)
setIsLoading(false)
}
return undefined
}
return { consume, consumeStep, consumeStepText, consumeError, isLoading }

View File

@ -29,6 +29,8 @@ interface UseSiteMetadata {
portisId: string
allowFixedPricing: string
allowDynamicPricing: string
allowAdvancedSettings: string
credentialType: string
}
}
@ -63,6 +65,8 @@ const query = graphql`
portisId
allowFixedPricing
allowDynamicPricing
allowAdvancedSettings
credentialType
}
}
}

3
src/images/cross.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg>

After

Width:  |  Height:  |  Size: 364 B

View File

@ -0,0 +1,72 @@
import {
CredentialAction,
Credential,
Credentials,
CredentialType,
DDO
} from '@oceanprotocol/lib'
import * as Yup from 'yup'
export interface AdvancedSettingsForm {
allow: string[]
deny: string[]
isOrderDisabled: boolean
}
export const validationSchema: Yup.SchemaOf<AdvancedSettingsForm> =
Yup.object().shape({
allow: Yup.array().nullable(),
deny: Yup.array().nullable(),
isOrderDisabled: Yup.boolean().nullable()
})
function getCredentialList(
credential: Credential[],
credentialType: CredentialType
): string[] {
const credentialByType = credential.find(
(credential) => credential.type === credentialType
)
return credentialByType.value && credentialByType.value.length > 0
? credentialByType.value
: []
}
function getAssetCredentials(
credentials: Credentials,
credentialType: CredentialType,
credentialAction: CredentialAction
): string[] {
if (!credentials) return []
if (credentialAction === 'allow') {
return credentials.allow
? getCredentialList(credentials.allow, credentialType)
: []
}
return credentials.deny
? getCredentialList(credentials.deny, credentialType)
: []
}
export function getInitialValues(
ddo: DDO,
credentailType: CredentialType
): AdvancedSettingsForm {
const allowCredential = getAssetCredentials(
ddo.credentials,
credentailType,
'allow'
)
const denyCredential = getAssetCredentials(
ddo.credentials,
credentailType,
'deny'
)
const metadata = ddo.findServiceByType('metadata')
return {
allow: allowCredential,
deny: denyCredential,
isOrderDisabled: metadata.attributes?.status?.isOrderDisabled || false
}
}

View File

@ -32,6 +32,7 @@ interface AssetProviderValue {
error?: string
refreshInterval: number
isAssetNetwork: boolean
loading: boolean
refreshDdo: (token?: CancelToken) => Promise<void>
}
@ -59,10 +60,12 @@ function AssetProvider({
const [owner, setOwner] = useState<string>()
const [error, setError] = useState<string>()
const [type, setType] = useState<MetadataMain['type']>()
const [loading, setLoading] = useState<boolean>(false)
const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>()
const fetchDdo = async (token?: CancelToken) => {
Logger.log('[asset] Init asset, get DDO')
setLoading(true)
const ddo = await retrieveDDO(
asset as string,
appConfig.metadataCacheUri,
@ -76,13 +79,16 @@ function AssetProvider({
} else {
setError(undefined)
}
setLoading(false)
return ddo
}
const refreshDdo = async (token?: CancelToken) => {
setLoading(true)
const ddo = await fetchDdo(token)
Logger.debug('[asset] Got DDO', ddo)
setDDO(ddo)
setLoading(false)
}
//
@ -123,7 +129,7 @@ function AssetProvider({
const initMetadata = useCallback(async (ddo: DDO): Promise<void> => {
if (!ddo) return
setLoading(true)
const returnedPrice = await getPrice(ddo)
setPrice({ ...returnedPrice })
@ -137,6 +143,7 @@ function AssetProvider({
setIsInPurgatory(ddo.isInPurgatory === 'true')
await setPurgatory(ddo.id)
setLoading(false)
}, [])
useEffect(() => {
@ -169,6 +176,7 @@ function AssetProvider({
isInPurgatory,
purgatoryData,
refreshInterval,
loading,
refreshDdo,
isAssetNetwork
} as AssetProviderValue

View File

@ -14,6 +14,17 @@ import { PriceList, getAssetsPriceList } from './subgraph'
import axios, { CancelToken, AxiosResponse } from 'axios'
import { DDO_TEMPORARY } from '../providers/Ocean'
function getQueryForAlgorithmDatasets(algorithmDid: string) {
return {
query: {
query_string: {
query: `service.attributes.main.privacy.publisherTrustedAlgorithms.did:${algorithmDid}`
}
},
sort: { created: -1 }
}
}
// TODO: import directly from ocean.js somehow.
// Transforming Aquarius' direct response is needed for getting actual DDOs
// and not just strings of DDOs. For now, taken from
@ -151,3 +162,33 @@ export async function transformDDOToAssetSelection(
})
return algorithmList
}
export async function getAlgorithmDatasetsForCompute(
algorithmId: string,
metadataCacheUri: string
): Promise<AssetSelectionAsset[]> {
const source = axios.CancelToken.source()
const computeDatasets = await queryMetadata(
getQueryForAlgorithmDatasets(algorithmId),
metadataCacheUri,
source.token
)
const computeDatasetsForCurrentAlgorithm: DDO[] = []
computeDatasets.results.forEach((data: DDO) => {
const algorithm = data
.findServiceByType('compute')
.attributes.main.privacy.publisherTrustedAlgorithms.find(
(algo) => algo.did === algorithmId
)
algorithm && computeDatasetsForCurrentAlgorithm.push(data)
})
if (computeDatasetsForCurrentAlgorithm.length === 0) {
return []
}
const datasets = await transformDDOToAssetSelection(
computeDatasetsForCurrentAlgorithm,
metadataCacheUri,
[]
)
return datasets
}

View File

@ -136,10 +136,18 @@ async function isDockerHubImageValid(
tag: string
): Promise<boolean> {
try {
const response = await axios.get(
`https://hub.docker.com/v2/repositories/${image}/tags/${tag}`
const response = await axios.post(
`https://dockerhub-proxy.oceanprotocol.com`,
{
image,
tag
}
)
if (!response || response.status !== 200 || !response.data) {
if (
!response ||
response.status !== 200 ||
response.data.status !== 'success'
) {
toast.error(
'Could not fetch docker hub image info. Please check image name and tag and try again'
)