Skip to content

Commit

Permalink
Add ThreatsDataView
Browse files Browse the repository at this point in the history
  • Loading branch information
nateweller committed Oct 25, 2024
1 parent c2b7dc0 commit cec6f1e
Show file tree
Hide file tree
Showing 20 changed files with 1,888 additions and 25 deletions.
357 changes: 357 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add ThreatsDataView component
39 changes: 39 additions & 0 deletions projects/js-packages/components/components/badge/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import clsx from 'clsx';
import React from 'react';
import styles from './style.module.scss';

type BadgeProps = {
children?: React.ReactNode;
className?: string;
variant?: 'success' | 'warning' | 'danger';
[ key: string ]: unknown;
};

/**
* Badge component
*
* @param {object} props - The component properties.
* @param {string} props.variant - The badge variant (i.e. 'success', 'warning', 'danger').
* @param {JSX.Element} props.children - Badge text or content.
* @param {string} props.className - Additional class name to pass to the Badge component.
*
* @return {React.ReactElement} The `Badge` component.
*/
const Badge: React.FC< BadgeProps > = ( { children, className, variant = 'info', ...props } ) => {
const classes = clsx(
styles.badge,
{
[ styles[ 'is-success' ] ]: variant === 'success',
[ styles[ 'is-warning' ] ]: variant === 'warning',
[ styles[ 'is-danger' ] ]: variant === 'danger',
},
className
);
return (
<span className={ classes } { ...props }>
{ children }
</span>
);
};

export default Badge;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Badge from '../index';

export default {
title: 'JS Packages/Components/Badge',
component: Badge,
argTypes: {
type: {
control: {
type: 'select',
},
options: [ 'info', 'danger', 'warning', 'success' ],
},
},
};

const Template = args => <Badge { ...args } />;

