Skip to content

Commit

Permalink
Backup: add on-demand backup feature (#37998)
Browse files Browse the repository at this point in the history
* Add enqueue backup endpoint

* changelog

* Add hook to decide whether to show the back up now button

* Update composer and changelog for backup package

* Fetch backups stopped flag from /site/backup/size endpoint

* Update hooks

* Update getBackupStoppedFlag

* Add back up now button component

* Add `@wordpress/components` and `prop-types` to the backup package

* Adjust in progress message when it is the initial backup or not

* Update `useBackupState` to return if the latest backup is an initial backup

* Add back up now button on the header

* Refetch backup state once the new backup is enqueued

* Add actions, reducers and selectors for fetching backups

* Update useBackupsState hook to use redux for backup state management

* Refactor ReviewMessage to use useBackupsState()

* Update useBackupsState hook to include enqueued parameter on BackupNowButton

* Refactor useBackupsState.js tests

* Add site-backups reducer tests

* Add tests for getBackups action

* Add site-backups selectors tests

* Update inProgressBackup to not use __() with ternary operators

* Add translations to `Back up now` button labels and tooltip descriptions
  • Loading branch information
Initsogar authored Jun 26, 2024
1 parent b023a31 commit 93d73d2
Show file tree
Hide file tree
Showing 28 changed files with 824 additions and 134 deletions.
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions projects/packages/backup/changelog/add-backup-on-demand
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add on-demand backups feature in the backup package
2 changes: 1 addition & 1 deletion projects/packages/backup/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"link-template": "https://github.com/Automattic/jetpack-backup/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "3.3.x-dev"
"dev-trunk": "3.4.x-dev"
}
},
"config": {
Expand Down
2 changes: 2 additions & 0 deletions projects/packages/backup/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@
"@automattic/jetpack-components": "workspace:*",
"@automattic/jetpack-connection": "workspace:*",
"@wordpress/api-fetch": "7.0.0",
"@wordpress/components": "28.0.0",
"@wordpress/data": "10.0.0",
"@wordpress/date": "5.0.0",
"@wordpress/element": "6.0.0",
"@wordpress/i18n": "5.0.0",
"prop-types": "^15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1"
},
Expand Down
40 changes: 40 additions & 0 deletions projects/packages/backup/src/class-jetpack-backup.php
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,17 @@ public static function register_rest_routes() {
),
)
);

// Enqueue a new backup
register_rest_route(
'jetpack/v4',
'/site/backup/enqueue',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => __CLASS__ . '::enqueue_backup',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
)
);
}

/**
Expand Down Expand Up @@ -744,6 +755,35 @@ public static function get_site_backup_addon_offer( $request ) {
return rest_ensure_response( $response );
}

/**
* Enqueue a new backup on demand
*
* @return string|WP_Error A JSON object with `success` if the request was successful,
* or a WP_Error otherwise.
*/
public static function enqueue_backup() {
$blog_id = Jetpack_Options::get_option( 'id' );
$endpoint = sprintf( '/sites/%d/rewind/backups/enqueue', $blog_id );

$response = Client::wpcom_json_api_request_as_user(
$endpoint,
'v2',
array(
'method' => 'POST',
),
null,
'wpcom'
);

if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}

return rest_ensure_response(
json_decode( $response['body'], true )
);
}

