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;
};