Skip to content

Commit

Permalink
Protect: Add Scan History UI (#37988)
Browse files Browse the repository at this point in the history
  • Loading branch information
dkmyta authored Jun 28, 2024
1 parent 8cda133 commit b52bbe5
Show file tree
Hide file tree
Showing 23 changed files with 822 additions and 211 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add Scan History UI
1 change: 1 addition & 0 deletions projects/plugins/protect/src/class-jetpack-protect.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ public function initial_state() {
'apiNonce' => wp_create_nonce( 'wp_rest' ),
'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ),
'status' => Status::get_status( $refresh_status_from_wpcom ),
'viewingScanHistory' => false,
'scanHistory' => Scan_History::get_scan_history( $refresh_status_from_wpcom ),
'installedPlugins' => Plugins_Installer::get_plugins(),
'installedThemes' => Sync_Functions::get_themes(),
Expand Down
51 changes: 51 additions & 0 deletions projects/plugins/protect/src/class-rest-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,30 @@ public static function register_rest_endpoints() {
},
)
);

register_rest_route(
'jetpack-protect/v1',
'scan-history',
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::api_get_scan_history',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
)
);

register_rest_route(
'jetpack-protect/v1',
'clear-scan-history-cache',
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::api_clear_scan_history_cache',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
)
);
}

/**
Expand Down Expand Up @@ -409,4 +433,31 @@ public static function api_complete_onboarding_steps( $request ) {

return new WP_REST_Response( 'Onboarding step(s) completed.' );
}

/**
* Return Scan History for the API endpoint
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response
*/
public static function api_get_scan_history( $request ) {
$scan_history = Scan_History::get_scan_history( false, $request['filter'] );
return rest_ensure_response( $scan_history, 200 );
}

/**
* Clear the Scan_History cache for the API endpoint
*
* @return WP_REST_Response
*/
public static function api_clear_scan_history_cache() {
$cache_cleared = Scan_History::delete_option();

if ( ! $cache_cleared ) {
return new WP_REST_Response( 'An error occured while attempting to clear the Jetpack Scan history cache.', 500 );
}

return new WP_REST_Response( 'Jetpack Scan history cache cleared.' );
}
}
14 changes: 14 additions & 0 deletions projects/plugins/protect/src/class-scan-history.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Protect_Models\Extension_Model;
use Automattic\Jetpack\Protect_Status\Plan;
use Jetpack_Options;
use WP_Error;

Expand Down Expand Up @@ -96,6 +98,18 @@ public static function update_history_option( $history ) {
update_option( static::OPTION_TIMESTAMP_NAME, time() + static::OPTION_EXPIRES_AFTER );
}

/**
* Delete the cached history and its timestamp
*
* @return bool Whether all related history options were successfully deleted.
*/
public static function delete_option() {
$option_deleted = delete_option( static::OPTION_NAME );
$option_timestamp_deleted = delete_option( static::OPTION_TIMESTAMP_NAME );

return $option_deleted && $option_timestamp_deleted;
}

/**
* Gets the current history of the Jetpack Protect checks
*
Expand Down
4 changes: 4 additions & 0 deletions projects/plugins/protect/src/class-threats.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public static function update_threat( $threat_id, $updates ) {

// clear the now out-of-date cache
Scan_Status::delete_option();
Scan_History::delete_option();

return true;
}
Expand Down Expand Up @@ -113,6 +114,7 @@ public static function fix_threats( $threat_ids ) {

// clear the now out-of-date cache
Scan_Status::delete_option();
Scan_History::delete_option();

$parsed_response = json_decode( $response['body'] );

Expand Down Expand Up @@ -158,6 +160,7 @@ public static function fix_threats_status( $threat_ids ) {

// clear the potentially out-of-date cache
Scan_Status::delete_option();
Scan_History::delete_option();

return $parsed_response;
}
Expand Down Expand Up @@ -197,6 +200,7 @@ public static function scan() {

// clear the now out-of-date cache
Scan_Status::delete_option();
Scan_History::delete_option();

return true;
}
Expand Down
7 changes: 7 additions & 0 deletions projects/plugins/protect/src/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ const API = {
method: 'POST',
data: { step_ids: stepIds },
} ),

fetchScanHistory: $filter =>
apiFetch( {
path: 'jetpack-protect/v1/scan-history',
method: 'POST',
data: { filter: $filter },
} ),
};

export default API;
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Spinner, Text, useBreakpointMatch } from '@automattic/jetpack-components';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { dateI18n } from '@wordpress/date';
import { sprintf, __ } from '@wordpress/i18n';
import { Icon, check, chevronDown, chevronUp } from '@wordpress/icons';
import clsx from 'clsx';
import React, { useState, useCallback, useContext } from 'react';
import useScanHistory from '../../hooks/use-scan-history';
import { STORE_ID } from '../../state/store';
import ThreatSeverityBadge from '../severity';
import styles from './styles.module.scss';
Expand All @@ -18,12 +20,15 @@ export const PaidAccordionItem = ( {
fixable,
severity,
children,
firstDetected,
fixedOn,
onOpen,
} ) => {
const accordionData = useContext( PaidAccordionContext );
const open = accordionData?.open === id;
const setOpen = accordionData?.setOpen;
const threatsAreFixing = useSelect( select => select( STORE_ID ).getThreatsAreFixing() );
const { viewingScanHistory } = useScanHistory();

const bodyClassNames = clsx( styles[ 'accordion-body' ], {
[ styles[ 'accordion-body-open' ] ]: open,
Expand All @@ -41,6 +46,49 @@ export const PaidAccordionItem = ( {

const [ isSmall ] = useBreakpointMatch( [ 'sm', 'lg' ], [ null, '<' ] );

const FixDetails = ( { date, isFixed } ) => (
<span className={ styles[ isFixed ? 'is-fixed' : 'is-ignored' ] }>
{ isFixed
? sprintf(
/* translators: %s: Fixed on date */
__( 'Threat fixed %s', 'jetpack-protect' ),
dateI18n( 'M j, Y', date )
)
: __( 'Threat ignored', 'jetpack-protect' ) }
</span>
);

