Skip to content

Commit d339c35

Browse files
dkmytanateweller
authored andcommitted
Components: Add ScanReport (#40419)
1 parent f0fcba8 commit d339c35

File tree

10 files changed

+360
-13
lines changed

10 files changed

+360
-13
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Adds ScanReport component
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 TYPES = [
11+
{ value: 'core', label: __( 'WordPress', 'jetpack-components' ) },
12+
{ value: 'plugins', label: __( 'Plugin', 'jetpack-components' ) },
13+
{ value: 'themes', label: __( 'Theme', 'jetpack-components' ) },
14+
{ value: 'files', label: __( 'Files', 'jetpack-components' ) },
15+
];
16+
17+
export const ICONS = {
18+
plugins: pluginIcon,
19+
themes: themeIcon,
20+
core: coreIcon,
21+
files: fileIcon,
22+
default: shieldIcon,
23+
};
24+
25+
export const FIELD_ICON = 'icon';
26+
export const FIELD_TYPE = 'type';
27+
export const FIELD_NAME = 'name';
28+
export const FIELD_STATUS = 'status';
29+
export const FIELD_UPDATE = 'update';
30+
export const FIELD_VERSION = 'version';
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { type ScanReportExtension } from '@automattic/jetpack-scan';
2+
import { Tooltip } from '@wordpress/components';
3+
import {
4+
type SupportedLayouts,
5+
type View,
6+
type Field,
7+
DataViews,
8+
filterSortAndPaginate,
9+
} from '@wordpress/dataviews';
10+
import { __ } from '@wordpress/i18n';
11+
import { Icon } from '@wordpress/icons';
12+
import { useCallback, useMemo, useState } from 'react';
13+
import ShieldIcon from '../shield-icon';
14+
import {
15+
FIELD_NAME,
16+
FIELD_VERSION,
17+
FIELD_ICON,
18+
FIELD_STATUS,
19+
FIELD_TYPE,
20+
TYPES,
21+
ICONS,
22+
} from './constants';
23+
import styles from './styles.module.scss';
24+
25+
/**
26+
* DataViews component for displaying a scan report.
27+
*
28+
* @param {object} props - Component props.
29+
* @param {Array} props.data - Scan report data.
30+
* @param {Function} props.onChangeSelection - Callback function run when an item is selected.
31+
*
32+
* @return {JSX.Element} The ScanReport component.
33+
*/
34+
export default function ScanReport( { data, onChangeSelection } ): JSX.Element {
35+
const baseView = {
36+
search: '',
37+
filters: [],
38+
page: 1,
39+
perPage: 20,
40+
};
41+
42+
/**
43+
* DataView default layouts.
44+
*
45+
* 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.
46+
*
47+
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#defaultlayouts-record-string-view
48+
*/
49+
const defaultLayouts: SupportedLayouts = {
50+
table: {
51+
...baseView,
52+
fields: [ FIELD_STATUS, FIELD_TYPE, FIELD_NAME, FIELD_VERSION ],
53+
layout: {
54+
primaryField: FIELD_STATUS,
55+
},
56+
},
57+
list: {
58+
...baseView,
59+
fields: [ FIELD_STATUS, FIELD_VERSION ],
60+
layout: {
61+
primaryField: FIELD_NAME,
62+
mediaField: FIELD_ICON,
63+
},
64+
},
65+
};
66+
67+
/**
68+
* DataView view object - configures how the dataset is visible to the user.
69+
*
70+
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#view-object
71+
*/
72+
const [ view, setView ] = useState< View >( {
73+
type: 'table',
74+
...defaultLayouts.table,
75+
} );
76+
77+
/**
78+
* DataView fields - describes the visible items for each record in the dataset.
79+
*
80+
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#fields-object
81+
*/
82+
const fields = useMemo( () => {
83+
const iconHeight = 20;
84+
const result: Field< ScanReportExtension >[] = [
85+
{
86+
id: FIELD_STATUS,
87+
label: __( 'Status', 'jetpack-components' ),
88+
render( { item }: { item: ScanReportExtension } ) {
89+
let variant: 'info' | 'warning' | 'success' = 'info';
90+
let text = __(
91+
'This item was added to your site after the most recent scan. We will check for threats during the next scheduled one.',
92+
'jetpack-components'
93+
);
94+
95+
if ( item.checked ) {
96+
if ( item.threats.length > 0 ) {
97+
variant = 'warning';
98+
text = __( 'Threat detected.', 'jetpack-components' );
99+
} else {
100+
variant = 'success';
101+
text = __( 'No known threats found that affect this version.', 'jetpack-components' );
102+
}
103+
}
104+
105+
return (
106+
<Tooltip className={ styles.tooltip } text={ text }>
107+
<div className={ styles.icon }>
108+
<ShieldIcon variant={ variant } height={ iconHeight } />
109+
</div>
110+
</Tooltip>
111+
);
112+
},
113+
},
114+
{
115+
id: FIELD_TYPE,
116+
label: __( 'Type', 'jetpack-components' ),
117+
elements: TYPES,
118+
},
119+
{
120+
id: FIELD_NAME,
121+
label: __( 'Name', 'jetpack-components' ),
122+
enableGlobalSearch: true,
123+
getValue( { item }: { item: ScanReportExtension } ) {
124+
return item.name ? item.name : '';
125+
},
126+
},
127+
{
128+
id: FIELD_VERSION,
129+
label: __( 'Version', 'jetpack-components' ),
130+
enableGlobalSearch: true,
131+
getValue( { item }: { item: ScanReportExtension } ) {
132+
return item.version ? item.version : '';
133+
},
134+
},
135+
...( view.type === 'list'
136+
? [
137+
{
138+
id: FIELD_ICON,
139+
label: __( 'Icon', 'jetpack-components' ),
140+
enableSorting: false,
141+
enableHiding: false,
142+
getValue( { item }: { item: ScanReportExtension } ) {
143+
return ICONS[ item.type ] || '';
144+
},
145+
render( { item }: { item: ScanReportExtension } ) {
146+
return (
147+
<div className={ styles.threat__media }>
148+
<Icon icon={ ICONS[ item.type ] } />
149+
</div>
150+
);
151+
},
152+
},
153+
]
154+
: [] ),
155+
];
156+
157+
return result;
158+
}, [ view ] );
159+
160+
/**
161+
* Apply the view settings (i.e. filters, sorting, pagination) to the dataset.
162+
*
163+
* @see https://github.com/WordPress/gutenberg/blob/trunk/packages/dataviews/src/filter-and-sort-data-view.ts
164+
*/
165+
const { data: processedData, paginationInfo } = useMemo( () => {
166+
return filterSortAndPaginate( data, view, fields );
167+
}, [ data, view, fields ] );
168+
169+
/**
170+
* Callback function to update the view state.
171+
*
172+
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#onchangeview-function
173+
*/
174+
const onChangeView = useCallback( ( newView: View ) => {
175+
setView( newView );
176+
}, [] );
177+
178+
/**
179+
* DataView getItemId function - returns the unique ID for each record in the dataset.
180+
*
181+
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#getitemid-function
182+
*/
183+
const getItemId = useCallback( ( item: ScanReportExtension ) => item.id.toString(), [] );
184+
185+
return (
186+
<DataViews
187+
data={ processedData }
188+
defaultLayouts={ defaultLayouts }
189+
fields={ fields }
190+
getItemId={ getItemId }
191+
onChangeSelection={ onChangeSelection }
192+
onChangeView={ onChangeView }
193+
paginationInfo={ paginationInfo }
194+
view={ view }
195+
/>
196+
);
197+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import ScanReport from '..';
2+
3+
export default {
4+
title: 'JS Packages/Components/Scan Report',
5+
component: ScanReport,
6+
parameters: {
7+
backgrounds: {
8+
default: 'light',
9+
values: [ { name: 'light', value: 'white' } ],
10+
},
11+
},
12+
decorators: [
13+
Story => (
14+
<div style={ { maxWidth: '100%', backgroundColor: 'white' } }>
15+
<Story />
16+
</div>
17+
),
18+
],
19+
};
20+
21+
export const Default = args => <ScanReport { ...args } />;
22+
Default.args = {
23+
data: [
24+
{
25+
id: 1,
26+
name: 'WordPress',
27+
slug: null,
28+
version: '6.7.1',
29+
threats: [],
30+
checked: true,
31+
type: 'core',
32+
},
33+
{
34+
id: 2,
35+
name: 'Jetpack',
36+
slug: 'jetpack/jetpack.php',
37+
version: '14.1-a.7',
38+
threats: [],
39+
checked: false,
40+
type: 'plugins',
41+
},
42+
{
43+
id: 3,
44+
name: 'Twenty Fifteen',
45+
slug: 'twentyfifteen',
46+
version: '1.1',
47+
threats: [
48+
{
49+
id: 198352527,
50+
signature: 'Vulnerable.WP.Extension',
51+
description: 'Vulnerable WordPress extension',
52+
severity: 3,
53+
},
54+
],
55+
checked: true,
56+
type: 'themes',
57+
},
58+
{
59+
id: 4,
60+
threats: [
61+
{
62+
id: 198352406,
63+
signature: 'EICAR_AV_Test_Suspicious',
64+
title: 'Malicious code found in file: jptt_eicar.php',
65+
severity: 1,
66+
},
67+
],
68+
checked: true,
69+
type: 'files',
70+
},
71+
],
72+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
@import '@wordpress/dataviews/build-style/style.css';
2+
3+
.threat__media {
4+
width: 100%;
5+
height: 100%;
6+
display: flex;
7+
align-items: center;
8+
justify-content: center;
9+
background-color: #EDFFEE;
10+
border-color: #EDFFEE;
11+
12+
svg {
13+
fill: var( --jp-black );
14+
}
15+
}
16+
17+
.tooltip {
18+
max-width: 240px;
19+
border-radius: 4px;
20+
text-align: left;
21+
}

projects/js-packages/components/components/shield-icon/index.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from 'react';
22

33
const COLORS = {
4-
error: '#D63638',
5-
warning: '#F0B849',
6-
success: '#069E08',
74
default: '#1d2327',
5+
info: '#A7AAAD',
6+
success: '#069E08',
7+
warning: '#F0B849',
8+
error: '#D63638',
89
};
910

1011
/**
@@ -32,11 +33,11 @@ export default function ShieldIcon( {
3233
}: {
3334
className?: string;
3435
contrast?: string;
35-
fill?: 'default' | 'success' | 'warning' | 'error' | string;
36+
fill?: 'default' | 'info' | 'success' | 'warning' | 'error' | string;
3637
height?: number;
3738
icon?: 'success' | 'error';
3839
outline?: boolean;
39-
variant: 'default' | 'success' | 'warning' | 'error';
40+
variant: 'default' | 'info' | 'success' | 'warning' | 'error';
4041
} ): JSX.Element {
4142
const shieldFill = COLORS[ fill ] || fill || COLORS[ variant ];
4243
const iconFill = outline ? shieldFill : contrast;
@@ -60,6 +61,9 @@ export default function ShieldIcon( {
6061
}
6162
fill={ shieldFill }
6263
/>
64+
{ 'info' === iconVariant && (
65+
<path d="M33.7 69H41.5V45.6H33.7V69ZM33.7 37.8H41.5V30H33.7V37.8Z" fill={ iconFill } />
66+
) }
6367
{ 'success' === iconVariant && (
6468
<path
6569
fillRule="evenodd"

projects/js-packages/components/components/shield-icon/stories/index.stories.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ export default {
1111
control: {
1212
type: 'select',
1313
},
14-
options: [ 'default', 'success', 'warning', 'error' ],
14+
options: [ 'default', 'info', 'success', 'warning', 'error' ],
1515
},
1616
icon: {
1717
control: {
1818
type: 'select',
1919
},
20-
options: [ 'success', 'error' ],
20+
options: [ 'info', 'success', 'error' ],
2121
},
2222
fill: {
2323
control: 'color',
@@ -39,12 +39,14 @@ export const Variants = () => {
3939
<div style={ { display: 'flex', flexDirection: 'column', gap: '8px' } }>
4040
<div style={ { display: 'flex', gap: '8px' } }>
4141
<ShieldIcon variant="default" />
42+
<ShieldIcon variant="info" />
4243
<ShieldIcon variant="success" />
4344
<ShieldIcon variant="warning" />
4445
<ShieldIcon variant="error" />
4546
</div>
4647
<div style={ { display: 'flex', gap: '8px' } }>
4748
<ShieldIcon variant="default" outline />
49+
<ShieldIcon variant="info" outline />
4850
<ShieldIcon variant="success" outline />
4951
<ShieldIcon variant="warning" outline />
5052
<ShieldIcon variant="error" outline />

0 commit comments

Comments
 (0)