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

ThreatsDataViews: Add ToggleGroupControl #39901

Merged
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6c3c360
Components: add hoverShow prop to IconTooltip
dkmyta Oct 27, 2024
81d25c6
changelog
nateweller Oct 27, 2024
f919501
Add ThreatsDataView
nateweller Oct 11, 2024
aaa4bed
Add ToggleGroupControl filters to ThreatsDataView
dkmyta Oct 28, 2024
9b5bdd2
Components: add hoverShow prop to IconTooltip
dkmyta Oct 27, 2024
18277d1
changelog
nateweller Oct 27, 2024
2ebc6a0
Add ThreatsDataView
nateweller Oct 11, 2024
2d529b8
Rebase, fix conflicts
dkmyta Oct 28, 2024
df3ce66
Fix rebase issues
dkmyta Oct 28, 2024
81594ac
Fixes, updates
dkmyta Oct 28, 2024
65fd9e3
Improve type checks
dkmyta Oct 29, 2024
e353eef
Components: add hoverShow prop to IconTooltip
dkmyta Oct 27, 2024
0802197
changelog
nateweller Oct 27, 2024
f27d02f
Add ThreatsDataView
nateweller Oct 11, 2024
e87dbbf
Rebase
dkmyta Oct 29, 2024
ae443bf
Story fixes
dkmyta Oct 29, 2024
02e98fc
Update toggle text
dkmyta Nov 4, 2024
2031331
Rebase, fix conflicts
dkmyta Nov 6, 2024
78c626e
Fix story data
dkmyta Nov 6, 2024
f5eb54c
Merge branch 'trunk' into add/component/threats-data-view-toggle-grou…
dkmyta Nov 6, 2024
2fffd3b
changelog
dkmyta Nov 6, 2024
dd7cee7
Fix changelog entry
dkmyta Nov 6, 2024
c2b6b0f
Fix types
dkmyta Nov 6, 2024
e047efc
Update approach to counting threats
dkmyta Nov 6, 2024
2f17bb7
Fix tests
dkmyta Nov 6, 2024
1806863
Update lock file
dkmyta Nov 6, 2024
3dcbe6c
Revert lock file changes
dkmyta Nov 6, 2024
e55e556
Set __nextHasNoMarginBottom to avoid deprecation warning
dkmyta Nov 6, 2024
9752c68
Move toggle to dedicated file
dkmyta Nov 8, 2024
1fa9179
Add default filters to stories
dkmyta Nov 8, 2024
f2e4c56
Wrap experimental component rendering in try/catch
dkmyta Nov 8, 2024
8b491ed
Update projects/js-packages/components/components/threats-data-views/…
dkmyta Nov 8, 2024
23e531b
Update projects/js-packages/components/components/threats-data-views/…
dkmyta Nov 8, 2024
9314e35
Update projects/js-packages/components/components/threats-data-views/…
dkmyta Nov 8, 2024
ec476f6
Update projects/js-packages/components/components/threats-data-views/…
dkmyta Nov 8, 2024
ac20168
Fix lint errors
dkmyta Nov 8, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

Add ToggleGroupControl to ThreatsDataViews for easily toggling between Active and Historical threats
175 changes: 162 additions & 13 deletions projects/js-packages/components/components/threats-data-views/index.tsx
Copy link
Contributor

@nateweller nateweller Nov 8, 2024

Choose a reason for hiding this comment

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

Idea - could we extract the <ThreatsStatusToggleGroupControl> component into a separate file?

It could be passed data: Threats[] and view: View properties, and compute/memoize its own values for counts/selected/etc.

It could also accept the existing onChangeView: ( newView: View ) => void callback, and call it with the full updated view objects on click.

The objective being to keep this main index file clean and straightforward, and isolate this custom functionality in its own file. Using the data/view/onChangeView properties directly essentially make it like a mini plugin/extension for the DataViews.

Copy link
Contributor

Choose a reason for hiding this comment

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

Second idea - since these are experimental components, what do you think about wrapping this component with a try/catch or React Error Boundary?

This will not help in the case of import errors, but if any breaking changes are missed, it will prevent the component from crashing the entire scan screen.

Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { getThreatType, type Threat } from '@automattic/jetpack-scan';
import { getThreatType, type Threat, type ThreatStatus } from '@automattic/jetpack-scan';
import {
type Action,
type ActionButton,
type Field,
type FieldType,
type Filter,
type SortDirection,
type SupportedLayouts,
type View,
__experimentalToggleGroupControl as ToggleGroupControl, // eslint-disable-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControlOption as ToggleGroupControlOption, // eslint-disable-line @wordpress/no-unsafe-wp-apis
} from '@wordpress/components';
import {
Action,
ActionButton,
DataViews,
Field,
FieldType,
Filter,
filterSortAndPaginate,
SortDirection,
SupportedLayouts,
type View,
} from '@wordpress/dataviews';
dkmyta marked this conversation as resolved.
Show resolved Hide resolved
import { dateI18n } from '@wordpress/date';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { Icon } from '@wordpress/icons';
import { useCallback, useMemo, useState } from 'react';
import Badge from '../badge';
Expand Down Expand Up @@ -42,6 +46,77 @@ import {
} from './constants';
import styles from './styles.module.scss';

