diff --git a/projects/js-packages/components/changelog/components-add-scan-report b/projects/js-packages/components/changelog/components-add-scan-report new file mode 100644 index 0000000000000..ba0fbd4cce025 --- /dev/null +++ b/projects/js-packages/components/changelog/components-add-scan-report @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Adds ScanReport component diff --git a/projects/js-packages/components/components/scan-report/constants.ts b/projects/js-packages/components/components/scan-report/constants.ts new file mode 100644 index 0000000000000..436eed91c5701 --- /dev/null +++ b/projects/js-packages/components/components/scan-report/constants.ts @@ -0,0 +1,30 @@ +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 TYPES = [ + { value: 'core', label: __( 'WordPress', 'jetpack-components' ) }, + { value: 'plugins', label: __( 'Plugin', 'jetpack-components' ) }, + { value: 'themes', label: __( 'Theme', 'jetpack-components' ) }, + { value: 'files', label: __( 'Files', 'jetpack-components' ) }, +]; + +export const ICONS = { + plugins: pluginIcon, + themes: themeIcon, + core: coreIcon, + files: fileIcon, + default: shieldIcon, +}; + +export const FIELD_ICON = 'icon'; +export const FIELD_TYPE = 'type'; +export const FIELD_NAME = 'name'; +export const FIELD_STATUS = 'status'; +export const FIELD_UPDATE = 'update'; +export const FIELD_VERSION = 'version'; diff --git a/projects/js-packages/components/components/scan-report/index.tsx b/projects/js-packages/components/components/scan-report/index.tsx new file mode 100644 index 0000000000000..14795376f7d95 --- /dev/null +++ b/projects/js-packages/components/components/scan-report/index.tsx @@ -0,0 +1,197 @@ +import { type ScanReportExtension } from '@automattic/jetpack-scan'; +import { Tooltip } from '@wordpress/components'; +import { + type SupportedLayouts, + type View, + type Field, + DataViews, + filterSortAndPaginate, +} from '@wordpress/dataviews'; +import { __ } from '@wordpress/i18n'; +import { Icon } from '@wordpress/icons'; +import { useCallback, useMemo, useState } from 'react'; +import ShieldIcon from '../shield-icon'; +import { + FIELD_NAME, + FIELD_VERSION, + FIELD_ICON, + FIELD_STATUS, + FIELD_TYPE, + TYPES, + ICONS, +} from './constants'; +import styles from './styles.module.scss'; + +/** + * DataViews component for displaying a scan report. + * + * @param {object} props - Component props. + * @param {Array} props.data - Scan report data. + * @param {Function} props.onChangeSelection - Callback function run when an item is selected. + * + * @return {JSX.Element} The ScanReport component. + */ +export default function ScanReport( { data, onChangeSelection } ): JSX.Element { + const baseView = { + search: '', + filters: [], + page: 1, + perPage: 20, + }; + + /** + * DataView default layouts. + * + * 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. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#defaultlayouts-record-string-view + */ + const defaultLayouts: SupportedLayouts = { + table: { + ...baseView, + fields: [ FIELD_STATUS, FIELD_TYPE, FIELD_NAME, FIELD_VERSION ], + layout: { + primaryField: FIELD_STATUS, + }, + }, + list: { + ...baseView, + fields: [ FIELD_STATUS, FIELD_VERSION ], + layout: { + primaryField: FIELD_NAME, + mediaField: FIELD_ICON, + }, + }, + }; + + /** + * DataView view object - configures how the dataset is visible to the user. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#view-object + */ + const [ view, setView ] = useState< View >( { + type: 'table', + ...defaultLayouts.table, + } ); + + /** + * DataView fields - describes the visible items for each record in the dataset. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#fields-object + */ + const fields = useMemo( () => { + const iconHeight = 20; + const result: Field< ScanReportExtension >[] = [ + { + id: FIELD_STATUS, + label: __( 'Status', 'jetpack-components' ), + render( { item }: { item: ScanReportExtension } ) { + let variant: 'info' | 'warning' | 'success' = 'info'; + let text = __( + 'This item was added to your site after the most recent scan. We will check for threats during the next scheduled one.', + 'jetpack-components' + ); + + if ( item.checked ) { + if ( item.threats.length > 0 ) { + variant = 'warning'; + text = __( 'Threat detected.', 'jetpack-components' ); + } else { + variant = 'success'; + text = __( 'No known threats found that affect this version.', 'jetpack-components' ); + } + } + + return ( + +
+ +
+
+ ); + }, + }, + { + id: FIELD_TYPE, + label: __( 'Type', 'jetpack-components' ), + elements: TYPES, + }, + { + id: FIELD_NAME, + label: __( 'Name', 'jetpack-components' ), + enableGlobalSearch: true, + getValue( { item }: { item: ScanReportExtension } ) { + return item.name ? item.name : ''; + }, + }, + { + id: FIELD_VERSION, + label: __( 'Version', 'jetpack-components' ), + enableGlobalSearch: true, + getValue( { item }: { item: ScanReportExtension } ) { + return item.version ? item.version : ''; + }, + }, + ...( view.type === 'list' + ? [ + { + id: FIELD_ICON, + label: __( 'Icon', 'jetpack-components' ), + enableSorting: false, + enableHiding: false, + getValue( { item }: { item: ScanReportExtension } ) { + return ICONS[ item.type ] || ''; + }, + render( { item }: { item: ScanReportExtension } ) { + return ( +
+ +
+ ); + }, + }, + ] + : [] ), + ]; + + return result; + }, [ view ] ); + + /** + * Apply the view settings (i.e. filters, sorting, pagination) to the dataset. + * + * @see https://github.com/WordPress/gutenberg/blob/trunk/packages/dataviews/src/filter-and-sort-data-view.ts + */ + const { data: processedData, paginationInfo } = useMemo( () => { + return filterSortAndPaginate( data, view, fields ); + }, [ data, view, fields ] ); + + /** + * Callback function to update the view state. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#onchangeview-function + */ + const onChangeView = useCallback( ( newView: View ) => { + setView( newView ); + }, [] ); + + /** + * DataView getItemId function - returns the unique ID for each record in the dataset. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#getitemid-function + */ + const getItemId = useCallback( ( item: ScanReportExtension ) => item.id.toString(), [] ); + + return ( + + ); +} diff --git a/projects/js-packages/components/components/scan-report/stories/index.stories.tsx b/projects/js-packages/components/components/scan-report/stories/index.stories.tsx new file mode 100644 index 0000000000000..63926908850de --- /dev/null +++ b/projects/js-packages/components/components/scan-report/stories/index.stories.tsx @@ -0,0 +1,72 @@ +import ScanReport from '..'; + +export default { + title: 'JS Packages/Components/Scan Report', + component: ScanReport, + parameters: { + backgrounds: { + default: 'light', + values: [ { name: 'light', value: 'white' } ], + }, + }, + decorators: [ + Story => ( +
+ +
+ ), + ], +}; + +export const Default = args => ; +Default.args = { + data: [ + { + id: 1, + name: 'WordPress', + slug: null, + version: '6.7.1', + threats: [], + checked: true, + type: 'core', + }, + { + id: 2, + name: 'Jetpack', + slug: 'jetpack/jetpack.php', + version: '14.1-a.7', + threats: [], + checked: false, + type: 'plugins', + }, + { + id: 3, + name: 'Twenty Fifteen', + slug: 'twentyfifteen', + version: '1.1', + threats: [ + { + id: 198352527, + signature: 'Vulnerable.WP.Extension', + description: 'Vulnerable WordPress extension', + severity: 3, + }, + ], + checked: true, + type: 'themes', + }, + { + id: 4, + threats: [ + { + id: 198352406, + signature: 'EICAR_AV_Test_Suspicious', + title: 'Malicious code found in file: jptt_eicar.php', + severity: 1, + }, + ], + checked: true, + type: 'files', + }, + ], +}; diff --git a/projects/js-packages/components/components/scan-report/styles.module.scss b/projects/js-packages/components/components/scan-report/styles.module.scss new file mode 100644 index 0000000000000..d313d4cb8898a --- /dev/null +++ b/projects/js-packages/components/components/scan-report/styles.module.scss @@ -0,0 +1,21 @@ +@import '@wordpress/dataviews/build-style/style.css'; + +.threat__media { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: #EDFFEE; + border-color: #EDFFEE; + + svg { + fill: var( --jp-black ); + } +} + +.tooltip { + max-width: 240px; + border-radius: 4px; + text-align: left; +} \ No newline at end of file diff --git a/projects/js-packages/components/components/shield-icon/index.tsx b/projects/js-packages/components/components/shield-icon/index.tsx index fee9f4d70c463..b07b943b5e7fa 100644 --- a/projects/js-packages/components/components/shield-icon/index.tsx +++ b/projects/js-packages/components/components/shield-icon/index.tsx @@ -1,10 +1,11 @@ import React from 'react'; const COLORS = { - error: '#D63638', - warning: '#F0B849', - success: '#069E08', default: '#1d2327', + info: '#A7AAAD', + success: '#069E08', + warning: '#F0B849', + error: '#D63638', }; /** @@ -32,11 +33,11 @@ export default function ShieldIcon( { }: { className?: string; contrast?: string; - fill?: 'default' | 'success' | 'warning' | 'error' | string; + fill?: 'default' | 'info' | 'success' | 'warning' | 'error' | string; height?: number; icon?: 'success' | 'error'; outline?: boolean; - variant: 'default' | 'success' | 'warning' | 'error'; + variant: 'default' | 'info' | 'success' | 'warning' | 'error'; } ): JSX.Element { const shieldFill = COLORS[ fill ] || fill || COLORS[ variant ]; const iconFill = outline ? shieldFill : contrast; @@ -60,6 +61,9 @@ export default function ShieldIcon( { } fill={ shieldFill } /> + { 'info' === iconVariant && ( + + ) } { 'success' === iconVariant && ( {
+
+ diff --git a/projects/js-packages/components/index.ts b/projects/js-packages/components/index.ts index 4b0f3612012e7..6df50ee7fdb61 100644 --- a/projects/js-packages/components/index.ts +++ b/projects/js-packages/components/index.ts @@ -48,6 +48,7 @@ export { default as ThreatFixerButton } from './components/threat-fixer-button'; export { default as ThreatSeverityBadge } from './components/threat-severity-badge'; export { default as ThreatsDataViews } from './components/threats-data-views'; export { default as ShieldIcon } from './components/shield-icon'; +export { default as ScanReport } from './components/scan-report'; export { default as Text, H2, H3, Title } from './components/text'; export { default as ToggleControl } from './components/toggle-control'; export { default as numberFormat } from './components/number-format'; diff --git a/projects/js-packages/scan/changelog/components-add-scan-report b/projects/js-packages/scan/changelog/components-add-scan-report new file mode 100644 index 0000000000000..eeb9c55de4a28 --- /dev/null +++ b/projects/js-packages/scan/changelog/components-add-scan-report @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Updates/adds scan types diff --git a/projects/js-packages/scan/src/types/threats.ts b/projects/js-packages/scan/src/types/threats.ts index 016e2e1eaee51..19f76cce6875a 100644 --- a/projects/js-packages/scan/src/types/threats.ts +++ b/projects/js-packages/scan/src/types/threats.ts @@ -4,6 +4,23 @@ export type ThreatStatus = 'fixed' | 'ignored' | 'current'; export type ThreatFixType = 'replace' | 'delete' | 'update' | string; +export type ScanReportExtension = { + id: number; + checked: boolean; + slug?: string; + name?: string; + version?: string; + threats: Threat[]; + type: 'plugin' | 'theme' | 'core' | 'files'; +}; + +export type Extension = { + slug?: string; + name: string; + version: string; + type: 'plugin' | 'theme' | 'core'; +}; + export type Threat = { /** The threat's unique ID. */ id: string | number; @@ -57,10 +74,5 @@ export type Threat = { diff?: string; /** The affected extension. */ - extension?: { - slug: string; - name: string; - version: string; - type: 'plugin' | 'theme' | 'core'; - }; + extension?: Extension; };