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

Merge branch 'main' into feature/enforce-docker-containers

This commit is contained in:
mihaisc 2022-10-07 12:17:41 +03:00
commit b505bddb4b
25 changed files with 2136 additions and 1405 deletions

View File

@ -86,7 +86,7 @@ npm start
To use the app together with MetaMask, importing one of the accounts auto-generated by the Ganache container is the easiest way to have test ETH available. All of them have 100 ETH by default. Upon start, the `ocean_ganache_1` container will print out the private keys of multiple accounts in its logs. Pick one of them and import into MetaMask. To use the app together with MetaMask, importing one of the accounts auto-generated by the Ganache container is the easiest way to have test ETH available. All of them have 100 ETH by default. Upon start, the `ocean_ganache_1` container will print out the private keys of multiple accounts in its logs. Pick one of them and import into MetaMask.
To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `subgraphUri` is hardcoded to the Rinkeby subgraph in our [`getDevelopmentConfig` function](https://github.com/oceanprotocol/market/blob/d0b1534d105e5dcb3790c65d4bb04ff1d2dbc575/src/utils/ocean.ts#L31). To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `subgraphUri` is hardcoded to the Goerli subgraph in our [`getDevelopmentConfig` function](https://github.com/oceanprotocol/market/blob/d0b1534d105e5dcb3790c65d4bb04ff1d2dbc575/src/utils/ocean.ts#L31).
> Cleaning all Docker images so they are fetched freshly is often a good idea to make sure no issues are caused by old or stale images: `docker system prune --all --volumes` > Cleaning all Docker images so they are fetched freshly is often a good idea to make sure no issues are caused by old or stale images: `docker system prune --all --volumes`
@ -97,7 +97,7 @@ The `app.config.js` file is setup to prioritize environment variables for settin
For local development, you can use a `.env` file: For local development, you can use a `.env` file:
```bash ```bash
# modify env variables, Rinkeby is enabled by default when using those files # modify env variables, Goerli is enabled by default when using those files
cp .env.example .env cp .env.example .env
``` ```
@ -316,7 +316,7 @@ npm run jest
A coverage report is automatically shown in console whenever `npm run jest` is called. Generated reports are sent to CodeClimate during CI runs. A coverage report is automatically shown in console whenever `npm run jest` is called. Generated reports are sent to CodeClimate during CI runs.
During local development you can continously get coverage report feedback in your console by running Jest in watch mode: During local development you can continuously get coverage report feedback in your console by running Jest in watch mode:
```bash ```bash
npm run jest:watch npm run jest:watch
@ -379,7 +379,7 @@ We encourage you to fork this repository and create your own data marketplace. W
- The Ocean Protocol logo is a trademark of the Ocean Protocol Foundation and must be removed from forked versions of the market. - The Ocean Protocol logo is a trademark of the Ocean Protocol Foundation and must be removed from forked versions of the market.
- The name "Ocean Market" is also copyright protected and should be changed to the name of your market. - The name "Ocean Market" is also copyright protected and should be changed to the name of your market.
Additionally, we would also advise that your retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team. Additionally, we would also advise that you retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team.
Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace! Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace!
@ -411,7 +411,7 @@ Feel free to adopt our provided privacy policies to your needs. Per default we c
### Privacy Preference Center ### Privacy Preference Center
Additionally, Ocean Market provides a privacy preference center for you to use. This feature is disabled per default since we do not use cookies requiring consent on our deployment of the market. However, if you need to add some functionality depending on cookies, you can simply enable this feature by changing the value of the `NEXT_PUBLIC_PRIVACY_PREFERENCE_CENTER` environmental variable to `"true"` in your `.env` file. This will enable a customizable cookie banner stating the use of your individual cookies. The content of this banner can be adjusted within the `content/gdpr.json` file. If no `optionalCookies` are provided, the privacy preference center will be set to a simpler version displaying only the `title`, `text` and `close`-button. This can be used to inform the user about the use of essential cookies, where no consent is needed. The privacy preference center supports two different styling options: `'small'` and `'default'`. Setting the style propertie to `'small'` will display a smaller cookie banner to the user at first, only showing the default styled privacy preference center upon the user's customization request. Additionally, Ocean Market provides a privacy preference center for you to use. This feature is disabled per default since we do not use cookies requiring consent on our deployment of the market. However, if you need to add some functionality depending on cookies, you can simply enable this feature by changing the value of the `NEXT_PUBLIC_PRIVACY_PREFERENCE_CENTER` environmental variable to `"true"` in your `.env` file. This will enable a customizable cookie banner stating the use of your individual cookies. The content of this banner can be adjusted within the `content/gdpr.json` file. If no `optionalCookies` are provided, the privacy preference center will be set to a simpler version displaying only the `title`, `text` and `close`-button. This can be used to inform the user about the use of essential cookies, where no consent is needed. The privacy preference center supports two different styling options: `'small'` and `'default'`. Setting the style property to `'small'` will display a smaller cookie banner to the user at first, only showing the default styled privacy preference center upon the user's customization request.
Now your market users will be provided with additional options to toggle the use of your configured cookie consent categories. You can always retrieve the current consent status per category with the provided `useConsent()` hook. See below, how you can set your own custom cookies depending on the market user's consent. Feel free to adjust the provided utility functions for cookie usage provided in the `src/utils/cookies.ts` file to your needs. Now your market users will be provided with additional options to toggle the use of your configured cookie consent categories. You can always retrieve the current consent status per category with the provided `useConsent()` hook. See below, how you can set your own custom cookies depending on the market user's consent. Feel free to adjust the provided utility functions for cookie usage provided in the `src/utils/cookies.ts` file to your needs.

View File

@ -9,20 +9,12 @@ module.exports = {
process.env.NEXT_PUBLIC_METADATACACHE_URI || process.env.NEXT_PUBLIC_METADATACACHE_URI ||
'https://v4.aquarius.oceanprotocol.com', 'https://v4.aquarius.oceanprotocol.com',
v3MetadataCacheUri:
process.env.NEXT_PUBLIC_V3_METADATACACHE_URI ||
'https://aquarius.oceanprotocol.com',
v3MarketUri:
process.env.NEXT_PUBLIC_V3_MARKET_URI ||
'https://v3.market.oceanprotocol.com',
// List of chainIds which metadata cache queries will return by default. // List of chainIds which metadata cache queries will return by default.
// This preselects the Chains user preferences. // This preselects the Chains user preferences.
chainIds: [1, 137, 56, 246, 1285], chainIds: [1, 137, 56, 246, 1285],
// List of all supported chainIds. Used to populate the Chains user preferences list. // List of all supported chainIds. Used to populate the Chains user preferences list.
chainIdsSupported: [1, 137, 56, 246, 1285, 3, 4, 80001, 1287], chainIdsSupported: [1, 137, 56, 246, 1285, 5, 80001, 1287],
infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx', infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx',

View File

@ -59,6 +59,13 @@
"placeholder": "e.g. Mrs McJellyfish", "placeholder": "e.g. Mrs McJellyfish",
"help": "Give proper attribution for your dataset.", "help": "Give proper attribution for your dataset.",
"required": false "required": false
},
{
"name": "tags",
"label": "New Tags",
"type": "tags",
"placeholder": "e.g. logistics",
"required": false
} }
] ]
} }