/**
* ToggleGroupControl component for filtering threats by status.
* @param {object} props - Component props.
* @param {number} props.activeCount - Number of active threats.
* @param {number} props.historicCount - Number of historic threats.
* @param {boolean} props.isViewingActiveThreats - Whether the active status is selected.
* @param {boolean} props.isViewingHistoricThreats - Whether the historic status is selected.
* @param {Function} props.onStatusFilterChange - Callback function to handle the status filter change.
* @return {JSX.Element|null} The component or null.
*/
export function ThreatsStatusToggleGroupControl( {
activeCount,
historicCount,
isViewingActiveThreats,
isViewingHistoricThreats,
onStatusFilterChange,
}: {
activeCount: number;
historicCount: number;
isViewingActiveThreats: boolean;
isViewingHistoricThreats: boolean;
onStatusFilterChange: ( newValue: string ) => void;
} ): JSX.Element {
if ( ! ( activeCount + historicCount ) ) {
return null;
}

let selectedValue = '';
if ( isViewingActiveThreats ) {
selectedValue = 'active';
} else if ( isViewingHistoricThreats ) {
selectedValue = 'historic';
}

return (
<ToggleGroupControl
className={ styles[ 'toggle-group-control' ] }
value={ selectedValue }
onChange={ onStatusFilterChange }
__nextHasNoMarginBottom
>
<ToggleGroupControlOption
value="active"
label={
<span className={ styles[ 'toggle-group-control__option' ] }>
{ sprintf(
/* translators: %d: number of active threats */ __(
'Active threats (%d)',
'jetpack'
),
activeCount
) }
</span>
}
/>
<ToggleGroupControlOption
value="historic"
label={
<span className={ styles[ 'toggle-group-control__option' ] }>
{ sprintf(
/* translators: %d: number of historic threats */
__( 'History (%d)', 'jetpack' ),
historicCount
) }
</span>
}
/>
</ToggleGroupControl>
);
}

/**
* DataViews component for displaying security threats.
*
Expand Down Expand Up @@ -143,27 +218,59 @@ export default function ThreatsDataViews( {
...defaultLayouts.table,
} );

/**
* Memoized function to determine if a status filter is selected.
*
* @param {Array} threatStatuses - List of threat statuses.
*/
const isStatusFilterSelected = useMemo(
() => ( threatStatuses: ThreatStatus[] ) =>
view.filters.some(
filter =>
filter.field === 'status' &&
Array.isArray( filter.value ) &&
filter.value.length === threatStatuses.length &&
threatStatuses.every( threatStatus => filter.value.includes( threatStatus ) )
),
[ view.filters ]
);

