Skip to content

Dashboard v2: Implement Settings -> Caching #103826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions client/dashboard/app/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import {
fetchPrimaryDataCenter,
fetchStaticFile404,
updateStaticFile404,
clearObjectCache,
clearEdgeCache,
fetchEdgeCacheStatus,
updateEdgeCacheStatus,
fetchEdgeCacheDefensiveMode,
updateEdgeCacheDefensiveMode,
fetchPurchases,
Expand Down Expand Up @@ -299,6 +303,36 @@ export function agencyBlogQuery( siteId: string ) {
};
}

export function siteObjectCacheClearMutation( siteSlug: string ) {
return {
mutationFn: ( reason: string ) => clearObjectCache( siteSlug, reason ),
};
}

export function siteEdgeCacheClearMutation( siteSlug: string ) {
return {
mutationFn: () => clearEdgeCache( siteSlug ),
};
}

export function siteEdgeCacheStatusQuery( siteSlug: string ) {
return {
queryKey: [ 'site', siteSlug, 'edge-cache-status' ],
queryFn: () => {
return fetchEdgeCacheStatus( siteSlug );
},
};
}

export function siteEdgeCacheStatusMutation( siteSlug: string ) {
return {
mutationFn: ( active: boolean ) => updateEdgeCacheStatus( siteSlug, active ),
onSuccess: ( active: boolean ) => {
queryClient.setQueryData( [ 'site', siteSlug, 'edge-cache-status' ], active );
},
};
}

