Projects STRLCPY gradejs Commits ef59b379
🤬
Revision indexing in progress... (symbol navigation in revisions will be accurate after indexed)
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/layouts/Error/Error.tsx
    skipped 39 lines
    40 40   </div>
    41 41   </section>
    42 42   
    43  - {/* TODO: Trying to fit separate domain entities within a single component seems like burden.
    44  - Feels like these <CardList/>'s should be separate components. */}
    45 43   <CardGroups>
    46 44   <CardGroup title='But we have'>
    47 45   <PackagesBySourceCardList cards={packagesBySourceListData} />
    skipped 13 lines
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/pages/Home.tsx
    1  -import React, { useCallback, useEffect } from 'react';
     1 +import React, { useCallback, useEffect, useState } from 'react';
    2 2  import { Error, Home } from 'components/layouts';
    3  -import { trackCustomEvent } from '../../services/analytics';
    4  -import { useAppDispatch, parseWebsite, useAppSelector, homeDefaultSelector } from '../../store';
    5 3  import { useNavigate } from 'react-router-dom';
     4 +import { useScanResult } from '../../store/hooks/useScanResult';
    6 5   
    7 6  export function HomePage() {
    8 7   const navigate = useNavigate();
    9  - const dispatch = useAppDispatch();
    10  - const state = useAppSelector(homeDefaultSelector);
     8 + const [requestedScanUrl, setRequestedScanUrl] = useState<string | undefined>(undefined);
     9 + const { normalizedUrl, scanResult } = useScanResult(requestedScanUrl);
    11 10   
    12  - // TODO: properly handle history/routing
    13 11   useEffect(() => {
    14  - if (!state.isLoading && !state.isFailed && state.hostname) {
    15  - navigate(`/w/${state.hostname}`, { replace: true });
     12 + if (scanResult && normalizedUrl && !scanResult.isLoading) {
     13 + navigate(`/w/${normalizedUrl}`);
    16 14   }
    17  - });
     15 + }, [scanResult, normalizedUrl]);
    18 16   
    19  - const handleDetectStart = useCallback(async (address: string) => {
    20  - trackCustomEvent('HomePage', 'WebsiteSubmitted');
    21  - // TODO: error state of input field, e.g. when empty
    22  - if (!address.startsWith('http://') && !address.startsWith('https://')) {
    23  - address = 'https://' + address;
    24  - }
    25  - await dispatch(parseWebsite(address));
     17 + const handleScanRequest = useCallback(async (address: string) => {
     18 + setRequestedScanUrl(address);
    26 19   }, []);
    27 20   
    28  - if (state.isFailed) {
    29  - return <Error host={state.hostname} />;
     21 + if (normalizedUrl && scanResult?.error) {
     22 + return <Error host={normalizedUrl} />;
    30 23   }
    31 24   
    32  - return <Home onSubmit={handleDetectStart} loading={state.isLoading} />;
     25 + return <Home onSubmit={handleScanRequest} loading={scanResult?.isLoading} />;
    33 26  }
    34 27   
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/pages/WebsiteResults.tsx
    1  -import React, { useEffect } from 'react';
     1 +import React, { useMemo } from 'react';
    2 2  import { Helmet } from 'react-helmet';
    3  -import { useParams, useNavigate } from 'react-router-dom';
     3 +import { useParams } from 'react-router-dom';
    4 4  import { Error as ErrorLayout, Website } from 'components/layouts';
    5 5  import { trackCustomEvent } from '../../services/analytics';
    6  -import {
    7  - useAppDispatch,
    8  - useAppSelector,
    9  - applyFilters,
    10  - getWebsite,
    11  - websiteResultsSelectors as selectors,
    12  -} from '../../store';
     6 +import { useAppDispatch, useAppSelector, websiteResultsSelectors as selectors } from '../../store';
    13 7  import { FiltersState } from '../layouts/Filters/Filters';
     8 +import { useScanResult } from '../../store/hooks/useScanResult';
     9 +import { applyScanFilters } from '../../store/slices/scans';
    14 10   
    15 11  export function WebsiteResultsPage() {
    16 12   const { hostname } = useParams();
    17  - const navigate = useNavigate();
    18 13   const dispatch = useAppDispatch();
    19  - const { vulnerabilities } = useAppSelector(selectors.default);
    20  - const packagesFiltered = useAppSelector(selectors.packagesSortedAndFiltered);
    21  - const packagesStats = useAppSelector(selectors.packagesStats);
    22  - const { isProtected, isPending, isLoading, isFailed, isInvalid } = useAppSelector(
    23  - selectors.stateFlags
     14 + 
     15 + const { normalizedUrl, scanResult } = useScanResult(hostname, true);
     16 + 
     17 + const setFilters = useMemo(() => {
     18 + if (!normalizedUrl) {
     19 + return () => {};
     20 + }
     21 + 
     22 + return (filters: FiltersState) =>
     23 + dispatch(applyScanFilters({ scanUrl: normalizedUrl, newFilters: filters }));
     24 + }, [normalizedUrl]);
     25 + 
     26 + const packagesFiltered = useAppSelector((state) =>
     27 + selectors.packagesSortedAndFiltered(state, normalizedUrl)
    24 28   );
    25  - const setFilters = (filters: FiltersState) => dispatch(applyFilters(filters));
    26 29   
    27  - // TODO: discuss. Looks ugly
    28  - // Fetch data for SSR if host is already processed
    29  - if (__isServer__ && hostname) {
    30  - dispatch(getWebsite({ hostname, useRetry: false }));
    31  - }
     30 + const packagesStats = useAppSelector((state) => selectors.packagesStats(state, normalizedUrl));
    32 31   
    33  - useEffect(() => {
    34  - if (hostname && isPending && !isFailed) {
    35  - const promise = dispatch(getWebsite({ hostname }));
    36  - return function cleanup() {
    37  - promise.abort();
    38  - };
    39  - }
    40  - return () => {};
    41  - }, [hostname, isPending, isFailed]);
     32 + const { isProtected, isPending, isLoading, isFailed, isInvalid } = useAppSelector((state) =>
     33 + selectors.scanState(state, normalizedUrl)
     34 + );
    42 35   
    43  - // TODO: properly handle history/routing
    44  - useEffect(() => {
    45  - if (!hostname || isFailed) {
    46  - navigate('/', { replace: true });
    47  - }
    48  - }, [hostname]);
     36 + if (isFailed) {
     37 + return (
     38 + <ErrorLayout
     39 + message='An unexpected error occurred. Try visiting us later.'
     40 + action='Would you like to try another URL or report an issue?'
     41 + actionTitle='Try another URL'
     42 + host={normalizedUrl ?? ''}
     43 + />
     44 + );
     45 + }
    49 46   
    50 47   if (isProtected) {
    51 48   // TODO: move to tracking middleware?
    skipped 3 lines
    55 52   message='The entered website appears to be protected by a third-party service, such as DDoS prevention, password protection or geolocation restrictions.'
    56 53   action='Would you like to try another URL or report an issue?'
    57 54   actionTitle='Try another URL'
    58  - host={hostname ?? ''}
     55 + host={normalizedUrl ?? ''}
    59 56   />
    60 57   );
    61 58   }
    skipped 6 lines
    68 65   message='It looks like the entered website is not built with Webpack.'
    69 66   action='Would you like to try another URL or report an issue?'
    70 67   actionTitle='Try another URL'
    71  - host={hostname ?? ''}
     68 + host={normalizedUrl ?? ''}
    72 69   />
    73 70   );
    74 71   }
    skipped 16 lines
    91 88   isLoading={isLoading}
    92 89   isPending={isPending}
    93 90   packages={packagesFiltered ?? []}
    94  - host={hostname ?? ''}
    95  - vulnerabilities={vulnerabilities ?? {}}
     91 + host={normalizedUrl ?? ''}
     92 + vulnerabilities={scanResult?.scan?.scanResult?.vulnerabilities ?? {}}
    96 93   onFiltersApply={setFilters}
    97 94   />
    98 95   </>
    skipped 3 lines
  • ■ ■ ■ ■ ■ ■
    packages/web/src/store/hooks/useScanResult.ts
     1 +import { useCallback, useEffect, useMemo } from 'react';
     2 +import { useAppDispatch, useAppSelector } from '../index';
     3 +import { requestWebPageScan } from '../slices/scans';
     4 +import { makeSelectScanResultByUrl } from '../selectors/websiteResults';
     5 + 
     6 +export const useScanResult = (scanUrl: string | undefined, pollWhilePending = false) => {
     7 + const { normalizedUrl, fullUrl } = useMemo(() => {
     8 + if (!scanUrl) {
     9 + return {};
     10 + }
     11 + 
     12 + try {
     13 + const prefixedScanUrl =
     14 + !scanUrl.startsWith('http://') && !scanUrl.startsWith('https://')
     15 + ? `https://${scanUrl}`
     16 + : scanUrl;
     17 + 
     18 + const parsedUrl = new URL(prefixedScanUrl);
     19 + return {
     20 + normalizedUrl: `${parsedUrl.hostname}${parsedUrl.pathname}`,
     21 + fullUrl: prefixedScanUrl,
     22 + };
     23 + } catch (_) {
     24 + return {};
     25 + }
     26 + }, [scanUrl]);
     27 + 
     28 + const dispatch = useAppDispatch();
     29 + 
     30 + const scanResultSelector = useMemo(makeSelectScanResultByUrl, []);
     31 + const scanResult = useAppSelector((state) => scanResultSelector(state, normalizedUrl));
     32 + 
     33 + const requestScan = useCallback(async (requestedScanUrl) => {
     34 + return dispatch(requestWebPageScan(requestedScanUrl));
     35 + }, []);
     36 + 
     37 + // Initial request if entity wasn't loaded
     38 + useEffect(() => {
     39 + if (fullUrl && !scanResult?.scan && !scanResult?.isLoading && !scanResult?.error) {
     40 + requestScan(fullUrl);
     41 + }
     42 + }, [fullUrl, scanResult]);
     43 + 
     44 + // Poll while scan is pending
     45 + useEffect(() => {
     46 + let timeoutId: number | undefined;
     47 + if (
     48 + pollWhilePending &&
     49 + scanResult?.scan?.status === 'pending' &&
     50 + !scanResult?.isLoading &&
     51 + !scanResult?.error
     52 + ) {
     53 + timeoutId = window.setTimeout(() => {
     54 + requestScan(fullUrl);
     55 + }, 1000);
     56 + }
     57 + 
     58 + return () => clearTimeout(timeoutId);
     59 + }, [pollWhilePending, fullUrl, scanResult]);
     60 + 
     61 + return { normalizedUrl, scanResult, requestScan };
     62 +};
     63 + 
  • ■ ■ ■ ■ ■ ■
    packages/web/src/store/index.ts
    1 1  import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
    2 2  import { configureStore } from '@reduxjs/toolkit';
    3  -import { homeReducer } from './slices/home';
    4  -import { websiteResultsReducer } from './slices/websiteResults';
     3 +import { scansReducer } from './slices/scans';
    5 4   
    6 5  export const store = configureStore({
    7 6   reducer: {
    8  - home: homeReducer,
    9  - webpageResults: websiteResultsReducer,
     7 + scans: scansReducer,
    10 8   },
    11 9   preloadedState: {},
    12 10  });
    skipped 3 lines
    16 14  export const useAppDispatch = () => useDispatch<typeof store.dispatch>();
    17 15  export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
    18 16   
    19  -export { parseWebsite, resetError, defaultSelector as homeDefaultSelector } from './slices/home';
    20  -export { applyFilters, getWebsite } from './slices/websiteResults';
    21 17  export { selectors as websiteResultsSelectors } from './selectors/websiteResults';
    22 18   
  • ■ ■ ■ ■ ■ ■
    packages/web/src/store/selectors/websiteResults.ts
    skipped 6 lines
    7 7  import { SeverityWeightMap } from '../../components/ui/Vulnerability/Vulnerability';
    8 8  import type { ClientApi } from '../../services/apiClient';
    9 9   
    10  -const getFlags = (state: RootState) => ({
    11  - isLoading: state.webpageResults.isLoading,
    12  - isFailed: state.webpageResults.isFailed,
    13  -});
    14  -const getScanStatus = (state: RootState) => state.webpageResults.detectionResult?.status;
    15  -const getPackages = (state: RootState) =>
    16  - state.webpageResults.detectionResult?.scanResult?.packages;
    17  -const getVulnerabilities = (state: RootState) =>
    18  - state.webpageResults.detectionResult?.scanResult?.vulnerabilities;
    19  -const getSorting = (state: RootState) => state.webpageResults.filters.sort;
    20  -const getFilter = (state: RootState) => state.webpageResults.filters.filter;
    21  -const getPackageNameFilter = (state: RootState) => state.webpageResults.filters.filterPackageName;
     10 +const makeSelectScanResultByUrl = () =>
     11 + createSelector(
     12 + [(state: RootState) => state.scans, (state: RootState, url: string | undefined) => url],
     13 + (scans, url) => (url ? scans[url] : undefined)
     14 + );
    22 15   
    23 16  const compareByPopularity = (
    24 17   left: ClientApi.ScanResultPackageResponse,
    skipped 69 lines
    94 87  };
    95 88   
    96 89  export const selectors = {
    97  - default: createSelector([getScanStatus, getVulnerabilities], (scanStatus, vulnerabilities) => ({
    98  - status: scanStatus,
    99  - vulnerabilities,
     90 + scanState: createSelector([makeSelectScanResultByUrl()], (scanResult) => ({
     91 + isLoading: scanResult?.isLoading ?? true,
     92 + isFailed: !!scanResult?.error,
     93 + isPending: scanResult?.scan?.status === 'pending',
     94 + isProtected: scanResult?.scan?.status === 'protected',
     95 + isInvalid: scanResult?.scan?.scanResult?.identifiedPackages.length === 0,
    100 96   })),
    101  - stateFlags: createSelector(
    102  - [getScanStatus, getPackages, getFlags],
    103  - (scanStatus, packages, flags) => ({
    104  - ...flags,
    105  - isInvalid: packages && packages.length === 0,
    106  - isPending: !scanStatus || scanStatus === 'pending',
    107  - isProtected: scanStatus === 'protected',
    108  - })
    109  - ),
    110  - packagesStats: createSelector(
    111  - [getPackages, getVulnerabilities],
    112  - (packages = [], vulnerabilities = {}) => ({
     97 + packagesStats: createSelector([makeSelectScanResultByUrl()], (scanResult) => {
     98 + const packages = scanResult?.scan?.scanResult?.identifiedPackages ?? [];
     99 + const vulnerabilities = scanResult?.scan?.scanResult?.vulnerabilities ?? {};
     100 + 
     101 + return {
    113 102   total: packages.length,
    114 103   vulnerable: packages.filter((pkg) => (vulnerabilities[pkg.name]?.length ?? 0) > 0).length,
    115 104   outdated: packages.filter(
    116 105   (pkg) =>
    117 106   pkg.registryMetadata && semver.gtr(pkg.registryMetadata.latestVersion, pkg.versionRange)
    118 107   ).length,
    119  - })
    120  - ),
    121  - packagesSortedAndFiltered: createSelector(
    122  - [getPackages, getVulnerabilities, getSorting, getFilter, getPackageNameFilter],
    123  - (packages, vulnerabilities, sorting, filter, packageNameFilter) =>
    124  - packages &&
    125  - vulnerabilities &&
    126  - filterModes[filter](
    127  - sortingModes[sorting](packages, vulnerabilities),
    128  - vulnerabilities,
    129  - packageNameFilter
    130  - )
    131  - ),
     108 + };
     109 + }),
     110 + packagesSortedAndFiltered: createSelector([makeSelectScanResultByUrl()], (scanResult) => {
     111 + const packages = scanResult?.scan?.scanResult?.identifiedPackages ?? [];
     112 + const vulnerabilities = scanResult?.scan?.scanResult?.vulnerabilities ?? {};
     113 + 
     114 + const filters = scanResult?.filters;
     115 + 
     116 + if (!filters) {
     117 + return [];
     118 + }
     119 + 
     120 + return filterModes[filters.filter](
     121 + sortingModes[filters.sort](packages, vulnerabilities),
     122 + vulnerabilities,
     123 + filters.filterPackageName
     124 + );
     125 + }),
    132 126  };
    133 127   
     128 +export { makeSelectScanResultByUrl };
     129 + 
  • ■ ■ ■ ■ ■ ■
    packages/web/src/store/slices/home.ts
    1  -import { createSlice, createAsyncThunk, SerializedError } from '@reduxjs/toolkit';
    2  -import { client } from '../../services/apiClient';
    3  -import { RootState } from '../';
    4  - 
    5  -const parseWebsite = createAsyncThunk('home/submitWebsite', async (url: string) => {
    6  - if (!url.startsWith('http://') && !url.startsWith('https://')) {
    7  - url = `https://${url}`;
    8  - }
    9  - 
    10  - await client.mutation('requestWebPageScan', url);
    11  -});
    12  - 
    13  -const home = createSlice({
    14  - name: 'home',
    15  - initialState: {
    16  - isLoading: false,
    17  - isFailed: false,
    18  - hostname: '',
    19  - loadError: null as SerializedError | null,
    20  - },
    21  - reducers: {
    22  - resetError(state) {
    23  - state.hostname = '';
    24  - state.isFailed = false;
    25  - },
    26  - },
    27  - extraReducers(builder) {
    28  - builder
    29  - .addCase(parseWebsite.pending, (state, action) => {
    30  - state.hostname = new URL(action.meta.arg).hostname;
    31  - state.isLoading = true;
    32  - state.isFailed = false;
    33  - state.loadError = null;
    34  - })
    35  - .addCase(parseWebsite.fulfilled, (state) => {
    36  - state.isLoading = false;
    37  - })
    38  - .addCase(parseWebsite.rejected, (state, action) => {
    39  - state.isFailed = true;
    40  - state.isLoading = false;
    41  - state.loadError = action.error;
    42  - });
    43  - },
    44  -});
    45  - 
    46  -export const { resetError } = home.actions;
    47  -export const defaultSelector = (state: RootState) => state.home;
    48  -export const homeReducer = home.reducer;
    49  -export { parseWebsite };
    50  - 
  • ■ ■ ■ ■ ■ ■
    packages/web/src/store/slices/scans.ts
     1 +import { createAsyncThunk, createSlice, PayloadAction, SerializedError } from '@reduxjs/toolkit';
     2 +import { client, RequestWebPageScanOutput } from '../../services/apiClient';
     3 +import { DefaultFiltersAndSorters, FiltersState } from '../../components/layouts/Filters/Filters';
     4 + 
     5 +export type ScanResultsState = {
     6 + isLoading: boolean;
     7 + error?: SerializedError;
     8 + scan?: RequestWebPageScanOutput;
     9 + filters: FiltersState;
     10 +};
     11 + 
     12 +export type ScanResultsSlice = Record<string, ScanResultsState>;
     13 + 
     14 +export type ResetFiltersPayload = { scanUrl: string };
     15 +export type ApplyFiltersPayload = { scanUrl: string; newFilters: FiltersState };
     16 + 
     17 +const requestWebPageScan = createAsyncThunk(
     18 + 'scans/requestWebPageScan',
     19 + async ({ normalizedUrl }: { normalizedUrl: string }) => {
     20 + return client.mutation('requestWebPageScan', normalizedUrl);
     21 + }
     22 +);
     23 + 
     24 +const scans = createSlice({
     25 + name: 'scans',
     26 + initialState: {} as ScanResultsSlice,
     27 + reducers: {
     28 + resetScanFilters(state, action: PayloadAction<ResetFiltersPayload>) {
     29 + const { scanUrl } = action.payload;
     30 + 
     31 + if (state[scanUrl]) {
     32 + state[scanUrl].filters = { ...DefaultFiltersAndSorters };
     33 + }
     34 + },
     35 + applyScanFilters(state, action: PayloadAction<ApplyFiltersPayload>) {
     36 + const { scanUrl, newFilters } = action.payload;
     37 + 
     38 + if (state[scanUrl]) {
     39 + state[scanUrl].filters = newFilters;
     40 + }
     41 + },
     42 + },
     43 + extraReducers: (builder) => {
     44 + builder
     45 + .addCase(requestWebPageScan.pending, (state, action) => {
     46 + const { normalizedUrl } = action.meta.arg;
     47 + 
     48 + const previousScanState = state[normalizedUrl] ?? null;
     49 + 
     50 + state[normalizedUrl] = {
     51 + ...previousScanState,
     52 + isLoading: true,
     53 + error: undefined,
     54 + scan: undefined,
     55 + };
     56 + })
     57 + .addCase(requestWebPageScan.rejected, (state, action) => {
     58 + const { normalizedUrl } = action.meta.arg;
     59 + 
     60 + const previousScanState = state[normalizedUrl] ?? null;
     61 + 
     62 + state[normalizedUrl] = {
     63 + ...previousScanState,
     64 + isLoading: false,
     65 + error: action.error,
     66 + scan: undefined,
     67 + };
     68 + })
     69 + .addCase(requestWebPageScan.fulfilled, (state, action) => {
     70 + const { normalizedUrl } = action.meta.arg;
     71 + 
     72 + const previousScanState = state[normalizedUrl] ?? null;
     73 + 
     74 + state[normalizedUrl] = {
     75 + ...previousScanState,
     76 + isLoading: false,
     77 + error: undefined,
     78 + scan: action.payload,
     79 + };
     80 + });
     81 + },
     82 +});
     83 + 
     84 +export const { resetScanFilters, applyScanFilters } = scans.actions;
     85 +export const scansReducer = scans.reducer;
     86 +export { requestWebPageScan };
     87 + 
  • ■ ■ ■ ■ ■ ■
    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';
    4  -import { trackCustomEvent } from '../../services/analytics';
    5  - 
    6  -type WebsiteResultsState = {
    7  - filters: typeof DefaultFiltersAndSorters;
    8  - isFailed: boolean;
    9  - isLoading: boolean;
    10  - detectionResult?: RequestWebPageScanOutput;
    11  -};
    12  - 
    13  -const initialState: WebsiteResultsState = {
    14  - filters: { ...DefaultFiltersAndSorters },
    15  - isFailed: false,
    16  - isLoading: false,
    17  - detectionResult: undefined,
    18  -};
    19  - 
    20  -const sleep = (ms: number | undefined) =>
    21  - new Promise((r) => {
    22  - setTimeout(r, ms);
    23  - });
    24  - 
    25  -const isScanPending = (result: DetectionResult) => result && result.status === 'pending';
    26  - 
    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}`;
    32  - }
    33  - 
    34  - const loadStartTime = Date.now();
    35  - let results = await client.mutation('requestWebPageScan', hostname);
    36  - if (useRetry) {
    37  - while (isScanPending(results)) {
    38  - await sleep(5000);
    39  - results = await client.mutation('requestWebPageScan', hostname);
    40  - }
    41  - }
    42  - // TODO: move to tracking middleware?
    43  - trackCustomEvent('HostnamePage', 'WebsiteLoaded', {
    44  - value: Date.now() - loadStartTime,
    45  - });
    46  - return results;
    47  - }
    48  -);
    49  - 
    50  -const websiteResults = createSlice({
    51  - name: 'websiteResults',
    52  - initialState,
    53  - reducers: {
    54  - resetFilters(state) {
    55  - state.filters = { ...DefaultFiltersAndSorters };
    56  - },
    57  - applyFilters(state, action: PayloadAction<FiltersState>) {
    58  - state.filters = action.payload;
    59  - },
    60  - },
    61  - extraReducers(builder) {
    62  - builder
    63  - .addCase(getWebsite.pending, (state) => {
    64  - state.isLoading = true;
    65  - state.isFailed = false;
    66  - state.detectionResult = undefined;
    67  - })
    68  - .addCase(getWebsite.fulfilled, (state, action) => {
    69  - state.isLoading = false;
    70  - state.detectionResult = action.payload;
    71  - })
    72  - .addCase(getWebsite.rejected, (state) => {
    73  - state.isFailed = true;
    74  - state.isLoading = false;
    75  - });
    76  - },
    77  -});
    78  - 
    79  -export type DetectionResult = RequestWebPageScanOutput | undefined;
    80  -export const { resetFilters, applyFilters } = websiteResults.actions;
    81  -export { getWebsite };
    82  -export const websiteResultsReducer = websiteResults.reducer;
    83  - 
Please wait...
Page is in error, reload to recover