-
Notifications
You must be signed in to change notification settings - Fork 293
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
Enhance/#6252 - Create the "Top cities driving traffic" key metric widget tile #7279
Changes from 17 commits
86d83ff
e19d27d
7929340
d92df6c
4607b5b
7739774
853fc9a
189c5fe
e76f952
80ab73a
8d74279
eac651a
f3fea56
402d100
59410ed
edbf618
666631f
7edccc5
6298942
9f8860c
b403efb
3ce47da
4f6898c
f49aafb
1aefc4b
672b200
b2f6412
bb266e1
5794203
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,15 +21,97 @@ | |
*/ | ||
import PropTypes from 'prop-types'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __ } from '@wordpress/i18n'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import Data from 'googlesitekit-data'; | ||
import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants'; | ||
import { | ||
DATE_RANGE_OFFSET, | ||
MODULES_ANALYTICS_4, | ||
} from '../../datastore/constants'; | ||
import { ZeroDataMessage } from '../../../analytics/components/common'; | ||
import { numFmt } from '../../../../util'; | ||
import { MetricTileTable } from '../../../../components/KeyMetrics'; | ||
const { useSelect, useInViewSelect } = Data; | ||
|
||
export default function TopCitiesWidget( { Widget } ) { | ||
const dates = useSelect( ( select ) => | ||
select( CORE_USER ).getDateRangeDates( { | ||
offsetDays: DATE_RANGE_OFFSET, | ||
} ) | ||
); | ||
|
||
const topcCitiesReportOptions = { | ||
...dates, | ||
dimensions: [ 'city' ], | ||
metrics: [ { name: 'totalUsers' } ], | ||
orderby: [ | ||
{ | ||
metric: { | ||
metricName: 'totalUsers', | ||
}, | ||
desc: true, | ||
}, | ||
], | ||
limit: 3, | ||
}; | ||
|
||
const topCitiesReport = useInViewSelect( ( select ) => | ||
select( MODULES_ANALYTICS_4 ).getReport( topcCitiesReportOptions ) | ||
); | ||
|
||
const loading = useInViewSelect( | ||
( select ) => | ||
! select( MODULES_ANALYTICS_4 ).hasFinishedResolution( | ||
'getReport', | ||
[ topcCitiesReportOptions ] | ||
) | ||
); | ||
|
||
const { rows = [] } = topCitiesReport || {}; | ||
|
||
const totalUsers = | ||
topCitiesReport?.totals?.[ 0 ]?.metricValues?.[ 0 ]?.value; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're already destructuring from topCitiesReport above with a fallback, how about we do the same with |
||
|
||
const columns = [ | ||
{ | ||
field: 'dimensionValues', | ||
Component: ( { fieldValue } ) => { | ||
const [ title ] = fieldValue; | ||
|
||
return ( | ||
<p className="googlesitekit-km-widget-tile__table-plain-text"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets extract this to a component since it will be used by others as well rather than relying on the same class name in each instance manually. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't extract it into a separate component since it will be used only in Cities and Countries. However, it's a good idea to extract it 👍 |
||
{ title.value } | ||
</p> | ||
); | ||
}, | ||
}, | ||
{ | ||
field: 'metricValues.0.value', | ||
Component: ( { fieldValue } ) => ( | ||
<strong>{ numFmt( fieldValue / totalUsers, '%' ) }</strong> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is consistent with other uses we have (except the |
||
), | ||
}, | ||
]; | ||
|
||
return ( | ||
<Widget> | ||
<div>TODO: UI for TopCitiesWidget</div> | ||
</Widget> | ||
<MetricTileTable | ||
Widget={ Widget } | ||
title={ __( 'Top cities driving traffic', 'google-site-kit' ) } | ||
loading={ loading } | ||
rows={ rows } | ||
columns={ columns } | ||
zeroState={ ZeroDataMessage } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not unique to this issue since I see it is following others, but We can fix this in a separate issue if you'd prefer, I'm just calling what I see :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. I have incorporated the changes in this PR to save a few hours 😃 |
||
/> | ||
); | ||
} | ||
|
||
TopCitiesWidget.propTypes = { | ||
Widget: PropTypes.elementType.isRequired, | ||
WidgetNull: PropTypes.elementType.isRequired, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
/** | ||
* TopCitiesWidget component stories. | ||
* | ||
* Site Kit by Google, Copyright 2023 Google LLC | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants'; | ||
import { MODULES_ANALYTICS_4 } from '../../datastore/constants'; | ||
import { | ||
provideKeyMetrics, | ||
provideModules, | ||
} from '../../../../../../tests/js/utils'; | ||
import { withWidgetComponentProps } from '../../../../googlesitekit/widgets/util'; | ||
import { getAnalytics4MockResponse } from '../../utils/data-mock'; | ||
import { replaceValuesInAnalytics4ReportWithZeroData } from '../../../../../../.storybook/utils/zeroReports'; | ||
import WithRegistrySetup from '../../../../../../tests/js/WithRegistrySetup'; | ||
import TopCitiesWidget from './TopCitiesWidget'; | ||
|
||
const reportOptions = { | ||
startDate: '2020-08-11', | ||
endDate: '2020-09-07', | ||
dimensions: [ 'city' ], | ||
metrics: [ { name: 'totalUsers' } ], | ||
orderby: [ | ||
{ | ||
metric: { | ||
metricName: 'totalUsers', | ||
}, | ||
desc: true, | ||
}, | ||
], | ||
limit: 3, | ||
}; | ||
|
||
const WidgetWithComponentProps = | ||
withWidgetComponentProps( 'test' )( TopCitiesWidget ); | ||
|
||
const Template = ( { setupRegistry, ...args } ) => ( | ||
<WithRegistrySetup func={ setupRegistry }> | ||
<WidgetWithComponentProps { ...args } /> | ||
</WithRegistrySetup> | ||
); | ||
|
||
export const Ready = Template.bind( {} ); | ||
Ready.storyName = 'Ready'; | ||
Ready.args = { | ||
setupRegistry: ( registry ) => { | ||
const report = getAnalytics4MockResponse( reportOptions ); | ||
// Calculate sum of metricValues for all rows | ||
const rowsSum = report.rows.reduce( ( total, row ) => { | ||
return total + Number( row.metricValues[ 0 ].value ); | ||
}, 0 ); | ||
|
||
// Generate totalValueForAllCities that is higher than the sum | ||
const totalValueForAllCities = rowsSum * 2; | ||
|
||
// Adjust totals field in the mock response | ||
report.totals = [ | ||
{ | ||
dimensionValues: [ { value: 'RESERVED_TOTAL' } ], | ||
metricValues: [ { value: totalValueForAllCities.toString() } ], | ||
}, | ||
]; | ||
registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetReport( report, { | ||
options: reportOptions, | ||
} ); | ||
}, | ||
}; | ||
Ready.scenario = { | ||
label: 'KeyMetrics/TopCitiesWidget/Ready', | ||
delay: 250, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's not add |
||
}; | ||
|
||
export const Loading = Template.bind( {} ); | ||
Loading.storyName = 'Loading'; | ||
Loading.args = { | ||
setupRegistry: ( { dispatch } ) => { | ||
dispatch( MODULES_ANALYTICS_4 ).startResolution( 'getReport', [ | ||
reportOptions, | ||
] ); | ||
}, | ||
}; | ||
Comment on lines
+88
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason we don't have VRT scenarios for the loading state? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't have VRT scenarios for the loading state for any other KM widget stories. I didn't create it to follow other widgets. Apart from that, IMO, we don't need a VRT scenario for the loading state. cc @jimmymadon |
||
|
||
export const ZeroData = Template.bind( {} ); | ||
ZeroData.storyName = 'Zero Data'; | ||
ZeroData.args = { | ||
setupRegistry: ( { dispatch } ) => { | ||
const report = getAnalytics4MockResponse( reportOptions ); | ||
const zeroReport = | ||
replaceValuesInAnalytics4ReportWithZeroData( report ); | ||
|
||
dispatch( MODULES_ANALYTICS_4 ).receiveGetReport( zeroReport, { | ||
options: reportOptions, | ||
} ); | ||
}, | ||
}; | ||
ZeroData.scenario = { | ||
label: 'KeyMetrics/TopCitiesWidget/ZeroData', | ||
delay: 250, | ||
}; | ||
|
||
export default { | ||
title: 'Key Metrics/TopCitiesWidget', | ||
decorators: [ | ||
( Story, { args } ) => { | ||
const setupRegistry = ( registry ) => { | ||
provideModules( registry, [ | ||
{ | ||
slug: 'analytics-4', | ||
active: true, | ||
connected: true, | ||
}, | ||
] ); | ||
|
||
registry.dispatch( CORE_USER ).setReferenceDate( '2020-09-08' ); | ||
|
||
provideKeyMetrics( registry ); | ||
|
||
// Call story-specific setup. | ||
args.setupRegistry( registry ); | ||
}; | ||
|
||
return ( | ||
<WithRegistrySetup func={ setupRegistry }> | ||
<Story /> | ||
</WithRegistrySetup> | ||
); | ||
}, | ||
], | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/** | ||
* TopCitiesWidget component tests. | ||
* | ||
* Site Kit by Google, Copyright 2023 Google LLC | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { render } from '../../../../../../tests/js/test-utils'; | ||
import { provideKeyMetrics } from '../../../../../../tests/js/utils'; | ||
import { provideAnalytics4MockReport } from '../../utils/data-mock'; | ||
import { getWidgetComponentProps } from '../../../../googlesitekit/widgets/util'; | ||
import { | ||
CORE_USER, | ||
KM_ANALYTICS_TOP_CITIES, | ||
} from '../../../../googlesitekit/datastore/user/constants'; | ||
import TopCitiesWidget from './TopCitiesWidget'; | ||
|
||
describe( 'TopCitiesWidget', () => { | ||
const { Widget } = getWidgetComponentProps( KM_ANALYTICS_TOP_CITIES ); | ||
|
||
it( 'renders correctly with the expected metrics', async () => { | ||
const { container, waitForRegistry } = render( | ||
<TopCitiesWidget Widget={ Widget } />, | ||
{ | ||
setupRegistry: ( registry ) => { | ||
registry | ||
.dispatch( CORE_USER ) | ||
.setReferenceDate( '2020-09-08' ); | ||
|
||
provideKeyMetrics( registry ); | ||
provideAnalytics4MockReport( registry, { | ||
startDate: '2020-08-11', | ||
endDate: '2020-09-07', | ||
dimensions: [ 'city' ], | ||
metrics: [ { name: 'totalUsers' } ], | ||
orderby: [ | ||
{ | ||
metric: { | ||
metricName: 'totalUsers', | ||
}, | ||
desc: true, | ||
}, | ||
], | ||
limit: 3, | ||
} ); | ||
}, | ||
} | ||
); | ||
await waitForRegistry(); | ||
|
||
expect( container ).toMatchSnapshot(); | ||
} ); | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The in-view select variant is only intended for selectors that get reports since it will return
undefined
unconditionally when not in view which would not be expected for a selector likehasFinishedResolution
which would never return anything other than a boolean. It could be used for other things too but it's essentially intended for guarding selectors that have resolvers and their respective side-effects.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I have updated it to use
useSelect
. However, I have checked other KM widgets, and they use theuseInViewSelect
selector, which needs to be addressed.