Skip to content

Commit dc0bdac

Browse files
yashwinclaude
andauthored
A4A: Add Latest activity card and Logs > Activity view to agency site detail (#112231)
* A4A: Add Latest activity card and Logs > Activity view to agency site detail Adds a Latest activity card to the agency site overview and a Logs > Activity detailed view, reusing the multi-site dashboard's activity components. The shared LatestActivityCard gains an optional activityLogUrl, and the activity DataViews takes searchParams as a prop so it is no longer bound to the dotcom route. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * A4A: Use non-throwing prefetchQuery in the activity route settings loader Matches the dotcom loadSiteLogsRoute so a transient settings error falls back to UTC via useSiteTimezoneWithJetpackFallback instead of crashing the route. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d9b0168 commit dc0bdac

10 files changed

Lines changed: 188 additions & 10 deletions

File tree

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

Lines changed: 10 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, backup, shield } from '@wordpress/icons';
5+
import { backup, category, formatListBullets, shield } from '@wordpress/icons';
66
import { agencySiteRoute } from '../../../app/router/agency';
77
import {
88
SidebarBackButton,
@@ -51,6 +51,15 @@ export default function AgencySiteSidebar() {
5151
</SidebarMenuItem>
5252
</SidebarExpandableMenuItem>
5353
) }
54+
<SidebarExpandableMenuItem
55+
label={ __( 'Logs' ) }
56+
icon={ formatListBullets }
57+
to={ `/sites/${ siteSlug }/logs/activity` }
58+
>
59+
<SidebarMenuItem to={ `/sites/${ siteSlug }/logs/activity` }>
60+
{ __( 'Activity' ) }
61+
</SidebarMenuItem>
62+
</SidebarExpandableMenuItem>
5463
</SidebarMenu>
5564
</VStack>
5665
) }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { siteBySlugQuery } from '@automattic/api-queries';
2+
import { useQuery } from '@tanstack/react-query';
3+
import { agencySiteRoute } from '../../../app/router/agency';
4+
import LatestActivityCard from '../../../sites/overview-latest-activity-card';
5+
6+
export default function ActivityCard() {
7+
const { siteSlug } = agencySiteRoute.useParams();
8+
const { data: site } = useQuery( siteBySlugQuery( siteSlug ) );
9+
10+
if ( ! site ) {
11+
return null;
12+
}
13+
14+
return (
15+
<LatestActivityCard site={ site } activityLogUrl={ `/sites/${ siteSlug }/logs/activity` } />
16+
);
17+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { HostingFeatures, LogType } from '@automattic/api-core';
2+
import { siteBySlugQuery } from '@automattic/api-queries';
3+
import { DateRangePicker } from '@automattic/date-range-picker';
4+
import { useSuspenseQuery } from '@tanstack/react-query';
5+
import { createInterpolateElement } from '@wordpress/element';
6+
import { __ } from '@wordpress/i18n';
7+
import { useState } from 'react';
8+
import { useDateRange } from '../../../app/hooks/use-date-range';
9+
import { useSiteTimezoneWithJetpackFallback } from '../../../app/hooks/use-site-timezone';
10+
import { useLocale } from '../../../app/locale';
11+
import { agencySiteActivityRoute, agencySiteRoute } from '../../../app/router/agency';
12+
import { Card, CardBody } from '../../../components/card';
13+
import InlineSupportLink from '../../../components/inline-support-link';
14+
import { PageHeader } from '../../../components/page-header';
15+
import PageLayout from '../../../components/page-layout';
16+
import SiteActivityLogsDataViews from '../../../sites/logs-activity/dataviews';
17+
import { hasHostingFeature, hasPlanFeature } from '../../../utils/site-features';
18+
19+
export default function AgencySiteActivity() {
20+
const { siteSlug } = agencySiteRoute.useParams();
21+
const { data: site } = useSuspenseQuery( siteBySlugQuery( siteSlug ) );
22+
const { gmtOffset, timezoneString } = useSiteTimezoneWithJetpackFallback( site );
23+
24+
const locale = useLocale();
25+
const searchParams = agencySiteActivityRoute.useSearch();
26+
27+
// Activity has no auto-refresh, but the shared DataViews props require this state.
28+
const [ autoRefresh, setAutoRefresh ] = useState( false );
29+
const { dateRange, handleDateRangeChange } = useDateRange( {
30+
timezoneString,
31+
gmtOffset,
32+
autoRefresh,
33+
} );
34+
const [ dateRangeVersion, setDateRangeVersion ] = useState( 0 );
35+
36+
const handleDateRangeChangeWrapper = ( next: { start: Date; end: Date } ) => {
37+
handleDateRangeChange( next );
38+
setDateRangeVersion( ( v ) => v + 1 );
39+
};
40+
41+
const hasActivityLogAccess =
42+
hasHostingFeature( site, HostingFeatures.ACTIVITY_LOG ) ||
43+
hasPlanFeature( site, HostingFeatures.ACTIVITY_LOG );
44+
45+
return (
46+
<PageLayout
47+
header={
48+
<PageHeader
49+
title={ __( 'Activity' ) }
50+
description={ createInterpolateElement(
51+
__( 'View your site’s activity log. <learnMoreLink />' ),
52+
{
53+
learnMoreLink: <InlineSupportLink supportContext="site-monitoring-logs" />,
54+
}
55+
) }
56+
actions={
57+
<DateRangePicker
58+
start={ dateRange.start }
59+
end={ dateRange.end }
60+
gmtOffset={ gmtOffset }
61+
timezoneString={ timezoneString }
62+
locale={ locale }
63+
onChange={ handleDateRangeChangeWrapper }
64+
/>
65+
}
66+
/>
67+
}
68+
>
69+
<Card className="site-logs-card site-logs-card--activity">
70+
<CardBody>
71+
{ /* TODO(A4A-2917): the reused activity DataViews shows a primary "Manage backup"
72+
row action on rewindable entries that navigates to the dotcom
73+
siteBackupDetailRoute, which A4A doesn't register — it dead-ends until
74+
agency backups routes exist. Hide or re-point it once backups land. */ }
75+
<SiteActivityLogsDataViews
76+
logType={ LogType.ACTIVITY }
77+
dateRange={ dateRange }
78+
dateRangeVersion={ dateRangeVersion }
79+
autoRefresh={ autoRefresh }
80+
setAutoRefresh={ setAutoRefresh }
81+
gmtOffset={ gmtOffset }
82+
timezoneString={ timezoneString }
83+
site={ site }
84+
hasActivityLogsAccess={ hasActivityLogAccess }
85+
searchParams={ searchParams }
86+
/>
87+
</CardBody>
88+
</Card>
89+
</PageLayout>
90+
);
91+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { agencySiteRoute } from '../../../app/router/agency';
44
import { PageHeader } from '../../../components/page-header';
55
import PageLayout from '../../../components/page-layout';
66
import { getDisplayUrl, getSiteName } from '../dataviews/site-data';
7+
import ActivityCard from './activity-card';
78
import BackupCard from './backup-card';
89
import ScanCard from './scan-card';
910

@@ -20,6 +21,7 @@ export default function AgencySiteOverview() {
2021
<BackupCard site={ site } />
2122
<ScanCard site={ site } siteSlug={ siteSlug } />
2223
</Grid>
24+
<ActivityCard />
2325
</PageLayout>
2426
);
2527
}
Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
1+
import { type Site, type SiteSettings } from '@automattic/api-core';
12
import { siteSettingsQuery } from '@automattic/api-queries';
2-
import { useSuspenseQuery } from '@tanstack/react-query';
3+
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
4+
5+
const selectTimezone = ( s: SiteSettings | undefined ) => ( {
6+
gmtOffset: Number( s?.gmt_offset ) || 0,
7+
timezoneString: s?.timezone_string || undefined,
8+
} );
39

410
export function useSiteTimezone( siteId: number ) {
511
return useSuspenseQuery( {
612
...siteSettingsQuery( siteId ),
7-
select: ( s ) => ( {
8-
gmtOffset: Number( s?.gmt_offset ) || 0,
9-
timezoneString: s?.timezone_string || undefined,
10-
} ),
13+
select: selectTimezone,
1114
} ).data;
1215
}
16+
17+
// Sites with a Jetpack connection error can't reach the settings endpoint, so we
18+
// skip the fetch and fall back to UTC defaults to keep the page accessible. Uses a
19+
// non-suspense query because the fetch has to be conditional on reachability.
20+
export function useSiteTimezoneWithJetpackFallback( site: Site ) {
21+
const isReachable = ! site.__inaccessible_jetpack_error;
22+
const { data } = useQuery( {
23+
...siteSettingsQuery( site.ID ),
24+
enabled: isReachable,
25+
select: selectTimezone,
26+
} );
27+
28+
return isReachable && data ? data : { gmtOffset: 0, timezoneString: undefined };
29+
}

client/dashboard/app/router/agency.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,40 @@ const agencySiteScanRoute = createRoute( {
381381
)
382382
);
383383

384+
// `/sites/$siteSlug/logs` – logs parent, redirects to the activity log
385+
export const agencySiteLogsRoute = createRoute( {
386+
head: () => ( { meta: [ { title: __( 'Logs' ) } ] } ),
387+
getParentRoute: () => agencySiteRoute,
388+
path: 'logs',
389+
} );
390+
391+
const agencySiteLogsIndexRoute = createRoute( {
392+
getParentRoute: () => agencySiteLogsRoute,
393+
path: '/',
394+
beforeLoad: ( { params: { siteSlug } } ) => {
395+
throw dashboardRedirect( { to: `/sites/${ siteSlug }/logs/activity` } );
396+
},
397+
} );
398+
399+
// `/sites/$siteSlug/logs/activity` – activity log detailed view
400+
export const agencySiteActivityRoute = createRoute( {
401+
head: () => ( { meta: [ { title: __( 'Activity' ) } ] } ),
402+
getParentRoute: () => agencySiteLogsRoute,
403+
path: 'activity',
404+
loader: async ( { params: { siteSlug } } ) => {
405+
const site = await queryClient.ensureQueryData( siteBySlugQuery( siteSlug ) );
406+
if ( ! site.__inaccessible_jetpack_error ) {
407+
await queryClient.prefetchQuery( siteSettingsQuery( site.ID ) );
408+
}
409+
},
410+
} ).lazy( () =>
411+
import( '../../agency/sites/site/activity' ).then( ( d ) =>
412+
createLazyRoute( 'agency-site-activity' )( {
413+
component: d.default,
414+
} )
415+
)
416+
);
417+
384418
const agencySiteScanIndexRoute = createRoute( {
385419
getParentRoute: () => agencySiteScanRoute,
386420
path: '/',
@@ -439,6 +473,7 @@ export const createAgencyRoutes = () => [
439473
agencySiteScanActiveRoute,
440474
agencySiteScanHistoryRoute,
441475
] ),
476+
agencySiteLogsRoute.addChildren( [ agencySiteLogsIndexRoute, agencySiteActivityRoute ] ),
442477
] ),
443478
] ),
444479
];

