From 43a19df36593f8ca87e74c920effc8a6c5878f82 Mon Sep 17 00:00:00 2001 From: isstuev Date: Thu, 3 Oct 2024 19:20:10 +0200 Subject: [PATCH] Graph integration --- configs/app/features/marketplace.ts | 3 ++ configs/envs/.env.gnosis | 1 + deploy/scripts/download_assets.sh | 1 + deploy/tools/envs-validator/index.ts | 1 + deploy/tools/envs-validator/schema.ts | 24 +++++++++++ docs/ENVS.md | 1 + icons/brands/graph.svg | 4 ++ lib/hooks/useGraphLinks.tsx | 19 +++++++++ public/icons/name.d.ts | 1 + ui/marketplace/MarketplaceAppCard.tsx | 17 ++++++-- ui/marketplace/MarketplaceAppCardLink.tsx | 11 ++--- ui/marketplace/MarketplaceAppGraphLinks.tsx | 47 +++++++++++++++++++++ ui/marketplace/MarketplaceAppModal.tsx | 28 +++++++----- ui/marketplace/MarketplaceList.tsx | 4 ++ ui/pages/Marketplace.tsx | 5 +++ 15 files changed, 148 insertions(+), 19 deletions(-) create mode 100644 icons/brands/graph.svg create mode 100644 lib/hooks/useGraphLinks.tsx create mode 100644 ui/marketplace/MarketplaceAppGraphLinks.tsx diff --git a/configs/app/features/marketplace.ts b/configs/app/features/marketplace.ts index 72f5d1ebde..ab5ab4a965 100644 --- a/configs/app/features/marketplace.ts +++ b/configs/app/features/marketplace.ts @@ -16,6 +16,7 @@ const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNE const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL'); const ratingAirtableApiKey = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY'); const ratingAirtableBaseId = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID'); +const graphLinksUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL'); const title = 'Marketplace'; @@ -30,6 +31,7 @@ const config: Feature<( featuredApp: string | undefined; banner: { contentUrl: string; linkUrl: string } | undefined; rating: { airtableApiKey: string; airtableBaseId: string } | undefined; + graphLinksUrl: string | undefined; }> = (() => { if (enabled === 'true' && chain.rpcUrl && submitFormUrl) { const props = { @@ -46,6 +48,7 @@ const config: Feature<( airtableApiKey: ratingAirtableApiKey, airtableBaseId: ratingAirtableBaseId, } : undefined, + graphLinksUrl, }; if (configUrl) { diff --git a/configs/envs/.env.gnosis b/configs/envs/.env.gnosis index fad2dde745..60e7c3c161 100644 --- a/configs/envs/.env.gnosis +++ b/configs/envs/.env.gnosis @@ -42,6 +42,7 @@ NEXT_PUBLIC_MARKETPLACE_ENABLED=true NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/marketplace-graph-test/test-configs/marketplace-graph-links.json NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} diff --git a/deploy/scripts/download_assets.sh b/deploy/scripts/download_assets.sh index d21e63482d..f53bbb2bea 100755 --- a/deploy/scripts/download_assets.sh +++ b/deploy/scripts/download_assets.sh @@ -18,6 +18,7 @@ ASSETS_ENVS=( "NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL" "NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL" "NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL" + "NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL" "NEXT_PUBLIC_FEATURED_NETWORKS" "NEXT_PUBLIC_FOOTER_LINKS" "NEXT_PUBLIC_NETWORK_LOGO" diff --git a/deploy/tools/envs-validator/index.ts b/deploy/tools/envs-validator/index.ts index d2c35ba50a..b867577fde 100644 --- a/deploy/tools/envs-validator/index.ts +++ b/deploy/tools/envs-validator/index.ts @@ -39,6 +39,7 @@ async function validateEnvs(appEnvs: Record) { 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL', 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', + 'NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL', 'NEXT_PUBLIC_FOOTER_LINKS', ]; diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 4ed440eef5..e7b1ff2ffd 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -153,6 +153,22 @@ const securityReportSchema: yup.ObjectSchema = chainsData: chainsDataSchema, }); +// const graphLinkSchema: yup.ObjectSchema<{ url: string; title: string }> = yup +// .object() +// .shape({ +// url: yup.string().test(urlTest).required(), +// title: yup.string().required(), +// }); + +// const graphLinksSchema = yup.lazy((obj) => { +// return yup.object().shape( +// Object.keys(obj).reduce((acc, key) => { +// acc[key] = graphLinkSchema; +// return acc; +// }, {}) +// ); +// }); + const marketplaceSchema = yup .object() .shape({ @@ -243,6 +259,14 @@ const marketplaceSchema = yup // eslint-disable-next-line max-len otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), }), + NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL: yup + .string() + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema, + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), }); const beaconChainSchema = yup diff --git a/docs/ENVS.md b/docs/ENVS.md index 2cafeee2dd..c3e3ab1225 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -505,6 +505,7 @@ This feature is **always enabled**, but you can disable it by passing `none` val | NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | v1.29.0+ | | NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY | `string` | Airtable API key | - | - | - | v1.33.0+ | | NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID | `string` | Airtable base ID with dapp ratings | - | - | - | v1.33.0+ | +| NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL | `string` | URL of the file (`.json` format only) which contains the list of The Graph links to be displayed on the Marketplace page | - | - | `https://example.com/graph_links.json` | v1.36.0+ | #### Marketplace app configuration properties diff --git a/icons/brands/graph.svg b/icons/brands/graph.svg new file mode 100644 index 0000000000..bd3cc916d9 --- /dev/null +++ b/icons/brands/graph.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/hooks/useGraphLinks.tsx b/lib/hooks/useGraphLinks.tsx new file mode 100644 index 0000000000..abcf9425af --- /dev/null +++ b/lib/hooks/useGraphLinks.tsx @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import config from 'configs/app'; +import type { ResourceError } from 'lib/api/resources'; +import useFetch from 'lib/hooks/useFetch'; + +const feature = config.features.marketplace; + +export default function useIsSafeAddress() { + const fetch = useFetch(); + + return useQuery, Record>>({ + queryKey: [ 'graph-links' ], + queryFn: async() => fetch((feature.isEnabled && feature.graphLinksUrl) ? feature.graphLinksUrl : '', undefined, { resource: 'graph-links' }), + enabled: feature.isEnabled && Boolean(feature.graphLinksUrl), + staleTime: Infinity, + placeholderData: {}, + }); +} diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts index c9c5819bc6..69a4cd9fc8 100644 --- a/public/icons/name.d.ts +++ b/public/icons/name.d.ts @@ -25,6 +25,7 @@ | "block" | "brands/blockscout" | "brands/celenium" + | "brands/graph" | "brands/safe" | "brands/solidity_scan" | "burger" diff --git a/ui/marketplace/MarketplaceAppCard.tsx b/ui/marketplace/MarketplaceAppCard.tsx index 050e17c5e3..6861f3b2e0 100644 --- a/ui/marketplace/MarketplaceAppCard.tsx +++ b/ui/marketplace/MarketplaceAppCard.tsx @@ -11,6 +11,7 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard'; import AppSecurityReport from './AppSecurityReport'; import FavoriteIcon from './FavoriteIcon'; import MarketplaceAppCardLink from './MarketplaceAppCardLink'; +import MarketplaceAppGraphLinks from './MarketplaceAppGraphLinks'; import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon'; import Rating from './Rating/Rating'; import type { RateFunction } from './Rating/useRatings'; @@ -28,6 +29,7 @@ interface Props extends MarketplaceAppWithSecurityReport { isRatingSending: boolean; isRatingLoading: boolean; canRate: boolean | undefined; + graphLinks: Array<{text: string; url: string}>; } const MarketplaceAppCard = ({ @@ -54,6 +56,7 @@ const MarketplaceAppCard = ({ isRatingSending, isRatingLoading, canRate, + graphLinks, }: Props) => { const isMobile = useIsMobile(); const categoriesLabel = categories.join(', '); @@ -118,11 +121,7 @@ const MarketplaceAppCard = ({ > + void; + className?: string; } -const MarketplaceAppCardLink = ({ url, external, id, title, onClick }: Props) => { +const MarketplaceAppCardLink = ({ url, external, id, title, onClick, className }: Props) => { const handleClick = React.useCallback((event: MouseEvent) => { onClick?.(event, id); }, [ onClick, id ]); return external ? ( - + { title } ) : ( - + { title } ); }; -export default MarketplaceAppCardLink; +export default chakra(MarketplaceAppCardLink); diff --git a/ui/marketplace/MarketplaceAppGraphLinks.tsx b/ui/marketplace/MarketplaceAppGraphLinks.tsx new file mode 100644 index 0000000000..9fc449e49b --- /dev/null +++ b/ui/marketplace/MarketplaceAppGraphLinks.tsx @@ -0,0 +1,47 @@ +import { + Text, + PopoverTrigger, + PopoverBody, + PopoverContent, + chakra, + Box, + VStack, +} from '@chakra-ui/react'; +import React from 'react'; + +import Popover from 'ui/shared/chakra/Popover'; +import IconSvg from 'ui/shared/IconSvg'; +import LinkExternal from 'ui/shared/links/LinkExternal'; + +interface Props { + className?: string; + links?: Array<{ title: string; url: string }>; +} + +const MarketplaceAppGraphLinks = ({ className, links }: Props) => { + if (!links || links.length === 0) { + return null; + } + + return ( + + + + + + + + + { `This dapp uses ${ links.length > 1 ? 'several subgraphs' : 'a subgraph' } provided by The Graph` } + { links.map(link => ( + { link.title } + )) } + + + + + + ); +}; + +export default React.memo(chakra(MarketplaceAppGraphLinks)); diff --git a/ui/marketplace/MarketplaceAppModal.tsx b/ui/marketplace/MarketplaceAppModal.tsx index 430766b1ac..4113283e00 100644 --- a/ui/marketplace/MarketplaceAppModal.tsx +++ b/ui/marketplace/MarketplaceAppModal.tsx @@ -18,6 +18,8 @@ import IconSvg from 'ui/shared/IconSvg'; import AppSecurityReport from './AppSecurityReport'; import FavoriteIcon from './FavoriteIcon'; +import MarketplaceAppGraphLinks from './MarketplaceAppGraphLinks'; +import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon'; import MarketplaceAppModalLink from './MarketplaceAppModalLink'; import Rating from './Rating/Rating'; import type { RateFunction } from './Rating/useRatings'; @@ -36,6 +38,7 @@ type Props = { isRatingSending: boolean; isRatingLoading: boolean; canRate: boolean | undefined; + graphLinks?: Array<{text: string; url: string}>; } const MarketplaceAppModal = ({ @@ -49,6 +52,7 @@ const MarketplaceAppModal = ({ isRatingSending, isRatingLoading, canRate, + graphLinks, }: Props) => { const { id, @@ -67,6 +71,7 @@ const MarketplaceAppModal = ({ categories, securityReport, rating, + internalWallet, } = data; const socialLinks = [ @@ -148,16 +153,19 @@ const MarketplaceAppModal = ({ /> - - { title } - + + + { title } + + + + >, unknown>; } const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, onAppClick, showContractList, userRatings, rateApp, isRatingSending, isRatingLoading, canRate, + graphLinksQuery, }: Props) => { const { cutRef, renderedItemsNum } = useLazyRenderedList(apps, !isLoading, 16); @@ -75,6 +78,7 @@ const MarketplaceList = ({ isRatingSending={ isRatingSending } isRatingLoading={ isRatingLoading } canRate={ canRate } + graphLinks={ graphLinksQuery.data?.[app.id] } /> )) } diff --git a/ui/pages/Marketplace.tsx b/ui/pages/Marketplace.tsx index 8e4347a6f1..1a3f251c84 100644 --- a/ui/pages/Marketplace.tsx +++ b/ui/pages/Marketplace.tsx @@ -7,6 +7,7 @@ import type { TabItem } from 'ui/shared/Tabs/types'; import config from 'configs/app'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; +import useGraphLinks from 'lib/hooks/useGraphLinks'; import useIsMobile from 'lib/hooks/useIsMobile'; import Banner from 'ui/marketplace/Banner'; import ContractListModal from 'ui/marketplace/ContractListModal'; @@ -80,6 +81,8 @@ const Marketplace = () => { const isMobile = useIsMobile(); + const graphLinksQuery = useGraphLinks(); + const categoryTabs = React.useMemo(() => { const tabs: Array = categories.map(category => ({ id: category.name, @@ -236,6 +239,7 @@ const Marketplace = () => { isRatingSending={ isRatingSending } isRatingLoading={ isRatingLoading } canRate={ canRate } + graphLinksQuery={ graphLinksQuery } /> { (selectedApp && isAppInfoModalOpen) && ( @@ -250,6 +254,7 @@ const Marketplace = () => { isRatingSending={ isRatingSending } isRatingLoading={ isRatingLoading } canRate={ canRate } + graphLinks={ graphLinksQuery.data?.[selectedApp.id] } /> ) }