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
) }