Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protect card: Add the Scan/Threats status column (with tooltip) #38165

Merged
merged 40 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a16fe28
Protect card: Add the Last scan time (with tooltip) based on plugin s…
elliottprogrammer Jun 27, 2024
85b9f14
Fix "Lern more" typo in comment, per feedback.
elliottprogrammer Jun 30, 2024
751cdda
Add defaults to destructured var, per feedback.
elliottprogrammer Jun 30, 2024
91fe0eb
Use React "FC" type, per feedback.
elliottprogrammer Jun 30, 2024
a40f71d
Fix snake case naming & state naming, per feedback.
elliottprogrammer Jun 30, 2024
e43589c
CSS style fixes/updates, per feedback.
elliottprogrammer Jun 30, 2024
88ab09f
Show dynamic data in tooltips, per feedback; And cleanup/refactor.
elliottprogrammer Jun 30, 2024
410b999
Close tooltip/popover when outside focus, per feedback.
elliottprogrammer Jun 30, 2024
21df889
Fix tooltip close fires twice when tooltip focus switches to button c…
elliottprogrammer Jun 30, 2024
9e9fb38
Fix responsiveness, per feedback.
elliottprogrammer Jul 1, 2024
389b0fe
Only show "Last scan:" if value available, per feedback.
elliottprogrammer Jul 1, 2024
a207481
Merge branch 'trunk' into add/mj-protect-card-last-scan-time
elliottprogrammer Jul 1, 2024
3203c4a
Add cursor: pointer to tooltip button, per feedback.
elliottprogrammer Jul 1, 2024
831bf8a
Use --font-body-extra-small & fix comment typo, per feedback.
elliottprogrammer Jul 1, 2024
35726a1
Only show "Plugins & Themes" when scan data availavle, per feedback.
elliottprogrammer Jul 1, 2024
667b057
Fixup project versions.
elliottprogrammer Jul 1, 2024
db6d6be
Merge branch 'trunk' into add/mj-protect-card-last-scan-time
elliottprogrammer Jul 1, 2024
994ea2e
Merge branch 'trunk' into add/mj-protect-card-last-scan-time
elliottprogrammer Jul 1, 2024
bab0c5a
Allow for singular/plural in translations.
elliottprogrammer Jul 2, 2024
10e0b39
Move Popover into its own InfoTooltip component.
elliottprogrammer Jul 2, 2024
4385062
Protect card: Add the Scan/Threats status column (with tooltip)
elliottprogrammer Jul 2, 2024
e4213b6
Critical threats, tooltip tracking, & additional styles.
elliottprogrammer Jul 4, 2024
c26a73e
Fix core.threats not iterable error.
elliottprogrammer Jul 4, 2024
3a03548
Merge branch 'trunk' into add/mj-protect-card-last-scan-time
elliottprogrammer Jul 5, 2024
e00c6c6
Merge branch 'add/mj-protect-card-last-scan-time' into add/my-protect…
elliottprogrammer Jul 5, 2024
ab96aad
changelog.
elliottprogrammer Jul 5, 2024
4df8c71
Scan status "Off" when site not connected.
elliottprogrammer Jul 5, 2024
158ccaa
Remove unnecessary non-breaking space.
elliottprogrammer Jul 10, 2024
95c92c7
Remove unnecessary icon width & height properties is scss.
elliottprogrammer Jul 10, 2024
c3e054a
Combine singular/plural translations into one translated string, per …
elliottprogrammer Jul 10, 2024
162ccdf
Split up scan-threats into smaller components for readability, per fe…
elliottprogrammer Jul 10, 2024
0e9d04c
Hide Protect card description, per feedback.
elliottprogrammer Jul 11, 2024
b38310c
Show "Upgrade" button & secondary "View" button when no paid plan.
elliottprogrammer Jul 11, 2024
1855b08
Define Threat object in global.d.ts, per feedback.
elliottprogrammer Jul 12, 2024
1010f7c
Add scss comment explaining specific letter-spacing, per feedback
elliottprogrammer Jul 12, 2024
9f75120
scss, use --spacing-base where possible.
elliottprogrammer Jul 12, 2024
386f824
Add comment about removing the <ProductCard /> Description, per feedb…
elliottprogrammer Jul 12, 2024
60cbe2f
Split if condition into multiple lines, per feedback.
elliottprogrammer Jul 12, 2024
c5eb3db
Remove ternary & split into 2 returns instead, per feedback.
elliottprogrammer Jul 12, 2024
c6643cb
Merge branch 'trunk' into add/my-protect-card-scan-threats
elliottprogrammer Jul 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
import { __ } from '@wordpress/i18n';
import { PRODUCT_STATUSES } from '../../../constants';
import { useCallback, type FC } from 'react';
import useProduct from '../../../data/products/use-product';
import useAnalytics from '../../../hooks/use-analytics';
import ProductCard from '../../connected-product-card';
import ProtectValueSection from './protect-value-section';
import type { FC } from 'react';