const ScanHistoryDetails = ( { viewingHistory, detectedAt, fixedAt } ) => {
if ( ! viewingHistory ) {
return null;
}

return (
<>
{ detectedAt && (
<Text className={ styles[ 'accordion-header-status' ] }>
{ sprintf(
/* translators: %s: First detected date */
__( 'Threat found %s', 'jetpack-protect' ),
dateI18n( 'M j, Y', detectedAt )
) }
<span className={ styles[ 'accordion-header-status-separator' ] }></span>
<FixDetails date={ fixedAt || detectedAt } isFixed={ !! fixedAt } />
</Text>
) }
<StatusBadge status={ fixedAt ? 'fixed' : 'ignored' } />
</>
);
};

const StatusBadge = ( { status } ) => (
<div className={ `${ styles[ 'status-badge' ] } ${ styles[ status ] }` }>
{ 'fixed' === status
? __( 'Fixed', 'jetpack-protect' )
: __( 'Ignored', 'jetpack-protect', /* dummy arg to avoid bad minification */ 0 ) }
</div>
);

return (
<div className={ styles[ 'accordion-item' ] }>
<button className={ styles[ 'accordion-header' ] } onClick={ handleClick }>
Expand All @@ -55,6 +103,11 @@ export const PaidAccordionItem = ( {
>
{ title }
</Text>
<ScanHistoryDetails
viewingHistory={ viewingScanHistory }
detectedAt={ firstDetected }
fixedAt={ fixedOn }
/>
</div>
<div>
<ThreatSeverityBadge severity={ severity } />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@
margin-bottom: var( --spacing-base ); // 8px
}

.accordion-header-status {
font-size: var( --font-body-small );
font-weight: normal;
margin-left: calc( var( --spacing-base ) * 4 ); // 32px
margin-bottom: var( --spacing-base ); // 8px
}

.accordion-header-status-separator {
display: inline-block;
height: 4px;
margin: 2px 12px;
width: 4px;
background-color: var( --jp-gray-50 );
}

.accordion-header-button {
align-items: center;
}
Expand Down Expand Up @@ -88,6 +103,34 @@
fill: var( --jp-green-40 );
}

.status-badge {
border-radius: 32px;
flex-shrink: 0;
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 16px;
padding: calc( var( --spacing-base ) / 2 ); // 4px
position: relative;
text-align: center;
width: 60px;
margin-left: calc( var( --spacing-base ) * 4 ); // 32px

&.fixed {
color: var( --jp-white );
background-color: #008a20;
}

&.ignored {
color: var( --jp-white );
background-color: var( --jp-gray-50 );
}
}

.is-fixed {
color: #008a20;
}

@media ( max-width: 599px ) {
.accordion-header {
display: grid;
Expand Down Expand Up @@ -118,4 +161,17 @@
}
}

}
.status-badge {
display: none;
}
}

@media ( max-width: 1200px ) {
.accordion-header-status {
display: grid;
}

.accordion-header-status-separator {
display: none;
}
}
Loading

0 comments on commit b52bbe5

Please sign in to comment.