client/dashboard/sites/logs-activity/dataviews/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { useAnalytics } from '../../../app/analytics';
1010
import { usePersistentView } from '../../../app/hooks/use-persistent-view';
1111
import { useLocale } from '../../../app/locale';
1212
import { PerformanceTrackerStop } from '../../../app/performance-tracking';
13-
import { siteLogsActivityRoute } from '../../../app/router/sites';
1413
import { DataViews, DataViewsEmptyStateLayout } from '../../../components/dataviews';
1514
import { formatDate } from '../../../utils/datetime';
1615
import { getActivityLogHiddenGroups } from '../../../utils/site-features';
@@ -28,6 +27,9 @@ import './style.scss';
2827
type SiteLogsDataViewsPropsActivity = SiteLogsDataViewsProps & {
2928
logType: typeof LogType.ACTIVITY;
3029
hasActivityLogsAccess: boolean;
30+
// View state deep-linked via the URL, read from the active route's search.
31+
// Passed in so this component isn't bound to a specific route.
32+
searchParams?: Record< string, unknown >;
3133
};
3234

3335
const ACTIVITY_LOGS_DEFAULT_PAGE_SIZE = 20;
@@ -38,6 +40,7 @@ function SiteActivityLogsDataViews( {
3840
dateRange,
3941
dateRangeVersion,
4042
hasActivityLogsAccess,
43+
searchParams,
4144
}: SiteLogsDataViewsPropsActivity ) {
4245
const { recordTracksEvent } = useAnalytics();
4346
const locale = useLocale();
@@ -47,8 +50,6 @@ function SiteActivityLogsDataViews( {
4750
[ dateRange.start, dateRange.end, gmtOffset, timezoneString ]
4851
);
4952

50-
const searchParams = siteLogsActivityRoute.useSearch();
51-
5253
const { view, updateView, resetView } = usePersistentView( {
5354
slug: 'site-logs-activity',
5455
defaultView: DEFAULT_VIEW,

client/dashboard/sites/logs-activity/dataviews/test/index.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ function renderActivityLogsDataViews(
168168
setAutoRefresh={ jest.fn() }
169169
logType="activity"
170170
hasActivityLogsAccess
171+
searchParams={ {} }
171172
/>
172173
);
173174
}

client/dashboard/sites/logs/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { HostingFeatures, LogType, type Site, type SiteSettings } from '@automat
22
import { siteBySlugQuery, siteSettingsQuery } from '@automattic/api-queries';
33
import { DateRangePicker, isLast7Days } from '@automattic/date-range-picker';
44
import { useSuspenseQuery } from '@tanstack/react-query';
5+
import { useSearch } from '@tanstack/react-router';
56
import { createInterpolateElement } from '@wordpress/element';
67
import { __ } from '@wordpress/i18n';
78
import { useEffect, useState } from 'react';
@@ -85,6 +86,7 @@ function SiteLogsContent( {
8586
);
8687

8788
const siteId = site.ID;
89+
const activitySearchParams = useSearch( { strict: false } );
8890
const showTimeMismatchNotice = useShouldShowTimeMismatchNotice( {
8991
siteTime: gmtOffset,
9092
siteId,
@@ -240,6 +242,7 @@ function SiteLogsContent( {
240242
timezoneString={ timezoneString }
241243
site={ site }
242244
hasActivityLogsAccess={ hasActivityLogAccess }
245+
searchParams={ activitySearchParams }
243246
/>
244247
</>
245248
) }

client/dashboard/sites/overview-latest-activity-card/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@ function getActivityLogUrl( site: Site ) {
6060
export default function LatestActivityCard( {
6161
site,
6262
isCompact = false,
63+
activityLogUrl,
6364
}: {
6465
site: Site;
6566
isCompact?: boolean;
67+
activityLogUrl?: string;
6668
} ) {
6769
const notGroup = getActivityLogHiddenGroups( site );
6870
const { data } = useQuery( siteLastFiveActivityLogEntriesQuery( site.ID, notGroup ) );
@@ -106,7 +108,7 @@ export default function LatestActivityCard( {
106108
{ data && data.length > 0 && (
107109
<SummaryButtonCardFooter
108110
title={ __( 'See all activity' ) }
109-
href={ getActivityLogUrl( site ) }
111+
href={ activityLogUrl ?? getActivityLogUrl( site ) }
110112
density="medium"
111113
onClick={ handleClickSeeAll }
112114
/>

0 commit comments

Comments
 (0)