Skip to content

Commit f435b0f

Browse files
authored
Dashboard v2: Implement Settings -> Caching (#103826)
1 parent d6eeabb commit f435b0f

File tree

6 files changed

+380
-0
lines changed

6 files changed

+380
-0
lines changed

client/dashboard/app/queries.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import {
2626
fetchPrimaryDataCenter,
2727
fetchStaticFile404,
2828
updateStaticFile404,
29+
clearObjectCache,
30+
clearEdgeCache,
31+
fetchEdgeCacheStatus,
32+
updateEdgeCacheStatus,
2933
fetchEdgeCacheDefensiveMode,
3034
updateEdgeCacheDefensiveMode,
3135
fetchPurchases,
@@ -299,6 +303,36 @@ export function agencyBlogQuery( siteId: string ) {
299303
};
300304
}
301305

306+
export function siteObjectCacheClearMutation( siteSlug: string ) {
307+
return {
308+
mutationFn: ( reason: string ) => clearObjectCache( siteSlug, reason ),
309+
};
310+
}
311+
312+
export function siteEdgeCacheClearMutation( siteSlug: string ) {
313+
return {
314+
mutationFn: () => clearEdgeCache( siteSlug ),
315+
};
316+
}
317+
318+
export function siteEdgeCacheStatusQuery( siteSlug: string ) {
319+
return {
320+
queryKey: [ 'site', siteSlug, 'edge-cache-status' ],
321+
queryFn: () => {
322+
return fetchEdgeCacheStatus( siteSlug );
323+
},
324+
};
325+
}
326+
327+
export function siteEdgeCacheStatusMutation( siteSlug: string ) {
328+
return {
329+
mutationFn: ( active: boolean ) => updateEdgeCacheStatus( siteSlug, active ),
330+
onSuccess: ( active: boolean ) => {
331+
queryClient.setQueryData( [ 'site', siteSlug, 'edge-cache-status' ], active );
332+
},
333+
};
334+
}
335+
302336
export function siteDefensiveModeQuery( siteSlug: string ) {
303337
return {
304338
queryKey: [ 'site', siteSlug, 'defensive-mode' ],

client/dashboard/app/router.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createLazyRoute,
77
} from '@tanstack/react-router';
88
import { fetchTwoStep } from '../data';
9+
import { canUpdateCaching } from '../sites/settings-caching';
910
import {
1011
canUpdatePHPVersion,
1112
canUpdateDefensiveMode,
@@ -28,6 +29,7 @@ import {
2829
siteWordPressVersionQuery,
2930
sitePHPVersionQuery,
3031
sitePrimaryDataCenterQuery,
32+
siteEdgeCacheStatusQuery,
3133
siteDefensiveModeQuery,
3234
agencyBlogQuery,
3335
} from './queries';
@@ -261,6 +263,23 @@ const siteSettingsStaticFile404Route = createRoute( {
261263
)
262264
);
263265

266+
const siteSettingsCachingRoute = createRoute( {
267+
getParentRoute: () => siteRoute,
268+
path: 'settings/caching',
269+
loader: async ( { params: { siteSlug } } ) => {
270+
const site = await queryClient.ensureQueryData( siteQuery( siteSlug ) );
271+
if ( canUpdateCaching( site ) ) {
272+
await queryClient.ensureQueryData( siteEdgeCacheStatusQuery( siteSlug ) );
273+
}
274+
},
275+
} ).lazy( () =>
276+
import( '../sites/settings-caching' ).then( ( d ) =>
277+
createLazyRoute( 'site-settings-caching' )( {
278+
component: () => <d.default siteSlug={ siteRoute.useParams().siteSlug } />,
279+
} )
280+
)
281+
);
282+
264283
const siteSettingsDefensiveModeRoute = createRoute( {
265284
getParentRoute: () => siteRoute,
266285
path: 'settings/defensive-mode',
@@ -460,6 +479,7 @@ const createRouteTree = ( config: AppConfig ) => {
460479
siteSettingsAgencyRoute,
461480
siteSettingsPrimaryDataCenterRoute,
462481
siteSettingsStaticFile404Route,
482+
siteSettingsCachingRoute,
463483
siteSettingsDefensiveModeRoute,
464484
siteSettingsTransferSiteRoute,
465485
] )

client/dashboard/data/index.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,43 @@ export const updateStaticFile404 = async (
522522
);
523523
};
524524

525+
export const clearObjectCache = async ( siteIdOrSlug: string, reason: string ): Promise< void > => {
526+
return wpcom.req.post(
527+
{
528+
path: `/sites/${ siteIdOrSlug }/hosting/clear-cache`,
529+
apiNamespace: 'wpcom/v2',
530+
},
531+
{ reason }
532+
);
533+
};
534+
535+
export const fetchEdgeCacheStatus = async ( siteIdOrSlug: string ): Promise< boolean > => {
536+
return wpcom.req.get( {
537+
path: `/sites/${ siteIdOrSlug }/hosting/edge-cache/active`,
538+
apiNamespace: 'wpcom/v2',
539+
} );
540+
};
541+
542+
export const updateEdgeCacheStatus = async (
543+
siteIdOrSlug: string,
544+
active: boolean
545+
): Promise< boolean > => {
546+
return wpcom.req.post(
547+
{
548+
path: `/sites/${ siteIdOrSlug }/hosting/edge-cache/active`,
549+
apiNamespace: 'wpcom/v2',
550+
},
551+
{ active }
552+
);
553+
};
554+
555+
export const clearEdgeCache = async ( siteIdOrSlug: string ): Promise< void > => {
556+
return wpcom.req.post( {
557+
path: `/sites/${ siteIdOrSlug }/hosting/edge-cache/purge`,
558+
apiNamespace: 'wpcom/v2',
559+
} );
560+
};
561+
525562
export const fetchEdgeCacheDefensiveMode = async (
526563
siteIdOrSlug: string
527564
): Promise< DefensiveModeSettings > => {
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { DataForm } from '@automattic/dataviews';
2+
import { useQuery, useMutation } from '@tanstack/react-query';
3+
import {
4+
Card,
5+
CardBody,
6+
__experimentalHStack as HStack,
7+
__experimentalVStack as VStack,
8+
Button,
9+
} from '@wordpress/components';
10+
import { useDispatch } from '@wordpress/data';
11+
import { createInterpolateElement } from '@wordpress/element';
12+
import { __ } from '@wordpress/i18n';
13+
import { store as noticesStore } from '@wordpress/notices';
14+
import { useEffect, useState } from 'react';
15+
import {
16+
siteQuery,
17+
siteEdgeCacheStatusQuery,
18+
siteEdgeCacheStatusMutation,
19+
siteEdgeCacheClearMutation,
20+
siteObjectCacheClearMutation,
21+
} from '../../app/queries';
22+
import { ActionList } from '../../components/action-list';
23+
import PageLayout from '../../components/page-layout';
24+
import SettingsCallout from '../settings-callout';
25+
import SettingsPageHeader from '../settings-page-header';
26+
import type { Site } from '../../data/types';
27+
import type { Field } from '@automattic/dataviews';
28+
29+
type CachingFormData = {
30+
active: boolean;
31+
};
32+
33+
const fields: Field< CachingFormData >[] = [
34+
{
35+
id: 'active',
36+
label: __( 'Enable global edge caching for faster content delivery' ),
37+
Edit: 'checkbox',
38+
},
39+
];
40+
41+
const form = {
42+
type: 'regular' as const,
43+
fields: [ 'active' ],
44+
};
45+
46+
export function canUpdateCaching( site: Site ) {
47+
return site.is_wpcom_atomic;
48+
}
49+
50+
export default function CachingSettings( { siteSlug }: { siteSlug: string } ) {
51+
const { data: site } = useQuery( siteQuery( siteSlug ) );
52+
const canUpdate = site && canUpdateCaching( site );
53+
54+
const { data: isEdgeCacheActive } = useQuery( {
55+
...siteEdgeCacheStatusQuery( siteSlug ),
56+
enabled: canUpdate,
57+
} );
58+
const edgeCacheStatusMutation = useMutation( siteEdgeCacheStatusMutation( siteSlug ) );
59+
const edgeCacheClearMutation = useMutation( siteEdgeCacheClearMutation( siteSlug ) );
60+
const objectCacheClearMutation = useMutation( siteObjectCacheClearMutation( siteSlug ) );
61+
62+
const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
63+
64+
const [ formData, setFormData ] = useState< CachingFormData >( {
65+
active: isEdgeCacheActive ?? false,
66+
} );
67+
68+
const isDirty = isEdgeCacheActive !== formData.active;
69+
const { isPending } = edgeCacheStatusMutation;
70+
71+
const handleUpdateEdgeCacheStatus = ( e: React.FormEvent ) => {
72+
e.preventDefault();
73+
edgeCacheStatusMutation.mutate( formData.active, {
74+
onSuccess: () => {
75+
createSuccessNotice( __( 'Settings saved.' ), { type: 'snackbar' } );
76+
},
77+
onError: () => {
78+
createErrorNotice( __( 'Failed to save settings.' ), { type: 'snackbar' } );
79+
},
80+
} );
81+
};
82+
83+
const handleClearEdgeCache = () => {
84+
edgeCacheClearMutation.mutate( undefined, {
85+
onSuccess: () => {
86+
createSuccessNotice( __( 'Global edge cache cleared.' ), { type: 'snackbar' } );
87+
},
88+
onError: () => {
89+
createErrorNotice( __( 'Failed to clear edge cache.' ), { type: 'snackbar' } );
90+
},
91+
} );
92+
};
93+
94+
const handleClearObjectCache = () => {
95+
objectCacheClearMutation.mutate( 'Manually clearing again.', {
96+
onSuccess: () => {
97+
createSuccessNotice( __( 'Object cache cleared.' ), { type: 'snackbar' } );
98+
},
99+
onError: () => {
100+
createErrorNotice( __( 'Failed to clear object cache.' ), { type: 'snackbar' } );
101+
},
102+
} );
103+
};
104+
105+
const [ isClearingAllCaches, setIsClearingAllCaches ] = useState( false );
106+
107+
useEffect( () => {
108+
if ( ! edgeCacheClearMutation.isPending && ! objectCacheClearMutation.isPending ) {
109+
setIsClearingAllCaches( false );
110+
}
111+
}, [ edgeCacheClearMutation.isPending, objectCacheClearMutation.isPending ] );
112+
113+
const handleClearAllCaches = () => {
114+
if ( isEdgeCacheActive ) {
115+
handleClearEdgeCache();
116+
}
117+
handleClearObjectCache();
118+
119+
setIsClearingAllCaches( true );
120+
};
121+
122+
const renderCallout = () => {
123+
return <SettingsCallout siteSlug={ siteSlug } />;
124+
};
125+
126+
const renderForm = () => {
127+
return (
128+
<>
129+
<Card>
130+
<CardBody>
131+
<form onSubmit={ handleUpdateEdgeCacheStatus }>
132+
<VStack spacing={ 4 } style={ { padding: '8px 0' } }>
133+
<DataForm< CachingFormData >
134+
data={ formData }
135+
fields={ fields }
136+
form={ form }
137+
onChange={ ( edits: Partial< CachingFormData > ) => {
138+
setFormData( ( data ) => ( { ...data, ...edits } ) );
139+
} }
140+
/>
141+
<HStack justify="flex-start">
142+
<Button
143+
variant="primary"
144+
type="submit"
145+
isBusy={ isPending }
146+
disabled={ isPending || ! isDirty }
147+
>
148+
{ __( 'Save' ) }
149+
</Button>
150+
</HStack>
151+
</VStack>
152+
</form>
153+
</CardBody>
154+
</Card>
155+
156+
<VStack spacing={ 4 }>
157+
<ActionList
158+
title={ __( 'Clear caches' ) }
159+
description={ __(
160+
'Clearing the cache may temporarily make your site less responsive.'
161+
) }
162+
>
163+
<ActionList.ActionItem
164+
title={ __( 'Global edge cache' ) }
165+
description={ __( 'Edge caching enables faster content delivery.' ) }
166+
actions={
167+
<Button
168+
variant="secondary"
169+
size="compact"
170+
onClick={ handleClearEdgeCache }
171+
isBusy={ edgeCacheClearMutation.isPending && ! isClearingAllCaches }
172+
disabled={
173+
! isEdgeCacheActive || edgeCacheClearMutation.isPending || isClearingAllCaches
174+
}
175+
>
176+
{ __( 'Clear' ) }
177+
</Button>
178+
}
179+
/>
180+
<ActionList.ActionItem
181+
title={ __( 'Object cache' ) }
182+
description={ __( 'Data is cached using Memcached to reduce database lookups.' ) }
183+
actions={
184+
<Button
185+
variant="secondary"
186+
size="compact"
187+
onClick={ handleClearObjectCache }
188+
isBusy={ objectCacheClearMutation.isPending && ! isClearingAllCaches }
189+
disabled={ objectCacheClearMutation.isPending || isClearingAllCaches }
190+
>
191+
{ __( 'Clear' ) }
192+
</Button>
193+
}
194+
/>
195+
</ActionList>
196+
<ActionList>
197+
<ActionList.ActionItem
198+
title={ __( 'Clear all caches' ) }
199+
actions={
200+
<Button
201+
variant="secondary"
202+
size="compact"
203+
onClick={ handleClearAllCaches }
204+
isBusy={ isClearingAllCaches }
205+
disabled={ isClearingAllCaches }
206+
>
207+
{ __( 'Clear all' ) }
208+
</Button>
209+
}
210+
/>
211+
</ActionList>
212+
</VStack>
213+
</>
214+
);
215+
};
216+
217+
const description = canUpdate
218+
? createInterpolateElement(
219+
__( 'Manage your site’s server-side caching. <learnMoreLink />.' ),
220+
{
221+
learnMoreLink: <a href="#learn-more">{ __( 'Learn more' ) }</a>,
222+
}
223+
)
224+
: '';
225+
226+
return (
227+
<PageLayout
228+
size="small"
229+
header={ <SettingsPageHeader title={ __( 'Caching' ) } description={ description } /> }
230+
>
231+
{ ! canUpdate ? renderCallout() : renderForm() }
232+
</PageLayout>
233+
);
234+
}

0 commit comments

Comments
 (0)