Projects STRLCPY gradejs Commits 4594f225
🤬
  • ■ ■ ■ ■ ■ ■
    packages/public-api/src/clientApiRouter.ts
    skipped 1 lines
    2 2  // See also: https://colinhacks.com/essays/painless-typesafety
    3 3  import { CreateExpressContextOptions } from '@trpc/server/adapters/express';
    4 4  import { z, ZodError } from 'zod';
    5  -import { getOrRequestWebPageScan } from './website/service';
     5 +import { getWebPageScan, requestWebPageRescan } from './website/service';
    6 6  import { getAffectingVulnerabilities } from './vulnerabilities/vulnerabilities';
    7 7  import {
    8 8   PackageMetadata,
    skipped 3 lines
    12 12   WebPageScan,
    13 13  } from '@gradejs-public/shared';
    14 14  import { getPackageMetadataByPackageNames } from './packageMetadata/packageMetadataService';
    15  - 
    16  -// const hostnameRe =
    17  -// /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/;
    18 15   
    19 16  // created for each request
    20 17  export const createContext = (_: CreateExpressContextOptions) => ({}); // no context
    skipped 18 lines
    39 36   }));
    40 37  }
    41 38   
    42  -type RequestWebPageScanResponse = Pick<WebPageScan, 'status' | 'finishedAt'> & {
     39 +enum WebPageStatusNoData {
     40 + NoData = 'noData',
     41 +}
     42 +type WebPageStatus = WebPageScan.Status | WebPageStatusNoData;
     43 + 
     44 +type RequestWebPageScanResponse = Pick<WebPageScan, 'finishedAt'> & {
    43 45   id: string;
     46 + status: WebPageStatus;
    44 47   scanResult?: {
    45 48   identifiedModuleMap: Record<string, WebPageScan.IdentifiedModule>;
    46 49   identifiedPackages: ScanResultPackageWithMetadata[];
    skipped 3 lines
    50 53   
    51 54  export const appRouter = trpc
    52 55   .router<Context>()
    53  - .mutation('requestWebPageScan', {
     56 + .mutation('requestWebPageRescan', {
     57 + input: z.string().url(),
     58 + async resolve({ input: url }) {
     59 + return await requestWebPageRescan(url);
     60 + },
     61 + })
     62 + .query('getWebPageScan', {
    54 63   input: z.string().url(),
    55 64   async resolve({ input: url }) {
    56  - const scan = await getOrRequestWebPageScan(url);
     65 + const scan = await getWebPageScan(url);
     66 + if (!scan) {
     67 + return { id: '', status: WebPageStatusNoData.NoData };
     68 + }
    57 69   
    58 70   const scanResponse: RequestWebPageScanResponse = {
    59 71   id: scan.id.toString(),
    skipped 40 lines
  • ■ ■ ■ ■ ■ ■
    packages/public-api/src/website/service.ts
    skipped 39 lines
    40 40   return webPageEntity;
    41 41  }
    42 42   
    43  -export async function getOrRequestWebPageScan(url: string) {
     43 +export async function getWebPageScan(url: string) {
    44 44   const parsedUrl = new URL(url);
     45 + const db = await getDatabaseConnection();
    45 46   
     47 + const webPageScanRepo = db.getRepository(WebPageScan);
     48 + const webPageEntity = await findOrCreateWebPage(parsedUrl, db.createEntityManager());
     49 + 
     50 + return await webPageScanRepo
     51 + .createQueryBuilder('scan')
     52 + .where('scan.web_page_id = :webPageId', { webPageId: webPageEntity.id })
     53 + .orderBy('scan.created_at', 'DESC')
     54 + .limit(1)
     55 + .getOne();
     56 +}
     57 + 
     58 +export async function requestWebPageRescan(url: string) {
     59 + const parsedUrl = new URL(url);
    46 60   const db = await getDatabaseConnection();
     61 + const lastScan = await getWebPageScan(url);
     62 + if (lastScan && Date.now() - lastScan?.createdAt.getTime() < RESCAN_TIMEOUT_MS) {
     63 + return false;
     64 + }
    47 65   
    48  - const result = await db.transaction(async (em) => {
     66 + return await db.transaction(async (em) => {
    49 67   const webPageScanRepo = em.getRepository(WebPageScan);
    50  - 
    51 68   const webPageEntity = await findOrCreateWebPage(parsedUrl, em);
    52 69   
    53  - const mostRecentScan = await webPageScanRepo
    54  - .createQueryBuilder('scan')
    55  - .where('scan.web_page_id = :webPageId', { webPageId: webPageEntity.id })
    56  - .orderBy('scan.created_at', 'DESC')
    57  - .limit(1)
    58  - .getOne();
    59  - 
    60  - if (mostRecentScan && Date.now() - mostRecentScan.createdAt.getTime() < RESCAN_TIMEOUT_MS) {
    61  - return mostRecentScan;
    62  - }
    63  - 
    64 70   const webPageScanEntity = await webPageScanRepo.save({
    65 71   webPage: webPageEntity,
    66 72   status: WebPageScan.Status.Pending,
    67 73   });
    68 74   
    69 75   await systemApi.requestWebPageScan(parsedUrl.toString(), webPageScanEntity.id.toString());
    70  - 
    71  - return webPageScanEntity;
     76 + return true;
    72 77   });
    73  - 
    74  - return result;
    75 78  }
    76 79   
    77 80  export async function syncWebPageScanResult(scanReport: systemApi.ScanReport) {
    skipped 46 lines
  • ■ ■ ■ ■
    packages/web/src/components/layouts/Filters/Filters.tsx
    skipped 12 lines
    13 13  };
    14 14   
    15 15  export type FiltersState = {
    16  - filter: 'all' | 'outdated' | 'vulnerable' | 'name';
     16 + filter: 'all' | 'outdated' | 'vulnerable';
    17 17   sort: 'name' | 'size' | 'severity' | 'importDepth' | 'packagePopularity' | 'confidenceScore';
    18 18   filterPackageName?: string;
    19 19  };
    skipped 95 lines
  • ■ ■ ■ ■ ■
    packages/web/src/components/layouts/SearchResults/SearchResults.tsx
    1  -import React, { useEffect, useRef, useState } from 'react';
    2  -import semver from 'semver';
     1 +import React, { useRef } from 'react';
    3 2  import styles from './SearchResults.module.scss';
    4 3  import Footer from 'components/ui/Footer/Footer';
    5 4  import Container from 'components/ui/Container/Container';
    6  -import { Icon } from '../../ui/Icon/Icon';
    7 5  import PackagePreview from '../../ui/PackagePreview/PackagePreview';
    8 6  import SearchedResource from '../../ui/SearchedResource/SearchedResource';
    9 7  import CardGroup from '../../ui/CardGroup/CardGroup';
    skipped 8 lines
    18 16  import PopularPackageCardList from '../../ui/CardList/PopularPackageCardList';
    19 17  import { RequestWebPageScanOutput } from '../../../services/apiClient';
    20 18  import { SubmitHandler } from 'react-hook-form';
    21  - 
    22  -type FiltersState = {
    23  - filter: 'all' | 'outdated' | 'vulnerable' | 'name';
    24  - sort: 'name' | 'size' | 'severity' | 'importDepth' | 'packagePopularity' | 'confidenceScore';
    25  - filterPackageName?: string;
    26  -};
     19 +import { ClientApi } from '../../../services/apiClient';
     20 +import { ScanStatus, IdentifiedPackage } from 'store/selectors/websiteResults';
    27 21   
    28 22  type Props = {
    29  - searchQuery: string;
    30  - host: string;
    31  - siteFavicon: string;
    32 23   isLoading: boolean;
    33 24   isPending: boolean;
    34  - scanOutput: RequestWebPageScanOutput;
    35  - onFiltersApply: SubmitHandler<FiltersState>;
     25 + searchQuery: string;
     26 + packages: IdentifiedPackage[];
     27 + packagesStats: { total: number; vulnerable: number; outdated: number };
     28 + vulnerabilities: Record<string, ClientApi.PackageVulnerabilityResponse[]>;
     29 + keywordsList: string[];
     30 + status: ScanStatus;
     31 + // siteFavicon: string;
    36 32  };
    37 33   
    38  -export default function SearchResults({ pageLoading = false }: Props) {
    39  - const [loading, setLoading] = useState<boolean>(pageLoading);
    40  - 
    41  - const loadingRef = useRef<LoadingBarRef>(null);
    42  - 
    43  - // FIXME: just for demo purposes to show how loading bar works
     34 +export default function SearchResults({
     35 + isLoading,
     36 + isPending,
     37 + searchQuery,
     38 + packages,
     39 + packagesStats,
     40 + vulnerabilities,
     41 + keywordsList,
     42 + status,
     43 +}: Props) {
    44 44   // Documentation: https://github.com/klendi/react-top-loading-bar
    45  - // Starts the loading indicator with a random starting value between 20-30 (or startingValue),
    46  - // then repetitively after an refreshRate (in milliseconds), increases it by a random value
    47  - // between 2-10. This continues until it reaches 90% of the indicator's width.
    48  - useEffect(() => {
    49  - loadingRef?.current?.continuousStart(10, 5000);
    50  - 
    51  - // After 10 seconds makes the loading indicator reach 100% of his width and then fade.
    52  - setTimeout(() => {
    53  - loadingRef?.current?.complete();
    54  - setLoading(false);
    55  - }, 60000);
    56  - }, []);
    57  - 
    58  - // TODO: memoize
    59  - // Corresponding flags are determined by array index (packages[i] <=> flags[i]).
    60  - const flags: Array<{ vulnerable: boolean; duplicate: boolean; outdated: boolean }> = (
    61  - scanOutput.scanResult?.packages ?? []
    62  - ).map((pkg) => ({
    63  - duplicate: false, // TODO
    64  - outdated: !!(
    65  - pkg.registryMetadata && semver.gtr(pkg.registryMetadata.latestVersion, pkg.versionRange)
    66  - ),
    67  - vulnerable: (scanOutput.scanResult?.vulnerabilities[pkg.name]?.length ?? 0) > 0,
    68  - }));
    69  - 
    70  - // TODO memoize
    71  - const outdatedCount = flags.filter((f) => f.outdated).length;
    72  - 
    73  - // TODO memoize
    74  - const keywordsList = [
    75  - ...new Set(
    76  - scanOutput.scanResult?.packages.reduce((acc, pkg) => {
    77  - return acc.concat(pkg.registryMetadata?.keywords ?? []);
    78  - }, [] as string[])
    79  - ),
    80  - ];
    81  - 
    82  - /*
    83  - // TODO: mock data, remove later
    84  - const metaItems = [
    85  - {
    86  - icon: <Icon kind='weight' width={24} height={24} />,
    87  - text: '159 kb webpack bundle size',
    88  - },
    89  - {
    90  - icon: <Icon kind='search' width={24} height={24} color='#212121' />,
    91  - text: '50 scripts found',
    92  - },
    93  - {
    94  - icon: <Icon kind='vulnerability' width={24} height={24} color='#F3512E' />,
    95  - text: '6 vulnerabilities in 4 packages',
    96  - },
    97  - {
    98  - icon: <Icon kind='duplicate' color='#F3812E' width={24} height={24} />,
    99  - text: '12 duplicate packages',
    100  - },
    101  - {
    102  - icon: <Icon kind='outdated' color='#F1CE61' stroke='white' width={24} height={24} />,
    103  - text: '18 outdated packages',
    104  - },
    105  - ];
    106  - 
    107  - // TODO: mock data, remove later
    108  - const keyWords = ['#moment', '#date', '#react', '#parse', '#fb', '#angular', '#vue', '#ember'];
    109  - 
    110  - // TODO: mock data, remove later
    111  - const vulnerabilities = ['Vulnerabilities', 'Outdated', 'Duplicate'];
    112  - 
    113  - // TODO: mock data, remove later
    114  - const authors = ['gaearon', 'acdlite', 'sophiebits', 'sebmarkbage', 'zpao', 'trueadm', 'bvaughn'];
    115  - */
     45 + const loadingRef = useRef<LoadingBarRef>(null);
    116 46   
    117 47   return (
    118 48   <>
    119  - {pageLoading && (
     49 + {isLoading && (
    120 50   <LoadingBar
    121 51   ref={loadingRef}
    122 52   color='linear-gradient(90deg, #2638D9 0%, #B22AF2 100%)'
    skipped 9 lines
    132 62   <Container>
    133 63   <div className={styles.searchResults}>
    134 64   <div className={styles.searchResultsResource}>
    135  - {loading ? (
     65 + {isLoading ? (
    136 66   <SearchedResourceSkeleton
    137 67   image='https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg'
    138 68   name='pinterest.com'
    skipped 2 lines
    141 71   <SearchedResource
    142 72   image={siteFavicon}
    143 73   name={host}
    144  - totalPackages={scanOutput.scanResult?.packages.length ?? 0}
    145  - lastScanDate={scanOutput.finishedAt}
     74 + totalPackages={totalPackages}
     75 + lastScanDate={finishedAt}
    146 76   />
    147 77   )}
    148 78   </div>
    149 79   
    150 80   <div className={styles.searchResultsSidebar}>
    151 81   <SearchResultsSidebar
    152  - metaItems={scanOutput.scanResult?.meta}
     82 + metaItems={metaItems}
    153 83   keyWords={keyWords}
    154 84   vulnerabilities={vulnerabilities}
    155  - authors={state.fetched.authors}
     85 + authors={authors}
    156 86   loading={loading}
    157 87   />
    158 88   </div>
    159 89   
    160 90   <div className={styles.packages}>
    161  - {loading ? (
     91 + {isLoading ? (
    162 92   <>
    163 93   <PackagePreviewSkeleton />
    164 94   <PackagePreviewSkeleton />
    skipped 3 lines
    168 98   <PackagePreviewSkeleton />
    169 99   </>
    170 100   ) : (
    171  - scanOutput.scanResult?.packages.map((pkg, index) => (
    172  - <PackagePreview // TODO пробрасываем сюда данные
     101 + packages.map((pkg, index) => (
     102 + <PackagePreview
    173 103   pkg={pkg}
    174  - flags={flags[index]}
    175 104   sites={[] /* TODO */}
    176 105   opened={index === 0}
    177 106   totalRatedPackages={totalRatedPackages}
    skipped 30 lines
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/layouts/Website/Website.module.scss
    1  -@import '~styles/responsive.scss';
    2  - 
    3  -.heading {
    4  - font-size: 44px;
    5  - margin: 0 0 32px;
    6  - 
    7  - @include mobile {
    8  - font-size: 28px;
    9  - }
    10  -}
    11  - 
    12  -.highlights {
    13  - margin-bottom: 64px;
    14  -}
    15  - 
    16  -.packages {
    17  - display: flex;
    18  - flex-wrap: wrap;
    19  - 
    20  - &.grid .package {
    21  - width: 25%;
    22  - border-left-width: 0;
    23  - 
    24  - @include mobile {
    25  - width: 50%;
    26  - height: 200px;
    27  - 
    28  - &:nth-child(2n + 1) {
    29  - padding-left: 0;
    30  - }
    31  - 
    32  - &:nth-child(2n) {
    33  - border-right-width: 0;
    34  - }
    35  - }
    36  - 
    37  - &:nth-child(4n + 1) {
    38  - padding-left: 0;
    39  - }
    40  - 
    41  - &:nth-child(4n) {
    42  - border-right-width: 0;
    43  - }
    44  - }
    45  - 
    46  - &.lines .package {
    47  - width: 100%;
    48  - }
    49  -}
    50  - 
    51  -.packagesHeading {
    52  - font-size: 16px;
    53  - line-height: 22px;
    54  - color: #0f0f0f;
    55  - font-weight: 700;
    56  - margin-bottom: 24px;
    57  - display: flex;
    58  - align-items: center;
    59  -}
    60  - 
    61  -.packagesTotal {
    62  - font-weight: 400;
    63  - color: #a5a5a5;
    64  - margin-left: 8px;
    65  - flex: 1;
    66  -}
    67  - 
    68  -.viewSelect {
    69  - margin-left: 12px;
    70  - cursor: pointer;
    71  -}
    72  - 
    73  -.disclaimer {
    74  - background: url('~assets/icons/warn.svg') 16px 16px / 20px 20px no-repeat;
    75  - padding: 16px 16px 16px 52px;
    76  - border: 1px solid #e6e6e6;
    77  - font-size: 14px;
    78  - line-height: 20px;
    79  - margin-bottom: 24px;
    80  - border-radius: 4px;
    81  -}
    82  - 
    83  -.disclaimerLink {
    84  - color: #000;
    85  -}
    86  - 
    87  -.disclaimerLoading {
    88  - background: url('~assets/loader-black.svg') 12px 12px / 28px 28px no-repeat;
    89  -}
    90  - 
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/layouts/Website/Website.stories.tsx
    1  -import React from 'react';
    2  -import Website, { Props } from './Website';
    3  - 
    4  -export default {
    5  - title: 'Layouts / Website',
    6  - component: Website,
    7  - parameters: {
    8  - layout: 'fullscreen',
    9  - },
    10  -};
    11  - 
    12  -export const Ready = (args: Props) => <Website {...args} />;
    13  -Ready.args = {
    14  - host: 'gradejs.com',
    15  - packages: [
    16  - {
    17  - name: 'react',
    18  - versionSet: ['17.0.2'],
    19  - versionRange: '17.0.2',
    20  - registryMetadata: {
    21  - latestVersion: '18.0.0',
    22  - description: 'Test description',
    23  - repositoryUrl: 'https://github.com',
    24  - homepageUrl: 'https://github.com/test',
    25  - monthlyDownloads: 1,
    26  - },
    27  - },
    28  - {
    29  - name: 'react-dom',
    30  - versionSet: ['17.0.2'],
    31  - versionRange: '17.0.2',
    32  - },
    33  - {
    34  - name: 'react',
    35  - versionSet: ['17.0.2'],
    36  - versionRange: '17.0.2',
    37  - },
    38  - {
    39  - name: 'react-dom',
    40  - versionSet: ['17.0.2'],
    41  - versionRange: '17.0.2',
    42  - },
    43  - {
    44  - name: 'react',
    45  - versionSet: ['17.0.2'],
    46  - versionRange: '17.0.2',
    47  - },
    48  - {
    49  - name: 'react-dom',
    50  - versionSet: ['17.0.2'],
    51  - versionRange: '17.0.2',
    52  - },
    53  - {
    54  - name: 'react',
    55  - versionSet: ['17.0.2'],
    56  - versionRange: '17.0.2',
    57  - },
    58  - {
    59  - name: 'react-dom',
    60  - versionSet: ['17.0.2'],
    61  - versionRange: '17.0.2',
    62  - },
    63  - {
    64  - name: 'react',
    65  - versionSet: ['17.0.2'],
    66  - versionRange: '17.0.2',
    67  - },
    68  - {
    69  - name: 'react-dom',
    70  - versionSet: ['17.0.2'],
    71  - versionRange: '17.0.2',
    72  - },
    73  - ],
    74  - vulnerabilities: {
    75  - react: [
    76  - {
    77  - affectedPackageName: 'react',
    78  - affectedVersionRange: '>=17.0.0 <18.0.0',
    79  - osvId: 'GRJS-test-id-1',
    80  - detailsUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
    81  - summary: 'Test vulnerability 1 summary',
    82  - severity: 'HIGH',
    83  - },
    84  - {
    85  - affectedPackageName: 'react',
    86  - affectedVersionRange: '>=17.0.0 <18.0.0',
    87  - osvId: 'GRJS-test-id-2',
    88  - detailsUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
    89  - summary: 'Test vulnerability 2 summary',
    90  - severity: 'LOW',
    91  - },
    92  - ],
    93  - },
    94  -};
    95  - 
    96  -export const Pending = (args: Props) => <Website {...args} />;
    97  -Pending.args = {
    98  - host: 'gradejs.com',
    99  - packages: [],
    100  - isLoading: false,
    101  - isPending: true,
    102  -};
    103  - 
    104  -export const Loading = (args: Props) => <Website {...args} />;
    105  -Loading.args = {
    106  - host: 'gradejs.com',
    107  - packages: [],
    108  - isLoading: true,
    109  - isPending: false,
    110  -};
    111  - 
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/layouts/Website/Website.tsx
    1  -/* eslint-disable react/button-has-type */
    2  -import React, { useState } from 'react';
    3  -import clsx from 'clsx';
    4  -import { SubmitHandler } from 'react-hook-form';
    5  -import { Package, Section, PackageSkeleton } from 'components/ui';
    6  -import styles from './Website.module.scss';
    7  -import Filters, { FiltersState } from '../Filters/Filters';
    8  -import TagBadge from '../../ui/TagBadge/TagBadge';
    9  -import { trackCustomEvent } from '../../../services/analytics';
    10  -import { ClientApi } from '../../../services/apiClient';
    11  -import { Icon } from '../../ui/Icon/Icon';
    12  -import DefaultHeader from '../../ui/Header/DefaultHeader';
    13  - 
    14  -// TODO: Add plashechka
    15  -export type Props = {
    16  - host: string;
    17  - // highlights?: Array<{
    18  - // description: string;
    19  - // title: string;
    20  - // icon: string;
    21  - // }>;
    22  - isLoading: boolean;
    23  - isPending: boolean;
    24  - packages: ClientApi.ScanResultPackageResponse[];
    25  - vulnerabilities: Record<string, ClientApi.PackageVulnerabilityResponse[]>;
    26  - onFiltersApply: SubmitHandler<FiltersState>;
    27  -};
    28  - 
    29  -export default function Website({
    30  - host,
    31  - isLoading,
    32  - isPending,
    33  - packages,
    34  - vulnerabilities,
    35  - onFiltersApply,
    36  -}: Props) {
    37  - const [view, setView] = useState<'grid' | 'lines'>('grid');
    38  - 
    39  - return (
    40  - <>
    41  - <DefaultHeader />
    42  - <Section>
    43  - <h1 className={styles.heading}>{host}</h1>
    44  - 
    45  - {isPending ? (
    46  - <div className={clsx(styles.disclaimer, styles.disclaimerLoading)}>
    47  - GradeJS is currently processing this website. <br />
    48  - It may take a few minutes and depends on the number of JavaScript files and their size.
    49  - </div>
    50  - ) : (
    51  - packages.length > 0 && (
    52  - <div className={styles.disclaimer}>
    53  - The <strong>beta</strong> version of GradeJS is able to detect only 1,826 popular
    54  - packages with up to 85% accuracy.
    55  - <br />
    56  - <a
    57  - href='https://github.com/gradejs/gradejs/discussions/8'
    58  - target='_blank'
    59  - rel='noopener noreferrer'
    60  - className={styles.disclaimerLink}
    61  - >
    62  - Learn more about accuracy
    63  - </a>{' '}
    64  - or{' '}
    65  - <a
    66  - href='https://github.com/gradejs/gradejs/issues'
    67  - target='_blank'
    68  - rel='noopener noreferrer'
    69  - className={styles.disclaimerLink}
    70  - >
    71  - submit an issue
    72  - </a>
    73  - .
    74  - </div>
    75  - )
    76  - )}
    77  - 
    78  - <div className={styles.disclaimer}>
    79  - Packages that are known to be vulnerable are now highlighted with{' '}
    80  - <TagBadge color='red'>Vulnerable</TagBadge> badge. You can view detailed information on
    81  - related vulnerabilities by hovering over the badge.
    82  - </div>
    83  - 
    84  - {/* <div className={styles.highlights}>
    85  - {highlights.map((data, index) => (
    86  - <HighlightTech key={index.toString()} {...data} />
    87  - ))}
    88  - </div> */}
    89  - 
    90  - {!isLoading && (
    91  - <div className={styles.packagesHeading}>
    92  - NPM packages
    93  - <span className={styles.packagesTotal}>({packages.length})</span>
    94  - <Filters onSubmit={onFiltersApply} />
    95  - <Icon
    96  - kind='lines'
    97  - className={styles.viewSelect}
    98  - color={view === 'lines' ? '#0F0F0F' : '#E6E6E6'}
    99  - onClick={() => {
    100  - trackCustomEvent('HostnamePage', 'SetViewLines');
    101  - setView('lines');
    102  - }}
    103  - />
    104  - <Icon
    105  - kind='grid'
    106  - className={styles.viewSelect}
    107  - color={view === 'grid' ? '#0F0F0F' : '#E6E6E6'}
    108  - onClick={() => {
    109  - trackCustomEvent('HostnamePage', 'SetViewGrid');
    110  - setView('grid');
    111  - }}
    112  - />
    113  - </div>
    114  - )}
    115  - <div className={clsx(styles.packages, styles[view])}>
    116  - {packages.map((data, index) => (
    117  - <Package
    118  - key={index.toString()}
    119  - variant={view}
    120  - className={styles.package}
    121  - pkg={data}
    122  - vulnerabilities={vulnerabilities[data.name] || []}
    123  - />
    124  - ))}
    125  - {isLoading && (
    126  - <>
    127  - <PackageSkeleton className={styles.package} />
    128  - <PackageSkeleton className={styles.package} />
    129  - <PackageSkeleton className={styles.package} />
    130  - <PackageSkeleton className={styles.package} />
    131  - <PackageSkeleton className={styles.package} />
    132  - <PackageSkeleton className={styles.package} />
    133  - <PackageSkeleton className={styles.package} />
    134  - <PackageSkeleton className={styles.package} />
    135  - <PackageSkeleton className={styles.package} />
    136  - <PackageSkeleton className={styles.package} />
    137  - <PackageSkeleton className={styles.package} />
    138  - <PackageSkeleton className={styles.package} />
    139  - </>
    140  - )}
    141  - </div>
    142  - </Section>
    143  - </>
    144  - );
    145  -}
    146  - 
  • ■ ■ ■ ■ ■
    packages/web/src/components/pages/WebsiteResults.tsx
    skipped 5 lines
    6 6  import {
    7 7   useAppDispatch,
    8 8   useAppSelector,
    9  - getWebsite,
     9 + getScanResults,
    10 10   websiteResultsSelectors as selectors,
    11 11  } from '../../store';
    12 12   
    13 13  export function WebsiteResultsPage() {
    14 14   const { address } = useParams();
    15  - const hostname = new URL(address!).hostname;
    16 15   const navigate = useNavigate();
    17 16   const dispatch = useAppDispatch();
    18  - const { vulnerabilities } = useAppSelector(selectors.default);
     17 + 
     18 + /* TODO
     19 + - Отключить фильтры из селекторов (временно) ./
     20 + - Прокинуть данные из packages в packagePreview (все что есть), формализовать тип pkg ./
     21 + - Разобраться с тем как работают фильтрующие компоненты. Мб прикрутить их в отдельный redux-слайс для порядку.
     22 + - Сформулировать типы для фильтров и вернуть их обратно в селекторы.
     23 + */
     24 + 
     25 + const { vulnerabilities, keywordsList, status } = useAppSelector(selectors.default);
    19 26   const packagesFiltered = useAppSelector(selectors.packagesSortedAndFiltered);
    20 27   const packagesStats = useAppSelector(selectors.packagesStats);
    21 28   const { isProtected, isPending, isLoading, isFailed, isInvalid } = useAppSelector(
    skipped 3 lines
    25 32   // TODO: discuss. Looks ugly
    26 33   // Fetch data for SSR if host is already processed
    27 34   if (__isServer__ && address) {
    28  - dispatch(getWebsite({ hostname: address, useRetry: false }));
     35 + dispatch(getScanResults({ address, useRetry: false }));
    29 36   }
    30 37   
    31 38   useEffect(() => {
    32 39   if (address && isPending && !isFailed) {
    33  - const promise = dispatch(getWebsite({ hostname: address }));
     40 + const promise = dispatch(getScanResults({ address }));
    34 41   return function cleanup() {
    35 42   promise.abort();
    36 43   };
    skipped 17 lines
    54 61   action='Would you like to try another URL or report an issue?'
    55 62   actionTitle='Try another URL'
    56 63   host={address ?? ''}
     64 + /*
    57 65   onRetryClick={() => {
    58 66   trackCustomEvent('HostnamePage', 'ClickRetry_Protected');
    59 67   navigate('/', { replace: false });
    skipped 1 lines
    61 69   onReportClick={() => {
    62 70   trackCustomEvent('HostnamePage', 'ClickReport_Protected');
    63 71   }}
     72 + */
    64 73   />
    65 74   );
    66 75   }
    skipped 7 lines
    74 83   action='Would you like to try another URL or report an issue?'
    75 84   actionTitle='Try another URL'
    76 85   host={address ?? ''}
     86 + /*
    77 87   onRetryClick={() => {
    78 88   trackCustomEvent('HostnamePage', 'ClickRetry_Invalid');
    79 89   navigate('/', { replace: false });
    skipped 1 lines
    81 91   onReportClick={() => {
    82 92   trackCustomEvent('HostnamePage', 'ClickReport_Invalid');
    83 93   }}
     94 + */
    84 95   />
    85 96   );
    86 97   }
    skipped 15 lines
    102 113   <SearchResults
    103 114   isLoading={isLoading}
    104 115   isPending={isPending}
    105  - host={address ?? ''}
    106  - scanOutput={}
     116 + searchQuery={address ?? ''}
     117 + packages={packagesFiltered}
     118 + packagesStats={packagesStats}
     119 + vulnerabilities={vulnerabilities ?? {}}
     120 + keywordsList={keywordsList}
     121 + status={status}
    107 122   />
    108 123   </>
    109 124   );
    skipped 2 lines
  • ■ ■ ■ ■ ■
    packages/web/src/services/apiClient.ts
    skipped 26 lines
    27 27   },
    28 28  });
    29 29   
    30  -export type RequestWebPageScanOutput = InferMutationOutput<'requestWebPageScan'>;
     30 +export type RequestWebPageRescanOutput = InferMutationOutput<'requestWebPageRescan'>;
     31 +export type GetWebPageScanOutput = InferQueryOutput<'getWebPageScan'>;
    31 32  export type { ClientApi };
    32 33  export type ApiClient = typeof client;
    33 34   
  • ■ ■ ■ ■
    packages/web/src/store/index.ts
    skipped 16 lines
    17 17  export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
    18 18   
    19 19  export { parseWebsite, resetError, defaultSelector as homeDefaultSelector } from './slices/home';
    20  -export { applyFilters, getWebsite } from './slices/websiteResults';
     20 +export { applyFilters, getScanResults } from './slices/websiteResults';
    21 21  export { selectors as websiteResultsSelectors } from './selectors/websiteResults';
    22 22   
  • ■ ■ ■ ■ ■ ■
    packages/web/src/store/selectors/websiteResults.ts
    1 1  import semver from 'semver';
    2 2  import memoize from 'lodash.memoize';
    3 3  import { createSelector } from '@reduxjs/toolkit';
    4  -import { GithubAdvisorySeverity } from '@gradejs-public/shared';
     4 +//import { GithubAdvisorySeverity } from '@gradejs-public/shared';
    5 5  import { RootState } from '../';
    6  -import { FiltersState } from '../../components/layouts/Filters/Filters';
    7  -import { SeverityWeightMap } from '../../components/ui/Vulnerability/Vulnerability';
    8  -import type { ClientApi } from '../../services/apiClient';
     6 +//import { FiltersState } from '../../components/layouts/Filters/Filters';
     7 +//import { SeverityWeightMap } from '../../components/ui/Vulnerability/Vulnerability';
     8 +import type { ClientApi, GetWebPageScanOutput } from '../../services/apiClient';
     9 + 
     10 +export type IdentifiedPackage = ClientApi.ScanResultPackageResponse & {
     11 + approximateByteSize?: number;
     12 + outdated?: boolean;
     13 + vulnerable?: boolean;
     14 + duplicate?: boolean;
     15 +};
    9 16   
    10 17  const getFlags = (state: RootState) => ({
    11 18   isLoading: state.webpageResults.isLoading,
    12 19   isFailed: state.webpageResults.isFailed,
    13 20  });
     21 + 
    14 22  const getScanStatus = (state: RootState) => state.webpageResults.detectionResult?.status;
     23 +export type ScanStatus = ReturnType<typeof getScanStatus>;
     24 + 
     25 +const getPackagesMemoized = memoize((result: GetWebPageScanOutput['scanResult']) => {
     26 + const packages: IdentifiedPackage[] = result?.identifiedPackages ?? [];
     27 + 
     28 + for (const pkg of packages) {
     29 + pkg.approximateByteSize = pkg.moduleIds.reduce((acc: number, id) => {
     30 + const size: number = result?.identifiedModuleMap?.[id]?.approximateByteSize ?? 0;
     31 + return acc + size;
     32 + }, 0);
     33 + pkg.duplicate = false; // TODO
     34 + pkg.outdated = !!(
     35 + pkg.registryMetadata &&
     36 + !pkg.versionSet.some(
     37 + (ver) => pkg.registryMetadata && semver.eq(pkg.registryMetadata.latestVersion, ver)
     38 + )
     39 + );
     40 + pkg.vulnerable = (result?.vulnerabilities[pkg.name]?.length ?? 0) > 0;
     41 + }
     42 + return packages;
     43 +});
     44 + 
    15 45  const getPackages = (state: RootState) =>
    16  - state.webpageResults.detectionResult?.scanResult?.packages;
     46 + getPackagesMemoized(state.webpageResults.detectionResult?.scanResult);
     47 + 
    17 48  const getVulnerabilities = (state: RootState) =>
    18 49   state.webpageResults.detectionResult?.scanResult?.vulnerabilities;
     50 + 
    19 51  const getSorting = (state: RootState) => state.webpageResults.filters.sort;
     52 + 
    20 53  const getFilter = (state: RootState) => state.webpageResults.filters.filter;
    21  -const getPackageNameFilter = (state: RootState) => state.webpageResults.filters.filterPackageName;
    22 54   
    23  -const compareByPopularity = (
    24  - left: ClientApi.ScanResultPackageResponse,
    25  - right: ClientApi.ScanResultPackageResponse
    26  -) =>
     55 +const getKeywords = (state: RootState) => [
     56 + ...new Set(
     57 + state.webpageResults.detectionResult?.scanResult?.identifiedPackages.reduce((acc, pkg) => {
     58 + return acc.concat(pkg.registryMetadata?.keywords ?? []);
     59 + }, [] as string[])
     60 + ),
     61 +];
     62 + 
     63 +/*
     64 +const compareByPopularity = (left: IdentifiedPackage, right: IdentifiedPackage) =>
    27 65   (right.registryMetadata?.monthlyDownloads ?? 0) - (left.registryMetadata?.monthlyDownloads ?? 0);
    28 66   
    29 67  const pickHighestSeverity = memoize(
    skipped 13 lines
    43 81  const sortingModes: Record<
    44 82   FiltersState['sort'],
    45 83   (
    46  - packages: ClientApi.ScanResultPackageResponse[],
     84 + packages: IdentifiedPackage[],
    47 85   vulnerabilities: Record<string, ClientApi.PackageVulnerabilityResponse[]>
    48 86   ) => ClientApi.ScanResultPackageResponse[]
    49 87  > = {
    skipped 22 lines
    72 110   
    73 111  const filterModes: Record<
    74 112   FiltersState['filter'],
    75  - (
    76  - packages: ClientApi.ScanResultPackageResponse[],
    77  - vulnerabilities: Record<string, ClientApi.PackageVulnerabilityResponse[]>,
    78  - packageName?: string
    79  - ) => ClientApi.ScanResultPackageResponse[]
     113 + (packages: IdentifiedPackage[], packageName?: string) => IdentifiedPackage[]
    80 114  > = {
    81  - name: (packages, vulnerabilities, packageName) => {
    82  - if (!packageName) {
    83  - return packages;
    84  - }
    85  - return packages.filter((pkg) => pkg.name.includes(packageName));
    86  - },
    87  - outdated: (packages) =>
    88  - packages.filter(
    89  - (pkg) =>
    90  - pkg.registryMetadata && semver.gtr(pkg.registryMetadata.latestVersion, pkg.versionRange)
    91  - ),
    92  - vulnerable: (packages, vulnerabilities) => packages.filter((pkg) => !!vulnerabilities[pkg.name]),
     115 + outdated: (packages) => packages.filter((pkg) => !!pkg.outdated),
     116 + vulnerable: (packages) => packages.filter((pkg) => !!pkg.vulnerable),
    93 117   all: (packages) => packages,
    94  -};
     118 +};*/
    95 119   
    96 120  export const selectors = {
    97  - default: createSelector([getScanStatus, getVulnerabilities], (scanStatus, vulnerabilities) => ({
    98  - status: scanStatus,
    99  - vulnerabilities,
    100  - })),
     121 + default: createSelector(
     122 + [getScanStatus, getVulnerabilities, getKeywords],
     123 + (status, vulnerabilities, keywordsList) => ({
     124 + status,
     125 + vulnerabilities,
     126 + keywordsList,
     127 + })
     128 + ),
    101 129   stateFlags: createSelector(
    102 130   [getScanStatus, getPackages, getFlags],
    103 131   (scanStatus, packages, flags) => ({
    skipped 3 lines
    107 135   isProtected: scanStatus === 'protected',
    108 136   })
    109 137   ),
    110  - packagesStats: createSelector(
    111  - [getPackages, getVulnerabilities],
    112  - (packages = [], vulnerabilities = {}) => ({
    113  - total: packages.length,
    114  - vulnerable: packages.filter((pkg) => (vulnerabilities[pkg.name]?.length ?? 0) > 0).length,
    115  - outdated: packages.filter(
    116  - (pkg) =>
    117  - pkg.registryMetadata && semver.gtr(pkg.registryMetadata.latestVersion, pkg.versionRange)
    118  - ).length,
    119  - })
    120  - ),
     138 + packagesStats: createSelector([getPackages], (packages = []) => ({
     139 + total: packages.length,
     140 + vulnerable: packages.filter((pkg) => !!pkg.vulnerable).length,
     141 + outdated: packages.filter((pkg) => !!pkg.outdated).length,
     142 + })),
    121 143   packagesSortedAndFiltered: createSelector(
    122  - [getPackages, getVulnerabilities, getSorting, getFilter, getPackageNameFilter],
    123  - (packages, vulnerabilities, sorting, filter, packageNameFilter) =>
    124  - packages &&
     144 + [getPackages, getVulnerabilities, getSorting, getFilter],
     145 + (packages /*, vulnerabilities, sorting, filter*/) => packages /* &&
    125 146   vulnerabilities &&
    126  - filterModes[filter](
    127  - sortingModes[sorting](packages, vulnerabilities),
    128  - vulnerabilities,
    129  - packageNameFilter
    130  - )
     147 + filterModes[filter](sortingModes[sorting](packages, vulnerabilities))*/
    131 148   ),
    132 149  };
    133 150   
  • ■ ■ ■ ■ ■ ■
    packages/web/src/store/slices/websiteResults.ts
    1  -import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
    2  -import { DefaultFiltersAndSorters, FiltersState } from '../../components/layouts/Filters/Filters';
    3  -import { client, RequestWebPageScanOutput } from '../../services/apiClient';
     1 +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
     2 +import { client, GetWebPageScanOutput } from '../../services/apiClient';
    4 3  import { trackCustomEvent } from '../../services/analytics';
    5 4   
    6 5  type WebsiteResultsState = {
    7 6   filters: typeof DefaultFiltersAndSorters;
    8 7   isFailed: boolean;
    9 8   isLoading: boolean;
    10  - detectionResult?: RequestWebPageScanOutput;
     9 + detectionResult?: GetWebPageScanOutput;
    11 10  };
    12 11   
    13 12  const initialState: WebsiteResultsState = {
    skipped 8 lines
    22 21   setTimeout(r, ms);
    23 22   });
    24 23   
    25  -const isScanPending = (result: DetectionResult) => result && result.status === 'pending';
     24 +const isScanPending = (result: GetWebPageScanOutput) => result.status === 'pending';
    26 25   
    27  -const getWebsite = createAsyncThunk(
    28  - 'websiteResults/getWebsite',
    29  - async ({ hostname, useRetry = true }: { hostname: string; useRetry?: boolean }) => {
    30  - if (!hostname.startsWith('http://') && !hostname.startsWith('https://')) {
    31  - hostname = `https://${hostname}`;
     26 +const getScanResults = createAsyncThunk(
     27 + 'scanResults/getScanResults',
     28 + async ({ address, useRetry = true }: { address: string; useRetry?: boolean }) => {
     29 + if (!address.startsWith('http://') && !address.startsWith('https://')) {
     30 + address = `https://${address}`;
    32 31   }
    33 32   
    34 33   const loadStartTime = Date.now();
    35  - let results = await client.mutation('requestWebPageScan', hostname);
     34 + let results = await client.query('getWebPageScan', address);
    36 35   if (useRetry) {
    37 36   while (isScanPending(results)) {
    38 37   await sleep(5000);
    39  - results = await client.mutation('requestWebPageScan', hostname);
     38 + results = await client.query('getWebPageScan', address);
    40 39   }
    41 40   }
    42 41   // TODO: move to tracking middleware?
    skipped 17 lines
    60 59   },
    61 60   extraReducers(builder) {
    62 61   builder
    63  - .addCase(getWebsite.pending, (state) => {
     62 + .addCase(getScanResults.pending, (state) => {
    64 63   state.isLoading = true;
    65 64   state.isFailed = false;
    66 65   state.detectionResult = undefined;
    67 66   })
    68  - .addCase(getWebsite.fulfilled, (state, action) => {
     67 + .addCase(getScanResults.fulfilled, (state, action) => {
    69 68   state.isLoading = false;
    70 69   state.detectionResult = action.payload;
    71 70   })
    72  - .addCase(getWebsite.rejected, (state) => {
     71 + .addCase(getScanResults.rejected, (state) => {
    73 72   state.isFailed = true;
    74 73   state.isLoading = false;
    75 74   });
    76 75   },
    77 76  });
    78 77   
    79  -export type DetectionResult = RequestWebPageScanOutput | undefined;
     78 +export type DetectionResult = GetWebPageScanOutput;
    80 79  export const { resetFilters, applyFilters } = websiteResults.actions;
    81  -export { getWebsite };
     80 +export { getScanResults };
    82 81  export const websiteResultsReducer = websiteResults.reducer;
    83 82   
Please wait...
Page is in error, reload to recover