Skip to content

Commit

Permalink
Components: add ThreatsDataViews
Browse files Browse the repository at this point in the history
  • Loading branch information
dkmyta authored and nateweller committed Nov 5, 2024
1 parent 1b4145b commit dd1f233
Show file tree
Hide file tree
Showing 25 changed files with 1,839 additions and 32 deletions.
366 changes: 360 additions & 6 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
@@ -0,0 +1,142 @@
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 styles from './styles.module.scss';

/**
* Threat Fixer Button component.
*
* @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;
onClick: ( items: Threat[] ) => void;
className?: string;
} ): JSX.Element {
const [ isPopoverVisible, setIsPopoverVisible ] = useState( false );

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

const children = useMemo( () => {
if ( ! threat.fixable ) {
return null;
}
if ( threat.fixer && 'error' in threat.fixer && threat.fixer.error ) {
return __( 'Error', 'jetpack' );
}
if ( threat.fixer && 'status' in 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 &&
'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 }
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>
) }
</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
@@ -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,49 @@
import { __ } from '@wordpress/i18n';
import {
code as fileIcon,
color as themeIcon,
plugins as pluginIcon,
shield as shieldIcon,
wordpress as coreIcon,
} from '@wordpress/icons';

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' ) },
];

export const THREAT_ICONS = {
plugin: pluginIcon,
theme: themeIcon,
core: coreIcon,
file: fileIcon,
default: shieldIcon,
};

export const THREAT_FIELD_THREAT = 'threat';
export const THREAT_FIELD_TITLE = 'title';
export const THREAT_FIELD_DESCRIPTION = 'description';
export const THREAT_FIELD_ICON = 'icon';
export const THREAT_FIELD_STATUS = 'status';
export const THREAT_FIELD_TYPE = 'type';
export const THREAT_FIELD_EXTENSION = 'extension';
export const THREAT_FIELD_PLUGIN = 'plugin';
export const THREAT_FIELD_THEME = 'theme';
export const THREAT_FIELD_SEVERITY = 'severity';
export const THREAT_FIELD_SIGNATURE = 'signature';
export const THREAT_FIELD_FIRST_DETECTED = 'first-detected';
export const THREAT_FIELD_FIXED_ON = 'fixed-on';
export const THREAT_FIELD_AUTO_FIX = 'auto-fix';

export const THREAT_ACTION_FIX = 'fix';
export const THREAT_ACTION_IGNORE = 'ignore';
export const THREAT_ACTION_UNIGNORE = 'unignore';
Loading

0 comments on commit dd1f233

Please sign in to comment.