mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Merge branch 'main' into fix/issue-1069-c2d-unsupported-networks
This commit is contained in:
commit
8844e82c75
@ -54,21 +54,22 @@
|
||||
},
|
||||
{
|
||||
"name": "dockerImageCustom",
|
||||
"label": "Docker Image URL",
|
||||
"placeholder": "e.g. oceanprotocol/algo_dockers or https://example.com/image_path",
|
||||
"help": "Provide the name of a public Docker image or the full url if you have it hosted in a 3rd party repo",
|
||||
"label": "Custom Docker Image",
|
||||
"placeholder": "e.g. oceanprotocol/algo_dockers:node-vibrant or quay.io/startx/mariadb",
|
||||
"help": "Provide the name and the tag of a public Docker hub image or the custom image if you have it hosted in a 3rd party repository",
|
||||
"type": "container",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "dockerImageCustomTag",
|
||||
"label": "Docker Image Tag",
|
||||
"placeholder": "e.g. latest",
|
||||
"help": "Provide the tag for your Docker image.",
|
||||
"name": "dockerImageChecksum",
|
||||
"label": "Docker Image Checksum",
|
||||
"placeholder": "e.g. sha256:xiXqb7Vet0FbN9q0GFMgUdi5C22wjJT0i2G6lYKC2jl6QxkKzVz7KaPDgqfTMjNF",
|
||||
"help": "Provide the checksum(DIGEST) of your docker image.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "dockerImageCustomEntrypoint",
|
||||
"label": "Docker Entrypoint",
|
||||
"label": "Docker Image Entrypoint",
|
||||
"placeholder": "e.g. python $ALGO",
|
||||
"help": "Provide the entrypoint for your algorithm.",
|
||||
"required": true
|
||||
|
@ -14,7 +14,7 @@
|
||||
"link": "/profile"
|
||||
}
|
||||
],
|
||||
"announcement": "Explore [OceanONDA V4](https://blog.oceanprotocol.com/how-to-publish-a-data-nft-f58ad2a622a9).",
|
||||
"announcement": "[Lock your OCEAN](https://df.oceandao.org/) to get veOCEAN, earn rewards and curate data.",
|
||||
"warning": {
|
||||
"ctd": "Compute-to-Data is still in a testing phase, please use it only on test networks."
|
||||
}
|
||||
|
14
package-lock.json
generated
14
package-lock.json
generated
@ -13,7 +13,7 @@
|
||||
"@coingecko/cryptoformat": "^0.5.4",
|
||||
"@loadable/component": "^5.15.2",
|
||||
"@oceanprotocol/art": "^3.2.0",
|
||||
"@oceanprotocol/lib": "^2.1.1",
|
||||
"@oceanprotocol/lib": "^2.2.3",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@oceanprotocol/use-dark-mode": "^2.4.3",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
@ -4539,9 +4539,9 @@
|
||||
"integrity": "sha512-Oe+oBRiu1dlco9PQ7eUYcTYi2Nua69S3TiSw62H46AIpwnFK8ORuO0Ny20No++KisBA9F+84b5lDn6kQy5Lt/Q=="
|
||||
},
|
||||
"node_modules/@oceanprotocol/lib": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.1.1.tgz",
|
||||
"integrity": "sha512-N7NKnwVujJDn2X9MwxFu15x8VvTVEDqWuIZFY4s3NG0NbwXGEHptewKlAVhOkvm6jOGuCN3NXWqRUTvMFOWGbQ==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.2.3.tgz",
|
||||
"integrity": "sha512-jBD6bD1dPd7MHiiMA0V2hBntHM0vhmYpzeuv2dNj/2i1FNvTzpQO3v4a5V+Ahxs6L6YirBBfzT3HHtGKOghc5w==",
|
||||
"dependencies": {
|
||||
"@oceanprotocol/contracts": "^1.1.7",
|
||||
"bignumber.js": "^9.1.0",
|
||||
@ -44803,9 +44803,9 @@
|
||||
"integrity": "sha512-Oe+oBRiu1dlco9PQ7eUYcTYi2Nua69S3TiSw62H46AIpwnFK8ORuO0Ny20No++KisBA9F+84b5lDn6kQy5Lt/Q=="
|
||||
},
|
||||
"@oceanprotocol/lib": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.1.1.tgz",
|
||||
"integrity": "sha512-N7NKnwVujJDn2X9MwxFu15x8VvTVEDqWuIZFY4s3NG0NbwXGEHptewKlAVhOkvm6jOGuCN3NXWqRUTvMFOWGbQ==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.2.3.tgz",
|
||||
"integrity": "sha512-jBD6bD1dPd7MHiiMA0V2hBntHM0vhmYpzeuv2dNj/2i1FNvTzpQO3v4a5V+Ahxs6L6YirBBfzT3HHtGKOghc5w==",
|
||||
"requires": {
|
||||
"@oceanprotocol/contracts": "^1.1.7",
|
||||
"bignumber.js": "^9.1.0",
|
||||
|
@ -26,7 +26,7 @@
|
||||
"@coingecko/cryptoformat": "^0.5.4",
|
||||
"@loadable/component": "^5.15.2",
|
||||
"@oceanprotocol/art": "^3.2.0",
|
||||
"@oceanprotocol/lib": "^2.1.1",
|
||||
"@oceanprotocol/lib": "^2.2.3",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@oceanprotocol/use-dark-mode": "^2.4.3",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
|
@ -32,22 +32,25 @@ export function getNetworkDisplayName(
|
||||
displayName = 'Polygon'
|
||||
break
|
||||
case 1287:
|
||||
displayName = 'Moonbase Alpha'
|
||||
displayName = 'Moonbase'
|
||||
break
|
||||
case 1285:
|
||||
displayName = 'Moonriver'
|
||||
break
|
||||
case 80001:
|
||||
displayName = 'Polygon Mumbai'
|
||||
displayName = 'Mumbai'
|
||||
break
|
||||
case 8996:
|
||||
displayName = 'Development'
|
||||
break
|
||||
case 3:
|
||||
displayName = 'ETH Ropsten'
|
||||
displayName = 'Ropsten'
|
||||
break
|
||||
case 5:
|
||||
displayName = 'Görli'
|
||||
break
|
||||
case 2021000:
|
||||
displayName = 'GAIA-X Testnet'
|
||||
displayName = 'GAIA-X'
|
||||
break
|
||||
default:
|
||||
displayName = data
|
||||
|
@ -6,7 +6,9 @@ export enum SortDirectionOptions {
|
||||
export enum SortTermOptions {
|
||||
Created = 'nft.created',
|
||||
Relevance = '_score',
|
||||
Stats = 'stats.orders'
|
||||
Orders = 'stats.orders',
|
||||
Allocated = 'stats.allocated',
|
||||
Price = 'stats.price.value'
|
||||
}
|
||||
|
||||
// Note: could not figure out how to get `enum` to be ambiant
|
||||
|
@ -53,7 +53,9 @@ export function generateBaseQuery(
|
||||
...baseQueryParams.nestedQuery,
|
||||
filter: [
|
||||
...(baseQueryParams.filters || []),
|
||||
getFilterTerm('chainId', baseQueryParams.chainIds),
|
||||
baseQueryParams.chainIds
|
||||
? getFilterTerm('chainId', baseQueryParams.chainIds)
|
||||
: [],
|
||||
getFilterTerm('_index', 'aquarius'),
|
||||
...(baseQueryParams.ignorePurgatory
|
||||
? []
|
||||
@ -213,6 +215,28 @@ export async function getAssetsFromDtList(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAssetsFromNftList(
|
||||
nftList: string[],
|
||||
chainIds: number[],
|
||||
cancelToken: CancelToken
|
||||
): Promise<Asset[]> {
|
||||
try {
|
||||
if (!(nftList.length > 0)) return
|
||||
|
||||
const baseParams = {
|
||||
chainIds,
|
||||
filters: [getFilterTerm('nftAddress', nftList)],
|
||||
ignorePurgatory: true
|
||||
} as BaseQueryParams
|
||||
const query = generateBaseQuery(baseParams)
|
||||
|
||||
const queryResult = await queryMetadata(query, cancelToken)
|
||||
return queryResult?.results
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
export async function retrieveDDOListByDIDs(
|
||||
didList: string[],
|
||||
chainIds: number[],
|
||||
@ -266,7 +290,7 @@ export async function getAlgorithmDatasetsForCompute(
|
||||
const query = generateBaseQuery(baseQueryParams)
|
||||
const computeDatasets = await queryMetadata(query, cancelToken)
|
||||
|
||||
if (computeDatasets.totalResults === 0) return []
|
||||
if (computeDatasets?.totalResults === 0) return []
|
||||
|
||||
const datasets = await transformAssetToAssetSelection(
|
||||
datasetProviderUri,
|
||||
@ -303,7 +327,7 @@ export async function getPublishedAssets(
|
||||
aggs: {
|
||||
totalOrders: {
|
||||
sum: {
|
||||
field: SortTermOptions.Stats
|
||||
field: SortTermOptions.Orders
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -357,7 +381,7 @@ export async function getTopPublishers(
|
||||
aggs: {
|
||||
totalSales: {
|
||||
sum: {
|
||||
field: SortTermOptions.Stats
|
||||
field: SortTermOptions.Orders
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,27 @@
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import axios from 'axios'
|
||||
import isUrl from 'is-url-superb'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
async function isDockerHubImageValid(
|
||||
export interface dockerContainerInfo {
|
||||
exists: boolean
|
||||
checksum: string
|
||||
}
|
||||
|
||||
export async function getContainerChecksum(
|
||||
image: string,
|
||||
tag: string
|
||||
): Promise<boolean> {
|
||||
): Promise<dockerContainerInfo> {
|
||||
const containerInfo: dockerContainerInfo = {
|
||||
exists: false,
|
||||
checksum: null
|
||||
}
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`https://dockerhub-proxy.oceanprotocol.com`,
|
||||
{ image, tag }
|
||||
{
|
||||
image,
|
||||
tag
|
||||
}
|
||||
)
|
||||
if (
|
||||
!response ||
|
||||
@ -18,46 +29,18 @@ async function isDockerHubImageValid(
|
||||
response.data.status !== 'success'
|
||||
) {
|
||||
toast.error(
|
||||
'Could not fetch docker hub image info. Please check image name and tag and try again'
|
||||
'Could not fetch docker hub image informations. If you have it hosted in a 3rd party repository please fill in the container checksum manually.'
|
||||
)
|
||||
return false
|
||||
return containerInfo
|
||||
}
|
||||
|
||||
return true
|
||||
containerInfo.exists = true
|
||||
containerInfo.checksum = response.data.result.checksum
|
||||
return containerInfo
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error.message)
|
||||
toast.error(
|
||||
'Could not fetch docker hub image info. Please check image name and tag and try again'
|
||||
'Could not fetch docker hub image informations. If you have it hosted in a 3rd party repository please fill in the container checksum manually.'
|
||||
)
|
||||
return false
|
||||
return containerInfo
|
||||
}
|
||||
}
|
||||
|
||||
async function is3rdPartyImageValid(imageURL: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await axios.head(imageURL)
|
||||
if (!response || response.status !== 200) {
|
||||
toast.error(
|
||||
'Could not fetch docker image info. Please check URL and try again'
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error.message)
|
||||
toast.error(
|
||||
'Could not fetch docker image info. Please check URL and try again'
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateDockerImage(
|
||||
dockerImage: string,
|
||||
tag: string
|
||||
): Promise<boolean> {
|
||||
const isValid = isUrl(dockerImage)
|
||||
? await is3rdPartyImageValid(dockerImage)
|
||||
: await isDockerHubImageValid(dockerImage, tag)
|
||||
return isValid
|
||||
}
|
||||
|
192
src/@utils/veAllocation.ts
Normal file
192
src/@utils/veAllocation.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import { AllLocked } from 'src/@types/subgraph/AllLocked'
|
||||
import { OwnAllocations } from 'src/@types/subgraph/OwnAllocations'
|
||||
import { NftOwnAllocation } from 'src/@types/subgraph/NftOwnAllocation'
|
||||
import { OceanLocked } from 'src/@types/subgraph/OceanLocked'
|
||||
import { gql, OperationResult } from 'urql'
|
||||
import { fetchData, getQueryContext } from './subgraph'
|
||||
import axios from 'axios'
|
||||
import networkdata from '../../content/networks-metadata.json'
|
||||
import {
|
||||
getNetworkDataById,
|
||||
getNetworkType,
|
||||
NetworkType
|
||||
} from '@hooks/useNetworkMetadata'
|
||||
import { getAssetsFromNftList } from './aquarius'
|
||||
import { chainIdsSupported } from 'app.config'
|
||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||
|
||||
const AllLocked = gql`
|
||||
query AllLocked {
|
||||
veOCEANs(first: 1000) {
|
||||
lockedAmount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const OwnAllocations = gql`
|
||||
query OwnAllocations($address: String) {
|
||||
veAllocations(where: { allocationUser: $address }) {
|
||||
id
|
||||
nftAddress
|
||||
allocated
|
||||
}
|
||||
}
|
||||
`
|
||||
const NftOwnAllocation = gql`
|
||||
query NftOwnAllocation($address: String, $nftAddress: String) {
|
||||
veAllocations(
|
||||
where: { allocationUser: $address, nftAddress: $nftAddress }
|
||||
) {
|
||||
allocated
|
||||
}
|
||||
}
|
||||
`
|
||||
const OceanLocked = gql`
|
||||
query OceanLocked($address: String) {
|
||||
veOCEAN(id: $address) {
|
||||
id
|
||||
lockedAmount
|
||||
unlockTime
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export interface TotalVe {
|
||||
totalLocked: number
|
||||
totalAllocated: number
|
||||
}
|
||||
export interface Allocation {
|
||||
nftAddress: string
|
||||
allocation: number
|
||||
}
|
||||
|
||||
export interface AssetWithOwnAllocation {
|
||||
asset: AssetExtended
|
||||
allocation: string
|
||||
}
|
||||
|
||||
export function getVeChainNetworkId(assetNetworkId: number): number {
|
||||
const networkData = getNetworkDataById(networkdata, assetNetworkId)
|
||||
const networkType = getNetworkType(networkData)
|
||||
if (networkType === NetworkType.Mainnet) return 1
|
||||
else return 5
|
||||
}
|
||||
|
||||
export function getVeChainNetworkIds(assetNetworkIds: number[]): number[] {
|
||||
const veNetworkIds: number[] = []
|
||||
assetNetworkIds.forEach((x) => {
|
||||
const id = getVeChainNetworkId(x)
|
||||
veNetworkIds.indexOf(id) === -1 && veNetworkIds.push(id)
|
||||
})
|
||||
return veNetworkIds
|
||||
}
|
||||
export async function getNftOwnAllocation(
|
||||
userAddress: string,
|
||||
nftAddress: string,
|
||||
networkId: number
|
||||
): Promise<number> {
|
||||
const veNetworkId = getVeChainNetworkId(networkId)
|
||||
const queryContext = getQueryContext(veNetworkId)
|
||||
const fetchedAllocation: OperationResult<NftOwnAllocation, any> =
|
||||
await fetchData(
|
||||
NftOwnAllocation,
|
||||
{
|
||||
address: userAddress.toLowerCase(),
|
||||
nftAddress: nftAddress.toLowerCase()
|
||||
},
|
||||
queryContext
|
||||
)
|
||||
|
||||
return fetchedAllocation.data?.veAllocations[0]?.allocated
|
||||
}
|
||||
|
||||
export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
|
||||
const totals = {
|
||||
totalLocked: 0,
|
||||
totalAllocated: 0
|
||||
}
|
||||
|
||||
const queryContext = getQueryContext(1)
|
||||
|
||||
const response = await axios.post(`https://df-sql.oceandao.org/nftinfo`)
|
||||
totals.totalAllocated = response.data?.reduce(
|
||||
(previousValue: number, currentValue: { ve_allocated: string }) =>
|
||||
previousValue + Number(currentValue.ve_allocated),
|
||||
0
|
||||
)
|
||||
|
||||
const fetchedLocked: OperationResult<AllLocked, any> = await fetchData(
|
||||
AllLocked,
|
||||
null,
|
||||
queryContext
|
||||
)
|
||||
totals.totalLocked = fetchedLocked.data?.veOCEANs.reduce(
|
||||
(previousValue, currentValue) =>
|
||||
previousValue + Number(currentValue.lockedAmount),
|
||||
0
|
||||
)
|
||||
return totals
|
||||
}
|
||||
|
||||
export async function getLocked(
|
||||
userAddress: string,
|
||||
networkIds: number[]
|
||||
): Promise<number> {
|
||||
let total = 0
|
||||
const veNetworkIds = getVeChainNetworkIds(networkIds)
|
||||
for (let i = 0; i < veNetworkIds.length; i++) {
|
||||
const queryContext = getQueryContext(veNetworkIds[i])
|
||||
const fetchedLocked: OperationResult<OceanLocked, any> = await fetchData(
|
||||
OceanLocked,
|
||||
{ address: userAddress.toLowerCase() },
|
||||
queryContext
|
||||
)
|
||||
|
||||
fetchedLocked.data?.veOCEAN?.lockedAmount &&
|
||||
(total += Number(fetchedLocked.data?.veOCEAN?.lockedAmount))
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
export async function getOwnAllocations(
|
||||
networkIds: number[],
|
||||
userAddress: string
|
||||
): Promise<Allocation[]> {
|
||||
const allocations: Allocation[] = []
|
||||
const veNetworkIds = getVeChainNetworkIds(networkIds)
|
||||
for (let i = 0; i < veNetworkIds.length; i++) {
|
||||
const queryContext = getQueryContext(veNetworkIds[i])
|
||||
const fetchedAllocations: OperationResult<OwnAllocations, any> =
|
||||
await fetchData(
|
||||
OwnAllocations,
|
||||
{ address: userAddress.toLowerCase() },
|
||||
queryContext
|
||||
)
|
||||
|
||||
fetchedAllocations.data?.veAllocations.forEach(
|
||||
(x) =>
|
||||
x.allocated !== '0' &&
|
||||
allocations.push({
|
||||
nftAddress: x.nftAddress,
|
||||
allocation: x.allocated / 100
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return allocations
|
||||
}
|
||||
|
||||
export async function getOwnAssetsWithAllocation(
|
||||
networkIds: number[],
|
||||
userAddress: string
|
||||
): Promise<Asset[]> {
|
||||
const allocations = await getOwnAllocations(networkIds, userAddress)
|
||||
const assets = await getAssetsFromNftList(
|
||||
allocations.map((x) => x.nftAddress),
|
||||
chainIdsSupported,
|
||||
null
|
||||
)
|
||||
|
||||
return assets
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import AssetTeaser from '@shared/AssetTeaser/AssetTeaser'
|
||||
import AssetTeaser from '@shared/AssetTeaser'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import Pagination from '@shared/Pagination'
|
||||
import styles from './index.module.css'
|
||||
|
@ -1,62 +0,0 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import Link from 'next/link'
|
||||
import Dotdotdot from 'react-dotdotdot'
|
||||
import Price from '@shared/Price'
|
||||
import removeMarkdown from 'remove-markdown'
|
||||
import Publisher from '@shared/Publisher'
|
||||
import AssetType from '@shared/AssetType'
|
||||
import NetworkName from '@shared/NetworkName'
|
||||
import styles from './AssetTeaser.module.css'
|
||||
import { getServiceByName } from '@utils/ddo'
|
||||
|
||||
declare type AssetTeaserProps = {
|
||||
asset: AssetExtended
|
||||
noPublisher?: boolean
|
||||
}
|
||||
|
||||
export default function AssetTeaser({
|
||||
asset,
|
||||
noPublisher
|
||||
}: AssetTeaserProps): ReactElement {
|
||||
const { name, type, description } = asset.metadata
|
||||
const { datatokens } = asset
|
||||
const isCompute = Boolean(getServiceByName(asset, 'compute'))
|
||||
const accessType = isCompute ? 'compute' : 'access'
|
||||
const { owner } = asset.nft
|
||||
const { orders } = asset.stats
|
||||
return (
|
||||
<article className={`${styles.teaser} ${styles[type]}`}>
|
||||
<Link href={`/asset/${asset.id}`}>
|
||||
<a className={styles.link}>
|
||||
<header className={styles.header}>
|
||||
<div className={styles.symbol}>{datatokens[0]?.symbol}</div>
|
||||
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
|
||||
{name.slice(0, 200)}
|
||||
</Dotdotdot>
|
||||
{!noPublisher && (
|
||||
<Publisher account={owner} minimal className={styles.publisher} />
|
||||
)}
|
||||
</header>
|
||||
|
||||
<AssetType
|
||||
type={type}
|
||||
accessType={accessType}
|
||||
className={styles.typeDetails}
|
||||
totalSales={orders}
|
||||
/>
|
||||
|
||||
<div className={styles.content}>
|
||||
<Dotdotdot tagName="p" clamp={3}>
|
||||
{removeMarkdown(description?.substring(0, 300) || '')}
|
||||
</Dotdotdot>
|
||||
</div>
|
||||
|
||||
<footer className={styles.foot}>
|
||||
<Price accessDetails={asset.accessDetails} size="small" />
|
||||
<NetworkName networkId={asset.chainId} className={styles.network} />
|
||||
</footer>
|
||||
</a>
|
||||
</Link>
|
||||
</article>
|
||||
)
|
||||
}
|
@ -9,6 +9,8 @@
|
||||
height: 100%;
|
||||
color: var(--color-secondary);
|
||||
position: relative;
|
||||
padding-top: calc(var(--spacer) / 2);
|
||||
padding-bottom: calc(var(--spacer) / 2);
|
||||
/* for sticking footer to bottom */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -18,8 +20,12 @@
|
||||
background-color: var(--background-body);
|
||||
}
|
||||
|
||||
.detailLine {
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
margin-top: calc(var(--spacer) / 3);
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
/* for sticking footer to bottom */
|
||||
@ -27,7 +33,7 @@
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin-bottom: calc(var(--spacer) / 4);
|
||||
margin-bottom: calc(var(--spacer) / 3);
|
||||
}
|
||||
|
||||
.title {
|
||||
@ -37,36 +43,20 @@
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.publisher {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.foot {
|
||||
.footer {
|
||||
margin-top: calc(var(--spacer) / 4);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.foot p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.typeDetails {
|
||||
position: absolute;
|
||||
top: calc(var(--spacer) / 3);
|
||||
right: calc(var(--spacer) / 3);
|
||||
width: auto;
|
||||
.typeLabel {
|
||||
font-size: var(--font-size-mini);
|
||||
display: inline-block;
|
||||
border-left: 1px solid var(--border-color);
|
||||
padding-left: calc(var(--spacer) / 4);
|
||||
margin-left: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.network {
|
||||
font-size: var(--font-size-mini);
|
||||
position: absolute;
|
||||
right: calc(var(--spacer) / 3);
|
||||
bottom: calc(var(--spacer) / 3);
|
||||
.typeLabel:first-child {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
86
src/components/@shared/AssetTeaser/index.tsx
Normal file
86
src/components/@shared/AssetTeaser/index.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import Link from 'next/link'
|
||||
import Dotdotdot from 'react-dotdotdot'
|
||||
import Price from '@shared/Price'
|
||||
import removeMarkdown from 'remove-markdown'
|
||||
import Publisher from '@shared/Publisher'
|
||||
import AssetType from '@shared/AssetType'
|
||||
import NetworkName from '@shared/NetworkName'
|
||||
import styles from './index.module.css'
|
||||
import { getServiceByName } from '@utils/ddo'
|
||||
import { formatPrice } from '@shared/Price/PriceUnit'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
|
||||
declare type AssetTeaserProps = {
|
||||
asset: AssetExtended
|
||||
noPublisher?: boolean
|
||||
}
|
||||
|
||||
export default function AssetTeaser({
|
||||
asset,
|
||||
noPublisher
|
||||
}: AssetTeaserProps): ReactElement {
|
||||
const { name, type, description } = asset.metadata
|
||||
const { datatokens } = asset
|
||||
const isCompute = Boolean(getServiceByName(asset, 'compute'))
|
||||
const accessType = isCompute ? 'compute' : 'access'
|
||||
const { owner } = asset.nft
|
||||
const { orders, allocated } = asset.stats
|
||||
const isUnsupportedPricing = asset?.accessDetails?.type === 'NOT_SUPPORTED'
|
||||
const { locale } = useUserPreferences()
|
||||
|
||||
return (
|
||||
<article className={`${styles.teaser} ${styles[type]}`}>
|
||||
<Link href={`/asset/${asset.id}`}>
|
||||
<a className={styles.link}>
|
||||
<aside className={styles.detailLine}>
|
||||
<AssetType
|
||||
className={styles.typeLabel}
|
||||
type={type}
|
||||
accessType={accessType}
|
||||
/>
|
||||
<span className={styles.typeLabel}>
|
||||
{datatokens[0]?.symbol.substring(0, 9)}
|
||||
</span>
|
||||
<NetworkName
|
||||
networkId={asset.chainId}
|
||||
className={styles.typeLabel}
|
||||
/>
|
||||
</aside>
|
||||
<header className={styles.header}>
|
||||
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
|
||||
{name.slice(0, 200)}
|
||||
</Dotdotdot>
|
||||
{!noPublisher && <Publisher account={owner} minimal />}
|
||||
</header>
|
||||
<div className={styles.content}>
|
||||
<Dotdotdot tagName="p" clamp={3}>
|
||||
{removeMarkdown(description?.substring(0, 300) || '')}
|
||||
</Dotdotdot>
|
||||
</div>
|
||||
{isUnsupportedPricing ? (
|
||||
<strong>No pricing schema available</strong>
|
||||
) : (
|
||||
<Price accessDetails={asset.accessDetails} size="small" />
|
||||
)}
|
||||
<footer className={styles.footer}>
|
||||
{allocated && allocated > 0 ? (
|
||||
<span className={styles.typeLabel}>
|
||||
{allocated < 0
|
||||
? ''
|
||||
: `${formatPrice(allocated, locale)} veOCEAN`}
|
||||
</span>
|
||||
) : null}
|
||||
{orders && orders > 0 ? (
|
||||
<span className={styles.typeLabel}>
|
||||
{orders < 0
|
||||
? 'N/A'
|
||||
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`}
|
||||
</span>
|
||||
) : null}
|
||||
</footer>
|
||||
</a>
|
||||
</Link>
|
||||
</article>
|
||||
)
|
||||
}
|
@ -7,13 +7,11 @@ import Lock from '@images/lock.svg'
|
||||
export default function AssetType({
|
||||
type,
|
||||
accessType,
|
||||
className,
|
||||
totalSales
|
||||
className
|
||||
}: {
|
||||
type: string
|
||||
accessType: string
|
||||
className?: string
|
||||
totalSales?: number
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className={className || null}>
|
||||
@ -28,14 +26,6 @@ export default function AssetType({
|
||||
<div className={styles.typeLabel}>
|
||||
{type === 'dataset' ? 'dataset' : 'algorithm'}
|
||||
</div>
|
||||
|
||||
{(totalSales || totalSales === 0) && (
|
||||
<div className={styles.typeLabel}>
|
||||
{totalSales < 0
|
||||
? 'N/A'
|
||||
: `${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
.info {
|
||||
border-radius: var(--border-radius);
|
||||
padding: calc(var(--spacer) / 2);
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--background-highlight);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.info ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info li {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-small);
|
||||
margin-right: calc(var(--spacer) / 2);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.info li.success {
|
||||
color: var(--brand-alert-green);
|
||||
}
|
||||
|
||||
.info li.error {
|
||||
color: var(--brand-alert-red);
|
||||
}
|
||||
|
||||
.contianer {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height);
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
padding-right: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: -0.2rem;
|
||||
right: 0;
|
||||
font-size: var(--font-size-h3);
|
||||
cursor: pointer;
|
||||
color: var(--font-color-text);
|
||||
background-color: transparent;
|
||||
}
|
29
src/components/@shared/FormFields/ContainerInput/Info.tsx
Normal file
29
src/components/@shared/FormFields/ContainerInput/Info.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import styles from './Info.module.css'
|
||||
|
||||
export default function ImageInfo({
|
||||
image,
|
||||
tag,
|
||||
valid,
|
||||
handleClose
|
||||
}: {
|
||||
image: string
|
||||
tag: string
|
||||
valid: boolean
|
||||
handleClose(): void
|
||||
}): ReactElement {
|
||||
const displayText = valid
|
||||
? '✓ Image found, container checksum automatically added!'
|
||||
: 'x Container checksum could not be fetched automatically, please add it manually'
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<h3 className={styles.contianer}>{`Image: ${image} Tag: ${tag}`}</h3>
|
||||
<ul>
|
||||
<li className={valid ? styles.success : styles.error}>{displayText}</li>
|
||||
</ul>
|
||||
<button className={styles.removeButton} onClick={handleClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
83
src/components/@shared/FormFields/ContainerInput/index.tsx
Normal file
83
src/components/@shared/FormFields/ContainerInput/index.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import { useField, useFormikContext } from 'formik'
|
||||
import UrlInput from '../URLInput'
|
||||
import { InputProps } from '@shared/FormInput'
|
||||
import { FormPublishData } from 'src/components/Publish/_types'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import ImageInfo from './Info'
|
||||
import { getContainerChecksum } from '@utils/docker'
|
||||
|
||||
export default function ContainerInput(props: InputProps): ReactElement {
|
||||
const [field] = useField(props.name)
|
||||
const [fieldChecksum, metaChecksum, helpersChecksum] = useField(
|
||||
'metadata.dockerImageCustomChecksum'
|
||||
)
|
||||
|
||||
const { values, setFieldError, setFieldValue } =
|
||||
useFormikContext<FormPublishData>()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isValid, setIsValid] = useState(false)
|
||||
const [checked, setChecked] = useState(false)
|
||||
|
||||
async function handleValidation(e: React.SyntheticEvent, container: string) {
|
||||
e.preventDefault()
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const parsedContainerValue = container?.split(':')
|
||||
const imageName =
|
||||
parsedContainerValue?.length > 1
|
||||
? parsedContainerValue?.slice(0, -1).join(':')
|
||||
: parsedContainerValue[0]
|
||||
const tag =
|
||||
parsedContainerValue?.length > 1 ? parsedContainerValue?.at(-1) : ''
|
||||
const containerInfo = await getContainerChecksum(imageName, tag)
|
||||
setFieldValue('metadata.dockerImageCustom', imageName)
|
||||
setFieldValue('metadata.dockerImageCustomTag', tag)
|
||||
setChecked(true)
|
||||
if (containerInfo.checksum) {
|
||||
setFieldValue(
|
||||
'metadata.dockerImageCustomChecksum',
|
||||
containerInfo.checksum
|
||||
)
|
||||
helpersChecksum.setTouched(false)
|
||||
setIsValid(true)
|
||||
}
|
||||
} catch (error) {
|
||||
setFieldError(`${field.name}[0].url`, error.message)
|
||||
LoggerInstance.error(error.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setFieldValue('metadata.dockerImageCustom', '')
|
||||
setFieldValue('metadata.dockerImageCustomTag', '')
|
||||
setFieldValue('metadata.dockerImageCustomChecksum', '')
|
||||
setChecked(false)
|
||||
setIsValid(false)
|
||||
helpersChecksum.setTouched(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{checked ? (
|
||||
<ImageInfo
|
||||
image={values.metadata.dockerImageCustom}
|
||||
tag={values.metadata.dockerImageCustomTag}
|
||||
valid={isValid}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
) : (
|
||||
<UrlInput
|
||||
submitText="Use"
|
||||
{...props}
|
||||
name={`${field.name}[0].url`}
|
||||
checkUrl={false}
|
||||
isLoading={isLoading}
|
||||
handleButtonClick={handleValidation}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -12,12 +12,14 @@ export default function URLInput({
|
||||
handleButtonClick,
|
||||
isLoading,
|
||||
name,
|
||||
checkUrl,
|
||||
...props
|
||||
}: {
|
||||
submitText: string
|
||||
handleButtonClick(e: React.SyntheticEvent, data: string): void
|
||||
isLoading: boolean
|
||||
name: string
|
||||
checkUrl?: boolean
|
||||
}): ReactElement {
|
||||
const [field, meta] = useField(name)
|
||||
const [isButtonDisabled, setIsButtonDisabled] = useState(true)
|
||||
@ -28,7 +30,7 @@ export default function URLInput({
|
||||
setIsButtonDisabled(
|
||||
!field?.value ||
|
||||
field.value === '' ||
|
||||
!isUrl(field.value) ||
|
||||
(checkUrl && !isUrl(field.value)) ||
|
||||
field.value.includes('javascript:') ||
|
||||
meta?.error
|
||||
)
|
||||
|
@ -11,6 +11,7 @@ import AssetSelection, {
|
||||
} from '../FormFields/AssetSelection'
|
||||
import Nft from '../FormFields/Nft'
|
||||
import InputRadio from './InputRadio'
|
||||
import ContainerInput from '@shared/FormFields/ContainerInput'
|
||||
import TagsAutoComplete from './TagsAutoComplete'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
@ -108,6 +109,8 @@ export default function InputElement({
|
||||
)
|
||||
case 'files':
|
||||
return <FilesInput {...field} {...props} />
|
||||
case 'container':
|
||||
return <ContainerInput {...field} {...props} />
|
||||
case 'providerUrl':
|
||||
return <CustomProvider {...field} {...props} />
|
||||
case 'nft':
|
||||
|
@ -9,7 +9,7 @@ import styles from './index.module.css'
|
||||
export function NetworkIcon({ name }: { name: string }): ReactElement {
|
||||
const IconMapped = name.includes('ETH')
|
||||
? EthIcon
|
||||
: name.includes('Polygon')
|
||||
: name.includes('Polygon') || name.includes('Mumbai')
|
||||
? PolygonIcon
|
||||
: name.includes('Moon')
|
||||
? MoonbeamIcon
|
||||
|
@ -18,7 +18,7 @@ export default function Conversion({
|
||||
const { prices } = usePrices()
|
||||
const { currency, locale } = useUserPreferences()
|
||||
|
||||
const [priceConverted, setPriceConverted] = useState('0.00')
|
||||
const [priceConverted, setPriceConverted] = useState('0')
|
||||
// detect fiat, only have those kick in full @coingecko/cryptoformat formatting
|
||||
const isFiat = !isCrypto(currency)
|
||||
// isCrypto() only checks for BTC & ETH & unknown but seems sufficient for now
|
||||
@ -28,7 +28,7 @@ export default function Conversion({
|
||||
const priceTokenId = getCoingeckoTokenId(symbol)
|
||||
|
||||
useEffect(() => {
|
||||
if (!prices || !price || !priceTokenId || !prices[priceTokenId]) {
|
||||
if (!prices || !priceTokenId || !prices[priceTokenId]) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ export default function Conversion({
|
||||
isFiat ? currency : '',
|
||||
locale,
|
||||
false,
|
||||
{ decimalPlaces: 2 }
|
||||
{ decimalPlaces: price === 0 ? 0 : 2 }
|
||||
)
|
||||
// It's a hack! Wrap everything in the string which is not a number or `.` or `,`
|
||||
// with a span for consistent visual symbol formatting.
|
||||
|
@ -7,7 +7,7 @@
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.price > div:firt-child {
|
||||
.price > div:first-child {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,14 @@
|
||||
import { markdownToHtml } from '@utils/markdown'
|
||||
import React, { ReactElement } from 'react'
|
||||
import styles from './Empty.module.css'
|
||||
|
||||
export default function Empty({ message }: { message?: string }): ReactElement {
|
||||
return <div className={styles.empty}>{message || 'No results found'}</div>
|
||||
return (
|
||||
<div
|
||||
className={styles.empty}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToHtml(message) || 'No results found'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -20,6 +20,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.stat {
|
||||
border-left: 1px solid var(--border-color);
|
||||
padding-left: calc(var(--spacer) / 3.5);
|
||||
margin-left: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.stat:first-child {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--font-color-heading);
|
||||
|
@ -1,22 +1,61 @@
|
||||
import { useAsset } from '@context/Asset'
|
||||
import React from 'react'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import Tooltip from '@shared/atoms/Tooltip'
|
||||
import { formatPrice } from '@shared/Price/PriceUnit'
|
||||
import { getNftOwnAllocation } from '@utils/veAllocation'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import styles from './index.module.css'
|
||||
|
||||
export default function AssetStats() {
|
||||
const { locale } = useUserPreferences()
|
||||
const { asset } = useAsset()
|
||||
const { accountId } = useWeb3()
|
||||
|
||||
const [ownAllocation, setOwnAllocation] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!asset || !accountId) return
|
||||
|
||||
async function init() {
|
||||
const allocation = await getNftOwnAllocation(
|
||||
accountId,
|
||||
asset.nftAddress,
|
||||
asset.chainId
|
||||
)
|
||||
setOwnAllocation(allocation / 100)
|
||||
}
|
||||
init()
|
||||
}, [accountId, asset])
|
||||
|
||||
return (
|
||||
<footer className={styles.stats}>
|
||||
{!asset || !asset?.stats || asset?.stats?.orders < 0 ? (
|
||||
{asset?.stats?.allocated && asset?.stats?.allocated > 0 ? (
|
||||
<span className={styles.stat}>
|
||||
<span className={styles.number}>
|
||||
{formatPrice(asset.stats.allocated, locale)}
|
||||
</span>
|
||||
veOCEAN
|
||||
</span>
|
||||
) : null}
|
||||
{!asset?.stats || asset?.stats?.orders < 0 ? (
|
||||
'N/A'
|
||||
) : asset?.stats?.orders === 0 ? (
|
||||
'No sales yet'
|
||||
) : (
|
||||
<>
|
||||
<span className={styles.stat}>
|
||||
<span className={styles.number}>{asset.stats.orders}</span> sale
|
||||
{asset.stats.orders === 1 ? '' : 's'}
|
||||
</>
|
||||
</span>
|
||||
)}
|
||||
{ownAllocation && ownAllocation > 0 ? (
|
||||
<span className={styles.stat}>
|
||||
<span className={styles.number}>{ownAllocation}</span>% allocated
|
||||
<Tooltip
|
||||
content={`You have ${ownAllocation}% of your total veOCEAN allocated to this asset.`}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
@ -5,14 +5,17 @@ import Caret from '@images/caret.svg'
|
||||
|
||||
export default function ComputeHistory({
|
||||
title,
|
||||
children
|
||||
children,
|
||||
refetchJobs
|
||||
}: {
|
||||
title: string
|
||||
children: ReactNode
|
||||
refetchJobs?: any
|
||||
}): ReactElement {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
function handleClick() {
|
||||
async function handleClick() {
|
||||
await refetchJobs(true)
|
||||
setOpen(!open)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, ReactElement, useEffect } from 'react'
|
||||
import React, { useState, ReactElement, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Asset,
|
||||
DDO,
|
||||
@ -29,7 +29,8 @@ import {
|
||||
isOrderable,
|
||||
getAlgorithmAssetSelectionList,
|
||||
getAlgorithmsForAsset,
|
||||
getComputeEnviroment
|
||||
getComputeEnviroment,
|
||||
getComputeJobs
|
||||
} from '@utils/compute'
|
||||
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
|
||||
import AlgorithmDatasetsListForCompute from './AlgorithmDatasetsListForCompute'
|
||||
@ -43,7 +44,9 @@ import { handleComputeOrder } from '@utils/order'
|
||||
import { getComputeFeedback } from '@utils/feedback'
|
||||
import { getDummyWeb3 } from '@utils/web3'
|
||||
import { initializeProviderForCompute } from '@utils/provider'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
|
||||
const refreshInterval = 10000 // 10 sec.
|
||||
export default function Compute({
|
||||
asset,
|
||||
dtBalance,
|
||||
@ -58,6 +61,7 @@ export default function Compute({
|
||||
consumableFeedback?: string
|
||||
}): ReactElement {
|
||||
const { accountId, web3, isSupportedOceanNetwork } = useWeb3()
|
||||
const { chainIds } = useUserPreferences()
|
||||
const newAbortController = useAbortController()
|
||||
const newCancelToken = useCancelToken()
|
||||
|
||||
@ -91,6 +95,8 @@ export default function Compute({
|
||||
const [isRequestingAlgoOrderPrice, setIsRequestingAlgoOrderPrice] =
|
||||
useState(false)
|
||||
const [refetchJobs, setRefetchJobs] = useState(false)
|
||||
const [isLoadingJobs, setIsLoadingJobs] = useState(false)
|
||||
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
|
||||
|
||||
const hasDatatoken = Number(dtBalance) >= 1
|
||||
const isComputeButtonDisabled =
|
||||
@ -246,6 +252,44 @@ export default function Compute({
|
||||
})
|
||||
}, [asset, isUnsupportedPricing])
|
||||
|
||||
const fetchJobs = useCallback(
|
||||
async (type: string) => {
|
||||
if (!chainIds || chainIds.length === 0 || !accountId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
type === 'init' && setIsLoadingJobs(true)
|
||||
const computeJobs = await getComputeJobs(
|
||||
[asset?.chainId] || chainIds,
|
||||
accountId,
|
||||
asset,
|
||||
newCancelToken()
|
||||
)
|
||||
setJobs(computeJobs.computeJobs)
|
||||
setIsLoadingJobs(!computeJobs.isLoaded)
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error.message)
|
||||
setIsLoadingJobs(false)
|
||||
}
|
||||
},
|
||||
[accountId, asset, chainIds, isLoadingJobs, newCancelToken]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
fetchJobs('init')
|
||||
|
||||
// init periodic refresh for jobs
|
||||
const balanceInterval = setInterval(
|
||||
() => fetchJobs('repeat'),
|
||||
refreshInterval
|
||||
)
|
||||
|
||||
return () => {
|
||||
clearInterval(balanceInterval)
|
||||
}
|
||||
}, [refetchJobs])
|
||||
|
||||
// Output errors in toast UI
|
||||
useEffect(() => {
|
||||
const newError = error
|
||||
@ -447,11 +491,15 @@ export default function Compute({
|
||||
)}
|
||||
</footer>
|
||||
{accountId && asset?.accessDetails?.datatoken && (
|
||||
<ComputeHistory title="Your Compute Jobs">
|
||||
<ComputeHistory
|
||||
title="Your Compute Jobs"
|
||||
refetchJobs={() => setRefetchJobs(!refetchJobs)}
|
||||
>
|
||||
<ComputeJobs
|
||||
minimal
|
||||
assetChainIds={[asset?.chainId]}
|
||||
refetchJobs={refetchJobs}
|
||||
jobs={jobs}
|
||||
isLoading={isLoadingJobs}
|
||||
refetchJobs={() => setRefetchJobs(!refetchJobs)}
|
||||
/>
|
||||
</ComputeHistory>
|
||||
)}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PriceUnit from '@shared/Price/PriceUnit'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { StatsTotal } from './_types'
|
||||
|
||||
@ -8,9 +9,9 @@ export default function MarketStatsTotal({
|
||||
}): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<strong>{total.orders}</strong> orders across{' '}
|
||||
<strong>{total.nfts}</strong> assets with{' '}
|
||||
<strong>{total.datatokens}</strong> different datatokens.
|
||||
<PriceUnit price={total.orders} size="small" /> orders across{' '}
|
||||
<PriceUnit price={total.nfts} size="small" /> assets with{' '}
|
||||
<PriceUnit price={total.datatokens} size="small" /> different datatokens.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -6,4 +6,6 @@ export interface StatsTotal {
|
||||
nfts: number
|
||||
datatokens: number
|
||||
orders: number
|
||||
veAllocated: number
|
||||
veLocked: number
|
||||
}
|
||||
|
@ -14,11 +14,15 @@ import { useMarketMetadata } from '@context/MarketMetadata'
|
||||
import Tooltip from '@shared/atoms/Tooltip'
|
||||
import Markdown from '@shared/Markdown'
|
||||
import content from '../../../../content/footer.json'
|
||||
import { getTotalAllocatedAndLocked } from '@utils/veAllocation'
|
||||
import PriceUnit from '@shared/Price/PriceUnit'
|
||||
|
||||
const initialTotal: StatsTotal = {
|
||||
nfts: 0,
|
||||
datatokens: 0,
|
||||
orders: 0
|
||||
orders: 0,
|
||||
veAllocated: 0,
|
||||
veLocked: 0
|
||||
}
|
||||
|
||||
export default function MarketStats(): ReactElement {
|
||||
@ -34,15 +38,15 @@ export default function MarketStats(): ReactElement {
|
||||
// Set the main chain ids we want to display stats for
|
||||
//
|
||||
useEffect(() => {
|
||||
if (!networksList || !appConfig || !appConfig?.chainIdsSupported) return
|
||||
if (!networksList || !appConfig || !appConfig?.chainIds) return
|
||||
|
||||
const mainChainIdsList = filterNetworksByType(
|
||||
'mainnet',
|
||||
appConfig.chainIdsSupported,
|
||||
appConfig.chainIds,
|
||||
networksList
|
||||
)
|
||||
setMainChainIds(mainChainIdsList)
|
||||
}, [appConfig, appConfig?.chainIdsSupported, networksList])
|
||||
}, [appConfig, appConfig?.chainIds, networksList])
|
||||
|
||||
//
|
||||
// Helper: fetch data from subgraph
|
||||
@ -68,6 +72,12 @@ export default function MarketStats(): ReactElement {
|
||||
LoggerInstance.error('Error fetching global stats: ', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const veTotals = await getTotalAllocatedAndLocked()
|
||||
total.veAllocated = veTotals.totalAllocated
|
||||
total.veLocked = veTotals.totalLocked
|
||||
setTotal(total)
|
||||
|
||||
setData(newData)
|
||||
}, [mainChainIds])
|
||||
|
||||
@ -83,9 +93,7 @@ export default function MarketStats(): ReactElement {
|
||||
//
|
||||
useEffect(() => {
|
||||
if (!data || !mainChainIds?.length) return
|
||||
const newTotal: StatsTotal = {
|
||||
...initialTotal // always start calculating beginning from initial 0 values
|
||||
}
|
||||
const newTotal: StatsTotal = total
|
||||
|
||||
for (const chainId of mainChainIds) {
|
||||
try {
|
||||
@ -106,7 +114,7 @@ export default function MarketStats(): ReactElement {
|
||||
|
||||
return (
|
||||
<div className={styles.stats}>
|
||||
<>
|
||||
<div>
|
||||
<MarketStatsTotal total={total} />{' '}
|
||||
<Tooltip
|
||||
className={styles.info}
|
||||
@ -114,7 +122,12 @@ export default function MarketStats(): ReactElement {
|
||||
<Markdown className={styles.note} text={content.stats.note} />
|
||||
}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
<div>
|
||||
<PriceUnit price={total.veLocked} symbol="OCEAN" size="small" /> locked.{' '}
|
||||
<PriceUnit price={total.veAllocated} symbol="veOCEAN" size="small" />{' '}
|
||||
allocated.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -58,9 +58,6 @@
|
||||
width: auto;
|
||||
left: auto;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.input:focus + .button {
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
47
src/components/Home/Allocations/AssetListTable.tsx
Normal file
47
src/components/Home/Allocations/AssetListTable.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React from 'react'
|
||||
import Table, { TableOceanColumn } from '@shared/atoms/Table'
|
||||
import AssetTitle from '@shared/AssetList/AssetListTitle'
|
||||
import { AssetWithOwnAllocation } from '@utils/veAllocation'
|
||||
|
||||
const columns: TableOceanColumn<AssetWithOwnAllocation>[] = [
|
||||
{
|
||||
name: 'Dataset',
|
||||
selector: (row) => {
|
||||
const { metadata } = row.asset
|
||||
return <AssetTitle title={metadata.name} asset={row.asset} />
|
||||
},
|
||||
maxWidth: '45rem',
|
||||
grow: 1
|
||||
},
|
||||
{
|
||||
name: 'Datatoken Symbol',
|
||||
selector: (row) => row.asset.datatokens[0].symbol,
|
||||
maxWidth: '10rem'
|
||||
},
|
||||
{
|
||||
name: 'Allocated',
|
||||
selector: (row) => row.allocation,
|
||||
right: true,
|
||||
sortable: true
|
||||
}
|
||||
]
|
||||
|
||||
export default function AssetListTable({
|
||||
data,
|
||||
isLoading
|
||||
}: {
|
||||
data: AssetWithOwnAllocation[]
|
||||
isLoading: boolean
|
||||
}) {
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data}
|
||||
defaultSortFieldId={3}
|
||||
sortAsc={false}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={`Your allocated assets will appear here. [Lock your OCEAN](https://df.oceandao.org) to get started.`}
|
||||
noTableHead
|
||||
/>
|
||||
)
|
||||
}
|
3
src/components/Home/Allocations/index.module.css
Normal file
3
src/components/Home/Allocations/index.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.section {
|
||||
composes: section from '../index.module.css';
|
||||
}
|
93
src/components/Home/Allocations/index.tsx
Normal file
93
src/components/Home/Allocations/index.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import { AssetWithOwnAllocation, getOwnAllocations } from '@utils/veAllocation'
|
||||
import styles from './index.module.css'
|
||||
import {
|
||||
getFilterTerm,
|
||||
generateBaseQuery,
|
||||
queryMetadata
|
||||
} from '@utils/aquarius'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import AssetListTable from './AssetListTable'
|
||||
|
||||
export default function Allocations(): ReactElement {
|
||||
const { accountId } = useWeb3()
|
||||
const { chainIds } = useUserPreferences()
|
||||
const isMounted = useIsMounted()
|
||||
const newCancelToken = useCancelToken()
|
||||
|
||||
const [loading, setLoading] = useState<boolean>()
|
||||
const [data, setData] = useState<AssetWithOwnAllocation[]>()
|
||||
const [hasAllocations, setHasAllocations] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountId) return
|
||||
|
||||
async function checkAllocations() {
|
||||
try {
|
||||
const allocations = await getOwnAllocations(chainIds, accountId)
|
||||
setHasAllocations(allocations && allocations.length > 0)
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
}
|
||||
checkAllocations()
|
||||
}, [accountId, chainIds])
|
||||
|
||||
useEffect(() => {
|
||||
async function getAllocationAssets() {
|
||||
if (!hasAllocations) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const allocations = await getOwnAllocations(chainIds, accountId)
|
||||
setHasAllocations(allocations && allocations.length > 0)
|
||||
|
||||
const baseParams = {
|
||||
chainIds,
|
||||
filters: [
|
||||
getFilterTerm(
|
||||
'nftAddress',
|
||||
allocations.map((x) => x.nftAddress)
|
||||
)
|
||||
],
|
||||
ignorePurgatory: true
|
||||
} as BaseQueryParams
|
||||
|
||||
const query = generateBaseQuery(baseParams)
|
||||
|
||||
const result = await queryMetadata(query, newCancelToken())
|
||||
|
||||
const assetsWithAllocation: AssetWithOwnAllocation[] = []
|
||||
|
||||
result?.results.forEach((asset) => {
|
||||
const allocation = allocations.find(
|
||||
(x) => x.nftAddress.toLowerCase() === asset.nftAddress.toLowerCase()
|
||||
)
|
||||
assetsWithAllocation.push({
|
||||
asset,
|
||||
allocation: `${allocation.allocation} %`
|
||||
})
|
||||
})
|
||||
|
||||
if (!isMounted()) return
|
||||
setData(assetsWithAllocation)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
}
|
||||
getAllocationAssets()
|
||||
}, [hasAllocations, accountId, chainIds, isMounted, newCancelToken])
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<h3>Your Allocated Assets</h3>
|
||||
<AssetListTable data={data} isLoading={loading} />
|
||||
</section>
|
||||
)
|
||||
}
|
83
src/components/Home/SectionQueryResult.tsx
Normal file
83
src/components/Home/SectionQueryResult.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||
import AssetList from '@shared/AssetList'
|
||||
import { queryMetadata } from '@utils/aquarius'
|
||||
import React, { ReactElement, useState, useEffect } from 'react'
|
||||
import styles from './index.module.css'
|
||||
|
||||
function sortElements(items: Asset[], sorted: string[]) {
|
||||
items.sort(function (a, b) {
|
||||
return sorted.indexOf(a.nftAddress) - sorted.indexOf(b.nftAddress)
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
export default function SectionQueryResult({
|
||||
title,
|
||||
query,
|
||||
action,
|
||||
queryData
|
||||
}: {
|
||||
title: ReactElement | string
|
||||
query: SearchQuery
|
||||
action?: ReactElement
|
||||
queryData?: string[]
|
||||
}): ReactElement {
|
||||
const { chainIds } = useUserPreferences()
|
||||
const [result, setResult] = useState<PagedAssets>()
|
||||
const [loading, setLoading] = useState<boolean>()
|
||||
const isMounted = useIsMounted()
|
||||
const newCancelToken = useCancelToken()
|
||||
|
||||
useEffect(() => {
|
||||
if (!query) return
|
||||
|
||||
async function init() {
|
||||
if (chainIds.length === 0) {
|
||||
const result: PagedAssets = {
|
||||
results: [],
|
||||
page: 0,
|
||||
totalPages: 0,
|
||||
totalResults: 0,
|
||||
aggregations: undefined
|
||||
}
|
||||
setResult(result)
|
||||
setLoading(false)
|
||||
} else {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const result = await queryMetadata(query, newCancelToken())
|
||||
if (!isMounted()) return
|
||||
if (queryData && result?.totalResults > 0) {
|
||||
const sortedAssets = sortElements(result.results, queryData)
|
||||
const overflow = sortedAssets.length - 6
|
||||
sortedAssets.splice(sortedAssets.length - overflow, overflow)
|
||||
result.results = sortedAssets
|
||||
}
|
||||
setResult(result)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
init()
|
||||
}, [chainIds.length, isMounted, newCancelToken, query, queryData])
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<h3>{title}</h3>
|
||||
|
||||
<AssetList
|
||||
assets={result?.results}
|
||||
showPagination={false}
|
||||
isLoading={loading || !query}
|
||||
/>
|
||||
|
||||
{action && action}
|
||||
</section>
|
||||
)
|
||||
}
|
45
src/components/Home/TopTags/_utils.ts
Normal file
45
src/components/Home/TopTags/_utils.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { generateBaseQuery, queryMetadata } from '@utils/aquarius'
|
||||
import axios, { CancelToken } from 'axios'
|
||||
import { SortTermOptions } from 'src/@types/aquarius/SearchQuery'
|
||||
|
||||
export async function getTopTags(
|
||||
chainIds: number[],
|
||||
cancelToken: CancelToken
|
||||
): Promise<string[]> {
|
||||
const baseQueryParams = {
|
||||
chainIds,
|
||||
aggs: {
|
||||
topTags: {
|
||||
terms: {
|
||||
field: 'metadata.tags.keyword',
|
||||
size: 20,
|
||||
order: { totalSales: 'desc' }
|
||||
},
|
||||
aggs: {
|
||||
totalSales: {
|
||||
sum: {
|
||||
field: SortTermOptions.Orders
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
esPaginationOptions: { from: 0, size: 0 }
|
||||
} as BaseQueryParams
|
||||
|
||||
const query = generateBaseQuery(baseQueryParams)
|
||||
try {
|
||||
const result = await queryMetadata(query, cancelToken)
|
||||
const tagsList = result?.aggregations?.topTags?.buckets.map(
|
||||
(x: { key: any }) => x.key
|
||||
)
|
||||
return tagsList
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
LoggerInstance.log(error.message)
|
||||
} else {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
}
|
||||
}
|
3
src/components/Home/TopTags/index.module.css
Normal file
3
src/components/Home/TopTags/index.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.section {
|
||||
composes: section from '../index.module.css';
|
||||
}
|
51
src/components/Home/TopTags/index.tsx
Normal file
51
src/components/Home/TopTags/index.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import styles from './index.module.css'
|
||||
import Tags from '@shared/atoms/Tags'
|
||||
import { getTopTags } from './_utils'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import Loader from '@shared/atoms/Loader'
|
||||
|
||||
export default function TopTags({
|
||||
title,
|
||||
action
|
||||
}: {
|
||||
title: ReactElement | string
|
||||
action?: ReactElement
|
||||
}): ReactElement {
|
||||
const { chainIds } = useUserPreferences()
|
||||
const [result, setResult] = useState<string[]>([])
|
||||
const [loading, setLoading] = useState<boolean>()
|
||||
const newCancelToken = useCancelToken()
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
setLoading(true)
|
||||
if (chainIds.length === 0) {
|
||||
const result: string[] = []
|
||||
setResult(result)
|
||||
setLoading(false)
|
||||
} else {
|
||||
try {
|
||||
const tags = await getTopTags(chainIds, newCancelToken())
|
||||
setResult(tags)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error.message)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
}, [chainIds])
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<h3>{title}</h3>
|
||||
{loading ? <Loader /> : <Tags items={result} />}
|
||||
|
||||
{action && action}
|
||||
</section>
|
||||
)
|
||||
}
|
@ -1,103 +1,27 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import AssetList from '@shared/AssetList'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import Bookmarks from './Bookmarks'
|
||||
import { generateBaseQuery, queryMetadata } from '@utils/aquarius'
|
||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { generateBaseQuery } from '@utils/aquarius'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import { SortTermOptions } from '../../@types/aquarius/SearchQuery'
|
||||
import TopSales from './TopSales'
|
||||
import TopTags from './TopTags'
|
||||
import SectionQueryResult from './SectionQueryResult'
|
||||
import styles from './index.module.css'
|
||||
|
||||
function sortElements(items: Asset[], sorted: string[]) {
|
||||
items.sort(function (a, b) {
|
||||
return (
|
||||
sorted.indexOf(a.services[0].datatokenAddress.toLowerCase()) -
|
||||
sorted.indexOf(b.services[0].datatokenAddress.toLowerCase())
|
||||
)
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
function SectionQueryResult({
|
||||
title,
|
||||
query,
|
||||
action,
|
||||
queryData
|
||||
}: {
|
||||
title: ReactElement | string
|
||||
query: SearchQuery
|
||||
action?: ReactElement
|
||||
queryData?: string[]
|
||||
}) {
|
||||
const { chainIds } = useUserPreferences()
|
||||
const [result, setResult] = useState<PagedAssets>()
|
||||
const [loading, setLoading] = useState<boolean>()
|
||||
const isMounted = useIsMounted()
|
||||
const newCancelToken = useCancelToken()
|
||||
|
||||
useEffect(() => {
|
||||
if (!query) return
|
||||
|
||||
async function init() {
|
||||
if (chainIds.length === 0) {
|
||||
const result: PagedAssets = {
|
||||
results: [],
|
||||
page: 0,
|
||||
totalPages: 0,
|
||||
totalResults: 0,
|
||||
aggregations: undefined
|
||||
}
|
||||
setResult(result)
|
||||
setLoading(false)
|
||||
} else {
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await queryMetadata(query, newCancelToken())
|
||||
if (!isMounted()) return
|
||||
if (queryData && result?.totalResults > 0) {
|
||||
const sortedAssets = sortElements(result.results, queryData)
|
||||
const overflow = sortedAssets.length - 9
|
||||
sortedAssets.splice(sortedAssets.length - overflow, overflow)
|
||||
result.results = sortedAssets
|
||||
}
|
||||
setResult(result)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
init()
|
||||
}, [chainIds.length, isMounted, newCancelToken, query, queryData])
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<h3>{title}</h3>
|
||||
|
||||
<AssetList
|
||||
assets={result?.results}
|
||||
showPagination={false}
|
||||
isLoading={loading || !query}
|
||||
/>
|
||||
|
||||
{action && action}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
import Allocations from './Allocations'
|
||||
|
||||
export default function HomePage(): ReactElement {
|
||||
const { chainIds } = useUserPreferences()
|
||||
|
||||
const [queryLatest, setQueryLatest] = useState<SearchQuery>()
|
||||
const [queryMostSales, setQueryMostSales] = useState<SearchQuery>()
|
||||
const { chainIds } = useUserPreferences()
|
||||
const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>()
|
||||
|
||||
useEffect(() => {
|
||||
const baseParams = {
|
||||
chainIds,
|
||||
esPaginationOptions: {
|
||||
size: 9
|
||||
size: 6
|
||||
},
|
||||
sortOptions: {
|
||||
sortBy: SortTermOptions.Created
|
||||
@ -108,24 +32,44 @@ export default function HomePage(): ReactElement {
|
||||
const baseParamsSales = {
|
||||
chainIds,
|
||||
esPaginationOptions: {
|
||||
size: 9
|
||||
size: 6
|
||||
},
|
||||
sortOptions: {
|
||||
sortBy: SortTermOptions.Stats
|
||||
sortBy: SortTermOptions.Orders
|
||||
} as SortOptions
|
||||
} as BaseQueryParams
|
||||
setQueryMostSales(generateBaseQuery(baseParamsSales))
|
||||
const baseParamsAllocation = {
|
||||
chainIds,
|
||||
esPaginationOptions: {
|
||||
size: 6
|
||||
},
|
||||
sortOptions: {
|
||||
sortBy: SortTermOptions.Allocated
|
||||
} as SortOptions
|
||||
} as BaseQueryParams
|
||||
setQueryMostAllocation(generateBaseQuery(baseParamsAllocation))
|
||||
}, [chainIds])
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className={styles.section}>
|
||||
<h3>Bookmarks</h3>
|
||||
<h3>Your Bookmarks</h3>
|
||||
<Bookmarks />
|
||||
</section>
|
||||
|
||||
<Allocations />
|
||||
|
||||
<SectionQueryResult
|
||||
title="Highest veOCEAN Allocations"
|
||||
query={queryMostAllocation}
|
||||
/>
|
||||
|
||||
<SectionQueryResult title="Most Sales" query={queryMostSales} />
|
||||
|
||||
<TopSales title="Publishers With Most Sales" />
|
||||
<TopTags title="Top Tags By Sales" />
|
||||
|
||||
<SectionQueryResult
|
||||
title="Recently Published"
|
||||
query={queryLatest}
|
||||
@ -135,8 +79,6 @@ export default function HomePage(): ReactElement {
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<TopSales title="Publishers With Most Sales" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
.number {
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
align-items: baseline;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import Tooltip from '@shared/atoms/Tooltip'
|
||||
import styles from './NumberUnit.module.css'
|
||||
|
||||
interface NumberUnitProps {
|
||||
label: string
|
||||
label: string | ReactElement
|
||||
value: number | string | Element | ReactElement
|
||||
small?: boolean
|
||||
icon?: Element | ReactElement
|
||||
|
@ -4,3 +4,24 @@
|
||||
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
|
||||
margin-top: var(--spacer);
|
||||
}
|
||||
|
||||
.stats [class^='PriceUnit_symbol'] {
|
||||
color: var(--color-secondary);
|
||||
font-weight: var(--font-weight-base);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.stats .link,
|
||||
.stats .link:hover {
|
||||
color: var(--color-primary);
|
||||
font-size: var(--font-size-small);
|
||||
text-decoration: none;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.stats [class^='PriceUnit_price'] {
|
||||
color: var(--color-secondary);
|
||||
font-weight: var(--font-weight-base);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
@ -6,16 +6,31 @@ import NumberUnit from './NumberUnit'
|
||||
import styles from './Stats.module.css'
|
||||
import { useProfile } from '@context/Profile'
|
||||
import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing'
|
||||
import { getLocked } from '@utils/veAllocation'
|
||||
import PriceUnit from '@shared/Price/PriceUnit'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
|
||||
export default function Stats({
|
||||
accountId
|
||||
}: {
|
||||
accountId: string
|
||||
}): ReactElement {
|
||||
const web3 = useWeb3()
|
||||
const { chainIds } = useUserPreferences()
|
||||
const { assets, assetsTotal, sales } = useProfile()
|
||||
|
||||
const [totalSales, setTotalSales] = useState(0)
|
||||
const [lockedOcean, setLockedOcean] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
async function getLockedOcean() {
|
||||
if (!accountId) return
|
||||
const locked = await getLocked(accountId, chainIds)
|
||||
setLockedOcean(locked)
|
||||
}
|
||||
getLockedOcean()
|
||||
}, [accountId, chainIds])
|
||||
|
||||
useEffect(() => {
|
||||
if (!assets || !accountId || !chainIds) return
|
||||
@ -59,6 +74,30 @@ export default function Stats({
|
||||
value={sales < 0 ? 0 : sales}
|
||||
/>
|
||||
<NumberUnit label="Published" value={assetsTotal} />
|
||||
<NumberUnit
|
||||
label={
|
||||
lockedOcean === 0 && accountId === web3.accountId ? (
|
||||
<Button
|
||||
className={styles.link}
|
||||
style="text"
|
||||
href="https://df.oceandao.org"
|
||||
>
|
||||
Lock OCEAN
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<PriceUnit price={lockedOcean} symbol="OCEAN" /> locked
|
||||
</>
|
||||
)
|
||||
}
|
||||
value={
|
||||
<Conversion
|
||||
price={lockedOcean > 0 ? lockedOcean : 0}
|
||||
symbol="OCEAN"
|
||||
hideApproximateSymbol
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { ReactElement, useEffect, useState, useCallback } from 'react'
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import Time from '@shared/atoms/Time'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import Table, { TableOceanColumn } from '@shared/atoms/Table'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
@ -8,11 +7,7 @@ import Details from './Details'
|
||||
import Refresh from '@images/refresh.svg'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import NetworkName from '@shared/NetworkName'
|
||||
import { getComputeJobs } from '@utils/compute'
|
||||
import styles from './index.module.css'
|
||||
import { useAsset } from '@context/Asset'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import AssetListTitle from '@shared/AssetList/AssetListTitle'
|
||||
|
||||
export function Status({ children }: { children: string }): ReactElement {
|
||||
@ -51,48 +46,19 @@ const columns: TableOceanColumn<ComputeJobMetaData>[] = [
|
||||
|
||||
export default function ComputeJobs({
|
||||
minimal,
|
||||
assetChainIds,
|
||||
jobs,
|
||||
isLoading,
|
||||
refetchJobs
|
||||
}: {
|
||||
minimal?: boolean
|
||||
assetChainIds?: number[]
|
||||
refetchJobs?: boolean
|
||||
jobs?: ComputeJobMetaData[]
|
||||
isLoading?: boolean
|
||||
refetchJobs?: any
|
||||
}): ReactElement {
|
||||
const { accountId } = useWeb3()
|
||||
const { asset } = useAsset()
|
||||
const { chainIds } = useUserPreferences()
|
||||
const isMounted = useIsMounted()
|
||||
const newCancelToken = useCancelToken()
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
|
||||
const [columnsMinimal] = useState([columns[4], columns[5], columns[3]])
|
||||
|
||||
const fetchJobs = useCallback(async () => {
|
||||
if (!chainIds || chainIds.length === 0 || !accountId) {
|
||||
setJobs([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const jobs = await getComputeJobs(
|
||||
assetChainIds || chainIds,
|
||||
accountId,
|
||||
asset,
|
||||
newCancelToken()
|
||||
)
|
||||
isMounted() && setJobs(jobs.computeJobs)
|
||||
setIsLoading(!jobs.isLoaded)
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
}, [chainIds, accountId, asset, isMounted, assetChainIds, newCancelToken])
|
||||
|
||||
useEffect(() => {
|
||||
fetchJobs()
|
||||
}, [fetchJobs, refetchJobs])
|
||||
|
||||
return accountId ? (
|
||||
<>
|
||||
{jobs?.length >= 0 && !minimal && (
|
||||
@ -100,7 +66,7 @@ export default function ComputeJobs({
|
||||
style="text"
|
||||
size="small"
|
||||
title="Refresh compute jobs"
|
||||
onClick={async () => await fetchJobs()}
|
||||
onClick={async () => await refetchJobs(true)}
|
||||
disabled={isLoading}
|
||||
className={styles.refresh}
|
||||
>
|
||||
|
@ -1,17 +1,30 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react'
|
||||
import Tabs from '@shared/atoms/Tabs'
|
||||
import PublishedList from './PublishedList'
|
||||
import Downloads from './Downloads'
|
||||
import ComputeJobs from './ComputeJobs'
|
||||
import styles from './index.module.css'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import { chainIds } from 'app.config'
|
||||
import { getComputeJobs } from '@utils/compute'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
|
||||
interface HistoryTab {
|
||||
title: string
|
||||
content: JSX.Element
|
||||
}
|
||||
|
||||
function getTabs(accountId: string, userAccountId: string): HistoryTab[] {
|
||||
const refreshInterval = 10000 // 10 sec.
|
||||
function getTabs(
|
||||
accountId: string,
|
||||
userAccountId: string,
|
||||
jobs: ComputeJobMetaData[],
|
||||
isLoadingJobs: boolean,
|
||||
refetchJobs: boolean,
|
||||
setRefetchJobs: any
|
||||
): HistoryTab[] {
|
||||
const defaultTabs: HistoryTab[] = [
|
||||
{
|
||||
title: 'Published',
|
||||
@ -24,7 +37,13 @@ function getTabs(accountId: string, userAccountId: string): HistoryTab[] {
|
||||
]
|
||||
const computeTab: HistoryTab = {
|
||||
title: 'Compute Jobs',
|
||||
content: <ComputeJobs />
|
||||
content: (
|
||||
<ComputeJobs
|
||||
jobs={jobs}
|
||||
isLoading={isLoadingJobs}
|
||||
refetchJobs={() => setRefetchJobs(!refetchJobs)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (accountId === userAccountId) {
|
||||
defaultTabs.push(computeTab)
|
||||
@ -38,10 +57,62 @@ export default function HistoryPage({
|
||||
accountIdentifier: string
|
||||
}): ReactElement {
|
||||
const { accountId } = useWeb3()
|
||||
const { chainIds } = useUserPreferences()
|
||||
const newCancelToken = useCancelToken()
|
||||
|
||||
const url = new URL(location.href)
|
||||
const defaultTab = url.searchParams.get('defaultTab')
|
||||
const tabs = getTabs(accountIdentifier, accountId)
|
||||
|
||||
const [refetchJobs, setRefetchJobs] = useState(false)
|
||||
const [isLoadingJobs, setIsLoadingJobs] = useState(false)
|
||||
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
|
||||
|
||||
const fetchJobs = useCallback(
|
||||
async (type: string) => {
|
||||
if (!chainIds || chainIds.length === 0 || !accountId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
type === 'init' && setIsLoadingJobs(true)
|
||||
const computeJobs = await getComputeJobs(
|
||||
chainIds,
|
||||
accountId,
|
||||
null,
|
||||
newCancelToken()
|
||||
)
|
||||
setJobs(computeJobs.computeJobs)
|
||||
setIsLoadingJobs(!computeJobs.isLoaded)
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error.message)
|
||||
setIsLoadingJobs(false)
|
||||
}
|
||||
},
|
||||
[accountId, chainIds, isLoadingJobs, newCancelToken]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
fetchJobs('init')
|
||||
|
||||
// init periodic refresh for jobs
|
||||
const balanceInterval = setInterval(
|
||||
() => fetchJobs('repeat'),
|
||||
refreshInterval
|
||||
)
|
||||
|
||||
return () => {
|
||||
clearInterval(balanceInterval)
|
||||
}
|
||||
}, [refetchJobs])
|
||||
|
||||
const tabs = getTabs(
|
||||
accountIdentifier,
|
||||
accountId,
|
||||
jobs,
|
||||
isLoadingJobs,
|
||||
refetchJobs,
|
||||
setRefetchJobs
|
||||
)
|
||||
|
||||
let defaultTabIndex = 0
|
||||
defaultTab === 'ComputeJobs' ? (defaultTabIndex = 4) : (defaultTabIndex = 0)
|
||||
|
@ -26,7 +26,6 @@ export default function AvailableNetworks(): ReactElement {
|
||||
{ title: 'Main', data: networksMain },
|
||||
{ title: 'Test', data: networksTest }
|
||||
]
|
||||
|
||||
const networkList = (networks: number[]) =>
|
||||
networks.map((chainId) => (
|
||||
<li key={chainId}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { BoxSelectionOption } from '@shared/FormFields/BoxSelection'
|
||||
import Input from '@shared/FormInput'
|
||||
import { Field, useFormikContext } from 'formik'
|
||||
import { Field, useField, useFormikContext } from 'formik'
|
||||
import React, { ReactElement, useEffect } from 'react'
|
||||
import content from '../../../../content/publish/form.json'
|
||||
import { FormPublishData } from '../_types'
|
||||
@ -23,6 +23,8 @@ export default function MetadataFields(): ReactElement {
|
||||
// connect with Form state, use for conditional field rendering
|
||||
const { values, setFieldValue } = useFormikContext<FormPublishData>()
|
||||
|
||||
const [field, meta] = useField('metadata.dockerImageCustomChecksum')
|
||||
|
||||
// BoxSelection component is not a Formik component
|
||||
// so we need to handle checked state manually.
|
||||
const assetTypeOptions: BoxSelectionOption[] = [
|
||||
@ -124,11 +126,14 @@ export default function MetadataFields(): ReactElement {
|
||||
/>
|
||||
<Field
|
||||
{...getFieldContent(
|
||||
'dockerImageCustomTag',
|
||||
'dockerImageChecksum',
|
||||
content.metadata.fields
|
||||
)}
|
||||
component={Input}
|
||||
name="metadata.dockerImageCustomTag"
|
||||
name="metadata.dockerImageCustomChecksum"
|
||||
disabled={
|
||||
values.metadata.dockerImageCustomChecksum && !meta.touched
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
{...getFieldContent(
|
||||
|
@ -96,17 +96,15 @@ export const initialValues: FormPublishData = {
|
||||
export const algorithmContainerPresets: MetadataAlgorithmContainer[] = [
|
||||
{
|
||||
image: 'node',
|
||||
tag: '18.6.0', // TODO: Put this back to latest once merging the PR that fetches the container digest from docker hub via dockerhub-proxy
|
||||
tag: 'latest',
|
||||
entrypoint: 'node $ALGO',
|
||||
checksum:
|
||||
'sha256:c60726646352202d95de70d9e8393c15f382f8c6074afc5748b7e570ccd5995f'
|
||||
checksum: ''
|
||||
},
|
||||
{
|
||||
image: 'python',
|
||||
tag: '3.10.5', // TODO: Put this back to latest once merging the PR that fetches the container digest from docker hub via dockerhub-proxy
|
||||
tag: 'latest',
|
||||
entrypoint: 'python $ALGO',
|
||||
checksum:
|
||||
'sha256:607635763e54907fd75397fedfeb83890e62a0f9b54a1d99d27d748c5d269be4'
|
||||
checksum: ''
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -26,20 +26,24 @@ import {
|
||||
publisherMarketFixedSwapFee
|
||||
} from '../../../app.config'
|
||||
import { sanitizeUrl } from '@utils/url'
|
||||
import { getContainerChecksum } from '@utils/docker'
|
||||
|
||||
function getUrlFileExtension(fileUrl: string): string {
|
||||
const splittedFileUrl = fileUrl.split('.')
|
||||
return splittedFileUrl[splittedFileUrl.length - 1]
|
||||
}
|
||||
|
||||
function getAlgorithmContainerPreset(
|
||||
async function getAlgorithmContainerPreset(
|
||||
dockerImage: string
|
||||
): MetadataAlgorithmContainer {
|
||||
): Promise<MetadataAlgorithmContainer> {
|
||||
if (dockerImage === '') return
|
||||
|
||||
const preset = algorithmContainerPresets.find(
|
||||
(preset) => `${preset.image}:${preset.tag}` === dockerImage
|
||||
)
|
||||
preset.checksum = await (
|
||||
await getContainerChecksum(preset.image, preset.tag)
|
||||
).checksum
|
||||
return preset
|
||||
}
|
||||
|
||||
@ -80,6 +84,11 @@ export async function transformPublishFormToDdo(
|
||||
const currentTime = dateToStringNoMS(new Date())
|
||||
const isPreview = !datatokenAddress && !nftAddress
|
||||
|
||||
const algorithmContainerPresets =
|
||||
type === 'algorithm' && dockerImage !== '' && dockerImage !== 'custom'
|
||||
? await getAlgorithmContainerPreset(dockerImage)
|
||||
: null
|
||||
|
||||
// Transform from files[0].url to string[] assuming only 1 file
|
||||
const filesTransformed = files?.length &&
|
||||
files[0].valid && [sanitizeUrl(files[0].url)]
|
||||
@ -110,20 +119,19 @@ export async function transformPublishFormToDdo(
|
||||
entrypoint:
|
||||
dockerImage === 'custom'
|
||||
? dockerImageCustomEntrypoint
|
||||
: getAlgorithmContainerPreset(dockerImage).entrypoint,
|
||||
: algorithmContainerPresets.entrypoint,
|
||||
image:
|
||||
dockerImage === 'custom'
|
||||
? dockerImageCustom
|
||||
: getAlgorithmContainerPreset(dockerImage).image,
|
||||
: algorithmContainerPresets.image,
|
||||
tag:
|
||||
dockerImage === 'custom'
|
||||
? dockerImageCustomTag
|
||||
: getAlgorithmContainerPreset(dockerImage).tag,
|
||||
: algorithmContainerPresets.tag,
|
||||
checksum:
|
||||
dockerImage === 'custom'
|
||||
? // ? dockerImageCustomChecksum
|
||||
''
|
||||
: getAlgorithmContainerPreset(dockerImage).checksum
|
||||
? dockerImageCustomChecksum
|
||||
: algorithmContainerPresets.checksum
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -3,13 +3,11 @@ div.filterList {
|
||||
white-space: normal;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
gap: calc(var(--spacer) / 4) calc(var(--spacer) / 2);
|
||||
flex-direction: column;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.filter {
|
||||
.filter,
|
||||
.filterList > div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
.row {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
|
@ -8,15 +8,9 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 55rem) {
|
||||
.sortList {
|
||||
align-self: flex-end;
|
||||
overflow-y: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.sortLabel {
|
||||
composes: label from '@shared/FormInput/Label.module.css';
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: calc(var(--spacer) / 2);
|
||||
margin-right: calc(var(--spacer) / 1.5);
|
||||
@ -26,7 +20,7 @@
|
||||
}
|
||||
|
||||
.sorted {
|
||||
display: flex;
|
||||
display: inline-block;
|
||||
padding: calc(var(--spacer) / 6) calc(var(--spacer) / 2);
|
||||
margin-right: calc(var(--spacer) / 4);
|
||||
color: var(--color-secondary);
|
||||
@ -35,6 +29,7 @@
|
||||
font-weight: var(--font-weight-base);
|
||||
background: var(--background-content);
|
||||
box-shadow: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sorted:hover,
|
||||
@ -47,7 +42,6 @@
|
||||
}
|
||||
|
||||
.direction {
|
||||
display: flex;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
|
@ -13,7 +13,10 @@ const cx = classNames.bind(styles)
|
||||
|
||||
const sortItems = [
|
||||
{ display: 'Relevance', value: SortTermOptions.Relevance },
|
||||
{ display: 'Published', value: SortTermOptions.Created }
|
||||
{ display: 'Published', value: SortTermOptions.Created },
|
||||
{ display: 'Sales', value: SortTermOptions.Orders },
|
||||
{ display: 'Total allocation', value: SortTermOptions.Allocated },
|
||||
{ display: 'Price', value: SortTermOptions.Price }
|
||||
]
|
||||
|
||||
export default function Sort({
|
||||
|
Loading…
Reference in New Issue
Block a user