/**
* Compute values from the provided threats data.
*
* @member {object[]} themes - List of unique themes included in the threats data.
* @member {object[]} plugins - List of unique plugins included in the threats data.
* @member {number} activeThreatsCount - Count of active threats.
* @member {number} historicThreatsCount - Count of historic threats.
* @member {object[]} themes - List of unique threat themes.
* @member {object[]} plugins - List of unique threat plugins.
* @member {object[]} signatures - List of unique threat signatures.
dkmyta marked this conversation as resolved.
Show resolved Hide resolved
* @member {string[]} dataFields - List of unique fields.
* @member {Array} dataFields - List of unique fields.
*/
dkmyta marked this conversation as resolved.
Show resolved Hide resolved
const {
activeThreatsCount,
historicThreatsCount,
themes,
plugins,
signatures,
dataFields,
}: {
activeThreatsCount: number;
historicThreatsCount: number;
themes: { value: string; label: string }[];
plugins: { value: string; label: string }[];
signatures: { value: string; label: string }[];
dataFields: string[];
} = useMemo( () => {
return data.reduce(
( acc, threat ) => {
// Active/Historic Threats
if ( threat.status ) {
if ( threat.status === 'current' ) {
acc.activeThreatsCount++;
} else {
acc.historicThreatsCount++;
}
}

// Extensions (Themes and Plugins)
if ( threat.extension ) {
switch ( threat.extension.type ) {
Expand Down Expand Up @@ -204,6 +311,8 @@ export default function ThreatsDataViews( {
return acc;
},
{
activeThreatsCount: 0,
historicThreatsCount: 0,
themes: [],
plugins: [],
signatures: [],
Expand Down Expand Up @@ -515,6 +624,37 @@ export default function ThreatsDataViews( {
*/
const getItemId = useCallback( ( item: Threat ) => item.id.toString(), [] );

/**
* Callback function to handle the status change filter.
*
* @param {string} newStatus - The new status filter value.
*/
const onStatusFilterChange = useCallback(
( newStatus: string ) => {
const updatedFilters = view.filters.filter( filter => filter.field !== 'status' );

if ( newStatus === 'active' ) {
updatedFilters.push( {
field: 'status',
operator: 'isAny',
value: [ 'current' ],
} );
} else if ( newStatus === 'historic' ) {
updatedFilters.push( {
field: 'status',
operator: 'isAny',
value: [ 'fixed', 'ignored' ],
} );
}

setView( {
...view,
filters: updatedFilters,
} );
},
[ view ]
);

return (
<DataViews
actions={ actions }
Expand All @@ -526,6 +666,15 @@ export default function ThreatsDataViews( {
onChangeView={ onChangeView }
paginationInfo={ paginationInfo }
view={ view }
header={
<ThreatsStatusToggleGroupControl
activeCount={ activeThreatsCount }
historicCount={ historicThreatsCount }
isViewingActiveThreats={ isStatusFilterSelected( [ 'current' ] ) }
isViewingHistoricThreats={ isStatusFilterSelected( [ 'fixed', 'ignored' ] ) }
onStatusFilterChange={ onStatusFilterChange }
/>
}
/>
);
}
Copy link
Contributor

@nateweller nateweller Nov 8, 2024

Choose a reason for hiding this comment

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

Minor - we could update the filters property passed to these story components to use the operator: 'is any' and value: [ 'current' ] approach, to ensure the filter is shown to be active on page load.

Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Default.args = {
fixedIn: '1.12.4',
severity: 3,
fixable: { fixer: 'update', target: '1.12.4', extensionStatus: 'inactive' },
fixer: { status: 'in_progress', last_updated: new Date().toISOString() },
fixer: { status: 'in_progress', lastUpdated: new Date().toISOString() },
status: 'current',
filename: null,
context: null,
Expand Down Expand Up @@ -176,7 +176,7 @@ FixerStatuses.args = {
severity: 4,
fixer: null,
fixedOn: '2024-07-15T22:01:42.000Z',
status: 'fixed',
status: 'current',
fixable: { fixer: 'update', target: '6.4.4', extensionStatus: 'inactive' },
version: '6.4.3',
source: '',
Expand All @@ -190,7 +190,7 @@ FixerStatuses.args = {
fixedIn: '1.2.4',
severity: 3,
fixable: { fixer: 'update', target: '1.12.4', extensionStatus: 'inactive' },
fixer: { status: 'in_progress', last_updated: new Date().toISOString() },
fixer: { status: 'in_progress', lastUpdated: new Date().toISOString() },
status: 'current',
source: 'https://wpscan.com/vulnerability/733d8a02-0d44-4b78-bbb2-37e447acd2f3',
extension: {
Expand All @@ -209,7 +209,7 @@ FixerStatuses.args = {
fixedIn: '2.22.22',
severity: 3,
fixable: { fixer: 'update', target: '1.12.4', extensionStatus: 'inactive' },
fixer: { status: 'in_progress', last_updated: new Date( '1999-01-01' ).toISOString() },
fixer: { status: 'in_progress', lastUpdated: new Date( '1999-01-01' ).toISOString() },
status: 'current',
source: 'https://wpscan.com/vulnerability/733d8a02-0d44-4b78-bbb2-37e447acd2f3',
extension: {
Expand Down Expand Up @@ -280,7 +280,6 @@ FreeResults.args = {
description:
'Versions 3.2.3 and earlier are affected by an issue where cached queries within shortcodes could lead to object injection. This is related to the recent WordPress 4.8.3 security release.This issue can only be exploited by users who can edit content and add shortcodes, but we still recommend all users running WooCommerce 3.x upgrade to 3.2 to mitigate this issue.',
fixedIn: '3.2.4',
status: 'current',
source: 'https://wpscan.com/vulnerability/1d0470df-4671-47ac-8d87-a165e8f7d502',
extension: {
name: 'WooCommerce',
Expand All @@ -296,7 +295,6 @@ FreeResults.args = {
description:
'The WooCommerce WordPress plugin was affected by an Authenticated Stored XSS security vulnerability.',
fixedIn: '3.4.6',
status: 'current',
source: 'https://wpscan.com/vulnerability/7275a176-d579-471a-8492-df8edbdf27de',
extension: {
name: 'WooCommerce',
Expand All @@ -311,7 +309,6 @@ FreeResults.args = {
description:
'The plugin was affected by an authenticated (admin+) RCE in the settings page due to input validation failure and weak $cache_path check in the WP Super Cache Settings -> Cache Location option. Direct access to the wp-cache-config.php file is not prohibited, so this vulnerability can be exploited for a web shell injection.\r\n\r\nAnother possible attack vector: from XSS (via another plugin affected by XSS) to RCE.',
fixedIn: '1.7.2',
status: 'current',
source: 'https://wpscan.com/vulnerability/733d8a02-0d44-4b78-bbb2-37e447acd2f3',
extension: {
name: 'WP Super Cache',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
border-color: #EDFFEE;

svg {
fill: black;
fill: var( --jp-black );
}
}

.toggle-group-control__option {
white-space: nowrap;
padding: 0 12px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const data = [
type: 'plugin' as const,
},
fixedIn: '3.2.4',
status: 'current' as const,
},
];

Expand Down
Loading