View File

@ -39,8 +39,8 @@
{ {
"name": "tags", "name": "tags",
"label": "Tags", "label": "Tags",
"placeholder": "e.g. logistics, ai", "type": "tags",
"help": "Separate tags with comma." "placeholder": "e.g. logistics"
}, },
{ {
"name": "dockerImage", "name": "dockerImage",

3152
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"deploy:s3": "bash scripts/deploy-s3.sh", "deploy:s3": "bash scripts/deploy-s3.sh",
"postinstall": "husky install && rm -r node_modules/apollo-language-server/node_modules/graphql", "postinstall": "husky install && rm -r node_modules/apollo-language-server/node_modules/graphql",
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.ropsten.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/", "codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.goerli.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet", "storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
"storybook:build": "cross-env NODE_ENV=test build-storybook" "storybook:build": "cross-env NODE_ENV=test build-storybook"
}, },
@ -26,7 +26,7 @@
"@coingecko/cryptoformat": "^0.5.4", "@coingecko/cryptoformat": "^0.5.4",
"@loadable/component": "^5.15.2", "@loadable/component": "^5.15.2",
"@oceanprotocol/art": "^3.2.0", "@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^2.0.2", "@oceanprotocol/lib": "^2.1.1",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@oceanprotocol/use-dark-mode": "^2.4.3", "@oceanprotocol/use-dark-mode": "^2.4.3",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
@ -38,13 +38,14 @@
"decimal.js": "^10.3.1", "decimal.js": "^10.3.1",
"dom-confetti": "^0.2.2", "dom-confetti": "^0.2.2",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"filesize": "^9.0.11", "filesize": "^10.0.5",
"formik": "^2.2.9", "formik": "^2.2.9",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"is-url-superb": "^6.1.0", "is-url-superb": "^6.1.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
"match-sorter": "^6.3.1",
"myetherwallet-blockies": "^0.1.1", "myetherwallet-blockies": "^0.1.1",
"next": "12.3.1", "next": "12.3.1",
"query-string": "^7.1.1", "query-string": "^7.1.1",
@ -55,6 +56,7 @@
"react-dotdotdot": "^1.3.1", "react-dotdotdot": "^1.3.1",
"react-modal": "^3.15.1", "react-modal": "^3.15.1",
"react-paginate": "^8.1.3", "react-paginate": "^8.1.3",
"react-select": "^5.4.0",
"react-spring": "^9.5.2", "react-spring": "^9.5.2",
"react-tabs": "^5.1.0", "react-tabs": "^5.1.0",
"react-toastify": "^9.0.4", "react-toastify": "^9.0.4",
@ -82,26 +84,26 @@
"@types/lodash.debounce": "^4.0.7", "@types/lodash.debounce": "^4.0.7",
"@types/lodash.omit": "^4.5.7", "@types/lodash.omit": "^4.5.7",
"@types/node": "^18.7.18", "@types/node": "^18.7.18",
"@types/react": "^18.0.14", "@types/react": "^18.0.21",
"@types/react-dom": "^18.0.5", "@types/react-dom": "^18.0.5",
"@types/react-modal": "^3.13.1", "@types/react-modal": "^3.13.1",
"@types/react-paginate": "^7.1.1", "@types/react-paginate": "^7.1.1",
"@types/remove-markdown": "^0.3.1", "@types/remove-markdown": "^0.3.1",
"@typescript-eslint/eslint-plugin": "^5.38.0", "@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/parser": "^5.38.0", "@typescript-eslint/parser": "^5.38.1",
"apollo": "^2.34.0", "apollo": "^2.34.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.23.1", "eslint": "^8.23.1",
"eslint-config-oceanprotocol": "^2.0.3", "eslint-config-oceanprotocol": "^2.0.4",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest-dom": "^4.0.2", "eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.8", "eslint-plugin-react": "^7.31.8",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^5.6.4", "eslint-plugin-testing-library": "^5.7.0",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"husky": "^8.0.1", "husky": "^8.0.1",
"jest": "^29.0.3", "jest": "^29.1.2",
"jest-environment-jsdom": "^29.0.3", "jest-environment-jsdom": "^29.0.3",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",

