Skip to content

Commit

Permalink
BackupScheduleSetting: enable fetching and updating schedule time (#9…
Browse files Browse the repository at this point in the history
…5449)

* Add useScheduledTimeQuery hook

* Add useScheduledTimeMutation

* Add backup schedule setting in development environment

* Query and update schedule time

* Use isFetching instead of isLoading for query

* Add details on who updated the schedule time

* Import TranslateResult

* Update messages

* Wrap settings page in SiteOffsetProvider context

* Add local time support

* Adjust scheduledBy type to be a `string | null`

* Add backup schedule setting to cloud horizon env

* Refactor convertHourToRange

Refactor convertHourToRange to consider UTC as a 24-hour format. Also, I’m shifting from building the time range manually, to use moment().
  • Loading branch information
Initsogar authored Oct 18, 2024
1 parent b36be24 commit 5d25cc3
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 18 deletions.
123 changes: 105 additions & 18 deletions client/components/jetpack/backup-schedule-setting/index.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,126 @@
import { Card } from '@automattic/components';
import { useQueryClient } from '@tanstack/react-query';
import { SelectControl } from '@wordpress/components';
import { useTranslate } from 'i18n-calypso';
import { TranslateResult, useTranslate } from 'i18n-calypso';
import { useLocalizedMoment } from 'calypso/components/localized-moment';
import useScheduledTimeMutation from 'calypso/data/jetpack-backup/use-scheduled-time-mutation';
import useScheduledTimeQuery from 'calypso/data/jetpack-backup/use-scheduled-time-query';
import { applySiteOffset } from 'calypso/lib/site/timezone';
import { useDispatch, useSelector } from 'calypso/state';
import { errorNotice, successNotice } from 'calypso/state/notices/actions';
import getSiteGmtOffset from 'calypso/state/selectors/get-site-gmt-offset';
import getSiteTimezoneValue from 'calypso/state/selectors/get-site-timezone-value';
import { getSelectedSiteId } from 'calypso/state/ui/selectors';
import type { FunctionComponent } from 'react';
import './style.scss';

// Helper function to generate all time slots
const generateTimeSlots = (): { label: string; value: string }[] => {
const options = [];
for ( let hour = 0; hour < 24; hour++ ) {
const startTime = hour.toString().padStart( 2, '0' ) + ':00';
const endTime = hour.toString().padStart( 2, '0' ) + ':59';
options.push( {
label: `${ startTime } - ${ endTime }`,
value: hour.toString(),
} );
}
return options;
};

const BackupScheduleSetting: FunctionComponent = () => {
const dispatch = useDispatch();
const translate = useTranslate();
const options = generateTimeSlots();
const queryClient = useQueryClient();
const moment = useLocalizedMoment();
const siteId = useSelector( getSelectedSiteId ) as number;
const timezone = useSelector( ( state ) => getSiteTimezoneValue( state, siteId ) );
const gmtOffset = useSelector( ( state ) => getSiteGmtOffset( state, siteId ) );

const convertHourToRange = ( hour: number, isUtc: boolean = false ): string => {
const time = isUtc
? moment.utc().startOf( 'day' ).hour( hour )
: moment().startOf( 'day' ).hour( hour );

const formatString = isUtc ? 'HH:mm' : 'LT'; // 24-hour format for UTC, 12-hour for local

const startTime = time.format( formatString );
const endTime = time.add( 59, 'minutes' ).format( formatString );

return `${ startTime } - ${ endTime }`;
};

const generateTimeSlots = (): { label: string; value: string }[] => {
const options = [];
for ( let hour = 0; hour < 24; hour++ ) {
const utcTime = moment.utc().startOf( 'day' ).hour( hour );
const localTime =
timezone && gmtOffset
? applySiteOffset( utcTime, { timezone, gmtOffset } )
: utcTime.local();
const localHour = localTime.hour();
const timeRange = convertHourToRange( localHour );

options.push( {
label: timeRange,
value: hour.toString(),
localHour, // for sorting
} );
}

// Sort options by local hour before returning
options.sort( ( a, b ) => a.localHour - b.localHour );

// Remove the localHour from the final result as it's not needed anymore
return options.map( ( { label, value } ) => ( { label, value } ) );
};

const timeSlotOptions = generateTimeSlots();
const { isFetching: isScheduledTimeQueryFetching, data } = useScheduledTimeQuery( siteId );
const { isPending: isScheduledTimeMutationLoading, mutate: scheduledTimeMutate } =
useScheduledTimeMutation( {
onSuccess: () => {
queryClient.invalidateQueries( { queryKey: [ 'jetpack-backup-scheduled-time', siteId ] } );
dispatch(
successNotice( translate( 'Daily backup time successfully changed.' ), {
duration: 5000,
isPersistent: true,
} )
);
},
onError: () => {
dispatch(
errorNotice( translate( 'Update daily backup time failed. Please, try again.' ), {
duration: 5000,
isPersistent: true,
} )
);
},
} );

const isLoading = isScheduledTimeQueryFetching || isScheduledTimeMutationLoading;

const updateScheduledTime = ( selectedTime: string ) => {
scheduledTimeMutate( { scheduledHour: Number( selectedTime ) } );
};

const getScheduleInfoMessage = (): TranslateResult => {
const hour = data?.scheduledHour || 0;
const range = convertHourToRange( hour, true );

if ( ! data || ! data.scheduledBy ) {
return `${ translate( 'Default time' ) }. UTC: ${ range }`;
}
return `${ translate( 'Time set by %(scheduledBy)s', {
args: { scheduledBy: data.scheduledBy },
} ) }. UTC: ${ range }`;
};

return (
<div id="backup-schedule" className="backup-schedule-setting">
<Card compact className="setting-title">
<h3>{ translate( 'Backup schedule' ) }</h3>
<h3>{ translate( 'Daily backup time schedule' ) }</h3>
</Card>
<Card className="setting-content">
<p>
{ translate(
'Pick a timeframe for your backup to run. Some site owners prefer scheduling backups at specific times for better control.'
) }
</p>
<SelectControl options={ options } help={ translate( 'Default time' ) } />
<SelectControl
disabled={ isLoading }
options={ timeSlotOptions }
value={ data?.scheduledHour?.toString() || '' }
help={ getScheduleInfoMessage() }
onChange={ updateScheduledTime }
__nextHasNoMarginBottom
/>
</Card>
</div>
);
Expand Down
34 changes: 34 additions & 0 deletions client/data/jetpack-backup/use-scheduled-time-mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useMutation, UseMutationResult, UseMutationOptions } from '@tanstack/react-query';
import wpcom from 'calypso/lib/wp';
import { useSelector } from 'calypso/state';
import { getSelectedSiteId } from 'calypso/state/ui/selectors';

