From af4505eb4b229bc986f071c016c43a5f2e362da0 Mon Sep 17 00:00:00 2001 From: Nate Weller Date: Sat, 22 Mar 2025 12:43:36 -0600 Subject: [PATCH 1/3] Protect Meets Core Protect: Add Go to Cloud and Scan now button to Protect primary header (#40057) Protect: Update Scan and History headers (#40058) Protect: de-emphasize cloud link by using link variant (#40211) Protect: Add ShieldIcon Component (#40402) Protect: Integrate ThreatsDataViews Component (#40076) Components: Add ScanReport (#40419) Protect: Refactor AdminSectionHero (#40516) Protect: Update Scan History extension types (#40548) Protect: Add Home page (#40317) Protect: Integrate ScanReport (#40420) ScanReport: Fix defaultLayout (#40603) Update onboarding popover placement (#40550) Protect Meets Core: Home Page Scan Report Data Adjustments (#40616) Scan Report: Align Status Icon (#40617) Apply max width to hero content (#40618) Protect: Hide Scan Report When No Data (#40619) Protect: Hide Threats Report When No Data (#40620) Protect: Update Threat Icons (#40621) Protect: fix home page stat card spacing (#40623) ScanReport: Disable hiding relevant fields (#40602) Use error variant (#40832) Protect: Restore HistoryAdminSectionHero (#40551) Protect: Separate scan results and history DataViews (#40845) ThreatsDataViews: Improve responsiveness (#40670) ThreatsDataViews: ThreatModal integration (#40202) Protect Meets Core: Fix "0" rendered by conditional render chain (#40882) --- pnpm-lock.yaml | 3 + ...nents-threats-data-views-modal-integration | 4 + .../add-hide-value-prop-to-stat-card | 4 + .../components/changelog/add-shield-icon | 4 + .../changelog/components-add-scan-report | 4 + .../changelog/force-a-release | 4 - ...nents-threats-data-views-modal-integration | 4 + .../add-threat-subtitle-and-icon-utils | 4 + .../scan/changelog/components-add-scan-report | 4 + .../js-packages/scan/src/components/index.js | 8 + .../changelog/add-protect-header-buttons | 4 + .../protect/changelog/add-protect-home | 4 + .../protect/changelog/add-threats-data-views | 5 + .../protect/changelog/refactor-alert-icon | 5 + .../update-protect-add-scan-report-to-home | 4 + .../update-protect-scan-and-history-headers | 4 + projects/plugins/protect/package.json | 1 + .../protect/src/class-jetpack-protect.php | 3 +- .../protect/src/class-scan-history.php | 30 +- projects/plugins/protect/src/js/api.ts | 15 +- .../src/js/components/admin-page/index.jsx | 40 ++- .../components/admin-page/styles.module.scss | 22 ++ .../components/admin-section-hero/index.tsx | 96 ++--- .../stories/index.stories.jsx | 14 +- .../admin-section-hero/styles.module.scss | 42 ++- .../src/js/components/alert-icon/index.jsx | 74 ---- .../alert-icon/stories/index.stories.jsx | 17 - .../components/alert-icon/styles.module.scss | 11 - .../error-admin-section-hero/index.tsx | 31 +- .../styles.module.scss | 6 +- .../js/components/fix-threat-modal/index.jsx | 9 +- .../js/components/free-accordion/index.jsx | 64 ---- .../free-accordion/stories/index.stories.jsx | 120 ------- .../free-accordion/styles.module.scss | 79 ----- .../components/ignore-threat-modal/index.jsx | 40 ++- .../src/js/components/navigation/badge.jsx | 101 ------ .../src/js/components/navigation/group.jsx | 51 --- .../src/js/components/navigation/index.jsx | 73 ---- .../src/js/components/navigation/item.jsx | 85 ----- .../src/js/components/navigation/label.jsx | 24 -- .../components/navigation/styles.module.scss | 142 -------- .../navigation/use-menu-navigation.js | 92 ----- .../js/components/notice/styles.module.scss | 2 +- .../js/components/paid-accordion/index.jsx | 187 ---------- .../stories/broken/index.stories.jsx | 120 ------- .../paid-accordion/styles.module.scss | 202 ----------- .../src/js/components/pricing-table/index.jsx | 4 +- .../components/protect-check-icon/index.tsx | 25 -- .../src/js/components/scan-button/index.jsx | 4 + .../js/components/scan-navigation/index.jsx | 44 --- .../components/seventy-five-layout/index.tsx | 73 ---- .../seventy-five-layout/styles.module.scss | 13 - .../js/components/threat-fix-header/index.jsx | 8 +- .../src/js/components/threats-list/empty.jsx | 140 -------- .../js/components/threats-list/free-list.jsx | 125 ------- .../src/js/components/threats-list/index.jsx | 194 ---------- .../js/components/threats-list/navigation.jsx | 130 ------- .../js/components/threats-list/pagination.jsx | 142 -------- .../js/components/threats-list/paid-list.jsx | 253 -------------- .../threats-list/styles.module.scss | 129 ------- .../threats-list/use-threats-list.js | 158 --------- .../unignore-threat-modal/index.jsx | 19 +- .../src/js/data/scan/use-fixers-mutation.ts | 16 +- .../data/scan/use-ignore-threat-mutation.ts | 62 +++- .../data/scan/use-unignore-threat-mutation.ts | 62 +++- .../src/js/data/use-credentials-query.ts | 5 +- .../protect/src/js/hooks/use-fixers.ts | 8 +- .../plugins/protect/src/js/hooks/use-plan.tsx | 2 +- .../src/js/hooks/use-protect-data/index.ts | 173 --------- projects/plugins/protect/src/js/index.tsx | 14 +- .../firewall/firewall-admin-section-hero.tsx | 40 ++- .../js/routes/firewall/firewall-footer.jsx | 2 - .../js/routes/firewall/firewall-statcards.jsx | 24 +- .../protect/src/js/routes/firewall/index.jsx | 3 +- .../src/js/routes/firewall/styles.module.scss | 61 ++-- .../routes/home/home-admin-section-hero.tsx | 54 +++ .../src/js/routes/home/home-statcards.jsx | 269 ++++++++++++++ .../protect/src/js/routes/home/index.jsx | 59 ++++ .../src/js/routes/home/styles.module.scss | 71 ++++ .../src/js/routes/scan/context-provider.tsx | 125 +++++++ .../history/history-admin-section-hero.tsx | 107 +++--- .../scan/history/history-data-views.tsx | 43 +++ .../src/js/routes/scan/history/index.jsx | 330 ++---------------- .../js/routes/scan/history/status-filters.jsx | 44 --- .../js/routes/scan/history/styles.module.scss | 51 +-- .../protect/src/js/routes/scan/index.jsx | 77 ++-- .../src/js/routes/scan/onboarding-steps.jsx | 61 ++-- .../routes/scan/scan-admin-section-hero.tsx | 212 +++++++---- .../src/js/routes/scan/scan-footer.jsx | 143 -------- .../routes/scan/scan-toggle-group-control.tsx | 75 ++++ .../scan/scanning-admin-section-hero.tsx | 81 +++-- .../src/js/routes/scan/styles.module.scss | 63 +++- .../plugins/protect/src/js/styles.module.scss | 3 +- .../plugins/protect/src/js/types/global.d.ts | 2 +- projects/plugins/protect/webpack.config.js | 18 + 95 files changed, 1627 insertions(+), 4056 deletions(-) create mode 100644 projects/js-packages/components/changelog/add-components-threats-data-views-modal-integration create mode 100644 projects/js-packages/components/changelog/add-hide-value-prop-to-stat-card create mode 100644 projects/js-packages/components/changelog/add-shield-icon create mode 100644 projects/js-packages/components/changelog/components-add-scan-report delete mode 100644 projects/js-packages/publicize-components/changelog/force-a-release create mode 100644 projects/js-packages/scan/changelog/add-components-threats-data-views-modal-integration create mode 100644 projects/js-packages/scan/changelog/add-threat-subtitle-and-icon-utils create mode 100644 projects/js-packages/scan/changelog/components-add-scan-report create mode 100644 projects/plugins/protect/changelog/add-protect-header-buttons create mode 100644 projects/plugins/protect/changelog/add-protect-home create mode 100644 projects/plugins/protect/changelog/add-threats-data-views create mode 100644 projects/plugins/protect/changelog/refactor-alert-icon create mode 100644 projects/plugins/protect/changelog/update-protect-add-scan-report-to-home create mode 100644 projects/plugins/protect/changelog/update-protect-scan-and-history-headers delete mode 100644 projects/plugins/protect/src/js/components/alert-icon/index.jsx delete mode 100644 projects/plugins/protect/src/js/components/alert-icon/stories/index.stories.jsx delete mode 100644 projects/plugins/protect/src/js/components/alert-icon/styles.module.scss delete mode 100644 projects/plugins/protect/src/js/components/free-accordion/index.jsx delete mode 100644 projects/plugins/protect/src/js/components/free-accordion/stories/index.stories.jsx delete mode 100644 projects/plugins/protect/src/js/components/free-accordion/styles.module.scss delete mode 100644 projects/plugins/protect/src/js/components/navigation/badge.jsx delete mode 100644 projects/plugins/protect/src/js/components/navigation/group.jsx delete mode 100644 projects/plugins/protect/src/js/components/navigation/index.jsx delete mode 100644 projects/plugins/protect/src/js/components/navigation/item.jsx delete mode 100644 projects/plugins/protect/src/js/components/navigation/label.jsx delete mode 100644 projects/plugins/protect/src/js/components/navigation/styles.module.scss delete mode 100644 projects/plugins/protect/src/js/components/navigation/use-menu-navigation.js delete mode 100644 projects/plugins/protect/src/js/components/paid-accordion/index.jsx delete mode 100644 projects/plugins/protect/src/js/components/paid-accordion/stories/broken/index.stories.jsx delete mode 100644 projects/plugins/protect/src/js/components/paid-accordion/styles.module.scss delete mode 100644 projects/plugins/protect/src/js/components/protect-check-icon/index.tsx delete mode 100644 projects/plugins/protect/src/js/components/scan-navigation/index.jsx delete mode 100644 projects/plugins/protect/src/js/components/seventy-five-layout/index.tsx delete mode 100644 projects/plugins/protect/src/js/components/seventy-five-layout/styles.module.scss delete mode 100644 projects/plugins/protect/src/js/components/threats-list/empty.jsx delete mode 100644 projects/plugins/protect/src/js/components/threats-list/free-list.jsx delete mode 100644 projects/plugins/protect/src/js/components/threats-list/index.jsx delete mode 100644 projects/plugins/protect/src/js/components/threats-list/navigation.jsx delete mode 100644 projects/plugins/protect/src/js/components/threats-list/pagination.jsx delete mode 100644 projects/plugins/protect/src/js/components/threats-list/paid-list.jsx delete mode 100644 projects/plugins/protect/src/js/components/threats-list/styles.module.scss delete mode 100644 projects/plugins/protect/src/js/components/threats-list/use-threats-list.js delete mode 100644 projects/plugins/protect/src/js/hooks/use-protect-data/index.ts create mode 100644 projects/plugins/protect/src/js/routes/home/home-admin-section-hero.tsx create mode 100644 projects/plugins/protect/src/js/routes/home/home-statcards.jsx create mode 100644 projects/plugins/protect/src/js/routes/home/index.jsx create mode 100644 projects/plugins/protect/src/js/routes/home/styles.module.scss create mode 100644 projects/plugins/protect/src/js/routes/scan/context-provider.tsx create mode 100644 projects/plugins/protect/src/js/routes/scan/history/history-data-views.tsx delete mode 100644 projects/plugins/protect/src/js/routes/scan/history/status-filters.jsx delete mode 100644 projects/plugins/protect/src/js/routes/scan/scan-footer.jsx create mode 100644 projects/plugins/protect/src/js/routes/scan/scan-toggle-group-control.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f27acc560762..499247870e0ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4897,6 +4897,9 @@ importers: specifier: 6.28.1 version: 6.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: + '@automattic/babel-plugin-replace-textdomain': + specifier: workspace:* + version: link:../../js-packages/babel-plugin-replace-textdomain '@automattic/jetpack-webpack-config': specifier: workspace:* version: link:../../js-packages/webpack-config diff --git a/projects/js-packages/components/changelog/add-components-threats-data-views-modal-integration b/projects/js-packages/components/changelog/add-components-threats-data-views-modal-integration new file mode 100644 index 0000000000000..809bb1cd3a788 --- /dev/null +++ b/projects/js-packages/components/changelog/add-components-threats-data-views-modal-integration @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Integrates ThreatModal in ThreatsDataViews diff --git a/projects/js-packages/components/changelog/add-hide-value-prop-to-stat-card b/projects/js-packages/components/changelog/add-hide-value-prop-to-stat-card new file mode 100644 index 0000000000000..0d4002c768dd8 --- /dev/null +++ b/projects/js-packages/components/changelog/add-hide-value-prop-to-stat-card @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Stat Card: add hideValue prop diff --git a/projects/js-packages/components/changelog/add-shield-icon b/projects/js-packages/components/changelog/add-shield-icon new file mode 100644 index 0000000000000..5c6cc27eeb809 --- /dev/null +++ b/projects/js-packages/components/changelog/add-shield-icon @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Add ShieldIcon component diff --git a/projects/js-packages/components/changelog/components-add-scan-report b/projects/js-packages/components/changelog/components-add-scan-report new file mode 100644 index 0000000000000..ba0fbd4cce025 --- /dev/null +++ b/projects/js-packages/components/changelog/components-add-scan-report @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Adds ScanReport component diff --git a/projects/js-packages/publicize-components/changelog/force-a-release b/projects/js-packages/publicize-components/changelog/force-a-release deleted file mode 100644 index d4ad6c7cc3379..0000000000000 --- a/projects/js-packages/publicize-components/changelog/force-a-release +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Update dependencies. diff --git a/projects/js-packages/scan/changelog/add-components-threats-data-views-modal-integration b/projects/js-packages/scan/changelog/add-components-threats-data-views-modal-integration new file mode 100644 index 0000000000000..ccbcd7068d542 --- /dev/null +++ b/projects/js-packages/scan/changelog/add-components-threats-data-views-modal-integration @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Adds utility for retrieving a detailed action description diff --git a/projects/js-packages/scan/changelog/add-threat-subtitle-and-icon-utils b/projects/js-packages/scan/changelog/add-threat-subtitle-and-icon-utils new file mode 100644 index 0000000000000..ad8fa81458278 --- /dev/null +++ b/projects/js-packages/scan/changelog/add-threat-subtitle-and-icon-utils @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Add utilities for generating threat subtitle and icons diff --git a/projects/js-packages/scan/changelog/components-add-scan-report b/projects/js-packages/scan/changelog/components-add-scan-report new file mode 100644 index 0000000000000..eeb9c55de4a28 --- /dev/null +++ b/projects/js-packages/scan/changelog/components-add-scan-report @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Updates/adds scan types diff --git a/projects/js-packages/scan/src/components/index.js b/projects/js-packages/scan/src/components/index.js index cabea8f139574..63080bd6926af 100644 --- a/projects/js-packages/scan/src/components/index.js +++ b/projects/js-packages/scan/src/components/index.js @@ -4,3 +4,11 @@ export { default as ThreatFixerButton } from './threat-fixer-button/index.js'; export * from './threat-modals/index.js'; export { default as ThreatSeverityBadge } from './threat-severity-badge/index.js'; export { default as ThreatsDataViews } from './threats-data-views/index.js'; +export { + CURRENT_TABLE_FIELDS, + HISTORIC_TABLE_FIELDS, + LIST_FIELDS, + THREAT_FIELD_AUTO_FIX, + THREAT_FIELD_DESCRIPTION, + THREAT_FIELD_EXTENSION, +} from './threats-data-views/constants.js'; diff --git a/projects/plugins/protect/changelog/add-protect-header-buttons b/projects/plugins/protect/changelog/add-protect-header-buttons new file mode 100644 index 0000000000000..24c40f542d7ee --- /dev/null +++ b/projects/plugins/protect/changelog/add-protect-header-buttons @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Adds Go to Cloud and Scan now buttons to the primary header diff --git a/projects/plugins/protect/changelog/add-protect-home b/projects/plugins/protect/changelog/add-protect-home new file mode 100644 index 0000000000000..0bcfedb6fe8ac --- /dev/null +++ b/projects/plugins/protect/changelog/add-protect-home @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Adds a Home page and StatCards diff --git a/projects/plugins/protect/changelog/add-threats-data-views b/projects/plugins/protect/changelog/add-threats-data-views new file mode 100644 index 0000000000000..e15bd6a461a71 --- /dev/null +++ b/projects/plugins/protect/changelog/add-threats-data-views @@ -0,0 +1,5 @@ +Significance: minor +Type: changed + +Added DataViews component for viewing scan results. + diff --git a/projects/plugins/protect/changelog/refactor-alert-icon b/projects/plugins/protect/changelog/refactor-alert-icon new file mode 100644 index 0000000000000..46b4c247b1b9f --- /dev/null +++ b/projects/plugins/protect/changelog/refactor-alert-icon @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Refactored icon component code. + + diff --git a/projects/plugins/protect/changelog/update-protect-add-scan-report-to-home b/projects/plugins/protect/changelog/update-protect-add-scan-report-to-home new file mode 100644 index 0000000000000..0478ae51501b8 --- /dev/null +++ b/projects/plugins/protect/changelog/update-protect-add-scan-report-to-home @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Adds ScanReport to HomeRoute diff --git a/projects/plugins/protect/changelog/update-protect-scan-and-history-headers b/projects/plugins/protect/changelog/update-protect-scan-and-history-headers new file mode 100644 index 0000000000000..cd930e395e0ed --- /dev/null +++ b/projects/plugins/protect/changelog/update-protect-scan-and-history-headers @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Updates the structure and content of the Scan and History page headers diff --git a/projects/plugins/protect/package.json b/projects/plugins/protect/package.json index afd758f2c60a0..a6bbc303bd5e2 100644 --- a/projects/plugins/protect/package.json +++ b/projects/plugins/protect/package.json @@ -49,6 +49,7 @@ "react-router-dom": "6.28.1" }, "devDependencies": { + "@automattic/babel-plugin-replace-textdomain": "workspace:*", "@automattic/jetpack-webpack-config": "workspace:*", "@babel/core": "7.26.10", "@babel/preset-env": "7.26.9", diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index 613e7afe1d9ec..2560573eb25b8 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -447,8 +447,9 @@ public static function get_waf_stats() { } return array( - 'blockedRequests' => Plan::has_required_plan() ? Waf_Stats::get_blocked_requests() : false, + 'blockedRequests' => Waf_Stats::get_blocked_requests(), 'automaticRulesLastUpdated' => Waf_Stats::get_automatic_rules_last_updated(), + 'blockedLogins' => (int) get_option( 'jetpack_protect_blocked_attempts', 0 ), ); } } diff --git a/projects/plugins/protect/src/class-scan-history.php b/projects/plugins/protect/src/class-scan-history.php index bd034c375caf9..23019ccd634ad 100644 --- a/projects/plugins/protect/src/class-scan-history.php +++ b/projects/plugins/protect/src/class-scan-history.php @@ -207,43 +207,23 @@ public static function fetch_from_api() { * Normalize API Data * Formats the payload from the Scan API into an instance of History_Model. * - * @phan-suppress PhanDeprecatedProperty -- Maintaining backwards compatibility. - * * @param object $scan_data The data returned by the scan API. * @return History_Model */ private static function normalize_api_data( $scan_data ) { - $history = new History_Model(); - $history->num_threats = 0; - $history->num_core_threats = 0; - $history->num_plugins_threats = 0; - $history->num_themes_threats = 0; - + $history = new History_Model(); $history->last_checked = $scan_data->last_checked; if ( empty( $scan_data->threats ) || ! is_array( $scan_data->threats ) ) { return $history; } - foreach ( $scan_data->threats as $threat ) { - if ( isset( $threat->extension->type ) ) { - if ( 'plugin' === $threat->extension->type ) { - self::handle_extension_threats( $threat, $history, 'plugin' ); - continue; - } - - if ( 'theme' === $threat->extension->type ) { - self::handle_extension_threats( $threat, $history, 'theme' ); - continue; - } - } - - if ( 'Vulnerable.WP.Core' === $threat->signature ) { - self::handle_core_threats( $threat, $history ); - continue; + foreach ( $scan_data->threats as $source_threat ) { + if ( ! empty( $source_threat->extension ) && in_array( $source_threat->extension->type, array( 'plugin', 'theme' ), true ) ) { + $source_threat->extension->type .= 's'; } - self::handle_additional_threats( $threat, $history ); + $history->threats[] = new Threat_Model( $source_threat ); } return $history; diff --git a/projects/plugins/protect/src/js/api.ts b/projects/plugins/protect/src/js/api.ts index e94c19ee4bec5..be8d4a50d13a3 100644 --- a/projects/plugins/protect/src/js/api.ts +++ b/projects/plugins/protect/src/js/api.ts @@ -1,8 +1,7 @@ -import { type FixersStatus, type ScanStatus } from '@automattic/jetpack-scan'; +import { type FixersStatus, type ScanStatus, Threat } from '@automattic/jetpack-scan'; import apiFetch from '@wordpress/api-fetch'; import camelize from 'camelize'; import { WafStatus } from './types/waf'; -import type { ProductData } from './types/products'; const API = { getAccountProtection: () => @@ -79,7 +78,7 @@ const API = { method: 'GET', } ).then( camelize ), - fixThreats: ( threatIds: number[] ): Promise< FixersStatus > => + fixThreats: ( threatIds: Array< Threat[ 'id' ] > ): Promise< FixersStatus > => apiFetch( { path: `jetpack-protect/v1/fix-threats`, method: 'POST', @@ -122,12 +121,10 @@ const API = { } ), getProductData: () => - ( - apiFetch( { - path: '/my-jetpack/v1/site/products?products=scan', - method: 'GET', - } ) as Promise< { [ key: string ]: ProductData } > - ).then( products => camelize( products?.scan ) ), + apiFetch( { + path: '/my-jetpack/v1/site/products/scan', + method: 'GET', + } ).then( camelize ), }; export default API; diff --git a/projects/plugins/protect/src/js/components/admin-page/index.jsx b/projects/plugins/protect/src/js/components/admin-page/index.jsx index ec60cb446c579..d9c5d228196ee 100644 --- a/projects/plugins/protect/src/js/components/admin-page/index.jsx +++ b/projects/plugins/protect/src/js/components/admin-page/index.jsx @@ -1,15 +1,19 @@ import { AdminPage as JetpackAdminPage, + Button, Container, + getRedirectUrl, JetpackProtectLogo, } from '@automattic/jetpack-components'; import { useConnection } from '@automattic/jetpack-connection'; import { __, sprintf } from '@wordpress/i18n'; import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; +import useScanStatusQuery from '../../data/scan/use-scan-status-query'; import useNotices from '../../hooks/use-notices'; -import useProtectData from '../../hooks/use-protect-data'; +import usePlan from '../../hooks/use-plan'; import Notice from '../notice'; +import ScanButton from '../scan-button'; import Tabs, { Tab } from '../tabs'; import styles from './styles.module.scss'; @@ -17,11 +21,9 @@ const AdminPage = ( { children } ) => { const { notice } = useNotices(); const { isRegistered } = useConnection(); const navigate = useNavigate(); - const { - counts: { - current: { threats: numThreats }, - }, - } = useProtectData(); + const { data: status } = useScanStatusQuery(); + const location = useLocation(); + const { hasPlan } = usePlan(); // Redirect to the setup page if the site is not registered. useEffect( () => { @@ -34,23 +36,41 @@ const AdminPage = ( { children } ) => { return null; } + const viewingScanPage = location.pathname.includes( '/scan' ); + + const { siteSuffix, blogID } = window.jetpackProtectInitialState || {}; + const goToCloudUrl = getRedirectUrl( 'jetpack-scan-dash', { site: blogID ?? siteSuffix } ); + return ( } + header={ +
+ + { hasPlan && viewingScanPage && ( +
+ + +
+ ) } +
+ } > { notice && } + - { numThreats > 0 + { status.threats.length > 0 ? sprintf( // translators: %d is the number of threats found. __( 'Scan (%d)', 'jetpack-protect' ), - numThreats + status.threats.length ) : __( 'Scan', 'jetpack-protect' ) } diff --git a/projects/plugins/protect/src/js/components/admin-page/styles.module.scss b/projects/plugins/protect/src/js/components/admin-page/styles.module.scss index e70d2cdb076c7..ecb634ac78b8c 100644 --- a/projects/plugins/protect/src/js/components/admin-page/styles.module.scss +++ b/projects/plugins/protect/src/js/components/admin-page/styles.module.scss @@ -2,6 +2,28 @@ white-space: nowrap; } +.header { + display: flex; + justify-content: space-between; + flex-direction: column; + gap: calc( var( --spacing-base ) * 3 ); // 24px + align-items: center; + width: 100%; + + @media ( min-width: 600px ) { + flex-direction: row; + } + + &__scan_buttons { + display: flex; + gap: calc( var( --spacing-base ) * 3 ); // 24px + + @media ( min-width: 600px ) { + flex-direction: row; + } + } +} + .navigation { margin-top: calc( var( --spacing-base ) * 3 * -1 ); // -24px } diff --git a/projects/plugins/protect/src/js/components/admin-section-hero/index.tsx b/projects/plugins/protect/src/js/components/admin-section-hero/index.tsx index 5ed83bebc8638..29cd3e69b97f4 100644 --- a/projects/plugins/protect/src/js/components/admin-section-hero/index.tsx +++ b/projects/plugins/protect/src/js/components/admin-section-hero/index.tsx @@ -1,66 +1,82 @@ import { AdminSectionHero as JetpackAdminSectionHero, H3, - getIconBySlug, + Container, + Col, } from '@automattic/jetpack-components'; -import SeventyFiveLayout from '../seventy-five-layout'; +import { ShieldIcon } from '@automattic/jetpack-scan'; +import clsx from 'clsx'; import AdminSectionHeroNotices from './admin-section-hero-notices'; import styles from './styles.module.scss'; -interface AdminSectionHeroProps { - main: React.ReactNode; - secondary?: React.ReactNode; - preserveSecondaryOnMobile?: boolean; - spacing?: number; -} - -interface AdminSectionHeroComponent extends React.FC< AdminSectionHeroProps > { - Heading: React.FC< { children: React.ReactNode; showIcon?: boolean } >; - Subheading: React.FC< { children: React.ReactNode } >; -} - -const AdminSectionHero: AdminSectionHeroComponent = ( { - main, - secondary, - preserveSecondaryOnMobile = true, - spacing = 7, -} ) => { +const AdminSectionHero = ( { + children, + ...props +}: React.ComponentProps< typeof JetpackAdminSectionHero > ) => { return ( - + - + + +
{ children }
+ +
); }; -AdminSectionHero.Heading = ( { +AdminSectionHero.Main = ( { children, - showIcon = false, + className, + ...props }: { children: React.ReactNode; - showIcon?: boolean; + className?: string; + [ key: string ]: unknown; } ) => { - const Icon = getIconBySlug( 'protect' ); + return ( +
+ { children } +
+ ); +}; +AdminSectionHero.Aside = ( { + children, + className, + ...props +}: React.ComponentProps< 'div' > & { + className?: string; +} ) => { return ( -

+
{ children } - { showIcon && } -

+ ); }; -AdminSectionHero.Subheading = ( { children }: { children: React.ReactNode } ) => { - return
{ children }
; +AdminSectionHero.Heading = ( { + children, + icon, + iconOutline, + ...props +}: React.ComponentProps< typeof H3 > & { + icon?: 'default' | 'success' | 'error'; + iconOutline?: boolean; +} ) => { + return ( +

+ { children } + { !! icon && ( + + ) } +

+ ); }; export default AdminSectionHero; diff --git a/projects/plugins/protect/src/js/components/admin-section-hero/stories/index.stories.jsx b/projects/plugins/protect/src/js/components/admin-section-hero/stories/index.stories.jsx index 7d5b4f8066c93..59ed9086d6317 100644 --- a/projects/plugins/protect/src/js/components/admin-section-hero/stories/index.stories.jsx +++ b/projects/plugins/protect/src/js/components/admin-section-hero/stories/index.stories.jsx @@ -9,14 +9,16 @@ export default { export const Default = args => ; Default.args = { - main: ( + children: ( <> - - { 'No threats found' } - + + + { 'No threats found' } { 'Most recent results' } - + + + + ), - secondary: , }; diff --git a/projects/plugins/protect/src/js/components/admin-section-hero/styles.module.scss b/projects/plugins/protect/src/js/components/admin-section-hero/styles.module.scss index 5881bcd910045..404abdd2d3f98 100644 --- a/projects/plugins/protect/src/js/components/admin-section-hero/styles.module.scss +++ b/projects/plugins/protect/src/js/components/admin-section-hero/styles.module.scss @@ -1,26 +1,40 @@ -.header-main { +.admin-section-hero { display: flex; flex-direction: column; - justify-content: center; - align-items: flex-start; + gap: calc( var( --spacing-base ) * 6 ); // 48px + + min-height: 265px; + max-width: var(--max-container-width); + padding: calc( var( --spacing-base ) * 6 ) 0; // 48px 0 + margin: 0 auto; + + @media ( min-width: 1100px ) { + flex-direction: row; + align-items: center; + gap: calc( var( --spacing-base ) * 3 ); // 24px + } } -.header-secondary { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-end; +.admin-section-hero__main { + flex: 2; } -.heading-icon { - margin-left: var( --spacing-base ); // 8px - margin-bottom: calc( var( --spacing-base ) / 2 * -1 ); // -4px +.admin-section-hero__aside { + flex: 1; + flex-shrink: 0; + + @media ( min-width: 1200px ) { + display: flex; + justify-content: flex-end; + } } -.subheading { - width: fit-content; +.heading { + display: flex; + align-items: center; + gap: calc( var( --spacing-base ) * 2 ); // 16px } .connection-error-col { margin-top: calc( var( --spacing-base ) * 3 + 1px ); // 25px -} \ No newline at end of file +} diff --git a/projects/plugins/protect/src/js/components/alert-icon/index.jsx b/projects/plugins/protect/src/js/components/alert-icon/index.jsx deleted file mode 100644 index 8a4d32da59553..0000000000000 --- a/projects/plugins/protect/src/js/components/alert-icon/index.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Path, SVG, Rect, G } from '@wordpress/components'; -import React from 'react'; -import styles from './styles.module.scss'; - -/** - * Alert icon - * - * @param {object} props - Props. - * @param {string} props.className - Optional component class name. - * @param {string} props.color - Optional icon color. Defaults to '#D63638'. - * @return { React.ReactNode } The Alert Icon component. - */ -export default function AlertSVGIcon( { className, color = '#D63638' } ) { - return ( -
- - - - - - - - - - - - - - - - - - - -
- ); -} diff --git a/projects/plugins/protect/src/js/components/alert-icon/stories/index.stories.jsx b/projects/plugins/protect/src/js/components/alert-icon/stories/index.stories.jsx deleted file mode 100644 index 47b2ee32d4b51..0000000000000 --- a/projects/plugins/protect/src/js/components/alert-icon/stories/index.stories.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import AlertIcon from '../index.jsx'; - -export default { - title: 'Plugins/Protect/Alert Icon', - component: AlertIcon, - argTypes: { - color: { - control: { - type: 'color', - }, - }, - }, -}; - -const FooterTemplate = args => ; -export const Default = FooterTemplate.bind( {} ); diff --git a/projects/plugins/protect/src/js/components/alert-icon/styles.module.scss b/projects/plugins/protect/src/js/components/alert-icon/styles.module.scss deleted file mode 100644 index 938a62897f2a8..0000000000000 --- a/projects/plugins/protect/src/js/components/alert-icon/styles.module.scss +++ /dev/null @@ -1,11 +0,0 @@ -.container { - width: 48px; - height: 56px; - margin-bottom: calc( var( --spacing-base ) * 8 ); // 64px - - > svg { - position: relative; - top: -36px; - left: -40px; - } -} diff --git a/projects/plugins/protect/src/js/components/error-admin-section-hero/index.tsx b/projects/plugins/protect/src/js/components/error-admin-section-hero/index.tsx index 5214531dcf362..d752090e67aea 100644 --- a/projects/plugins/protect/src/js/components/error-admin-section-hero/index.tsx +++ b/projects/plugins/protect/src/js/components/error-admin-section-hero/index.tsx @@ -1,8 +1,7 @@ import { Text } from '@automattic/jetpack-components'; +import { ShieldIcon } from '@automattic/jetpack-scan'; import { __ } from '@wordpress/i18n'; -import { Icon, warning } from '@wordpress/icons'; import AdminSectionHero from '../admin-section-hero'; -import ScanNavigation from '../scan-navigation'; import styles from './styles.module.scss'; interface ErrorAdminSectionHeroProps { @@ -20,25 +19,17 @@ const ErrorAdminSectionHero: React.FC< ErrorAdminSectionHeroProps > = ( { displayErrorMessage += ' ' + __( 'Try again in a few minutes.', 'jetpack-protect' ); return ( - - -
- - { __( 'An error occurred', 'jetpack-protect' ) } -
-
- - { displayErrorMessage } - -
- + + + +
+ { __( 'An error occurred', 'jetpack-protect' ) } +
- - } - preserveSecondaryOnMobile={ false } - /> +
+ { displayErrorMessage } +
+
); }; diff --git a/projects/plugins/protect/src/js/components/error-admin-section-hero/styles.module.scss b/projects/plugins/protect/src/js/components/error-admin-section-hero/styles.module.scss index 6f0750abd02f8..1c89377d4b4b5 100644 --- a/projects/plugins/protect/src/js/components/error-admin-section-hero/styles.module.scss +++ b/projects/plugins/protect/src/js/components/error-admin-section-hero/styles.module.scss @@ -4,11 +4,7 @@ } .warning { - width: 54px; - height: 54px; - fill: var( --jp-red ); - margin-left: -8px; - margin-right: var( --spacing-base ); // 8px + margin-left: calc( var( --spacing-base ) * 1.5 ); // 12px } .scan-navigation { diff --git a/projects/plugins/protect/src/js/components/fix-threat-modal/index.jsx b/projects/plugins/protect/src/js/components/fix-threat-modal/index.jsx index e1274e8e29a17..cbb49498c353f 100644 --- a/projects/plugins/protect/src/js/components/fix-threat-modal/index.jsx +++ b/projects/plugins/protect/src/js/components/fix-threat-modal/index.jsx @@ -7,7 +7,7 @@ import ThreatFixHeader from '../threat-fix-header'; import UserConnectionGate from '../user-connection-gate'; import styles from './styles.module.scss'; -const FixThreatModal = ( { id, fixable, label, icon, severity } ) => { +const FixThreatModal = ( { threat } ) => { const { setModal } = useModal(); const { fixThreats, isLoading: isFixersLoading } = useFixers(); @@ -21,7 +21,7 @@ const FixThreatModal = ( { id, fixable, label, icon, severity } ) => { const handleFixClick = () => { return async event => { event.preventDefault(); - await fixThreats( [ id ] ); + await fixThreats( [ threat.id ] ); setModal( { type: null } ); }; }; @@ -37,10 +37,7 @@ const FixThreatModal = ( { id, fixable, label, icon, severity } ) => {
- +
diff --git a/projects/plugins/protect/src/js/components/free-accordion/index.jsx b/projects/plugins/protect/src/js/components/free-accordion/index.jsx deleted file mode 100644 index e801d9374fd33..0000000000000 --- a/projects/plugins/protect/src/js/components/free-accordion/index.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Text } from '@automattic/jetpack-components'; -import { Icon, chevronDown, chevronUp } from '@wordpress/icons'; -import clsx from 'clsx'; -import React, { useState, useCallback, useContext } from 'react'; -import styles from './styles.module.scss'; - -const FreeAccordionContext = React.createContext(); - -export const FreeAccordionItem = ( { id, title, label, icon, children, onOpen } ) => { - const accordionData = useContext( FreeAccordionContext ); - const open = accordionData?.open === id; - const setOpen = accordionData?.setOpen; - - const bodyClassNames = clsx( styles[ 'accordion-body' ], { - [ styles[ 'accordion-body-open' ] ]: open, - [ styles[ 'accordion-body-close' ] ]: ! open, - } ); - - const handleClick = useCallback( () => { - if ( ! open ) { - onOpen?.(); - } - setOpen( current => { - return current === id ? null : id; - } ); - }, [ open, onOpen, setOpen, id ] ); - - return ( -
- -
- { children } -
-
- ); -}; - -const FreeAccordion = ( { children } ) => { - const [ open, setOpen ] = useState(); - - return ( - -
{ children }
-
- ); -}; - -export default FreeAccordion; diff --git a/projects/plugins/protect/src/js/components/free-accordion/stories/index.stories.jsx b/projects/plugins/protect/src/js/components/free-accordion/stories/index.stories.jsx deleted file mode 100644 index 43ad41e2501eb..0000000000000 --- a/projects/plugins/protect/src/js/components/free-accordion/stories/index.stories.jsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Text } from '@automattic/jetpack-components'; -import { wordpress, plugins } from '@wordpress/icons'; -import React from 'react'; -import FreeAccordion, { FreeAccordionItem } from '..'; - -export default { - title: 'Plugins/Protect/Free Accordion', - component: FreeAccordion, - parameters: { - layout: 'centered', - }, - decorators: [ - Story => ( -
- -
- ), - ], -}; - -// eslint-disable-next-line no-unused-vars -export const Default = args => ( - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - -); diff --git a/projects/plugins/protect/src/js/components/free-accordion/styles.module.scss b/projects/plugins/protect/src/js/components/free-accordion/styles.module.scss deleted file mode 100644 index 5278f6eff39f4..0000000000000 --- a/projects/plugins/protect/src/js/components/free-accordion/styles.module.scss +++ /dev/null @@ -1,79 +0,0 @@ -.accordion { - border-radius: var( --jp-border-radius ); - border: 1px solid var( --jp-gray ); - - & > *:not(:last-child) { - border-bottom: 1px solid var( --jp-gray ); - } -} - -.accordion-item { - background-color: var( --jp-white ); -} - -.accordion-header { - margin: 0; - display: grid; - grid-template-columns: repeat(9, 1fr); - cursor: pointer; - box-sizing: border-box; - background: none; - border: none; - width: 100%; - align-items: center; - outline-color: var( --jp-black ); - padding: calc( var( --spacing-base ) * 2) calc( var( --spacing-base ) * 3); // 16px | 24px - text-align: start; - - >:first-of-type { - grid-column: 1/8; - } - - >:last-of-type { - grid-column: 9; - } - - &:hover { - background: var( --jp-gray-0 ); - } -} - -.accordion-header-label { - display: flex; - align-items: center; - font-size: var( --font-body-small ); - font-weight: normal; -} - -.accordion-header-label-icon { - margin-right: var( --spacing-base ); // 8px -} - -.accordion-header-description { - font-weight: 600; - margin-left: calc( var( --spacing-base ) * 4 ); // 32px - margin-bottom: var( --spacing-base ); // 8px -} - -.accordion-header-button { - align-items: center; -} - -.accordion-body { - transform-origin: top center; - overflow: hidden; - - &-close { - transition: all .1s; - max-height: 0; - padding: 0; - transform: scaleY(0); - } - - &-open { - transition: max-height .3s, transform .2s; - padding: calc( var( --spacing-base ) * 4 ) calc( var( --spacing-base ) * 7 ); // 32 px | 56px - max-height: 1000px; - transform: scaleY(1); - } -} diff --git a/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx b/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx index e679757c5b73d..b4fb1cfe42a5f 100644 --- a/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx +++ b/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx @@ -1,17 +1,20 @@ import { Button, getRedirectUrl, Text } from '@automattic/jetpack-components'; -import { ThreatSeverityBadge } from '@automattic/jetpack-scan'; +import { ThreatSeverityBadge, getThreatIcon, getThreatSubtitle } from '@automattic/jetpack-scan'; +import { Icon } from '@wordpress/components'; import { createInterpolateElement, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Icon } from '@wordpress/icons'; import useIgnoreThreatMutation from '../../data/scan/use-ignore-threat-mutation'; import useModal from '../../hooks/use-modal'; +import useWafData from '../../hooks/use-waf-data'; import UserConnectionGate from '../user-connection-gate'; import styles from './styles.module.scss'; -const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { +const IgnoreThreatModal = ( { threat } ) => { + const { wafSupported } = useWafData(); const { setModal } = useModal(); const ignoreThreatMutation = useIgnoreThreatMutation(); const codeableURL = getRedirectUrl( 'jetpack-protect-codeable-referral' ); + const icon = getThreatIcon( threat ); const [ isIgnoring, setIsIgnoring ] = useState( false ); @@ -26,7 +29,7 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { return async event => { event.preventDefault(); setIsIgnoring( true ); - await ignoreThreatMutation.mutateAsync( id ); + await ignoreThreatMutation.mutateAsync( threat.id ); setModal( { type: null } ); setIsIgnoring( false ); }; @@ -43,25 +46,30 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => {
- { label } + { getThreatSubtitle( threat ) } - { title } + { threat.title }
- +
- { createInterpolateElement( - __( - 'By choosing to ignore this threat, you acknowledge that you have reviewed the detected code. You are accepting the risks of maintaining a potentially malicious or vulnerable file on your site. If you are unsure, please request an estimate with Codeable.', - 'jetpack-protect' - ), - { - codeableLink: -
- ) } - - - ); -}; - -export default NavigationGroup; diff --git a/projects/plugins/protect/src/js/components/navigation/index.jsx b/projects/plugins/protect/src/js/components/navigation/index.jsx deleted file mode 100644 index bd30dfbdad964..0000000000000 --- a/projects/plugins/protect/src/js/components/navigation/index.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Text } from '@automattic/jetpack-components'; -import { Popover } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { Icon, chevronDown, chevronUp } from '@wordpress/icons'; -import React, { useState, useRef, useCallback } from 'react'; -import NavigationGroup from './group'; -import NavigationItem from './item'; -import styles from './styles.module.scss'; -import useMenuNavigation, { NavigationContext } from './use-menu-navigation'; - -const NavigationList = ( { children } ) => ( -
    - { children } -
-); - -const NavigationDropdown = ( { children, data } ) => { - const ref = useRef( undefined ); - const [ listOpen, setListOpen ] = useState( false ); - const item = data?.items?.find( navItem => navItem?.id === data?.selectedItem ) ?? { - label: __( 'See all results', 'jetpack-protect' ), - }; - const { label, icon } = item; - - const handleOpen = useCallback( () => { - setListOpen( open => ! open ); - }, [] ); - - return ( - - ); -}; - -const getNavigationComponent = mode => { - switch ( mode ) { - case 'list': - return NavigationList; - case 'dropdown': - return NavigationDropdown; - default: - return NavigationList; - } -}; - -const Navigation = ( { children, selected, onSelect, mode = 'list' } ) => { - const data = useMenuNavigation( { selected, onSelect } ); - const Component = getNavigationComponent( mode ); - - return ( - - { children } - - ); -}; - -export default Navigation; -export { NavigationItem, NavigationGroup }; diff --git a/projects/plugins/protect/src/js/components/navigation/item.jsx b/projects/plugins/protect/src/js/components/navigation/item.jsx deleted file mode 100644 index d902625c3997d..0000000000000 --- a/projects/plugins/protect/src/js/components/navigation/item.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import clsx from 'clsx'; -import React, { useContext, useEffect, useCallback } from 'react'; -import ItemBadge from './badge'; -import ItemLabel from './label'; -import styles from './styles.module.scss'; -import { NavigationContext } from './use-menu-navigation'; - -const NavigationItem = ( { - id, - label, - icon, - badge, - disabled, - onClick, - onKeyDown, - onFocus, - checked, -} ) => { - const context = useContext( NavigationContext ); - - const selected = context?.selectedItem === id; - const registerItem = context?.registerItem; - const registerRef = context?.registerRef; - const handleClickItem = context?.handleClickItem; - const handleKeyDownItem = context?.handleKeyDownItem; - const handleFocusItem = context?.handleFocusItem; - - const wrapperClassName = clsx( styles[ 'navigation-item' ], { - [ styles.clickable ]: ! disabled, - [ styles.selected ]: selected, - } ); - - const handleClick = useCallback( - evt => { - onClick?.( evt ); - handleClickItem?.( id ); - }, - [ handleClickItem, id, onClick ] - ); - - const handleKeyDown = useCallback( - evt => { - onKeyDown?.( evt ); - handleKeyDownItem?.( evt ); - }, - [ handleKeyDownItem, onKeyDown ] - ); - - const handleRef = useCallback( - ref => { - registerRef( ref, id ); - }, - [ registerRef, id ] - ); - - const handleFocus = useCallback( - evt => { - onFocus?.( evt ); - handleFocusItem?.( id ); - }, - [ handleFocusItem, id, onFocus ] - ); - - useEffect( () => { - registerItem( { id, disabled, label, icon } ); - // eslint-disable-next-line - }, [] ); - - return ( -
  • - { label } - -
  • - ); -}; - -export default NavigationItem; diff --git a/projects/plugins/protect/src/js/components/navigation/label.jsx b/projects/plugins/protect/src/js/components/navigation/label.jsx deleted file mode 100644 index 8f075caae020a..0000000000000 --- a/projects/plugins/protect/src/js/components/navigation/label.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Text } from '@automattic/jetpack-components'; -import { Icon } from '@wordpress/icons'; -import clsx from 'clsx'; -import PropTypes from 'prop-types'; -import React from 'react'; -import styles from './styles.module.scss'; - -const ItemLabel = ( { icon, children, className } ) => { - return ( - - { icon && } - { children } - - ); -}; - -ItemLabel.propTypes = { - /* An icon that will be rendered before text */ - icon: PropTypes.node, - /* Label text that will be rendered */ - children: PropTypes.node.isRequired, -}; - -export default ItemLabel; diff --git a/projects/plugins/protect/src/js/components/navigation/styles.module.scss b/projects/plugins/protect/src/js/components/navigation/styles.module.scss deleted file mode 100644 index df9e3ef1f8a25..0000000000000 --- a/projects/plugins/protect/src/js/components/navigation/styles.module.scss +++ /dev/null @@ -1,142 +0,0 @@ -.navigation { - background-color: var( --jp-white ); - box-shadow: 0px 0px 40px rgba(0, 0, 0, 0.08); - border-radius: var( --jp-border-radius ); - margin: 0; -} - -.navigation-item { - display: flex; - padding: calc( var( --spacing-base ) * 2 ); // 16px; - align-items: center; - justify-content: space-between; - margin: 0; - text-align: left; - - // Clickable State - &.clickable { - cursor: pointer; - outline-color: var( --jp-black ); - - // Focus/Hover State - &:hover:not(.selected), - &:focus:not(.selected) { - background-color: var( --jp-gray-0 ); - } - } - - // Selected State - &.selected { - background-color: var( --jp-black ); - - & .navigation-item-label { - color: var( --jp-white ) - } - - & .navigation-item-icon { - fill: var( --jp-white ); - } - - & .navigation-item-badge { - border: 1px solid var( --jp-red ); - background-color: var( --jp-red ); - color: var( --jp-white ); - } - } - - // CHILDRENS - - // .navigation-item-label - &-label { - display: flex; - align-items: center; - padding-right: var( --spacing-base ); // 8px - overflow-x: hidden; - } - - // .navigation-item-label-content - &-label-text { - display: block; - overflow-x: hidden; - text-overflow: ellipsis; - } - - // .navigation-item-icon - &-icon { - margin-right: calc( var( --spacing-base ) * 2); // 16px - } - - // .navigation-item-badge - &-badge { - border: 1px solid var( --jp-red-60 ); - color: var( --jp-red-60 ); - border-radius: 50%; - padding: calc( var( --spacing-base ) / 2 ) var( --spacing-base ); // 4px | 8px - min-width: 30px; - display: flex; - align-items: center; - justify-content: center; - box-sizing: border-box; - } - - &-check-badge { - fill: var( --jp-green-50 ); - } - - &-info-badge { - fill: var( --jp-gray-20 ); - } -} - -.navigation-group { - --icon-size: 28px; - --item-spacing: calc( var( --spacing-base ) * 2 ); // 16px - --left-spacing: calc( var( --icon-size ) + var( --item-spacing ) ); // 28px + 16px - - list-style: none; - - &-label { - padding: calc( var( --spacing-base ) * 2 ); // 16px - } - - &-content { - padding: 0; - } - - &-list { - margin-left: var( --left-spacing ) ; - } - - &-truncate { - padding: calc( var( --spacing-base ) * 2 ); // 16px - display: flex; - justify-content: flex-start; - } -} - -.popover-text { - width: 250px; - padding: calc( var( --spacing-base ) * 2 ); // 16px -} - -.navigation-dropdown { - &-button { - display: flex; - border: 1px solid var( --jp-gray-10 ); - border-radius: var( --jp-border-radius ); - padding: calc( var( --spacing-base ) * 2 ); // 16px - background-color: var( --jp-white ); - justify-content: space-between; - align-items: center; - width: 100%; - } - - &-label { - display: flex; - justify-content: flex-start; - } - - &-icon { - margin-right: var( --spacing-base ); // 8px - } -} diff --git a/projects/plugins/protect/src/js/components/navigation/use-menu-navigation.js b/projects/plugins/protect/src/js/components/navigation/use-menu-navigation.js deleted file mode 100644 index 2972dac06b572..0000000000000 --- a/projects/plugins/protect/src/js/components/navigation/use-menu-navigation.js +++ /dev/null @@ -1,92 +0,0 @@ -import React, { useState } from 'react'; - -export const NavigationContext = React.createContext(); - -const useMenuNavigation = ( { selected, onSelect } ) => { - const [ items, setItems ] = useState( [] ); - const [ refs, setRef ] = useState( [] ); - const [ focusedItem, setFocusedItem ] = useState(); - - const handleClickItem = id => { - onSelect( id ); - }; - - const handleFocusItem = id => { - setFocusedItem( id ); - }; - - const getPrevItem = ( current, last ) => { - const startMinusOne = current - 1; - const prevIndex = startMinusOne < 0 ? last : startMinusOne; - const prevItem = items[ prevIndex ]; - return prevItem?.disabled ? getPrevItem( prevIndex, last ) : prevItem; - }; - - const getNextItem = ( current, last ) => { - const startPlusOne = current + 1; - const nextIndex = startPlusOne > last ? 0 : startPlusOne; - const nextItem = items[ nextIndex ]; - return nextItem?.disabled ? getNextItem( nextIndex, last ) : nextItem; - }; - - const handleKeyDownItem = input => { - const code = input?.code; - const current = items.findIndex( item => item?.id === selected ); - const lastIndex = items.length - 1; - - let nextId; - - if ( code === 'ArrowUp' ) { - const prevItem = getPrevItem( current, lastIndex ); - nextId = prevItem?.id; - } else if ( code === 'ArrowDown' ) { - const nextItem = getNextItem( current, lastIndex ); - nextId = nextItem?.id; - } else if ( ( code === 'Enter' || code === 'Space' ) && focusedItem ) { - nextId = focusedItem; - } - - if ( nextId ) { - const element = refs[ nextId ]; - element?.focus(); - onSelect( nextId ); - } - }; - - const registerRef = ( ref, id ) => { - setRef( allRefs => { - if ( ! allRefs[ id ] && ref ) { - return { ...allRefs, [ id ]: ref }; - } - return allRefs; - } ); - }; - - const registerItem = data => { - setItems( allItems => { - const newItems = [ ...allItems ]; - const id = data?.id; - const currentIdx = newItems.findIndex( item => item?.id === id ); - - if ( currentIdx >= 0 ) { - newItems[ currentIdx ] = data; - } else { - newItems.push( data ); - } - - return newItems; - } ); - }; - - return { - selectedItem: selected, - handleClickItem, - handleKeyDownItem, - handleFocusItem, - registerRef, - registerItem, - items, - }; -}; - -export default useMenuNavigation; diff --git a/projects/plugins/protect/src/js/components/notice/styles.module.scss b/projects/plugins/protect/src/js/components/notice/styles.module.scss index 1b5a51f3940eb..15c923ef0c10f 100644 --- a/projects/plugins/protect/src/js/components/notice/styles.module.scss +++ b/projects/plugins/protect/src/js/components/notice/styles.module.scss @@ -4,7 +4,7 @@ display: flex; border-radius: var( --jp-border-radius ); // 4px overflow: hidden; - z-index: 1; + z-index: 1000001; &.notice--info { border-left: 4px solid var( --jp-yellow-20 ); diff --git a/projects/plugins/protect/src/js/components/paid-accordion/index.jsx b/projects/plugins/protect/src/js/components/paid-accordion/index.jsx deleted file mode 100644 index e59b619dba4b3..0000000000000 --- a/projects/plugins/protect/src/js/components/paid-accordion/index.jsx +++ /dev/null @@ -1,187 +0,0 @@ -import { IconTooltip, Spinner, Text, useBreakpointMatch } from '@automattic/jetpack-components'; -import { ThreatSeverityBadge } from '@automattic/jetpack-scan'; -import { ExternalLink } from '@wordpress/components'; -import { dateI18n } from '@wordpress/date'; -import { createInterpolateElement } from '@wordpress/element'; -import { sprintf, __ } from '@wordpress/i18n'; -import { Icon, check, chevronDown, chevronUp } from '@wordpress/icons'; -import clsx from 'clsx'; -import React, { useState, useCallback, useContext, useMemo } from 'react'; -import { PAID_PLUGIN_SUPPORT_URL } from '../../constants'; -import useFixers from '../../hooks/use-fixers'; -import styles from './styles.module.scss'; - -// Extract context provider for clarity and reusability -const PaidAccordionContext = React.createContext(); - -// Component for displaying threat dates -const ScanHistoryDetails = ( { firstDetected, fixedOn, status } ) => { - const statusText = useMemo( () => { - if ( status === 'fixed' ) { - return sprintf( - /* translators: %s: Fixed on date */ - __( 'Threat fixed %s', 'jetpack-protect' ), - dateI18n( 'M j, Y', fixedOn ) - ); - } - if ( status === 'ignored' ) { - return __( 'Threat ignored', 'jetpack-protect' ); - } - return null; - }, [ status, fixedOn ] ); - - return ( - firstDetected && ( - <> - - { sprintf( - /* translators: %s: First detected date */ - __( 'Threat found %s', 'jetpack-protect' ), - dateI18n( 'M j, Y', firstDetected ) - ) } - { statusText && ( - <> - - { statusText } - - ) } - - { [ 'fixed', 'ignored' ].includes( status ) && } - - ) - ); -}; - -// Badge for displaying the status (fixed or ignored) -const StatusBadge = ( { status } ) => ( -
    - { status === 'fixed' - ? __( 'Fixed', 'jetpack-protect' ) - : __( 'Ignored', 'jetpack-protect', /* dummy arg to avoid bad minification */ 0 ) } -
    -); - -const renderFixerStatus = ( isActiveFixInProgress, isStaleFixInProgress ) => { - if ( isStaleFixInProgress ) { - return ( - - - { createInterpolateElement( - __( - 'The fixer is taking longer than expected. Please try again or contact support.', - 'jetpack-protect' - ), - { - supportLink: ( - - ), - } - ) } - - - ); - } - - if ( isActiveFixInProgress ) { - return ; - } - - return ; -}; - -export const PaidAccordionItem = ( { - id, - title, - label, - icon, - fixable, - severity, - children, - firstDetected, - fixedOn, - onOpen, - status, - hideAutoFixColumn = false, -} ) => { - const { open, setOpen } = useContext( PaidAccordionContext ); - const isOpen = open === id; - - const { isThreatFixInProgress, isThreatFixStale } = useFixers(); - - const handleClick = useCallback( () => { - if ( ! isOpen ) { - onOpen?.(); - } - setOpen( current => ( current === id ? null : id ) ); - }, [ isOpen, onOpen, setOpen, id ] ); - - const [ isSmall ] = useBreakpointMatch( [ 'sm', 'lg' ], [ null, '<' ] ); - - return ( -
    - -
    - { children } -
    -
    - ); -}; - -const PaidAccordion = ( { children } ) => { - const [ open, setOpen ] = useState(); - - return ( - -
    { children }
    -
    - ); -}; - -export default PaidAccordion; diff --git a/projects/plugins/protect/src/js/components/paid-accordion/stories/broken/index.stories.jsx b/projects/plugins/protect/src/js/components/paid-accordion/stories/broken/index.stories.jsx deleted file mode 100644 index 252f22b2bad77..0000000000000 --- a/projects/plugins/protect/src/js/components/paid-accordion/stories/broken/index.stories.jsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Text } from '@automattic/jetpack-components'; -import { wordpress, plugins } from '@wordpress/icons'; -import React from 'react'; -import PaidAccordion, { PaidAccordionItem } from '..'; - -export default { - title: 'Plugins/Protect/Paid Accordion', - component: PaidAccordion, - parameters: { - layout: 'centered', - }, - decorators: [ - Story => ( -
    - -
    - ), - ], -}; - -// eslint-disable-next-line no-unused-vars -export const Default = args => ( - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - -); diff --git a/projects/plugins/protect/src/js/components/paid-accordion/styles.module.scss b/projects/plugins/protect/src/js/components/paid-accordion/styles.module.scss deleted file mode 100644 index 8304942b206d4..0000000000000 --- a/projects/plugins/protect/src/js/components/paid-accordion/styles.module.scss +++ /dev/null @@ -1,202 +0,0 @@ -.accordion { - display: inline-block; - width: 100%; - border-radius: var( --jp-border-radius ); - border: 1px solid var( --jp-gray ); - - & > *:not(:last-child) { - border-bottom: 1px solid var( --jp-gray ); - } -} - -.accordion-item { - background-color: var( --jp-white ); -} - -.accordion-header { - margin: 0; - display: grid; - grid-template-columns: repeat(9, 1fr); - cursor: pointer; - box-sizing: border-box; - background: none; - border: none; - width: 100%; - align-items: center; - outline-color: var( --jp-black ); - padding: calc( var( --spacing-base ) * 2) calc( var( --spacing-base ) * 3); // 16px | 24px - text-align: start; - - >:first-of-type { - grid-column: 1/7; - } - - >:last-of-type { - grid-column: 9; - } - - >:not( :first-child ) { - margin: auto; - } - - &:hover { - background: var( --jp-gray-0 ); - } -} - -.accordion-header-label { - display: flex; - align-items: center; - font-size: var( --font-body-small ); - font-weight: normal; -} - -.accordion-header-label-icon { - margin-right: var( --spacing-base ); // 8px -} - -.accordion-header-description { - font-weight: 600; - margin-left: calc( var( --spacing-base ) * 4 ); // 32px - margin-bottom: var( --spacing-base ); // 8px -} - -.accordion-header-status { - font-size: var( --font-body-small ); - font-weight: normal; - margin-left: calc( var( --spacing-base ) * 4 ); // 32px - margin-bottom: var( --spacing-base ); // 8px -} - -.accordion-header-status-separator { - display: inline-block; - height: 4px; - margin: 2px 12px; - width: 4px; - background-color: var( --jp-gray-50 ); -} - -.accordion-header-button { - align-items: center; -} - -.accordion-body { - transform-origin: top center; - overflow: hidden; - - &-close { - transition: all .1s; - max-height: 0; - padding: 0; - transform: scaleY(0); - } - - &-open { - transition: max-height .3s, transform .2s; - padding: calc( var( --spacing-base ) * 4 ) calc( var( --spacing-base ) * 7 ); // 32 px | 56px - max-height: 1000px; - transform: scaleY(1); - } -} - -.icon-check { - fill: var( --jp-green-40 ); -} - -.status-badge { - border-radius: 32px; - flex-shrink: 0; - font-size: 12px; - font-style: normal; - font-weight: 600; - line-height: 16px; - padding: calc( var( --spacing-base ) / 2 ); // 4px - position: relative; - text-align: center; - width: 60px; - margin-left: calc( var( --spacing-base ) * 4 ); // 32px - - &.fixed { - color: var( --jp-white ); - background-color: #008a20; - } - - &.ignored { - color: var( --jp-white ); - background-color: var( --jp-gray-50 ); - } -} - -.is-fixed { - color: #008a20; -} - -.support-link { - color: inherit; - - &:focus, - &:hover { - color: inherit; - box-shadow: none; - } -} - -.icon-tooltip { - max-height: 20px; - margin-left: calc( var( --spacing-base ) / 2 ); // 4px - - &__icon { - color: var( --jp-red ); - } - - &__content { - color: var( --jp-gray-70 ); - font-weight: 400; - line-height: 24px; - } -} - -@media ( max-width: 599px ) { - .accordion-header { - display: grid; - grid-auto-rows: minmax( auto, auto ); - - >:first-child { - grid-column: 1/8; - grid-row: 1; - } - - >:nth-child( 2 ) { - padding-left: calc( var( --spacing-base ) * 4 ); // 32px - grid-row: 2; - } - - >:nth-child( 3 ) { - grid-row: 2; - } - - >:nth-child( 3 ) span { - position: absolute; - margin-top: var( --spacing-base ); // 8px - } - - >:last-child { - grid-column: 10; - grid-row: 1/3; - } - } - - .status-badge { - display: none; - } -} - -@media ( max-width: 1200px ) { - .accordion-header-status { - display: grid; - } - - .accordion-header-status-separator { - display: none; - } -} diff --git a/projects/plugins/protect/src/js/components/pricing-table/index.jsx b/projects/plugins/protect/src/js/components/pricing-table/index.jsx index de8c95147f23a..8bcb6fb1be99b 100644 --- a/projects/plugins/protect/src/js/components/pricing-table/index.jsx +++ b/projects/plugins/protect/src/js/components/pricing-table/index.jsx @@ -9,10 +9,10 @@ import { import { useConnection } from '@automattic/jetpack-connection'; import { __ } from '@wordpress/i18n'; import React, { useCallback, useState } from 'react'; +import useProductDataQuery from '../../data/use-product-data-query'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; import useNotices from '../../hooks/use-notices'; import usePlan from '../../hooks/use-plan'; -import useProtectData from '../../hooks/use-protect-data'; /** * Product Detail component. @@ -35,7 +35,7 @@ const ConnectedPricingTable = () => { const [ hasCheckoutStarted, setHasCheckoutStarted ] = useState( false ); // Access paid protect product data - const { jetpackScan } = useProtectData(); + const { data: jetpackScan } = useProductDataQuery(); const { pricingForUi } = jetpackScan; const { introductoryOffer, currencyCode: currency = 'USD' } = pricingForUi; diff --git a/projects/plugins/protect/src/js/components/protect-check-icon/index.tsx b/projects/plugins/protect/src/js/components/protect-check-icon/index.tsx deleted file mode 100644 index d1100d8ce6d5e..0000000000000 --- a/projects/plugins/protect/src/js/components/protect-check-icon/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { type JSX } from 'react'; - -/** - * Protect Shield and Checkmark SVG Icon - * - * @return {JSX.Element} Protect Shield and Checkmark SVG Icon - */ -export default function ProtectCheck(): JSX.Element { - return ( - - - - - ); -} diff --git a/projects/plugins/protect/src/js/components/scan-button/index.jsx b/projects/plugins/protect/src/js/components/scan-button/index.jsx index 9df71f5984cf1..19134582abe3c 100644 --- a/projects/plugins/protect/src/js/components/scan-button/index.jsx +++ b/projects/plugins/protect/src/js/components/scan-button/index.jsx @@ -1,12 +1,14 @@ import { Button } from '@automattic/jetpack-components'; import { __ } from '@wordpress/i18n'; import React, { forwardRef, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query'; import useStartScanMutator from '../../data/scan/use-start-scan-mutation'; const ScanButton = forwardRef( ( { variant = 'secondary', children, ...props }, ref ) => { const startScanMutation = useStartScanMutator(); const { data: status } = useScanStatusQuery(); + const navigate = useNavigate(); const disabled = useMemo( () => { return startScanMutation.isPending || isScanInProgress( status ); @@ -15,6 +17,7 @@ const ScanButton = forwardRef( ( { variant = 'secondary', children, ...props }, const handleScanClick = () => { return event => { event.preventDefault(); + navigate( '/scan' ); startScanMutation.mutate(); }; }; @@ -25,6 +28,7 @@ const ScanButton = forwardRef( ( { variant = 'secondary', children, ...props }, variant={ variant } onClick={ handleScanClick() } disabled={ disabled } + weight={ 'regular' } { ...props } > { children ?? __( 'Scan now', 'jetpack-protect' ) } diff --git a/projects/plugins/protect/src/js/components/scan-navigation/index.jsx b/projects/plugins/protect/src/js/components/scan-navigation/index.jsx deleted file mode 100644 index e626b6af066c7..0000000000000 --- a/projects/plugins/protect/src/js/components/scan-navigation/index.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import { __ } from '@wordpress/i18n'; -import React, { useCallback } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; -import usePlan from '../../hooks/use-plan'; -import ButtonGroup from '../button-group'; - -/** - * Navigation for scan sections. - * - * @return {React.Element} The React Component. - */ -export default function ScanNavigation() { - const navigate = useNavigate(); - const location = useLocation(); - const { hasPlan } = usePlan(); - - const viewingScanPage = location.pathname === '/scan'; - const viewingHistoryPage = location.pathname.includes( '/scan/history' ); - const navigateToScanPage = useCallback( () => navigate( '/scan' ), [ navigate ] ); - const navigateToHistoryPage = useCallback( () => navigate( '/scan/history' ), [ navigate ] ); - - if ( ! hasPlan || ( ! viewingScanPage && ! viewingHistoryPage ) ) { - return null; - } - - return ( - <> - - - { __( 'Scanner', 'jetpack-protect' ) } - - - { __( 'History', 'jetpack-protect' ) } - - - - ); -} diff --git a/projects/plugins/protect/src/js/components/seventy-five-layout/index.tsx b/projects/plugins/protect/src/js/components/seventy-five-layout/index.tsx deleted file mode 100644 index 19ee4309e55a5..0000000000000 --- a/projects/plugins/protect/src/js/components/seventy-five-layout/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Container, Col, useBreakpointMatch } from '@automattic/jetpack-components'; -import React from 'react'; - -// Define the props interface for the SeventyFiveLayout component -interface SeventyFiveLayoutProps { - spacing?: number; - gap?: number; - main: React.ReactNode; - mainClassName?: string; - secondary: React.ReactNode; - secondaryClassName?: string; - preserveSecondaryOnMobile?: boolean; - fluid?: boolean; -} - -/** - * SeventyFive layout meta component - * The component name references to - * the sections disposition of the layout. - * FiftyFifty, 75, thus 7|5 means the cols numbers - * for main and secondary sections respectively, - * in large lg viewport size. - * - * @param {object} props - Component props - * @param {number} props.spacing - Horizontal spacing - * @param {number} props.gap - Horizontal gap - * @param {React.ReactNode} props.main - Main section component - * @param {string} props.mainClassName - Main section class name - * @param {React.ReactNode} props.secondary - Secondary section component - * @param {string} props.secondaryClassName - Secondary section class name - * @param {boolean} props.preserveSecondaryOnMobile - Whether to show secondary section on mobile - * @param {boolean} props.fluid - Whether to use fluid layout - * @return {React.ReactNode} - React meta-component - */ -const SeventyFiveLayout: React.FC< SeventyFiveLayoutProps > = ( { - spacing = 0, - gap = 0, - main, - mainClassName, - secondary, - secondaryClassName, - preserveSecondaryOnMobile = false, - fluid, -} ) => { - // Ensure the correct typing for useBreakpointMatch - const [ isSmall, isLarge ] = useBreakpointMatch( [ 'sm', 'lg' ] ); - - /* - * By convention, secondary section is not shown when: - * - preserveSecondaryOnMobile is false - * - on mobile breakpoint (sm) - */ - const hideSecondarySection = ! preserveSecondaryOnMobile && isSmall; - - return ( - - { ! hideSecondarySection && ( - <> - - { main } - - { isLarge && } - - { secondary } - - - ) } - { hideSecondarySection && { main } } - - ); -}; - -export default SeventyFiveLayout; diff --git a/projects/plugins/protect/src/js/components/seventy-five-layout/styles.module.scss b/projects/plugins/protect/src/js/components/seventy-five-layout/styles.module.scss deleted file mode 100644 index 5405c6e28a9b4..0000000000000 --- a/projects/plugins/protect/src/js/components/seventy-five-layout/styles.module.scss +++ /dev/null @@ -1,13 +0,0 @@ -// seventy-five layout -// Handle large lg size from here, -// adding a gap on one column -// in between main and secondary sections. -@media ( min-width: 960px ) { - .main { - grid-column: 1 / span 6; - } - - .secondary { - grid-column: 8 / span 5; - } -} diff --git a/projects/plugins/protect/src/js/components/threat-fix-header/index.jsx b/projects/plugins/protect/src/js/components/threat-fix-header/index.jsx index 708a110e65ceb..1a591752f03b9 100644 --- a/projects/plugins/protect/src/js/components/threat-fix-header/index.jsx +++ b/projects/plugins/protect/src/js/components/threat-fix-header/index.jsx @@ -1,7 +1,7 @@ import { Text } from '@automattic/jetpack-components'; -import { ThreatSeverityBadge } from '@automattic/jetpack-scan'; +import { ThreatSeverityBadge, getThreatIcon, getThreatSubtitle } from '@automattic/jetpack-scan'; +import { Icon } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; -import { Icon } from '@wordpress/icons'; import React, { useState, useCallback } from 'react'; import styles from './styles.module.scss'; @@ -66,10 +66,10 @@ export default function ThreatFixHeader( { threat, fixAllDialog, onCheckFix } ) return ( <>
    - +
    - { threat.label } + { getThreatSubtitle( threat ) } { getFixerMessage( threat.fixable ) } diff --git a/projects/plugins/protect/src/js/components/threats-list/empty.jsx b/projects/plugins/protect/src/js/components/threats-list/empty.jsx deleted file mode 100644 index 2d493b11e64a4..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/empty.jsx +++ /dev/null @@ -1,140 +0,0 @@ -import { H3, Text } from '@automattic/jetpack-components'; -import { createInterpolateElement } from '@wordpress/element'; -import { sprintf, __, _n } from '@wordpress/i18n'; -import { useMemo, useState } from 'react'; -import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query'; -import usePlan from '../../hooks/use-plan'; -import useProtectData from '../../hooks/use-protect-data'; -import OnboardingPopover from '../onboarding-popover'; -import ScanButton from '../scan-button'; -import styles from './styles.module.scss'; - -const ProtectCheck = () => ( - - - - -); - -/** - * Time Since - * - * @param {string} date - The past date to compare to the current date. - * @return {string} - A description of the amount of time between a date and now, i.e. "5 minutes ago". - */ -const timeSince = date => { - const now = new Date(); - const offset = now.getTimezoneOffset() * 60000; - - const seconds = Math.floor( ( new Date( now.getTime() + offset ).getTime() - date ) / 1000 ); - - let interval = seconds / 31536000; // 364 days - if ( interval > 1 ) { - return sprintf( - // translators: placeholder is a number amount of years i.e. "5 years ago". - _n( '%s year ago', '%s years ago', Math.floor( interval ), 'jetpack-protect' ), - Math.floor( interval ) - ); - } - - interval = seconds / 2592000; // 30 days - if ( interval > 1 ) { - return sprintf( - // translators: placeholder is a number amount of months i.e. "5 months ago". - _n( '%s month ago', '%s months ago', Math.floor( interval ), 'jetpack-protect' ), - Math.floor( interval ) - ); - } - - interval = seconds / 86400; // 1 day - if ( interval > 1 ) { - return sprintf( - // translators: placeholder is a number amount of days i.e. "5 days ago". - _n( '%s day ago', '%s days ago', Math.floor( interval ), 'jetpack-protect' ), - Math.floor( interval ) - ); - } - - interval = seconds / 3600; // 1 hour - if ( interval > 1 ) { - return sprintf( - // translators: placeholder is a number amount of hours i.e. "5 hours ago". - _n( '%s hour ago', '%s hours ago', Math.floor( interval ), 'jetpack-protect' ), - Math.floor( interval ) - ); - } - - interval = seconds / 60; // 1 minute - if ( interval > 1 ) { - return sprintf( - // translators: placeholder is a number amount of minutes i.e. "5 minutes ago". - _n( '%s minute ago', '%s minutes ago', Math.floor( interval ), 'jetpack-protect' ), - Math.floor( interval ) - ); - } - - return __( 'a few seconds ago', 'jetpack-protect' ); -}; - -const EmptyList = () => { - const { lastChecked } = useProtectData(); - const { hasPlan } = usePlan(); - const { data: status } = useScanStatusQuery(); - - const [ dailyAndManualScansPopoverAnchor, setDailyAndManualScansPopoverAnchor ] = - useState( null ); - - const timeSinceLastScan = useMemo( () => { - return lastChecked ? timeSince( Date.parse( lastChecked ) ) : null; - }, [ lastChecked ] ); - - return ( -
    - -

    - { __( "Don't worry about a thing", 'jetpack-protect' ) } -

    - - { timeSinceLastScan - ? createInterpolateElement( - sprintf( - // translators: placeholder is the amount of time since the last scan, i.e. "5 minutes ago". - __( - 'The last Protect scan ran %s and everything looked great.', - 'jetpack-protect' - ), - timeSinceLastScan - ), - { - strong: , - } - ) - : __( 'No threats have been detected by the current scan.', 'jetpack-protect' ) } - - { hasPlan && ( - <> - - { ! isScanInProgress( status ) && ( -
    - ); -}; - -export default EmptyList; diff --git a/projects/plugins/protect/src/js/components/threats-list/free-list.jsx b/projects/plugins/protect/src/js/components/threats-list/free-list.jsx deleted file mode 100644 index 88d4a92f9bac5..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/free-list.jsx +++ /dev/null @@ -1,125 +0,0 @@ -import { Text, Button, ContextualUpgradeTrigger } from '@automattic/jetpack-components'; -import { __, sprintf } from '@wordpress/i18n'; -import React, { useCallback } from 'react'; -import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; -import usePlan from '../../hooks/use-plan'; -import FreeAccordion, { FreeAccordionItem } from '../free-accordion'; -import Pagination from './pagination'; -import styles from './styles.module.scss'; - -const ThreatAccordionItem = ( { - description, - fixedIn, - icon, - id, - label, - name, - source, - title, - type, -} ) => { - const { recordEvent } = useAnalyticsTracks(); - const { upgradePlan } = usePlan(); - - const getScan = useCallback( () => { - recordEvent( 'jetpack_protect_threat_list_get_scan_link_click' ); - upgradePlan(); - }, [ recordEvent, upgradePlan ] ); - - const learnMoreButton = source ? ( - - ) : null; - - return ( - { - if ( ! [ 'core', 'plugin', 'theme' ].includes( type ) ) { - return; - } - recordEvent( `jetpack_protect_${ type }_threat_open` ); - }, [ recordEvent, type ] ) } - > - { description && ( -
    - - { __( 'What is the problem?', 'jetpack-protect' ) } - - { description } - { learnMoreButton } -
    - ) } - { fixedIn && ( -
    - - { __( 'How to fix it?', 'jetpack-protect' ) } - - - { - /* translators: Translates to Update to. %1$s: Name. %2$s: Fixed version */ - sprintf( __( 'Update to %1$s %2$s', 'jetpack-protect' ), name, fixedIn ) - } - - -
    - ) } - { ! description &&
    { learnMoreButton }
    } -
    - ); -}; - -const FreeList = ( { list } ) => { - return ( - - { ( { currentItems } ) => ( - - { currentItems.map( - ( { - description, - fixedIn, - icon, - id, - label, - name, - source, - table, - title, - type, - version, - } ) => ( - - ) - ) } - - ) } - - ); -}; - -export default FreeList; diff --git a/projects/plugins/protect/src/js/components/threats-list/index.jsx b/projects/plugins/protect/src/js/components/threats-list/index.jsx deleted file mode 100644 index 2823a804c1412..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/index.jsx +++ /dev/null @@ -1,194 +0,0 @@ -import { - Container, - Col, - Title, - Button, - useBreakpointMatch, - Text, -} from '@automattic/jetpack-components'; -import { __, sprintf } from '@wordpress/i18n'; -import React, { useCallback, useMemo, useState } from 'react'; -import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query'; -import useFixers from '../../hooks/use-fixers'; -import useModal from '../../hooks/use-modal'; -import usePlan from '../../hooks/use-plan'; -import OnboardingPopover from '../onboarding-popover'; -import ScanButton from '../scan-button'; -import EmptyList from './empty'; -import FreeList from './free-list'; -import ThreatsNavigation from './navigation'; -import PaidList from './paid-list'; -import styles from './styles.module.scss'; -import useThreatsList from './use-threats-list'; - -const ThreatsList = () => { - const { hasPlan } = usePlan(); - const { item, list, selected, setSelected } = useThreatsList(); - const [ isSm ] = useBreakpointMatch( 'sm' ); - const { isThreatFixInProgress, isThreatFixStale } = useFixers(); - - const { data: status } = useScanStatusQuery(); - const scanning = isScanInProgress( status ); - - // List of fixable threats that do not have a fix in progress - const fixableList = useMemo( () => { - return list.filter( threat => { - const threatId = parseInt( threat.id ); - return ( - threat.fixable && ! isThreatFixInProgress( threatId ) && ! isThreatFixStale( threatId ) - ); - } ); - }, [ list, isThreatFixInProgress, isThreatFixStale ] ); - - // Popover anchors - const [ yourScanResultsPopoverAnchor, setYourScanResultsPopoverAnchor ] = useState( null ); - const [ understandSeverityPopoverAnchor, setUnderstandSeverityPopoverAnchor ] = useState( null ); - const [ showAutoFixersPopoverAnchor, setShowAutoFixersPopoverAnchor ] = useState( null ); - const [ dailyAndManualScansPopoverAnchor, setDailyAndManualScansPopoverAnchor ] = - useState( null ); - - const { setModal } = useModal(); - - const handleShowAutoFixersClick = threatList => { - return event => { - event.preventDefault(); - setModal( { - type: 'FIX_ALL_THREATS', - props: { threatList }, - } ); - }; - }; - - const getTitle = useCallback( () => { - switch ( selected ) { - case 'all': - if ( list.length === 1 ) { - return __( 'All threats', 'jetpack-protect' ); - } - return sprintf( - /* translators: placeholder is the amount of threats found on the site. */ - __( 'All %s threats', 'jetpack-protect' ), - list.length - ); - case 'core': - return sprintf( - /* translators: placeholder is the amount of WordPress threats found on the site. */ - __( '%1$s WordPress %2$s', 'jetpack-protect' ), - list.length, - list.length === 1 ? 'threat' : 'threats' - ); - case 'files': - return sprintf( - /* translators: placeholder is the amount of file threats found on the site. */ - __( '%1$s file %2$s', 'jetpack-protect' ), - list.length, - list.length === 1 ? 'threat' : 'threats' - ); - case 'database': - return sprintf( - /* translators: placeholder is the amount of database threats found on the site. */ - __( '%1$s database %2$s', 'jetpack-protect' ), - list.length, - list.length === 1 ? 'threat' : 'threats' - ); - default: - return sprintf( - /* translators: Translates to Update to. %1$s: Name. %2$s: Fixed version */ - __( '%1$s %2$s in %3$s %4$s', 'jetpack-protect' ), - list.length, - list.length === 1 ? 'threat' : 'threats', - item?.name, - item?.version - ); - } - }, [ selected, list, item ] ); - - return ( - - -
    - -
    - { ! scanning && ( - - ) } - - - { list?.length > 0 ? ( - <> -
    - { getTitle() } - { hasPlan && ( -
    - { fixableList.length > 0 && ( - <> - - { ! scanning && ( -
    - ) } -
    - { hasPlan ? ( - <> -
    - -
    - - { __( - 'If you have manually fixed any of the threats listed above, you can run a manual scan now or wait for Jetpack to scan your site later today.', - 'jetpack-protect' - ) } - - -
    -
    - { ! scanning && ( -
    - ); -}; - -export default ThreatsList; diff --git a/projects/plugins/protect/src/js/components/threats-list/navigation.jsx b/projects/plugins/protect/src/js/components/threats-list/navigation.jsx deleted file mode 100644 index 9befe85a78612..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/navigation.jsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useBreakpointMatch } from '@automattic/jetpack-components'; -import { __ } from '@wordpress/i18n'; -import { - wordpress as coreIcon, - plugins as pluginsIcon, - warning as warningIcon, - color as themesIcon, - code as filesIcon, -} from '@wordpress/icons'; -import { useCallback, useMemo } from 'react'; -import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; -import usePlan from '../../hooks/use-plan'; -import useProtectData from '../../hooks/use-protect-data'; -import Navigation, { NavigationItem, NavigationGroup } from '../navigation'; - -const ThreatsNavigation = ( { selected, onSelect, sourceType = 'scan', statusFilter = 'all' } ) => { - const { hasPlan } = usePlan(); - const { - results: { plugins, themes }, - counts: { - current: { threats: numThreats, core: numCoreThreats, files: numFilesThreats }, - }, - } = useProtectData( { sourceType, filter: { status: statusFilter } } ); - - const { recordEvent } = useAnalyticsTracks(); - const [ isSmallOrLarge ] = useBreakpointMatch( 'lg', '<' ); - - const trackNavigationClickAll = useCallback( () => { - recordEvent( 'jetpack_protect_navigation_all_click' ); - }, [ recordEvent ] ); - - const trackNavigationClickCore = useCallback( () => { - recordEvent( 'jetpack_protect_navigation_core_click' ); - }, [ recordEvent ] ); - - const trackNavigationClickPlugin = useCallback( () => { - recordEvent( 'jetpack_protect_navigation_plugin_click' ); - }, [ recordEvent ] ); - - const trackNavigationClickTheme = useCallback( () => { - recordEvent( 'jetpack_protect_navigation_theme_click' ); - }, [ recordEvent ] ); - - const trackNavigationClickFiles = useCallback( () => { - recordEvent( 'jetpack_protect_navigation_file_click' ); - }, [ recordEvent ] ); - - const allLabel = useMemo( () => { - if ( statusFilter === 'fixed' ) { - return __( 'All fixed threats', 'jetpack-protect' ); - } - if ( statusFilter === 'ignored' ) { - return __( - 'All ignored threats', - 'jetpack-protect', - /** dummy arg to avoid bad minification */ 0 - ); - } - return __( 'All threats', 'jetpack-protect' ); - }, [ statusFilter ] ); - - return ( - - - - - { plugins.map( ( { name, threats, checked } ) => ( - - ) ) } - - - { themes.map( ( { name, threats, checked } ) => ( - - ) ) } - - { hasPlan && ( - <> - - - ) } - - ); -}; - -export default ThreatsNavigation; diff --git a/projects/plugins/protect/src/js/components/threats-list/pagination.jsx b/projects/plugins/protect/src/js/components/threats-list/pagination.jsx deleted file mode 100644 index 3e17bed0eeac4..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/pagination.jsx +++ /dev/null @@ -1,142 +0,0 @@ -import { Button, useBreakpointMatch } from '@automattic/jetpack-components'; -import { __, sprintf } from '@wordpress/i18n'; -import { chevronLeft, chevronRight } from '@wordpress/icons'; -import React, { useCallback, useState, useMemo } from 'react'; -import styles from './styles.module.scss'; - -const PaginationButton = ( { pageNumber, currentPage, onPageChange } ) => { - const isCurrentPage = useMemo( () => currentPage === pageNumber, [ currentPage, pageNumber ] ); - - const handleClick = useCallback( () => { - onPageChange( pageNumber ); - }, [ onPageChange, pageNumber ] ); - - return ( - - ); -}; - -const Pagination = ( { list, itemPerPage = 10, children } ) => { - const [ isSm ] = useBreakpointMatch( 'sm' ); - - const [ currentPage, setCurrentPage ] = useState( 1 ); - - const handlePreviousPageClick = useCallback( - () => setCurrentPage( currentPage - 1 ), - [ currentPage, setCurrentPage ] - ); - const handleNextPageClick = useCallback( - () => setCurrentPage( currentPage + 1 ), - [ currentPage, setCurrentPage ] - ); - - const totalPages = useMemo( () => Math.ceil( list.length / itemPerPage ), [ list, itemPerPage ] ); - - const currentItems = useMemo( () => { - const indexOfLastItem = currentPage * itemPerPage; - const indexOfFirstItem = indexOfLastItem - itemPerPage; - return list.slice( indexOfFirstItem, indexOfLastItem ); - }, [ currentPage, list, itemPerPage ] ); - - const pageNumbers = useMemo( () => { - if ( isSm ) { - return [ currentPage ]; - } - - const result = [ 1 ]; - if ( currentPage > 3 && totalPages > 4 ) { - result.push( '…' ); - } - - if ( currentPage === 1 ) { - // Current page is the first page. - // i.e. [ 1 ] 2 3 4 ... 10 - result.push( currentPage + 1, currentPage + 2, currentPage + 3 ); - } else if ( currentPage === 2 ) { - // Current page is the second to first page. - // i.e. 1 [ 2 ] 3 4 ... 10 - result.push( currentPage, currentPage + 1, currentPage + 2 ); - } else if ( currentPage < totalPages - 1 ) { - // Current page is positioned in the middle of the pagination. - // i.e. 1 ... 3 [ 4 ] 5 ... 10 - result.push( currentPage - 1, currentPage, currentPage + 1 ); - } else if ( currentPage === totalPages - 1 ) { - // Current page is the second to last page. - // i.e. 1 ... 7 8 [ 9 ] 10 - currentPage > 3 && result.push( currentPage - 2 ); - currentPage > 2 && result.push( currentPage - 1 ); - result.push( currentPage ); - } else if ( currentPage === totalPages ) { - // Current page is the last page. - // i.e. 1 ... 7 8 9 [ 10 ] - currentPage >= 5 && result.push( currentPage - 3 ); - currentPage >= 4 && result.push( currentPage - 2 ); - result.push( currentPage - 1 ); - } - - if ( result[ result.length - 1 ] < totalPages - 1 ) { - result.push( '…' ); - result.push( totalPages ); - } else if ( result[ result.length - 1 ] < totalPages ) { - result.push( totalPages ); - } - - return result.filter( pageNumber => pageNumber <= totalPages || isNaN( pageNumber ) ); - }, [ currentPage, isSm, totalPages ] ); - - return ( - <> - { children( { currentItems } ) } - { totalPages > 1 && ( - - ) } - - ); -}; - -export default Pagination; diff --git a/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx b/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx deleted file mode 100644 index baedf8dfa5184..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx +++ /dev/null @@ -1,253 +0,0 @@ -import { - Text, - Button, - DiffViewer, - MarkedLines, - useBreakpointMatch, -} from '@automattic/jetpack-components'; -import { __, sprintf } from '@wordpress/i18n'; -import React, { useCallback } from 'react'; -import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; -import useFixers from '../../hooks/use-fixers'; -import useModal from '../../hooks/use-modal'; -import PaidAccordion, { PaidAccordionItem } from '../paid-accordion'; -import Pagination from './pagination'; -import styles from './styles.module.scss'; - -const ThreatAccordionItem = ( { - context, - description, - diff, - filename, - firstDetected, - fixedIn, - fixedOn, - icon, - fixable, - id, - label, - name, - source, - title, - type, - severity, - status, - hideAutoFixColumn = false, -} ) => { - const { setModal } = useModal(); - const { recordEvent } = useAnalyticsTracks(); - - const { isThreatFixInProgress, isThreatFixStale } = useFixers(); - const isActiveFixInProgress = isThreatFixInProgress( id ); - const isStaleFixInProgress = isThreatFixStale( id ); - - const learnMoreButton = source ? ( - - ) : null; - - const handleIgnoreThreatClick = () => { - return event => { - event.preventDefault(); - setModal( { - type: 'IGNORE_THREAT', - props: { id, label, title, icon, severity }, - } ); - }; - }; - - const handleUnignoreThreatClick = () => { - return event => { - event.preventDefault(); - setModal( { - type: 'UNIGNORE_THREAT', - props: { id, label, title, icon, severity }, - } ); - }; - }; - - const handleFixThreatClick = () => { - return event => { - event.preventDefault(); - setModal( { - type: 'FIX_THREAT', - props: { id, fixable, label, icon, severity }, - } ); - }; - }; - - return ( - { - if ( ! [ 'core', 'plugin', 'theme', 'file', 'database' ].includes( type ) ) { - return; - } - recordEvent( `jetpack_protect_${ type }_threat_open` ); - }, [ recordEvent, type ] ) } - hideAutoFixColumn={ hideAutoFixColumn } - > - { description && ( -
    - - { status !== 'fixed' - ? __( 'What is the problem?', 'jetpack-protect' ) - : __( - 'What was the problem?', - 'jetpack-protect', - /** dummy arg to avoid bad minification */ 0 - ) } - - { description } - { learnMoreButton } -
    - ) } - { ( filename || context || diff ) && ( - - { __( 'The technical details', 'jetpack-protect' ) } - - ) } - { filename && ( - <> - - { - /* translators: filename follows in separate line; e.g. "PHP.Injection.5 in: `post.php`" */ - __( 'Threat found in file:', 'jetpack-protect' ) - } - -
    { filename }
    - - ) } - { context && } - { diff && } - { fixedIn && status !== 'fixed' && ( -
    - - { __( 'How to fix it?', 'jetpack-protect' ) } - - - { - /* translators: Translates to Update to. %1$s: Name. %2$s: Fixed version */ - sprintf( __( 'Update to %1$s %2$s', 'jetpack-protect' ), name, fixedIn ) - } - -
    - ) } - { ! description &&
    { learnMoreButton }
    } - { [ 'ignored', 'current' ].includes( status ) && ( -
    - { 'ignored' === status && ( - - ) } - { 'current' === status && ( - <> - - { fixable && ( - - ) } - - ) } -
    - ) } -
    - ); -}; - -const PaidList = ( { list, hideAutoFixColumn = false } ) => { - const [ isSmall ] = useBreakpointMatch( [ 'sm', 'lg' ], [ null, '<' ] ); - - return ( - <> - { ! isSmall && ( -
    - { __( 'Details', 'jetpack-protect' ) } - { __( 'Severity', 'jetpack-protect' ) } - { ! hideAutoFixColumn && { __( 'Auto-fix', 'jetpack-protect' ) } } - -
    - ) } - - { ( { currentItems } ) => ( - - { currentItems.map( - ( { - context, - description, - diff, - filename, - firstDetected, - fixedIn, - fixedOn, - icon, - fixable, - id, - label, - name, - severity, - source, - table, - title, - type, - version, - status, - } ) => ( - - ) - ) } - - ) } - - - ); -}; - -export default PaidList; diff --git a/projects/plugins/protect/src/js/components/threats-list/styles.module.scss b/projects/plugins/protect/src/js/components/threats-list/styles.module.scss deleted file mode 100644 index 4a50d87b2562b..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/styles.module.scss +++ /dev/null @@ -1,129 +0,0 @@ -.empty { - display: flex; - width: 100%; - height: 100%; - align-items: center; - justify-content: center; - max-height: 600px; - flex-direction: column; -} - -.threat-section + .threat-section { - margin-top: calc( var( --spacing-base ) * 5 ); // 40px -} - -.threat-filename { - background-color: var( --jp-gray-0 ); - padding: calc( var( --spacing-base ) * 3 ); // 24px - overflow-x: scroll; -} - -.threat-footer { - display: flex; - justify-content: flex-end; - border-top: 1px solid var( --jp-gray ); - padding-top: calc( var( --spacing-base ) * 3 ); // 24px - margin-top: calc( var( --spacing-base ) * 3 ); // 24px -} -.threat-item-cta { - margin-top: calc( var( --spacing-base ) * 4 ); // 36px -} - -.list-header { - display: flex; - align-items: flex-end; - margin-bottom: calc( var( --spacing-base ) * 2.25 ); // 18px -} - -.list-title { - flex: 1; - margin-bottom: 0; -} - -.list-header__controls { - display: flex; - gap: calc( var( --spacing-base ) * 2 ); // 16px -} - -.threat-footer { - width: 100%; - display: flex; - justify-content: right; - padding-top: calc( var( --spacing-base ) * 4 ); // 32px - border-top: 1px solid var( --jp-gray ); - - > :last-child { - margin-left: calc( var( --spacing-base ) * 2 ); // 16px - } -} - -.accordion-header { - display: grid; - grid-template-columns: repeat( 9, 1fr ); - background-color: white; - padding: calc( var( --spacing-base ) * 2 ) calc( var( --spacing-base ) * 3 ); // 16px | 24px - border: 1px solid var( --jp-gray ); - border-bottom: none; - color: var( --jp-gray-50 ); - width: 100%; - - > span:first-child { - grid-column: 1 / 7; - } - - > span:not( :first-child ) { - text-align: center; - } -} - -.manual-scan { - margin: calc( var( --spacing-base ) * 4 ) calc( var( --spacing-base ) * 8 ); // 32px | 64px - text-align: center; -} - -@media ( max-width: 599px ) { - - .list-header { - margin-bottom: calc( var( --spacing-base ) * 3 ); // 24px - } - - .list-title { - display: none; - } - - .threat-footer { - justify-content: center; - - > * { - width: 50%; - } - } -} - -.pagination-container { - display: flex; - justify-content: center; - align-items: center; - gap: 4px; - margin-top: calc( var( --spacing-base ) * 4 ); // 24px - margin-bottom: calc(var(--spacing-base) * 2); // 16px - - button { - font-size: var( --font-body ); - width: auto; - height: auto; - padding: 0 var( --spacing-base ); // 0 | 8px - line-height: 32px; - min-width: 32px; - - &.unfocused { - color: var( --jp-black ); - background: none; - - &:hover:not(:disabled) { - color: var( --jp-black ); - background: none; - } - } - } -} diff --git a/projects/plugins/protect/src/js/components/threats-list/use-threats-list.js b/projects/plugins/protect/src/js/components/threats-list/use-threats-list.js deleted file mode 100644 index de000288251ae..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/use-threats-list.js +++ /dev/null @@ -1,158 +0,0 @@ -import { - plugins as pluginsIcon, - wordpress as coreIcon, - color as themesIcon, - code as filesIcon, - grid as databaseIcon, -} from '@wordpress/icons'; -import { useEffect, useMemo, useState } from 'react'; -import useProtectData from '../../hooks/use-protect-data'; - -const sortThreats = ( a, b ) => b.severity - a.severity; - -/** - * Flatten threats data - * - * Merges threat category data with each threat it contains, plus any additional data provided. - * - * @param {object} data - The threat category data, i.e. "core", "plugins", "themes", etc. - * @param {object} newData - Additional data to add to each threat. - * @return {object[]} Array of threats with additional properties from the threat category and function argument. - */ -const flattenThreats = ( data, newData ) => { - // If "data" is an empty object - if ( typeof data === 'object' && Object.keys( data ).length === 0 ) { - return []; - } - - // If "data" has multiple entries, recursively flatten each one. - if ( Array.isArray( data ) ) { - return data.map( extension => flattenThreats( extension, newData ) ).flat(); - } - - // Merge the threat category data with each threat it contains, plus any additional data provided. - return data?.threats.map( threat => ( { - ...threat, - ...data, - ...newData, - } ) ); -}; - -/** - * Threats List Hook - * - * @param {object} args - Arguments for the hook. - * @param {string} args.source - "scan" or "history". - * @param {string} args.status - "all", "fixed", or "ignored". - * --- - * @typedef {object} UseThreatsList - * @property {object} item - The selected threat category. - * @property {object[]} list - The list of threats to display. - * @property {string} selected - The selected threat category. - * @property {Function} setSelected - Sets the selected threat category. - * --- - * @return {UseThreatsList} useThreatsList hook. - */ -const useThreatsList = ( { source, status } = { source: 'scan', status: 'all' } ) => { - const [ selected, setSelected ] = useState( 'all' ); - const { - results: { plugins, themes, core, files, database }, - } = useProtectData( { - sourceType: source, - filter: { status, key: selected }, - } ); - - const { unsortedList, item } = useMemo( () => { - // If a specific threat category is selected, filter for and flatten the category's threats. - if ( selected && selected !== 'all' ) { - // Core, files, and database data threats are already grouped together, - // so we just need to flatten them and add the appropriate icon. - switch ( selected ) { - case 'core': - return { - unsortedList: flattenThreats( core, { icon: coreIcon } ), - item: core, - }; - case 'files': - return { - unsortedList: flattenThreats( { threats: files }, { icon: filesIcon } ), - item: files, - }; - case 'database': - return { - unsortedList: flattenThreats( { threats: database }, { icon: databaseIcon } ), - item: database, - }; - default: - break; - } - - // Extensions (i.e. plugins and themes) have entries for each individual extension, - // so we need to check for a matching threat in each extension. - const selectedPlugin = plugins.find( plugin => plugin?.name === selected ); - if ( selectedPlugin ) { - return { - unsortedList: flattenThreats( selectedPlugin, { icon: pluginsIcon } ), - item: selectedPlugin, - }; - } - const selectedTheme = themes.find( theme => theme?.name === selected ); - if ( selectedTheme ) { - return { - unsortedList: flattenThreats( selectedTheme, { icon: themesIcon } ), - item: selectedTheme, - }; - } - } - - // Otherwise, return all threats. - return { - unsortedList: [ - ...flattenThreats( core, { icon: coreIcon } ), - ...flattenThreats( plugins, { icon: pluginsIcon } ), - ...flattenThreats( themes, { icon: themesIcon } ), - ...flattenThreats( { threats: files }, { icon: filesIcon } ), - ...flattenThreats( { threats: database }, { icon: databaseIcon } ), - ], - item: null, - }; - }, [ core, database, files, plugins, selected, themes ] ); - - const getLabel = threat => { - if ( threat.name && threat.version ) { - // Extension threat i.e. "Woocommerce (3.0.0)" - return `${ threat.name } (${ threat.version })`; - } - - if ( threat.filename ) { - // File threat i.e. "index.php" - return threat.filename.split( '/' ).pop(); - } - - if ( threat.table ) { - // Database threat i.e. "wp_posts" - return threat.table; - } - }; - - const list = useMemo( () => { - return unsortedList - .sort( sortThreats ) - .map( threat => ( { label: getLabel( threat ), ...threat } ) ); - }, [ unsortedList ] ); - - useEffect( () => { - if ( selected !== 'all' && status !== 'all' && list.length === 0 ) { - setSelected( 'all' ); - } - }, [ selected, status, item, list ] ); - - return { - item, - list, - selected, - setSelected, - }; -}; - -export default useThreatsList; diff --git a/projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx b/projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx index 36b7ae1f3ee4b..70bb4243b6263 100644 --- a/projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx +++ b/projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx @@ -1,5 +1,5 @@ import { Button, Text } from '@automattic/jetpack-components'; -import { ThreatSeverityBadge } from '@automattic/jetpack-scan'; +import { ThreatSeverityBadge, getThreatIcon, getThreatSubtitle } from '@automattic/jetpack-scan'; import { __ } from '@wordpress/i18n'; import { Icon } from '@wordpress/icons'; import { useState } from 'react'; @@ -8,9 +8,14 @@ import useModal from '../../hooks/use-modal'; import UserConnectionGate from '../user-connection-gate'; import styles from './styles.module.scss'; -const UnignoreThreatModal = ( { id, title, label, icon, severity } ) => { +const UnignoreThreatModal = ( { threat } ) => { const { setModal } = useModal(); + + const icon = getThreatIcon( threat ); + + const [ isUnignoring, setIsUnignoring ] = useState( false ); const unignoreThreatMutation = useUnIgnoreThreatMutation(); + const handleCancelClick = () => { return event => { event.preventDefault(); @@ -18,13 +23,11 @@ const UnignoreThreatModal = ( { id, title, label, icon, severity } ) => { }; }; - const [ isUnignoring, setIsUnignoring ] = useState( false ); - const handleUnignoreClick = () => { return async event => { event.preventDefault(); setIsUnignoring( true ); - await unignoreThreatMutation.mutateAsync( id ); + await unignoreThreatMutation.mutateAsync( threat.id ); setModal( { type: null } ); setIsUnignoring( false ); }; @@ -41,12 +44,12 @@ const UnignoreThreatModal = ( { id, title, label, icon, severity } ) => {
    - { label } + { getThreatSubtitle( threat ) } - { title } + { threat.title }
    - +
    diff --git a/projects/plugins/protect/src/js/data/scan/use-fixers-mutation.ts b/projects/plugins/protect/src/js/data/scan/use-fixers-mutation.ts index 873d115c0d68b..c23aaddb411a5 100644 --- a/projects/plugins/protect/src/js/data/scan/use-fixers-mutation.ts +++ b/projects/plugins/protect/src/js/data/scan/use-fixers-mutation.ts @@ -1,3 +1,4 @@ +import { ThreatFixStatus } from '@automattic/jetpack-scan'; import { useMutation, type UseMutationResult, useQueryClient } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; import API from '../../api'; @@ -29,7 +30,20 @@ export default function useFixersMutation(): UseMutationResult { } // The data returned from the API is the same as the data we need to update the cache. - queryClient.setQueryData( [ QUERY_FIXERS_KEY ], data ); + queryClient.setQueryData( [ QUERY_FIXERS_KEY ], ( previousData: ThreatFixStatus ) => { + let previousThreats = {}; + if ( previousData && 'threats' in previousData ) { + previousThreats = previousData.threats; + } + + return { + ...data, + threats: { + ...previousThreats, + ...data.threats, + }, + }; + } ); // Show a success notice. showSuccessNotice( diff --git a/projects/plugins/protect/src/js/data/scan/use-ignore-threat-mutation.ts b/projects/plugins/protect/src/js/data/scan/use-ignore-threat-mutation.ts index f15ac1086f442..be3e07b232720 100644 --- a/projects/plugins/protect/src/js/data/scan/use-ignore-threat-mutation.ts +++ b/projects/plugins/protect/src/js/data/scan/use-ignore-threat-mutation.ts @@ -1,3 +1,4 @@ +import { ScanStatus } from '@automattic/jetpack-scan'; import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; import API from '../../api'; @@ -14,23 +15,60 @@ export default function useIgnoreThreatMutation(): UseMutationResult { const { showSuccessNotice, showErrorNotice } = useNotices(); return useMutation( { - mutationFn: async ( threatId: number ) => { - const response = await API.ignoreThreat( threatId ); - - // Refetch the scan status and history queries as a part of the mutation function. - // This keeps the mutator in a loading state until the side effects of the mutation are handled. - await Promise.all( [ - queryClient.refetchQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] } ), - queryClient.refetchQueries( { queryKey: [ QUERY_HISTORY_KEY ] } ), + mutationFn: API.ignoreThreat, + onMutate: async ( threatId: number ) => { + // Cancel any outgoing refetches (so they don't overwrite our optimistic update) + await queryClient.cancelQueries( { queryKey: [ QUERY_HISTORY_KEY ] }, { silent: true } ); + await queryClient.cancelQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] }, { silent: true } ); + + // Snapshot the current value + const previousHistory = queryClient.getQueryData< ScanStatus >( [ QUERY_HISTORY_KEY ] ); + const previousScanStatus = queryClient.getQueryData< ScanStatus >( [ + QUERY_SCAN_STATUS_KEY, ] ); - return response; - }, - onSuccess: () => { + // Optimistically update to the new value + queryClient.setQueryData( [ QUERY_HISTORY_KEY ], ( old?: ScanStatus ) => { + if ( ! old ) { + return; + } + + return { + ...old, + threats: [ + ...old.threats, + { + ...previousScanStatus.threats.find( threat => threat.id === threatId ), + status: 'ignored', + }, + ], + }; + } ); + queryClient.setQueryData( [ QUERY_SCAN_STATUS_KEY ], ( old?: ScanStatus ) => { + if ( ! old ) { + return; + } + + return { + ...old, + threats: old.threats.filter( threat => threat.id !== threatId ), + }; + } ); + showSuccessNotice( __( 'Threat ignored.', 'jetpack-protect' ) ); + + return { previousHistory, previousScanStatus }; }, - onError: () => { + onError: ( error, threatId, context ) => { + // Roll back to the previous value + queryClient.setQueryData( [ QUERY_HISTORY_KEY ], context.previousHistory ); + queryClient.setQueryData( [ QUERY_SCAN_STATUS_KEY ], context.previousScanStatus ); + showErrorNotice( __( 'An error occurred ignoring the threat.', 'jetpack-protect' ) ); }, + onSettled: () => { + queryClient.invalidateQueries( { queryKey: [ QUERY_HISTORY_KEY ] } ); + queryClient.invalidateQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] } ); + }, } ); } diff --git a/projects/plugins/protect/src/js/data/scan/use-unignore-threat-mutation.ts b/projects/plugins/protect/src/js/data/scan/use-unignore-threat-mutation.ts index fedae11299c8b..87d4281cea6d4 100644 --- a/projects/plugins/protect/src/js/data/scan/use-unignore-threat-mutation.ts +++ b/projects/plugins/protect/src/js/data/scan/use-unignore-threat-mutation.ts @@ -1,3 +1,4 @@ +import { ScanStatus } from '@automattic/jetpack-scan'; import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; import API from '../../api'; @@ -14,23 +15,60 @@ export default function useUnIgnoreThreatMutation(): UseMutationResult { const { showSuccessNotice, showErrorNotice } = useNotices(); return useMutation( { - mutationFn: async ( threatId: number ) => { - const response = await API.unIgnoreThreat( threatId ); - - // Refetch the scan status and history queries as a part of the mutation function. - // This keeps the mutator in a loading state until the side effects of the mutation are handled. - await Promise.all( [ - queryClient.refetchQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] } ), - queryClient.refetchQueries( { queryKey: [ QUERY_HISTORY_KEY ] } ), + mutationFn: API.unIgnoreThreat, + onMutate: async ( threatId: number ) => { + // Cancel any outgoing refetches (so they don't overwrite our optimistic update) + await queryClient.cancelQueries( { queryKey: [ QUERY_HISTORY_KEY ] } ); + await queryClient.cancelQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] } ); + + // Snapshot the current value + const previousHistory = queryClient.getQueryData< ScanStatus >( [ QUERY_HISTORY_KEY ] ); + const previousScanStatus = queryClient.getQueryData< ScanStatus >( [ + QUERY_SCAN_STATUS_KEY, ] ); - return response; - }, - onSuccess: () => { + // Optimistically update to the new value + queryClient.setQueryData( [ QUERY_HISTORY_KEY ], ( old?: ScanStatus ) => { + if ( ! old ) { + return; + } + + return { + ...old, + threats: old.threats.filter( threat => threat.id !== threatId ), + }; + } ); + queryClient.setQueryData( [ QUERY_SCAN_STATUS_KEY ], ( old?: ScanStatus ) => { + if ( ! old ) { + return; + } + + return { + ...old, + threats: [ + ...old.threats, + { + ...previousHistory.threats.find( threat => threat.id === threatId ), + status: 'current', + }, + ], + }; + } ); + showSuccessNotice( __( 'Threat is no longer ignored.', 'jetpack-protect' ) ); + + return { previousHistory, previousScanStatus }; }, - onError: () => { + onError: ( error, threatId, context ) => { + // Roll back to the previous value + queryClient.setQueryData( [ QUERY_HISTORY_KEY ], context.previousHistory ); + queryClient.setQueryData( [ QUERY_SCAN_STATUS_KEY ], context.previousScanStatus ); + showErrorNotice( __( 'An error occurred un-ignoring the threat.', 'jetpack-protect' ) ); }, + onSettled: () => { + queryClient.invalidateQueries( { queryKey: [ QUERY_HISTORY_KEY ] } ); + queryClient.invalidateQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] } ); + }, } ); } diff --git a/projects/plugins/protect/src/js/data/use-credentials-query.ts b/projects/plugins/protect/src/js/data/use-credentials-query.ts index a5d2ddc8cc703..ebe8339569086 100644 --- a/projects/plugins/protect/src/js/data/use-credentials-query.ts +++ b/projects/plugins/protect/src/js/data/use-credentials-query.ts @@ -8,12 +8,15 @@ import { QUERY_CREDENTIALS_KEY } from '../constants'; * * @return {UseQueryResult} useQuery result. */ -export default function useCredentialsQuery(): UseQueryResult< [ Record< string, unknown > ] > { +export default function useCredentialsQuery(): UseQueryResult< + false | [] | [ Record< string, unknown > ] +> { const { isRegistered } = useConnection( { autoTrigger: false, from: 'protect', redirectUri: null, skipUserConnection: true, + skipPricingPage: true, } ); return useQuery( { diff --git a/projects/plugins/protect/src/js/hooks/use-fixers.ts b/projects/plugins/protect/src/js/hooks/use-fixers.ts index 3921626af055a..16a478d467c9e 100644 --- a/projects/plugins/protect/src/js/hooks/use-fixers.ts +++ b/projects/plugins/protect/src/js/hooks/use-fixers.ts @@ -1,4 +1,4 @@ -import { type FixersStatus, type ThreatFixStatus } from '@automattic/jetpack-scan'; +import { Threat, type FixersStatus, type ThreatFixStatus } from '@automattic/jetpack-scan'; import { useCallback } from 'react'; import useFixersMutation from '../data/scan/use-fixers-mutation'; import useFixersQuery from '../data/scan/use-fixers-query'; @@ -23,10 +23,10 @@ export const fixerStatusIsStale = ( fixerStatus: ThreatFixStatus ) => { type UseFixersResult = { fixableThreatIds: number[]; fixersStatus: FixersStatus; - fixThreats: ( threatIds: number[] ) => Promise< unknown >; + fixThreats: ( threatIds: Array< Threat[ 'id' ] > ) => Promise< unknown >; isLoading: boolean; - isThreatFixInProgress: ( threatId: number ) => boolean; - isThreatFixStale: ( threatId: number ) => boolean; + isThreatFixInProgress: ( threatId: Threat[ 'id' ] ) => boolean; + isThreatFixStale: ( threatId: Threat[ 'id' ] ) => boolean; }; /** diff --git a/projects/plugins/protect/src/js/hooks/use-plan.tsx b/projects/plugins/protect/src/js/hooks/use-plan.tsx index b5ab18da01875..f5cd1d54943b9 100644 --- a/projects/plugins/protect/src/js/hooks/use-plan.tsx +++ b/projects/plugins/protect/src/js/hooks/use-plan.tsx @@ -48,7 +48,7 @@ export default function usePlan( { redirectUrl }: { redirectUrl?: string } = {} const { run: checkout } = useProductCheckoutWorkflow( { productSlug: JETPACK_SCAN_SLUG, - redirectUrl: redirectUrl || adminUrl, + redirectUrl: redirectUrl || adminUrl + '#/scan', siteProductAvailabilityHandler: API.checkPlan, useBlogIdSuffix: true, connectAfterCheckout: false, diff --git a/projects/plugins/protect/src/js/hooks/use-protect-data/index.ts b/projects/plugins/protect/src/js/hooks/use-protect-data/index.ts deleted file mode 100644 index 2338d306e6780..0000000000000 --- a/projects/plugins/protect/src/js/hooks/use-protect-data/index.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { type ExtensionStatus, type Threat, type ThreatStatus } from '@automattic/jetpack-scan'; -import { __ } from '@wordpress/i18n'; -import { useMemo } from 'react'; -import useHistoryQuery from '../../data/scan/use-history-query'; -import useScanStatusQuery from '../../data/scan/use-scan-status-query'; -import useProductDataQuery from '../../data/use-product-data-query'; - -type ThreatFilterKey = 'all' | 'core' | 'files' | 'database' | string; - -type Filter = { key: ThreatFilterKey; status: ThreatStatus | 'all' }; - -// Valid "key" values for filtering. -const KEY_FILTERS = [ 'all', 'core', 'plugins', 'themes', 'files', 'database' ]; - -/** - * Filter Extension Threats - * - * @param {Array} threats - The threats to filter. - * @param {object} filter - The filter to apply to the data. - * @param {string} filter.status - The status to filter: 'all', 'current', 'fixed', or 'ignored'. - * @param {string} filter.key - The key to filter: 'all', 'core', 'files', 'database', or an extension name. - * @param {string} key - The threat's key: 'all', 'core', 'files', 'database', or an extension name. - * - * @return {Array} The filtered threats. - */ -const filterThreats = ( threats: Threat[], filter: Filter, key: ThreatFilterKey ): Threat[] => { - if ( ! Array.isArray( threats ) ) { - return []; - } - - return threats.filter( threat => { - if ( filter.status && filter.status !== 'all' && threat.status !== filter.status ) { - return false; - } - if ( filter.key && filter.key !== 'all' && filter.key !== key ) { - return false; - } - return true; - } ); -}; - -/** - * Get parsed data from the initial state - * - * @param {object} options - The options to use when getting the data. - * @param {string} options.sourceType - 'scan' or 'history'. - * @param {object} options.filter - The filter to apply to the data. - * _param {string} options.filter.status - 'all', 'fixed', or 'ignored'. - * _param {string} options.filter.key - 'all', 'core', 'files', 'database', or an extension name. - * - * @return {object} The information available in Protect's initial state. - */ -export default function useProtectData( - { sourceType, filter } = { - sourceType: 'scan', - filter: { status: null, key: null }, - } -) { - const { data: status } = useScanStatusQuery(); - const { data: scanHistory } = useHistoryQuery(); - const { data: jetpackScan } = useProductDataQuery(); - - const { counts, results, error, lastChecked, hasUncheckedItems } = useMemo( () => { - // This hook can provide data from two sources: the current scan or the scan history. - const data = sourceType === 'history' ? { ...scanHistory } : { ...status }; - - // Prepare the result object. - const result = { - results: { - core: [], - plugins: [], - themes: [], - files: [], - database: [], - }, - counts: { - all: { - threats: 0, - core: 0, - plugins: 0, - themes: 0, - files: 0, - database: 0, - }, - current: { - threats: 0, - core: 0, - plugins: 0, - themes: 0, - files: 0, - database: 0, - }, - }, - error: null, - lastChecked: data.lastChecked || null, - hasUncheckedItems: data.hasUncheckedItems || false, - }; - - // Loop through the provided extensions, and update the result object. - const processExtensions = ( extensions: Array< ExtensionStatus >, key: ThreatFilterKey ) => { - if ( ! Array.isArray( extensions ) ) { - return []; - } - extensions.forEach( extension => { - // Update the total counts. - result.counts.all[ key ] += extension?.threats?.length || 0; - result.counts.all.threats += extension?.threats?.length || 0; - - // Filter the extension's threats based on the current filters. - const filteredThreats = filterThreats( - extension?.threats || [], - filter, - KEY_FILTERS.includes( filter.key ) ? key : extension?.name - ); - - // Update the result object with the extension and its filtered threats. - result.results[ key ].push( { ...extension, threats: filteredThreats } ); - - // Update the current counts. - result.counts.current[ key ] += filteredThreats.length; - result.counts.current.threats += filteredThreats.length; - } ); - }; - - // Loop through the provided threats, and update the result object. - const processThreats = ( threatsToProcess: Threat[], key: ThreatFilterKey ) => { - if ( ! Array.isArray( threatsToProcess ) ) { - return []; - } - - result.counts.all[ key ] += threatsToProcess.length; - result.counts.all.threats += threatsToProcess.length; - - const filteredThreats = filterThreats( threatsToProcess, filter, key ); - - result.results[ key ] = [ ...result.results[ key ], ...filteredThreats ]; - result.counts.current[ key ] += filteredThreats.length; - result.counts.current.threats += filteredThreats.length; - }; - - // Core data may be either a single object or an array of multiple objects. - let cores = Array.isArray( data.core ) ? data.core : []; - if ( data?.core?.threats ) { - cores = [ data.core ]; - } - - // Process the data - processExtensions( cores, 'core' ); - processExtensions( data?.plugins, 'plugins' ); - processExtensions( data?.themes, 'themes' ); - processThreats( data?.files, 'files' ); - processThreats( data?.database, 'database' ); - - // Handle errors - if ( data.error ) { - result.error = { - message: data.errorMessage || __( 'An error occurred.', 'jetpack-protect' ), - code: data.errorCode || 500, - }; - } - - return result; - }, [ scanHistory, sourceType, status, filter ] ); - - return { - results, - counts, - error, - lastChecked, - hasUncheckedItems, - jetpackScan, - }; -} diff --git a/projects/plugins/protect/src/js/index.tsx b/projects/plugins/protect/src/js/index.tsx index 3ffe20e853986..db443e6a7c01b 100644 --- a/projects/plugins/protect/src/js/index.tsx +++ b/projects/plugins/protect/src/js/index.tsx @@ -2,7 +2,7 @@ import { ThemeProvider } from '@automattic/jetpack-components'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import * as WPElement from '@wordpress/element'; -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { HashRouter, Routes, Route, useLocation, Navigate } from 'react-router-dom'; import Modal from './components/modal'; import PaidPlanGate from './components/paid-plan-gate'; @@ -11,8 +11,9 @@ import { NoticeProvider } from './hooks/use-notices'; import { OnboardingRenderedContextProvider } from './hooks/use-onboarding'; import { CheckoutProvider } from './hooks/use-plan'; import FirewallRoute from './routes/firewall'; +import HomeRoute from './routes/home'; import ScanRoute from './routes/scan'; -import ScanHistoryRoute from './routes/scan/history'; +import HistoryRoute from './routes/scan/history'; import SettingsRoute from './routes/settings'; import SetupRoute from './routes/setup'; import './styles.module.scss'; @@ -49,7 +50,7 @@ function render() { const component = ( - + @@ -59,12 +60,13 @@ function render() { } /> } /> + } /> } /> - + } /> @@ -72,12 +74,12 @@ function render() { path="/scan/history/:filter" element={ - + } /> } /> - } /> + } /> diff --git a/projects/plugins/protect/src/js/routes/firewall/firewall-admin-section-hero.tsx b/projects/plugins/protect/src/js/routes/firewall/firewall-admin-section-hero.tsx index 3f70a75509b76..d855b18b75f1e 100644 --- a/projects/plugins/protect/src/js/routes/firewall/firewall-admin-section-hero.tsx +++ b/projects/plugins/protect/src/js/routes/firewall/firewall-admin-section-hero.tsx @@ -5,6 +5,7 @@ import AdminSectionHero from '../../components/admin-section-hero'; import useWafData from '../../hooks/use-waf-data'; import FirewallStatCards from './firewall-statcards'; import FirewallSubheading from './firewall-subheading'; +import styles from './styles.module.scss'; const FirewallAdminSectionHero = () => { const { @@ -29,7 +30,12 @@ const FirewallAdminSectionHero = () => { if ( status === 'on' ) { return standaloneMode ? __( 'Standalone mode', 'jetpack-protect' ) - : __( 'Active', 'jetpack-protect', 0 ); + : __( + 'Active', + 'jetpack-protect', + // @ts-expect-error TS2554 - dummy arg to avoid bad minification + 0 + ); } return __( 'Inactive', 'jetpack-protect' ); @@ -46,7 +52,8 @@ const FirewallAdminSectionHero = () => { : __( 'Firewall is on', 'jetpack-protect', - /* dummy arg to avoid bad minification */ 0 + // @ts-expect-error TS2554 - dummy arg to avoid bad minification + 0 ) ) } ); @@ -62,7 +69,8 @@ const FirewallAdminSectionHero = () => { : __( 'Firewall is off', 'jetpack-protect', - /* dummy arg to avoid bad minification */ 0 + // @ts-expect-error TS2554 - dummy arg to avoid bad minification + 0 ) ) } ); @@ -84,16 +92,22 @@ const FirewallAdminSectionHero = () => { }, [ status ] ); return ( - - - { heading } - { subheading } - - } - secondary={ wafSupported && } - /> + + + + { heading } + { subheading } + + { wafSupported && ( + + + + ) } + ); }; diff --git a/projects/plugins/protect/src/js/routes/firewall/firewall-footer.jsx b/projects/plugins/protect/src/js/routes/firewall/firewall-footer.jsx index 0e28d7bae7c98..0c175b1cd651f 100644 --- a/projects/plugins/protect/src/js/routes/firewall/firewall-footer.jsx +++ b/projects/plugins/protect/src/js/routes/firewall/firewall-footer.jsx @@ -69,7 +69,6 @@ const ShareData = () => {
    { __( 'Share data with Jetpack', 'jetpack-protect' ) } { ) } /> { - const { hasPlan } = usePlan(); const { config: { bruteForceProtection: isBruteForceModuleEnabled }, isEnabled: isWafModuleEnabled, @@ -22,26 +20,22 @@ const FirewallStatCards = () => { const { currentDay: currentDayBlockCount, thirtyDays: thirtyDayBlockCounts } = stats ? stats.blockedRequests : { currentDay: 0, thirtyDays: 0 }; - const isFeatureDisabled = ! isSupportedWafFeatureEnabled || ! hasPlan; const defaultArgs = useMemo( () => ( { - className: isFeatureDisabled ? styles.disabled : styles.active, + className: ! isSupportedWafFeatureEnabled ? styles.disabled : styles.active, variant: isSmall ? 'horizontal' : 'square', } ), - [ isFeatureDisabled, isSmall ] + [ isSupportedWafFeatureEnabled, isSmall ] ); const StatCardIcon = useCallback( ( { icon } ) => ( - { ! isSmall && ! hasPlan && ( - { __( 'Paid feature', 'jetpack-protect' ) } - ) } ), - [ isSmall, hasPlan ] + [] ); const StatCardLabel = useCallback( @@ -77,9 +71,9 @@ const FirewallStatCards = () => { ...defaultArgs, icon: , label: , - value: isFeatureDisabled ? 0 : currentDayBlockCount, + value: ! isSupportedWafFeatureEnabled ? 0 : currentDayBlockCount, } ), - [ defaultArgs, StatCardIcon, StatCardLabel, isFeatureDisabled, currentDayBlockCount ] + [ defaultArgs, StatCardIcon, StatCardLabel, isSupportedWafFeatureEnabled, currentDayBlockCount ] ); const thirtyDaysArgs = useMemo( @@ -87,13 +81,13 @@ const FirewallStatCards = () => { ...defaultArgs, icon: , label: , - value: isFeatureDisabled ? 0 : thirtyDayBlockCounts, + value: ! isSupportedWafFeatureEnabled ? 0 : thirtyDayBlockCounts, } ), - [ defaultArgs, StatCardIcon, StatCardLabel, isFeatureDisabled, thirtyDayBlockCounts ] + [ defaultArgs, StatCardIcon, StatCardLabel, isSupportedWafFeatureEnabled, thirtyDayBlockCounts ] ); return ( -
    +
    diff --git a/projects/plugins/protect/src/js/routes/firewall/index.jsx b/projects/plugins/protect/src/js/routes/firewall/index.jsx index a88d0864a7c24..7673cd70360d1 100644 --- a/projects/plugins/protect/src/js/routes/firewall/index.jsx +++ b/projects/plugins/protect/src/js/routes/firewall/index.jsx @@ -21,7 +21,6 @@ import useWafUpgradeSeenMutation from '../../data/waf/use-waf-upgrade-seen-mutat import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; import usePlan from '../../hooks/use-plan'; import useWafData from '../../hooks/use-waf-data'; -import ScanFooter from '../scan/scan-footer'; import FirewallAdminSectionHero from './firewall-admin-section-hero'; import FirewallFooter from './firewall-footer'; import styles from './styles.module.scss'; @@ -562,7 +561,7 @@ const FirewallPage = () => {
    - { wafSupported ? : } + { wafSupported && } ); }; diff --git a/projects/plugins/protect/src/js/routes/firewall/styles.module.scss b/projects/plugins/protect/src/js/routes/firewall/styles.module.scss index afcbc2ad69b30..3034aba82feb1 100644 --- a/projects/plugins/protect/src/js/routes/firewall/styles.module.scss +++ b/projects/plugins/protect/src/js/routes/firewall/styles.module.scss @@ -3,6 +3,10 @@ max-width: calc( 744px + ( var( --spacing-base ) * 6 ) ); // 744px + 48px (desired inner width + horizontal padding) } +.status { + margin-bottom: calc( var( --spacing-base ) * 2 ); // 16px +} + .toggle-section { display: flex; @@ -145,14 +149,10 @@ align-items: center; } -.stat-card-wrapper { +.stat-cards-wrapper { display: flex; - margin-left: auto; - flex-wrap: wrap; - - >:first-child { - margin-right: calc( var( --spacing-base ) * 3 ); // 24px - } + justify-content: flex-end; + gap: calc( var( --spacing-base ) * 3 ); // 24px .disabled { opacity: 0.5; @@ -171,36 +171,6 @@ white-space: nowrap; } -@media ( max-width: 1115px ) { - .stat-card-wrapper { - margin-top: calc( var( --spacing-base ) * 3 ); // 24px - } -} - -@media ( max-width: 599px ) { - .stat-card-wrapper { - margin-top: calc( var( --spacing-base ) * 3 ); // 24px - - >:first-child { - margin-right: 0; - margin-bottom: var( --spacing-base ); // 8px - } - } - - .stat-card-icon { - margin-bottom: 0; - } -} - -.share-data-section { - display: flex; - - .share-data-toggle { - margin-top: calc( var( --spacing-base ) / 2 ); // 4px - margin-right: var( --spacing-base ); // 8px - } -} - .icon-tooltip { max-height: 20px; margin-left: calc( var( --spacing-base ) / 2 ); // 4px @@ -220,6 +190,23 @@ background-color: var( --jp-white-off ); } +@media ( max-width: 1200px ) { + .stat-cards-wrapper { + justify-content: flex-start; + } +} + +@media ( max-width: 599px ) { + .stat-cards-wrapper { + flex-direction: column; + gap: var( --spacing-base ); // 8px + } + + .stat-card-icon { + margin-bottom: 0; + } +} + .standalone-mode, .share-data { display: flex; flex-direction: column; diff --git a/projects/plugins/protect/src/js/routes/home/home-admin-section-hero.tsx b/projects/plugins/protect/src/js/routes/home/home-admin-section-hero.tsx new file mode 100644 index 0000000000000..06a6f6ddf9219 --- /dev/null +++ b/projects/plugins/protect/src/js/routes/home/home-admin-section-hero.tsx @@ -0,0 +1,54 @@ +import { Text, Button } from '@automattic/jetpack-components'; +import { __ } from '@wordpress/i18n'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import AdminSectionHero from '../../components/admin-section-hero'; +import usePlan from '../../hooks/use-plan'; +import HomeStatCards from './home-statcards'; +import styles from './styles.module.scss'; + +const HomeAdminSectionHero: React.FC = () => { + const { hasPlan } = usePlan(); + const navigate = useNavigate(); + const handleScanReportClick = useCallback( () => { + navigate( '/scan' ); + }, [ navigate ] ); + + return ( + + + <> + + { __( 'Your site is safe with us', 'jetpack-protect' ) } + + + { hasPlan + ? __( + 'We stay ahead of security threats to keep your site protected.', + 'jetpack-protect' + ) + : __( + 'We stay ahead of security vulnerabilities to keep your site protected.', + 'jetpack-protect', + // @ts-expect-error TS2554 - dummy arg to avoid bad minification + 0 + ) } + + + + + + + + + ); +}; + +export default HomeAdminSectionHero; diff --git a/projects/plugins/protect/src/js/routes/home/home-statcards.jsx b/projects/plugins/protect/src/js/routes/home/home-statcards.jsx new file mode 100644 index 0000000000000..517255f085898 --- /dev/null +++ b/projects/plugins/protect/src/js/routes/home/home-statcards.jsx @@ -0,0 +1,269 @@ +import { Text, useBreakpointMatch, StatCard } from '@automattic/jetpack-components'; +import { ShieldIcon } from '@automattic/jetpack-scan'; +import { Spinner, Tooltip } from '@wordpress/components'; +import { dateI18n } from '@wordpress/date'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import { useMemo } from 'react'; +import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query'; +import usePlan from '../../hooks/use-plan'; +import useWafData from '../../hooks/use-waf-data'; +import styles from './styles.module.scss'; + +const IconWithLabel = ( { label, isSmall, icon } ) => ( + + { icon } + { ! isSmall && ( + + { label } + + ) } + +); + +const HomeStatCard = ( { text, args } ) => ( + +
    + +
    +
    +); + +const HomeStatCards = () => { + const ICON_HEIGHT = 20; + + const { hasPlan } = usePlan(); + const [ isSmall ] = useBreakpointMatch( [ 'sm', 'lg' ], [ null, '<' ] ); + + const { data: status } = useScanStatusQuery(); + const scanning = isScanInProgress( status ); + const numThreats = status.threats.length; + const scanError = status.error; + + let lastCheckedLocalTimestamp = null; + if ( status.lastChecked ) { + // Convert the lastChecked UTC date to a local timestamp + lastCheckedLocalTimestamp = dateI18n( + 'F jS g:i A', + new Date( status.lastChecked + ' UTC' ).getTime(), + false + ); + } + + const { + config: { bruteForceProtection: isBruteForceModuleEnabled }, + isEnabled: isWafModuleEnabled, + wafSupported, + stats, + } = useWafData(); + + const { + blockedRequests: { allTime: allTimeBlockedRequestsCount = 0 } = {}, + blockedLogins: allTimeBlockedLoginsCount = 0, + } = stats || {}; + + const variant = useMemo( () => ( isSmall ? 'horizontal' : 'square' ), [ isSmall ] ); + + const lastCheckedMessage = useMemo( () => { + if ( scanning ) { + return __( 'Your results will be ready soon.', 'jetpack-protect' ); + } + + if ( scanError ) { + return __( + 'Please check your connection or try scanning again in a few minutes.', + 'jetpack-protect' + ); + } + + if ( lastCheckedLocalTimestamp ) { + if ( numThreats > 0 ) { + if ( hasPlan ) { + return sprintf( + // translators: %1$s: date/time, %2$d: number + _n( + 'Last checked on %1$s: We found %2$d threat.', + 'Last checked on %1$s: We found %2$d threats.', + numThreats, + 'jetpack-protect' + ), + lastCheckedLocalTimestamp, + numThreats + ); + } + return sprintf( + // translators: %1$s: date/time, %2$d: number + _n( + 'Last checked on %1$s: We found %2$d vulnerability.', + 'Last checked on %1$s: We found %2$d vulnerabilities.', + numThreats, + 'jetpack-protect' + ), + lastCheckedLocalTimestamp, + numThreats + ); + } + return sprintf( + // translators: %s: date/time + __( 'Last checked on %s: Your site is secure.', 'jetpack-protect' ), + lastCheckedLocalTimestamp + ); + } + if ( hasPlan ) { + return sprintf( + // translators: %d: number + _n( + 'Last scan we found %d threat.', + 'Last scan we found %d threats.', + numThreats, + 'jetpack-protect' + ), + numThreats + ); + } + return sprintf( + // translators: %d: number + _n( + 'Last scan we found %2$d vulnerability.', + 'Last scan we found %2$d vulnerabilities.', + numThreats, + 'jetpack-protect' + ), + numThreats + ); + }, [ scanError, scanning, numThreats, lastCheckedLocalTimestamp, hasPlan ] ); + + const scanArgs = useMemo( () => { + let scanIcon; + if ( scanning ) { + scanIcon = ; + } else if ( scanError ) { + scanIcon = ; + } else { + scanIcon = ; + } + + let scanLabel; + if ( scanning ) { + scanLabel = __( 'One moment, please…', 'jetpack-protect' ); + } else if ( scanError ) { + scanLabel = __( 'An error occurred', 'jetpack-protect' ); + } else if ( hasPlan ) { + scanLabel = _n( 'Threat identified', 'Threats identified', numThreats, 'jetpack-protect' ); + } else { + scanLabel = _n( + 'Vulnerability identified', + 'Vulnerabilities identified', + numThreats, + 'jetpack-protect' + ); + } + + return { + variant, + icon: ( + + ), + label: { scanLabel }, + value: numThreats, + hideValue: !! ( scanError || scanning ), + }; + }, [ variant, scanning, ICON_HEIGHT, scanError, numThreats, hasPlan, isSmall ] ); + + const wafArgs = useMemo( + () => ( { + variant: variant, + className: isWafModuleEnabled ? styles.active : styles.disabled, + icon: ( + + + { ! isSmall && ( + + { __( 'Firewall', 'jetpack-protect' ) } + + ) } + + ), + label: ( + + { __( 'Blocked requests', 'jetpack-protect' ) } + + ), + value: allTimeBlockedRequestsCount, + hideValue: ! isWafModuleEnabled, + } ), + [ variant, isWafModuleEnabled, ICON_HEIGHT, isSmall, allTimeBlockedRequestsCount ] + ); + + const bruteForceArgs = useMemo( + () => ( { + variant: variant, + className: isBruteForceModuleEnabled ? styles.active : styles.disabled, + icon: ( + + + { ! isSmall && ( + + { __( 'Brute force', 'jetpack-protect' ) } + + ) } + + ), + label: ( + + { __( 'Blocked login attempts', 'jetpack-protect' ) } + + ), + value: allTimeBlockedLoginsCount, + hideValue: ! isBruteForceModuleEnabled, + } ), + [ variant, isBruteForceModuleEnabled, ICON_HEIGHT, isSmall, allTimeBlockedLoginsCount ] + ); + + return ( +
    + + { wafSupported && ( + + ) } + +
    + ); +}; + +export default HomeStatCards; diff --git a/projects/plugins/protect/src/js/routes/home/index.jsx b/projects/plugins/protect/src/js/routes/home/index.jsx new file mode 100644 index 0000000000000..f5e46971bfa7d --- /dev/null +++ b/projects/plugins/protect/src/js/routes/home/index.jsx @@ -0,0 +1,59 @@ +import { AdminSection, Container, Col } from '@automattic/jetpack-components'; +import { ScanReport } from '@automattic/jetpack-scan'; +import { useMemo } from 'react'; +import AdminPage from '../../components/admin-page'; +import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query'; +import HomeAdminSectionHero from './home-admin-section-hero'; +import styles from './styles.module.scss'; + +/** + * Home Page + * + * The entry point for the Home page. + * + * @return {Component} The root component for the scan page. + */ +const HomePage = () => { + const { data: status } = useScanStatusQuery( { usePolling: true } ); + + const data = useMemo( + () => [ + ...( Object.keys( status.core || {} ).length ? [ status.core ] : [] ), + ...status.plugins, + ...status.themes, + ...( status.dataSource === 'scan_api' + ? [ + { + checked: !! status.lastChecked, + threats: status.files, + type: 'files', + }, + ] + : [] ), + ], + [ status ] + ); + + const showReport = ! isScanInProgress( status ); + + return ( + + + { showReport && ( + + + + + + + + ) } + + ); +}; + +export default HomePage; diff --git a/projects/plugins/protect/src/js/routes/home/styles.module.scss b/projects/plugins/protect/src/js/routes/home/styles.module.scss new file mode 100644 index 0000000000000..3a7bd7d064d21 --- /dev/null +++ b/projects/plugins/protect/src/js/routes/home/styles.module.scss @@ -0,0 +1,71 @@ +.scan-report-container { + padding-left: 0; + padding-right: 0; + overflow: hidden; + + > * { + margin-left: -24px; + margin-right: -24px; + } +} + +.view-scan-report { + margin-top: calc( var( --spacing-base ) * 4 ); // 32px +} + +.stat-cards-wrapper { + display: flex; + justify-content: flex-start; + gap: calc( var( --spacing-base ) * 3 ); // 24px + + .disabled { + opacity: 0.5; + } +} + +.stat-card-icon { + width: 100%; + margin-bottom: calc( var( --spacing-base ) * 3 ); // 24px + display: flex; + align-items: center; + gap: 8px; + + svg { + margin: 0; + } + + .active { + fill: var( --jp-green-40 ); + } + + .warning { + fill: var( --jp-yellow-40 ); + } + + .disabled { + fill: var( --jp-gray-40 ); + } + + &-label { + color: var( --jp-black ); + white-space: nowrap; + } +} + +.stat-card-tooltip { + margin-top: 8px; + max-width: 240px; + border-radius: 4px; + text-align: left; +} + +@media ( max-width: 599px ) { + .stat-cards-wrapper { + flex-direction: column; + gap: var( --spacing-base ); // 8px + } + + .stat-card-icon { + margin-bottom: 0; + } +} \ No newline at end of file diff --git a/projects/plugins/protect/src/js/routes/scan/context-provider.tsx b/projects/plugins/protect/src/js/routes/scan/context-provider.tsx new file mode 100644 index 0000000000000..745ee40d3076e --- /dev/null +++ b/projects/plugins/protect/src/js/routes/scan/context-provider.tsx @@ -0,0 +1,125 @@ +import { getRedirectUrl } from '@automattic/jetpack-components'; +import { useConnection } from '@automattic/jetpack-connection'; +import { + Threat, + THREAT_ACTION_FIX, + THREAT_ACTION_IGNORE, + THREAT_ACTION_UNIGNORE, + ThreatsContextProvider, +} from '@automattic/jetpack-scan'; +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useEffect, useState } from 'react'; +import { QUERY_CREDENTIALS_KEY } from '../../constants'; +import useIgnoreThreatMutation from '../../data/scan/use-ignore-threat-mutation'; +import useUnIgnoreThreatMutation from '../../data/scan/use-unignore-threat-mutation'; +import useCredentialsQuery from '../../data/use-credentials-query'; +import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; +import useFixers from '../../hooks/use-fixers'; +import usePlan from '../../hooks/use-plan'; +import useWafData from '../../hooks/use-waf-data'; + +/** + * Scan Context Provider + * + * @param { object } props - Component props. + * @param { React.ReactNode } props.children - Component children. + * + * @return { JSX.Element } ScanContextProvider component. + */ +export default function ScanContextProvider( { children } ) { + const { siteSuffix, blogID } = window.jetpackProtectInitialState; + const queryClient = useQueryClient(); + + const { upgradePlan, hasPlan } = usePlan(); + const { wafSupported } = useWafData(); + const { recordEvent } = useAnalyticsTracks(); + const { fixersStatus, fixThreats } = useFixers(); + const ignoreThreatMutation = useIgnoreThreatMutation(); + const unignoreThreatMutation = useUnIgnoreThreatMutation(); + + const { data: credentials, isLoading: credentialsIsFetching } = useCredentialsQuery(); + + const { isUserConnected, hasConnectedOwner, userIsConnecting, handleConnectUser } = useConnection( + { + redirectUri: 'admin.php?page=jetpack-protect', + from: 'scan', + autoTrigger: false, + skipUserConnection: false, + skipPricingPage: true, + } + ); + + const actionCallbacks = { + [ THREAT_ACTION_FIX ]: ( + threats: Threat[], + { onActionPerformed }: { onActionPerformed: ( items: Threat[] ) => void } + ) => { + fixThreats( [ threats[ 0 ].id ] ); + onActionPerformed?.( threats ); + }, + [ THREAT_ACTION_IGNORE ]: ( + threats: Threat[], + { onActionPerformed }: { onActionPerformed: ( items: Threat[] ) => void } + ) => { + ignoreThreatMutation.mutateAsync( threats[ 0 ].id ); + onActionPerformed?.( threats ); + }, + [ THREAT_ACTION_UNIGNORE ]: ( + threats: Threat[], + { onActionPerformed }: { onActionPerformed: ( items: Threat[] ) => void } + ) => { + unignoreThreatMutation.mutateAsync( threats[ 0 ].id ); + onActionPerformed?.( threats ); + }, + }; + + const [ pollCredentials, setPollCredentials ] = useState( false ); + + const onUpgradePlan = useCallback( () => { + recordEvent( 'jetpack_protect_threat_modal_get_scan_link_click' ); + upgradePlan(); + }, [ recordEvent, upgradePlan ] ); + + /** + * Poll credentials as long as the modal is open. + */ + useEffect( () => { + const interval = setInterval( () => { + if ( ! pollCredentials ) { + return; + } + + if ( credentials && credentials.length > 0 ) { + setPollCredentials( false ); + } + + queryClient.invalidateQueries( { queryKey: [ QUERY_CREDENTIALS_KEY ] } ); + }, 5_000 ); + + return () => clearInterval( interval ); + }, [ queryClient, credentials, pollCredentials ] ); + + return ( + 0, + fetching: credentialsIsFetching, + redirectUrl: getRedirectUrl( 'jetpack-settings-security-credentials', { + site: String( blogID ?? siteSuffix ), + } ), + startPolling: () => setPollCredentials( true ), + stopPolling: () => setPollCredentials( false ), + } } + connection={ { + connected: isUserConnected && hasConnectedOwner, + connecting: userIsConnecting, + connect: handleConnectUser, + } } + upgradePlan={ ! hasPlan ? onUpgradePlan : undefined } + referToCodeable={ wafSupported } + fixersStatus={ fixersStatus } + children={ children } + /> + ); +} diff --git a/projects/plugins/protect/src/js/routes/scan/history/history-admin-section-hero.tsx b/projects/plugins/protect/src/js/routes/scan/history/history-admin-section-hero.tsx index 9c8f30b7b8067..8b760e91aa757 100644 --- a/projects/plugins/protect/src/js/routes/scan/history/history-admin-section-hero.tsx +++ b/projects/plugins/protect/src/js/routes/scan/history/history-admin-section-hero.tsx @@ -1,85 +1,80 @@ -import { Status, Text } from '@automattic/jetpack-components'; +import { Text } from '@automattic/jetpack-components'; import { dateI18n } from '@wordpress/date'; import { __, sprintf } from '@wordpress/i18n'; +import clsx from 'clsx'; import { useMemo } from 'react'; -import { useParams } from 'react-router-dom'; import AdminSectionHero from '../../../components/admin-section-hero'; import ErrorAdminSectionHero from '../../../components/error-admin-section-hero'; -import ScanNavigation from '../../../components/scan-navigation'; -import useThreatsList from '../../../components/threats-list/use-threats-list'; -import useProtectData from '../../../hooks/use-protect-data'; +import useHistoryQuery from '../../../data/scan/use-history-query'; import styles from './styles.module.scss'; -const HistoryAdminSectionHero: React.FC = () => { - const { filter = 'all' } = useParams(); - const { list } = useThreatsList( { - source: 'history', - status: filter, - } ); - const { counts, error } = useProtectData( { - sourceType: 'history', - filter: { status: filter }, - } ); - const { threats: numAllThreats } = counts.all; +const HistoryAdminSectionHero: React.FC = ( { + size = 'normal', +}: { + size?: 'normal' | 'large'; +} ) => { + const { data: history } = useHistoryQuery(); + const numThreats = history ? history.threats.length : 0; const oldestFirstDetected = useMemo( () => { - if ( ! list.length ) { + if ( ! history || ! history.threats.length ) { return null; } - return list.reduce( ( oldest, current ) => { + return history.threats.reduce( ( oldest, current ) => { return new Date( current.firstDetected ) < new Date( oldest.firstDetected ) ? current : oldest; } ).firstDetected; - }, [ list ] ); + }, [ history ] ); - if ( error ) { + if ( history && history.error ) { return ( ); } return ( - - - - { numAllThreats > 0 - ? sprintf( - /* translators: %s: Total number of threats */ - __( '%1$s previously active %2$s', 'jetpack-protect' ), - numAllThreats, - numAllThreats === 1 ? 'threat' : 'threats' - ) - : __( 'No previously active threats', 'jetpack-protect' ) } - - - - { oldestFirstDetected ? ( - - { sprintf( - /* translators: %s: Oldest first detected date */ - __( '%s - Today', 'jetpack-protect' ), - dateI18n( 'F jS g:i A', oldestFirstDetected, false ) - ) } - - ) : ( - __( 'Most recent results', 'jetpack-protect' ) + + + + { oldestFirstDetected ? ( + + { sprintf( + /* translators: %s: Oldest first detected date */ + __( '%s – Today', 'jetpack-protect' ), + dateI18n( 'F jS g:i A', oldestFirstDetected, false ) ) } - - -
    - -
    - - } - /> + + ) : ( + __( 'Most recent results', 'jetpack-protect' ) + ) } + + 0 ? 'default' : 'success' } + iconOutline={ numThreats > 0 } + mb={ 2 } + > + { numThreats > 0 + ? sprintf( + /* translators: %s: Total number of threats */ + __( '%1$s previously active %2$s', 'jetpack-protect' ), + numThreats, + numThreats === 1 ? 'threat' : 'threats' + ) + : __( 'No previously active threats', 'jetpack-protect' ) } + + { __( 'Here you can view all of your threats to date.', 'jetpack-protect' ) } + +
    ); }; diff --git a/projects/plugins/protect/src/js/routes/scan/history/history-data-views.tsx b/projects/plugins/protect/src/js/routes/scan/history/history-data-views.tsx new file mode 100644 index 0000000000000..31ea5387f9f18 --- /dev/null +++ b/projects/plugins/protect/src/js/routes/scan/history/history-data-views.tsx @@ -0,0 +1,43 @@ +import { + HISTORIC_TABLE_FIELDS, + THREAT_FIELD_AUTO_FIX, + ThreatsDataViews, +} from '@automattic/jetpack-scan'; +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import useHistoryQuery from '../../../data/scan/use-history-query'; +import ScanToggleGroupControl from '../scan-toggle-group-control'; + +/** + * Scan History Data Views + * + * @return {JSX.Element} HistoryDataViews component. + */ +export default function HistoryDataViews(): JSX.Element { + const { filter } = useParams(); + const { data: history } = useHistoryQuery(); + + const filters = useMemo( () => { + if ( filter ) { + return [ + { + field: 'status', + value: filter, + operator: 'isAny' as const, + }, + ]; + } + + return []; + }, [ filter ] ); + + return ( + } + /> + ); +} diff --git a/projects/plugins/protect/src/js/routes/scan/history/index.jsx b/projects/plugins/protect/src/js/routes/scan/history/index.jsx index 723f9de9ab230..2afa10ef4ab9a 100644 --- a/projects/plugins/protect/src/js/routes/scan/history/index.jsx +++ b/projects/plugins/protect/src/js/routes/scan/history/index.jsx @@ -1,301 +1,47 @@ -import { AdminSection, Container, Col, H3, Text, Title } from '@automattic/jetpack-components'; -import { __, _n, sprintf } from '@wordpress/i18n'; -import { useCallback } from 'react'; -import { Navigate, useParams } from 'react-router-dom'; +import { AdminSection, Container, Col } from '@automattic/jetpack-components'; import AdminPage from '../../../components/admin-page'; -import ProtectCheck from '../../../components/protect-check-icon'; -import ThreatsNavigation from '../../../components/threats-list/navigation'; -import PaidList from '../../../components/threats-list/paid-list'; -import useThreatsList from '../../../components/threats-list/use-threats-list'; -import useAnalyticsTracks from '../../../hooks/use-analytics-tracks'; -import usePlan from '../../../hooks/use-plan'; -import useProtectData from '../../../hooks/use-protect-data'; -import ScanFooter from '../scan-footer'; +import useHistoryQuery from '../../../data/scan/use-history-query'; +import useScanStatusQuery from '../../../data/scan/use-scan-status-query'; +import ScanContextProvider from '../context-provider'; import HistoryAdminSectionHero from './history-admin-section-hero'; -import StatusFilters from './status-filters'; +import HistoryDataViews from './history-data-views'; import styles from './styles.module.scss'; -const ScanHistoryRoute = () => { - // Track page view. - useAnalyticsTracks( { pageViewEventName: 'protect_scan_history' } ); - - const { hasPlan } = usePlan(); - const { filter = 'all' } = useParams(); - - const { item, list, selected, setSelected } = useThreatsList( { - source: 'history', - status: filter, - } ); - - const { counts, error } = useProtectData( { - sourceType: 'history', - filter: { status: filter }, - } ); - const { threats: numAllThreats } = counts.all; - - const { counts: fixedCounts } = useProtectData( { - sourceType: 'history', - filter: { status: 'fixed', key: selected }, - } ); - const { threats: numFixed } = fixedCounts.current; - - const { counts: ignoredCounts } = useProtectData( { - sourceType: 'history', - filter: { status: 'ignored', key: selected }, - } ); - const { threats: numIgnored } = ignoredCounts.current; - - /** - * Get the title for the threats list based on the selected filters and the amount of threats. - */ - const getTitle = useCallback( () => { - switch ( selected ) { - case 'all': - if ( list.length === 1 ) { - switch ( filter ) { - case 'fixed': - return __( 'All fixed threats', 'jetpack-protect' ); - case 'ignored': - return __( - 'All ignored threats', - 'jetpack-protect', - /** dummy arg to avoid bad minification */ 0 - ); - default: - return __( 'All threats', 'jetpack-protect' ); - } - } - switch ( filter ) { - case 'fixed': - return sprintf( - /* translators: placeholder is the amount of fixed threats found on the site. */ - __( 'All %s fixed threats', 'jetpack-protect' ), - list.length - ); - case 'ignored': - return sprintf( - /* translators: placeholder is the amount of ignored threats found on the site. */ - __( 'All %s ignored threats', 'jetpack-protect' ), - list.length - ); - default: - return sprintf( - /* translators: placeholder is the amount of threats found on the site. */ - __( 'All %s threats', 'jetpack-protect' ), - list.length - ); - } - case 'core': - switch ( filter ) { - case 'fixed': - return sprintf( - /* translators: placeholder is the amount of fixed WordPress threats found on the site. */ - _n( - '%1$s fixed WordPress threat', - '%1$s fixed WordPress threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - case 'ignored': - return sprintf( - /* translators: placeholder is the amount of ignored WordPress threats found on the site. */ - _n( - '%1$s ignored WordPress threat', - '%1$s ignored WordPress threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - default: - return sprintf( - /* translators: placeholder is the amount of WordPress threats found on the site. */ - _n( - '%1$s WordPress threat', - '%1$s WordPress threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - } - case 'files': - switch ( filter ) { - case 'fixed': - return sprintf( - /* translators: placeholder is the amount of fixed file threats found on the site. */ - _n( - '%1$s fixed file threat', - '%1$s fixed file threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - case 'ignored': - return sprintf( - /* translators: placeholder is the amount of ignored file threats found on the site. */ - _n( - '%1$s ignored file threat', - '%1$s ignored file threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - default: - return sprintf( - /* translators: placeholder is the amount of file threats found on the site. */ - _n( '%1$s file threat', '%1$s file threats', list.length, 'jetpack-protect' ), - list.length - ); - } - case 'database': - switch ( filter ) { - case 'fixed': - return sprintf( - /* translators: placeholder is the amount of fixed database threats found on the site. */ - _n( - '%1$s fixed database threat', - '%1$s fixed database threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - case 'ignored': - return sprintf( - /* translators: placeholder is the amount of ignored database threats found on the site. */ - _n( - '%1$s ignored database threat', - '%1$s ignored database threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - default: - return sprintf( - /* translators: placeholder is the amount of database threats found on the site. */ - _n( '%1$s database threat', '%1$s database threats', list.length, 'jetpack-protect' ), - list.length - ); - } - default: - switch ( filter ) { - case 'fixed': - return sprintf( - /* translators: Translates to "123 fixed threats in Example Plugin (1.2.3)" */ - _n( - '%1$s fixed threat in %2$s %3$s', - '%1$s fixed threats in %2$s %3$s', - list.length, - 'jetpack-protect' - ), - list.length, - item?.name, - item?.version - ); - case 'ignored': - return sprintf( - /* translators: Translates to "123 ignored threats in Example Plugin (1.2.3)" */ - _n( - '%1$s ignored threat in %2$s %3$s', - '%1$s ignored threats in %2$s %3$s', - list.length, - 'jetpack-protect' - ), - list.length, - item?.name, - item?.version - ); - default: - return sprintf( - /* translators: Translates to "123 threats in Example Plugin (1.2.3)" */ - _n( - '%1$s threat in %2$s %3$s', - '%1$s threats in %2$s %3$s', - list.length, - 'jetpack-protect' - ), - list.length, - item?.name, - item?.version - ); - } - } - }, [ selected, list.length, filter, item?.name, item?.version ] ); - - // Threat history is only available for paid plans. - if ( ! hasPlan ) { - return ; - } - - // Remove the filter if there are no threats to show. - if ( list.length === 0 && filter !== 'all' ) { - return ; - } +/** + * History Page + * + * The entry point for the History page. + * + * @return {Component} The root component for the scan page. + */ +const HistoryPage = () => { + const { data: status } = useScanStatusQuery(); + const { data: history } = useHistoryQuery(); + + const hasActiveThreats = status && status.threats.length; + const hasHistory = history && history.threats.length; + const showResults = hasActiveThreats || hasHistory; return ( - - - { ( ! error || numAllThreats ) && ( - - - - - - - - - { list.length > 0 ? ( -
    -
    - { getTitle() } -
    - -
    -
    - -
    - ) : ( - <> -
    -
    - -
    -
    -
    - -

    - { __( "Don't worry about a thing", 'jetpack-protect' ) } -

    - - { sprintf( - /* translators: %s: Filter type */ - __( 'There are no%sthreats in your scan history.', 'jetpack-protect' ), - 'all' === filter ? ' ' : ` ${ filter } ` - ) } - -
    - - ) } - -
    - -
    -
    - ) } - -
    + + + + { showResults && ( + + + + + + + + ) } + + ); }; -export default ScanHistoryRoute; +export default HistoryPage; diff --git a/projects/plugins/protect/src/js/routes/scan/history/status-filters.jsx b/projects/plugins/protect/src/js/routes/scan/history/status-filters.jsx deleted file mode 100644 index 1bc9668b11065..0000000000000 --- a/projects/plugins/protect/src/js/routes/scan/history/status-filters.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import { __ } from '@wordpress/i18n'; -import React, { useCallback } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import ButtonGroup from '../../../components/button-group'; - -/** - * Status Filters component. - * - * @param {object} props - Component props. - * @param {number} props.numFixed - Number of fixed threats. - * @param {number} props.numIgnored - Number of ignored threats. - * - * @return {React.ReactNode} StatusFilters component. - */ -export default function StatusFilters( { numFixed, numIgnored } ) { - const navigate = useNavigate(); - const { filter = 'all' } = useParams(); - const navigateOnClick = useCallback( path => () => navigate( path ), [ navigate ] ); - - return ( - - - { __( 'All', 'jetpack-protect' ) } - - - { __( 'Fixed', 'jetpack-protect' ) } - - - { __( 'Ignored', 'jetpack-protect' ) } - - - ); -} diff --git a/projects/plugins/protect/src/js/routes/scan/history/styles.module.scss b/projects/plugins/protect/src/js/routes/scan/history/styles.module.scss index f66602e59a9e9..d26a18f627df1 100644 --- a/projects/plugins/protect/src/js/routes/scan/history/styles.module.scss +++ b/projects/plugins/protect/src/js/routes/scan/history/styles.module.scss @@ -1,45 +1,20 @@ -.empty { - display: flex; - width: 100%; - height: 100%; - align-items: center; - justify-content: center; - max-height: 600px; - flex-direction: column; +.hero-main { + max-width: 550px; } -.subheading-content { - font-weight: bold; +.hero-main--large { + flex: 1 auto; + align-content: center; + height: calc( 100vh - 408px ); } -.list-header { - display: flex; - justify-content: flex-end; - align-items: flex-end; - margin-bottom: calc( var( --spacing-base ) * 2.25 ); // 18px -} - -.list-title { - flex: 1; - margin-bottom: 0; -} - -.list-header__controls { - display: flex; - gap: calc( var( --spacing-base ) * 2 ); // 16px -} +.history-container { + padding-left: 0; + padding-right: 0; + overflow: hidden; -@media ( max-width: 599px ) { - - .list-header { - margin-bottom: calc( var( --spacing-base ) * 3 ); // 24px - } - - .list-title { - display: none; + > * { + margin-left: calc( var( --spacing-base ) * -3 ); // -24px + margin-right: calc( var( --spacing-base ) * -3 ); // -24px } -} - -.scan-navigation { - margin-top: calc( var( --spacing-base ) * 3 ); // 24px } \ No newline at end of file diff --git a/projects/plugins/protect/src/js/routes/scan/index.jsx b/projects/plugins/protect/src/js/routes/scan/index.jsx index 1f3cdfdd7520f..3f07f08bab769 100644 --- a/projects/plugins/protect/src/js/routes/scan/index.jsx +++ b/projects/plugins/protect/src/js/routes/scan/index.jsx @@ -1,14 +1,18 @@ import { AdminSection, Container, Col } from '@automattic/jetpack-components'; +import { ThreatsDataViews } from '@automattic/jetpack-scan'; +import { useState } from 'react'; import AdminPage from '../../components/admin-page'; -import ThreatsList from '../../components/threats-list'; -import useScanStatusQuery from '../../data/scan/use-scan-status-query'; +import OnboardingPopover from '../../components/onboarding-popover'; +import useHistoryQuery from '../../data/scan/use-history-query'; +import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; import { OnboardingContext } from '../../hooks/use-onboarding'; import usePlan from '../../hooks/use-plan'; -import useProtectData from '../../hooks/use-protect-data'; +import ScanContextProvider from './context-provider'; import onboardingSteps from './onboarding-steps'; import ScanAdminSectionHero from './scan-admin-section-hero'; -import ScanFooter from './scan-footer'; +import ScanToggleGroupControl from './scan-toggle-group-control'; +import styles from './styles.module.scss'; /** * Scan Page @@ -19,23 +23,24 @@ import ScanFooter from './scan-footer'; */ const ScanPage = () => { const { hasPlan } = usePlan(); - const { - counts: { - current: { threats: numThreats }, - }, - lastChecked, - } = useProtectData(); const { data: status } = useScanStatusQuery( { usePolling: true } ); + const { data: history } = useHistoryQuery(); + + const [ scanResultsAnchor, setScanResultsAnchor ] = useState( null ); let currentScanStatus; if ( status.error ) { currentScanStatus = 'error'; - } else if ( ! lastChecked ) { + } else if ( ! status.lastChecked ) { currentScanStatus = 'in_progress'; } else { currentScanStatus = 'active'; } + const hasActiveThreats = status && !! status.threats.length; + const hasHistory = history && !! history.threats.length; + const showResults = hasActiveThreats || hasHistory; + // Track view for Protect admin page. useAnalyticsTracks( { pageViewEventName: 'protect_admin', @@ -47,19 +52,43 @@ const ScanPage = () => { return ( - - - { ( ! status.error || numThreats ) && ( - - - - - - - - ) } - - + + + + { showResults && ( + + + +
    + } + /> +
    + { !! status && ! isScanInProgress( status ) && ( + + ) } + { !! status && ! isScanInProgress( status ) && hasPlan && ( + + ) } + +
    +
    + ) } +
    +
    ); }; diff --git a/projects/plugins/protect/src/js/routes/scan/onboarding-steps.jsx b/projects/plugins/protect/src/js/routes/scan/onboarding-steps.jsx index 0e85aa56d9289..c29af26bcb409 100644 --- a/projects/plugins/protect/src/js/routes/scan/onboarding-steps.jsx +++ b/projects/plugins/protect/src/js/routes/scan/onboarding-steps.jsx @@ -6,15 +6,6 @@ import usePlan from '../../hooks/use-plan'; const { siteSuffix } = window.jetpackProtectInitialState; -const scanResultsTitle = __( 'Your scan results', 'jetpack-protect' ); -const scanResultsDescription = ( - - { __( - 'Navigate through the results of the scan on your WordPress installation, plugins, themes, and other files', - 'jetpack-protect' - ) } - -); const UpgradeButton = props => { const { upgradePlan } = usePlan(); const { recordEvent } = useAnalyticsTracks(); @@ -27,11 +18,6 @@ const UpgradeButton = props => { }; export default [ - { - id: 'free-scan-results', - title: scanResultsTitle, - description: scanResultsDescription, - }, { id: 'free-daily-scans', title: __( 'Daily automated scans', 'jetpack-protect' ), @@ -49,10 +35,41 @@ export default [ ), }, + { + id: 'paid-daily-and-manual-scans', + title: __( 'Daily & manual scanning', 'jetpack-protect' ), + description: ( + + { __( + 'We run daily automated scans but you can also run on-demand scans if you want to check the latest status.', + 'jetpack-protect' + ) } + + ), + }, + { + id: 'free-scan-results', + title: __( 'Your scan results', 'jetpack-protect' ), + description: ( + + { __( + 'Navigate through the results of the scan on your WordPress installation, plugins, and themes.', + 'jetpack-protect' + ) } + + ), + }, { id: 'paid-scan-results', - title: scanResultsTitle, - description: scanResultsDescription, + title: __( 'Your scan results', 'jetpack-protect' ), + description: ( + + { __( + 'Navigate through the results of the scan on your WordPress installation, plugins, themes, and other files.', + 'jetpack-protect' + ) } + + ), }, { id: 'paid-fix-all-threats', @@ -97,16 +114,4 @@ export default [ ), }, - { - id: 'paid-daily-and-manual-scans', - title: __( 'Daily & manual scanning', 'jetpack-protect' ), - description: ( - - { __( - 'We run daily automated scans but you can also run on-demand scans if you want to check the latest status.', - 'jetpack-protect' - ) } - - ), - }, ]; diff --git a/projects/plugins/protect/src/js/routes/scan/scan-admin-section-hero.tsx b/projects/plugins/protect/src/js/routes/scan/scan-admin-section-hero.tsx index 60d484cd4a16f..099ecbd79de8d 100644 --- a/projects/plugins/protect/src/js/routes/scan/scan-admin-section-hero.tsx +++ b/projects/plugins/protect/src/js/routes/scan/scan-admin-section-hero.tsx @@ -1,39 +1,96 @@ -import { Text, Status, useBreakpointMatch } from '@automattic/jetpack-components'; +import { Text, Button, useBreakpointMatch } from '@automattic/jetpack-components'; +import { ThreatsContext } from '@automattic/jetpack-scan'; +import { Tooltip } from '@wordpress/components'; import { dateI18n } from '@wordpress/date'; import { __, _n, sprintf } from '@wordpress/i18n'; -import { useState } from 'react'; +import clsx from 'clsx'; +import { useCallback, useState, useMemo, useContext } from 'react'; import AdminSectionHero from '../../components/admin-section-hero'; import ErrorAdminSectionHero from '../../components/error-admin-section-hero'; import OnboardingPopover from '../../components/onboarding-popover'; -import ScanNavigation from '../../components/scan-navigation'; import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query'; +import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; +import useFixers from '../../hooks/use-fixers'; import usePlan from '../../hooks/use-plan'; -import useProtectData from '../../hooks/use-protect-data'; +import useWafData from '../../hooks/use-waf-data'; import ScanningAdminSectionHero from './scanning-admin-section-hero'; import styles from './styles.module.scss'; -const ScanAdminSectionHero: React.FC = () => { - const { hasPlan } = usePlan(); +const ScanAdminSectionHero: React.FC = ( { size = 'normal' }: { size?: 'normal' | 'large' } ) => { + const { recordEvent } = useAnalyticsTracks(); + const { hasPlan, upgradePlan } = usePlan(); const [ isSm ] = useBreakpointMatch( 'sm' ); - const { - counts: { - current: { threats: numThreats }, - }, - lastChecked, - } = useProtectData(); const { data: status } = useScanStatusQuery(); + const { isThreatFixInProgress, isThreatFixStale } = useFixers(); + + const { setActionToConfirm } = useContext( ThreatsContext ); + + const getScan = useCallback( () => { + recordEvent( 'jetpack_protect_scan_header_get_scan_link_click' ); + upgradePlan(); + }, [ recordEvent, upgradePlan ] ); + + const { globalStats } = useWafData(); + const totalVulnerabilities = parseInt( globalStats?.totalVulnerabilities ); + const totalVulnerabilitiesFormatted = isNaN( totalVulnerabilities ) + ? '50,000' + : totalVulnerabilities.toLocaleString(); + + const numThreats = status.threats.length; // Popover anchor const [ dailyScansPopoverAnchor, setDailyScansPopoverAnchor ] = useState( null ); + const [ showAutoFixersPopoverAnchor, setShowAutoFixersPopoverAnchor ] = useState( null ); + + // List of fixable threats that do not have a fix in progress + const fixableList = useMemo( () => { + return status.threats.filter( threat => { + const threatId = typeof threat.id === 'string' ? parseInt( threat.id ) : threat.id; + return ( + threat.fixable && ! isThreatFixInProgress( threatId ) && ! isThreatFixStale( threatId ) + ); + } ); + }, [ status.threats, isThreatFixInProgress, isThreatFixStale ] ); + + const scanning = isScanInProgress( status ); let lastCheckedLocalTimestamp = null; - if ( lastChecked ) { + if ( status.lastChecked ) { // Convert the lastChecked UTC date to a local timestamp - lastCheckedLocalTimestamp = new Date( lastChecked + ' UTC' ).getTime(); + lastCheckedLocalTimestamp = new Date( status.lastChecked + ' UTC' ).getTime(); + } + + let heading = __( "Don't worry about a thing", 'jetpack-protect' ); + if ( numThreats > 0 ) { + if ( hasPlan ) { + heading = sprintf( + /* translators: %s: Total number of threats */ + _n( '%1$s active threat', '%1$s active threats', numThreats, 'jetpack-protect' ), + numThreats + ); + } else { + heading = sprintf( + /* translators: %s: Total number of vulnerabilities */ + _n( + '%1$s active vulnerability', + '%1$s active vulnerabilities', + numThreats, + 'jetpack-protect' + ), + numThreats + ); + } } - if ( isScanInProgress( status ) ) { - return ; + const handleShowAutoFixersClick = threatList => { + return event => { + event.preventDefault(); + setActionToConfirm( { id: 'fix', items: threatList } ); + }; + }; + + if ( scanning ) { + return ; } if ( status.error ) { @@ -47,62 +104,87 @@ const ScanAdminSectionHero: React.FC = () => { } return ( - - - - { numThreats > 0 + <> + + + + { lastCheckedLocalTimestamp ? sprintf( - /* translators: %s: Total number of threats/vulnerabilities */ - __( '%1$s %2$s found', 'jetpack-protect' ), - numThreats, - hasPlan - ? _n( 'threat', 'threats', numThreats, 'jetpack-protect' ) - : _n( 'vulnerability', 'vulnerabilities', numThreats, 'jetpack-protect' ) + // translators: %s: date and time of the last scan + __( '%s results', 'jetpack-protect' ), + dateI18n( 'F jS, g:i A', lastCheckedLocalTimestamp, false ) ) - : sprintf( - /* translators: %s: Pluralized type of threat/vulnerability */ - __( 'No %s found', 'jetpack-protect' ), - hasPlan - ? __( 'threats', 'jetpack-protect' ) - : __( - 'vulnerabilities', - 'jetpack-protect', - /* dummy arg to avoid bad minification */ 0 - ) - ) } + : __( 'Most recent results', 'jetpack-protect' ) } + + + 0 ? 'error' : 'success' } mb={ 2 }> + { heading } - + { hasPlan ? ( + + { __( + "We actively review your site's files line-by-line to identify threats and vulnerabilities.", + 'jetpack-protect' + ) } + + ) : ( <> - - { lastCheckedLocalTimestamp ? ( - <> - - { dateI18n( 'F jS g:i A', lastCheckedLocalTimestamp, false ) } - -   - { __( 'results', 'jetpack-protect' ) } - - ) : ( - __( 'Most recent results', 'jetpack-protect' ) + + { sprintf( + // translators: placeholder is the number of total vulnerabilities i.e. "22,000". + __( + 'Every day we check your plugins, themes, and WordPress version against our %s listed vulnerabilities powered by WPScan, an Automattic brand.', + 'jetpack-protect' + ), + totalVulnerabilitiesFormatted ) } - { ! hasPlan && ( - - ) } + + + + + ) } + { fixableList.length > 0 && ( + <> +
    + +
    +
    -
    - -
    - - } - /> + ) } +
    +
    + ); }; diff --git a/projects/plugins/protect/src/js/routes/scan/scan-footer.jsx b/projects/plugins/protect/src/js/routes/scan/scan-footer.jsx deleted file mode 100644 index 56d7ea1c14a7d..0000000000000 --- a/projects/plugins/protect/src/js/routes/scan/scan-footer.jsx +++ /dev/null @@ -1,143 +0,0 @@ -import { - Text, - Button, - Title, - getRedirectUrl, - ContextualUpgradeTrigger, - Col, - Container, -} from '@automattic/jetpack-components'; -import { __, sprintf } from '@wordpress/i18n'; -import React, { useCallback } from 'react'; -import SeventyFiveLayout from '../../components/seventy-five-layout'; -import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; -import usePlan from '../../hooks/use-plan'; -import useWafData from '../../hooks/use-waf-data'; -import styles from './styles.module.scss'; - -const ProductPromotion = () => { - const { recordEvent } = useAnalyticsTracks(); - const { hasPlan, upgradePlan } = usePlan(); - const { siteSuffix, blogID } = window.jetpackProtectInitialState || {}; - - const getScan = useCallback( () => { - recordEvent( 'jetpack_protect_footer_get_scan_link_click' ); - upgradePlan(); - }, [ recordEvent, upgradePlan ] ); - - if ( hasPlan ) { - const goToCloudUrl = getRedirectUrl( 'jetpack-scan-dash', { site: blogID ?? siteSuffix } ); - - return ( -
    - { __( 'Get access to our Cloud', 'jetpack-protect' ) } - - { __( - 'With your Protect upgrade, you have free access to scan your site on our Cloud, so you can be aware and fix your threats even if your site goes down.', - 'jetpack-protect' - ) } - - - -
    - ); - } - - return ( -
    - { __( 'Advanced scan results', 'jetpack-protect' ) } - - { __( - 'Upgrade Jetpack Protect to get advanced scan tools, including one-click fixes for most threats and malware scanning.', - 'jetpack-protect' - ) } - - - -
    - ); -}; - -const FooterInfo = () => { - const { hasPlan } = usePlan(); - const { globalStats } = useWafData(); - const totalVulnerabilities = parseInt( globalStats?.totalVulnerabilities ); - const totalVulnerabilitiesFormatted = isNaN( totalVulnerabilities ) - ? '50,000' - : totalVulnerabilities.toLocaleString(); - - if ( hasPlan ) { - const learnMoreScanUrl = getRedirectUrl( 'protect-footer-learn-more-scan' ); - - return ( -
    - { __( 'Line-by-line scanning', 'jetpack-protect' ) } - - { __( - 'We actively review line-by-line of your site files to identify threats and vulnerabilities. Jetpack monitors millions of websites to keep your site secure all the time.', - 'jetpack-protect' - ) }{ ' ' } - - -
    - ); - } - - const learnMoreProtectUrl = getRedirectUrl( 'jetpack-protect-footer-learn-more' ); - - return ( -
    - - { sprintf( - // translators: placeholder is the number of total vulnerabilities i.e. "22,000". - __( 'Over %s listed vulnerabilities', 'jetpack-protect' ), - totalVulnerabilitiesFormatted - ) } - - - { sprintf( - // translators: placeholder is the number of total vulnerabilities i.e. "22,000". - __( - 'Every day we check your plugin, theme, and WordPress versions against our %s listed vulnerabilities powered by WPScan, an Automattic brand.', - 'jetpack-protect' - ), - totalVulnerabilitiesFormatted - ) } - - - -
    - ); -}; - -const ScanFooter = () => { - const { waf } = window.jetpackProtectInitialState || {}; - return waf.wafSupported ? ( - } - secondary={ } - preserveSecondaryOnMobile={ true } - /> - ) : ( - - - - - - ); -}; - -export default ScanFooter; diff --git a/projects/plugins/protect/src/js/routes/scan/scan-toggle-group-control.tsx b/projects/plugins/protect/src/js/routes/scan/scan-toggle-group-control.tsx new file mode 100644 index 0000000000000..9d08918d5993f --- /dev/null +++ b/projects/plugins/protect/src/js/routes/scan/scan-toggle-group-control.tsx @@ -0,0 +1,75 @@ +import { + __experimentalToggleGroupControl as ToggleGroupControl, // eslint-disable-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControlOption as ToggleGroupControlOption, // eslint-disable-line @wordpress/no-unsafe-wp-apis +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useCallback } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import useHistoryQuery from '../../data/scan/use-history-query'; +import useScanStatusQuery from '../../data/scan/use-scan-status-query'; +import styles from './styles.module.scss'; + +/** + * ToggleGroupControl component for filtering threats by status. + * + * @return {JSX.Element|null} The component or null. + */ +export default function ScanToggleGroupControl(): JSX.Element { + const location = useLocation(); + const navigate = useNavigate(); + const { data: status } = useScanStatusQuery(); + const { data: history } = useHistoryQuery(); + const numActiveThreats = status ? status.threats.length : 0; + const numHistoricThreats = history ? history.threats.length : 0; + + const selectedValue = location.pathname.includes( '/history' ) ? 'historic' : 'current'; + + const onChange = useCallback( + ( value: 'current' | 'historic' ) => { + navigate( value === 'current' ? '/scan' : '/scan/history' ); + }, + [ navigate ] + ); + + if ( ! numHistoricThreats ) { + return null; + } + + try { + return ( +
    +
    + + + + +
    +
    + ); + } catch { + return null; + } +} diff --git a/projects/plugins/protect/src/js/routes/scan/scanning-admin-section-hero.tsx b/projects/plugins/protect/src/js/routes/scan/scanning-admin-section-hero.tsx index c214c52ff8892..0131cb5771f56 100644 --- a/projects/plugins/protect/src/js/routes/scan/scanning-admin-section-hero.tsx +++ b/projects/plugins/protect/src/js/routes/scan/scanning-admin-section-hero.tsx @@ -1,15 +1,19 @@ import { Text } from '@automattic/jetpack-components'; import { __, sprintf } from '@wordpress/i18n'; +import clsx from 'clsx'; import AdminSectionHero from '../../components/admin-section-hero'; import InProgressAnimation from '../../components/in-progress-animation'; import ProgressBar from '../../components/progress-bar'; -import ScanNavigation from '../../components/scan-navigation'; import useScanStatusQuery from '../../data/scan/use-scan-status-query'; import usePlan from '../../hooks/use-plan'; import useWafData from '../../hooks/use-waf-data'; import styles from './styles.module.scss'; -const ScanningAdminSectionHero: React.FC = () => { +const ScanningAdminSectionHero = ( { + size = 'normal', +}: { + size?: 'normal' | 'large' | 'wide'; +} ) => { const { hasPlan } = usePlan(); const { globalStats } = useWafData(); const { data: status } = useScanStatusQuery( { usePolling: true } ); @@ -19,42 +23,43 @@ const ScanningAdminSectionHero: React.FC = () => { : totalVulnerabilities.toLocaleString(); return ( - - - { __( 'Your results will be ready soon', 'jetpack-protect' ) } - - - <> - { hasPlan && ( - - ) } - - { sprintf( - // translators: placeholder is the number of total vulnerabilities i.e. "22,000". - __( - 'We are scanning for security threats from our more than %s listed vulnerabilities, powered by WPScan. This could take a minute or two.', - 'jetpack-protect' - ), - totalVulnerabilitiesFormatted - ) } - - - -
    - -
    - - } - secondary={ } - preserveSecondaryOnMobile={ false } - spacing={ 4 } - /> + + + + { __( 'Your results will be ready soon', 'jetpack-protect' ) } + + { hasPlan && ( + + ) } + + { hasPlan + ? __( + "Jetpack is actively scanning your site's files line-by-line to identify threats and vulnerabilities. This could take a minute or two.", + 'jetpack-protect' + ) + : sprintf( + // translators: placeholder is the number of total vulnerabilities i.e. "22,000". + __( + 'We are scanning for security threats from our more than %s listed vulnerabilities, powered by WPScan. This could take a minute or two.', + 'jetpack-protect' + ), + totalVulnerabilitiesFormatted + ) } + + + + + + ); }; diff --git a/projects/plugins/protect/src/js/routes/scan/styles.module.scss b/projects/plugins/protect/src/js/routes/scan/styles.module.scss index 8651420159fa1..927ccd80e386d 100644 --- a/projects/plugins/protect/src/js/routes/scan/styles.module.scss +++ b/projects/plugins/protect/src/js/routes/scan/styles.module.scss @@ -1,12 +1,59 @@ -.subheading-content { - font-weight: bold; +.scan-results-container { + :global { + @media ( max-width: 599px ) { + div.dataviews-wrapper .dataviews__view-actions { + flex-wrap: wrap; + justify-content: center; + gap: calc( var( --spacing-base ) * 1.5 ); // 12px; + } + } + } } -.product-section, .info-section { - margin-top: calc( var( --spacing-base ) * 7 ); // 56px - margin-bottom: calc( var( --spacing-base ) * 7 ); // 56px +.hero-main--large { + flex: 1 auto; + align-content: center; + height: calc( 100vh - 408px ); } -.scan-navigation { - margin-top: calc( var( --spacing-base ) * 3 ); // 24px -} \ No newline at end of file +.auto-fixers { + margin-top: calc( var( --spacing-base ) * 4 ); // 32px + width: fit-content; +} + +.scan-results-container { + padding-left: 0; + padding-right: 0; + overflow: hidden; + + > * { + margin-left: -24px; + margin-right: -24px; + } +} + +.progress-animation { + @media (max-width: 1099px) { + display: none; + } +} + +.last-checked { + width: fit-content; +} + +.toggle-group-control { + min-width: 300px; +} + +.vulnerabilities-text { + max-width: 550px; +} + +.scanning-text { + max-width: 550px; +} + +.progress { + max-width: 575px; +} diff --git a/projects/plugins/protect/src/js/styles.module.scss b/projects/plugins/protect/src/js/styles.module.scss index 4d7ac08500360..f15232cf87cdd 100644 --- a/projects/plugins/protect/src/js/styles.module.scss +++ b/projects/plugins/protect/src/js/styles.module.scss @@ -3,7 +3,8 @@ box-sizing: border-box; } - #jetpack-protect-root { + #jetpack-protect-root, + .components-modal__screen-overlay { --wp-admin-theme-color: var(--jp-black); --wp-admin-theme-color-darker-10: var(--jp-black-80); --wp-admin-theme-color-darker-20: var(--jp-black-80); diff --git a/projects/plugins/protect/src/js/types/global.d.ts b/projects/plugins/protect/src/js/types/global.d.ts index 6f1d984bd96d2..c8345c9d3698b 100644 --- a/projects/plugins/protect/src/js/types/global.d.ts +++ b/projects/plugins/protect/src/js/types/global.d.ts @@ -13,7 +13,7 @@ declare global { apiRoot: string; apiNonce: string; registrationNonce: string; - credentials: [ Record< string, unknown > ]; + credentials: false | [] | [ Record< string, unknown > ]; status: ScanStatus; fixerStatus: FixersStatus; scanHistory: ScanStatus; diff --git a/projects/plugins/protect/webpack.config.js b/projects/plugins/protect/webpack.config.js index 2f6a45721b100..0c65dfec146a7 100644 --- a/projects/plugins/protect/webpack.config.js +++ b/projects/plugins/protect/webpack.config.js @@ -33,6 +33,24 @@ module.exports = [ includeNodeModules: [ '@automattic/jetpack-' ], } ), + /** + * Transpile @wordpress/dataviews in node_modules too. + * + * @see https://github.com/Automattic/jetpack/issues/39907 + */ + jetpackWebpackConfig.TranspileRule( { + includeNodeModules: [ '@wordpress/dataviews/' ], + babelOpts: { + configFile: false, + plugins: [ + [ + require.resolve( '@automattic/babel-plugin-replace-textdomain' ), + { textdomain: 'jetpack-protect' }, + ], + ], + }, + } ), + // Handle CSS. jetpackWebpackConfig.CssRule( { extensions: [ 'css', 'sass', 'scss' ], From ea5282d42efa43ce5342a3d4898fe240f29514a1 Mon Sep 17 00:00:00 2001 From: Nate Weller Date: Sat, 29 Mar 2025 15:23:20 -0600 Subject: [PATCH 2/3] Protect Components: Use @wordpress/dataviews/wp import path (#41510) --- .pnpmfile.cjs | 3 + pnpm-lock.yaml | 8 +- .../scan/src/components/scan-report/index.tsx | 2 +- .../threat-severity-badge/index.tsx | 2 +- .../components/threats-data-views/index.tsx | 13 ++- .../threats-data-views/test/index.test.jsx | 82 ------------------- projects/js-packages/scan/tsconfig.json | 1 + projects/plugins/protect/webpack.config.js | 47 ++++++++++- 8 files changed, 63 insertions(+), 95 deletions(-) delete mode 100644 projects/js-packages/scan/src/components/threats-data-views/test/index.test.jsx diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs index d0ba5ccb874fb..e240885b44358 100644 --- a/.pnpmfile.cjs +++ b/.pnpmfile.cjs @@ -66,6 +66,9 @@ function fixDeps( pkg ) { '@use-gesture/react', 'use-memo-one', 'uuid', + // Needed for storybook to build with the /wp endpoint. Normal builds don't need them due to dependency extraction. + '@wordpress/date', + '@wordpress/hooks', ] ) { pkg.optionalDependencies[ dep ] = '*'; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 499247870e0ff..f31b636042fbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,7 +4,7 @@ settings: autoInstallPeers: false excludeLinksFromLockfile: false -pnpmfileChecksum: sha256-zCxWVmeqIVdzVzgWqWEctKVmBIozYb01nBTVlyTN+ps= +pnpmfileChecksum: sha256-vsSLfJFMcv7GYtNElbicbg3ukYfUNh3r8B4Wm2GpQv0= patchedDependencies: '@wordpress/dataviews': @@ -20698,6 +20698,8 @@ snapshots: '@emotion/utils': 1.4.2 '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@use-gesture/react': 10.3.1(react@18.3.1) + '@wordpress/date': 5.19.0 + '@wordpress/hooks': 4.19.0 change-case: 4.1.2 colord: 2.9.3 date-fns: 3.6.0 @@ -20739,6 +20741,8 @@ snapshots: '@emotion/utils': 1.4.2 '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@use-gesture/react': 10.3.1(react@18.3.1) + '@wordpress/date': 5.19.0 + '@wordpress/hooks': 4.19.0 change-case: 4.1.2 colord: 2.9.3 date-fns: 3.6.0 @@ -20780,6 +20784,8 @@ snapshots: '@emotion/utils': 1.4.2 '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@use-gesture/react': 10.3.1(react@18.3.1) + '@wordpress/date': 5.19.0 + '@wordpress/hooks': 4.19.0 change-case: 4.1.2 colord: 2.9.3 date-fns: 3.6.0 diff --git a/projects/js-packages/scan/src/components/scan-report/index.tsx b/projects/js-packages/scan/src/components/scan-report/index.tsx index 16391109c4941..44d98e41c46bd 100644 --- a/projects/js-packages/scan/src/components/scan-report/index.tsx +++ b/projects/js-packages/scan/src/components/scan-report/index.tsx @@ -5,7 +5,7 @@ import { type Field, DataViews, filterSortAndPaginate, -} from '@wordpress/dataviews'; +} from '@wordpress/dataviews/wp'; import { __, _n } from '@wordpress/i18n'; import { Icon } from '@wordpress/icons'; import { useCallback, useMemo, useState } from 'react'; diff --git a/projects/js-packages/scan/src/components/threat-severity-badge/index.tsx b/projects/js-packages/scan/src/components/threat-severity-badge/index.tsx index 112ea50ecd43d..1b69d39bbd5f7 100644 --- a/projects/js-packages/scan/src/components/threat-severity-badge/index.tsx +++ b/projects/js-packages/scan/src/components/threat-severity-badge/index.tsx @@ -1,6 +1,6 @@ import { Badge } from '@automattic/jetpack-components'; import { __, sprintf } from '@wordpress/i18n'; -import { getSeverityLabel, getSeverityVariant } from '@automattic/jetpack-scan'; +import { getSeverityLabel, getSeverityVariant } from '../../utils/severity.js'; const ThreatSeverityBadge = ( { severity, showLabel = false } ) => { const title = getSeverityLabel( severity ); diff --git a/projects/js-packages/scan/src/components/threats-data-views/index.tsx b/projects/js-packages/scan/src/components/threats-data-views/index.tsx index bff6b30dee3f1..ec80db9a666c7 100644 --- a/projects/js-packages/scan/src/components/threats-data-views/index.tsx +++ b/projects/js-packages/scan/src/components/threats-data-views/index.tsx @@ -8,18 +8,15 @@ import { type View, DataViews, filterSortAndPaginate, -} from '@wordpress/dataviews'; +} from '@wordpress/dataviews/wp'; import { dateI18n } from '@wordpress/date'; import { __ } from '@wordpress/i18n'; import { Icon } from '@wordpress/icons'; import { useCallback, useContext, useMemo, useState } from 'react'; -import { - getThreatType, - THREAT_ACTION_FIX, - THREAT_ACTION_IGNORE, - ThreatsContext, - type Threat, -} from '@automattic/jetpack-scan'; +import { THREAT_ACTION_FIX, THREAT_ACTION_IGNORE } from '../../actions/index.js'; +import { ThreatsContext } from '../../context/index.js'; +import { type Threat } from '../../types/threats.js'; +import { getThreatType } from '../../utils/threats.js'; import ThreatFixerButton from '../threat-fixer-button/index.js'; import ThreatDetailsModal from '../threat-modals/details-modal/index.js'; import ThreatFixerModal from '../threat-modals/fixer-modal/index.js'; diff --git a/projects/js-packages/scan/src/components/threats-data-views/test/index.test.jsx b/projects/js-packages/scan/src/components/threats-data-views/test/index.test.jsx deleted file mode 100644 index 49f68132d853f..0000000000000 --- a/projects/js-packages/scan/src/components/threats-data-views/test/index.test.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { ThreatsContextProvider } from '@automattic/jetpack-scan'; -import ThreatsDataViews from '../index.js'; - -const data = [ - // Scan API Data - { - id: 185869885, - signature: 'EICAR_AV_Test', - title: 'Malicious code found in file: index.php', - description: - "This is the standard EICAR antivirus test code, and not a real infection. If your site contains this code when you don't expect it to, contact Jetpack support for some help.", - firstDetected: '2024-10-07T20:45:06.000Z', - fixedIn: null, - fixedOn: '2024-10-07T20:45:06.000Z', - fixable: { fixer: 'rollback', target: 'January 26, 2024, 6:49 am', extensionStatus: '' }, - fixer: { - status: 'in_progress', - startedAt: '2024-10-07T20:45:06.000Z', - lastUpdated: '2024-10-07T20:45:06.000Z', - }, - severity: 8, - status: 'current', - filename: '/var/www/html/wp-content/index.php', - context: { - 1: 'echo << {}, - onFixThreats: () => {}, - onIgnoreThreats: () => {}, - onUnignoreThreats: () => {}, - isThreatEligibleForFix: () => true, - isThreatEligibleForIgnore: () => true, - isThreatEligibleForUnignore: () => true, - isUserConnected: true, - hasConnectedOwner: true, - userIsConnecting: false, - handleConnectUser: () => {}, - credentials: [], - credentialsIsFetching: false, - credentialsRedirectUrl: '/redirect-url', - onModalOpen: () => {}, - onModalClose: () => {}, -}; - -describe( 'ThreatsDataViews', () => { - it( 'renders threat data', () => { - render( - - - - ); - expect( screen.getByText( 'Malicious code found in file: index.php' ) ).toBeInTheDocument(); - expect( - screen.getByText( 'WooCommerce <= 3.2.3 - Authenticated PHP Object Injection' ) - ).toBeInTheDocument(); - } ); -} ); diff --git a/projects/js-packages/scan/tsconfig.json b/projects/js-packages/scan/tsconfig.json index ad9a26c4877f5..17a2fff4bcd52 100644 --- a/projects/js-packages/scan/tsconfig.json +++ b/projects/js-packages/scan/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "jetpack-js-tools/tsconfig.tsc.json", "include": [ "./src/**/*" ], + "exclude": [ "**/test/**", "**/stories/**" ], "compilerOptions": { "typeRoots": [ "./node_modules/@types/", "src/*" ], "sourceMap": false, diff --git a/projects/plugins/protect/webpack.config.js b/projects/plugins/protect/webpack.config.js index 0c65dfec146a7..0815336f45487 100644 --- a/projects/plugins/protect/webpack.config.js +++ b/projects/plugins/protect/webpack.config.js @@ -39,13 +39,56 @@ module.exports = [ * @see https://github.com/Automattic/jetpack/issues/39907 */ jetpackWebpackConfig.TranspileRule( { - includeNodeModules: [ '@wordpress/dataviews/' ], + includeNodeModules: [ '@wordpress/dataviews/build-wp/' ], babelOpts: { configFile: false, plugins: [ [ require.resolve( '@automattic/babel-plugin-replace-textdomain' ), - { textdomain: 'jetpack-protect' }, + { + textdomain: 'jetpack-protect', + functions: { + __: 1, + __1: 1, + __2: 1, + __3: 1, + __4: 1, + __5: 1, + __6: 1, + __7: 1, + __8: 1, + __9: 1, + __10: 1, + __11: 1, + __12: 1, + __13: 1, + __14: 1, + __15: 1, + __16: 1, + __17: 1, + __18: 1, + __19: 1, + __20: 1, + __21: 1, + __22: 1, + __23: 1, + __24: 1, + __25: 1, + __26: 1, + __27: 1, + __28: 1, + __29: 1, + __30: 1, + _x: 2, + _x1: 2, + _x2: 2, + _x3: 2, + _x4: 2, + _x5: 2, + _n: 3, + _nx: 4, + }, + }, ], ], }, From d16927f921785485403aea25688b73f1bbd38ecc Mon Sep 17 00:00:00 2001 From: Nate Weller Date: Sun, 30 Mar 2025 17:38:57 -0600 Subject: [PATCH 3/3] Fix up changelogs --- .../add-components-threats-data-views-modal-integration | 4 ---- .../components/changelog/add-hide-value-prop-to-stat-card | 4 ---- projects/js-packages/components/changelog/add-shield-icon | 4 ---- .../components/changelog/components-add-scan-report | 4 ---- .../publicize-components/changelog/force-a-release | 4 ++++ projects/plugins/protect/changelog/add-protect-home | 2 +- projects/plugins/protect/changelog/refactor-alert-icon | 5 ----- .../protect/changelog/update-protect-add-scan-report-to-home | 4 ---- .../changelog/update-protect-scan-and-history-headers | 4 ---- 9 files changed, 5 insertions(+), 30 deletions(-) delete mode 100644 projects/js-packages/components/changelog/add-components-threats-data-views-modal-integration delete mode 100644 projects/js-packages/components/changelog/add-hide-value-prop-to-stat-card delete mode 100644 projects/js-packages/components/changelog/add-shield-icon delete mode 100644 projects/js-packages/components/changelog/components-add-scan-report create mode 100644 projects/js-packages/publicize-components/changelog/force-a-release delete mode 100644 projects/plugins/protect/changelog/refactor-alert-icon delete mode 100644 projects/plugins/protect/changelog/update-protect-add-scan-report-to-home delete mode 100644 projects/plugins/protect/changelog/update-protect-scan-and-history-headers diff --git a/projects/js-packages/components/changelog/add-components-threats-data-views-modal-integration b/projects/js-packages/components/changelog/add-components-threats-data-views-modal-integration deleted file mode 100644 index 809bb1cd3a788..0000000000000 --- a/projects/js-packages/components/changelog/add-components-threats-data-views-modal-integration +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: added - -Integrates ThreatModal in ThreatsDataViews diff --git a/projects/js-packages/components/changelog/add-hide-value-prop-to-stat-card b/projects/js-packages/components/changelog/add-hide-value-prop-to-stat-card deleted file mode 100644 index 0d4002c768dd8..0000000000000 --- a/projects/js-packages/components/changelog/add-hide-value-prop-to-stat-card +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: added - -Stat Card: add hideValue prop diff --git a/projects/js-packages/components/changelog/add-shield-icon b/projects/js-packages/components/changelog/add-shield-icon deleted file mode 100644 index 5c6cc27eeb809..0000000000000 --- a/projects/js-packages/components/changelog/add-shield-icon +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: added - -Add ShieldIcon component diff --git a/projects/js-packages/components/changelog/components-add-scan-report b/projects/js-packages/components/changelog/components-add-scan-report deleted file mode 100644 index ba0fbd4cce025..0000000000000 --- a/projects/js-packages/components/changelog/components-add-scan-report +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: added - -Adds ScanReport component diff --git a/projects/js-packages/publicize-components/changelog/force-a-release b/projects/js-packages/publicize-components/changelog/force-a-release new file mode 100644 index 0000000000000..06bc02ad1a10a --- /dev/null +++ b/projects/js-packages/publicize-components/changelog/force-a-release @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Update dependencies. \ No newline at end of file diff --git a/projects/plugins/protect/changelog/add-protect-home b/projects/plugins/protect/changelog/add-protect-home index 0bcfedb6fe8ac..6ec70fe59a4e9 100644 --- a/projects/plugins/protect/changelog/add-protect-home +++ b/projects/plugins/protect/changelog/add-protect-home @@ -1,4 +1,4 @@ Significance: minor Type: added -Adds a Home page and StatCards +Adds a Home page tab with stat cards and scan report diff --git a/projects/plugins/protect/changelog/refactor-alert-icon b/projects/plugins/protect/changelog/refactor-alert-icon deleted file mode 100644 index 46b4c247b1b9f..0000000000000 --- a/projects/plugins/protect/changelog/refactor-alert-icon +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: changed -Comment: Refactored icon component code. - - diff --git a/projects/plugins/protect/changelog/update-protect-add-scan-report-to-home b/projects/plugins/protect/changelog/update-protect-add-scan-report-to-home deleted file mode 100644 index 0478ae51501b8..0000000000000 --- a/projects/plugins/protect/changelog/update-protect-add-scan-report-to-home +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: added - -Adds ScanReport to HomeRoute diff --git a/projects/plugins/protect/changelog/update-protect-scan-and-history-headers b/projects/plugins/protect/changelog/update-protect-scan-and-history-headers deleted file mode 100644 index cd930e395e0ed..0000000000000 --- a/projects/plugins/protect/changelog/update-protect-scan-and-history-headers +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: changed - -Updates the structure and content of the Scan and History page headers