From e9b0e4fc8d5563dd9357b56ff97e7a8bb45c850d Mon Sep 17 00:00:00 2001 From: dkmyta <43220201+dkmyta@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:41:19 -0800 Subject: [PATCH] ThreatsDataViews: Add ToggleGroupControl (#39901) * Components: add hoverShow prop to IconTooltip * changelog * Add ThreatsDataView changelog * Add ToggleGroupControl filters to ThreatsDataView * Components: add hoverShow prop to IconTooltip * changelog * Add ThreatsDataView changelog Use inline button for primary fixer action * Fix rebase issues * Fixes, updates * Improve type checks * Components: add hoverShow prop to IconTooltip * changelog * Add ThreatsDataView * Story fixes * Update toggle text * Fix story data * changelog * Fix changelog entry * Fix types * Update approach to counting threats * Fix tests * Update lock file * Revert lock file changes * Set __nextHasNoMarginBottom to avoid deprecation warning * Move toggle to dedicated file * Add default filters to stories * Wrap experimental component rendering in try/catch * Update projects/js-packages/components/components/threats-data-views/index.tsx Co-authored-by: Nate Weller * Update projects/js-packages/components/components/threats-data-views/index.tsx Co-authored-by: Nate Weller * Update projects/js-packages/components/components/threats-data-views/index.tsx Co-authored-by: Nate Weller * Update projects/js-packages/components/components/threats-data-views/threats-status-toggle-group-control.tsx Co-authored-by: Nate Weller * Fix lint errors --------- Co-authored-by: Nate Weller Co-authored-by: Nate Weller --- ...ent-threats-data-view-toggle-group-control | 4 + .../components/threats-data-views/index.tsx | 14 +- .../stories/index.stories.tsx | 18 +- .../threats-data-views/styles.module.scss | 7 +- .../threats-data-views/test/index.test.tsx | 1 - .../threats-status-toggle-group-control.tsx | 160 ++++++++++++++++++ 6 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 projects/js-packages/components/changelog/add-component-threats-data-view-toggle-group-control create mode 100644 projects/js-packages/components/components/threats-data-views/threats-status-toggle-group-control.tsx diff --git a/projects/js-packages/components/changelog/add-component-threats-data-view-toggle-group-control b/projects/js-packages/components/changelog/add-component-threats-data-view-toggle-group-control new file mode 100644 index 0000000000000..9c528d1825437 --- /dev/null +++ b/projects/js-packages/components/changelog/add-component-threats-data-view-toggle-group-control @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Add ToggleGroupControl to ThreatsDataViews for easily toggling between Active and Historical threats 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 c041e31699f42..f141f223c07dc 100644 --- a/projects/js-packages/components/components/threats-data-views/index.tsx +++ b/projects/js-packages/components/components/threats-data-views/index.tsx @@ -41,6 +41,7 @@ import { THREAT_TYPES, } from './constants'; import styles from './styles.module.scss'; +import ThreatsStatusToggleGroupControl from './threats-status-toggle-group-control'; /** * DataViews component for displaying security threats. @@ -146,10 +147,10 @@ export default function ThreatsDataViews( { /** * Compute values from the provided threats data. * - * @member {object[]} themes - List of unique themes included in the threats data. - * @member {object[]} plugins - List of unique plugins included in the threats data. + * @member {object[]} themes - List of unique themes included in the threats data. + * @member {object[]} plugins - plugins included in the threats data. * @member {object[]} signatures - List of unique threat signatures. - * @member {string[]} dataFields - List of unique fields. + * @member {string[]} dataFields - List of unique fields. */ const { themes, @@ -526,6 +527,13 @@ export default function ThreatsDataViews( { onChangeView={ onChangeView } paginationInfo={ paginationInfo } view={ view } + 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 00a493eea73de..46ca286e1d13e 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 @@ -87,7 +87,7 @@ Default.args = { fixedIn: '1.12.4', severity: 3, fixable: { fixer: 'update', target: '1.12.4', extensionStatus: 'inactive' }, - fixer: { status: 'in_progress', last_updated: new Date().toISOString() }, + fixer: { status: 'in_progress', lastUpdated: new Date().toISOString() }, status: 'current', filename: null, context: null, @@ -176,7 +176,7 @@ FixerStatuses.args = { severity: 4, fixer: null, fixedOn: '2024-07-15T22:01:42.000Z', - status: 'fixed', + status: 'current', fixable: { fixer: 'update', target: '6.4.4', extensionStatus: 'inactive' }, version: '6.4.3', source: '', @@ -190,7 +190,7 @@ FixerStatuses.args = { fixedIn: '1.2.4', severity: 3, fixable: { fixer: 'update', target: '1.12.4', extensionStatus: 'inactive' }, - fixer: { status: 'in_progress', last_updated: new Date().toISOString() }, + fixer: { status: 'in_progress', lastUpdated: new Date().toISOString() }, status: 'current', source: 'https://wpscan.com/vulnerability/733d8a02-0d44-4b78-bbb2-37e447acd2f3', extension: { @@ -209,7 +209,7 @@ FixerStatuses.args = { fixedIn: '2.22.22', severity: 3, fixable: { fixer: 'update', target: '1.12.4', extensionStatus: 'inactive' }, - fixer: { status: 'in_progress', last_updated: new Date( '1999-01-01' ).toISOString() }, + fixer: { status: 'in_progress', lastUpdated: new Date( '1999-01-01' ).toISOString() }, status: 'current', source: 'https://wpscan.com/vulnerability/733d8a02-0d44-4b78-bbb2-37e447acd2f3', extension: { @@ -260,6 +260,13 @@ FixerStatuses.args = { }, }, ], + filters: [ + { + field: 'status', + operator: 'isAny', + value: [ 'current' ], + }, + ], onFixThreats: () => alert( 'Threat fix action callback triggered! This is handled by the component consumer.' ), // eslint-disable-line no-alert onIgnoreThreats: () => @@ -280,7 +287,6 @@ FreeResults.args = { description: 'Versions 3.2.3 and earlier are affected by an issue where cached queries within shortcodes could lead to object injection. This is related to the recent WordPress 4.8.3 security release.This issue can only be exploited by users who can edit content and add shortcodes, but we still recommend all users running WooCommerce 3.x upgrade to 3.2 to mitigate this issue.', fixedIn: '3.2.4', - status: 'current', source: 'https://wpscan.com/vulnerability/1d0470df-4671-47ac-8d87-a165e8f7d502', extension: { name: 'WooCommerce', @@ -296,7 +302,6 @@ FreeResults.args = { description: 'The WooCommerce WordPress plugin was affected by an Authenticated Stored XSS security vulnerability.', fixedIn: '3.4.6', - status: 'current', source: 'https://wpscan.com/vulnerability/7275a176-d579-471a-8492-df8edbdf27de', extension: { name: 'WooCommerce', @@ -311,7 +316,6 @@ FreeResults.args = { description: 'The plugin was affected by an authenticated (admin+) RCE in the settings page due to input validation failure and weak $cache_path check in the WP Super Cache Settings -> Cache Location option. Direct access to the wp-cache-config.php file is not prohibited, so this vulnerability can be exploited for a web shell injection.\r\n\r\nAnother possible attack vector: from XSS (via another plugin affected by XSS) to RCE.', fixedIn: '1.7.2', - status: 'current', source: 'https://wpscan.com/vulnerability/733d8a02-0d44-4b78-bbb2-37e447acd2f3', extension: { name: 'WP Super Cache', 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 98789b2f794ec..820b07916687b 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 @@ -31,6 +31,11 @@ border-color: #EDFFEE; svg { - fill: black; + fill: var( --jp-black ); } } + +.toggle-group-control__option { + white-space: nowrap; + padding: 0 12px; +} diff --git a/projects/js-packages/components/components/threats-data-views/test/index.test.tsx b/projects/js-packages/components/components/threats-data-views/test/index.test.tsx index e6bcedd84ebdd..2e7dfea35d673 100644 --- a/projects/js-packages/components/components/threats-data-views/test/index.test.tsx +++ b/projects/js-packages/components/components/threats-data-views/test/index.test.tsx @@ -42,7 +42,6 @@ const data = [ type: 'plugin' as const, }, fixedIn: '3.2.4', - status: 'current' as const, }, ]; 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 new file mode 100644 index 0000000000000..d9282cc07839a --- /dev/null +++ b/projects/js-packages/components/components/threats-data-views/threats-status-toggle-group-control.tsx @@ -0,0 +1,160 @@ +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 ( + + + { sprintf( + /* translators: %d: number of active threats */ __( + 'Active threats (%d)', + 'jetpack' + ), + activeThreatsCount + ) } + + } + /> + + { sprintf( + /* translators: %d: number of historic threats */ + __( 'History (%d)', 'jetpack' ), + historicThreatsCount + ) } + + } + /> + + ); + } catch ( error ) { + return null; + } +}