diff --git a/projects/js-packages/components/components/threats-data-views/constants.ts b/projects/js-packages/components/components/threats-data-views/constants.ts index 40941b43ad032..68112cc1d8717 100644 --- a/projects/js-packages/components/components/threats-data-views/constants.ts +++ b/projects/js-packages/components/components/threats-data-views/constants.ts @@ -7,12 +7,10 @@ import { wordpress as coreIcon, } from '@wordpress/icons'; -export const THREAT_STATUSES: { value: string; label: string; variant?: 'success' | 'warning' }[] = - [ - { value: 'current', label: __( 'Active', 'jetpack-components' ), variant: 'warning' }, - { value: 'fixed', label: __( 'Fixed', 'jetpack-components' ), variant: 'success' }, - { value: 'ignored', label: __( 'Ignored', 'jetpack-components' ) }, - ]; +export const THREAT_STATUSES: { value: string; label: string; variant?: 'success' }[] = [ + { value: 'fixed', label: __( 'Fixed', 'jetpack-components' ), variant: 'success' }, + { value: 'ignored', label: __( 'Ignored', 'jetpack-components' ) }, +]; export const THREAT_TYPES = [ { value: 'plugins', label: __( 'Plugin', 'jetpack-components' ) }, @@ -45,6 +43,26 @@ export const THREAT_FIELD_FIRST_DETECTED = 'first-detected'; export const THREAT_FIELD_FIXED_ON = 'fixed-on'; export const THREAT_FIELD_AUTO_FIX = 'auto-fix'; +export const CURRENT_TABLE_FIELDS = [ + THREAT_FIELD_SEVERITY, + THREAT_FIELD_TYPE, + THREAT_FIELD_AUTO_FIX, +]; + +export const HISTORIC_TABLE_FIELDS = [ + THREAT_FIELD_SEVERITY, + THREAT_FIELD_TYPE, + THREAT_FIELD_FIRST_DETECTED, + THREAT_FIELD_FIXED_ON, +]; + +export const LIST_FIELDS = [ + THREAT_FIELD_SEVERITY, + THREAT_FIELD_TYPE, + THREAT_FIELD_EXTENSION, + THREAT_FIELD_SIGNATURE, +]; + export const THREAT_ACTION_FIX = 'fix'; export const THREAT_ACTION_IGNORE = 'ignore'; export const THREAT_ACTION_UNIGNORE = 'unignore'; diff --git a/projects/js-packages/components/components/threats-data-views/index.tsx b/projects/js-packages/components/components/threats-data-views/index.tsx index 27cbfa23935fe..b859212392957 100644 --- a/projects/js-packages/components/components/threats-data-views/index.tsx +++ b/projects/js-packages/components/components/threats-data-views/index.tsx @@ -1,4 +1,4 @@ -import { getThreatType, type Threat } from '@automattic/jetpack-scan'; +import { getFixerAction, getThreatType, type Threat } from '@automattic/jetpack-scan'; import { type Action, type ActionButton, @@ -19,6 +19,8 @@ import Badge from '../badge'; import ThreatFixerButton from '../threat-fixer-button'; import ThreatSeverityBadge from '../threat-severity-badge'; import { + CURRENT_TABLE_FIELDS, + LIST_FIELDS, THREAT_ACTION_FIX, THREAT_ACTION_IGNORE, THREAT_ACTION_UNIGNORE, @@ -40,27 +42,33 @@ import { THREAT_TYPES, } from './constants'; import styles from './styles.module.scss'; -import ThreatsStatusToggleGroupControl from './threats-status-toggle-group-control'; + +export { HISTORIC_TABLE_FIELDS } from './constants'; /** * DataViews component for displaying security threats. * - * @param {object} props - Component props. - * @param {Array} props.data - Threats data. - * @param {Array} props.filters - Initial DataView filters. - * @param {Function} props.onChangeSelection - Callback function run when an item is selected. - * @param {Function} props.onFixThreats - Threat fix action callback. - * @param {Function} props.onIgnoreThreats - Threat ignore action callback. - * @param {Function} props.onUnignoreThreats - Threat unignore action callback. - * @param {Function} props.isThreatEligibleForFix - Function to determine if a threat is eligible for fixing. - * @param {Function} props.isThreatEligibleForIgnore - Function to determine if a threat is eligible for ignoring. - * @param {Function} props.isThreatEligibleForUnignore - Function to determine if a threat is eligible for unignoring. + * @param {object} props - Component props. + * @param {string} props.status - Flag to indicate if the threats are current or historic. + * @param {Array} props.data - Threats data. + * @param {Array} props.initialFilters - Initial DataView filters. + * @param {Array} props.initialFields - Initial DataView fields. + * @param {Function} props.onChangeSelection - Callback function run when an item is selected. + * @param {Function} props.onFixThreats - Threat fix action callback. + * @param {Function} props.onIgnoreThreats - Threat ignore action callback. + * @param {Function} props.onUnignoreThreats - Threat unignore action callback. + * @param {Function} props.isThreatEligibleForFix - Function to determine if a threat is eligible for fixing. + * @param {Function} props.isThreatEligibleForIgnore - Function to determine if a threat is eligible for ignoring. + * @param {Function} props.isThreatEligibleForUnignore - Function to determine if a threat is eligible for unignoring. + * @param {JSX.Element} props.header - Header component. * * @return {JSX.Element} The ThreatsDataViews component. */ export default function ThreatsDataViews( { + status = 'current', data, - filters, + initialFields = CURRENT_TABLE_FIELDS, + initialFilters = [], onChangeSelection, isThreatEligibleForFix, isThreatEligibleForIgnore, @@ -68,9 +76,12 @@ export default function ThreatsDataViews( { onFixThreats, onIgnoreThreats, onUnignoreThreats, + header, }: { + status?: string; data: Threat[]; - filters?: Filter[]; + initialFields?: string[]; + initialFilters?: Filter[]; onChangeSelection?: ( selectedItemIds: string[] ) => void; isThreatEligibleForFix?: ( threat: Threat ) => boolean; isThreatEligibleForIgnore?: ( threat: Threat ) => boolean; @@ -78,6 +89,7 @@ export default function ThreatsDataViews( { onFixThreats?: ( threats: Threat[] ) => void; onIgnoreThreats?: ActionButton< Threat >[ 'callback' ]; onUnignoreThreats?: ActionButton< Threat >[ 'callback' ]; + header?: JSX.Element; } ): JSX.Element { const baseView = { sort: { @@ -85,7 +97,7 @@ export default function ThreatsDataViews( { direction: 'desc' as SortDirection, }, search: '', - filters: filters || [], + filters: initialFilters, page: 1, perPage: 20, }; @@ -100,19 +112,14 @@ export default function ThreatsDataViews( { const defaultLayouts: SupportedLayouts = { table: { ...baseView, - fields: [ THREAT_FIELD_SEVERITY, THREAT_FIELD_TYPE, THREAT_FIELD_AUTO_FIX ], + fields: initialFields, titleField: THREAT_FIELD_TITLE, descriptionField: THREAT_FIELD_DESCRIPTION, showMedia: false, }, list: { ...baseView, - fields: [ - THREAT_FIELD_SEVERITY, - THREAT_FIELD_TYPE, - THREAT_FIELD_EXTENSION, - THREAT_FIELD_SIGNATURE, - ], + fields: LIST_FIELDS, titleField: THREAT_FIELD_TITLE, mediaField: THREAT_FIELD_ICON, showMedia: true, @@ -238,28 +245,6 @@ export default function ThreatsDataViews( { ); }, }, - { - id: THREAT_FIELD_STATUS, - label: __( 'Status', 'jetpack-components' ), - elements: THREAT_STATUSES, - getValue( { item }: { item: Threat } ) { - if ( ! item.status ) { - return 'current'; - } - return ( - THREAT_STATUSES.find( ( { value } ) => value === item.status )?.value ?? item.status - ); - }, - render( { item }: { item: Threat } ) { - if ( item.status ) { - const status = THREAT_STATUSES.find( ( { value } ) => value === item.status ); - if ( status ) { - return { status.label }; - } - } - return { __( 'Active', 'jetpack-components' ) }; - }, - }, { id: THREAT_FIELD_TYPE, label: __( 'Type', 'jetpack-components' ), @@ -300,6 +285,35 @@ export default function ThreatsDataViews( { return item.extension ? item.extension.slug : ''; }, }, + ...( 'historic' === status && dataFields.includes( 'status' ) + ? [ + { + id: THREAT_FIELD_STATUS, + label: __( 'Status', 'jetpack-components' ), + elements: THREAT_STATUSES, + getValue( { item }: { item: Threat } ) { + if ( ! item.status ) { + return 'current'; + } + return ( + THREAT_STATUSES.find( ( { value } ) => value === item.status )?.value ?? + item.status + ); + }, + render( { item }: { item: Threat } ) { + if ( item.status ) { + const threatStatus = THREAT_STATUSES.find( + ( { value } ) => value === item.status + ); + if ( threatStatus ) { + return { threatStatus.label }; + } + } + return { __( 'Current', 'jetpack-components' ) }; + }, + }, + ] + : [] ), ...( dataFields.includes( 'severity' ) ? [ { @@ -366,7 +380,7 @@ export default function ThreatsDataViews( { }, ] : [] ), - ...( dataFields.includes( 'fixable' ) + ...( 'historic' !== status && dataFields.includes( 'fixable' ) ? [ { id: THREAT_FIELD_AUTO_FIX, @@ -398,7 +412,7 @@ export default function ThreatsDataViews( { ]; return result; - }, [ dataFields, plugins, themes, signatures, onFixThreats ] ); + }, [ dataFields, plugins, themes, signatures, status, onFixThreats ] ); /** * DataView actions - collection of operations that can be performed upon each record. @@ -408,10 +422,12 @@ export default function ThreatsDataViews( { const actions = useMemo( () => { const result: Action< Threat >[] = []; - if ( dataFields.includes( 'fixable' ) ) { + if ( dataFields.includes( 'fixable' ) && view.type === 'list' ) { result.push( { id: THREAT_ACTION_FIX, - label: __( 'Auto-fix', 'jetpack-components' ), + label: items => { + return getFixerAction( items[ 0 ] ); + }, isPrimary: true, callback: onFixThreats, isEligible( item ) { @@ -466,6 +482,7 @@ export default function ThreatsDataViews( { return result; }, [ + view.type, dataFields, onFixThreats, onIgnoreThreats, @@ -511,13 +528,7 @@ export default function ThreatsDataViews( { onChangeView={ onChangeView } paginationInfo={ paginationInfo } view={ view } - header={ - - } + header={ header } /> ); } diff --git a/projects/js-packages/components/components/threats-data-views/stories/index.stories.tsx b/projects/js-packages/components/components/threats-data-views/stories/index.stories.tsx index 4839415084b80..6848824c3c45e 100644 --- a/projects/js-packages/components/components/threats-data-views/stories/index.stories.tsx +++ b/projects/js-packages/components/components/threats-data-views/stories/index.stories.tsx @@ -1,4 +1,5 @@ import ThreatsDataViews from '..'; +import { HISTORIC_TABLE_FIELDS } from '../constants'; export default { title: 'JS Packages/Components/Threats Data Views', @@ -18,8 +19,8 @@ export default { ], }; -export const Default = args => ; -Default.args = { +export const Current = args => ; +Current.args = { data: [ { id: 185869885, @@ -41,20 +42,6 @@ Default.args = { marks: {}, }, }, - { - id: 185869883, - signature: 'Suspicious.Files', - title: 'Malicious code found in file: fuzzy.php', - description: - 'Our security scanners detected that this file is identical to a previously identified malicious file', - firstDetected: '2024-10-07T20:45:06.000Z', - fixedIn: null, - severity: 4, - fixable: false, - status: 'ignored', - filename: '/var/www/html/wp-content/fuzzy.php', - context: '', - }, { id: 185868972, signature: 'EICAR_AV_Test_Suspicious', @@ -116,19 +103,6 @@ Default.args = { filename: '/var/www/html/wp-admin/index.php', diff: "--- /tmp/wordpress/6.6.2/wordpress/wp-admin/index.php\t2024-10-07 20:40:04.887546480 +0000\n+++ /var/www/html/wp-admin/index.php\t2024-10-07 20:39:58.775512965 +0000\n@@ -210,3 +210,4 @@\n wp_print_community_events_templates();\n \n require_once ABSPATH . 'wp-admin/admin-footer.php';\n+if ( true === false ) exit();\n\\ No newline at end of file\n", }, - { - id: 13216959, - signature: 'Vulnerable.WP.Core', - title: 'Vulnerable WordPress Version (6.4.3)', - description: 'The installed version of WordPress (6.4.3) has a known vulnerability. ', - firstDetected: '2024-07-15T21:56:50.000Z', - severity: 4, - fixedOn: '2024-07-15T22:01:42.000Z', - status: 'fixed', - fixable: false, - version: '6.4.3', - source: '', - }, { id: '7275a176-d579-471a-8492-df8edbdf27de', title: 'WooCommerce <= 3.4.5 - Authenticated Stored XSS', @@ -146,13 +120,50 @@ Default.args = { }, }, ], - filters: [ + onFixThreats: () => + alert( 'Threat fix action callback triggered! This is handled by the component consumer.' ), // eslint-disable-line no-alert + onIgnoreThreats: () => + alert( 'Ignore threat action callback triggered! This is handled by the component consumer.' ), // eslint-disable-line no-alert + onUnignoreThreats: () => + // eslint-disable-next-line no-alert + alert( + 'Unignore threat action callback triggered! This is handled by the component consumer.' + ), +}; + +export const Historic = args => ; +Historic.args = { + status: 'historic', + data: [ { - field: 'status', - operator: 'isAny', - value: [ 'current' ], + id: 185869883, + signature: 'Suspicious.Files', + title: 'Malicious code found in file: fuzzy.php', + description: + 'Our security scanners detected that this file is identical to a previously identified malicious file', + firstDetected: '2024-10-07T20:45:06.000Z', + fixedIn: null, + severity: 4, + fixable: false, + status: 'ignored', + filename: '/var/www/html/wp-content/fuzzy.php', + context: '', + }, + { + id: 13216959, + signature: 'Vulnerable.WP.Core', + title: 'Vulnerable WordPress Version (6.4.3)', + description: 'The installed version of WordPress (6.4.3) has a known vulnerability. ', + firstDetected: '2024-07-15T21:56:50.000Z', + severity: 4, + fixedOn: '2024-07-15T22:01:42.000Z', + status: 'fixed', + fixable: false, + version: '6.4.3', + source: '', }, ], + initialFields: HISTORIC_TABLE_FIELDS, onFixThreats: () => alert( 'Threat fix action callback triggered! This is handled by the component consumer.' ), // eslint-disable-line no-alert onIgnoreThreats: () => @@ -260,13 +271,6 @@ FixerStatuses.args = { }, }, ], - filters: [ - { - field: 'status', - operator: 'isAny', - value: [ 'current' ], - }, - ], onFixThreats: () => alert( 'Fix threat action callback triggered! This is handled by the component consumer.' ), // eslint-disable-line no-alert onIgnoreThreats: () => diff --git a/projects/js-packages/components/components/threats-data-views/styles.module.scss b/projects/js-packages/components/components/threats-data-views/styles.module.scss index 7d97f69b25ccd..7da6717e97f97 100644 --- a/projects/js-packages/components/components/threats-data-views/styles.module.scss +++ b/projects/js-packages/components/components/threats-data-views/styles.module.scss @@ -34,7 +34,3 @@ fill: var( --jp-black ); } } - -.toggle-group-control { - min-width: 300px; -} diff --git a/projects/js-packages/components/components/threats-data-views/threats-status-toggle-group-control.tsx b/projects/js-packages/components/components/threats-data-views/threats-status-toggle-group-control.tsx deleted file mode 100644 index 3254a1050c1e9..0000000000000 --- a/projects/js-packages/components/components/threats-data-views/threats-status-toggle-group-control.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { type Threat, type ThreatStatus } from '@automattic/jetpack-scan'; -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 { type View } from '@wordpress/dataviews'; -import { useMemo, useCallback } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; -import styles from './styles.module.scss'; - -/** - * ToggleGroupControl component for filtering threats by status. - * @param {object} props - Component props. - * @param { Threat[]} props.data - Threats data. - * @param { View } props.view - The current view. - * @param { Function } props.onChangeView - Callback function to handle view changes. - * - * @return {JSX.Element|null} The component or null. - */ -export default function ThreatsStatusToggleGroupControl( { - data, - view, - onChangeView, -}: { - data: Threat[]; - view: View; - onChangeView: ( newView: View ) => void; -} ): JSX.Element { - /** - * Compute values from the provided threats data. - * - * @member {number} activeThreatsCount - Count of active threats. - * @member {number} historicThreatsCount - Count of historic threats. - */ - const { - activeThreatsCount, - historicThreatsCount, - }: { - activeThreatsCount: number; - historicThreatsCount: number; - } = useMemo( () => { - return data.reduce( - ( acc, threat ) => { - if ( threat.status ) { - if ( threat.status === 'current' ) { - acc.activeThreatsCount++; - } else { - acc.historicThreatsCount++; - } - } - return acc; - }, - { - activeThreatsCount: 0, - historicThreatsCount: 0, - } - ); - }, [ data ] ); - - /** - * Callback function to handle the status change filter. - * - * @param {string} newStatus - The new status filter value. - */ - const onStatusFilterChange = useCallback( - ( newStatus: string ) => { - const updatedFilters = view.filters.filter( filter => filter.field !== 'status' ); - - if ( newStatus === 'active' ) { - updatedFilters.push( { - field: 'status', - operator: 'isAny', - value: [ 'current' ], - } ); - } else if ( newStatus === 'historic' ) { - updatedFilters.push( { - field: 'status', - operator: 'isAny', - value: [ 'fixed', 'ignored' ], - } ); - } - - onChangeView( { - ...view, - filters: updatedFilters, - } ); - }, - [ view, onChangeView ] - ); - - /** - * Memoized function to determine if a status filter is selected. - * - * @param {Array} threatStatuses - List of threat statuses. - */ - const isStatusFilterSelected = useMemo( - () => ( threatStatuses: ThreatStatus[] ) => - view.filters.some( - filter => - filter.field === 'status' && - Array.isArray( filter.value ) && - filter.value.length === threatStatuses.length && - threatStatuses.every( threatStatus => filter.value.includes( threatStatus ) ) - ), - [ view.filters ] - ); - - const selectedValue = useMemo( () => { - if ( isStatusFilterSelected( [ 'current' ] ) ) { - return 'active' as const; - } - if ( isStatusFilterSelected( [ 'fixed', 'ignored' ] ) ) { - return 'historic' as const; - } - return '' as const; - }, [ isStatusFilterSelected ] ); - - if ( ! ( activeThreatsCount + historicThreatsCount ) ) { - return null; - } - - try { - return ( -
-
- - - - -
-
- ); - } catch { - return null; - } -} diff --git a/projects/js-packages/components/index.ts b/projects/js-packages/components/index.ts index 6df50ee7fdb61..c3cb89a23081f 100644 --- a/projects/js-packages/components/index.ts +++ b/projects/js-packages/components/index.ts @@ -46,7 +46,10 @@ export { default as SplitButton } from './components/split-button'; export { default as ThemeProvider } from './components/theme-provider'; export { default as ThreatFixerButton } from './components/threat-fixer-button'; export { default as ThreatSeverityBadge } from './components/threat-severity-badge'; -export { default as ThreatsDataViews } from './components/threats-data-views'; +export { + default as ThreatsDataViews, + HISTORIC_TABLE_FIELDS, +} from './components/threats-data-views'; export { default as ShieldIcon } from './components/shield-icon'; export { default as ScanReport } from './components/scan-report'; export { default as Text, H2, H3, Title } from './components/text'; diff --git a/projects/plugins/protect/src/js/index.tsx b/projects/plugins/protect/src/js/index.tsx index 4438d5021a664..18bc1fa09e2f5 100644 --- a/projects/plugins/protect/src/js/index.tsx +++ b/projects/plugins/protect/src/js/index.tsx @@ -13,6 +13,7 @@ import { CheckoutProvider } from './hooks/use-plan'; import FirewallRoute from './routes/firewall'; import HomeRoute from './routes/home'; import ScanRoute from './routes/scan'; +import HistoryRoute from './routes/scan/history'; import SetupRoute from './routes/setup'; import './styles.module.scss'; @@ -63,7 +64,7 @@ function render() { path="/scan/history" element={ - + } /> @@ -71,7 +72,7 @@ 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 c302f93dd8863..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 @@ -30,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' ); @@ -47,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 ) ) } ); @@ -63,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 ) ) } ); 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 index 59958c84024b9..06a6f6ddf9219 100644 --- 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 @@ -30,7 +30,8 @@ const HomeAdminSectionHero: React.FC = () => { : __( 'We stay ahead of security vulnerabilities to keep your site protected.', 'jetpack-protect', - /* dummy arg to avoid bad minification */ 0 + // @ts-expect-error TS2554 - dummy arg to avoid bad minification + 0 ) }