diff --git a/projects/js-packages/components/changelog/add-components-threat-fixer-button-tooltips b/projects/js-packages/components/changelog/add-components-threat-fixer-button-tooltips
new file mode 100644
index 0000000000000..e230d4e10ef34
--- /dev/null
+++ b/projects/js-packages/components/changelog/add-components-threat-fixer-button-tooltips
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Adds tooltips for each ThreatFixerButton state
diff --git a/projects/js-packages/components/components/threat-fixer-button/index.tsx b/projects/js-packages/components/components/threat-fixer-button/index.tsx
index 7c1c92fcc718a..cd15c0294c313 100644
--- a/projects/js-packages/components/components/threat-fixer-button/index.tsx
+++ b/projects/js-packages/components/components/threat-fixer-button/index.tsx
@@ -1,8 +1,13 @@
-import { Button, Text, ActionPopover } from '@automattic/jetpack-components';
-import { CONTACT_SUPPORT_URL, type Threat, fixerStatusIsStale } from '@automattic/jetpack-scan';
-import { ExternalLink } from '@wordpress/components';
-import { createInterpolateElement, useCallback, useMemo, useState } from '@wordpress/element';
-import { __, sprintf } from '@wordpress/i18n';
+import { Button } from '@automattic/jetpack-components';
+import {
+ type Threat,
+ fixerIsInError,
+ fixerIsInProgress,
+ fixerStatusIsStale,
+} from '@automattic/jetpack-scan';
+import { Tooltip } from '@wordpress/components';
+import { useCallback, useMemo } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
import styles from './styles.module.scss';
/**
@@ -24,119 +29,133 @@ export default function ThreatFixerButton( {
onClick: ( items: Threat[] ) => void;
className?: string;
} ): JSX.Element {
- const [ isPopoverVisible, setIsPopoverVisible ] = useState( false );
-
- const [ anchor, setAnchor ] = useState( null );
+ const fixerState = useMemo( () => {
+ const inProgress = threat.fixer && fixerIsInProgress( threat.fixer );
+ const error = threat.fixer && fixerIsInError( threat.fixer );
+ const stale = threat.fixer && fixerStatusIsStale( threat.fixer );
+ return { inProgress, error, stale };
+ }, [ threat.fixer ] );
- const children = useMemo( () => {
+ const tooltipText = useMemo( () => {
if ( ! threat.fixable ) {
return null;
}
- if ( threat.fixer && 'error' in threat.fixer && threat.fixer.error ) {
- return __( 'Error', 'jetpack' );
+
+ if ( fixerState.error ) {
+ return __( 'An error occurred auto-fixing this threat.', 'jetpack' );
}
- if ( threat.fixer && 'status' in threat.fixer && threat.fixer.status === 'in_progress' ) {
- return __( 'Fixing…', 'jetpack' );
+
+ if ( fixerState.stale ) {
+ return __( 'The auto-fixer is taking longer than expected.', 'jetpack' );
}
- if ( threat.fixable.fixer === 'delete' ) {
- return __( 'Delete', 'jetpack' );
+
+ if ( fixerState.inProgress ) {
+ return __( 'An auto-fixer is in progress.', 'jetpack' );
}
- if ( threat.fixable.fixer === 'update' ) {
- return __( 'Update', 'jetpack' );
+
+ switch ( threat.fixable.fixer ) {
+ case 'delete':
+ if ( threat.filename ) {
+ if ( threat.filename.endsWith( '/' ) ) {
+ return __( 'Deletes the directory that the infected file is in.', 'jetpack' );
+ }
+
+ if ( threat.signature === 'Core.File.Modification' ) {
+ return __( 'Deletes the unexpected file in a core WordPress directory.', 'jetpack' );
+ }
+
+ return __( 'Deletes the infected file.', 'jetpack' );
+ }
+
+ if ( threat.extension?.type === 'plugin' ) {
+ return __( 'Deletes the plugin directory to fix the threat.', 'jetpack' );
+ }
+
+ if ( threat.extension?.type === 'theme' ) {
+ return __( 'Deletes the theme directory to fix the threat.', 'jetpack' );
+ }
+ break;
+ case 'update':
+ return __( 'Upgrades the plugin or theme to a newer version.', 'jetpack' );
+ case 'replace':
+ case 'rollback':
+ if ( threat.filename ) {
+ return threat.signature === 'Core.File.Modification'
+ ? __(
+ 'Replaces the modified core WordPress file with the original clean version from the WordPress source code.',
+ 'jetpack'
+ )
+ : __(
+ 'Replaces the infected file with a previously backed up version that is clean.',
+ 'jetpack'
+ );
+ }
+
+ if ( threat.signature === 'php_hardening_WP_Config_NoSalts_001' ) {
+ return __(
+ 'Replaces the default salt keys in wp-config.php with unique ones.',
+ 'jetpack'
+ );
+ }
+ break;
+ default:
+ return __( 'An auto-fixer is available.', 'jetpack' );
}
- return __( 'Fix', 'jetpack' );
- }, [ threat.fixable, threat.fixer ] );
+ }, [ threat, fixerState ] );
- const errorMessage = useMemo( () => {
- if ( threat.fixer && fixerStatusIsStale( threat.fixer ) ) {
- return __( 'The fixer is taking longer than expected.', 'jetpack' );
+ const buttonText = useMemo( () => {
+ if ( ! threat.fixable ) {
+ return null;
}
- if ( threat.fixer && 'error' in threat.fixer && threat.fixer.error ) {
- return __( 'An error occurred auto-fixing this threat.', 'jetpack' );
+ if ( fixerState.error ) {
+ return __( 'Error', 'jetpack' );
}
- return null;
- }, [ threat.fixer ] );
+ switch ( threat.fixable.fixer ) {
+ case 'delete':
+ return __( 'Delete', 'jetpack' );
+ case 'update':
+ return __( 'Update', 'jetpack' );
+ case 'replace':
+ case 'rollback':
+ return __( 'Replace', 'jetpack' );
+ default:
+ return __( 'Fix', 'jetpack' );
+ }
+ }, [ threat.fixable, fixerState.error ] );
const handleClick = useCallback(
( event: React.MouseEvent ) => {
event.stopPropagation();
- if ( errorMessage && ! isPopoverVisible ) {
- setIsPopoverVisible( true );
- return;
- }
onClick( [ threat ] );
},
- [ onClick, errorMessage, isPopoverVisible, threat ]
+ [ onClick, threat ]
);
- const closePopover = useCallback( () => {
- setIsPopoverVisible( false );
- }, [] );
-
if ( ! threat.fixable ) {
return null;
}
return (
-
- { isPopoverVisible && (
-
+
- ) }
+ children={ buttonText }
+ className={ className }
+ isLoading={ fixerState.inProgress }
+ isDestructive={
+ ( threat.fixable && threat.fixable.fixer === 'delete' ) ||
+ fixerState.error ||
+ fixerState.stale
+ }
+ style={ { minWidth: '72px' } }
+ />
+
);
}
diff --git a/projects/js-packages/components/components/threat-fixer-button/stories/index.stories.tsx b/projects/js-packages/components/components/threat-fixer-button/stories/index.stories.tsx
index 6b378030a2d89..4f346d04d394b 100644
--- a/projects/js-packages/components/components/threat-fixer-button/stories/index.stories.tsx
+++ b/projects/js-packages/components/components/threat-fixer-button/stories/index.stories.tsx
@@ -3,12 +3,59 @@ import ThreatFixerButton from '../index.js';
export default {
title: 'JS Packages/Components/Threat Fixer Button',
component: ThreatFixerButton,
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+ parameters: {
+ layout: 'centered',
+ },
};
export const Default = args => ;
Default.args = {
threat: { fixable: { fixer: 'edit' } },
- onClick: () => alert( 'Edit fixer callback triggered' ), // eslint-disable-line no-alert
+ onClick: () => alert( 'Fixer callback triggered' ), // eslint-disable-line no-alert
+};
+
+export const DeletePlugin = args => ;
+DeletePlugin.args = {
+ threat: { fixable: { fixer: 'delete' }, extension: { type: 'plugin' } },
+ onClick: () => alert( 'Delete fixer callback triggered' ), // eslint-disable-line no-alert
+};
+
+export const DeleteTheme = args => ;
+DeleteTheme.args = {
+ threat: { fixable: { fixer: 'delete' }, extension: { type: 'theme' } },
+ onClick: () => alert( 'Delete fixer callback triggered' ), // eslint-disable-line no-alert
+};
+
+export const DeleteDirectory = args => ;
+DeleteDirectory.args = {
+ threat: { fixable: { fixer: 'delete' }, filename: '/var/www/html/wp-content/uploads/' },
+ onClick: () => alert( 'Delete fixer callback triggered' ), // eslint-disable-line no-alert
+};
+
+export const DeleteCoreFile = args => ;
+DeleteCoreFile.args = {
+ threat: {
+ fixable: { fixer: 'delete' },
+ signature: 'Core.File.Modification',
+ filename: '/var/www/html/wp-admin/index.php',
+ },
+ onClick: () => alert( 'Delete fixer callback triggered' ), // eslint-disable-line no-alert
+};
+
+export const DeleteFile = args => ;
+DeleteFile.args = {
+ threat: {
+ fixable: { fixer: 'delete' },
+ filename: '/var/www/html/wp-content/uploads/jptt_eicar.php',
+ },
+ onClick: () => alert( 'Delete fixer callback triggered' ), // eslint-disable-line no-alert
};
export const Update = args => ;
@@ -17,14 +64,48 @@ Update.args = {
onClick: () => alert( 'Update fixer callback triggered' ), // eslint-disable-line no-alert
};
-export const Delete = args => ;
-Delete.args = {
- threat: { fixable: { fixer: 'delete' } },
- onClick: () => alert( 'Delete fixer callback triggered' ), // eslint-disable-line no-alert
+export const ReplaceSaltKeys = args => ;
+ReplaceSaltKeys.args = {
+ threat: { fixable: { fixer: 'replace' }, signature: 'php_hardening_WP_Config_NoSalts_001' },
+ onClick: () => alert( 'Replace fixer callback triggered' ), // eslint-disable-line no-alert
+};
+
+export const ReplaceCoreFile = args => ;
+ReplaceCoreFile.args = {
+ threat: {
+ fixable: { fixer: 'replace' },
+ signature: 'Core.File.Modification',
+ filename: '/var/www/html/wp-admin/index.php',
+ },
+ onClick: () => alert( 'Replace fixer callback triggered' ), // eslint-disable-line no-alert
+};
+
+export const ReplaceFile = args => ;
+ReplaceFile.args = {
+ threat: {
+ fixable: { fixer: 'replace' },
+ filename: '/var/www/html/wp-content/uploads/jptt_eicar.php',
+ },
+ onClick: () => alert( 'Replace fixer callback triggered' ), // eslint-disable-line no-alert
};
export const Loading = args => ;
Loading.args = {
- threat: { fixable: { fixer: 'edit' }, fixer: { status: 'in_progress' } },
- onClick: () => alert( 'Fixer callback triggered' ), // eslint-disable-line no-alert
+ threat: { fixable: { fixer: 'update' }, fixer: { status: 'in_progress' } },
+ onClick: () => alert( 'In progress fixer callback triggered' ), // eslint-disable-line no-alert
+};
+
+export const StaleFixer = args => ;
+StaleFixer.args = {
+ threat: {
+ fixable: { fixer: 'update' },
+ fixer: { status: 'in_progress', lastUpdated: new Date( '1999-01-01' ).toISOString() },
+ },
+ onClick: () => alert( 'Stale fixer callback triggered.' ), // eslint-disable-line no-alert
+};
+
+export const ErrorFixer = args => ;
+ErrorFixer.args = {
+ threat: { fixable: { fixer: 'update' }, fixer: { error: 'error' } },
+ onClick: () => alert( 'Error fixer callback triggered.' ), // eslint-disable-line no-alert
};
diff --git a/projects/js-packages/components/components/threat-fixer-button/styles.module.scss b/projects/js-packages/components/components/threat-fixer-button/styles.module.scss
index 071761daff049..ec490b100f8a9 100644
--- a/projects/js-packages/components/components/threat-fixer-button/styles.module.scss
+++ b/projects/js-packages/components/components/threat-fixer-button/styles.module.scss
@@ -7,3 +7,10 @@
box-shadow: none;
}
}
+
+.tooltip {
+ margin-top: var( --spacing-base ) ;
+ max-width: 240px;
+ border-radius: 4px;
+ text-align: left;
+}
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 f141f223c07dc..aec1f1c3086a6 100644
--- a/projects/js-packages/components/components/threats-data-views/index.tsx
+++ b/projects/js-packages/components/components/threats-data-views/index.tsx
@@ -385,7 +385,7 @@ export default function ThreatsDataViews( {
? [
{
id: THREAT_FIELD_AUTO_FIX,
- label: __( 'Auto-Fix', 'jetpack' ),
+ label: __( 'Auto-fix', 'jetpack' ),
enableHiding: false,
elements: [
{
@@ -426,7 +426,7 @@ export default function ThreatsDataViews( {
if ( dataFields.includes( 'fixable' ) ) {
result.push( {
id: THREAT_ACTION_FIX,
- label: __( 'Auto-Fix', 'jetpack' ),
+ label: __( 'Auto-fix', 'jetpack' ),
isPrimary: true,
supportsBulk: true,
callback: onFixThreats,
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 46ca286e1d13e..52afdded5ad24 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
@@ -30,7 +30,7 @@ Default.args = {
firstDetected: '2024-10-07T20:45:06.000Z',
fixedIn: null,
severity: 8,
- fixable: { fixer: 'rollback', target: 'January 26, 2024, 6:49 am', extensionStatus: '' },
+ fixable: { fixer: 'delete' },
fixer: { status: 'not_started' },
status: 'current',
filename: '/var/www/html/wp-content/index.php',
@@ -268,7 +268,7 @@ FixerStatuses.args = {
},
],
onFixThreats: () =>
- alert( 'Threat fix action callback triggered! This is handled by the component consumer.' ), // eslint-disable-line no-alert
+ alert( 'Fix threat 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: () =>
diff --git a/projects/js-packages/scan/changelog/add-components-threat-fixer-button-tooltips b/projects/js-packages/scan/changelog/add-components-threat-fixer-button-tooltips
new file mode 100644
index 0000000000000..d992a0cc39a03
--- /dev/null
+++ b/projects/js-packages/scan/changelog/add-components-threat-fixer-button-tooltips
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Adds fixer utility functions
diff --git a/projects/js-packages/scan/src/utils/index.ts b/projects/js-packages/scan/src/utils/index.ts
index 1c727460eff03..be4fe59047694 100644
--- a/projects/js-packages/scan/src/utils/index.ts
+++ b/projects/js-packages/scan/src/utils/index.ts
@@ -20,10 +20,18 @@ export const fixerTimestampIsStale = ( lastUpdatedTimestamp: string ) => {
return now.getTime() - lastUpdated.getTime() >= FIXER_IS_STALE_THRESHOLD;
};
+export const fixerIsInError = ( fixerStatus: ThreatFixStatus ) => {
+ return 'error' in fixerStatus && fixerStatus.error;
+};
+
+export const fixerIsInProgress = ( fixerStatus: ThreatFixStatus ) => {
+ return 'status' in fixerStatus && fixerStatus.status === 'in_progress';
+};
+
export const fixerStatusIsStale = ( fixerStatus: ThreatFixStatus ) => {
return (
- 'status' in fixerStatus &&
- fixerStatus.status === 'in_progress' &&
+ fixerIsInProgress( fixerStatus ) &&
+ 'lastUpdated' in fixerStatus &&
fixerTimestampIsStale( fixerStatus.lastUpdated )
);
};