new publish preview (#947)

* refactor preview

* make preview render

* more preview elements, proper debug output

* make more elements work

* cleanup and fixes

* make asset actions preview work, kinda

* more fixes

* reorg

* make preview price display work

* fix timeout

* layout tweaks

* fixes

* another fix

* make file info preview work

* empty render fix
This commit is contained in:
Matthias Kretschmann 2021-11-23 12:53:09 +00:00 committed by GitHub
parent c387b27f23
commit c484a5b40c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 299 additions and 491 deletions

View File

@ -129,7 +129,7 @@ function usePricing(): UsePricing {
Decimal.set({ precision: 18 })
switch (price?.type) {
case 'pool': {
case 'dynamic': {
const oceanAmmount = new Decimal(price.value).times(1.05).toString()
const maxPrice = new Decimal(price.value).times(2).toString()
@ -152,7 +152,7 @@ function usePricing(): UsePricing {
Logger.log('DT buy response', tx)
break
}
case 'exchange': {
case 'fixed': {
if (!config.oceanTokenAddress) {
Logger.error(`'oceanTokenAddress' not set in config`)
return

View File

@ -22,7 +22,7 @@ interface Service {
files: string
datatokenAddress: string
serviceEndpoint: string
timeout: string
timeout: number
name?: string
description?: string
compute?: ServiceComputeOptions

View File

@ -1,5 +1,5 @@
interface BestPrice {
type: 'pool' | 'exchange' | 'free' | ''
type: 'dynamic' | 'fixed' | 'free' | ''
address: string
value: number
isConsumable?: 'true' | 'false' | ''
@ -15,7 +15,7 @@ interface PriceOptions {
price: number
amountDataToken: number
amountOcean: number
type: 'fixed' | 'dynamic' | 'free' | string
type: 'dynamic' | 'fixed' | 'free' | ''
weightOnDataToken: string
weightOnOcean: string
// easier to keep this as number for Yup input validation

View File

@ -2,6 +2,8 @@ import axios, { CancelToken, AxiosResponse } from 'axios'
import { DID, Logger } from '@oceanprotocol/lib'
export interface FileMetadata {
index: number
valid: boolean
contentType: string
contentLength: string
}
@ -35,20 +37,13 @@ export async function getFileInfo(
): Promise<FileMetadata[]> {
let postBody
try {
if (url instanceof DID)
postBody = {
did: url.getDid()
}
else
postBody = {
url
}
if (url instanceof DID) postBody = { did: url.getDid() }
else postBody = { url }
const response: AxiosResponse<FileMetadata[]> = await axios.post(
`${providerUrl}/api/v1/services/fileinfo`,
postBody,
{
cancelToken
}
{ cancelToken }
)
if (!response || response.status !== 200 || !response.data) return

View File

@ -346,7 +346,7 @@ function transformPriceToBestPrice(
) {
if (poolPrice?.length > 0) {
const price: BestPrice = {
type: 'pool',
type: 'dynamic',
address: poolPrice[0]?.id,
value:
poolPrice[0]?.consumePrice === '-1'
@ -363,7 +363,7 @@ function transformPriceToBestPrice(
// TODO Hacky hack, temporary™: set isConsumable to true for fre assets.
// isConsumable: 'true'
const price: BestPrice = {
type: 'exchange',
type: 'fixed',
value: frePrice[0]?.rate,
address: frePrice[0]?.id,
exchangeId: frePrice[0]?.id,

View File

@ -3,7 +3,7 @@ import { Logger } from '@oceanprotocol/lib'
import { getOceanConfig } from './ocean'
export function accountTruncate(account: string): string {
if (!account) return
if (!account || account === '') return
const middle = account.substring(6, 38)
const truncated = account.replace(middle, '…')
return truncated

View File

@ -10,7 +10,7 @@ export default function DebugOutput({
return (
<div style={{ marginTop: 'var(--spacer)' }}>
<h5>{title}</h5>
<pre>
<pre style={{ wordWrap: 'break-word' }}>
<code>{JSON.stringify(output, null, 2)}</code>
</pre>
</div>

View File

@ -22,7 +22,7 @@ export default function FileInfo({
return (
<div className={styles.info}>
{/* <h3 className={styles.url}>{file}</h3> */}
<h3 className={styles.url}>{(file as any).url}</h3>
<ul>
<li>URL confirmed</li>
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>}

View File

@ -27,7 +27,7 @@ export default function FilesInput(props: InputProps): ReactElement {
config?.providerUri,
newCancelToken()
)
checkedFile && helpers.setValue([checkedFile])
checkedFile && helpers.setValue([{ url: fileUrl, ...checkedFile[0] }])
} catch (error) {
toast.error('Could not fetch file info. Please check URL and try again')
console.error(error.message)

View File

@ -21,13 +21,13 @@ export default function Publisher({
minimal?: boolean
className?: string
}): ReactElement {
const { accountId } = useWeb3()
// const { accountId } = useWeb3()
const isMounted = useIsMounted()
const [profile, setProfile] = useState<Profile>()
const [name, setName] = useState(accountTruncate(account))
const [accountEns, setAccountEns] = useState<string>()
const showAdd = account === accountId && !profile
// const showAdd = account === accountId && !profile
useEffect(() => {
if (!account) return
@ -70,7 +70,7 @@ export default function Publisher({
<Link href={`/profile/${accountEns || account}`}>
<a title="Show profile page.">{name}</a>
</Link>
{showAdd && <Add />}
{/* {showAdd && <Add />} */}
</>
)}
</div>

View File

@ -34,6 +34,10 @@
border-color: var(--font-color-heading);
}
.tab[aria-disabled='true'] {
cursor: not-allowed;
}
.tabContent {
padding: calc(var(--spacer) / 2);
}

View File

@ -2,9 +2,10 @@ import React, { ReactElement, ReactNode } from 'react'
import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs'
import styles from './Tabs.module.css'
interface TabsItem {
export interface TabsItem {
title: string
content: ReactNode
disabled?: boolean
}
export default function Tabs({
@ -29,6 +30,7 @@ export default function Tabs({
className={styles.tab}
key={item.title}
onClick={handleTabChange ? () => handleTabChange(item.title) : null}
disabled={item.disabled}
>
{item.title}
</Tab>

View File

@ -3,38 +3,39 @@ import styles from './AlgorithmDatasetsListForCompute.module.css'
import { getAlgorithmDatasetsForCompute } from '@utils/aquarius'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import AssetComputeList from '@shared/AssetList/AssetComputeList'
import { useAsset } from '@context/Asset'
import { useCancelToken } from '@hooks/useCancelToken'
import { getServiceByName } from '@utils/ddo'
export default function AlgorithmDatasetsListForCompute({
algorithmDid,
dataset
ddo,
algorithmDid
}: {
ddo: Asset
algorithmDid: string
dataset: Asset
}): ReactElement {
const { ddo } = useAsset()
const [datasetsForCompute, setDatasetsForCompute] =
useState<AssetSelectionAsset[]>()
const newCancelToken = useCancelToken()
useEffect(() => {
if (!ddo) return
async function getDatasetsAllowedForCompute() {
const isCompute = Boolean(getServiceByName(dataset, 'compute'))
const isCompute = Boolean(getServiceByName(ddo, 'compute'))
const datasetComputeService = getServiceByName(
dataset,
ddo,
isCompute ? 'compute' : 'access'
)
const datasets = await getAlgorithmDatasetsForCompute(
algorithmDid,
datasetComputeService?.serviceEndpoint,
dataset?.chainId,
ddo?.chainId,
newCancelToken()
)
setDatasetsForCompute(datasets)
}
ddo.metadata.type === 'algorithm' && getDatasetsAllowedForCompute()
}, [ddo.metadata.type])
}, [ddo?.metadata?.type])
return (
<div className={styles.datasetsContainer}>

View File

@ -160,7 +160,7 @@ export default function FormStartCompute({
}
hasPreviousOrder={hasPreviousOrder}
hasDatatoken={hasDatatoken}
dtSymbol={ddo.dataTokenInfo.symbol}
dtSymbol={ddo?.dataTokenInfo?.symbol}
dtBalance={dtBalance}
datasetLowPoolLiquidity={datasetLowPoolLiquidity}
assetTimeout={assetTimeout}

View File

@ -37,12 +37,16 @@ import { SortTermOptions } from '../../../../@types/aquarius/SearchQuery'
import { FileMetadata } from '@utils/provider'
export default function Compute({
ddo,
price,
dtBalance,
file,
fileIsLoading,
isConsumable,
consumableFeedback
}: {
ddo: Asset
price: BestPrice
dtBalance: string
file: FileMetadata
fileIsLoading?: boolean
@ -51,8 +55,7 @@ export default function Compute({
}): ReactElement {
const { appConfig } = useSiteMetadata()
const { accountId } = useWeb3()
const { ocean, account } = useOcean()
const { price, ddo } = useAsset()
const { ocean } = useOcean()
const { buyDT, pricingError, pricingStepText } = usePricing()
const [isJobStarting, setIsJobStarting] = useState(false)
const [error, setError] = useState<string>()
@ -399,10 +402,7 @@ export default function Compute({
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}
dataset={ddo}
/>
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} ddo={ddo} />
</>
) : (
<Formik
@ -423,7 +423,7 @@ export default function Compute({
hasDatatoken={hasDatatoken}
dtBalance={dtBalance}
datasetLowPoolLiquidity={!isConsumablePrice}
assetType={ddo.metadata.type}
assetType={ddo?.metadata.type}
assetTimeout={datasetTimeout}
hasPreviousOrderSelectedComputeAsset={hasPreviousAlgorithmOrder}
hasDatatokenSelectedComputeAsset={hasAlgoAssetDatatoken}
@ -448,7 +448,7 @@ export default function Compute({
<SuccessConfetti success="Your job started successfully! Watch the progress below or on your profile." />
)}
</footer>
{accountId && (
{accountId && price?.datatoken && (
<AssetActionHistoryTable title="Your Compute Jobs">
<ComputeJobs minimal />
</AssetActionHistoryTable>

View File

@ -8,7 +8,6 @@ import { gql } from 'urql'
import { fetchData, getQueryContext } from '@utils/subgraph'
import { OrdersData } from '../../../@types/apollo/OrdersData'
import BigNumber from 'bignumber.js'
import { useOcean } from '@context/Ocean'
import { useWeb3 } from '@context/Web3'
import { usePricing } from '@hooks/usePricing'
import { useConsume } from '@hooks/useConsume'
@ -35,6 +34,7 @@ const previousOrderQuery = gql`
export default function Consume({
ddo,
price,
file,
isBalanceSufficient,
dtBalance,
@ -43,6 +43,7 @@ export default function Consume({
consumableFeedback
}: {
ddo: Asset
price: BestPrice
file: FileMetadata
isBalanceSufficient: boolean
dtBalance: string
@ -51,11 +52,10 @@ export default function Consume({
consumableFeedback?: string
}): ReactElement {
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { appConfig } = useSiteMetadata()
const [hasPreviousOrder, setHasPreviousOrder] = useState(false)
const [previousOrderId, setPreviousOrderId] = useState<string>()
const { isInPurgatory, price, isAssetNetwork } = useAsset()
const { isInPurgatory, isAssetNetwork } = useAsset()
const { buyDT, pricingStepText, pricingError, pricingIsLoading } =
usePricing()
const { consumeStepText, consume, consumeError, isLoading } = useConsume()
@ -105,6 +105,8 @@ export default function Consume({
}, [data, assetTimeout, accountId, isAssetNetwork])
useEffect(() => {
if (!ddo) return
const { timeout } = ddo.services[0]
setAssetTimeout(`${timeout}`)
}, [ddo])
@ -125,8 +127,7 @@ export default function Consume({
if (!accountId) return
setIsDisabled(
!isConsumable ||
((!ocean ||
!isBalanceSufficient ||
((!isBalanceSufficient ||
!isAssetNetwork ||
typeof consumeStepText !== 'undefined' ||
pricingIsLoading ||
@ -135,7 +136,6 @@ export default function Consume({
!hasDatatoken)
)
}, [
ocean,
hasPreviousOrder,
isBalanceSufficient,
isAssetNetwork,
@ -182,7 +182,7 @@ export default function Consume({
datasetLowPoolLiquidity={!isConsumablePrice}
onClick={handleConsume}
assetTimeout={secondsToString(parseInt(assetTimeout))}
assetType={ddo?.metadata.type}
assetType={ddo?.metadata?.type}
stepText={consumeStepText || pricingStepText}
isLoading={pricingIsLoading || isLoading}
priceType={price?.type}
@ -203,8 +203,8 @@ export default function Consume({
{!isInPurgatory && <PurchaseButton />}
</div>
</div>
{ddo.metadata.type === 'algorithm' && (
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} dataset={ddo} />
{ddo?.metadata?.type === 'algorithm' && (
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} ddo={ddo} />
)}
</aside>
)

View File

@ -2,7 +2,7 @@ import React, { ReactElement, useState, useEffect } from 'react'
import Compute from './Compute'
import Consume from './Consume'
import { Logger } from '@oceanprotocol/lib'
import Tabs from '@shared/atoms/Tabs'
import Tabs, { TabsItem } from '@shared/atoms/Tabs'
import { compareAsBN } from '@utils/numbers'
import Pool from './Pool'
import Trade from './Trade'
@ -15,11 +15,20 @@ import { getOceanConfig } from '@utils/ocean'
import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted'
import styles from './index.module.css'
import { useFormikContext } from 'formik'
import { FormPublishData } from 'src/components/Publish/_types'
export default function AssetActions(): ReactElement {
export default function AssetActions({
ddo,
price
}: {
ddo: Asset
price: BestPrice
}): ReactElement {
const { accountId, balance } = useWeb3()
const { ocean, account } = useOcean()
const { price, ddo, isAssetNetwork } = useAsset()
const { isAssetNetwork } = useAsset()
const { values } = useFormikContext<FormPublishData>()
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const [dtBalance, setDtBalance] = useState<string>()
@ -51,25 +60,30 @@ export default function AssetActions(): ReactElement {
// }, [accountId, isAssetNetwork, ddo, ocean])
useEffect(() => {
const oceanConfig = getOceanConfig(ddo.chainId)
const oceanConfig = getOceanConfig(ddo?.chainId)
if (!oceanConfig) return
async function initFileInfo() {
setFileIsLoading(true)
const asset = values?.services?.[0].files?.[0].url || ddo.id
const providerUrl =
values?.services[0].providerUrl || oceanConfig.providerUri
try {
const fileInfoResponse = await getFileInfo(
ddo.id,
asset,
oceanConfig.providerUri,
newCancelToken()
)
fileInfoResponse && setFileMetadata(fileInfoResponse[0])
isMounted() && setFileIsLoading(false)
setFileIsLoading(false)
} catch (error) {
Logger.error(error.message)
}
}
initFileInfo()
}, [ddo, isMounted, newCancelToken])
}, [ddo, isMounted, newCancelToken, values?.services])
// Get and set user DT balance
useEffect(() => {
@ -104,6 +118,8 @@ export default function AssetActions(): ReactElement {
const UseContent = isCompute ? (
<Compute
ddo={ddo}
price={price}
dtBalance={dtBalance}
file={fileMetadata}
fileIsLoading={fileIsLoading}
@ -113,6 +129,7 @@ export default function AssetActions(): ReactElement {
) : (
<Consume
ddo={ddo}
price={price}
dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient}
file={fileMetadata}
@ -122,22 +139,24 @@ export default function AssetActions(): ReactElement {
/>
)
const tabs = [
const tabs: TabsItem[] = [
{
title: 'Use',
content: UseContent
}
]
price?.type === 'pool' &&
price?.type === 'dynamic' &&
tabs.push(
{
title: 'Pool',
content: <Pool />
content: <Pool />,
disabled: !price.datatoken
},
{
title: 'Trade',
content: <Trade />
content: <Trade />,
disabled: !price.datatoken
}
)

View File

@ -68,14 +68,14 @@ export default function EditHistory(): ReactElement {
<ul className={styles.history}>
{receipts?.map((receipt) => (
<li key={receipt.id} className={styles.item}>
<ExplorerLink networkId={ddo.chainId} path={`/tx/${receipt.tx}`}>
<ExplorerLink networkId={ddo?.chainId} path={`/tx/${receipt.tx}`}>
edited <Time date={`${receipt.timestamp}`} relative isUnix />
</ExplorerLink>
</li>
))}
<li className={styles.item}>
<ExplorerLink networkId={ddo.chainId} path={`/tx/${creationTx}`}>
published <Time date={ddo.metadata.created} relative />
<ExplorerLink networkId={ddo?.chainId} path={`/tx/${creationTx}`}>
published <Time date={ddo?.metadata?.created} relative />
</ExplorerLink>
</li>
</ul>

View File

@ -4,27 +4,28 @@ import styles from './MetaFull.module.css'
import Publisher from '@shared/Publisher'
import { useAsset } from '@context/Asset'
export default function MetaFull(): ReactElement {
const { ddo, isInPurgatory } = useAsset()
const { type, author, algorithm } = ddo?.metadata
export default function MetaFull({ ddo }: { ddo: Asset }): ReactElement {
const { isInPurgatory } = useAsset()
function DockerImage() {
const { image, tag } = algorithm?.container
const { image, tag } = ddo?.metadata?.algorithm?.container
return <span>{`${image}:${tag}`}</span>
}
return (
return ddo ? (
<div className={styles.metaFull}>
{!isInPurgatory && <MetaItem title="Data Author" content={author} />}
{!isInPurgatory && (
<MetaItem title="Data Author" content={ddo?.metadata?.author} />
)}
<MetaItem
title="Owner"
content={<Publisher account={ddo?.nft?.owner} />}
/>
{type === 'algorithm' && algorithm && (
{ddo?.metadata?.type === 'algorithm' && ddo?.metadata?.algorithm && (
<MetaItem title="Docker Image" content={<DockerImage />} />
)}
<MetaItem title="DID" content={<code>{ddo?.id}</code>} />
</div>
)
) : null
}

View File

@ -9,8 +9,8 @@ import AssetType from '@shared/AssetType'
import styles from './MetaMain.module.css'
import { getServiceByName } from '@utils/ddo'
export default function MetaMain(): ReactElement {
const { ddo, owner, isAssetNetwork } = useAsset()
export default function MetaMain({ ddo }: { ddo: Asset }): ReactElement {
const { isAssetNetwork } = useAsset()
const { web3ProviderInfo } = useWeb3()
const isCompute = Boolean(getServiceByName(ddo, 'compute'))
@ -18,6 +18,9 @@ export default function MetaMain(): ReactElement {
const blockscoutNetworks = [1287, 2021000, 2021001, 44787, 246, 1285]
const isBlockscoutExplorer = blockscoutNetworks.includes(ddo?.chainId)
const dataTokenName = ddo?.dataTokenInfo?.name
const dataTokenSymbol = ddo?.dataTokenInfo?.symbol
return (
<aside className={styles.meta}>
<header className={styles.asset}>
@ -35,16 +38,16 @@ export default function MetaMain(): ReactElement {
: `token/${ddo?.services[0].datatokenAddress}`
}
>
{`${ddo?.dataTokenInfo.name}${ddo?.dataTokenInfo.symbol}`}
{`${dataTokenName}${dataTokenSymbol}`}
</ExplorerLink>
{web3ProviderInfo?.name === 'MetaMask' && isAssetNetwork && (
<span className={styles.addWrap}>
<AddToken
address={ddo?.services[0].datatokenAddress}
symbol={ddo?.dataTokenInfo.symbol}
symbol={(ddo as Asset)?.dataTokenInfo?.symbol}
logo="https://raw.githubusercontent.com/oceanprotocol/art/main/logo/datatoken.png"
text={`Add ${ddo?.dataTokenInfo.symbol} to wallet`}
text={`Add ${(ddo as Asset)?.dataTokenInfo?.symbol} to wallet`}
className={styles.add}
minimal
/>
@ -53,7 +56,7 @@ export default function MetaMain(): ReactElement {
</header>
<div className={styles.byline}>
Published By <Publisher account={owner} />
Published By <Publisher account={(ddo as Asset)?.nft?.owner} />
<p>
<Time date={ddo?.metadata.created} relative />
{ddo?.metadata.created !== ddo?.metadata.updated && (

View File

@ -3,7 +3,6 @@ import MetaItem from './MetaItem'
import styles from './MetaSecondary.module.css'
import Tags from '@shared/atoms/Tags'
import Button from '@shared/atoms/Button'
import { useAsset } from '@context/Asset'
const SampleButton = ({ url }: { url: string }) => (
<Button
@ -18,9 +17,7 @@ const SampleButton = ({ url }: { url: string }) => (
</Button>
)
export default function MetaSecondary(): ReactElement {
const { ddo } = useAsset()
export default function MetaSecondary({ ddo }: { ddo: Asset }): ReactElement {
return (
<aside className={styles.metaSecondary}>
{ddo?.metadata.links?.length > 0 && (

View File

@ -1,4 +1,4 @@
import React, { ReactElement, useEffect, useState } from 'react'
import React, { ReactElement } from 'react'
import Markdown from '@shared/Markdown'
import MetaFull from './MetaFull'
import MetaSecondary from './MetaSecondary'
@ -7,62 +7,34 @@ import { useUserPreferences } from '@context/UserPreferences'
import Bookmark from './Bookmark'
import { useAsset } from '@context/Asset'
import Alert from '@shared/atoms/Alert'
import Button from '@shared/atoms/Button'
import Edit from '../AssetActions/Edit'
import EditComputeDataset from '../AssetActions/Edit/EditComputeDataset'
import DebugOutput from '@shared/DebugOutput'
import MetaMain from './MetaMain'
import EditHistory from './EditHistory'
import { useWeb3 } from '@context/Web3'
import styles from './index.module.css'
import NetworkName from '@shared/NetworkName'
import content from '../../../../content/purgatory.json'
export default function AssetContent({ ddo }: { ddo: Asset }): ReactElement {
export default function AssetContent({
ddo,
price
}: {
ddo: Asset
price: BestPrice
}): ReactElement {
const { debug } = useUserPreferences()
const { accountId } = useWeb3()
const { price, owner, isInPurgatory, purgatoryData, isAssetNetwork } =
useAsset()
const [showEdit, setShowEdit] = useState<boolean>()
const [isComputeType, setIsComputeType] = useState<boolean>(false)
const [showEditCompute, setShowEditCompute] = useState<boolean>()
const [isOwner, setIsOwner] = useState(false)
const { isInPurgatory, purgatoryData } = useAsset()
const serviceCompute = ddo.services.filter(
(service) => service.type === 'compute'
)[0]
useEffect(() => {
if (!accountId || !owner) return
const isOwner = accountId.toLowerCase() === owner.toLowerCase()
setIsOwner(isOwner)
setIsComputeType(Boolean(serviceCompute))
}, [accountId, price, owner, ddo])
function handleEditButton() {
setShowEdit(true)
}
function handleEditComputeButton() {
setShowEditCompute(true)
}
return showEdit ? (
<Edit setShowEdit={setShowEdit} isComputeType={isComputeType} />
) : showEditCompute ? (
<EditComputeDataset setShowEdit={setShowEditCompute} />
) : (
return (
<>
<div className={styles.networkWrap}>
<NetworkName networkId={ddo.chainId} className={styles.network} />
<NetworkName networkId={ddo?.chainId} className={styles.network} />
</div>
<article className={styles.grid}>
<div>
<div className={styles.content}>
<MetaMain />
<Bookmark did={ddo.id} />
<MetaMain ddo={ddo} />
{price?.datatoken && <Bookmark did={ddo?.id} />}
{isInPurgatory ? (
<Alert
@ -77,43 +49,49 @@ export default function AssetContent({ ddo }: { ddo: Asset }): ReactElement {
className={styles.description}
text={ddo?.metadata.description || ''}
/>
<MetaSecondary />
{isOwner && isAssetNetwork && (
<div className={styles.ownerActions}>
<Button
style="text"
size="small"
onClick={handleEditButton}
>
Edit Metadata
</Button>
{serviceCompute && ddo?.metadata.type === 'dataset' && (
<>
<span className={styles.separator}>|</span>
<Button
style="text"
size="small"
onClick={handleEditComputeButton}
>
Edit Compute Settings
</Button>
</>
)}
</div>
)}
<MetaSecondary ddo={ddo} />
</>
)}
<MetaFull />
<EditHistory />
{debug === true && <DebugOutput title="DDO" output={ddo} />}
<MetaFull ddo={ddo} />
{price?.datatoken && <EditHistory />}
{price?.datatoken && debug === true && (
<DebugOutput title="DDO" output={ddo} />
)}
</div>
</div>
<div className={styles.actions}>
<AssetActions />
<AssetActions ddo={ddo} price={price} />
{/*
TODO: restore edit actions, ideally put edit screens on new page
with own URL instead of switching out AssetContent in place.
Simple way would be modal usage
*/}
{/* {isOwner && isAssetNetwork && (
<div className={styles.ownerActions}>
<Button
style="text"
size="small"
onClick={handleEditButton}
>
Edit Metadata
</Button>
{serviceCompute && ddo?.metadata.type === 'dataset' && (
<>
<span className={styles.separator}>|</span>
<Button
style="text"
size="small"
onClick={handleEditComputeButton}
>
Edit Compute Settings
</Button>
</>
)}
</div>
)} */}
</div>
</article>
</>

View File

@ -12,7 +12,7 @@ import { transformComputeFormToServiceComputePrivacy } from '@utils/compute'
import { setMinterToDispenser, setMinterToPublisher } from '@utils/freePrice'
import Web3Feedback from '@shared/Web3Feedback'
import { getInitialValues, validationSchema } from './_constants'
import content from '../../../../../content/pages/editComputeDataset.json'
import content from '../../../../content/pages/editComputeDataset.json'
export default function EditComputeDataset({
setShowEdit

View File

@ -15,7 +15,7 @@ import { publisherTrustedAlgorithm as PublisherTrustedAlgorithm } from '@oceanpr
import { useSiteMetadata } from '@hooks/useSiteMetadata'
import FormActions from './FormActions'
import { useCancelToken } from '@hooks/useCancelToken'
import { SortTermOptions } from '../../../../@types/aquarius/SearchQuery'
import { SortTermOptions } from '../../../@types/aquarius/SearchQuery'
import { getServiceByName } from '@utils/ddo'
export default function FormEditComputeDataset({

View File

@ -4,7 +4,7 @@ import { useOcean } from '@context/Ocean'
import Input, { InputProps } from '@shared/FormInput'
import FormActions from './FormActions'
import styles from './FormEditMetadata.module.css'
import { FormPublishData } from '../../../Publish/_types'
import { FormPublishData } from '../../Publish/_types'
// function handleTimeoutCustomOption(
// data: FormFieldContent[],

View File

@ -1,4 +1,4 @@
import { secondsToString } from '@utils/ddo'
import { mapTimeoutStringToSeconds, secondsToString } from '@utils/ddo'
import { EditableMetadataLinks } from '@oceanprotocol/lib'
import * as Yup from 'yup'
import { MetadataEditForm } from './_types'
@ -16,7 +16,7 @@ export const validationSchema = Yup.object().shape({
export function getInitialValues(
metadata: Metadata,
timeout: string,
timeout: number,
price: number
): Partial<MetadataEditForm> {
return {

View File

@ -3,7 +3,7 @@ import { EditableMetadataLinks } from '@oceanprotocol/lib'
export interface MetadataEditForm {
name: string
description: string
timeout: string
timeout: number
price?: number
links?: string | EditableMetadataLinks[]
author?: string

View File

@ -12,7 +12,7 @@ import { Logger } from '@oceanprotocol/lib'
import { useWeb3 } from '@context/Web3'
import { useOcean } from '@context/Ocean'
import { setMinterToDispenser, setMinterToPublisher } from '@utils/freePrice'
import content from '../../../../../content/pages/edit.json'
import content from '../../../../content/pages/edit.json'
import { MetadataEditForm } from './_types'
export default function Edit({

View File

@ -6,7 +6,7 @@ import { useAsset } from '@context/Asset'
import AssetContent from './AssetContent'
export default function AssetDetails({ uri }: { uri: string }): ReactElement {
const { ddo, title, error, isInPurgatory, loading } = useAsset()
const { ddo, title, error, isInPurgatory, loading, price } = useAsset()
const [pageTitle, setPageTitle] = useState<string>()
useEffect(() => {
@ -14,13 +14,12 @@ export default function AssetDetails({ uri }: { uri: string }): ReactElement {
setPageTitle('Could not retrieve asset')
return
}
setPageTitle(isInPurgatory ? '' : title)
}, [ddo, error, isInPurgatory, title])
return ddo && pageTitle !== undefined && !loading ? (
<Page title={pageTitle} uri={uri}>
<AssetContent ddo={ddo} />
<AssetContent ddo={ddo} price={price} />
</Page>
) : error ? (
<Page title={pageTitle} noPageHeader uri={uri}>

View File

@ -52,7 +52,7 @@ export default function Stats({
const accountPoolAdresses: string[] = []
const assetsPrices = await getAssetsBestPrices(assets)
for (const priceInfo of assetsPrices) {
if (priceInfo.price.type === 'pool') {
if (priceInfo.price.type === 'dynamic') {
accountPoolAdresses.push(priceInfo.price.address.toLowerCase())
}
}

View File

@ -1,17 +1,17 @@
import React, { FormEvent, ReactElement, Ref, RefObject } from 'react'
import { useOcean } from '@context/Ocean'
import Button from '@shared/atoms/Button'
import styles from './index.module.css'
import { FormikContextType, useFormikContext } from 'formik'
import { FormPublishData } from '../_types'
import { wizardSteps } from '../_constants'
import { useWeb3 } from '@context/Web3'
export default function Actions({
scrollToRef
}: {
scrollToRef: RefObject<any>
}): ReactElement {
const { ocean, account } = useOcean()
const { accountId } = useWeb3()
const {
status,
values,
@ -21,13 +21,13 @@ export default function Actions({
function handleNext(e: FormEvent) {
e.preventDefault()
setFieldValue('stepCurrent', values.user.stepCurrent + 1)
setFieldValue('user.stepCurrent', values.user.stepCurrent + 1)
scrollToRef.current.scrollIntoView()
}
function handlePrevious(e: FormEvent) {
e.preventDefault()
setFieldValue('stepCurrent', values.user.stepCurrent - 1)
setFieldValue('user.stepCurrent', values.user.stepCurrent - 1)
scrollToRef.current.scrollIntoView()
}
@ -42,11 +42,7 @@ export default function Actions({
Continue
</Button>
) : (
<Button
type="submit"
style="primary"
disabled={!ocean || !account || !isValid}
>
<Button type="submit" style="primary" disabled={!accountId || !isValid}>
Submit
</Button>
)}

View File

@ -1,36 +0,0 @@
import React, { ReactElement } from 'react'
import DebugOutput from '@shared/DebugOutput'
import styles from './index.module.css'
// import { transformPublishFormToMetadata } from '@utils/metadata'
import { FormPublishData } from './_types'
import { useFormikContext } from 'formik'
export default function Debug(): ReactElement {
const { values } = useFormikContext<FormPublishData>()
const ddo = {
'@context': 'https://w3id.org/did/v1'
// dataTokenInfo: {
// ...values.dataTokenOptions
// },
// service: [
// {
// index: 0,
// type: 'metadata',
// attributes: { ...transformPublishFormToMetadata(values) }
// },
// {
// index: 1,
// type: values.access,
// serviceEndpoint: values.providerUri,
// attributes: {}
// }
// ]
}
return (
<div className={styles.grid}>
<DebugOutput title="Collected Form Values" output={values} />
<DebugOutput title="Transformed DDO Values" output={ddo} />
</div>
)
}

View File

@ -0,0 +1,6 @@
.debug {
display: grid;
gap: var(--spacer);
grid-template-columns: 1fr 1fr;
word-break: break-all;
}

View File

@ -0,0 +1,26 @@
import React, { ReactElement, useEffect, useState } from 'react'
import DebugOutput from '@shared/DebugOutput'
import { FormPublishData } from '../_types'
import { useFormikContext } from 'formik'
import { transformPublishFormToDdo } from '../_utils'
import styles from './index.module.css'
export default function Debug(): ReactElement {
const { values } = useFormikContext<FormPublishData>()
const [ddo, setDdo] = useState<DDO>()
useEffect(() => {
async function makeDdo() {
const ddo = await transformPublishFormToDdo(values)
setDdo(ddo)
}
makeDdo()
}, [values])
return (
<div className={styles.debug}>
<DebugOutput title="Collected Form Values" output={values} />
<DebugOutput title="Transformed DDO Values" output={ddo} />
</div>
)
}

View File

@ -12,32 +12,26 @@ export default function Navigation(): ReactElement {
setFieldValue('user.stepCurrent', step)
}
console.log(errors)
const isSuccessMetadata = errors.metadata === undefined
const isSuccessServices = errors.services === undefined
return (
<nav className={styles.navigation}>
<ol>
{wizardSteps.map((step) => {
const isSuccessMetadata = errors.metadata === undefined
const isSuccessServices = errors.services === undefined
return (
<li
key={step.title}
onClick={() => handleStepClick(step.step)}
// TODO: add success class for all steps
className={`${
values.user.stepCurrent === step.step ? styles.current : null
} ${
step.step === 1 && isSuccessMetadata ? styles.success : null
} ${
step.step === 2 && isSuccessServices ? styles.success : null
}`}
>
{step.title}
</li>
)
})}
{wizardSteps.map((step) => (
<li
key={step.title}
onClick={() => handleStepClick(step.step)}
// TODO: add success class for all steps
className={`${
values.user.stepCurrent === step.step ? styles.current : null
} ${step.step === 1 && isSuccessMetadata ? styles.success : null} ${
step.step === 2 && isSuccessServices ? styles.success : null
}`}
>
{step.title}
</li>
))}
</ol>
</nav>
)

View File

@ -2,23 +2,15 @@
font-size: var(--font-size-small);
margin-top: calc(var(--spacer) / 2);
margin-bottom: var(--spacer);
margin-left: calc(var(--spacer) / -4);
margin-right: calc(var(--spacer) / -4);
}
.preview header {
margin-bottom: var(--spacer);
}
.metaFull {
display: grid;
gap: var(--spacer);
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);
@media (min-width: 60rem) {
.preview {
margin-left: calc(var(--spacer) * -3);
margin-right: calc(var(--spacer) * -3);
}
}
.previewTitle {
@ -28,33 +20,6 @@
margin-bottom: calc(var(--spacer) / 2);
}
.title {
margin: 0;
}
.datatoken {
margin-bottom: 0;
color: var(--color-secondary);
}
.preview [class*='MetaItem-module--metaItem'] h3 {
margin-bottom: calc(var(--spacer) / 12);
}
.description {
position: relative;
margin-top: calc(var(--spacer) / 4);
}
.toggle {
position: absolute;
bottom: 0.15rem;
right: 0;
}
.asset {
display: grid;
grid-template-columns: 1fr 4fr;
align-items: center;
margin-bottom: calc(var(--spacer) / 2);
.assetTitle {
margin-bottom: calc(var(--spacer) / 1.5);
}

View File

@ -1,181 +1,39 @@
import React, { FormEvent, ReactElement, useState } from 'react'
import Markdown from '@shared/Markdown'
import Tags from '@shared/atoms/Tags'
import MetaItem from '../../Asset/AssetContent/MetaItem'
import FileIcon from '@shared/FileIcon'
import Button from '@shared/atoms/Button'
import NetworkName from '@shared/NetworkName'
import { useWeb3 } from '@context/Web3'
import React, { ReactElement, useEffect, useState } from 'react'
import styles from './index.module.css'
import Web3Feedback from '@shared/Web3Feedback'
import { useAsset } from '@context/Asset'
import { FormPublishData } from '../_types'
import { useFormikContext } from 'formik'
function Description({ description }: { description: string }) {
const [fullDescription, setFullDescription] = useState<boolean>(false)
const textLimit = 500 // string.length
const descriptionDisplay =
fullDescription === true
? description
: `${description.substring(0, textLimit)}${
description.length > textLimit ? '...' : ''
}`
function handleDescriptionToggle(e: FormEvent<HTMLButtonElement>) {
e.preventDefault()
setFullDescription(!fullDescription)
}
return (
<div className={styles.description}>
<Markdown text={descriptionDisplay} />
{description.length > textLimit && (
<Button
style="text"
size="small"
onClick={handleDescriptionToggle}
className={styles.toggle}
>
{fullDescription === true ? 'Close' : 'Expand'}
</Button>
)}
</div>
)
}
function MetaFull({ values }: { values: Partial<FormPublishData> }) {
return (
<div className={styles.metaFull}>
{Object.entries(values)
.filter(
([key, value]) =>
!(
key.includes('name') ||
key.includes('description') ||
key.includes('tags') ||
key.includes('files') ||
key.includes('links') ||
key.includes('termsAndConditions') ||
key.includes('dataTokenOptions') ||
key.includes('dockerImage') ||
key.includes('algorithmPrivacy') ||
value === undefined
)
)
.map(([key, value]) => (
<MetaItem key={key} title={key} content={value} />
))}
</div>
)
}
function Sample({ url }: { url: string }) {
return (
<Button
href={url}
target="_blank"
rel="noreferrer"
download
style="text"
size="small"
>
Download Sample
</Button>
)
}
import AssetContent from 'src/components/Asset/AssetContent'
import { transformPublishFormToDdo } from '../_utils'
export default function Preview(): ReactElement {
const { networkId } = useWeb3()
const { isAssetNetwork } = useAsset()
const [ddo, setDdo] = useState<Asset>()
const [price, setPrice] = useState<BestPrice>()
const { values } = useFormikContext<FormPublishData>()
return (
<div className={styles.preview}>
<h2 className={styles.previewTitle}>Preview</h2>
{/* <header>
{networkId && <NetworkName networkId={networkId} />}
{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} />}
useEffect(() => {
async function makeDdo() {
const ddo = await transformPublishFormToDdo(values)
setDdo(ddo as Asset)
<div className={styles.asset}>
{values.files?.length > 0 && typeof values.files !== 'string' && (
<FileIcon
file={values.files[0] as FileMetadata}
className={styles.file}
small
/>
)}
</div>
{typeof values.links !== 'string' && values.links?.length && (
<Sample url={(values.links[0] as FileMetadata).url} />
)}
{values.tags && <Tags items={transformTags(values.tags)} />}
</header>
<MetaFull values={values} />
{isAssetNetwork === false && (
<Web3Feedback isAssetNetwork={isAssetNetwork} />
)}
</div>
)
}
export function MetadataAlgorithmPreview({
values
}: {
values: Partial<FormPublishData>
}): ReactElement {
const { networkId } = useWeb3()
// dummy BestPrice to trigger certain AssetActions
const price: BestPrice = {
type: values.pricing.type,
address: '0x...',
value: values.pricing.price,
pools: [],
oceanSymbol: 'OCEAN'
}
setPrice(price)
}
makeDdo()
}, [values])
return (
<div className={styles.preview}>
<h2 className={styles.previewTitle}>Preview</h2>
<header>
{networkId && <NetworkName networkId={networkId} />}
{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} />}
<div className={styles.asset}>
{values.files?.length > 0 && typeof values.files !== 'string' && (
<FileIcon
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} /> */}
<h3 className={styles.assetTitle}>{values.metadata.name}</h3>
<AssetContent ddo={ddo} price={price} />
</div>
)
}

View File

@ -23,13 +23,13 @@ export default function PricingFields(): ReactElement {
function handleTabChange(tabName: string) {
const type = tabName.toLowerCase()
setFieldValue('pricing.type', type)
type === 'fixed' && setFieldValue('pricing.amountDataToken', 1000)
type === 'dynamic' && setFieldValue('pricing.amountDataToken', 1000)
type === 'free' && price < 1 && setFieldValue('pricing.price', 1)
}
// Always update everything when price value changes
useEffect(() => {
if (type === 'fixed' || type === 'free') return
if (type === 'dynamic' || type === 'free') return
const amountDataToken =
isValidNumber(amountOcean) &&
@ -70,36 +70,8 @@ export default function PricingFields(): ReactElement {
<Tabs
items={tabs}
handleTabChange={handleTabChange}
defaultIndex={type === 'fixed' ? 0 : type === 'dynamic' ? 1 : 2}
defaultIndex={type === 'dynamic' ? 1 : type === 'free' ? 2 : 0}
className={styles.pricing}
/>
)
// async function handleCreatePricing(values: PriceOptions) {
// try {
// const priceOptions = {
// ...values,
// // swapFee is tricky: to get 0.1% you need to send 0.001 as value
// swapFee: `${values.swapFee / 100}`
// }
// const tx = await createPricing(priceOptions, ddo)
// // Pricing failed
// if (!tx || pricingError) {
// toast.error(pricingError || 'Price creation failed.')
// Logger.error(pricingError || 'Price creation failed.')
// return
// }
// // Pricing succeeded
// setSuccess(
// `🎉 Successfully created a ${values.type} price. 🎉 Reload the page to get all updates.`
// )
// Logger.log(`Transaction: ${tx}`)
// } catch (error) {
// toast.error(error.message)
// Logger.error(error.message)
// }
// }
}

View File

@ -76,6 +76,8 @@ export const initialValues: FormPublishData = {
}
}
// TODO: conditional validation
// e.g. when algo is selected, Docker image is required
const validationMetadata = {
type: Yup.string()
.matches(/dataset|algorithm/g, { excludeEmptyString: true })

View File

@ -3,7 +3,12 @@ import { NftOptions } from '@utils/nft'
import { ReactElement } from 'react'
export interface FormPublishService {
files: string[]
files: {
url: string
valid: boolean
contentLength: string
contentType: string
}[]
timeout: string
dataTokenOptions: DataTokenOptions
access: 'Download' | 'Compute' | string

View File

@ -1,3 +1,4 @@
import { mapTimeoutStringToSeconds } from '@utils/ddo'
import { getEncryptedFileUrls } from '@utils/provider'
import { sha256 } from 'js-sha256'
import slugify from 'slugify'
@ -27,12 +28,14 @@ function transformTags(value: string): string[] {
export async function transformPublishFormToDdo(
values: FormPublishData,
datatokenAddress: string,
nftAddress: string
// Those 2 are only passed during actual publishing process
// so we can always assume if they are not passed, we are on preview.
datatokenAddress?: string,
nftAddress?: string
): Promise<DDO> {
const { metadata, services } = values
const { chainId, accountId } = values.user
const did = sha256(`${nftAddress}${chainId}`)
const { metadata, services, user } = values
const { chainId, accountId } = user
const did = nftAddress ? `0x${sha256(`${nftAddress}${chainId}`)}` : '0x...'
const currentTime = dateToStringNoMS(new Date())
const {
type,
@ -48,12 +51,12 @@ export async function transformPublishFormToDdo(
} = metadata
const { access, files, providerUrl, timeout } = services[0]
const filesEncrypted = await getEncryptedFileUrls(
files as string[],
providerUrl,
did,
accountId
)
const filesTransformed = files?.length && files[0].valid && [...files[0].url]
const filesEncrypted =
files?.length &&
files[0].valid &&
(await getEncryptedFileUrls(filesTransformed, providerUrl, did, accountId))
const newMetadata: Metadata = {
created: currentTime,
@ -70,13 +73,13 @@ export async function transformPublishFormToDdo(
},
...(type === 'algorithm' && {
algorithm: {
language: getUrlFileExtension(files[0]),
language: files?.length ? getUrlFileExtension(filesTransformed[0]) : '',
version: '0.1',
container: {
entrypoint: dockerImageCustomEntrypoint,
image: dockerImageCustom,
tag: dockerImageCustomTag,
checksum: '' // how to get? Is it user input?
checksum: '' // TODO: how to get? Is it user input?
}
}
})
@ -87,7 +90,7 @@ export async function transformPublishFormToDdo(
files: filesEncrypted,
datatokenAddress,
serviceEndpoint: providerUrl,
timeout,
timeout: mapTimeoutStringToSeconds(timeout),
...(access === 'compute' && {
compute: {
namespace: 'ocean-compute',
@ -110,7 +113,17 @@ export async function transformPublishFormToDdo(
version: '4.0.0',
chainId,
metadata: newMetadata,
services: [newService]
services: [newService],
// only added for DDO preview, reflecting Asset response
...(!datatokenAddress && {
dataTokenInfo: {
name: values.services[0].dataTokenOptions.name,
symbol: values.services[0].dataTokenOptions.symbol
},
nft: {
owner: accountId
}
})
}
return newDdo

View File

@ -16,7 +16,9 @@ import { Steps } from './Steps'
import { FormPublishData } from './_types'
import { sha256 } from 'js-sha256'
import { generateNftCreateData } from '@utils/nft'
import { useUserPreferences } from '@context/UserPreferences'
// TODO: restore FormikPersist, add back clear form action
const formName = 'ocean-publish-form'
export default function PublishPage({
@ -24,6 +26,7 @@ export default function PublishPage({
}: {
content: { title: string; description: string; warning: string }
}): ReactElement {
const { debug } = useUserPreferences()
const { accountId, chainId } = useWeb3()
const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId)
// const { publish, publishError, isLoading, publishStepText } = usePublish()
@ -39,6 +42,11 @@ export default function PublishPage({
// const nftOptions = values.metadata.nft
// const nftCreateData = generateNftCreateData(nftOptions)
// const ercParams = {}
// const priceOptions = {
// ...values,
// // swapFee is tricky: to get 0.1% you need to send 0.001 as value
// swapFee: `${values.swapFee / 100}`
// }
// const txMint = await createNftWithErc(accountId, nftCreateData)
// const { nftAddress, datatokenAddress } = txMint.logs[0].args
//
@ -137,7 +145,7 @@ export default function PublishPage({
<Steps />
<Actions scrollToRef={scrollToRef} />
</Form>
<Debug />
{debug && <Debug />}
</>
</Formik>
)}