Commit 37f66880 authored by Max Alekseenko's avatar Max Alekseenko

add security score component, integrate real data

parent 95a83474
......@@ -10,13 +10,19 @@ const submitFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM');
const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM');
const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL');
const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const securityReportsUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL');
const title = 'Marketplace';
const config: Feature<(
{ configUrl: string } |
{ api: { endpoint: string; basePath: string } }
) & { submitFormUrl: string; categoriesUrl: string | undefined; suggestIdeasFormUrl: string | undefined }
) & {
submitFormUrl: string;
categoriesUrl: string | undefined;
suggestIdeasFormUrl: string | undefined;
securityReportsUrl: string | undefined;
}
> = (() => {
if (enabled === 'true' && chain.rpcUrl && submitFormUrl) {
if (configUrl) {
......@@ -27,6 +33,7 @@ const config: Feature<(
submitFormUrl,
categoriesUrl,
suggestIdeasFormUrl,
securityReportsUrl,
});
} else if (adminServiceApiHost) {
return Object.freeze({
......@@ -35,6 +42,7 @@ const config: Feature<(
submitFormUrl,
categoriesUrl,
suggestIdeasFormUrl,
securityReportsUrl,
api: {
endpoint: adminServiceApiHost,
basePath: '',
......
......@@ -16,6 +16,7 @@ ASSETS_DIR="$1"
ASSETS_ENVS=(
"NEXT_PUBLIC_MARKETPLACE_CONFIG_URL"
"NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL"
"NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL"
"NEXT_PUBLIC_FEATURED_NETWORKS"
"NEXT_PUBLIC_FOOTER_LINKS"
"NEXT_PUBLIC_NETWORK_LOGO"
......
......@@ -150,8 +150,7 @@ const MarketplaceAppCard = ({
{ !isLoading && (
<IconButton
display={{ base: 'block', sm: isFavorite ? 'block' : 'none' }}
_groupHover={{ display: 'block' }}
display="block"
position="absolute"
right={{ base: 3, sm: '10px' }}
top={{ base: 3, sm: '14px' }}
......@@ -163,8 +162,8 @@ const MarketplaceAppCard = ({
h={ 8 }
onClick={ handleFavoriteClick }
icon={ isFavorite ?
<IconSvg name="star_filled" w={ 4 } h={ 4 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 4 } h={ 4 } color="gray.300"/>
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
}
/>
) }
......
......@@ -138,8 +138,8 @@ const MarketplaceAppModal = ({
h={ 8 }
onClick={ handleFavoriteClick }
icon={ isFavorite ?
<IconSvg name="star_filled" w={ 4 } h={ 4 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 4 } h={ 4 } color="gray.300"/> }
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.600"/> }
/>
</Box>
</Box>
......
......@@ -5,6 +5,7 @@ import type { MouseEvent } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace';
import config from 'configs/app';
import { apos } from 'lib/html-entities';
import DataListDisplay from 'ui/shared/DataListDisplay';
import EmptySearchResult from 'ui/shared/EmptySearchResult';
......@@ -21,13 +22,39 @@ interface Props {
isLoading: boolean;
selectedCategoryId?: string;
onAppClick: (event: MouseEvent, id: string) => void;
securityReports: Array<any> | undefined; // eslint-disable-line @typescript-eslint/no-explicit-any
}
const MarketplaceListWithScores = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, onAppClick }: Props) => {
const MarketplaceListWithScores = ({
apps,
showAppInfo,
favoriteApps,
onFavoriteClick,
isLoading,
selectedCategoryId,
onAppClick,
securityReports = [],
}: Props) => {
const displayedApps = React.useMemo(() =>
apps
.map((app) => {
const securityReport = securityReports.find(item => item.appName === app.id)?.chainsData[config.chain.name?.toLowerCase() || ''];
if (securityReport) {
const issues: Record<string, number> = securityReport.overallInfo.issueSeverityDistribution;
securityReport.overallInfo.totalIssues = Object.values(issues).reduce((acc, val) => acc + val, 0);
securityReport.overallInfo.securityScore = Number(securityReport.overallInfo.securityScore.toFixed(2));
}
return { ...app, securityReport };
})
.filter((app) => app.securityReport)
.sort((a, b) => b.securityReport.overallInfo.securityScore - a.securityReport.overallInfo.securityScore)
, [ apps, securityReports ]);
const content = apps.length > 0 ? (
<>
<Show below="lg" ssr={ false }>
{ apps.map((app, index) => (
{ displayedApps.map((app, index) => (
<ListItem
key={ app.id + (isLoading ? index : '') }
app={ app }
......@@ -41,7 +68,7 @@ const MarketplaceListWithScores = ({ apps, showAppInfo, favoriteApps, onFavorite
</Show>
<Hide below="lg" ssr={ false }>
<Table
apps={ apps }
apps={ displayedApps }
isLoading={ isLoading }
onAppClick={ onAppClick }
favoriteApps={ favoriteApps }
......
import { Box, Text } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import { apos } from 'lib/html-entities';
import LinkExternal from 'ui/shared/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
type Props = {
securityReport?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
isLarge?: boolean;
}
const AppSecurityReport = ({ securityReport, isLarge }: Props) => {
const {
overallInfo: {
securityScore,
solidityScanContractsNumber,
issueSeverityDistribution,
totalIssues,
},
} = securityReport;
return (
<SolidityscanReportButton
height={ isLarge ? undefined : '30px' }
score={ securityScore }
popoverContent={ (
<>
<Box mb={ 5 }>
{ solidityScanContractsNumber } smart contracts were evaluated to determine
this protocol{ apos }s overall security score on the { config.chain.name } network.
</Box>
<SolidityscanReportScore score={ securityScore }/>
{ issueSeverityDistribution && totalIssues > 0 && (
<Box mb={ 5 }>
<Text py="7px" variant="secondary" fontSize="xs" fontWeight={ 500 }>Threat score & vulnerabilities</Text>
<SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/>
</Box>
) }
<LinkExternal href="#">Analyzed contracts</LinkExternal>
</>
) }
/>
);
};
export default AppSecurityReport;
import { Flex, LinkBox, IconButton } from '@chakra-ui/react';
import { Flex, IconButton } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
......@@ -9,10 +9,11 @@ import IconSvg from 'ui/shared/IconSvg';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import AppLink from './AppLink';
import AppSecurityReport from './AppSecurityReport';
import LinkButton from './LinkButton';
interface Props {
app: MarketplaceAppPreview;
type Props = {
app: MarketplaceAppPreview & { securityReport?: any }; // eslint-disable-line @typescript-eslint/no-explicit-any
onInfoClick: (id: string) => void;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
......@@ -21,7 +22,16 @@ interface Props {
}
const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, onAppClick }: Props) => {
const { id } = app;
const {
id,
securityReport,
securityReport: {
overallInfo: {
verifiedNumber,
totalContractsNumber,
},
},
} = app;
const handleInfoClick = React.useCallback((event: MouseEvent) => {
event.preventDefault();
......@@ -39,28 +49,19 @@ const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, on
py={ 3 }
sx={{ ':first-child': { borderTop: 'none' }, ':last-child': { borderBottom: 'none' } }}
>
<LinkBox height="100%" width="100%" role="group">
<Flex
direction="column"
justifyContent="stretch"
padding={ 3 }
>
<Flex
direction="column"
justifyContent="stretch"
padding={ 3 }
width="100%"
>
<Flex position="relative">
<AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick }/>
<Flex>
<Flex flex={ 1 } gap={ 3 }>
<LinkButton onClick={ handleInfoClick } icon="contracts">13</LinkButton>
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">13</LinkButton>
</Flex>
{ !isLoading && (
<LinkButton onClick={ handleInfoClick }>More info</LinkButton>
) }
</Flex>
{ !isLoading && (
<IconButton
position="absolute"
right={ 0 }
top={ 0 }
right={ -1 }
top={ -1 }
aria-label="Mark as favorite"
title="Mark as favorite"
variant="ghost"
......@@ -69,13 +70,23 @@ const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, on
h={ 8 }
onClick={ handleFavoriteClick }
icon={ isFavorite ?
<IconSvg name="star_filled" w={ 4 } h={ 4 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 4 } h={ 4 } color="gray.300"/>
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
}
/>
) }
</Flex>
</LinkBox>
<Flex alignItems="center">
<Flex flex={ 1 } gap={ 3 } alignItems="center">
<AppSecurityReport securityReport={ securityReport }/>
<LinkButton onClick={ handleInfoClick } icon="contracts">{ totalContractsNumber }</LinkButton>
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">{ verifiedNumber }</LinkButton>
</Flex>
{ !isLoading && (
<LinkButton onClick={ handleInfoClick }>More info</LinkButton>
) }
</Flex>
</Flex>
</ListItemMobile>
);
};
......
......@@ -20,7 +20,7 @@ type Props = {
const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onInfoClick }: Props) => {
return (
<ChakraTable>
<Thead top={ 80 }>
<Thead top={ 0 }>
<Tr>
<Th w="5%"></Th>
<Th w="40%">App</Th>
......
......@@ -8,10 +8,11 @@ import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg';
import AppLink from './AppLink';
import AppSecurityReport from './AppSecurityReport';
import LinkButton from './LinkButton';
type Props = {
app: MarketplaceAppPreview;
app: MarketplaceAppPreview & { securityReport?: any }; // eslint-disable-line @typescript-eslint/no-explicit-any
isLoading?: boolean;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
......@@ -28,7 +29,16 @@ const TableItem = ({
onInfoClick,
}: Props) => {
const { id } = app;
const {
id,
securityReport,
securityReport: {
overallInfo: {
verifiedNumber,
totalContractsNumber,
},
},
} = app;
const handleInfoClick = React.useCallback((event: MouseEvent) => {
event.preventDefault();
......@@ -52,20 +62,22 @@ const TableItem = ({
h={ 8 }
onClick={ handleFavoriteClick }
icon={ isFavorite ?
<IconSvg name="star_filled" w={ 4 } h={ 4 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 4 } h={ 4 } color="gray.300"/>
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
}
/>
</Td>
<Td verticalAlign="middle">
<AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick } isLarge/>
</Td>
<Td verticalAlign="middle"></Td>
<Td verticalAlign="middle">
<LinkButton onClick={ handleInfoClick } icon="contracts">13</LinkButton>
<AppSecurityReport securityReport={ securityReport } isLarge/>
</Td>
<Td verticalAlign="middle">
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">13</LinkButton>
<LinkButton onClick={ handleInfoClick } icon="contracts">{ totalContractsNumber }</LinkButton>
</Td>
<Td verticalAlign="middle">
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">{ verifiedNumber }</LinkButton>
</Td>
<Td verticalAlign="middle" isNumeric>
<LinkButton onClick={ handleInfoClick }>More info</LinkButton>
......
import { useQuery } from '@tanstack/react-query';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/hooks/useFetch';
const feature = config.features.marketplace;
const securityReportsUrl = (feature.isEnabled && feature.securityReportsUrl) || '';
export default function useSecurityReports() {
const apiFetch = useApiFetch();
return useQuery<unknown, ResourceError<unknown>, Array<any>>({ // eslint-disable-line @typescript-eslint/no-explicit-any
queryKey: [ 'marketplace-security-reports' ],
queryFn: async() => apiFetch(securityReportsUrl, undefined, { resource: 'marketplace-security-reports' }),
placeholderData: securityReportsUrl ? [] : undefined,
staleTime: Infinity,
enabled: Boolean(securityReportsUrl),
});
}
......@@ -24,6 +24,7 @@ import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
import useMarketplace from '../marketplace/useMarketplace';
import useSecurityReports from '../marketplace/useSecurityReports';
const feature = config.features.marketplace;
const links: Array<{ label: string; href: string; icon: IconName }> = [];
......@@ -68,6 +69,12 @@ const Marketplace = () => {
appsTotal,
isCategoriesPlaceholderData,
} = useMarketplace();
const {
data: securityReports,
isPlaceholderData: isSecurityReportsPlaceholderData,
} = useSecurityReports();
const isMobile = useIsMobile();
const categoryTabs = React.useMemo(() => {
......@@ -203,9 +210,10 @@ const Marketplace = () => {
showAppInfo={ showAppInfo }
favoriteApps={ favoriteApps }
onFavoriteClick={ onFavoriteClick }
isLoading={ isPlaceholderData }
isLoading={ isPlaceholderData || isSecurityReportsPlaceholderData }
selectedCategoryId={ selectedCategoryId }
onAppClick={ handleAppClick }
securityReports={ securityReports }
/>
) : (
<MarketplaceList
......
......@@ -19,9 +19,10 @@ interface Props {
score: number;
popoverContent?: React.ReactNode;
isLoading?: boolean;
height?: string;
}
const SolidityscanReportButton = ({ className, score, popoverContent, isLoading }: Props) => {
const SolidityscanReportButton = ({ className, score, popoverContent, isLoading, height = '32px' }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const { scoreColor } = useScoreLevelAndColor(score);
......@@ -39,7 +40,7 @@ const SolidityscanReportButton = ({ className, score, popoverContent, isLoading
aria-label="SolidityScan score"
fontWeight={ 500 }
px="6px"
h="32px"
h={ height }
flexShrink={ 0 }
>
<IconSvg name={ score < 80 ? 'score/score-not-ok' : 'score/score-ok' } boxSize={ 5 } mr={ 1 }/>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment