@automattic/jetpack-components 0.58.1 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/components/badge/index.tsx +39 -0
- package/components/badge/style.module.scss +25 -0
- package/components/icon-tooltip/index.tsx +28 -1
- package/components/icon-tooltip/types.ts +5 -0
- package/components/layout/container/index.tsx +16 -11
- package/components/pricing-card/index.tsx +2 -3
- package/components/text/index.tsx +2 -3
- package/components/text/types.ts +0 -1
- package/components/threat-fixer-button/index.tsx +161 -0
- package/components/threat-fixer-button/styles.module.scss +16 -0
- package/components/threat-severity-badge/index.tsx +14 -24
- package/components/threats-data-views/constants.ts +49 -0
- package/components/threats-data-views/index.tsx +539 -0
- package/components/threats-data-views/styles.module.scss +41 -0
- package/components/threats-data-views/threats-status-toggle-group-control.tsx +160 -0
- package/index.ts +2 -0
- package/package.json +19 -17
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
### This is a list detailing changes for the Jetpack RNA Components package releases.
|
|
4
4
|
|
|
5
|
+
## [0.60.0] - 2024-11-14
|
|
6
|
+
### Added
|
|
7
|
+
- Adds tooltips for each ThreatFixerButton state [#40111]
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Fixes the loading placeholder that didn't disappear when the price loads. [#40157]
|
|
11
|
+
|
|
12
|
+
## [0.59.0] - 2024-11-11
|
|
13
|
+
### Added
|
|
14
|
+
- Add ThreatsDataViews component [#39754]
|
|
15
|
+
- Components: add ref for container component [#39850]
|
|
16
|
+
- IconTooltip: add support for showing tooltip on hover. [#39916]
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Add ToggleGroupControl to ThreatsDataViews for easily toggling between Active and Historical threats [#39901]
|
|
20
|
+
- Updated package dependencies. [#39999]
|
|
21
|
+
- Updated package dependencies. [#40000]
|
|
22
|
+
- Updated package dependencies. [#40060]
|
|
23
|
+
|
|
5
24
|
## [0.58.1] - 2024-11-04
|
|
6
25
|
### Added
|
|
7
26
|
- Enable test coverage. [#39961]
|
|
@@ -1202,6 +1221,8 @@
|
|
|
1202
1221
|
### Changed
|
|
1203
1222
|
- Update node version requirement to 14.16.1
|
|
1204
1223
|
|
|
1224
|
+
[0.60.0]: https://github.com/Automattic/jetpack-components/compare/0.59.0...0.60.0
|
|
1225
|
+
[0.59.0]: https://github.com/Automattic/jetpack-components/compare/0.58.1...0.59.0
|
|
1205
1226
|
[0.58.1]: https://github.com/Automattic/jetpack-components/compare/0.58.0...0.58.1
|
|
1206
1227
|
[0.58.0]: https://github.com/Automattic/jetpack-components/compare/0.57.0...0.58.0
|
|
1207
1228
|
[0.57.0]: https://github.com/Automattic/jetpack-components/compare/0.56.3...0.57.0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import styles from './style.module.scss';
|
|
4
|
+
|
|
5
|
+
type BadgeProps = {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
variant?: 'success' | 'warning' | 'danger';
|
|
9
|
+
[ key: string ]: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Badge component
|
|
14
|
+
*
|
|
15
|
+
* @param {object} props - The component properties.
|
|
16
|
+
* @param {string} props.variant - The badge variant (i.e. 'success', 'warning', 'danger').
|
|
17
|
+
* @param {JSX.Element} props.children - Badge text or content.
|
|
18
|
+
* @param {string} props.className - Additional class name to pass to the Badge component.
|
|
19
|
+
*
|
|
20
|
+
* @return {React.ReactElement} The `Badge` component.
|
|
21
|
+
*/
|
|
22
|
+
const Badge: React.FC< BadgeProps > = ( { children, className, variant = 'info', ...props } ) => {
|
|
23
|
+
const classes = clsx(
|
|
24
|
+
styles.badge,
|
|
25
|
+
{
|
|
26
|
+
[ styles[ 'is-success' ] ]: variant === 'success',
|
|
27
|
+
[ styles[ 'is-warning' ] ]: variant === 'warning',
|
|
28
|
+
[ styles[ 'is-danger' ] ]: variant === 'danger',
|
|
29
|
+
},
|
|
30
|
+
className
|
|
31
|
+
);
|
|
32
|
+
return (
|
|
33
|
+
<span className={ classes } { ...props }>
|
|
34
|
+
{ children }
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default Badge;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.badge {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
border-radius: 4px;
|
|
4
|
+
background-color: var(--jp-gray-0);
|
|
5
|
+
color: var(--jp-gray-80);
|
|
6
|
+
padding: 4px 8px;
|
|
7
|
+
font-size: 13px;
|
|
8
|
+
font-weight: 400;
|
|
9
|
+
line-height: 16px;
|
|
10
|
+
|
|
11
|
+
&.is-success {
|
|
12
|
+
background-color: var(--jp-green-5);
|
|
13
|
+
color: var(--jp-green-50);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
&.is-warning {
|
|
17
|
+
background-color: var(--jp-yellow-5);
|
|
18
|
+
color: var(--jp-yellow-60);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&.is-danger {
|
|
22
|
+
background-color: var(--jp-red-5);
|
|
23
|
+
color: var(--jp-red-70);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -38,12 +38,14 @@ const IconTooltip: React.FC< IconTooltipProps > = ( {
|
|
|
38
38
|
children,
|
|
39
39
|
popoverAnchorStyle = 'icon',
|
|
40
40
|
forceShow = false,
|
|
41
|
+
hoverShow = false,
|
|
41
42
|
wide = false,
|
|
42
43
|
inline = true,
|
|
43
44
|
shift = false,
|
|
44
45
|
} ) => {
|
|
45
46
|
const POPOVER_HELPER_WIDTH = 124;
|
|
46
47
|
const [ isVisible, setIsVisible ] = useState( false );
|
|
48
|
+
const [ hoverTimeout, setHoverTimeout ] = useState( null );
|
|
47
49
|
const hideTooltip = useCallback( () => setIsVisible( false ), [ setIsVisible ] );
|
|
48
50
|
const toggleTooltip = useCallback(
|
|
49
51
|
e => {
|
|
@@ -78,8 +80,33 @@ const IconTooltip: React.FC< IconTooltipProps > = ( {
|
|
|
78
80
|
|
|
79
81
|
const isForcedToShow = isAnchorWrapper && forceShow;
|
|
80
82
|
|
|
83
|
+
const handleMouseEnter = useCallback( () => {
|
|
84
|
+
if ( hoverShow ) {
|
|
85
|
+
if ( hoverTimeout ) {
|
|
86
|
+
clearTimeout( hoverTimeout );
|
|
87
|
+
setHoverTimeout( null );
|
|
88
|
+
}
|
|
89
|
+
setIsVisible( true );
|
|
90
|
+
}
|
|
91
|
+
}, [ hoverShow, hoverTimeout ] );
|
|
92
|
+
|
|
93
|
+
const handleMouseLeave = useCallback( () => {
|
|
94
|
+
if ( hoverShow ) {
|
|
95
|
+
const id = setTimeout( () => {
|
|
96
|
+
setIsVisible( false );
|
|
97
|
+
setHoverTimeout( null );
|
|
98
|
+
}, 100 );
|
|
99
|
+
setHoverTimeout( id );
|
|
100
|
+
}
|
|
101
|
+
}, [ hoverShow ] );
|
|
102
|
+
|
|
81
103
|
return (
|
|
82
|
-
<div
|
|
104
|
+
<div
|
|
105
|
+
className={ wrapperClassNames }
|
|
106
|
+
data-testid="icon-tooltip_wrapper"
|
|
107
|
+
onMouseEnter={ handleMouseEnter }
|
|
108
|
+
onMouseLeave={ handleMouseLeave }
|
|
109
|
+
>
|
|
83
110
|
{ ! isAnchorWrapper && (
|
|
84
111
|
<Button variant="link" onMouseDown={ toggleTooltip }>
|
|
85
112
|
<Gridicon className={ iconClassName } icon={ iconCode } size={ iconSize } />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import { createElement, useMemo } from 'react';
|
|
2
|
+
import { createElement, forwardRef, useMemo } from 'react';
|
|
3
3
|
import { ContainerProps } from '../types';
|
|
4
4
|
import styles from './style.module.scss';
|
|
5
5
|
import type React from 'react';
|
|
@@ -7,17 +7,21 @@ import type React from 'react';
|
|
|
7
7
|
/**
|
|
8
8
|
* JP Container
|
|
9
9
|
*
|
|
10
|
-
* @param {ContainerProps}
|
|
10
|
+
* @param {ContainerProps} props - Component properties.
|
|
11
|
+
* @param {React.MutableRefObject} ref - Ref to the component
|
|
11
12
|
* @return {React.ReactElement} Container component.
|
|
12
13
|
*/
|
|
13
|
-
const Container
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
const Container = (
|
|
15
|
+
{
|
|
16
|
+
children,
|
|
17
|
+
fluid = false,
|
|
18
|
+
tagName = 'div',
|
|
19
|
+
className,
|
|
20
|
+
horizontalGap = 1,
|
|
21
|
+
horizontalSpacing = 1,
|
|
22
|
+
}: ContainerProps,
|
|
23
|
+
ref: React.MutableRefObject< HTMLElement | null >
|
|
24
|
+
): React.ReactElement => {
|
|
21
25
|
const containerStyle = useMemo( () => {
|
|
22
26
|
const padding = `calc( var(--horizontal-spacing) * ${ horizontalSpacing } )`;
|
|
23
27
|
const rowGap = `calc( var(--horizontal-spacing) * ${ horizontalGap } )`;
|
|
@@ -38,9 +42,10 @@ const Container: React.FC< ContainerProps > = ( {
|
|
|
38
42
|
{
|
|
39
43
|
className: containerClassName,
|
|
40
44
|
style: containerStyle,
|
|
45
|
+
ref,
|
|
41
46
|
},
|
|
42
47
|
children
|
|
43
48
|
);
|
|
44
49
|
};
|
|
45
50
|
|
|
46
|
-
export default Container;
|
|
51
|
+
export default forwardRef< HTMLElement, ContainerProps >( Container );
|
|
@@ -55,7 +55,8 @@ const PricingCard: React.FC< PricingCardProps > = ( {
|
|
|
55
55
|
) }
|
|
56
56
|
<h1 className="jp-components__pricing-card__title">{ props.title }</h1>
|
|
57
57
|
<div className="jp-components__pricing-card__pricing">
|
|
58
|
-
{ props.
|
|
58
|
+
{ props.priceAfter === 0 && <LoadingPlaceholder width="100%" height={ 48 } /> }
|
|
59
|
+
{ props.priceBefore !== props.priceAfter && props.priceAfter > 0 && (
|
|
59
60
|
<div className="jp-components__pricing-card__price-before">
|
|
60
61
|
<span className="jp-components__pricing-card__currency">
|
|
61
62
|
{ currencyObjectBefore.symbol }
|
|
@@ -71,8 +72,6 @@ const PricingCard: React.FC< PricingCardProps > = ( {
|
|
|
71
72
|
) }
|
|
72
73
|
<div className="jp-components__pricing-card__price-strikethrough"></div>
|
|
73
74
|
</div>
|
|
74
|
-
) : (
|
|
75
|
-
<LoadingPlaceholder width="100%" height={ 48 } />
|
|
76
75
|
) }
|
|
77
76
|
{ props.priceAfter > 0 && (
|
|
78
77
|
<>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import React, {
|
|
2
|
+
import React, { forwardRef, useMemo } from 'react';
|
|
3
3
|
import { BOX_MODEL_VALUES, VARIANTS_MAPPING } from './constants';
|
|
4
4
|
import styles from './style.module.scss';
|
|
5
5
|
import type { H3Props, TextProps, TitleProps } from './types';
|
|
@@ -26,12 +26,11 @@ const Text = forwardRef< HTMLElement, TextProps >(
|
|
|
26
26
|
}, '' );
|
|
27
27
|
}, [ componentProps ] );
|
|
28
28
|
|
|
29
|
-
componentProps.ref = ref;
|
|
30
|
-
|
|
31
29
|
return (
|
|
32
30
|
<Component
|
|
33
31
|
className={ clsx( styles.reset, styles[ variant ], className, boxModelClasses ) }
|
|
34
32
|
{ ...componentProps }
|
|
33
|
+
ref={ ref }
|
|
35
34
|
>
|
|
36
35
|
{ children }
|
|
37
36
|
</Component>
|
package/components/text/types.ts
CHANGED
|
@@ -39,7 +39,6 @@ export type TextProps = {
|
|
|
39
39
|
children: React.ReactNode;
|
|
40
40
|
/** Force an specific tag (span, div) or use a custom component that will receive className and children */
|
|
41
41
|
component?: React.FC< { [ prop: string ]: unknown } > | React.ElementType;
|
|
42
|
-
[ prop: string ]: unknown;
|
|
43
42
|
};
|
|
44
43
|
|
|
45
44
|
export type H3Props = TextProps & {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Button } from '@automattic/jetpack-components';
|
|
2
|
+
import {
|
|
3
|
+
type Threat,
|
|
4
|
+
fixerIsInError,
|
|
5
|
+
fixerIsInProgress,
|
|
6
|
+
fixerStatusIsStale,
|
|
7
|
+
} from '@automattic/jetpack-scan';
|
|
8
|
+
import { Tooltip } from '@wordpress/components';
|
|
9
|
+
import { useCallback, useMemo } from '@wordpress/element';
|
|
10
|
+
import { __ } from '@wordpress/i18n';
|
|
11
|
+
import styles from './styles.module.scss';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Threat Fixer Button component.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} props - Component props.
|
|
17
|
+
* @param {object} props.threat - The threat.
|
|
18
|
+
* @param {Function} props.onClick - The onClick function.
|
|
19
|
+
* @param {string} props.className - The className.
|
|
20
|
+
*
|
|
21
|
+
* @return {JSX.Element} The component.
|
|
22
|
+
*/
|
|
23
|
+
export default function ThreatFixerButton( {
|
|
24
|
+
threat,
|
|
25
|
+
className,
|
|
26
|
+
onClick,
|
|
27
|
+
}: {
|
|
28
|
+
threat: Threat;
|
|
29
|
+
onClick: ( items: Threat[] ) => void;
|
|
30
|
+
className?: string;
|
|
31
|
+
} ): JSX.Element {
|
|
32
|
+
const fixerState = useMemo( () => {
|
|
33
|
+
const inProgress = threat.fixer && fixerIsInProgress( threat.fixer );
|
|
34
|
+
const error = threat.fixer && fixerIsInError( threat.fixer );
|
|
35
|
+
const stale = threat.fixer && fixerStatusIsStale( threat.fixer );
|
|
36
|
+
return { inProgress, error, stale };
|
|
37
|
+
}, [ threat.fixer ] );
|
|
38
|
+
|
|
39
|
+
const tooltipText = useMemo( () => {
|
|
40
|
+
if ( ! threat.fixable ) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if ( fixerState.error ) {
|
|
45
|
+
return __( 'An error occurred auto-fixing this threat.', 'jetpack' );
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if ( fixerState.stale ) {
|
|
49
|
+
return __( 'The auto-fixer is taking longer than expected.', 'jetpack' );
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if ( fixerState.inProgress ) {
|
|
53
|
+
return __( 'An auto-fixer is in progress.', 'jetpack' );
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
switch ( threat.fixable.fixer ) {
|
|
57
|
+
case 'delete':
|
|
58
|
+
if ( threat.filename ) {
|
|
59
|
+
if ( threat.filename.endsWith( '/' ) ) {
|
|
60
|
+
return __( 'Deletes the directory that the infected file is in.', 'jetpack' );
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if ( threat.signature === 'Core.File.Modification' ) {
|
|
64
|
+
return __( 'Deletes the unexpected file in a core WordPress directory.', 'jetpack' );
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return __( 'Deletes the infected file.', 'jetpack' );
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ( threat.extension?.type === 'plugin' ) {
|
|
71
|
+
return __( 'Deletes the plugin directory to fix the threat.', 'jetpack' );
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if ( threat.extension?.type === 'theme' ) {
|
|
75
|
+
return __( 'Deletes the theme directory to fix the threat.', 'jetpack' );
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case 'update':
|
|
79
|
+
return __( 'Upgrades the plugin or theme to a newer version.', 'jetpack' );
|
|
80
|
+
case 'replace':
|
|
81
|
+
case 'rollback':
|
|
82
|
+
if ( threat.filename ) {
|
|
83
|
+
return threat.signature === 'Core.File.Modification'
|
|
84
|
+
? __(
|
|
85
|
+
'Replaces the modified core WordPress file with the original clean version from the WordPress source code.',
|
|
86
|
+
'jetpack'
|
|
87
|
+
)
|
|
88
|
+
: __(
|
|
89
|
+
'Replaces the infected file with a previously backed up version that is clean.',
|
|
90
|
+
'jetpack'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ( threat.signature === 'php_hardening_WP_Config_NoSalts_001' ) {
|
|
95
|
+
return __(
|
|
96
|
+
'Replaces the default salt keys in wp-config.php with unique ones.',
|
|
97
|
+
'jetpack'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
return __( 'An auto-fixer is available.', 'jetpack' );
|
|
103
|
+
}
|
|
104
|
+
}, [ threat, fixerState ] );
|
|
105
|
+
|
|
106
|
+
const buttonText = useMemo( () => {
|
|
107
|
+
if ( ! threat.fixable ) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if ( fixerState.error ) {
|
|
112
|
+
return __( 'Error', 'jetpack' );
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
switch ( threat.fixable.fixer ) {
|
|
116
|
+
case 'delete':
|
|
117
|
+
return __( 'Delete', 'jetpack' );
|
|
118
|
+
case 'update':
|
|
119
|
+
return __( 'Update', 'jetpack' );
|
|
120
|
+
case 'replace':
|
|
121
|
+
case 'rollback':
|
|
122
|
+
return __( 'Replace', 'jetpack' );
|
|
123
|
+
default:
|
|
124
|
+
return __( 'Fix', 'jetpack' );
|
|
125
|
+
}
|
|
126
|
+
}, [ threat.fixable, fixerState.error ] );
|
|
127
|
+
|
|
128
|
+
const handleClick = useCallback(
|
|
129
|
+
( event: React.MouseEvent ) => {
|
|
130
|
+
event.stopPropagation();
|
|
131
|
+
onClick( [ threat ] );
|
|
132
|
+
},
|
|
133
|
+
[ onClick, threat ]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if ( ! threat.fixable ) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div>
|
|
142
|
+
<Tooltip className={ styles.tooltip } text={ tooltipText }>
|
|
143
|
+
<Button
|
|
144
|
+
size="small"
|
|
145
|
+
weight="regular"
|
|
146
|
+
variant="secondary"
|
|
147
|
+
onClick={ handleClick }
|
|
148
|
+
children={ buttonText }
|
|
149
|
+
className={ className }
|
|
150
|
+
isLoading={ fixerState.inProgress }
|
|
151
|
+
isDestructive={
|
|
152
|
+
( threat.fixable && threat.fixable.fixer === 'delete' ) ||
|
|
153
|
+
fixerState.error ||
|
|
154
|
+
fixerState.stale
|
|
155
|
+
}
|
|
156
|
+
style={ { minWidth: '72px' } }
|
|
157
|
+
/>
|
|
158
|
+
</Tooltip>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -1,34 +1,24 @@
|
|
|
1
1
|
import { _x } from '@wordpress/i18n';
|
|
2
|
-
import
|
|
2
|
+
import Badge from '../badge';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const ThreatSeverityBadge = ( { severity } ) => {
|
|
5
5
|
if ( severity >= 5 ) {
|
|
6
|
-
return
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
return (
|
|
7
|
+
<Badge variant="danger">
|
|
8
|
+
{ _x( 'Critical', 'Severity label for issues rated 5 or higher.', 'jetpack' ) }
|
|
9
|
+
</Badge>
|
|
10
|
+
);
|
|
9
11
|
}
|
|
10
|
-
return 'is-low';
|
|
11
|
-
};
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
if ( severity >= 3 && severity < 5 ) {
|
|
14
|
+
return (
|
|
15
|
+
<Badge variant="warning">
|
|
16
|
+
{ _x( 'High', 'Severity label for issues rated between 3 and 5.', 'jetpack' ) }
|
|
17
|
+
</Badge>
|
|
18
|
+
);
|
|
18
19
|
}
|
|
19
|
-
return _x( 'Low', 'Severity label for issues rated below 3.', 'jetpack' );
|
|
20
|
-
};
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div
|
|
25
|
-
className={ `${ styles[ 'threat-severity-badge' ] } ${
|
|
26
|
-
styles[ severityClassNames( severity ) ]
|
|
27
|
-
}` }
|
|
28
|
-
>
|
|
29
|
-
{ severityText( severity ) }
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
21
|
+
return <Badge>{ _x( 'Low', 'Severity label for issues rated below 3.', 'jetpack' ) }</Badge>;
|
|
32
22
|
};
|
|
33
23
|
|
|
34
24
|
export default ThreatSeverityBadge;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
import {
|
|
3
|
+
code as fileIcon,
|
|
4
|
+
color as themeIcon,
|
|
5
|
+
plugins as pluginIcon,
|
|
6
|
+
shield as shieldIcon,
|
|
7
|
+
wordpress as coreIcon,
|
|
8
|
+
} from '@wordpress/icons';
|
|
9
|
+
|
|
10
|
+
export const THREAT_STATUSES: { value: string; label: string; variant?: 'success' | 'warning' }[] =
|
|
11
|
+
[
|
|
12
|
+
{ value: 'current', label: __( 'Active', 'jetpack' ), variant: 'warning' },
|
|
13
|
+
{ value: 'fixed', label: __( 'Fixed', 'jetpack' ), variant: 'success' },
|
|
14
|
+
{ value: 'ignored', label: __( 'Ignored', 'jetpack' ) },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const THREAT_TYPES = [
|
|
18
|
+
{ value: 'plugin', label: __( 'Plugin', 'jetpack' ) },
|
|
19
|
+
{ value: 'theme', label: __( 'Theme', 'jetpack' ) },
|
|
20
|
+
{ value: 'core', label: __( 'WordPress', 'jetpack' ) },
|
|
21
|
+
{ value: 'file', label: __( 'File', 'jetpack' ) },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export const THREAT_ICONS = {
|
|
25
|
+
plugin: pluginIcon,
|
|
26
|
+
theme: themeIcon,
|
|
27
|
+
core: coreIcon,
|
|
28
|
+
file: fileIcon,
|
|
29
|
+
default: shieldIcon,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const THREAT_FIELD_THREAT = 'threat';
|
|
33
|
+
export const THREAT_FIELD_TITLE = 'title';
|
|
34
|
+
export const THREAT_FIELD_DESCRIPTION = 'description';
|
|
35
|
+
export const THREAT_FIELD_ICON = 'icon';
|
|
36
|
+
export const THREAT_FIELD_STATUS = 'status';
|
|
37
|
+
export const THREAT_FIELD_TYPE = 'type';
|
|
38
|
+
export const THREAT_FIELD_EXTENSION = 'extension';
|
|
39
|
+
export const THREAT_FIELD_PLUGIN = 'plugin';
|
|
40
|
+
export const THREAT_FIELD_THEME = 'theme';
|
|
41
|
+
export const THREAT_FIELD_SEVERITY = 'severity';
|
|
42
|
+
export const THREAT_FIELD_SIGNATURE = 'signature';
|
|
43
|
+
export const THREAT_FIELD_FIRST_DETECTED = 'first-detected';
|
|
44
|
+
export const THREAT_FIELD_FIXED_ON = 'fixed-on';
|
|
45
|
+
export const THREAT_FIELD_AUTO_FIX = 'auto-fix';
|
|
46
|
+
|
|
47
|
+
export const THREAT_ACTION_FIX = 'fix';
|
|
48
|
+
export const THREAT_ACTION_IGNORE = 'ignore';
|
|
49
|
+
export const THREAT_ACTION_UNIGNORE = 'unignore';
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { getThreatType, type Threat } from '@automattic/jetpack-scan';
|
|
2
|
+
import {
|
|
3
|
+
type Action,
|
|
4
|
+
type ActionButton,
|
|
5
|
+
type Field,
|
|
6
|
+
type FieldType,
|
|
7
|
+
type Filter,
|
|
8
|
+
type SortDirection,
|
|
9
|
+
type SupportedLayouts,
|
|
10
|
+
type View,
|
|
11
|
+
DataViews,
|
|
12
|
+
filterSortAndPaginate,
|
|
13
|
+
} from '@wordpress/dataviews';
|
|
14
|
+
import { dateI18n } from '@wordpress/date';
|
|
15
|
+
import { __ } from '@wordpress/i18n';
|
|
16
|
+
import { Icon } from '@wordpress/icons';
|
|
17
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
18
|
+
import Badge from '../badge';
|
|
19
|
+
import ThreatFixerButton from '../threat-fixer-button';
|
|
20
|
+
import ThreatSeverityBadge from '../threat-severity-badge';
|
|
21
|
+
import {
|
|
22
|
+
THREAT_ACTION_FIX,
|
|
23
|
+
THREAT_ACTION_IGNORE,
|
|
24
|
+
THREAT_ACTION_UNIGNORE,
|
|
25
|
+
THREAT_FIELD_AUTO_FIX,
|
|
26
|
+
THREAT_FIELD_DESCRIPTION,
|
|
27
|
+
THREAT_FIELD_EXTENSION,
|
|
28
|
+
THREAT_FIELD_FIRST_DETECTED,
|
|
29
|
+
THREAT_FIELD_FIXED_ON,
|
|
30
|
+
THREAT_FIELD_ICON,
|
|
31
|
+
THREAT_FIELD_PLUGIN,
|
|
32
|
+
THREAT_FIELD_SEVERITY,
|
|
33
|
+
THREAT_FIELD_SIGNATURE,
|
|
34
|
+
THREAT_FIELD_STATUS,
|
|
35
|
+
THREAT_FIELD_THEME,
|
|
36
|
+
THREAT_FIELD_THREAT,
|
|
37
|
+
THREAT_FIELD_TITLE,
|
|
38
|
+
THREAT_FIELD_TYPE,
|
|
39
|
+
THREAT_ICONS,
|
|
40
|
+
THREAT_STATUSES,
|
|
41
|
+
THREAT_TYPES,
|
|
42
|
+
} from './constants';
|
|
43
|
+
import styles from './styles.module.scss';
|
|
44
|
+
import ThreatsStatusToggleGroupControl from './threats-status-toggle-group-control';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* DataViews component for displaying security threats.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} props - Component props.
|
|
50
|
+
* @param {Array} props.data - Threats data.
|
|
51
|
+
* @param {Array} props.filters - Initial DataView filters.
|
|
52
|
+
* @param {Function} props.onChangeSelection - Callback function run when an item is selected.
|
|
53
|
+
* @param {Function} props.onFixThreats - Threat fix action callback.
|
|
54
|
+
* @param {Function} props.onIgnoreThreats - Threat ignore action callback.
|
|
55
|
+
* @param {Function} props.onUnignoreThreats - Threat unignore action callback.
|
|
56
|
+
* @param {Function} props.isThreatEligibleForFix - Function to determine if a threat is eligible for fixing.
|
|
57
|
+
* @param {Function} props.isThreatEligibleForIgnore - Function to determine if a threat is eligible for ignoring.
|
|
58
|
+
* @param {Function} props.isThreatEligibleForUnignore - Function to determine if a threat is eligible for unignoring.
|
|
59
|
+
*
|
|
60
|
+
* @return {JSX.Element} The ThreatsDataViews component.
|
|
61
|
+
*/
|
|
62
|
+
export default function ThreatsDataViews( {
|
|
63
|
+
data,
|
|
64
|
+
filters,
|
|
65
|
+
onChangeSelection,
|
|
66
|
+
isThreatEligibleForFix,
|
|
67
|
+
isThreatEligibleForIgnore,
|
|
68
|
+
isThreatEligibleForUnignore,
|
|
69
|
+
onFixThreats,
|
|
70
|
+
onIgnoreThreats,
|
|
71
|
+
onUnignoreThreats,
|
|
72
|
+
}: {
|
|
73
|
+
data: Threat[];
|
|
74
|
+
filters?: Filter[];
|
|
75
|
+
onChangeSelection?: ( selectedItemIds: string[] ) => void;
|
|
76
|
+
isThreatEligibleForFix?: ( threat: Threat ) => boolean;
|
|
77
|
+
isThreatEligibleForIgnore?: ( threat: Threat ) => boolean;
|
|
78
|
+
isThreatEligibleForUnignore?: ( threat: Threat ) => boolean;
|
|
79
|
+
onFixThreats?: ( threats: Threat[] ) => void;
|
|
80
|
+
onIgnoreThreats?: ActionButton< Threat >[ 'callback' ];
|
|
81
|
+
onUnignoreThreats?: ActionButton< Threat >[ 'callback' ];
|
|
82
|
+
} ): JSX.Element {
|
|
83
|
+
const baseView = {
|
|
84
|
+
sort: {
|
|
85
|
+
field: 'severity',
|
|
86
|
+
direction: 'desc' as SortDirection,
|
|
87
|
+
},
|
|
88
|
+
search: '',
|
|
89
|
+
filters: filters || [],
|
|
90
|
+
page: 1,
|
|
91
|
+
perPage: 20,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* DataView default layouts.
|
|
96
|
+
*
|
|
97
|
+
* This property provides layout information about the view types that are active. If empty, enables all layout types (see “Layout Types”) with empty layout data.
|
|
98
|
+
*
|
|
99
|
+
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#defaultlayouts-record-string-view
|
|
100
|
+
*/
|
|
101
|
+
const defaultLayouts: SupportedLayouts = {
|
|
102
|
+
table: {
|
|
103
|
+
...baseView,
|
|
104
|
+
fields: [
|
|
105
|
+
THREAT_FIELD_SEVERITY,
|
|
106
|
+
THREAT_FIELD_THREAT,
|
|
107
|
+
THREAT_FIELD_TYPE,
|
|
108
|
+
THREAT_FIELD_AUTO_FIX,
|
|
109
|
+
],
|
|
110
|
+
layout: {
|
|
111
|
+
primaryField: THREAT_FIELD_SEVERITY,
|
|
112
|
+
combinedFields: [
|
|
113
|
+
{
|
|
114
|
+
id: THREAT_FIELD_THREAT,
|
|
115
|
+
label: __( 'Threat', 'jetpack' ),
|
|
116
|
+
children: [ THREAT_FIELD_TITLE, THREAT_FIELD_DESCRIPTION ],
|
|
117
|
+
direction: 'vertical',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
list: {
|
|
123
|
+
...baseView,
|
|
124
|
+
fields: [
|
|
125
|
+
THREAT_FIELD_SEVERITY,
|
|
126
|
+
THREAT_FIELD_TYPE,
|
|
127
|
+
THREAT_FIELD_EXTENSION,
|
|
128
|
+
THREAT_FIELD_SIGNATURE,
|
|
129
|
+
],
|
|
130
|
+
layout: {
|
|
131
|
+
primaryField: THREAT_FIELD_TITLE,
|
|
132
|
+
mediaField: THREAT_FIELD_ICON,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* DataView view object - configures how the dataset is visible to the user.
|
|
139
|
+
*
|
|
140
|
+
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#view-object
|
|
141
|
+
*/
|
|
142
|
+
const [ view, setView ] = useState< View >( {
|
|
143
|
+
type: 'table',
|
|
144
|
+
...defaultLayouts.table,
|
|
145
|
+
} );
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Compute values from the provided threats data.
|
|
149
|
+
*
|
|
150
|
+
* @member {object[]} themes - List of unique themes included in the threats data.
|
|
151
|
+
* @member {object[]} plugins - plugins included in the threats data.
|
|
152
|
+
* @member {object[]} signatures - List of unique threat signatures.
|
|
153
|
+
* @member {string[]} dataFields - List of unique fields.
|
|
154
|
+
*/
|
|
155
|
+
const {
|
|
156
|
+
themes,
|
|
157
|
+
plugins,
|
|
158
|
+
signatures,
|
|
159
|
+
dataFields,
|
|
160
|
+
}: {
|
|
161
|
+
themes: { value: string; label: string }[];
|
|
162
|
+
plugins: { value: string; label: string }[];
|
|
163
|
+
signatures: { value: string; label: string }[];
|
|
164
|
+
dataFields: string[];
|
|
165
|
+
} = useMemo( () => {
|
|
166
|
+
return data.reduce(
|
|
167
|
+
( acc, threat ) => {
|
|
168
|
+
// Extensions (Themes and Plugins)
|
|
169
|
+
if ( threat.extension ) {
|
|
170
|
+
switch ( threat.extension.type ) {
|
|
171
|
+
case 'theme':
|
|
172
|
+
if ( ! acc.themes.find( ( { value } ) => value === threat.extension.slug ) ) {
|
|
173
|
+
acc.themes.push( { value: threat.extension.slug, label: threat.extension.name } );
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case 'plugin':
|
|
177
|
+
if ( ! acc.plugins.find( ( { value } ) => value === threat.extension.slug ) ) {
|
|
178
|
+
acc.plugins.push( { value: threat.extension.slug, label: threat.extension.name } );
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
default:
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Signatures
|
|
187
|
+
if ( threat.signature ) {
|
|
188
|
+
if ( ! acc.signatures.find( ( { value } ) => value === threat.signature ) ) {
|
|
189
|
+
acc.signatures.push( { value: threat.signature, label: threat.signature } );
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Fields
|
|
194
|
+
const fields = Object.keys( threat );
|
|
195
|
+
fields.forEach( field => {
|
|
196
|
+
if (
|
|
197
|
+
! acc.dataFields.includes( field ) &&
|
|
198
|
+
threat[ field ] !== null &&
|
|
199
|
+
threat[ field ] !== undefined
|
|
200
|
+
) {
|
|
201
|
+
acc.dataFields.push( field );
|
|
202
|
+
}
|
|
203
|
+
} );
|
|
204
|
+
|
|
205
|
+
return acc;
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
themes: [],
|
|
209
|
+
plugins: [],
|
|
210
|
+
signatures: [],
|
|
211
|
+
dataFields: [],
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
}, [ data ] );
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* DataView fields - describes the visible items for each record in the dataset.
|
|
218
|
+
*
|
|
219
|
+
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#fields-object
|
|
220
|
+
*/
|
|
221
|
+
const fields = useMemo( () => {
|
|
222
|
+
const result: Field< Threat >[] = [
|
|
223
|
+
{
|
|
224
|
+
id: THREAT_FIELD_TITLE,
|
|
225
|
+
label: __( 'Title', 'jetpack' ),
|
|
226
|
+
enableGlobalSearch: true,
|
|
227
|
+
enableHiding: false,
|
|
228
|
+
render: ( { item }: { item: Threat } ) => (
|
|
229
|
+
<div className={ styles.threat__title }>{ item.title }</div>
|
|
230
|
+
),
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: THREAT_FIELD_DESCRIPTION,
|
|
234
|
+
label: __( 'Description', 'jetpack' ),
|
|
235
|
+
enableGlobalSearch: true,
|
|
236
|
+
enableHiding: false,
|
|
237
|
+
render: ( { item }: { item: Threat } ) => (
|
|
238
|
+
<div className={ styles.threat__description }>{ item.description }</div>
|
|
239
|
+
),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: THREAT_FIELD_ICON,
|
|
243
|
+
label: __( 'Icon', 'jetpack' ),
|
|
244
|
+
enableHiding: false,
|
|
245
|
+
getValue( { item }: { item: Threat } ) {
|
|
246
|
+
return getThreatType( item );
|
|
247
|
+
},
|
|
248
|
+
render( { item }: { item: Threat } ) {
|
|
249
|
+
return (
|
|
250
|
+
<div className={ styles.threat__media }>
|
|
251
|
+
<Icon icon={ THREAT_ICONS[ getThreatType( item ) ] } size={ 20 } />
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: THREAT_FIELD_STATUS,
|
|
258
|
+
label: __( 'Status', 'jetpack' ),
|
|
259
|
+
elements: THREAT_STATUSES,
|
|
260
|
+
getValue( { item }: { item: Threat } ) {
|
|
261
|
+
if ( ! item.status ) {
|
|
262
|
+
return 'current';
|
|
263
|
+
}
|
|
264
|
+
return (
|
|
265
|
+
THREAT_STATUSES.find( ( { value } ) => value === item.status )?.value ?? item.status
|
|
266
|
+
);
|
|
267
|
+
},
|
|
268
|
+
render( { item }: { item: Threat } ) {
|
|
269
|
+
if ( item.status ) {
|
|
270
|
+
const status = THREAT_STATUSES.find( ( { value } ) => value === item.status );
|
|
271
|
+
if ( status ) {
|
|
272
|
+
return <Badge variant={ status?.variant }>{ status.label }</Badge>;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return <Badge variant="warning">{ __( 'Active', 'jetpack' ) }</Badge>;
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: THREAT_FIELD_TYPE,
|
|
280
|
+
label: __( 'Type', 'jetpack' ),
|
|
281
|
+
elements: THREAT_TYPES,
|
|
282
|
+
getValue( { item }: { item: Threat } ) {
|
|
283
|
+
return getThreatType( item ) ?? '';
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: THREAT_FIELD_EXTENSION,
|
|
288
|
+
label: __( 'Extension', 'jetpack' ),
|
|
289
|
+
enableGlobalSearch: true,
|
|
290
|
+
enableHiding: true,
|
|
291
|
+
getValue( { item }: { item: Threat } ) {
|
|
292
|
+
return item.extension ? item.extension.slug : '';
|
|
293
|
+
},
|
|
294
|
+
render( { item }: { item: Threat } ) {
|
|
295
|
+
return item.extension ? item.extension.name : '';
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: THREAT_FIELD_PLUGIN,
|
|
300
|
+
label: __( 'Plugin', 'jetpack' ),
|
|
301
|
+
enableGlobalSearch: true,
|
|
302
|
+
enableHiding: false,
|
|
303
|
+
elements: plugins,
|
|
304
|
+
getValue( { item }: { item: Threat } ) {
|
|
305
|
+
return item.extension ? item.extension.slug : '';
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: THREAT_FIELD_THEME,
|
|
310
|
+
label: __( 'Theme', 'jetpack' ),
|
|
311
|
+
enableGlobalSearch: true,
|
|
312
|
+
enableHiding: false,
|
|
313
|
+
elements: themes,
|
|
314
|
+
getValue( { item }: { item: Threat } ) {
|
|
315
|
+
return item.extension ? item.extension.slug : '';
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
...( dataFields.includes( 'severity' )
|
|
319
|
+
? [
|
|
320
|
+
{
|
|
321
|
+
id: THREAT_FIELD_SEVERITY,
|
|
322
|
+
label: __( 'Severity', 'jetpack' ),
|
|
323
|
+
type: 'integer' as FieldType,
|
|
324
|
+
getValue( { item }: { item: Threat } ) {
|
|
325
|
+
return item.severity ?? 0;
|
|
326
|
+
},
|
|
327
|
+
render( { item }: { item: Threat } ) {
|
|
328
|
+
return <ThreatSeverityBadge severity={ item.severity } />;
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
]
|
|
332
|
+
: [] ),
|
|
333
|
+
...( dataFields.includes( 'signature' )
|
|
334
|
+
? [
|
|
335
|
+
{
|
|
336
|
+
id: THREAT_FIELD_SIGNATURE,
|
|
337
|
+
label: __( 'Signature', 'jetpack' ),
|
|
338
|
+
elements: signatures,
|
|
339
|
+
enableGlobalSearch: true,
|
|
340
|
+
getValue( { item }: { item: Threat } ) {
|
|
341
|
+
return item.signature || '';
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
]
|
|
345
|
+
: [] ),
|
|
346
|
+
...( dataFields.includes( 'firstDetected' )
|
|
347
|
+
? [
|
|
348
|
+
{
|
|
349
|
+
id: THREAT_FIELD_FIRST_DETECTED,
|
|
350
|
+
label: __( 'First Detected', 'jetpack' ),
|
|
351
|
+
type: 'datetime' as FieldType,
|
|
352
|
+
getValue( { item }: { item: Threat } ) {
|
|
353
|
+
return item.firstDetected ? new Date( item.firstDetected ) : null;
|
|
354
|
+
},
|
|
355
|
+
render( { item }: { item: Threat } ) {
|
|
356
|
+
return item.firstDetected ? (
|
|
357
|
+
<span className={ styles.threat__firstDetected }>
|
|
358
|
+
{ dateI18n( 'F j Y', item.firstDetected, false ) }
|
|
359
|
+
</span>
|
|
360
|
+
) : null;
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
]
|
|
364
|
+
: [] ),
|
|
365
|
+
...( dataFields.includes( 'fixedOn' )
|
|
366
|
+
? [
|
|
367
|
+
{
|
|
368
|
+
id: THREAT_FIELD_FIXED_ON,
|
|
369
|
+
label: __( 'Fixed On', 'jetpack' ),
|
|
370
|
+
type: 'datetime' as FieldType,
|
|
371
|
+
getValue( { item }: { item: Threat } ) {
|
|
372
|
+
return item.fixedOn ? new Date( item.fixedOn ) : null;
|
|
373
|
+
},
|
|
374
|
+
render( { item }: { item: Threat } ) {
|
|
375
|
+
return item.fixedOn ? (
|
|
376
|
+
<span className={ styles.threat__fixedOn }>
|
|
377
|
+
{ dateI18n( 'F j Y', item.fixedOn, false ) }
|
|
378
|
+
</span>
|
|
379
|
+
) : null;
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
]
|
|
383
|
+
: [] ),
|
|
384
|
+
...( dataFields.includes( 'fixable' )
|
|
385
|
+
? [
|
|
386
|
+
{
|
|
387
|
+
id: THREAT_FIELD_AUTO_FIX,
|
|
388
|
+
label: __( 'Auto-fix', 'jetpack' ),
|
|
389
|
+
enableHiding: false,
|
|
390
|
+
elements: [
|
|
391
|
+
{
|
|
392
|
+
value: 'yes',
|
|
393
|
+
label: __( 'Yes', 'jetpack' ),
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
value: 'no',
|
|
397
|
+
label: __( 'No', 'jetpack' ),
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
getValue( { item }: { item: Threat } ) {
|
|
401
|
+
return item.fixable ? 'yes' : 'no';
|
|
402
|
+
},
|
|
403
|
+
render( { item }: { item: Threat } ) {
|
|
404
|
+
if ( ! item.fixable ) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return <ThreatFixerButton threat={ item } onClick={ onFixThreats } />;
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
]
|
|
412
|
+
: [] ),
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
return result;
|
|
416
|
+
}, [ dataFields, plugins, themes, signatures, onFixThreats ] );
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* DataView actions - collection of operations that can be performed upon each record.
|
|
420
|
+
*
|
|
421
|
+
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#actions-object
|
|
422
|
+
*/
|
|
423
|
+
const actions = useMemo( () => {
|
|
424
|
+
const result: Action< Threat >[] = [];
|
|
425
|
+
|
|
426
|
+
if ( dataFields.includes( 'fixable' ) ) {
|
|
427
|
+
result.push( {
|
|
428
|
+
id: THREAT_ACTION_FIX,
|
|
429
|
+
label: __( 'Auto-fix', 'jetpack' ),
|
|
430
|
+
isPrimary: true,
|
|
431
|
+
supportsBulk: true,
|
|
432
|
+
callback: onFixThreats,
|
|
433
|
+
isEligible( item ) {
|
|
434
|
+
if ( ! onFixThreats ) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
if ( isThreatEligibleForFix ) {
|
|
438
|
+
return isThreatEligibleForFix( item );
|
|
439
|
+
}
|
|
440
|
+
return !! item.fixable;
|
|
441
|
+
},
|
|
442
|
+
} );
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if ( dataFields.includes( 'status' ) ) {
|
|
446
|
+
result.push( {
|
|
447
|
+
id: THREAT_ACTION_IGNORE,
|
|
448
|
+
label: __( 'Ignore', 'jetpack' ),
|
|
449
|
+
isPrimary: true,
|
|
450
|
+
isDestructive: true,
|
|
451
|
+
callback: onIgnoreThreats,
|
|
452
|
+
isEligible( item ) {
|
|
453
|
+
if ( ! onIgnoreThreats ) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
if ( isThreatEligibleForIgnore ) {
|
|
457
|
+
return isThreatEligibleForIgnore( item );
|
|
458
|
+
}
|
|
459
|
+
return item.status === 'current';
|
|
460
|
+
},
|
|
461
|
+
} );
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if ( dataFields.includes( 'status' ) ) {
|
|
465
|
+
result.push( {
|
|
466
|
+
id: THREAT_ACTION_UNIGNORE,
|
|
467
|
+
label: __( 'Unignore', 'jetpack' ),
|
|
468
|
+
isPrimary: true,
|
|
469
|
+
isDestructive: true,
|
|
470
|
+
callback: onUnignoreThreats,
|
|
471
|
+
isEligible( item ) {
|
|
472
|
+
if ( ! onUnignoreThreats ) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
if ( isThreatEligibleForUnignore ) {
|
|
476
|
+
return isThreatEligibleForUnignore( item );
|
|
477
|
+
}
|
|
478
|
+
return item.status === 'ignored';
|
|
479
|
+
},
|
|
480
|
+
} );
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return result;
|
|
484
|
+
}, [
|
|
485
|
+
dataFields,
|
|
486
|
+
onFixThreats,
|
|
487
|
+
onIgnoreThreats,
|
|
488
|
+
onUnignoreThreats,
|
|
489
|
+
isThreatEligibleForFix,
|
|
490
|
+
isThreatEligibleForIgnore,
|
|
491
|
+
isThreatEligibleForUnignore,
|
|
492
|
+
] );
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Apply the view settings (i.e. filters, sorting, pagination) to the dataset.
|
|
496
|
+
*
|
|
497
|
+
* @see https://github.com/WordPress/gutenberg/blob/trunk/packages/dataviews/src/filter-and-sort-data-view.ts
|
|
498
|
+
*/
|
|
499
|
+
const { data: processedData, paginationInfo } = useMemo( () => {
|
|
500
|
+
return filterSortAndPaginate( data, view, fields );
|
|
501
|
+
}, [ data, view, fields ] );
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Callback function to update the view state.
|
|
505
|
+
*
|
|
506
|
+
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#onchangeview-function
|
|
507
|
+
*/
|
|
508
|
+
const onChangeView = useCallback( ( newView: View ) => {
|
|
509
|
+
setView( newView );
|
|
510
|
+
}, [] );
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* DataView getItemId function - returns the unique ID for each record in the dataset.
|
|
514
|
+
*
|
|
515
|
+
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#getitemid-function
|
|
516
|
+
*/
|
|
517
|
+
const getItemId = useCallback( ( item: Threat ) => item.id.toString(), [] );
|
|
518
|
+
|
|
519
|
+
return (
|
|
520
|
+
<DataViews
|
|
521
|
+
actions={ actions }
|
|
522
|
+
data={ processedData }
|
|
523
|
+
defaultLayouts={ defaultLayouts }
|
|
524
|
+
fields={ fields }
|
|
525
|
+
getItemId={ getItemId }
|
|
526
|
+
onChangeSelection={ onChangeSelection }
|
|
527
|
+
onChangeView={ onChangeView }
|
|
528
|
+
paginationInfo={ paginationInfo }
|
|
529
|
+
view={ view }
|
|
530
|
+
header={
|
|
531
|
+
<ThreatsStatusToggleGroupControl
|
|
532
|
+
data={ data }
|
|
533
|
+
view={ view }
|
|
534
|
+
onChangeView={ onChangeView }
|
|
535
|
+
/>
|
|
536
|
+
}
|
|
537
|
+
/>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
@import '@wordpress/dataviews/build-style/style.css';
|
|
2
|
+
|
|
3
|
+
.threat__title {
|
|
4
|
+
color: var( --jp-gray-80 );
|
|
5
|
+
font-weight: 510;
|
|
6
|
+
white-space: initial;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.threat__description {
|
|
10
|
+
color: var( --jp-gray-80 );
|
|
11
|
+
font-size: 12px;
|
|
12
|
+
white-space: initial;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.threat__fixedOn,
|
|
16
|
+
.threat__firstDetected {
|
|
17
|
+
white-space: nowrap;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.threat__fixedOn {
|
|
21
|
+
color: var( --jp-green-70 );
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.threat__media {
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: 100%;
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
background-color: #EDFFEE;
|
|
31
|
+
border-color: #EDFFEE;
|
|
32
|
+
|
|
33
|
+
svg {
|
|
34
|
+
fill: var( --jp-black );
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.toggle-group-control__option {
|
|
39
|
+
white-space: nowrap;
|
|
40
|
+
padding: 0 12px;
|
|
41
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { type Threat, type ThreatStatus } from '@automattic/jetpack-scan';
|
|
2
|
+
import {
|
|
3
|
+
__experimentalToggleGroupControl as ToggleGroupControl, // eslint-disable-line @wordpress/no-unsafe-wp-apis
|
|
4
|
+
__experimentalToggleGroupControlOption as ToggleGroupControlOption, // eslint-disable-line @wordpress/no-unsafe-wp-apis
|
|
5
|
+
} from '@wordpress/components';
|
|
6
|
+
import { type View } from '@wordpress/dataviews';
|
|
7
|
+
import { useMemo, useCallback } from '@wordpress/element';
|
|
8
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
9
|
+
import styles from './styles.module.scss';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ToggleGroupControl component for filtering threats by status.
|
|
13
|
+
* @param {object} props - Component props.
|
|
14
|
+
* @param { Threat[]} props.data - Threats data.
|
|
15
|
+
* @param { View } props.view - The current view.
|
|
16
|
+
* @param { Function } props.onChangeView - Callback function to handle view changes.
|
|
17
|
+
* @return {JSX.Element|null} The component or null.
|
|
18
|
+
*/
|
|
19
|
+
export default function ThreatsStatusToggleGroupControl( {
|
|
20
|
+
data,
|
|
21
|
+
view,
|
|
22
|
+
onChangeView,
|
|
23
|
+
}: {
|
|
24
|
+
data: Threat[];
|
|
25
|
+
view: View;
|
|
26
|
+
onChangeView: ( newView: View ) => void;
|
|
27
|
+
} ): JSX.Element {
|
|
28
|
+
/**
|
|
29
|
+
* Compute values from the provided threats data.
|
|
30
|
+
*
|
|
31
|
+
* @member {number} activeThreatsCount - Count of active threats.
|
|
32
|
+
* @member {number} historicThreatsCount - Count of historic threats.
|
|
33
|
+
*/
|
|
34
|
+
const {
|
|
35
|
+
activeThreatsCount,
|
|
36
|
+
historicThreatsCount,
|
|
37
|
+
}: {
|
|
38
|
+
activeThreatsCount: number;
|
|
39
|
+
historicThreatsCount: number;
|
|
40
|
+
} = useMemo( () => {
|
|
41
|
+
return data.reduce(
|
|
42
|
+
( acc, threat ) => {
|
|
43
|
+
if ( threat.status ) {
|
|
44
|
+
if ( threat.status === 'current' ) {
|
|
45
|
+
acc.activeThreatsCount++;
|
|
46
|
+
} else {
|
|
47
|
+
acc.historicThreatsCount++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return acc;
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
activeThreatsCount: 0,
|
|
54
|
+
historicThreatsCount: 0,
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}, [ data ] );
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Callback function to handle the status change filter.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} newStatus - The new status filter value.
|
|
63
|
+
*/
|
|
64
|
+
const onStatusFilterChange = useCallback(
|
|
65
|
+
( newStatus: string ) => {
|
|
66
|
+
const updatedFilters = view.filters.filter( filter => filter.field !== 'status' );
|
|
67
|
+
|
|
68
|
+
if ( newStatus === 'active' ) {
|
|
69
|
+
updatedFilters.push( {
|
|
70
|
+
field: 'status',
|
|
71
|
+
operator: 'isAny',
|
|
72
|
+
value: [ 'current' ],
|
|
73
|
+
} );
|
|
74
|
+
} else if ( newStatus === 'historic' ) {
|
|
75
|
+
updatedFilters.push( {
|
|
76
|
+
field: 'status',
|
|
77
|
+
operator: 'isAny',
|
|
78
|
+
value: [ 'fixed', 'ignored' ],
|
|
79
|
+
} );
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
onChangeView( {
|
|
83
|
+
...view,
|
|
84
|
+
filters: updatedFilters,
|
|
85
|
+
} );
|
|
86
|
+
},
|
|
87
|
+
[ view, onChangeView ]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Memoized function to determine if a status filter is selected.
|
|
92
|
+
*
|
|
93
|
+
* @param {Array} threatStatuses - List of threat statuses.
|
|
94
|
+
*/
|
|
95
|
+
const isStatusFilterSelected = useMemo(
|
|
96
|
+
() => ( threatStatuses: ThreatStatus[] ) =>
|
|
97
|
+
view.filters.some(
|
|
98
|
+
filter =>
|
|
99
|
+
filter.field === 'status' &&
|
|
100
|
+
Array.isArray( filter.value ) &&
|
|
101
|
+
filter.value.length === threatStatuses.length &&
|
|
102
|
+
threatStatuses.every( threatStatus => filter.value.includes( threatStatus ) )
|
|
103
|
+
),
|
|
104
|
+
[ view.filters ]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const selectedValue = useMemo( () => {
|
|
108
|
+
if ( isStatusFilterSelected( [ 'current' ] ) ) {
|
|
109
|
+
return 'active' as const;
|
|
110
|
+
}
|
|
111
|
+
if ( isStatusFilterSelected( [ 'fixed', 'ignored' ] ) ) {
|
|
112
|
+
return 'historic' as const;
|
|
113
|
+
}
|
|
114
|
+
return '' as const;
|
|
115
|
+
}, [ isStatusFilterSelected ] );
|
|
116
|
+
|
|
117
|
+
if ( ! ( activeThreatsCount + historicThreatsCount ) ) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
return (
|
|
123
|
+
<ToggleGroupControl
|
|
124
|
+
className={ styles[ 'toggle-group-control' ] }
|
|
125
|
+
value={ selectedValue }
|
|
126
|
+
onChange={ onStatusFilterChange }
|
|
127
|
+
__nextHasNoMarginBottom
|
|
128
|
+
>
|
|
129
|
+
<ToggleGroupControlOption
|
|
130
|
+
value="active"
|
|
131
|
+
label={
|
|
132
|
+
<span className={ styles[ 'toggle-group-control__option' ] }>
|
|
133
|
+
{ sprintf(
|
|
134
|
+
/* translators: %d: number of active threats */ __(
|
|
135
|
+
'Active threats (%d)',
|
|
136
|
+
'jetpack'
|
|
137
|
+
),
|
|
138
|
+
activeThreatsCount
|
|
139
|
+
) }
|
|
140
|
+
</span>
|
|
141
|
+
}
|
|
142
|
+
/>
|
|
143
|
+
<ToggleGroupControlOption
|
|
144
|
+
value="historic"
|
|
145
|
+
label={
|
|
146
|
+
<span className={ styles[ 'toggle-group-control__option' ] }>
|
|
147
|
+
{ sprintf(
|
|
148
|
+
/* translators: %d: number of historic threats */
|
|
149
|
+
__( 'History (%d)', 'jetpack' ),
|
|
150
|
+
historicThreatsCount
|
|
151
|
+
) }
|
|
152
|
+
</span>
|
|
153
|
+
}
|
|
154
|
+
/>
|
|
155
|
+
</ToggleGroupControl>
|
|
156
|
+
);
|
|
157
|
+
} catch ( error ) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
package/index.ts
CHANGED
|
@@ -44,7 +44,9 @@ export { default as CopyToClipboard } from './components/copy-to-clipboard';
|
|
|
44
44
|
export * from './components/icons';
|
|
45
45
|
export { default as SplitButton } from './components/split-button';
|
|
46
46
|
export { default as ThemeProvider } from './components/theme-provider';
|
|
47
|
+
export { default as ThreatFixerButton } from './components/threat-fixer-button';
|
|
47
48
|
export { default as ThreatSeverityBadge } from './components/threat-severity-badge';
|
|
49
|
+
export { default as ThreatsDataViews } from './components/threats-data-views';
|
|
48
50
|
export { default as Text, H2, H3, Title } from './components/text';
|
|
49
51
|
export { default as ToggleControl } from './components/toggle-control';
|
|
50
52
|
export { default as numberFormat } from './components/number-format';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/jetpack-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"description": "Jetpack Components Package",
|
|
5
5
|
"author": "Automattic",
|
|
6
6
|
"license": "GPL-2.0-or-later",
|
|
@@ -15,29 +15,31 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@automattic/format-currency": "1.0.1",
|
|
18
|
-
"@automattic/jetpack-boost-score-api": "^0.1.
|
|
18
|
+
"@automattic/jetpack-boost-score-api": "^0.1.46",
|
|
19
|
+
"@automattic/jetpack-scan": "^0.2.0",
|
|
19
20
|
"@babel/runtime": "^7",
|
|
20
|
-
"@wordpress/browserslist-config": "6.
|
|
21
|
-
"@wordpress/components": "28.
|
|
22
|
-
"@wordpress/compose": "7.
|
|
23
|
-
"@wordpress/data": "10.
|
|
24
|
-
"@wordpress/
|
|
25
|
-
"@wordpress/
|
|
26
|
-
"@wordpress/
|
|
27
|
-
"@wordpress/
|
|
28
|
-
"@wordpress/
|
|
21
|
+
"@wordpress/browserslist-config": "6.11.0",
|
|
22
|
+
"@wordpress/components": "28.11.0",
|
|
23
|
+
"@wordpress/compose": "7.11.0",
|
|
24
|
+
"@wordpress/data": "10.11.0",
|
|
25
|
+
"@wordpress/dataviews": "4.7.0",
|
|
26
|
+
"@wordpress/date": "5.11.0",
|
|
27
|
+
"@wordpress/element": "6.11.0",
|
|
28
|
+
"@wordpress/i18n": "5.11.0",
|
|
29
|
+
"@wordpress/icons": "10.11.0",
|
|
30
|
+
"@wordpress/notices": "5.11.0",
|
|
29
31
|
"clsx": "2.1.1",
|
|
30
32
|
"prop-types": "^15.7.2",
|
|
31
33
|
"qrcode.react": "3.1.0",
|
|
32
34
|
"react-slider": "2.0.5",
|
|
33
|
-
"social-logos": "^3.1.
|
|
35
|
+
"social-logos": "^3.1.12",
|
|
34
36
|
"uplot": "1.6.31",
|
|
35
37
|
"uplot-react": "1.1.4"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
|
-
"@automattic/jetpack-base-styles": "^0.6.
|
|
39
|
-
"@babel/core": "7.
|
|
40
|
-
"@babel/preset-react": "7.
|
|
40
|
+
"@automattic/jetpack-base-styles": "^0.6.36",
|
|
41
|
+
"@babel/core": "7.26.0",
|
|
42
|
+
"@babel/preset-react": "7.25.9",
|
|
41
43
|
"@jest/globals": "29.4.3",
|
|
42
44
|
"@storybook/addon-actions": "8.3.5",
|
|
43
45
|
"@storybook/blocks": "8.3.5",
|
|
@@ -47,8 +49,8 @@
|
|
|
47
49
|
"@testing-library/user-event": "14.5.2",
|
|
48
50
|
"@types/jest": "29.5.12",
|
|
49
51
|
"@types/qrcode.react": "1.0.5",
|
|
50
|
-
"@types/react": "18.3.
|
|
51
|
-
"@types/react-dom": "18.3.
|
|
52
|
+
"@types/react": "18.3.12",
|
|
53
|
+
"@types/react-dom": "18.3.1",
|
|
52
54
|
"@types/react-slider": "1.3.6",
|
|
53
55
|
"jest": "29.7.0",
|
|
54
56
|
"jest-environment-jsdom": "29.7.0",
|