Skip to content

Commit

Permalink
ThreatFixerButton: Add tooltips alt (#40111)
Browse files Browse the repository at this point in the history
  • Loading branch information
dkmyta authored Nov 13, 2024
1 parent 88d9ec8 commit 494d36b
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 103 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Adds tooltips for each ThreatFixerButton state
199 changes: 109 additions & 90 deletions projects/js-packages/components/components/threat-fixer-button/index.tsx
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -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 (
<div>
<Button
size="small"
weight="regular"
variant="secondary"
onClick={ handleClick }
children={ children }
className={ className }
disabled={
threat.fixer &&
'status' in threat.fixer &&
threat.fixer.status === 'in_progress' &&
! errorMessage
}
isLoading={
threat.fixer && 'status' in threat.fixer && threat.fixer.status === 'in_progress'
}
isDestructive={
( threat.fixable && threat.fixable.fixer === 'delete' ) ||
( threat.fixer && 'error' in threat.fixer && threat.fixer.error ) ||
( threat.fixer && fixerStatusIsStale( threat.fixer ) )
}
style={ { minWidth: '72px' } }
ref={ setAnchor }
/>
{ isPopoverVisible && (
<ActionPopover
anchor={ anchor }
buttonContent={ __( 'Retry Fix', 'jetpack' ) }
hideCloseButton={ true }
noArrow={ false }
<Tooltip className={ styles.tooltip } text={ tooltipText }>
<Button
size="small"
weight="regular"
variant="secondary"
onClick={ handleClick }
onClose={ closePopover }
title={ __( 'Auto-fix error', 'jetpack' ) }
>
<Text>
{ createInterpolateElement(
sprintf(
/* translators: placeholder is an error message. */
__(
'%s Please try again or <supportLink>contact support</supportLink>.',
'jetpack'
),
errorMessage
),
{
supportLink: (
<ExternalLink
href={ CONTACT_SUPPORT_URL }
className={ styles[ 'support-link' ] }
/>
),
}
) }
</Text>
</ActionPopover>
) }
children={ buttonText }
className={ className }
isLoading={ fixerState.inProgress }
isDestructive={
( threat.fixable && threat.fixable.fixer === 'delete' ) ||
fixerState.error ||
fixerState.stale
}
style={ { minWidth: '72px' } }
/>
</Tooltip>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,59 @@ import ThreatFixerButton from '../index.js';
export default {
title: 'JS Packages/Components/Threat Fixer Button',
component: ThreatFixerButton,
decorators: [
Story => (
<div style={ { height: '175px' } }>
<Story />
</div>
),
],
parameters: {
layout: 'centered',
},
};

export const Default = args => <ThreatFixerButton { ...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 => <ThreatFixerButton { ...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 => <ThreatFixerButton { ...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 => <ThreatFixerButton { ...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 => <ThreatFixerButton { ...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 => <ThreatFixerButton { ...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 => <ThreatFixerButton { ...args } />;
Expand All @@ -17,14 +64,48 @@ Update.args = {
onClick: () => alert( 'Update fixer callback triggered' ), // eslint-disable-line no-alert
};

export const Delete = args => <ThreatFixerButton { ...args } />;
Delete.args = {
threat: { fixable: { fixer: 'delete' } },
onClick: () => alert( 'Delete fixer callback triggered' ), // eslint-disable-line no-alert
export const ReplaceSaltKeys = args => <ThreatFixerButton { ...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 => <ThreatFixerButton { ...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 => <ThreatFixerButton { ...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 => <ThreatFixerButton { ...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 => <ThreatFixerButton { ...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 => <ThreatFixerButton { ...args } />;
ErrorFixer.args = {
threat: { fixable: { fixer: 'update' }, fixer: { error: 'error' } },
onClick: () => alert( 'Error fixer callback triggered.' ), // eslint-disable-line no-alert
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@
box-shadow: none;
}
}

.tooltip {
margin-top: var( --spacing-base ) ;
max-width: 240px;
border-radius: 4px;
text-align: left;
}
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ export default function ThreatsDataViews( {
? [
{
id: THREAT_FIELD_AUTO_FIX,
label: __( 'Auto-Fix', 'jetpack' ),
label: __( 'Auto-fix', 'jetpack' ),
enableHiding: false,
elements: [
{
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 494d36b

Please sign in to comment.