export interface UpdateSchedulePayload {
scheduledHour: number; // The new scheduled hour (0-23)
}

export interface UpdateScheduleResponse {
ok: boolean;
error: string;
}

export default function useScheduledTimeMutation<
TData = UpdateScheduleResponse,
TError = Error,
TContext = unknown,
>(
options: UseMutationOptions< TData, TError, UpdateSchedulePayload, TContext > = {}
): UseMutationResult< TData, TError, UpdateSchedulePayload, TContext > {
const siteId = useSelector( getSelectedSiteId ) as number;

return useMutation< TData, TError, UpdateSchedulePayload, TContext >( {
...options,
mutationFn: ( { scheduledHour }: UpdateSchedulePayload ): Promise< TData > => {
return wpcom.req.post( {
path: `/sites/${ siteId }/rewind/scheduled`,
apiNamespace: 'wpcom/v2',
body: { schedule_hour: scheduledHour },
} );
},
} );
}
34 changes: 34 additions & 0 deletions client/data/jetpack-backup/use-scheduled-time-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useQuery, UseQueryResult } from '@tanstack/react-query';
import wpcom from 'calypso/lib/wp';

export interface ScheduledTimeApi {
ok: boolean;
scheduled_hour: number;
scheduled_by: string | null;
}

export interface ScheduledTime {
scheduledHour: number;
scheduledBy: string | null;
}

const useScheduledTimeQuery = ( blogId: number ): UseQueryResult< ScheduledTime, Error > => {
const queryKey = [ 'jetpack-backup-scheduled-time', blogId ];

return useQuery< ScheduledTimeApi, Error, ScheduledTime >( {
queryKey,
queryFn: async () =>
wpcom.req.get( {
path: `/sites/${ blogId }/rewind/scheduled`,
apiNamespace: 'wpcom/v2',
} ),
refetchIntervalInBackground: false,
refetchOnWindowFocus: false,
select: ( data ) => ( {
scheduledHour: data.scheduled_hour,
scheduledBy: data.scheduled_by,
} ),
} );
};

export default useScheduledTimeQuery;
2 changes: 2 additions & 0 deletions client/jetpack-cloud/sections/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from 'calypso/jetpack-cloud/sections/settings/controller';
import isJetpackCloud from 'calypso/lib/jetpack/is-jetpack-cloud';
import { confirmDisconnectPath, disconnectPath, settingsPath } from 'calypso/lib/jetpack/paths';
import wrapInSiteOffsetProvider from 'calypso/lib/wrap-in-site-offset';
import { navigation, siteSelection, sites } from 'calypso/my-sites/controller';

export default function () {
Expand All @@ -20,6 +21,7 @@ export default function () {
siteSelection,
navigation,
isEnabled( 'jetpack/server-credentials-advanced-flow' ) ? advancedCredentials : settings,
wrapInSiteOffsetProvider,
showNotAuthorizedForNonAdmins,
makeLayout,
clientRender
Expand Down
1 change: 1 addition & 0 deletions config/jetpack-cloud-development.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"jetpack/backup-messaging-i3": true,
"jetpack/backup-restore-preflight-checks": true,
"jetpack/backup-retention-settings": true,
"jetpack/backup-schedule-setting": true,
"jetpack/card-addition-improvements": true,
"jetpack/golden-token": true,
"jetpack/plugin-management": true,
Expand Down
1 change: 1 addition & 0 deletions config/jetpack-cloud-horizon.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"jetpack/backup-messaging-i3": true,
"jetpack/backup-restore-preflight-checks": true,
"jetpack/backup-retention-settings": true,
"jetpack/backup-schedule-setting": true,
"jetpack/card-addition-improvements": true,
"jetpack/golden-token": false,
"jetpack/plugin-management": true,
Expand Down

0 comments on commit 5d25cc3

Please sign in to comment.