/**
* Removes plugin from the connection manager
* If it's the last plugin using the connection, the site will be disconnected.
Expand Down
2 changes: 1 addition & 1 deletion projects/packages/backup/src/class-package-version.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
class Package_Version {

const PACKAGE_VERSION = '3.3.17';
const PACKAGE_VERSION = '3.4.0-alpha';

const PACKAGE_SLUG = 'backup';

Expand Down
20 changes: 20 additions & 0 deletions projects/packages/backup/src/js/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
SITE_BACKUP_POLICIES_GET_SUCCESS,
SITE_BACKUP_STORAGE_SET,
SITE_BACKUP_STORAGE_ADDON_OFFER_SET,
SITE_BACKUPS_GET,
SITE_BACKUPS_GET_FAILED,
SITE_BACKUPS_GET_SUCCESS,
} from './types';

const getSiteSize =
Expand All @@ -29,6 +32,7 @@ const getSiteSize =
daysOfBackupsAllowed: res.days_of_backups_allowed,
daysOfBackupsSaved: res.days_of_backups_saved,
retentionDays: res.retention_days,
backupsStopped: res.backups_stopped,
};

dispatch( { type: SITE_BACKUP_SIZE_GET_SUCCESS, payload } );
Expand Down Expand Up @@ -77,7 +81,23 @@ const setAddonStorageOfferSlug =
} );
};

const getBackups =
() =>
( { dispatch } ) => {
dispatch( { type: SITE_BACKUPS_GET } );

apiFetch( { path: '/jetpack/v4/backups' } ).then(
res => {
dispatch( { type: SITE_BACKUPS_GET_SUCCESS, payload: res } );
},
() => {
dispatch( { type: SITE_BACKUPS_GET_FAILED } );
}
);
};

const actions = {
getBackups,
getSiteSize,
getSitePolicies,
setStorageUsageLevel,
Expand Down
70 changes: 70 additions & 0 deletions projects/packages/backup/src/js/actions/test/get-backups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import apiFetch from '@wordpress/api-fetch';
import actions from '../index';
import { SITE_BACKUPS_GET, SITE_BACKUPS_GET_SUCCESS, SITE_BACKUPS_GET_FAILED } from '../types';

jest.mock( '@wordpress/api-fetch' );

const anyFunction = () => {};

const apiFixtures = {
requestOptions: {
path: '/jetpack/v4/backups',
},
successResponse: [
{
id: '123456789',
started: '2024-06-26 11:40:54',
last_updated: '2024-06-26 11:44:55',
status: 'not-accessible',
period: '321321321',
percent: '0',
is_backup: '1',
is_scan: '0',
},
{
id: '987654321',
started: '2024-06-26 06:36:08',
last_updated: '2024-06-26 06:39:05',
status: 'finished',
period: '123123123',
percent: '100',
is_backup: '1',
is_scan: '0',
has_snapshot: true,
discarded: '0',
stats: {},
},
],
failureResponse: 'Timeout error',
};

describe( 'getBackups', () => {
beforeEach( () => jest.clearAllMocks() );

it( 'dispatches SITE_BACKUPS_GET and SITE_BACKUPS_GET_SUCCESS on successful fetch', async () => {
const dispatch = jest.fn( anyFunction );
apiFetch.mockReturnValue( Promise.resolve( apiFixtures.successResponse ) );

await actions.getBackups()( { dispatch } );
expect( apiFetch ).toHaveBeenCalledWith( apiFixtures.requestOptions );

expect( dispatch ).toHaveBeenCalledTimes( 2 );
expect( dispatch ).toHaveBeenCalledWith( { type: SITE_BACKUPS_GET } );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUPS_GET_SUCCESS,
payload: apiFixtures.successResponse,
} );
} );

it( 'dispatches SITE_BACKUPS_GET and SITE_BACKUPS_GET_FAILED when API call fails', async () => {
const dispatch = jest.fn( anyFunction );
apiFetch.mockReturnValue( Promise.reject( apiFixtures.failureResponse ) );

await actions.getBackups()( { dispatch } );
expect( apiFetch ).toHaveBeenCalledWith( apiFixtures.requestOptions );

expect( dispatch ).toHaveBeenCalledTimes( 2 );
expect( dispatch ).toHaveBeenCalledWith( { type: SITE_BACKUPS_GET } );
expect( dispatch ).toHaveBeenCalledWith( { type: SITE_BACKUPS_GET_FAILED } );
} );
} );
3 changes: 3 additions & 0 deletions projects/packages/backup/src/js/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ export const SITE_BACKUP_SIZE_GET_FAILED = 'SITE_BACKUP_SIZE_GET_FAILED';
export const SITE_BACKUP_SIZE_GET_SUCCESS = 'SITE_BACKUP_SIZE_GET_SUCCESS';
export const SITE_BACKUP_STORAGE_ADDON_OFFER_SET = 'SITE_BACKUP_STORAGE_ADDON_OFFER_SET';
export const SITE_BACKUP_STORAGE_SET = 'SITE_BACKUP_STORAGE_SET';
export const SITE_BACKUPS_GET = 'SITE_BACKUPS_GET';
export const SITE_BACKUPS_GET_FAILED = 'SITE_BACKUPS_GET_FAILED';
export const SITE_BACKUPS_GET_SUCCESS = 'SITE_BACKUPS_GET_SUCCESS';
8 changes: 8 additions & 0 deletions projects/packages/backup/src/js/components/Admin/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,25 @@ import { useMemo } from 'react';
import useCapabilities from '../../hooks/useCapabilities';
import useConnection from '../../hooks/useConnection';
import { STORE_ID } from '../../store';
import { useShowBackUpNow } from '../back-up-now/hooks';
import { BackupNowButton } from '../back-up-now/index';
import { useIsFullyConnected } from './hooks';

const Header = () => {
const showActivateLicenseLink = useShowActivateLicenseLink();
const showBackUpNowButton = useShowBackUpNow();

return (
<div className="jetpack-admin-page__header">
<span className="jetpack-admin-page__logo">
<JetpackVaultPressBackupLogo />
</span>
{ showActivateLicenseLink && <ActivateLicenseLink /> }
{ showBackUpNowButton && (
<BackupNowButton variant="primary" tracksEventName="jetpack_backup_plugin_backup_now">
{ __( 'Back up now', 'jetpack-backup-pkg' ) }
</BackupNowButton>
) }
</div>
);
};
Expand Down
24 changes: 2 additions & 22 deletions projects/packages/backup/src/js/components/Admin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ExternalLink } from '@wordpress/components';
import { createInterpolateElement, useState, useEffect, useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import useAnalytics from '../../hooks/useAnalytics';
import useBackupsState from '../../hooks/useBackupsState';
import useCapabilities from '../../hooks/useCapabilities';
import useConnection from '../../hooks/useConnection';
import { Backups, Loading as BackupsLoadingPlaceholder } from '../Backups';
Expand Down Expand Up @@ -180,7 +181,7 @@ const BackupSegments = ( { hasBackupPlan, connectionLoaded } ) => {

const ReviewMessage = connectionLoaded => {
const [ restores ] = useRestores( connectionLoaded );
const [ backups ] = useBackups( connectionLoaded );
const { backups } = useBackupsState();
const { tracks } = useAnalytics();
let requestReason = '';
let reviewText = '';
Expand Down Expand Up @@ -290,27 +291,6 @@ const useRestores = connectionLoaded => {
return [ restores, setRestores ];
};

const useBackups = connectionLoaded => {
const [ backups, setBackups ] = useState( [] );

useEffect( () => {
if ( ! connectionLoaded ) {
setBackups( [] );
return;
}
apiFetch( { path: '/jetpack/v4/backups' } ).then(
res => {
setBackups( res );
},
() => {
setBackups( [] );
}
);
}, [ setBackups, connectionLoaded ] );

return [ backups, setBackups ];
};

const useDismissedReviewRequest = ( connectionLoaded, requestReason, tracksDismissReview ) => {
const [ dismissedReview, setDismissedReview ] = useState( true );

Expand Down
Loading

0 comments on commit 93d73d2

Please sign in to comment.