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

Components: Add Threats DataView #39754

Merged
merged 10 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 ThreatsDataViews 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( {
Copy link
Member

Choose a reason for hiding this comment

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

Do these threats specific components belong to the generic components package? May be they belong to the scan JS package?

CC: @Automattic/jetpack-garage

Copy link
Member

Choose a reason for hiding this comment

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

The reason for that is these components and thus the dataviews component may be bundled to all the consumer plugins that import components package.

Copy link
Contributor

@anomiex anomiex Nov 26, 2024

Choose a reason for hiding this comment

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

Do these threats specific components belong to the generic components package?

Personally I'd say no. I think the components package should have stick to things that would be very widely useful. But I can't speak for all of Garage on this, that's just my own view.

You might also ask @Automattic/jetpack-agora, I think they still do work on the package.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@manzoorwanijk @anomiex @Automattic/jetpack-agora

We have initially included these components here as:

  • they are candidates for re-use across Jetpack projects (notably the Jetpack and Protect plugins)
  • there are existing product/feature specific components in the package (BoostScoreBar, BoostScoreGraph, etc)
  • These threat components rely on other Jetpack components (Button, Badge, Text) so we would need the scan package to depend on components. The components package extends the tsconfig.base.json config, and the use of "moduleResolution": "bundler" is incompatible with the scan package's current use of tsconfig.tsc.json. Though we could address this somehow I'm sure.

To avoid weighing down the main components package, we can definitely look into the possibility of distributing these components elsewhere.

Copy link
Contributor

Choose a reason for hiding this comment

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

The components package extends the tsconfig.base.json config, and the use of "moduleResolution": "bundler" is incompatible with the scan package's current use of tsconfig.tsc.json. Though we could address this somehow I'm sure.

Probably just switching the import statements to use .js extensions (and explicit index.js instead of directory includes) would be sufficient.

OTOH, fully switching it to generate a build directory, along the lines of what #40299 did for scan, wouldn't be a bad thing for someone to do. It'd make the package more usable outside the monorepo.

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 __( 'The 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
Loading