Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Activity Log: default the page to the Table layout, load the upsell-callout stylesheet from the main entry, and surface the disabled toolbar + disabled date-range picker on the free tier with upgrade tooltips.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import { addQueryArgs } from '@wordpress/url';
import { useCallback } from 'react';
import { useAnalytics } from '../../hooks/use-analytics';
import illustrationUrl from './activity-logs-callout-illustration.svg';
import './upsell-callout.scss';
// Stylesheet is `@use`d from `src/js/style.scss` so the rules ride the
// main entry chunk instead of relying on a side-effect JS import.

const PRODUCT_SLUG = 'jetpack_security_t1_yearly';
const UPSELL_SOURCE = 'activity-log-page-purchase';
Expand Down Expand Up @@ -75,40 +76,37 @@ export function UpsellCallout() {

return (
<div className="jp-activity-log__upsell-callout">
<img
className="jp-activity-log__upsell-callout-image"
src={ illustrationUrl }
alt=""
role="presentation"
/>
<div className="jp-activity-log__upsell-callout-content">
<h2 className="jp-activity-log__upsell-callout-title">
{ __( 'Track every action with Jetpack Activity Log', 'jetpack-activity-log' ) }
{ __( 'Track every action with Activity logs', 'jetpack-activity-log' ) }
</h2>
<Text as="p" variant="muted">
{ __(
'Debug issues faster with insights from a comprehensive audit log of all your admin activities.',
'Debug issues faster with insights from a comprehensive audit log of all your site events.',
'jetpack-activity-log'
) }
</Text>
<Text as="p" variant="muted">
{ __(
'With your free plan, you can see your 20 most recent events. Upgrade for 30 days of history, plus filtering and date range controls.',
'Upgrade to get complete activity history for the last 30 days, advanced filtering and date range selection. Available on the Jetpack Security and Complete plans.',
'jetpack-activity-log'
) }
</Text>
<Text as="p" variant="muted">
{ __( 'Available on the Jetpack Security and Complete plans.', 'jetpack-activity-log' ) }
</Text>
<Button
variant="primary"
onClick={ onClickUpgrade }
isBusy={ hasCheckoutStarted }
disabled={ hasCheckoutStarted }
>
{ __( 'Upgrade', 'jetpack-activity-log' ) }
{ __( 'Upgrade plan', 'jetpack-activity-log' ) }
</Button>
</div>
<img
className="jp-activity-log__upsell-callout-image"
src={ illustrationUrl }
alt=""
role="presentation"
/>
</div>
);
}
175 changes: 124 additions & 51 deletions projects/packages/activity-log/src/js/components/ActivityLog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,55 @@ export default function ActivityLog() {
return () => observer.disconnect();
}, [] );

// On free tier, neutralize DataViews' real search + filter cluster
// (the `.dataviews__search` Stack rendered by `DataViews`'s default
// UI). We let DataViews ship its own toolbar so the page tracks
// upstream changes for free, then attach: `aria-disabled` for
// assistive tech, a `title` attribute that surfaces the upgrade
// nudge as a native browser tooltip on hover, and `tabindex="-1"`
// on every focusable descendant so the cluster is unreachable via
// keyboard. Pointer-event blocking on the children is handled in
// CSS via the `[aria-disabled="true"]` rule.
// `MutationObserver` re-applies after DataViews remounts the
// toolbar / re-renders the input (e.g., on initial fetch resolution
// or layout switch) so React's render doesn't strip the attributes.
useEffect( () => {
if ( hasActivityLogsAccess ) {
return;
}

const wrapper = wrapperRef.current;
if ( ! wrapper ) {
return;
}

const tooltipText = __( 'Upgrade your plan to use this feature.', 'jetpack-activity-log' );

const apply = ( root: ParentNode ) => {
const cluster = root.querySelector< HTMLElement >( '.dataviews__search' );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This selector will go away eventually and we can't rely on it as a stable API.

if ( ! cluster ) {
return;
}

if ( cluster.getAttribute( 'aria-disabled' ) !== 'true' ) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Recommend to just set "inert" attribute in the wrapper or overlay which doesn't let clicks/interactions through. Anything else like done here is prone to be fragile and bug prone.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

inert won't let the tooltip work on Firefox.

cluster.setAttribute( 'aria-disabled', 'true' );
cluster.setAttribute( 'title', tooltipText );
}

cluster.querySelectorAll< HTMLElement >( 'input, button, [tabindex]' ).forEach( el => {
if ( el.getAttribute( 'tabindex' ) !== '-1' ) {
el.setAttribute( 'tabindex', '-1' );
}
} );
};

apply( wrapper );
const observer = new MutationObserver( () => apply( wrapper ) );
observer.observe( wrapper, { subtree: true, childList: true } );

return () => observer.disconnect();
}, [ hasActivityLogsAccess ] );

