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 beb94c8a3b4c9..66846fcde9948 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,8 @@ -import { type Threat } from '@automattic/jetpack-scan'; +import { ThreatStatus, type Threat } 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 { Action, ActionButton, @@ -12,7 +16,7 @@ import { type View, } from '@wordpress/dataviews'; import { dateI18n } from '@wordpress/date'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Icon } from '@wordpress/icons'; import { useCallback, useMemo, useState } from 'react'; import Badge from '../badge'; @@ -22,6 +26,73 @@ import FixerStatusIcon, { FixerStatusBadge } from './fixer-status'; import styles from './styles.module.scss'; import { getThreatIcon, getThreatSubtitle, getThreatType } from './utils'; +/** + * ToggleGroupControl component for filtering threats by status. + * @param {object} props - Component props. + * @param {number} props.activeThreatsCount - Number of active threats. + * @param {number} props.historicThreatsCount - Number of historic threats. + * @param {boolean} props.isViewingActiveThreats - Whether the active status is selected. + * @param {boolean} props.isViewingHistoricThreats - Whether the historic status is selected. + * @param {Function} props.onStatusFilterChange - Callback function to handle the status filter change. + * @return {JSX.Element|null} The component or null. + */ +export function ThreatsStatusToggleGroupControl( { + activeThreatsCount, + historicThreatsCount, + isViewingActiveThreats, + isViewingHistoricThreats, + onStatusFilterChange, +}: { + activeThreatsCount: number; + historicThreatsCount: number; + isViewingActiveThreats: boolean; + isViewingHistoricThreats: boolean; + onStatusFilterChange: ( newValue: string ) => void; +} ): JSX.Element { + if ( ! ( activeThreatsCount && historicThreatsCount ) ) { + return null; + } + + let selectedValue = ''; + if ( isViewingActiveThreats ) { + selectedValue = 'active'; + } else if ( isViewingHistoricThreats ) { + selectedValue = 'historic'; + } + + return ( + + + { sprintf( + /* translators: %d: number of active threats */ __( 'Active (%d)', 'jetpack' ), + activeThreatsCount + ) } + + } + /> + + { sprintf( + /* translators: %d: number of historic threats */ + __( 'Historic (%d)', 'jetpack' ), + historicThreatsCount + ) } + + } + /> + + ); +} + /** * DataViews component for displaying security threats. * @@ -367,6 +438,18 @@ export default function ThreatsDataViews( { return result; }, [ extensions, signatures, dataFields, view ] ); + const isStatusFilterSelected = ( 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 ) ) + ); + + const isViewingActiveThreats = isStatusFilterSelected( [ 'current' ] ); + const isViewingHistoricThreats = isStatusFilterSelected( [ 'fixed', 'ignored' ] ); + /** * DataView actions - collection of operations that can be performed upon each record. * @@ -468,6 +551,53 @@ export default function ThreatsDataViews( { */ const getItemId = useCallback( ( item: Threat ) => item.id.toString(), [] ); + /** + * 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' ], + } ); + } + + setView( { + ...view, + filters: updatedFilters, + } ); + }, + [ view ] + ); + + /** + * Compute the number of active and historic threats. + */ + const activeThreatsCount = useMemo( + () => data.filter( item => item.status === 'current' ).length, + [ data ] + ); + + /** + * Compute the number of active and historic threats. + */ + const historicThreatsCount = useMemo( + () => data.filter( item => [ 'fixed', 'ignored' ].includes( item.status ) ).length, + [ data ] + ); + return ( + } /> ); } 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 4248f49871d5a..21bbc984a3dcf 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 @@ -126,3 +126,7 @@ box-shadow: none; } } + +.toggle-control { + white-space: nowrap; +}