const ProtectCard: FC< { admin: boolean } > = ( { admin } ) => {
// Override the primary action button to read "Protect your site" instead
// of the default text, "Learn more".
const primaryActionOverride = {
[ PRODUCT_STATUSES.ABSENT ]: {
label: __( 'Protect your site', 'jetpack-my-jetpack' ),
},
const { recordEvent } = useAnalytics();
const slug = 'protect';
const { detail } = useProduct( slug );
const { hasPaidPlanForProduct: hasProtectPaidPlan } = detail;

/**
* Called when secondary "View" button is clicked.
*/
const onViewButtonClick = useCallback( () => {
recordEvent( 'jetpack_myjetpack_product_card_manage_click', {
product: slug,
} );
}, [ recordEvent ] );

const shouldShowSecondaryButton = useCallback(
() => ! hasProtectPaidPlan,
[ hasProtectPaidPlan ]
);

const viewButton = {
href: 'admin.php?page=jetpack-protect',
label: __( 'View', 'jetpack-my-jetpack' ),
onClick: onViewButtonClick,
shouldShowButton: shouldShowSecondaryButton,
};

// This is a workaround to remove the Description from the product card. However if we end
// up needing to remove the Description from additional cards in the future, we might consider
// extending <ProductCard /> functionality to support that.
const noDescription = useCallback( () => null, [] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can leave a comment for the future reference that if we gonna remove description to more cards, we should consider extending <ProductCard /> functionality to support that.

Clever workaround here though! 🙌


return (
<ProductCard admin={ admin } slug="protect" primaryActionOverride={ primaryActionOverride }>
<ProductCard
admin={ admin }
slug={ slug }
upgradeInInterstitial={ true }
secondaryAction={ viewButton }
Description={ noDescription }
>
<ProtectValueSection />
</ProductCard>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Gridicon } from '@automattic/jetpack-components';
import { Popover } from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import { useState, useCallback, useRef } from 'react';
import useAnalytics from '../../../../hooks/use-analytics';
import type { FC, ReactNode } from 'react';

import './style.scss';

type Props = {
children: ReactNode;
icon?: string;
iconSize?: number;
tracksEventName?: string;
tracksEventProps?: { [ key: string ]: string | boolean | number };
};

export const InfoTooltip: FC< Props > = ( {
children,
icon = 'info-outline',
iconSize = 14,
tracksEventName,
tracksEventProps = {},
} ) => {
const { recordEvent } = useAnalytics();
const useTooltipRef = useRef< HTMLButtonElement >();
const isMobileViewport: boolean = useViewportMatch( 'medium', '<' );
const [ isPopoverVisible, setIsPopoverVisible ] = useState( false );

const toggleTooltip = useCallback(
() =>
setIsPopoverVisible( prevState => {
if ( ! prevState === true && tracksEventName ) {
recordEvent( `jetpack_${ tracksEventName }`, {
page: 'my-jetpack',
feature: 'jetpack-protect',
...tracksEventProps,
} );
}
return ! prevState;
} ),
[ recordEvent, tracksEventName, tracksEventProps ]
);

const hideTooltip = useCallback( () => {
// Don't hide the tooltip here if it's the tooltip button that was clicked (the button
// becoming the document's activeElement). Instead let toggleTooltip() handle the closing.
if ( useTooltipRef.current && ! useTooltipRef.current.contains( document.activeElement ) ) {
setIsPopoverVisible( false );
}
}, [ setIsPopoverVisible, useTooltipRef ] );

return (
<span>
<button className="info-tooltip__button" onClick={ toggleTooltip } ref={ useTooltipRef }>
<Gridicon icon={ icon } size={ iconSize } />
</button>
{ isPopoverVisible && (
<Popover
placement={ isMobileViewport ? 'top-end' : 'right' }
noArrow={ false }
offset={ 10 }
focusOnMount={ 'container' }
onClose={ hideTooltip }
>
{ children }
</Popover>
) }
</span>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.info-tooltip__button {
display: flex;
align-items: center;
background: transparent;
border: none;
color: var(--jp-gray-50);
padding: 2px;
cursor: pointer;
svg {
margin: 0 auto;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Gridicon } from '@automattic/jetpack-components';
import { Popover } from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import { __, sprintf } from '@wordpress/i18n';
import { useState, useCallback, useMemo, useRef } from 'react';
import { __, _n, sprintf } from '@wordpress/i18n';
import { useMemo } from 'react';
import useProduct from '../../../data/products/use-product';
import { getMyJetpackWindowInitialState } from '../../../data/utils/get-my-jetpack-window-state';
import { timeSince } from '../../../utils/time-since';
import { InfoTooltip } from './info-tooltip';
import { ScanAndThreatStatus } from './scan-threats';
import { useProtectTooltipCopy } from './use-protect-tooltip-copy';
import type { TooltipContent } from './use-protect-tooltip-copy';
import type { FC } from 'react';
Expand Down Expand Up @@ -39,11 +38,21 @@ const ProtectValueSection = () => {
}
return null;
}
return sprintf(
/* translators: `\xa0` is a non-breaking space. %1$d is the number (integer) of plugins and %2$d is the number (integer) of themes the site has. */
__( '%1$d plugins &\xa0%2$d\xa0themes', 'jetpack-my-jetpack' ),
pluginsCount,
themesCount
return (
sprintf(
/* translators: %d is the number of plugins installed on the site. */
_n( '%d plugin', '%d plugins', pluginsCount, 'jetpack-my-jetpack' ),
pluginsCount
) +
' ' +
/* translators: The ampersand symbol here (&) is meaning "and". */
__( '&', 'jetpack-my-jetpack' ) +
'\xa0' + // `\xa0` is a non-breaking space.
sprintf(
/* translators: %d is the number of themes installed on the site. */
_n( '%d theme', '%d themes', themesCount, 'jetpack-my-jetpack' ).replace( ' ', '\xa0' ), // `\xa0` is a non-breaking space.
themesCount
)
);
}, [ isPluginActive, timeSinceLastScan, pluginsCount, themesCount ] );

Expand All @@ -65,59 +74,30 @@ const ValueSection: FC< {
lastScanText?: string;
tooltipContent: TooltipContent;
} > = ( { isProtectActive, lastScanText, tooltipContent } ) => {
const useTooltipRef = useRef< HTMLButtonElement >();
const isMobileViewport: boolean = useViewportMatch( 'medium', '<' );
const [ isPopoverVisible, setIsPopoverVisible ] = useState( false );
// TODO: `scanThreatsTooltip` will be utilized in a followup PR.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { pluginsThemesTooltip, scanThreatsTooltip } = tooltipContent;

const toggleTooltip = useCallback(
() => setIsPopoverVisible( prevState => ! prevState ),
[ setIsPopoverVisible ]
);
const hideTooltip = useCallback( () => {
// Don't hide the tooltip here if it's the tooltip button that was clicked (the button
// becoming the document's activeElement). Instead let toggleTooltip() handle the closing.
if ( useTooltipRef.current && ! useTooltipRef.current.contains( document.activeElement ) ) {
setIsPopoverVisible( false );
}
}, [ setIsPopoverVisible, useTooltipRef ] );
const { pluginsThemesTooltip } = tooltipContent;

return (
<>
<div className="value-section__last-scan">
{ lastScanText && <div>{ lastScanText }</div> }
{ ! isProtectActive && (
<div>
<button
className="value-section__tooltip-button"
onClick={ toggleTooltip }
ref={ useTooltipRef }
>
<Gridicon icon="info-outline" size={ 14 } />
</button>
{ isPopoverVisible && (
<Popover
placement={ isMobileViewport ? 'top-end' : 'right' }
noArrow={ false }
offset={ 10 }
focusOnMount={ 'container' }
onClose={ hideTooltip }
>
<>
<h3 className="value-section__tooltip-heading">{ pluginsThemesTooltip.title }</h3>
<p className="value-section__tooltip-content">{ pluginsThemesTooltip.text }</p>
</>
</Popover>
) }
</div>
<InfoTooltip
tracksEventName={ 'protect_card_tooltip_open' }
tracksEventProps={ {
location: 'plugins&themes',
status: 'inactive',
} }
>
<>
<h3 className="value-section__tooltip-heading">{ pluginsThemesTooltip.title }</h3>
<p className="value-section__tooltip-content">{ pluginsThemesTooltip.text }</p>
</>
elliottprogrammer marked this conversation as resolved.
Show resolved Hide resolved
</InfoTooltip>
) }
</div>
<div className="value-section">
<div className="value-section__scan-threats">
<div className="value-section__heading">Scan</div>
<div></div>
<ScanAndThreatStatus />
</div>
<div className="value-section__auto-firewall">
<div className="value-section__heading">Auto-Firewall</div>
Expand Down
Loading
Loading