// Date-range defaults to "Last 7 days" anchored at the site's calendar
// today (not the browser's) — matches Calypso's `getDefaultDateRange`.
// The range is client-only state: refreshes reset to the default
Expand Down Expand Up @@ -328,17 +377,21 @@ export default function ActivityLog() {

// Mounting the picker as an admin-ui `actions` slot places it in the
// AdminPage header alongside the title/subtitle — matches MSD's
// layout for the logs pages.
const headerActions = hasActivityLogsAccess ? (
// layout for the logs pages. On free tier the picker renders as a
// disabled upgrade affordance (no popover, hover tooltip), keeping
// the page surface visually consistent with the paid version.
const headerActions = (
<DateRangePicker
start={ dateRange.start }
end={ dateRange.end }
onChange={ onChangeDateRange }
timezoneString={ timezoneString }
gmtOffset={ gmtOffset }
locale={ locale }
disabled={ ! hasActivityLogsAccess }
disabledTooltipText={ __( 'Upgrade your plan to use this feature.', 'jetpack-activity-log' ) }
/>
) : undefined;
);

return (
<AdminPage
Expand All @@ -351,54 +404,74 @@ export default function ActivityLog() {
showFooter={ false }
unwrapped
>
<div ref={ wrapperRef } className="jp-activity-log__dataviews-wrapper">
<DataViews< Activity >
data={ logData }
isLoading={ isFetching || isLoadingList }
paginationInfo={ paginationInfo }
fields={ fields as Field< Activity >[] }
view={ view }
actions={ actions }
getItemId={ getItemId }
search
// Advertise both DataViews' built-in Activity timeline
// (the default) and a Table layout. Toggle lives in
// the cog popover's layout switcher. Each layout maps
// the event parts to the right slots:
// - Activity: `event_icon` → mediaField (left
// bullet slot), `event_title` → titleField,
// `event_description` → descriptionField, plus
// `groupBy: published_date` for day headers.
// - Table: one composite `event` column alongside
// Date / User.
// See DEFAULT_LAYOUTS in ./views for the full shape —
// it explicitly nulls slot/groupBy refs on Table so a
// round-trip Activity → Table doesn't carry those
// over and double-render as a primary column.
defaultLayouts={ DEFAULT_LAYOUTS }
onChangeView={ onChangeView }
onReset={ isViewModified ? onResetView : false }
// On the free tier, lock the perPage selector to the
// capped size and hide search/filters/sort/view-config
// by replacing the default UI with just the table (same
// switches Calypso uses at logs-activity/dataviews/
// index.tsx:201-208).
config={
hasActivityLogsAccess
? undefined
: { perPageSizes: [ ACTIVITY_LOGS_DEFAULT_PAGE_SIZE ] }
}
empty={
<p>
{ view.search
? __( 'No activity found', 'jetpack-activity-log' )
: __( 'No activities', 'jetpack-activity-log' ) }
</p>
}
>
{ hasActivityLogsAccess ? undefined : <DataViews.Layout /> }
</DataViews>
{ ! hasActivityLogsAccess && ! isFetching && logData.length > 0 && <UpsellCallout /> }
<div
ref={ wrapperRef }
className={
'jp-activity-log__dataviews-wrapper' +
( hasActivityLogsAccess ? '' : ' jp-activity-log__dataviews-wrapper--free-tier' )
}
>
{ /*
* Single inner div soaks up `jetpack-admin-page-layout`'s
* `.admin-ui-page > :not(...):not(...) > *` rule (which
* force-applies `flex: 1 1 auto; flex-direction: column`
* to every direct child of the page's scroll column).
* With the chain landing here, DataViews and the
* free-tier UpsellCallout are grandchildren and stack
* without competing for flex space — no `!important`
* overrides needed.
*/ }
<div className="jp-activity-log__inner">
<DataViews< Activity >
data={ logData }
isLoading={ isFetching || isLoadingList }
paginationInfo={ paginationInfo }
fields={ fields as Field< Activity >[] }
view={ view }
actions={ actions }
getItemId={ getItemId }
search
// Advertise both DataViews' built-in Activity timeline
// (the default) and a Table layout. Toggle lives in
// the cog popover's layout switcher. Each layout maps
// the event parts to the right slots:
// - Activity: `event_icon` → mediaField (left
// bullet slot), `event_title` → titleField,
// `event_description` → descriptionField, plus
// `groupBy: published_date` for day headers.
// - Table: one composite `event` column alongside
// Date / User.
// See DEFAULT_LAYOUTS in ./views for the full shape —
// it explicitly nulls slot/groupBy refs on Table so a
// round-trip Activity → Table doesn't carry those
// over and double-render as a primary column.
defaultLayouts={ DEFAULT_LAYOUTS }
onChangeView={ onChangeView }
onReset={ isViewModified ? onResetView : false }
// On the free tier, lock the perPage selector to the
// capped size. DataViews keeps rendering its real
// toolbar (search + filter toggle + cog); the
// search/filter cluster is neutralized by the
// `aria-disabled` + `tabindex="-1"` overlay
// applied in the effect above — Calypso's
// equivalent switch at logs-activity/dataviews/
// index.tsx:201-208 hides them, but we want the
// upgrade affordance to be discoverable on hover.
config={
hasActivityLogsAccess
? undefined
: { perPageSizes: [ ACTIVITY_LOGS_DEFAULT_PAGE_SIZE ] }
}
empty={
<p>
{ view.search
? __( 'No activity found', 'jetpack-activity-log' )
: __( 'No activities', 'jetpack-activity-log' ) }
</p>
}
/>
{ ! hasActivityLogsAccess && ! isFetching && logData.length > 0 && <UpsellCallout /> }
</div>
</div>
</AdminPage>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,56 @@
// Upsell callout rendered below the Activity Log table on free plans.
// Visual intent mirrors Calypso's `ActivityLogsCallout` layout: a centered
// card with the illustration on one side, copy + CTA on the other.
// Visual: a bordered card with the copy + CTA on the left and the
// illustration on the right (column-reverse on mobile, image moves
// below the copy).
.jp-activity-log__upsell-callout {
display: flex;
flex-direction: column-reverse;
align-items: center;
flex-direction: column;
gap: 24px;
padding: 32px 24px;
margin: 32px auto 0;
margin: 24px auto 0;
max-width: 960px;
padding: 24px;
border: 1px solid var(--wpds-color-stroke-surface-neutral-weak, #dcdcde);
border-radius: 4px;
background-color: var(--wpds-color-bg-surface-neutral, #fff);

@media (min-width: 782px) {
flex-direction: row;
align-items: center;
gap: 48px;
padding: 40px;
gap: 32px;
padding: 32px;
}
}

.jp-activity-log__upsell-callout-image {
display: block;
width: 100%;
max-width: 320px;
max-width: 360px;
height: auto;
flex-shrink: 0;
align-self: center;
}

.jp-activity-log__upsell-callout-content {
display: flex;
flex-direction: column;
gap: 16px;
gap: 12px;
flex: 1 1 auto;
min-width: 0;
}

.jp-activity-log__upsell-callout-title {
margin: 0;
font-size: 24px;
font-size: 20px;
line-height: 1.3;
font-weight: 500;
color: var(--wpds-color-fg-content-neutral, #1e1e1e);
}

// The <Button variant="primary"> provides its own styling; we just align
// it to the start of the content column so it doesn't stretch full-width.
// it to the start of the content column so it doesn't stretch full-width
// and add a small top margin so it sits visually distinct from the copy
// above.
.jp-activity-log__upsell-callout-content .components-button {
align-self: flex-start;
margin-top: 8px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,14 @@ const TABLE_LAYOUT = {
};

export const DEFAULT_VIEW: View = {
type: 'activity',
type: 'table',
perPage: 20,
sort: {
field: 'published',
direction: 'desc',
},
fields: [ 'published', 'actor' ],
layout: { density: 'balanced' },
titleField: 'event_title',
mediaField: 'event_icon',
descriptionField: 'event_description',
// Group consecutive events that fall on the same calendar day
// (site timezone) under a "Apr 24, 2026" header. See the matching
// entry in DEFAULT_LAYOUTS.activity below for the full rationale.
groupBy: { field: 'published_date', direction: 'desc', showLabel: false },
fields: [ 'published', 'event', 'actor' ],
layout: TABLE_LAYOUT,
showLevels: false,
};

Expand Down
Loading
Loading