Skip to content

Commit 8a0a1c0

Browse files
yashwinclaude
andauthored
A4A: Add backups to the agency site detail view (#112205)
* A4A: Add backups to the agency site detail view Adds the full Backups experience (list, detail with file browser, Download, and Restore) to the A4A agency site detail, reusing the multi-site dashboard's backup components. Shared, route-agnostic pieces used by both dotcom and A4A: - BackupsBrowser (list + detail grid + selection + activity log) - BackupRestoreFlow / BackupDownloadFlow (step machines) - BackupDetails (navigation injected via callbacks) - BackupCardContent (parameterized by siteId + backupUrl) The dotcom pages become thin shells over those shared components; A4A adds its own thin shells + routes (/sites/$siteSlug/backups[/$rewindId[/restore|/download]]), gated on the agency site's has_backup flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * A4A: Add backup-now action and route prefetching to agency backups * Add the "Back up now" button (with useBackupState and the BackupNotices progress banner) to the agency backups list page. * Forward searchParams to BackupsBrowser so the DataView hydrates page/search state from the URL, matching dotcom. * Add a loader on the backups route to prefetch site settings and backups in parallel, removing the site → settings suspense waterfall. * Fix stale $siteId route comments to $siteSlug. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * A4A: Consume shared backup page shells Replace the duplicated agency backup shells with the shared components extracted in the base branch, addressing the review feedback: * backups-list-page renders <BackupsPage> with agency-route navigation built via useBackupsNavigation * backups uses <BackupFileBrowserProvider> * backup-download renders <BackupDownloadPage> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent eabb7b4 commit 8a0a1c0

8 files changed

Lines changed: 292 additions & 3 deletions

File tree

client/dashboard/agency/sites/site-sidebar/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { agencySiteQuery } from '@automattic/api-queries';
22
import { useQuery } from '@tanstack/react-query';
33
import { __experimentalVStack as VStack } from '@wordpress/components';
44
import { __ } from '@wordpress/i18n';
5-
import { category } from '@wordpress/icons';
5+
import { category, backup } from '@wordpress/icons';
66
import { agencySiteRoute } from '../../../app/router/agency';
77
import { SidebarBackButton, SidebarMenu, SidebarMenuItem } from '../../../components/sidebar';
88
import AgencySiteSwitcherItem from './site-switcher-item';
@@ -27,6 +27,11 @@ export default function AgencySiteSidebar() {
2727
>
2828
{ __( 'Overview' ) }
2929
</SidebarMenuItem>
30+
{ site.has_backup && (
31+
<SidebarMenuItem icon={ backup } to={ `/sites/${ siteSlug }/backups` }>
32+
{ __( 'Backups' ) }
33+
</SidebarMenuItem>
34+
) }
3035
</SidebarMenu>
3136
</VStack>
3237
) }
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { __ } from '@wordpress/i18n';
2+
import { backup } from '@wordpress/icons';
3+
import OverviewCard from '../../../components/overview-card';
4+
import { BackupCardContent } from '../../../sites/overview-backup-card';
5+
import type { AgencySite } from '@automattic/api-core';
6+
7+
export default function BackupCard( { site }: { site: AgencySite } ) {
8+
if ( ! site.has_backup ) {
9+
return (
10+
<OverviewCard
11+
icon={ backup }
12+
title={ __( 'Last backup' ) }
13+
heading={ __( 'Not active' ) }
14+
description={ __( 'Backups aren’t enabled for this site.' ) }
15+
/>
16+
);
17+
}
18+
19+
return <BackupCardContent siteId={ site.blog_id } backupUrl={ `/sites/${ site.url }/backups` } />;
20+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { siteBySlugQuery } from '@automattic/api-queries';
2+
import { useSuspenseQuery } from '@tanstack/react-query';
3+
import { useRouter } from '@tanstack/react-router';
4+
import { useCallback } from 'react';
5+
import { agencySiteBackupDownloadRoute } from '../../../app/router/agency';
6+
import { BackupDownloadPage } from '../../../sites/backup-download/download-page';
7+
8+
export default function AgencySiteBackupDownload() {
9+
const { siteSlug, rewindId } = agencySiteBackupDownloadRoute.useParams();
10+
const { downloadId } = agencySiteBackupDownloadRoute.useSearch();
11+
const { data: site } = useSuspenseQuery( siteBySlugQuery( siteSlug ) );
12+
const router = useRouter();
13+
14+
const onConsumeDownloadId = useCallback( () => {
15+
router.navigate( {
16+
to: agencySiteBackupDownloadRoute.fullPath,
17+
params: { siteSlug, rewindId },
18+
search: {},
19+
replace: true,
20+
} );
21+
}, [ router, siteSlug, rewindId ] );
22+
23+
return (
24+
<BackupDownloadPage
25+
site={ site }
26+
rewindId={ rewindId }
27+
downloadId={ downloadId }
28+
onConsumeDownloadId={ onConsumeDownloadId }
29+
/>
30+
);
31+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { siteBySlugQuery } from '@automattic/api-queries';
2+
import { useSuspenseQuery } from '@tanstack/react-query';
3+
import { __ } from '@wordpress/i18n';
4+
import Breadcrumbs from '../../../app/breadcrumbs';
5+
import { agencySiteBackupRestoreRoute } from '../../../app/router/agency';
6+
import { PageHeader } from '../../../components/page-header';
7+
import PageLayout from '../../../components/page-layout';
8+
import { BackupRestoreFlow } from '../../../sites/backup-restore/flow';
9+
10+
export default function AgencySiteBackupRestore() {
11+
const { siteSlug, rewindId } = agencySiteBackupRestoreRoute.useParams();
12+
const { data: site } = useSuspenseQuery( siteBySlugQuery( siteSlug ) );
13+
14+
return (
15+
<PageLayout
16+
size="small"
17+
header={
18+
<PageHeader prefix={ <Breadcrumbs length={ 2 } /> } title={ __( 'Site restore' ) } />
19+
}
20+
>
21+
<BackupRestoreFlow site={ site } rewindId={ rewindId } />
22+
</PageLayout>
23+
);
24+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { siteBySlugQuery } from '@automattic/api-queries';
2+
import { useSuspenseQuery } from '@tanstack/react-query';
3+
import { useParams, useRouter } from '@tanstack/react-router';
4+
import { useMemo } from 'react';
5+
import {
6+
agencySiteRoute,
7+
agencySiteBackupsRoute,
8+
agencySiteBackupsIndexRoute,
9+
agencySiteBackupDetailRoute,
10+
agencySiteBackupRestoreRoute,
11+
agencySiteBackupDownloadRoute,
12+
} from '../../../app/router/agency';
13+
import { BackupsPage, type BackupsNavigation } from '../../../sites/backups/backups-page';
14+
15+
export default function AgencySiteBackupsListPage() {
16+
const { siteSlug } = agencySiteRoute.useParams();
17+
const router = useRouter();
18+
const { data: site } = useSuspenseQuery( siteBySlugQuery( siteSlug ) );
19+
const searchParams = agencySiteBackupsRoute.useSearch();
20+
21+
const routeParams = useParams( { strict: false, shouldThrow: false } ) as
22+
| { rewindId?: string }
23+
| undefined;
24+
const rewindId = routeParams?.rewindId;
25+
26+
const navigation = useMemo< BackupsNavigation >(
27+
() => ( {
28+
selectBackup: ( id ) =>
29+
id
30+
? router.navigate( {
31+
to: agencySiteBackupDetailRoute.fullPath,
32+
params: { siteSlug, rewindId: id },
33+
search: ( query: Record< string, string > ) => query,
34+
} )
35+
: router.navigate( {
36+
to: agencySiteBackupsIndexRoute.fullPath,
37+
params: { siteSlug },
38+
search: ( query: Record< string, string > ) => query,
39+
} ),
40+
requestRestore: ( id ) =>
41+
router.navigate( {
42+
to: agencySiteBackupRestoreRoute.fullPath,
43+
params: { siteSlug, rewindId: id },
44+
} ),
45+
requestDownload: ( id, downloadId ) =>
46+
downloadId
47+
? router.navigate( {
48+
to: agencySiteBackupDownloadRoute.fullPath,
49+
params: { siteSlug, rewindId: id },
50+
search: { downloadId },
51+
} )
52+
: router.navigate( {
53+
to: agencySiteBackupDownloadRoute.fullPath,
54+
params: { siteSlug, rewindId: id },
55+
} ),
56+
} ),
57+
[ router, siteSlug ]
58+
);
59+
60+
return (
61+
<BackupsPage
62+
site={ site }
63+
navigation={ navigation }
64+
rewindId={ rewindId }
65+
searchParams={ searchParams }
66+
/>
67+
);
68+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { agencySiteQuery } from '@automattic/api-queries';
2+
import { useSuspenseQuery } from '@tanstack/react-query';
3+
import { Outlet } from '@tanstack/react-router';
4+
import { __experimentalHStack as HStack } from '@wordpress/components';
5+
import { __ } from '@wordpress/i18n';
6+
import { Icon, backup as backupIcon } from '@wordpress/icons';
7+
import { agencySiteRoute } from '../../../app/router/agency';
8+
import { Card, CardBody } from '../../../components/card';
9+
import { PageHeader } from '../../../components/page-header';
10+
import PageLayout from '../../../components/page-layout';
11+
import { Text } from '../../../components/text';
12+
import { BackupFileBrowserProvider } from '../../../sites/backups/backup-file-browser-provider';
13+
14+
export default function AgencySiteBackups() {
15+
const { siteSlug } = agencySiteRoute.useParams();
16+
const { data: site } = useSuspenseQuery( agencySiteQuery( siteSlug ) );
17+
18+
if ( ! site?.has_backup ) {
19+
return (
20+
<PageLayout header={ <PageHeader title={ __( 'Backups' ) } /> }>
21+
<Card>
22+
<CardBody>
23+
<HStack justify="flex-start" spacing={ 2 } expanded={ false }>
24+
<Icon icon={ backupIcon } />
25+
<Text variant="muted">{ __( 'Backups aren’t enabled for this site.' ) }</Text>
26+
</HStack>
27+
</CardBody>
28+
</Card>
29+
</PageLayout>
30+
);
31+
}
32+
33+
return (
34+
<BackupFileBrowserProvider>
35+
<Outlet />
36+
</BackupFileBrowserProvider>
37+
);
38+
}

client/dashboard/agency/sites/site/overview.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import { agencySiteRoute } from '../../../app/router/agency';
22
import { PageHeader } from '../../../components/page-header';
33
import PageLayout from '../../../components/page-layout';
44
import { getDisplayUrl, getSiteName } from '../dataviews/site-data';
5+
import BackupCard from './backup-card';
56

67
export default function AgencySiteOverview() {
78
const site = agencySiteRoute.useLoaderData();
89

910
return (
1011
<PageLayout
1112
header={ <PageHeader title={ getSiteName( site ) } description={ getDisplayUrl( site ) } /> }
12-
/>
13+
>
14+
<BackupCard site={ site } />
15+
</PageLayout>
1316
);
1417
}

client/dashboard/app/router/agency.tsx

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {
66
mcpSettingsQuery,
77
queryClient,
88
rawUserPreferencesQuery,
9+
siteBackupsQuery,
10+
siteBySlugQuery,
11+
siteSettingsQuery,
912
} from '@automattic/api-queries';
1013
import { createRoute, createLazyRoute, notFound } from '@tanstack/react-router';
1114
import { __ } from '@wordpress/i18n';
@@ -266,6 +269,93 @@ const agencySiteOverviewRoute = createRoute( {
266269
)
267270
);
268271

272+
// `/sites/$siteSlug/backups` – layout that hosts the backups list/detail views
273+
export const agencySiteBackupsRoute = createRoute( {
274+
head: () => ( { meta: [ { title: __( 'Backups' ) } ] } ),
275+
getParentRoute: () => agencySiteRoute,
276+
path: 'backups',
277+
loader: async ( { params: { siteSlug } } ) => {
278+
const [ agencySite, site ] = await Promise.all( [
279+
queryClient.ensureQueryData( agencySiteQuery( siteSlug ) ),
280+
queryClient.ensureQueryData( siteBySlugQuery( siteSlug ) ),
281+
] );
282+
283+
if ( ! agencySite?.has_backup ) {
284+
return;
285+
}
286+
287+
await Promise.all( [
288+
queryClient.ensureQueryData( siteSettingsQuery( site.ID ) ),
289+
queryClient.ensureQueryData( siteBackupsQuery( site.ID ) ),
290+
] );
291+
},
292+
} ).lazy( () =>
293+
import( '../../agency/sites/site/backups' ).then( ( d ) =>
294+
createLazyRoute( 'agency-site-backups' )( {
295+
component: d.default,
296+
} )
297+
)
298+
);
299+
300+
export const agencySiteBackupsIndexRoute = createRoute( {
301+
getParentRoute: () => agencySiteBackupsRoute,
302+
path: '/',
303+
} ).lazy( () =>
304+
import( '../../agency/sites/site/backups-list-page' ).then( ( d ) =>
305+
createLazyRoute( 'agency-site-backups-index' )( {
306+
component: d.default,
307+
} )
308+
)
309+
);
310+
311+
// `/sites/$siteSlug/backups/$rewindId` – layout hosting the detail view + restore/download flows
312+
export const agencySiteBackupDetailRoute = createRoute( {
313+
head: () => ( { meta: [ { title: __( 'Backups' ) } ] } ),
314+
getParentRoute: () => agencySiteBackupsRoute,
315+
path: '$rewindId',
316+
} );
317+
318+
const agencySiteBackupDetailIndexRoute = createRoute( {
319+
getParentRoute: () => agencySiteBackupDetailRoute,
320+
path: '/',
321+
} ).lazy( () =>
322+
import( '../../agency/sites/site/backups-list-page' ).then( ( d ) =>
323+
createLazyRoute( 'agency-site-backup-detail' )( {
324+
component: d.default,
325+
} )
326+
)
327+
);
328+
329+
export const agencySiteBackupRestoreRoute = createRoute( {
330+
head: () => ( { meta: [ { title: __( 'Site restore' ) } ] } ),
331+
getParentRoute: () => agencySiteBackupDetailRoute,
332+
path: 'restore',
333+
} ).lazy( () =>
334+
import( '../../agency/sites/site/backup-restore' ).then( ( d ) =>
335+
createLazyRoute( 'agency-site-backup-restore' )( {
336+
component: d.default,
337+
} )
338+
)
339+
);
340+
341+
export const agencySiteBackupDownloadRoute = createRoute( {
342+
head: () => ( { meta: [ { title: __( 'Download backup' ) } ] } ),
343+
getParentRoute: () => agencySiteBackupDetailRoute,
344+
path: 'download',
345+
validateSearch: ( search ) => {
346+
const downloadId = Number( search.downloadId );
347+
return {
348+
downloadId: downloadId > 0 ? downloadId : undefined,
349+
};
350+
},
351+
} ).lazy( () =>
352+
import( '../../agency/sites/site/backup-download' ).then( ( d ) =>
353+
createLazyRoute( 'agency-site-backup-download' )( {
354+
component: d.default,
355+
} )
356+
)
357+
);
358+
269359
export const createAgencyRoutes = () => [
270360
agencyRoute.addChildren( [
271361
agencyOverviewRoute,
@@ -279,6 +369,16 @@ export const createAgencyRoutes = () => [
279369
earnWooPaymentsRoute,
280370
earnMigrationsRoute,
281371
earnPayoutSettingsRoute,
282-
agencySiteRoute.addChildren( [ agencySiteOverviewRoute ] ),
372+
agencySiteRoute.addChildren( [
373+
agencySiteOverviewRoute,
374+
agencySiteBackupsRoute.addChildren( [
375+
agencySiteBackupsIndexRoute,
376+
agencySiteBackupDetailRoute.addChildren( [
377+
agencySiteBackupDetailIndexRoute,
378+
agencySiteBackupRestoreRoute,
379+
agencySiteBackupDownloadRoute,
380+
] ),
381+
] ),
382+
] ),
283383
] ),
284384
];

0 commit comments

Comments
 (0)