export function siteDefensiveModeQuery( siteSlug: string ) {
return {
queryKey: [ 'site', siteSlug, 'defensive-mode' ],
Expand Down
20 changes: 20 additions & 0 deletions client/dashboard/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createLazyRoute,
} from '@tanstack/react-router';
import { fetchTwoStep } from '../data';
import { canUpdateCaching } from '../sites/settings-caching';
import {
canUpdatePHPVersion,
canUpdateDefensiveMode,
Expand All @@ -28,6 +29,7 @@ import {
siteWordPressVersionQuery,
sitePHPVersionQuery,
sitePrimaryDataCenterQuery,
siteEdgeCacheStatusQuery,
siteDefensiveModeQuery,
agencyBlogQuery,
} from './queries';
Expand Down Expand Up @@ -261,6 +263,23 @@ const siteSettingsStaticFile404Route = createRoute( {
)
);

const siteSettingsCachingRoute = createRoute( {
getParentRoute: () => siteRoute,
path: 'settings/caching',
loader: async ( { params: { siteSlug } } ) => {
const site = await queryClient.ensureQueryData( siteQuery( siteSlug ) );
if ( canUpdateCaching( site ) ) {
await queryClient.ensureQueryData( siteEdgeCacheStatusQuery( siteSlug ) );
}
},
} ).lazy( () =>
import( '../sites/settings-caching' ).then( ( d ) =>
createLazyRoute( 'site-settings-caching' )( {
component: () => <d.default siteSlug={ siteRoute.useParams().siteSlug } />,
} )
)
);

const siteSettingsDefensiveModeRoute = createRoute( {
getParentRoute: () => siteRoute,
path: 'settings/defensive-mode',
Expand Down Expand Up @@ -460,6 +479,7 @@ const createRouteTree = ( config: AppConfig ) => {
siteSettingsAgencyRoute,
siteSettingsPrimaryDataCenterRoute,
siteSettingsStaticFile404Route,
siteSettingsCachingRoute,
siteSettingsDefensiveModeRoute,
siteSettingsTransferSiteRoute,
] )
Expand Down
37 changes: 37 additions & 0 deletions client/dashboard/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,43 @@ export const updateStaticFile404 = async (
);
};

export const clearObjectCache = async ( siteIdOrSlug: string, reason: string ): Promise< void > => {
return wpcom.req.post(
{
path: `/sites/${ siteIdOrSlug }/hosting/clear-cache`,
apiNamespace: 'wpcom/v2',
},
{ reason }
);
};

export const fetchEdgeCacheStatus = async ( siteIdOrSlug: string ): Promise< boolean > => {
return wpcom.req.get( {
path: `/sites/${ siteIdOrSlug }/hosting/edge-cache/active`,
apiNamespace: 'wpcom/v2',
} );
};

export const updateEdgeCacheStatus = async (
siteIdOrSlug: string,
active: boolean
): Promise< boolean > => {
return wpcom.req.post(
{
path: `/sites/${ siteIdOrSlug }/hosting/edge-cache/active`,
apiNamespace: 'wpcom/v2',
},
{ active }
);
};

export const clearEdgeCache = async ( siteIdOrSlug: string ): Promise< void > => {
return wpcom.req.post( {
path: `/sites/${ siteIdOrSlug }/hosting/edge-cache/purge`,
apiNamespace: 'wpcom/v2',
} );
};

export const fetchEdgeCacheDefensiveMode = async (
siteIdOrSlug: string
): Promise< DefensiveModeSettings > => {
Expand Down
234 changes: 234 additions & 0 deletions client/dashboard/sites/settings-caching/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { DataForm } from '@automattic/dataviews';
import { useQuery, useMutation } from '@tanstack/react-query';
import {
Card,
CardBody,
__experimentalHStack as HStack,
__experimentalVStack as VStack,
Button,
} from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { createInterpolateElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { useEffect, useState } from 'react';
import {
siteQuery,
siteEdgeCacheStatusQuery,
siteEdgeCacheStatusMutation,
siteEdgeCacheClearMutation,
siteObjectCacheClearMutation,
} from '../../app/queries';
import { ActionList } from '../../components/action-list';
import PageLayout from '../../components/page-layout';
import SettingsCallout from '../settings-callout';
import SettingsPageHeader from '../settings-page-header';
import type { Site } from '../../data/types';
import type { Field } from '@automattic/dataviews';

type CachingFormData = {
active: boolean;
};

const fields: Field< CachingFormData >[] = [
{
id: 'active',
label: __( 'Enable global edge caching for faster content delivery' ),
Edit: 'checkbox',
},
];

const form = {
type: 'regular' as const,
fields: [ 'active' ],
};

export function canUpdateCaching( site: Site ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return site.is_wpcom_atomic;
}

export default function CachingSettings( { siteSlug }: { siteSlug: string } ) {
const { data: site } = useQuery( siteQuery( siteSlug ) );
const canUpdate = site && canUpdateCaching( site );

const { data: isEdgeCacheActive } = useQuery( {
...siteEdgeCacheStatusQuery( siteSlug ),
enabled: canUpdate,
} );
const edgeCacheStatusMutation = useMutation( siteEdgeCacheStatusMutation( siteSlug ) );
const edgeCacheClearMutation = useMutation( siteEdgeCacheClearMutation( siteSlug ) );
const objectCacheClearMutation = useMutation( siteObjectCacheClearMutation( siteSlug ) );

const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );

const [ formData, setFormData ] = useState< CachingFormData >( {
active: isEdgeCacheActive ?? false,
} );

const isDirty = isEdgeCacheActive !== formData.active;
const { isPending } = edgeCacheStatusMutation;

const handleUpdateEdgeCacheStatus = ( e: React.FormEvent ) => {
e.preventDefault();
edgeCacheStatusMutation.mutate( formData.active, {
onSuccess: () => {
createSuccessNotice( __( 'Settings saved.' ), { type: 'snackbar' } );
},
onError: () => {
createErrorNotice( __( 'Failed to save settings.' ), { type: 'snackbar' } );
},
} );
};

const handleClearEdgeCache = () => {
edgeCacheClearMutation.mutate( undefined, {
onSuccess: () => {
createSuccessNotice( __( 'Global edge cache cleared.' ), { type: 'snackbar' } );
},
onError: () => {
createErrorNotice( __( 'Failed to clear edge cache.' ), { type: 'snackbar' } );
},
} );
};

const handleClearObjectCache = () => {
objectCacheClearMutation.mutate( 'Manually clearing again.', {
onSuccess: () => {
createSuccessNotice( __( 'Object cache cleared.' ), { type: 'snackbar' } );
},
onError: () => {
createErrorNotice( __( 'Failed to clear object cache.' ), { type: 'snackbar' } );
},
} );
};

const [ isClearingAllCaches, setIsClearingAllCaches ] = useState( false );

useEffect( () => {
if ( ! edgeCacheClearMutation.isPending && ! objectCacheClearMutation.isPending ) {
setIsClearingAllCaches( false );
}
}, [ edgeCacheClearMutation.isPending, objectCacheClearMutation.isPending ] );

const handleClearAllCaches = () => {
if ( isEdgeCacheActive ) {
handleClearEdgeCache();
}
handleClearObjectCache();

setIsClearingAllCaches( true );
};

const renderCallout = () => {
return <SettingsCallout siteSlug={ siteSlug } />;
};

const renderForm = () => {
return (
<>
<Card>
<CardBody>
<form onSubmit={ handleUpdateEdgeCacheStatus }>
<VStack spacing={ 4 } style={ { padding: '8px 0' } }>
<DataForm< CachingFormData >
data={ formData }
fields={ fields }
form={ form }
onChange={ ( edits: Partial< CachingFormData > ) => {
setFormData( ( data ) => ( { ...data, ...edits } ) );
} }
/>
<HStack justify="flex-start">
<Button
variant="primary"
type="submit"
isBusy={ isPending }
disabled={ isPending || ! isDirty }
>
{ __( 'Save' ) }
</Button>
</HStack>
</VStack>
</form>
</CardBody>
</Card>

<VStack spacing={ 4 }>
<ActionList
title={ __( 'Clear caches' ) }
description={ __(
'Clearing the cache may temporarily make your site less responsive.'
) }
>
<ActionList.ActionItem
title={ __( 'Global edge cache' ) }
description={ __( 'Edge caching enables faster content delivery.' ) }
actions={
<Button
variant="secondary"
size="compact"
onClick={ handleClearEdgeCache }
isBusy={ edgeCacheClearMutation.isPending && ! isClearingAllCaches }
disabled={
! isEdgeCacheActive || edgeCacheClearMutation.isPending || isClearingAllCaches
}
>
{ __( 'Clear' ) }
</Button>
}
/>
<ActionList.ActionItem
title={ __( 'Object cache' ) }
description={ __( 'Data is cached using Memcached to reduce database lookups.' ) }
actions={
<Button
variant="secondary"
size="compact"
onClick={ handleClearObjectCache }
isBusy={ objectCacheClearMutation.isPending && ! isClearingAllCaches }
disabled={ objectCacheClearMutation.isPending || isClearingAllCaches }
>
{ __( 'Clear' ) }
</Button>
}
/>
</ActionList>
<ActionList>
<ActionList.ActionItem
title={ __( 'Clear all caches' ) }
actions={
<Button
variant="secondary"
size="compact"
onClick={ handleClearAllCaches }
isBusy={ isClearingAllCaches }
disabled={ isClearingAllCaches }
>
{ __( 'Clear all' ) }
</Button>
}
/>
</ActionList>
</VStack>
</>
);
};

const description = canUpdate
? createInterpolateElement(
__( 'Manage your site’s server-side caching. <learnMoreLink />.' ),
{
learnMoreLink: <a href="#learn-more">{ __( 'Learn more' ) }</a>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is waiting for the help center to land?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, we have this placeholder all over the place, we should clean this up when help center is ready.

}
)
: '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also a description for the upsell 🙂

Screenshot 2025-06-02 at 4 22 31 PM

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description in this upsell situation is not accurate when the site is Business but not Atomic-transferrred yet. I talked more this in DOTCOM-13376. Let's solve it separately


return (
<PageLayout
size="small"
header={ <SettingsPageHeader title={ __( 'Caching' ) } description={ description } /> }
>
{ ! canUpdate ? renderCallout() : renderForm() }
</PageLayout>
);
}
Loading