View File

@ -9,7 +9,7 @@ import React, {
} from 'react' } from 'react'
import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib' import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib'
import { CancelToken } from 'axios' import { CancelToken } from 'axios'
import { checkV3Asset, retrieveAsset } from '@utils/aquarius' import { retrieveAsset } from '@utils/aquarius'
import { useWeb3 } from './Web3' import { useWeb3 } from './Web3'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean' import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean'
@ -25,7 +25,6 @@ export interface AssetProviderValue {
owner: string owner: string
error?: string error?: string
isAssetNetwork: boolean isAssetNetwork: boolean
isV3Asset: boolean
isOwner: boolean isOwner: boolean
oceanConfig: Config oceanConfig: Config
loading: boolean loading: boolean
@ -53,7 +52,6 @@ function AssetProvider({
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>() const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>()
const [isV3Asset, setIsV3Asset] = useState<boolean>()
const [oceanConfig, setOceanConfig] = useState<Config>() const [oceanConfig, setOceanConfig] = useState<Config>()
const newCancelToken = useCancelToken() const newCancelToken = useCancelToken()
@ -71,7 +69,6 @@ function AssetProvider({
const asset = await retrieveAsset(did, token) const asset = await retrieveAsset(did, token)
if (!asset) { if (!asset) {
setIsV3Asset(await checkV3Asset(did, token))
setError( setError(
`\`${did}\`` + `\`${did}\`` +
'\n\nWe could not find an asset for this DID in the cache. If you just published a new asset, wait some seconds and refresh this page.' '\n\nWe could not find an asset for this DID in the cache. If you just published a new asset, wait some seconds and refresh this page.'
@ -96,7 +93,6 @@ function AssetProvider({
} }
setTitle(`This asset has been flagged as "${state}" by the publisher`) setTitle(`This asset has been flagged as "${state}" by the publisher`)
setIsV3Asset(await checkV3Asset(did, token))
setError(`\`${did}\`` + `\n\nPublisher Address: ${asset.nft.owner}`) setError(`\`${did}\`` + `\n\nPublisher Address: ${asset.nft.owner}`)
LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset) LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset)
return return
@ -208,7 +204,6 @@ function AssetProvider({
loading, loading,
fetchAsset, fetchAsset,
isAssetNetwork, isAssetNetwork,
isV3Asset,
isOwner, isOwner,
oceanConfig oceanConfig
} as AssetProviderValue } as AssetProviderValue

View File

@ -26,8 +26,6 @@ export interface AppConfig {
classNameLight: string classNameLight: string
storageKey: string storageKey: string
} }
v3MetadataCacheUri: string
v3MarketUri: string
} }
export interface SiteContent { export interface SiteContent {
siteTitle: string siteTitle: string

View File

@ -133,6 +133,14 @@ function UserPreferencesProvider({
setBookmarks(newPinned) setBookmarks(newPinned)
}, [bookmarks]) }, [bookmarks])
// chainIds old data migration
// remove deprecated networks from user-saved chainIds
useEffect(() => {
if (!chainIds.includes(3) && !chainIds.includes(4)) return
const newChainIds = chainIds.filter((id) => id !== 3 && id !== 4)
setChainIds(newChainIds)
}, [chainIds])
return ( return (
<UserPreferencesContext.Provider <UserPreferencesContext.Provider
value={ value={

4
src/@types/aquarius/TagsList.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
interface AggregatedTag {
doc_count: number
key: string
}

View File

@ -2,7 +2,7 @@ import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection' import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import axios, { CancelToken, AxiosResponse } from 'axios' import axios, { CancelToken, AxiosResponse } from 'axios'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData' import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
import { metadataCacheUri, v3MetadataCacheUri } from '../../app.config' import { metadataCacheUri } from '../../app.config'
import { import {
SortDirectionOptions, SortDirectionOptions,
SortTermOptions SortTermOptions
@ -44,7 +44,10 @@ export function generateBaseQuery(
): SearchQuery { ): SearchQuery {
const generatedQuery = { const generatedQuery = {
from: baseQueryParams.esPaginationOptions?.from || 0, from: baseQueryParams.esPaginationOptions?.from || 0,
size: baseQueryParams.esPaginationOptions?.size || 1000, size:
baseQueryParams.esPaginationOptions?.size >= 0
? baseQueryParams.esPaginationOptions?.size
: 1000,
query: { query: {
bool: { bool: {
...baseQueryParams.nestedQuery, ...baseQueryParams.nestedQuery,
@ -145,28 +148,6 @@ export async function retrieveAsset(
} }
} }
export async function checkV3Asset(
did: string,
cancelToken: CancelToken
): Promise<boolean> {
try {
const response: AxiosResponse<Asset> = await axios.get(
`${v3MetadataCacheUri}/api/v1/aquarius/assets/ddo/${did}`,
{ cancelToken }
)
if (!response || response.status !== 200 || !response.data) return false
return true
} catch (error) {
if (axios.isCancel(error)) {
LoggerInstance.log(error.message)
} else {
LoggerInstance.error(error.message)
}
return false
}
}
export async function getAssetsNames( export async function getAssetsNames(
didList: string[], didList: string[],
cancelToken: CancelToken cancelToken: CancelToken
@ -478,3 +459,47 @@ export async function getDownloadAssets(
} }
} }
} }
export async function getTagsList(
chainIds: number[],
cancelToken: CancelToken
): Promise<string[]> {
const baseQueryParams = {
chainIds,
esPaginationOptions: { from: 0, size: 0 }
} as BaseQueryParams
const query = {
...generateBaseQuery(baseQueryParams),
aggs: {
tags: {
terms: {
field: 'metadata.tags.keyword',
size: 1000
}
}
}
}
try {
const response: AxiosResponse<SearchResponse> = await axios.post(
`${metadataCacheUri}/api/aquarius/assets/query`,
{ ...query },
{ cancelToken }
)
if (response?.status !== 200 || !response?.data) return
const { buckets }: { buckets: AggregatedTag[] } =
response.data.aggregations.tags
const tagsList = buckets
.filter((tag) => tag.key !== '')
.map((tag) => tag.key)
return tagsList.sort()
} catch (error) {
if (axios.isCancel(error)) {
LoggerInstance.log(error.message)
} else {
LoggerInstance.error(error.message)
}
}
}

View File

@ -24,7 +24,7 @@ export function getDevelopmentConfig(): Config {
// fixedRateExchangeAddress: contractAddresses.development?.FixedRateExchange, // fixedRateExchangeAddress: contractAddresses.development?.FixedRateExchange,
// metadataContractAddress: contractAddresses.development?.Metadata, // metadataContractAddress: contractAddresses.development?.Metadata,
// oceanTokenAddress: contractAddresses.development?.Ocean, // oceanTokenAddress: contractAddresses.development?.Ocean,
// There is no subgraph in barge so we hardcode the Rinkeby one for now // There is no subgraph in barge so we hardcode the Goerli one for now
subgraphUri: 'https://v4.subgraph.rinkeby.oceanprotocol.com' subgraphUri: 'https://v4.subgraph.goerli.oceanprotocol.com'
} as Config } as Config
} }

View File

@ -1,13 +1,10 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import filesize from 'filesize' import { filesize } from 'filesize'
import classNames from 'classnames/bind'
import cleanupContentType from '@utils/cleanupContentType' import cleanupContentType from '@utils/cleanupContentType'
import styles from './index.module.css' import styles from './index.module.css'
import Loader from '@shared/atoms/Loader' import Loader from '@shared/atoms/Loader'
import { FileInfo } from '@oceanprotocol/lib' import { FileInfo } from '@oceanprotocol/lib'
const cx = classNames.bind(styles)
function LoaderArea() { function LoaderArea() {
return ( return (
<div className={styles.loaderWrap}> <div className={styles.loaderWrap}>
@ -27,11 +24,9 @@ export default function FileIcon({
small?: boolean small?: boolean
isLoading?: boolean isLoading?: boolean
}): ReactElement { }): ReactElement {
const styleClasses = cx({ const styleClasses = `${styles.file} ${small ? styles.small : ''} ${
file: true, className || ''
small, }`
[className]: className
})
return ( return (
<ul className={styleClasses}> <ul className={styleClasses}>
@ -42,7 +37,7 @@ export default function FileIcon({
<li>{cleanupContentType(file.contentType)}</li> <li>{cleanupContentType(file.contentType)}</li>
<li> <li>
{file.contentLength && file.contentLength !== '0' {file.contentLength && file.contentLength !== '0'
? filesize(Number(file.contentLength)) ? filesize(Number(file.contentLength)).toString()
: ''} : ''}
</li> </li>
</> </>

View File

@ -12,6 +12,7 @@ import AssetSelection, {
import Nft from '../FormFields/Nft' import Nft from '../FormFields/Nft'
import InputRadio from './InputRadio' import InputRadio from './InputRadio'
import ContainerInput from '@shared/FormFields/ContainerInput' import ContainerInput from '@shared/FormFields/ContainerInput'
import TagsAutoComplete from './TagsAutoComplete'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -124,6 +125,8 @@ export default function InputElement({
{...props} {...props}
/> />
) )
case 'tags':
return <TagsAutoComplete {...field} {...props} />
default: default:
return prefix || postfix ? ( return prefix || postfix ? (
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}> <div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>

View File

@ -0,0 +1,101 @@
.select [class$='control'] {
border-color: var(--border-color);
border-radius: var(--border-radius);
box-shadow: none;
background-color: var(--background-content);
font-size: var(--font-size-base);
font-family: var(--font-family-base);
font-weight: var(--font-weight-bold);
cursor: text;
min-height: 43px;
}
.select [class$='control']:hover {
border-color: var(--border-color);
}
.select [class$='control']:focus-within {
border-color: var(--font-color-text);
}
.select [class$='ValueContainer'] {
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 3);
}
.select [class$='Input'] {
margin: 0;
padding-bottom: 0;
padding-top: 0;
font-size: var(--font-size-base);
font-family: var(--font-family-base);
font-weight: var(--font-weight-bold);
}
.select input {
color: var(--font-color-heading) !important;
}
.select [class$='menu'] {
background-color: var(--background-highlight);
border-radius: var(--border-radius);
}
.select [class$='option'] {
color: var(--font-color-heading);
font-size: var(--font-size-base);
font-family: var(--font-family-base);
font-weight: var(--font-weight-bold);
}
.select [class$='option']:active {
background-color: var(--color-secondary);
}
.select [class$='multiValue'],
.select [class$='multiValue'] > *,
.select [class$='multiValue']:hover > * {
border-radius: var(--border-radius);
background-color: var(--background-highlight);
color: var(--font-color-text);
font-size: var(--font-size-base);
font-family: var(--font-family-base);
font-weight: var(--font-weight-bold);
padding-top: 0;
padding-bottom: 0;
}
.select [class$='multiValue'] > div[role$='button'],
.select [class$='indicatorContainer'] svg {
cursor: pointer;
}
.select [class$='placeholder'] {
margin-left: 0;
margin-right: 0;
color: var(--color-secondary);
font-weight: var(--font-weight-base);
transition: 0.2s ease-out;
opacity: 0.7;
}
.select [class$='menu'] {
background-color: var(--background-content);
margin-top: -2px;
border: 1px solid var(--font-color-text);
border-top-color: var(--border-color);
border-top-left-radius: 0;
border-top-right-radius: 0;
box-shadow: none;
}
.select [class$='menu'] [class$='option']:hover,
.select [class$='menu'] [class$='option']:focus-within {
background-color: var(--font-color-heading);
color: var(--background-content);
}
.select [class$='NoOptionsMessage'] {
font-size: var(--font-size-small);
color: var(--color-secondary);
text-align: left;
}

View File

@ -0,0 +1,92 @@
import React, { ReactElement, useEffect, useState } from 'react'
import CreatableSelect from 'react-select/creatable'
import { OnChangeValue } from 'react-select'
import { useField } from 'formik'
import { InputProps } from '.'
import { getTagsList } from '@utils/aquarius'
import { chainIds } from 'app.config'
import { useCancelToken } from '@hooks/useCancelToken'
import styles from './TagsAutoComplete.module.css'
import { matchSorter } from 'match-sorter'
interface AutoCompleteOption {
readonly value: string
readonly label: string
}
export default function TagsAutoComplete({
...props
}: InputProps): ReactElement {
const { name, placeholder } = props
const [tagsList, setTagsList] = useState<AutoCompleteOption[]>()
const [matchedTagsList, setMatchedTagsList] = useState<AutoCompleteOption[]>(
[]
)
const [field, meta, helpers] = useField(name)
const [input, setInput] = useState<string>()
const newCancelToken = useCancelToken()
const generateAutocompleteOptions = (
options: string[]
): AutoCompleteOption[] => {
return options?.map((tag) => ({
value: tag,
label: tag
}))
}
const defaultTags = !field.value
? undefined
: generateAutocompleteOptions(field.value)
useEffect(() => {
const generateTagsList = async () => {
const tags = await getTagsList(chainIds, newCancelToken())
const autocompleteOptions = generateAutocompleteOptions(tags)
setTagsList(autocompleteOptions)
}
generateTagsList()
}, [newCancelToken])
const handleChange = (userInput: OnChangeValue<AutoCompleteOption, true>) => {
const normalizedInput = userInput.map((input) => input.value)
helpers.setValue(normalizedInput)
helpers.setTouched(true)
}
const handleOptionsFilter = (
options: AutoCompleteOption[],
input: string
): void => {
setInput(input)
const matchedTagsList = matchSorter(options, input, { keys: ['value'] })
setMatchedTagsList(matchedTagsList)
}
return (
<CreatableSelect
components={{
DropdownIndicator: () => null,
IndicatorSeparator: () => null
}}
className={styles.select}
defaultValue={defaultTags}
hideSelectedOptions
isMulti
isClearable={false}
noOptionsMessage={() =>
'Start typing to get suggestions based on tags from all published assets.'
}
onChange={(value: AutoCompleteOption[]) => handleChange(value)}
onInputChange={(value) => handleOptionsFilter(tagsList, value)}
openMenuOnClick
options={!input || input?.length < 1 ? [] : matchedTagsList}
placeholder={placeholder}
theme={(theme) => ({
...theme,
colors: { ...theme.colors, primary25: 'var(--border-color)' }
})}
/>
)
}

View File

@ -73,7 +73,8 @@ export default function Edit({
name: values.name, name: values.name,
description: values.description, description: values.description,
links: linksTransformed, links: linksTransformed,
author: values.author author: values.author,
tags: values.tags
} }
asset?.accessDetails?.type === 'fixed' && asset?.accessDetails?.type === 'fixed' &&

View File

@ -14,7 +14,8 @@ export function getInitialValues(
links: metadata?.links as any, links: metadata?.links as any,
files: [{ url: '', type: '' }], files: [{ url: '', type: '' }],
timeout: secondsToString(timeout), timeout: secondsToString(timeout),
author: metadata?.author author: metadata?.author,
tags: metadata?.tags
} }
} }

View File

@ -7,6 +7,7 @@ export interface MetadataEditForm {
files: FileInfo[] files: FileInfo[]
links?: FileInfo[] links?: FileInfo[]
author?: string author?: string
tags?: string[]
} }
export interface ComputeEditForm { export interface ComputeEditForm {

View File

@ -24,7 +24,8 @@ export const validationSchema = Yup.object().shape({
) )
.nullable(), .nullable(),
timeout: Yup.string().required('Required'), timeout: Yup.string().required('Required'),
author: Yup.string().nullable() author: Yup.string().nullable(),
tags: Yup.array<string[]>().nullable()
}) })
export const computeSettingsValidationSchema = Yup.object().shape({ export const computeSettingsValidationSchema = Yup.object().shape({

View File

@ -5,29 +5,25 @@ import Alert from '@shared/atoms/Alert'
import Loader from '@shared/atoms/Loader' import Loader from '@shared/atoms/Loader'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import AssetContent from './AssetContent' import AssetContent from './AssetContent'
import { v3MarketUri } from 'app.config'
export default function AssetDetails({ uri }: { uri: string }): ReactElement { export default function AssetDetails({ uri }: { uri: string }): ReactElement {
const router = useRouter() const router = useRouter()
const { asset, title, error, isInPurgatory, loading, isV3Asset } = useAsset() const { asset, title, error, isInPurgatory, loading } = useAsset()
const [pageTitle, setPageTitle] = useState<string>() const [pageTitle, setPageTitle] = useState<string>()
useEffect(() => { useEffect(() => {
if (isV3Asset) {
router.push(`${v3MarketUri}${uri}`)
}
if (!asset || error) { if (!asset || error) {
setPageTitle(title || 'Could not retrieve asset') setPageTitle(title || 'Could not retrieve asset')
return return
} }
setPageTitle(isInPurgatory ? '' : title) setPageTitle(isInPurgatory ? '' : title)
}, [asset, error, isInPurgatory, isV3Asset, router, title, uri]) }, [asset, error, isInPurgatory, router, title, uri])
return asset && pageTitle !== undefined && !loading ? ( return asset && pageTitle !== undefined && !loading ? (
<Page title={pageTitle} uri={uri}> <Page title={pageTitle} uri={uri}>
<AssetContent asset={asset} /> <AssetContent asset={asset} />
</Page> </Page>
) : error && isV3Asset === false ? ( ) : error ? (
<Page title={pageTitle} noPageHeader uri={uri}> <Page title={pageTitle} noPageHeader uri={uri}>
<Alert title={pageTitle} text={error} state={'error'} /> <Alert title={pageTitle} text={error} state={'error'} />
</Page> </Page>

View File

@ -63,7 +63,7 @@ export const initialValues: FormPublishData = {
name: '', name: '',
author: '', author: '',
description: '', description: '',
tags: '', tags: [],
termsAndConditions: false, termsAndConditions: false,
dockerImage: '', dockerImage: '',
dockerImageCustom: '', dockerImageCustom: '',

View File

@ -26,7 +26,7 @@ export interface FormPublishData {
description: string description: string
author: string author: string
termsAndConditions: boolean termsAndConditions: boolean
tags?: string tags?: string[]
dockerImage?: string dockerImage?: string
dockerImageCustom?: string dockerImageCustom?: string
dockerImageCustomTag?: string dockerImageCustomTag?: string

View File

@ -51,8 +51,7 @@ function dateToStringNoMS(date: Date): string {
return date.toISOString().replace(/\.[0-9]{3}Z/, 'Z') return date.toISOString().replace(/\.[0-9]{3}Z/, 'Z')
} }
function transformTags(value: string): string[] { function transformTags(originalTags: string[]): string[] {
const originalTags = value?.split(',')
const transformedTags = originalTags?.map((tag) => slugify(tag).toLowerCase()) const transformedTags = originalTags?.map((tag) => slugify(tag).toLowerCase())
return transformedTags return transformedTags
} }

View File

@ -22,7 +22,7 @@ const validationMetadata = {
) )
.required('Required'), .required('Required'),
author: Yup.string().required('Required'), author: Yup.string().required('Required'),
tags: Yup.string().nullable(), tags: Yup.array<string[]>().nullable(),
termsAndConditions: Yup.boolean() termsAndConditions: Yup.boolean()
.required('Required') .required('Required')
.isTrue('Please agree to the Terms and Conditions.') .isTrue('Please agree to the Terms and Conditions.')