export const _default = Template.bind( {} );
_default.args = {
type: 'info',
children: 'Hello World',
};
25 changes: 25 additions & 0 deletions projects/js-packages/components/components/badge/style.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.badge {
display: inline-block;
border-radius: 4px;
background-color: var(--jp-gray-0);
color: var(--jp-gray-80);
padding: 4px 8px;
font-size: 13px;
font-weight: 400;
line-height: 16px;

&.is-success {
background-color: var(--jp-green-5);
color: var(--jp-green-50);
}

&.is-warning {
background-color: var(--jp-yellow-5);
color: var(--jp-yellow-60);
}

&.is-danger {
background-color: var(--jp-red-5);
color: var(--jp-red-70);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ const IconTooltip: React.FC< IconTooltipProps > = ( {
wide = false,
inline = true,
shift = false,
hoverShow = false,
} ) => {
const POPOVER_HELPER_WIDTH = 124;
const [ isVisible, setIsVisible ] = useState( false );
const [ timeoutId, setTimeoutId ] = useState( null );

const hideTooltip = useCallback( () => setIsVisible( false ), [ setIsVisible ] );
const toggleTooltip = useCallback(
e => {
Expand All @@ -53,6 +56,26 @@ const IconTooltip: React.FC< IconTooltipProps > = ( {
[ isVisible, setIsVisible ]
);

const handleMouseEnter = useCallback( () => {
if ( hoverShow ) {
if ( timeoutId ) {
clearTimeout( timeoutId );
setTimeoutId( null );
}
setIsVisible( true );
}
}, [ hoverShow, timeoutId ] );

const handleMouseLeave = useCallback( () => {
if ( hoverShow ) {
const id = setTimeout( () => {
setIsVisible( false );
setTimeoutId( null );
}, 100 );
setTimeoutId( id );
}
}, [ hoverShow ] );

const args = {
// To be compatible with deprecating prop `position`.
position: placementsToPositions( placement ),
Expand All @@ -79,7 +102,12 @@ const IconTooltip: React.FC< IconTooltipProps > = ( {
const isForcedToShow = isAnchorWrapper && forceShow;

return (
<div className={ wrapperClassNames } data-testid="icon-tooltip_wrapper">
<div
className={ wrapperClassNames }
data-testid="icon-tooltip_wrapper"
onMouseEnter={ handleMouseEnter }
onMouseLeave={ handleMouseLeave }
>
{ ! isAnchorWrapper && (
<Button variant="link" onMouseDown={ toggleTooltip }>
<Gridicon className={ iconClassName } icon={ iconCode } size={ iconSize } />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export default {
wide: {
control: { type: 'boolean' },
},
hoverShow: {
control: { type: 'boolean' },
},
},
};

Expand Down Expand Up @@ -106,3 +109,11 @@ Wide.args = {
wide: true,
placement: 'bottom-start',
};

export const HoverShow = Template.bind( {} );
HoverShow.args = {
title: 'This is title!',
children: <div>This is a tooltip!</div>,
placement: 'bottom-start',
hoverShow: true,
};
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,9 @@ export type IconTooltipProps = {
* Enables the Popover to shift in order to stay in view when meeting the viewport edges.
*/
shift?: boolean;

/**
* Enables the Popover to show on hover.
*/
hoverShow?: boolean;
};
Original file line number Diff line number Diff line change
@@ -1,34 +1,24 @@
import { _x } from '@wordpress/i18n';
import styles from './styles.module.scss';
import Badge from '../badge';

const severityClassNames = severity => {
const ThreatSeverityBadge = ( { severity } ) => {
if ( severity >= 5 ) {
return 'is-critical';
} else if ( severity >= 3 && severity < 5 ) {
return 'is-high';
return (
<Badge variant="danger">
{ _x( 'Critical', 'Severity label for issues rated 5 or higher.', 'jetpack' ) }
</Badge>
);
}
return 'is-low';
};

const severityText = severity => {
if ( severity >= 5 ) {
return _x( 'Critical', 'Severity label for issues rated 5 or higher.', 'jetpack' );
} else if ( severity >= 3 && severity < 5 ) {
return _x( 'High', 'Severity label for issues rated between 3 and 5.', 'jetpack' );
if ( severity >= 3 && severity < 5 ) {
return (
<Badge variant="warning">
{ _x( 'High', 'Severity label for issues rated between 3 and 5.', 'jetpack' ) }
</Badge>
);
}
return _x( 'Low', 'Severity label for issues rated below 3.', 'jetpack' );
};

const ThreatSeverityBadge = ( { severity } ) => {
return (
<div
className={ `${ styles[ 'threat-severity-badge' ] } ${
styles[ severityClassNames( severity ) ]
}` }
>
{ severityText( severity ) }
</div>
);
return <Badge>{ _x( 'Low', 'Severity label for issues rated below 3.', 'jetpack' ) }</Badge>;
};

export default ThreatSeverityBadge;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { __ } from '@wordpress/i18n';

export const PAID_PLUGIN_SUPPORT_URL = 'https://jetpack.com/contact-support/?rel=support';

export const THREAT_STATUSES: { value: string; label: string; variant?: 'success' | 'warning' }[] =
[
{ value: 'current', label: __( 'Active', 'jetpack' ), variant: 'warning' },
{ value: 'fixed', label: __( 'Fixed', 'jetpack' ), variant: 'success' },
{ value: 'ignored', label: __( 'Ignored', 'jetpack' ) },
];

export const THREAT_TYPES = [
{ value: 'plugin', label: __( 'Plugin', 'jetpack' ) },
{ value: 'theme', label: __( 'Theme', 'jetpack' ) },
{ value: 'core', label: __( 'WordPress', 'jetpack' ) },
{ value: 'file', label: __( 'File', 'jetpack' ) },
{ value: 'database', label: __( 'Database', 'jetpack' ) },
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { ExternalLink, Spinner } from '@wordpress/components';
import { createInterpolateElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { Icon } from '@wordpress/icons';
import { check } from '@wordpress/icons';
import IconTooltip from '../icon-tooltip';
import Text from '../text';
import { PAID_PLUGIN_SUPPORT_URL } from './constants';
import styles from './styles.module.scss';
import { type ThreatFixStatus } from './types';
import { fixerStatusIsStale } from './utils';

/**
* InfoIconTooltip component.
*
* @param {object} props - Component props.
* @param {boolean} props.message - The popover message.
* @param {object} props.size - The size of the icon.
*
* @return {JSX.Elenment} The component.
*/
export function InfoIconTooltip( {
message,
size = 20,
}: {
message?: string;
size?: number;
} ): JSX.Element {
return (
<IconTooltip
placement={ 'top' }
className={ styles[ 'icon-tooltip__container' ] }
iconClassName={ styles[ 'icon-tooltip__icon' ] }
iconSize={ size }
hoverShow={ true }
>
<Text variant={ 'body-small' }>
{ createInterpolateElement(
sprintf(
/* translators: %s: Number of hide items */
__( '%s Please try again or <supportLink>contact support</supportLink>.', 'jetpack' ),
message
),
{
supportLink: (
<ExternalLink
className={ styles[ 'support-link' ] }
href={ PAID_PLUGIN_SUPPORT_URL }
/>
),
}
) }
</Text>
</IconTooltip>
);
}

/**
* Fixer Status component.
*
* @param {object} props - Component props.
* @param {boolean} props.fixer - The fixer status.
*
* @return {JSX.Element} The component.
*/
export default function FixerStatusIcon( { fixer }: { fixer?: ThreatFixStatus } ): JSX.Element {
if ( fixer && fixerStatusIsStale( fixer ) ) {
return (
<InfoIconTooltip message={ __( 'The fixer is taking longer than expected.', 'jetpack' ) } />
);
}

if ( fixer && 'error' in fixer && fixer.error ) {
return (
<InfoIconTooltip message={ __( 'An error occurred auto-fixing this threat.', 'jetpack' ) } />
);
}

if ( fixer && 'status' in fixer && fixer.status === 'in_progress' ) {
return (
<div className={ styles[ 'icon-spinner' ] }>
<Spinner color="black" />
</div>
);
}

return <Icon icon={ check } className={ styles[ 'icon-check' ] } size={ 28 } />;
}

/**
* FixerStatusText component.
*
* @param {object} props - Component props.
* @param {boolean} props.fixer - The fixer status.
*
* @return {JSX.Element} The component.
*/
function FixerStatusText( { fixer }: { fixer?: ThreatFixStatus } ): JSX.Element {
if ( fixer && fixerStatusIsStale( fixer ) ) {
return (
<span className={ styles[ 'info-spacer' ] }>
{ __( 'Fixer is taking longer than expected', 'jetpack' ) }
</span>
);
}

if ( fixer && 'error' in fixer && fixer.error ) {
return (
<span className={ styles[ 'info-spacer' ] }>
{ __( 'An error occurred auto-fixing this threat', 'jetpack' ) }
</span>
);
}

if ( fixer && 'status' in fixer && fixer.status === 'in_progress' ) {
return <span className={ styles[ 'spinner-spacer' ] }>{ __( 'Auto-fixing', 'jetpack' ) }</span>;
}

return <span className={ styles[ 'check-spacer' ] }>{ __( 'Auto-fixable', 'jetpack' ) }</span>;
}

/**
* FixerStatusBadge component.
*
* @param {object} props - Component props.
* @param {boolean} props.fixer - The fixer status.
*
* @return {string} The component.
*/
export function FixerStatusBadge( { fixer }: { fixer?: ThreatFixStatus } ): JSX.Element {
return (
<div className={ styles[ 'fixer-status' ] }>
<FixerStatusIcon fixer={ fixer } />
<FixerStatusText fixer={ fixer } />
</div>
);
}
Loading

0 comments on commit cec6f1e

Please sign in to comment.