Repository navigation
Premium Analytics: add a Leaderboard component over the chart pieces - #52965
Conversation
rows plus status in; states, shares, deltas and detail links owned; three widgets migrated
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! |
Code Coverage SummaryCoverage changed in 3 files.
4 files are newly checked for coverage.
|
ReportLink and the detail links render route links; one WidgetRoot per story
header, overlay and legend flags go; legend labels stay as one prop; JSDoc per prop
the component serves the package's own widgets too; nothing goes without a use in hand
rows declare children; the component owns the path, the back link and the healing
the row chrome is the overlay design; the chart keeps the switch for its own callers
no widget styles the body from outside
There was a problem hiding this comment.
Thanks @retrofox, this is a nice cleanup, and the three widgets look the same as trunk on a local Docker site 👍
I left a few inline comments. The main one to fix is Retry showing up again on "access denied" errors. The one about exports is a question, and the README one can be a follow-up.
| ); | ||
|
|
||
| const errorState = useMemo< WidgetStateError | undefined >( () => { | ||
| if ( error?.actions || ! refetch ) { |
There was a problem hiding this comment.
describeError() leaves actions out on purpose for 403s and response-shape errors, since retrying can't help there. This check treats a missing actions as "add Retry", so Search terms and Most commented authors show a Retry button again under "You don't have access to this data." I forced the stats request to return 403 on a local Docker site to confirm it.
Maybe only add the default Retry when no error is passed at all, and have Top videos use describeError() like the other two?
| if ( error?.actions || ! refetch ) { | |
| if ( error || ! refetch ) { |
The test at __tests__/leaderboard.test.tsx L131 passes actions: [], a shape describeError() never returns, so it doesn't catch this. It would be good to cover it with describeError( { status: 403 } ).
There was a problem hiding this comment.
Good catch, and thanks for forcing the 403 to confirm it. Fixed in 2687d48 with your suggestion: the component only adds a Retry to the default error, the one it shows when the widget passes none. An error from the widget renders as given, so describeError() decides.
Top videos moved to describeError() like the other two, so a denied request reads "You don't have access to this data." there as well.
The actions: [] test is gone. The suite now covers the three cases with real shapes: no error, describeError( { status: 403 } ), and a retryable describeError() result. The widget suite has the 403 case too.
| type LeaderboardChartData, | ||
| type LeaderboardSkeletonProps, | ||
| Leaderboard, | ||
| buildLeaderboardChartData, |
There was a problem hiding this comment.
Are all of these supposed to be public? I couldn't find anything outside components/leaderboard/ that imports buildLeaderboardChartData, BuildLeaderboardChartDataOptions or LeaderboardRowInputAction. Once #52885 exposes this through the SDK, they'd be hard to take back. Should we keep them internal until a widget needs to drive the chart itself?
There was a problem hiding this comment.
Agreed, they were not meant to be public. The toolkit now exports the component and its four types: LeaderboardProps, LeaderboardRowInput, LeaderboardStatus and LeaderboardDrillDown.
buildLeaderboardChartData(), its options type and LeaderboardRowInputAction stay internal to components/leaderboard/ until a widget needs to drive the chart itself.
| ```tsx | ||
| <WidgetRoot attributes={ attributes }> | ||
| <Leaderboard | ||
| rows={ rows.map( row => ( { |
There was a problem hiding this comment.
Small nitpick: this example passes rows={ rows.map( … ) } inline, which rebuilds the chart data every render. The three widgets memoize their rows correctly, so maybe the example should show the useMemo form, given SDK consumers will likely copy it. Fine as a follow-up.
There was a problem hiding this comment.
Done here rather than as a follow-up, since it is the snippet people will copy: the example memoizes the rows and passes describeError().
The PR description shows the same form.
a describeError() result decides for itself; the chart-data builder stays internal
louwie17
left a comment
There was a problem hiding this comment.
Thanks for the quick updates @retrofox, and for moving Top videos onto describeError() too. Retested on a local Docker site: a 403 now reads "You don't have access to this data." with no Retry on all three widgets, and a 500 still offers Retry. The exports and README look good as well.
Lets ship it :) 🚢
dognose24
left a comment
There was a problem hiding this comment.
Read the new component and the three migrated widgets, and ran the package's Jest suite (including the two extra timezones) and the tsgo typecheck at 8b50bd8: all pass. The three widgets build the same rows, shares and states as before, and the Stack-based layout matches the CSS it replaces. The 403 handling and the narrowed exports from the earlier round look right.
Three small suggestions inline, all on the drill-down path that no widget uses yet, so none of them changes what ships here. They can go with the follow-up that moves the drill-down widgets (WOOA7S-2240).
| const select = useCallback( | ||
| ( row: LeaderboardRowInput ) => setPath( [ ...( path ?? [] ), row.id ] ), | ||
| [ path, setPath ] | ||
| ); |
There was a problem hiding this comment.
[suggestion] select appends to the raw path, while goBack steps from the resolved trail. If the rows change while isFetching is still true (say the primary query lands before the comparison one), path still holds an id the new rows no longer have, so the clicked row is appended after it and never resolves. The click does nothing, and once the data settles the effect resets the selection.
I reproduced it with a throwaway test: drill into a row, rerender with different rows and isFetching: true, then click a row with children.
Building from the trail fixes it:
| const select = useCallback( | |
| ( row: LeaderboardRowInput ) => setPath( [ ...( path ?? [] ), row.id ] ), | |
| [ path, setPath ] | |
| ); | |
| const select = useCallback( | |
| ( row: LeaderboardRowInput ) => setPath( [ ...trail.map( step => step.id ), row.id ] ), | |
| [ trail, setPath ] | |
| ); |
Referrers' drillInto has the same shape today, so this is inherited rather than new. Fine as a follow-up.
| const backLink = | ||
| parent && drillDown ? ( | ||
| <WidgetBackLink | ||
| label={ grandparent ? grandparent.label : drillDown.backLabel } | ||
| ariaLabel={ | ||
| grandparent | ||
| ? sprintf( | ||
| /* translators: %s is the label of the list the back link returns to. */ | ||
| __( 'Back to %s', 'jetpack-premium-analytics-pkg' ), | ||
| grandparent.label | ||
| ) | ||
| : ( drillDown.backAriaLabel ?? drillDown.backLabel ) | ||
| } | ||
| onClick={ goBack } | ||
| /> | ||
| ) : null; |
There was a problem hiding this comment.
[suggestion] The back link sits outside WidgetState, so when it unmounts on the way back to the top level, focus falls to <body>: WidgetState only restores focus for elements inside its own root. Drilling in is fine, focus lands on the WidgetState root. Checked with a throwaway test: after clicking the top-level back link, document.activeElement is body.
Referrers behaves the same today. Since this component will own the drill-down for the remaining widgets, it may be worth fixing once here, for example by moving focus to the list when the back link goes away. Happy for it to go with WOOA7S-2240.
| /** | ||
| * Accessible name of a row that drills down, e.g. "View clicked links for %s". | ||
| */ | ||
| rowAriaLabel: ( row: LeaderboardRowInput ) => string; |
There was a problem hiding this comment.
[suggestion] nit: this sentence is the same as the one on LeaderboardDrillDown.rowAriaLabel in leaderboard.tsx. The public type is the one consumers read, so it could stay there and this internal copy could go.


Fixes WOOA7S-2239
Proposed changes
A ranked-rows widget takes nine toolkit names to render today: the chart, its skeleton,
WidgetState, the row limit, the row builder, three math helpers, and the navigation hook.Nineteen widgets in the package render
LeaderboardChart. Thirteen repeat the same composition by hand: the largest value of either period, a share per row, a delta, the row chrome, the skeleton, the states. Seven of them add a drill-down on top, also by hand.The VideoPress stack was about to expose that construction kit through the SDK (#52885). That is the sign the abstraction stops one level short: a consumer wants to hand over its rows and its request status, not assemble a chart.
The component
Leaderboardlives inpackages/widgets-toolkit/src/components/leaderboard/. It takes rows in the widget's own terms and a plain request status, and renders insideWidgetRoot.What it owns, so the widget does not:
buildLeaderboardRow()for the row chrome.WidgetStatefor the error and empty states: the widget's error asdescribeError()built it, or a generic one with a Retry bound torefetchwhen the widget passes none.Stack, plus an optional footer; the period legend when the widget names its labels; and the dashboard window on apostLinkorvideoLinkaction that omitssearch.The props are the ones the package's own widgets need and nothing else:
rows,status,error,empty,maxRows,format,legend,drillDown,navigation,footer.Drill-down
Rows may carry
children. With adrillDown(backLabel,rowAriaLabel), every such row becomes a button that shows its children under a back link.The component keeps the path of selected rows, any number of levels deep, and labels the back link after the list it returns to: the top-level label at depth one, the parent row's label further down, as Referrers does today.
The comparison switches to the children's own. A selection the settled data no longer backs is trimmed, so it survives in-flight fetches and transient failures.
A widget whose rows change under its own control, like the UTM dimension, resets the selection by giving the leaderboard a
key.The toolkit exports the component and its four types (
LeaderboardProps,LeaderboardRowInput,LeaderboardStatus,LeaderboardDrillDown). The pure step behind it,buildLeaderboardChartData(), stays internal until a widget needs it. The chart and its pieces stay the same.Dogfooding
This PR moves three widgets to it: Top videos, Search terms, and Most commented authors. Each loses its data builder, its
WidgetStatewiring, and its layout CSS. Top videos also moves its error todescribeError(), like the other two: an access-denied failure now reads "You don't have access to this data." with no Retry.The remaining leaderboards follow in batches (WOOA7S-2240). The metric-tabs family gets the same treatment (WOOA7S-2241).
What it changes for the SDK
Nothing yet. Once this lands, #52885 exports
Leaderboardand its types instead of the eight leaderboard pieces, and the Top videos widget of #52909 renders<Leaderboard>.Screenshots
Compare, for instance, the
Most commented AuthorswidgetDrill-down (Storybook,
WithDrillDown)Related product discussion/links
Linear: WOOA7S-2239, under the SDK audit WOOA7S-2228. Follow-ups WOOA7S-2240 and WOOA7S-2241.
Does this pull request change what data or activity we track or use?
No.
Testing instructions
jetpack test js packages/premium-analyticsOn the dashboard, the three migrated widgets look and behave as before.
In Storybook,
Widgets Toolkit/Components/Leaderboardshows the component on its own in every state, includingWithDrillDown.