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