Skip to content

Commit

Permalink
Use inline button for primary fixer action
Browse files Browse the repository at this point in the history
  • Loading branch information
nateweller committed Oct 28, 2024
1 parent f919501 commit 7a4a251
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 352 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Button, Text, ActionPopover } from '@automattic/jetpack-components';
import { Threat } from '@automattic/jetpack-scan';
import { ExternalLink } from '@wordpress/components';
import { createInterpolateElement, useCallback, useMemo, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { PAID_PLUGIN_SUPPORT_URL } from '../threats-data-views/constants';
import styles from './styles.module.scss';
import { fixerStatusIsStale } from './utils';

/**
* ThreatFixerButton
*
* @param {object} props - Component props.
* @param {object} props.threat - The threat.
* @param {Function} props.onClick - The onClick function.
* @param {string} props.className - The className.
*
* @return {JSX.Element} The component.
*/
export default function ThreatFixerButton( {
threat,
className,
onClick,
}: {
threat: Threat;
className?: string;
onClick: ( items: Threat[] ) => void;
} ): JSX.Element {
const [ isPopoverVisible, setIsPopoverVisible ] = useState( false );

const [ anchor, setAnchor ] = useState( null );

const children = useMemo( () => {
if ( ! threat.fixable ) {
return null;
}
if ( threat.fixer && threat.fixer.error ) {
return __( 'Error', 'jetpack' );
}
if ( threat.fixer && threat.fixer.status === 'in_progress' ) {
return __( 'Fixing…', 'jetpack' );
}
if ( threat.fixable.fixer === 'delete' ) {
return __( 'Delete', 'jetpack' );
}
if ( threat.fixable.fixer === 'update' ) {
return __( 'Update', 'jetpack' );
}
return __( 'Fix', 'jetpack' );
}, [ threat.fixable, threat.fixer ] );

const errorMessage = useMemo( () => {
if ( threat.fixer && fixerStatusIsStale( threat.fixer ) ) {
return __( 'Fixer is taking longer than expected.', 'jetpack' );
}

if ( threat.fixer && 'error' in threat.fixer && threat.fixer.error ) {
return __( 'An error occurred auto-fixing this threat.', 'jetpack' );
}

return null;
}, [ threat.fixer ] );

const handleClick = useCallback(
( event: React.MouseEvent ) => {
event.stopPropagation();
if ( errorMessage && ! isPopoverVisible ) {
setIsPopoverVisible( true );
return;
}
onClick( [ threat ] );
},
[ onClick, errorMessage, isPopoverVisible, 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 && threat.fixer.status === 'in_progress' && ! errorMessage }
isLoading={ threat.fixer && threat.fixer.status === 'in_progress' }
isDestructive={
( threat.fixable && threat.fixable.fixer === 'delete' ) ||
( 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 }
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={ PAID_PLUGIN_SUPPORT_URL }
className={ styles[ 'support-link' ] }
/>
),
}
) }
</Text>
</ActionPopover>
) }
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import ThreatFixerButton from '../index.js';

export default {
title: 'JS Packages/Components/Threat Fixer Button',
component: ThreatFixerButton,
};

export const Default = args => <ThreatFixerButton { ...args } />;
Default.args = {
threat: { fixable: { fixer: 'edit' } },
onClick: () => alert( 'Edit fixer callback triggered' ), // eslint-disable-line no-alert
};

export const Update = args => <ThreatFixerButton { ...args } />;
Update.args = {
threat: { fixable: { fixer: 'update' } },
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 Loading = args => <ThreatFixerButton { ...args } />;
Loading.args = {
threat: { fixable: { fixer: 'edit' }, fixer: { status: 'in_progress' } },
onClick: () => alert( 'Fixer callback triggered' ), // eslint-disable-line no-alert
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.support-link {
color: inherit;

&:focus,
&:hover {
color: inherit;
box-shadow: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ThreatFixStatus } from '@automattic/jetpack-scan';

const FIXER_IS_STALE_THRESHOLD = 1000 * 60 * 60 * 24; // 24 hours

export const fixerTimestampIsStale = ( lastUpdatedTimestamp: string ) => {
const now = new Date();
const lastUpdated = new Date( lastUpdatedTimestamp );
return now.getTime() - lastUpdated.getTime() >= FIXER_IS_STALE_THRESHOLD;
};

export const fixerStatusIsStale = ( fixerStatus: ThreatFixStatus ) => {
return (
'status' in fixerStatus &&
fixerStatus.status === 'in_progress' &&
fixerTimestampIsStale( fixerStatus.last_updated )
);
};

This file was deleted.

Loading

0 comments on commit 7a4a251

Please sign in to comment.