diff --git a/content/publish/form.json b/content/publish/form.json index 69daabd8e..e625624ac 100644 --- a/content/publish/form.json +++ b/content/publish/form.json @@ -54,21 +54,22 @@ }, { "name": "dockerImageCustom", - "label": "Docker Image URL", - "placeholder": "e.g. oceanprotocol/algo_dockers or https://example.com/image_path", - "help": "Provide the name of a public Docker image or the full url if you have it hosted in a 3rd party repo", + "label": "Custom Docker Image", + "placeholder": "e.g. oceanprotocol/algo_dockers:node-vibrant or quay.io/startx/mariadb", + "help": "Provide the name and the tag of a public Docker hub image or the custom image if you have it hosted in a 3rd party repository", + "type": "container", "required": true }, { - "name": "dockerImageCustomTag", - "label": "Docker Image Tag", - "placeholder": "e.g. latest", - "help": "Provide the tag for your Docker image.", + "name": "dockerImageChecksum", + "label": "Docker Image Checksum", + "placeholder": "e.g. sha256:xiXqb7Vet0FbN9q0GFMgUdi5C22wjJT0i2G6lYKC2jl6QxkKzVz7KaPDgqfTMjNF", + "help": "Provide the checksum(DIGEST) of your docker image.", "required": true }, { "name": "dockerImageCustomEntrypoint", - "label": "Docker Entrypoint", + "label": "Docker Image Entrypoint", "placeholder": "e.g. python $ALGO", "help": "Provide the entrypoint for your algorithm.", "required": true diff --git a/content/site.json b/content/site.json index 0c231e412..3e3378201 100644 --- a/content/site.json +++ b/content/site.json @@ -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." } diff --git a/package-lock.json b/package-lock.json index 14589e725..5c14421a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 60ad602b2..454d933f2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/@hooks/useNetworkMetadata/utils.ts b/src/@hooks/useNetworkMetadata/utils.ts index f3ec518f1..c97dcdc2e 100644 --- a/src/@hooks/useNetworkMetadata/utils.ts +++ b/src/@hooks/useNetworkMetadata/utils.ts @@ -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 diff --git a/src/@types/aquarius/SearchQuery.ts b/src/@types/aquarius/SearchQuery.ts index c4e0dea5c..026adbcfd 100644 --- a/src/@types/aquarius/SearchQuery.ts +++ b/src/@types/aquarius/SearchQuery.ts @@ -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 diff --git a/src/@utils/aquarius.ts b/src/@utils/aquarius.ts index 1491144bc..2e0ed6c60 100644 --- a/src/@utils/aquarius.ts +++ b/src/@utils/aquarius.ts @@ -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 { + 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 } } } diff --git a/src/@utils/docker.ts b/src/@utils/docker.ts index 9453f5447..ee917828d 100644 --- a/src/@utils/docker.ts +++ b/src/@utils/docker.ts @@ -1,16 +1,27 @@ import { LoggerInstance } from '@oceanprotocol/lib' import axios from 'axios' -import isUrl from 'is-url-superb' import { toast } from 'react-toastify' -async function isDockerHubImageValid( +export interface dockerContainerInfo { + exists: boolean + checksum: string +} + +export async function getContainerChecksum( image: string, tag: string -): Promise { +): Promise { + const containerInfo: dockerContainerInfo = { + exists: false, + checksum: null + } try { const response = await axios.post( `https://dockerhub-proxy.oceanprotocol.com`, - { image, tag } + { + image, + tag + } ) if ( !response || @@ -18,46 +29,18 @@ async function isDockerHubImageValid( response.data.status !== 'success' ) { toast.error( - 'Could not fetch docker hub image info. Please check image name and tag and try again' + 'Could not fetch docker hub image informations. If you have it hosted in a 3rd party repository please fill in the container checksum manually.' ) - return false + return containerInfo } - - return true + containerInfo.exists = true + containerInfo.checksum = response.data.result.checksum + return containerInfo } catch (error) { LoggerInstance.error(error.message) toast.error( - 'Could not fetch docker hub image info. Please check image name and tag and try again' + 'Could not fetch docker hub image informations. If you have it hosted in a 3rd party repository please fill in the container checksum manually.' ) - return false + return containerInfo } } - -async function is3rdPartyImageValid(imageURL: string): Promise { - try { - const response = await axios.head(imageURL) - if (!response || response.status !== 200) { - toast.error( - 'Could not fetch docker image info. Please check URL and try again' - ) - return false - } - return true - } catch (error) { - LoggerInstance.error(error.message) - toast.error( - 'Could not fetch docker image info. Please check URL and try again' - ) - return false - } -} - -export async function validateDockerImage( - dockerImage: string, - tag: string -): Promise { - const isValid = isUrl(dockerImage) - ? await is3rdPartyImageValid(dockerImage) - : await isDockerHubImageValid(dockerImage, tag) - return isValid -} diff --git a/src/@utils/veAllocation.ts b/src/@utils/veAllocation.ts new file mode 100644 index 000000000..2b1c3efad --- /dev/null +++ b/src/@utils/veAllocation.ts @@ -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 { + const veNetworkId = getVeChainNetworkId(networkId) + const queryContext = getQueryContext(veNetworkId) + const fetchedAllocation: OperationResult = + await fetchData( + NftOwnAllocation, + { + address: userAddress.toLowerCase(), + nftAddress: nftAddress.toLowerCase() + }, + queryContext + ) + + return fetchedAllocation.data?.veAllocations[0]?.allocated +} + +export async function getTotalAllocatedAndLocked(): Promise { + 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 = 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 { + let total = 0 + const veNetworkIds = getVeChainNetworkIds(networkIds) + for (let i = 0; i < veNetworkIds.length; i++) { + const queryContext = getQueryContext(veNetworkIds[i]) + const fetchedLocked: OperationResult = 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 { + const allocations: Allocation[] = [] + const veNetworkIds = getVeChainNetworkIds(networkIds) + for (let i = 0; i < veNetworkIds.length; i++) { + const queryContext = getQueryContext(veNetworkIds[i]) + const fetchedAllocations: OperationResult = + 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 { + const allocations = await getOwnAllocations(networkIds, userAddress) + const assets = await getAssetsFromNftList( + allocations.map((x) => x.nftAddress), + chainIdsSupported, + null + ) + + return assets +} diff --git a/src/components/@shared/AssetList/index.tsx b/src/components/@shared/AssetList/index.tsx index 6fa0cc51e..7de0b25b8 100644 --- a/src/components/@shared/AssetList/index.tsx +++ b/src/components/@shared/AssetList/index.tsx @@ -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' diff --git a/src/components/@shared/AssetTeaser/AssetTeaser.tsx b/src/components/@shared/AssetTeaser/AssetTeaser.tsx deleted file mode 100644 index d1dbeea34..000000000 --- a/src/components/@shared/AssetTeaser/AssetTeaser.tsx +++ /dev/null @@ -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 ( - - ) -} diff --git a/src/components/@shared/AssetTeaser/AssetTeaser.module.css b/src/components/@shared/AssetTeaser/index.module.css similarity index 58% rename from src/components/@shared/AssetTeaser/AssetTeaser.module.css rename to src/components/@shared/AssetTeaser/index.module.css index daa73e675..ca468094f 100644 --- a/src/components/@shared/AssetTeaser/AssetTeaser.module.css +++ b/src/components/@shared/AssetTeaser/index.module.css @@ -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; } diff --git a/src/components/@shared/AssetTeaser/index.tsx b/src/components/@shared/AssetTeaser/index.tsx new file mode 100644 index 000000000..f0db24936 --- /dev/null +++ b/src/components/@shared/AssetTeaser/index.tsx @@ -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 ( +
+ + + +
+ + {name.slice(0, 200)} + + {!noPublisher && } +
+
+ + {removeMarkdown(description?.substring(0, 300) || '')} + +
+ {isUnsupportedPricing ? ( + No pricing schema available + ) : ( + + )} +
+ {allocated && allocated > 0 ? ( + + {allocated < 0 + ? '' + : `${formatPrice(allocated, locale)} veOCEAN`} + + ) : null} + {orders && orders > 0 ? ( + + {orders < 0 + ? 'N/A' + : `${orders} ${orders === 1 ? 'sale' : 'sales'}`} + + ) : null} +
+
+ +
+ ) +} diff --git a/src/components/@shared/AssetType/index.tsx b/src/components/@shared/AssetType/index.tsx index 7d3384c02..14be396dc 100644 --- a/src/components/@shared/AssetType/index.tsx +++ b/src/components/@shared/AssetType/index.tsx @@ -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 (
@@ -28,14 +26,6 @@ export default function AssetType({
{type === 'dataset' ? 'dataset' : 'algorithm'}
- - {(totalSales || totalSales === 0) && ( -
- {totalSales < 0 - ? 'N/A' - : `${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`} -
- )}
) } diff --git a/src/components/@shared/FormFields/ContainerInput/Info.module.css b/src/components/@shared/FormFields/ContainerInput/Info.module.css new file mode 100644 index 000000000..ed25b655f --- /dev/null +++ b/src/components/@shared/FormFields/ContainerInput/Info.module.css @@ -0,0 +1,48 @@ +.info { + border-radius: var(--border-radius); + padding: calc(var(--spacer) / 2); + border: 1px solid var(--border-color); + background-color: var(--background-highlight); + position: relative; +} + +.info ul { + margin: 0; +} + +.info li { + display: inline-block; + font-size: var(--font-size-small); + margin-right: calc(var(--spacer) / 2); + color: var(--color-secondary); +} + +.info li.success { + color: var(--brand-alert-green); +} + +.info li.error { + color: var(--brand-alert-red); +} + +.contianer { + margin: 0; + font-size: var(--font-size-base); + line-height: var(--line-height); + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-all; + padding-right: calc(var(--spacer) / 2); +} + +.removeButton { + cursor: pointer; + border: none; + position: absolute; + top: -0.2rem; + right: 0; + font-size: var(--font-size-h3); + cursor: pointer; + color: var(--font-color-text); + background-color: transparent; +} diff --git a/src/components/@shared/FormFields/ContainerInput/Info.tsx b/src/components/@shared/FormFields/ContainerInput/Info.tsx new file mode 100644 index 000000000..a24332273 --- /dev/null +++ b/src/components/@shared/FormFields/ContainerInput/Info.tsx @@ -0,0 +1,29 @@ +import React, { ReactElement } from 'react' +import styles from './Info.module.css' + +export default function ImageInfo({ + image, + tag, + valid, + handleClose +}: { + image: string + tag: string + valid: boolean + handleClose(): void +}): ReactElement { + const displayText = valid + ? '✓ Image found, container checksum automatically added!' + : 'x Container checksum could not be fetched automatically, please add it manually' + return ( +
+

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

+
    +
  • {displayText}
  • +
+ +
+ ) +} diff --git a/src/components/@shared/FormFields/ContainerInput/index.tsx b/src/components/@shared/FormFields/ContainerInput/index.tsx new file mode 100644 index 000000000..400088ae1 --- /dev/null +++ b/src/components/@shared/FormFields/ContainerInput/index.tsx @@ -0,0 +1,83 @@ +import React, { ReactElement, useState } from 'react' +import { useField, useFormikContext } from 'formik' +import UrlInput from '../URLInput' +import { InputProps } from '@shared/FormInput' +import { FormPublishData } from 'src/components/Publish/_types' +import { LoggerInstance } from '@oceanprotocol/lib' +import ImageInfo from './Info' +import { getContainerChecksum } from '@utils/docker' + +export default function ContainerInput(props: InputProps): ReactElement { + const [field] = useField(props.name) + const [fieldChecksum, metaChecksum, helpersChecksum] = useField( + 'metadata.dockerImageCustomChecksum' + ) + + const { values, setFieldError, setFieldValue } = + useFormikContext() + const [isLoading, setIsLoading] = useState(false) + const [isValid, setIsValid] = useState(false) + const [checked, setChecked] = useState(false) + + async function handleValidation(e: React.SyntheticEvent, container: string) { + e.preventDefault() + try { + setIsLoading(true) + const parsedContainerValue = container?.split(':') + const imageName = + parsedContainerValue?.length > 1 + ? parsedContainerValue?.slice(0, -1).join(':') + : parsedContainerValue[0] + const tag = + parsedContainerValue?.length > 1 ? parsedContainerValue?.at(-1) : '' + const containerInfo = await getContainerChecksum(imageName, tag) + setFieldValue('metadata.dockerImageCustom', imageName) + setFieldValue('metadata.dockerImageCustomTag', tag) + setChecked(true) + if (containerInfo.checksum) { + setFieldValue( + 'metadata.dockerImageCustomChecksum', + containerInfo.checksum + ) + helpersChecksum.setTouched(false) + setIsValid(true) + } + } catch (error) { + setFieldError(`${field.name}[0].url`, error.message) + LoggerInstance.error(error.message) + } finally { + setIsLoading(false) + } + } + + function handleClose() { + setFieldValue('metadata.dockerImageCustom', '') + setFieldValue('metadata.dockerImageCustomTag', '') + setFieldValue('metadata.dockerImageCustomChecksum', '') + setChecked(false) + setIsValid(false) + helpersChecksum.setTouched(true) + } + + return ( + <> + {checked ? ( + + ) : ( + + )} + + ) +} diff --git a/src/components/@shared/FormFields/URLInput/index.tsx b/src/components/@shared/FormFields/URLInput/index.tsx index 3890032c9..af7d0fdf3 100644 --- a/src/components/@shared/FormFields/URLInput/index.tsx +++ b/src/components/@shared/FormFields/URLInput/index.tsx @@ -12,12 +12,14 @@ export default function URLInput({ handleButtonClick, isLoading, name, + checkUrl, ...props }: { submitText: string handleButtonClick(e: React.SyntheticEvent, data: string): void isLoading: boolean name: string + checkUrl?: boolean }): ReactElement { const [field, meta] = useField(name) const [isButtonDisabled, setIsButtonDisabled] = useState(true) @@ -28,7 +30,7 @@ export default function URLInput({ setIsButtonDisabled( !field?.value || field.value === '' || - !isUrl(field.value) || + (checkUrl && !isUrl(field.value)) || field.value.includes('javascript:') || meta?.error ) diff --git a/src/components/@shared/FormInput/InputElement.tsx b/src/components/@shared/FormInput/InputElement.tsx index 5d8d57f82..79e846f54 100644 --- a/src/components/@shared/FormInput/InputElement.tsx +++ b/src/components/@shared/FormInput/InputElement.tsx @@ -11,6 +11,7 @@ import AssetSelection, { } from '../FormFields/AssetSelection' import Nft from '../FormFields/Nft' import InputRadio from './InputRadio' +import ContainerInput from '@shared/FormFields/ContainerInput' import TagsAutoComplete from './TagsAutoComplete' const cx = classNames.bind(styles) @@ -108,6 +109,8 @@ export default function InputElement({ ) case 'files': return + case 'container': + return case 'providerUrl': return case 'nft': diff --git a/src/components/@shared/NetworkName/NetworkIcon.tsx b/src/components/@shared/NetworkName/NetworkIcon.tsx index 5af785ca9..55dc360e1 100644 --- a/src/components/@shared/NetworkName/NetworkIcon.tsx +++ b/src/components/@shared/NetworkName/NetworkIcon.tsx @@ -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 diff --git a/src/components/@shared/Price/Conversion.tsx b/src/components/@shared/Price/Conversion.tsx index 509395ce8..d97c1aa12 100644 --- a/src/components/@shared/Price/Conversion.tsx +++ b/src/components/@shared/Price/Conversion.tsx @@ -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. diff --git a/src/components/@shared/Price/PriceUnit.module.css b/src/components/@shared/Price/PriceUnit.module.css index 1a965e00d..bcf01e4a2 100644 --- a/src/components/@shared/Price/PriceUnit.module.css +++ b/src/components/@shared/Price/PriceUnit.module.css @@ -7,7 +7,7 @@ line-height: 1.2; } -.price > div:firt-child { +.price > div:first-child { white-space: nowrap; } diff --git a/src/components/@shared/atoms/Table/Empty.tsx b/src/components/@shared/atoms/Table/Empty.tsx index f3ca115ce..68b5fbffd 100644 --- a/src/components/@shared/atoms/Table/Empty.tsx +++ b/src/components/@shared/atoms/Table/Empty.tsx @@ -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
{message || 'No results found'}
+ return ( +
+ ) } diff --git a/src/components/Asset/AssetActions/AssetStats/index.module.css b/src/components/Asset/AssetActions/AssetStats/index.module.css index 0519db863..a8d2ae270 100644 --- a/src/components/Asset/AssetActions/AssetStats/index.module.css +++ b/src/components/Asset/AssetActions/AssetStats/index.module.css @@ -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); diff --git a/src/components/Asset/AssetActions/AssetStats/index.tsx b/src/components/Asset/AssetActions/AssetStats/index.tsx index 4ee046802..a32dc2d38 100644 --- a/src/components/Asset/AssetActions/AssetStats/index.tsx +++ b/src/components/Asset/AssetActions/AssetStats/index.tsx @@ -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 (
- {!asset || !asset?.stats || asset?.stats?.orders < 0 ? ( + {asset?.stats?.allocated && asset?.stats?.allocated > 0 ? ( + + + {formatPrice(asset.stats.allocated, locale)} + + veOCEAN + + ) : null} + {!asset?.stats || asset?.stats?.orders < 0 ? ( 'N/A' ) : asset?.stats?.orders === 0 ? ( 'No sales yet' ) : ( - <> + {asset.stats.orders} sale {asset.stats.orders === 1 ? '' : 's'} - + )} + {ownAllocation && ownAllocation > 0 ? ( + + {ownAllocation}% allocated + + + ) : null}
) } diff --git a/src/components/Asset/AssetActions/Compute/History.tsx b/src/components/Asset/AssetActions/Compute/History.tsx index 9fa285476..805ba8585 100644 --- a/src/components/Asset/AssetActions/Compute/History.tsx +++ b/src/components/Asset/AssetActions/Compute/History.tsx @@ -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) } diff --git a/src/components/Asset/AssetActions/Compute/index.tsx b/src/components/Asset/AssetActions/Compute/index.tsx index ffb59b3e8..7aa4b4838 100644 --- a/src/components/Asset/AssetActions/Compute/index.tsx +++ b/src/components/Asset/AssetActions/Compute/index.tsx @@ -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([]) 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({ )} {accountId && asset?.accessDetails?.datatoken && ( - + setRefetchJobs(!refetchJobs)} + > setRefetchJobs(!refetchJobs)} /> )} diff --git a/src/components/Footer/MarketStats/Total.tsx b/src/components/Footer/MarketStats/Total.tsx index e23a426c1..34ee13b18 100644 --- a/src/components/Footer/MarketStats/Total.tsx +++ b/src/components/Footer/MarketStats/Total.tsx @@ -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 ( <> - {total.orders} orders across{' '} - {total.nfts} assets with{' '} - {total.datatokens} different datatokens. + orders across{' '} + assets with{' '} + different datatokens. ) } diff --git a/src/components/Footer/MarketStats/_types.ts b/src/components/Footer/MarketStats/_types.ts index 7f3d89939..5d2cc2295 100644 --- a/src/components/Footer/MarketStats/_types.ts +++ b/src/components/Footer/MarketStats/_types.ts @@ -6,4 +6,6 @@ export interface StatsTotal { nfts: number datatokens: number orders: number + veAllocated: number + veLocked: number } diff --git a/src/components/Footer/MarketStats/index.tsx b/src/components/Footer/MarketStats/index.tsx index 665be6cd1..19f4ab32d 100644 --- a/src/components/Footer/MarketStats/index.tsx +++ b/src/components/Footer/MarketStats/index.tsx @@ -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 (
- <> +
{' '} } /> - +
+
+ locked.{' '} + {' '} + allocated. +
) } diff --git a/src/components/Header/SearchBar.module.css b/src/components/Header/SearchBar.module.css index a2f582104..2192c23a5 100644 --- a/src/components/Header/SearchBar.module.css +++ b/src/components/Header/SearchBar.module.css @@ -58,9 +58,6 @@ width: auto; left: auto; background: none; - } - - .input:focus + .button { z-index: 3; } } diff --git a/src/components/Home/Allocations/AssetListTable.tsx b/src/components/Home/Allocations/AssetListTable.tsx new file mode 100644 index 000000000..36a22bae5 --- /dev/null +++ b/src/components/Home/Allocations/AssetListTable.tsx @@ -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[] = [ + { + name: 'Dataset', + selector: (row) => { + const { metadata } = row.asset + return + }, + 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 ( + + ) +} diff --git a/src/components/Home/Allocations/index.module.css b/src/components/Home/Allocations/index.module.css new file mode 100644 index 000000000..d176386d7 --- /dev/null +++ b/src/components/Home/Allocations/index.module.css @@ -0,0 +1,3 @@ +.section { + composes: section from '../index.module.css'; +} diff --git a/src/components/Home/Allocations/index.tsx b/src/components/Home/Allocations/index.tsx new file mode 100644 index 000000000..a58c7890f --- /dev/null +++ b/src/components/Home/Allocations/index.tsx @@ -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() + const [data, setData] = useState() + 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 ( +
+

Your Allocated Assets

+ +
+ ) +} diff --git a/src/components/Home/SectionQueryResult.tsx b/src/components/Home/SectionQueryResult.tsx new file mode 100644 index 000000000..7efc796f1 --- /dev/null +++ b/src/components/Home/SectionQueryResult.tsx @@ -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() + const [loading, setLoading] = useState() + 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 ( +
+

{title}

+ + + + {action && action} +
+ ) +} diff --git a/src/components/Home/TopTags/_utils.ts b/src/components/Home/TopTags/_utils.ts new file mode 100644 index 000000000..dfcb48884 --- /dev/null +++ b/src/components/Home/TopTags/_utils.ts @@ -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 { + 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) + } + } +} diff --git a/src/components/Home/TopTags/index.module.css b/src/components/Home/TopTags/index.module.css new file mode 100644 index 000000000..d176386d7 --- /dev/null +++ b/src/components/Home/TopTags/index.module.css @@ -0,0 +1,3 @@ +.section { + composes: section from '../index.module.css'; +} diff --git a/src/components/Home/TopTags/index.tsx b/src/components/Home/TopTags/index.tsx new file mode 100644 index 000000000..f20232868 --- /dev/null +++ b/src/components/Home/TopTags/index.tsx @@ -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([]) + const [loading, setLoading] = useState() + 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 ( +
+

{title}

+ {loading ? : } + + {action && action} +
+ ) +} diff --git a/src/components/Home/index.tsx b/src/components/Home/index.tsx index e256ee102..59672f9b4 100644 --- a/src/components/Home/index.tsx +++ b/src/components/Home/index.tsx @@ -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() - const [loading, setLoading] = useState() - 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 ( -
-

{title}

- - - - {action && action} -
- ) -} +import Allocations from './Allocations' export default function HomePage(): ReactElement { + const { chainIds } = useUserPreferences() + const [queryLatest, setQueryLatest] = useState() const [queryMostSales, setQueryMostSales] = useState() - const { chainIds } = useUserPreferences() + const [queryMostAllocation, setQueryMostAllocation] = useState() 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 ( <>
-

Bookmarks

+

Your Bookmarks

+ + + + + + + } /> - - ) } diff --git a/src/components/Profile/Header/NumberUnit.module.css b/src/components/Profile/Header/NumberUnit.module.css index 67dd67b9b..244036758 100644 --- a/src/components/Profile/Header/NumberUnit.module.css +++ b/src/components/Profile/Header/NumberUnit.module.css @@ -9,7 +9,7 @@ .number { white-space: nowrap; display: inline-flex; - align-items: center; + align-items: baseline; line-height: 1; } diff --git a/src/components/Profile/Header/NumberUnit.tsx b/src/components/Profile/Header/NumberUnit.tsx index 930e5d4b1..4341ad383 100644 --- a/src/components/Profile/Header/NumberUnit.tsx +++ b/src/components/Profile/Header/NumberUnit.tsx @@ -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 diff --git a/src/components/Profile/Header/Stats.module.css b/src/components/Profile/Header/Stats.module.css index 67a6a0c7c..27ddca386 100644 --- a/src/components/Profile/Header/Stats.module.css +++ b/src/components/Profile/Header/Stats.module.css @@ -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); +} diff --git a/src/components/Profile/Header/Stats.tsx b/src/components/Profile/Header/Stats.tsx index e5a8e3ea3..3678d541d 100644 --- a/src/components/Profile/Header/Stats.tsx +++ b/src/components/Profile/Header/Stats.tsx @@ -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} /> + + Lock OCEAN + + ) : ( + <> + locked + + ) + } + value={ + 0 ? lockedOcean : 0} + symbol="OCEAN" + hideApproximateSymbol + /> + } + /> ) } diff --git a/src/components/Profile/History/ComputeJobs/index.tsx b/src/components/Profile/History/ComputeJobs/index.tsx index 405b50809..a1fd24cc7 100644 --- a/src/components/Profile/History/ComputeJobs/index.tsx +++ b/src/components/Profile/History/ComputeJobs/index.tsx @@ -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[] = [ 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([]) 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} > diff --git a/src/components/Profile/History/index.tsx b/src/components/Profile/History/index.tsx index 10af47f11..8be94572a 100644 --- a/src/components/Profile/History/index.tsx +++ b/src/components/Profile/History/index.tsx @@ -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: + content: ( + 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([]) + + 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) diff --git a/src/components/Publish/AvailableNetworks/index.tsx b/src/components/Publish/AvailableNetworks/index.tsx index d6bc5e97b..8e74dad5d 100644 --- a/src/components/Publish/AvailableNetworks/index.tsx +++ b/src/components/Publish/AvailableNetworks/index.tsx @@ -26,7 +26,6 @@ export default function AvailableNetworks(): ReactElement { { title: 'Main', data: networksMain }, { title: 'Test', data: networksTest } ] - const networkList = (networks: number[]) => networks.map((chainId) => (
  • diff --git a/src/components/Publish/Metadata/index.tsx b/src/components/Publish/Metadata/index.tsx index 2be4852bd..775246ab9 100644 --- a/src/components/Publish/Metadata/index.tsx +++ b/src/components/Publish/Metadata/index.tsx @@ -1,6 +1,6 @@ import { BoxSelectionOption } from '@shared/FormFields/BoxSelection' import Input from '@shared/FormInput' -import { Field, useFormikContext } from 'formik' +import { Field, useField, useFormikContext } from 'formik' import React, { ReactElement, useEffect } from 'react' import content from '../../../../content/publish/form.json' import { FormPublishData } from '../_types' @@ -23,6 +23,8 @@ export default function MetadataFields(): ReactElement { // connect with Form state, use for conditional field rendering const { values, setFieldValue } = useFormikContext() + const [field, meta] = useField('metadata.dockerImageCustomChecksum') + // BoxSelection component is not a Formik component // so we need to handle checked state manually. const assetTypeOptions: BoxSelectionOption[] = [ @@ -124,11 +126,14 @@ export default function MetadataFields(): ReactElement { /> { if (dockerImage === '') return const preset = algorithmContainerPresets.find( (preset) => `${preset.image}:${preset.tag}` === dockerImage ) + preset.checksum = await ( + await getContainerChecksum(preset.image, preset.tag) + ).checksum return preset } @@ -80,6 +84,11 @@ export async function transformPublishFormToDdo( const currentTime = dateToStringNoMS(new Date()) const isPreview = !datatokenAddress && !nftAddress + const algorithmContainerPresets = + type === 'algorithm' && dockerImage !== '' && dockerImage !== 'custom' + ? await getAlgorithmContainerPreset(dockerImage) + : null + // Transform from files[0].url to string[] assuming only 1 file const filesTransformed = files?.length && files[0].valid && [sanitizeUrl(files[0].url)] @@ -110,20 +119,19 @@ export async function transformPublishFormToDdo( entrypoint: dockerImage === 'custom' ? dockerImageCustomEntrypoint - : getAlgorithmContainerPreset(dockerImage).entrypoint, + : algorithmContainerPresets.entrypoint, image: dockerImage === 'custom' ? dockerImageCustom - : getAlgorithmContainerPreset(dockerImage).image, + : algorithmContainerPresets.image, tag: dockerImage === 'custom' ? dockerImageCustomTag - : getAlgorithmContainerPreset(dockerImage).tag, + : algorithmContainerPresets.tag, checksum: dockerImage === 'custom' - ? // ? dockerImageCustomChecksum - '' - : getAlgorithmContainerPreset(dockerImage).checksum + ? dockerImageCustomChecksum + : algorithmContainerPresets.checksum } } }) diff --git a/src/components/Search/Filters.module.css b/src/components/Search/Filters.module.css index 70359b151..2138604a4 100644 --- a/src/components/Search/Filters.module.css +++ b/src/components/Search/Filters.module.css @@ -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; } diff --git a/src/components/Search/index.module.css b/src/components/Search/index.module.css index 9a3840e2e..0d8ccfa23 100644 --- a/src/components/Search/index.module.css +++ b/src/components/Search/index.module.css @@ -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); } diff --git a/src/components/Search/sort.module.css b/src/components/Search/sort.module.css index dad17cba1..5a774c7b5 100644 --- a/src/components/Search/sort.module.css +++ b/src/components/Search/sort.module.css @@ -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; diff --git a/src/components/Search/sort.tsx b/src/components/Search/sort.tsx index 710854c57..b71371496 100644 --- a/src/components/Search/sort.tsx +++ b/src/components/Search/sort.tsx @@ -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({