diff --git a/assets/js/components/KeyMetrics/MetricTileTable.js b/assets/js/components/KeyMetrics/MetricTileTable.js
index f10ca5b4058..ded66071212 100644
--- a/assets/js/components/KeyMetrics/MetricTileTable.js
+++ b/assets/js/components/KeyMetrics/MetricTileTable.js
@@ -34,7 +34,7 @@ export default function MetricTileTable( props ) {
rows = [],
columns = [],
limit,
- zeroState: ZeroState,
+ ZeroState,
} = props;
let tileBody = null;
@@ -114,5 +114,5 @@ MetricTileTable.propTypes = {
rows: PropTypes.array,
columns: PropTypes.array,
limit: PropTypes.number,
- zeroState: PropTypes.func,
+ ZeroState: PropTypes.elementType,
};
diff --git a/assets/js/components/KeyMetrics/MetricTileTable.stories.js b/assets/js/components/KeyMetrics/MetricTileTable.stories.js
index 5fec00a96fe..af7119ad159 100644
--- a/assets/js/components/KeyMetrics/MetricTileTable.stories.js
+++ b/assets/js/components/KeyMetrics/MetricTileTable.stories.js
@@ -70,7 +70,7 @@ ZeroData.args = {
title,
rows: [],
columns,
- zeroState: () => 'No data available',
+ ZeroState: () =>
No data available
,
};
ZeroData.scenario = {
label: 'KeyMetrics/MetricTileTable/ZeroData',
diff --git a/assets/js/components/KeyMetrics/MetricTileTablePlainText.js b/assets/js/components/KeyMetrics/MetricTileTablePlainText.js
new file mode 100644
index 00000000000..6840301cd6d
--- /dev/null
+++ b/assets/js/components/KeyMetrics/MetricTileTablePlainText.js
@@ -0,0 +1,34 @@
+/**
+ * MetricTileTablePlainText component.
+ *
+ * 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.
+ */
+
+/**
+ * External dependencies
+ */
+import PropTypes from 'prop-types';
+
+export default function MetricTileTablePlainText( { content } ) {
+ return (
+
+ { content }
+
+ );
+}
+
+MetricTileTablePlainText.propTypes = {
+ content: PropTypes.string.isRequired,
+};
diff --git a/assets/js/components/KeyMetrics/index.js b/assets/js/components/KeyMetrics/index.js
index 6bf83cfce3a..99eba3a72f6 100644
--- a/assets/js/components/KeyMetrics/index.js
+++ b/assets/js/components/KeyMetrics/index.js
@@ -17,4 +17,5 @@
export { default as KeyMetricsSetupCTAWidget } from './KeyMetricsSetupCTAWidget';
export { default as MetricTileNumeric } from './MetricTileNumeric';
export { default as MetricTileTable } from './MetricTileTable';
+export { default as MetricTileTablePlainText } from './MetricTileTablePlainText';
export { default as MetricTileText } from './MetricTileText';
diff --git a/assets/js/modules/analytics-4/components/widgets/PopularContentWidget.js b/assets/js/modules/analytics-4/components/widgets/PopularContentWidget.js
index e173c274860..497e59b141e 100644
--- a/assets/js/modules/analytics-4/components/widgets/PopularContentWidget.js
+++ b/assets/js/modules/analytics-4/components/widgets/PopularContentWidget.js
@@ -119,7 +119,7 @@ export default function PopularContentWidget( props ) {
loading={ loading }
rows={ rows }
columns={ columns }
- zeroState={ ZeroDataMessage }
+ ZeroState={ ZeroDataMessage }
/>
);
}
diff --git a/assets/js/modules/analytics-4/components/widgets/TopCitiesWidget.js b/assets/js/modules/analytics-4/components/widgets/TopCitiesWidget.js
index 7e5dd3c788a..7443c8ca0f6 100644
--- a/assets/js/modules/analytics-4/components/widgets/TopCitiesWidget.js
+++ b/assets/js/modules/analytics-4/components/widgets/TopCitiesWidget.js
@@ -21,15 +21,100 @@
*/
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,
+ MetricTileTablePlainText,
+} 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 = useSelect(
+ ( select ) =>
+ ! select( MODULES_ANALYTICS_4 ).hasFinishedResolution(
+ 'getReport',
+ [ topcCitiesReportOptions ]
+ )
+ );
+
+ const { rows = [], totals = [] } = topCitiesReport || {};
+
+ const totalUsers = totals?.[ 0 ]?.metricValues?.[ 0 ]?.value;
+
+ const columns = [
+ {
+ field: 'dimensionValues',
+ Component: ( { fieldValue } ) => {
+ const [ title ] = fieldValue;
+
+ return ;
+ },
+ },
+ {
+ field: 'metricValues.0.value',
+ Component: ( { fieldValue } ) => (
+
+ { numFmt( fieldValue / totalUsers, {
+ style: 'percent',
+ maximumFractionDigits: 1,
+ } ) }
+
+ ),
+ },
+ ];
+
return (
-
- TODO: UI for TopCitiesWidget
-
+
);
}
TopCitiesWidget.propTypes = {
Widget: PropTypes.elementType.isRequired,
- WidgetNull: PropTypes.elementType.isRequired,
};
diff --git a/assets/js/modules/analytics-4/components/widgets/TopCitiesWidget.stories.js b/assets/js/modules/analytics-4/components/widgets/TopCitiesWidget.stories.js
new file mode 100644
index 00000000000..4057ae63a0d
--- /dev/null
+++ b/assets/js/modules/analytics-4/components/widgets/TopCitiesWidget.stories.js
@@ -0,0 +1,143 @@
+/**
+ * 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 } ) => (
+
+
+
+);
+
+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',
+};
+
+export const Loading = Template.bind( {} );
+Loading.storyName = 'Loading';
+Loading.args = {
+ setupRegistry: ( { dispatch } ) => {
+ dispatch( MODULES_ANALYTICS_4 ).startResolution( 'getReport', [
+ reportOptions,
+ ] );
+ },
+};
+
+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',
+};
+
+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 (
+
+
+
+ );
+ },
+ ],
+};
diff --git a/assets/js/modules/analytics-4/components/widgets/TopCitiesWidget.test.js b/assets/js/modules/analytics-4/components/widgets/TopCitiesWidget.test.js
new file mode 100644
index 00000000000..ae4054ed7a9
--- /dev/null
+++ b/assets/js/modules/analytics-4/components/widgets/TopCitiesWidget.test.js
@@ -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(
+ ,
+ {
+ 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();
+ } );
+} );
diff --git a/assets/js/modules/analytics-4/components/widgets/__snapshots__/TopCitiesWidget.test.js.snap b/assets/js/modules/analytics-4/components/widgets/__snapshots__/TopCitiesWidget.test.js.snap
new file mode 100644
index 00000000000..4389079dfac
--- /dev/null
+++ b/assets/js/modules/analytics-4/components/widgets/__snapshots__/TopCitiesWidget.test.js.snap
@@ -0,0 +1,91 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TopCitiesWidget renders correctly with the expected metrics 1`] = `
+
+`;
diff --git a/assets/js/modules/analytics-4/utils/data-mock.js b/assets/js/modules/analytics-4/utils/data-mock.js
index 90cafb6313e..e848b30b5c4 100644
--- a/assets/js/modules/analytics-4/utils/data-mock.js
+++ b/assets/js/modules/analytics-4/utils/data-mock.js
@@ -73,6 +73,15 @@ const ANALYTICS_4_DIMENSION_OPTIONS = {
'Italy',
'Mexico',
],
+ city: [
+ 'Dublin',
+ '(not set)',
+ 'Cork',
+ 'New York',
+ 'London',
+ 'Los Angeles',
+ 'San Francisco',
+ ],
deviceCategory: [ 'Desktop', 'Tablet', 'Mobile' ],
pageTitle: ( i ) => ( i <= 12 ? `Test Post ${ i }` : false ),
pagePath: ( i ) => ( i <= 12 ? `/test-post-${ i }/` : false ),
diff --git a/assets/js/modules/search-console/components/widgets/PopularKeywordsWidget.js b/assets/js/modules/search-console/components/widgets/PopularKeywordsWidget.js
index ecb7a86e67a..6926903bc6c 100644
--- a/assets/js/modules/search-console/components/widgets/PopularKeywordsWidget.js
+++ b/assets/js/modules/search-console/components/widgets/PopularKeywordsWidget.js
@@ -126,7 +126,7 @@ export default function PopularKeywordsWidget( { Widget } ) {
loading={ loading }
rows={ rows }
columns={ columns }
- zeroState={ ZeroDataMessage }
+ ZeroState={ ZeroDataMessage }
limit={ 3 }
/>
);
diff --git a/assets/sass/components/key-metrics/_googlesitekit-km-widget-tile.scss b/assets/sass/components/key-metrics/_googlesitekit-km-widget-tile.scss
index 2bd22239ef2..cb98b9f0db2 100644
--- a/assets/sass/components/key-metrics/_googlesitekit-km-widget-tile.scss
+++ b/assets/sass/components/key-metrics/_googlesitekit-km-widget-tile.scss
@@ -113,5 +113,13 @@
font-weight: $fw-medium;
}
}
+
+ .googlesitekit-km-widget-tile__table-plain-text {
+ color: $c-surfaces-on-surface;
+ font-size: $fs-body-sm;
+ letter-spacing: $ls-xs;
+ line-height: $lh-body-sm;
+ margin: 0;
+ }
}
}
diff --git a/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_Ready_0_document_0_small.png b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_Ready_0_document_0_small.png
new file mode 100644
index 00000000000..7d88ce7727c
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_Ready_0_document_0_small.png differ
diff --git a/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_Ready_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_Ready_0_document_1_medium.png
new file mode 100644
index 00000000000..9221883c562
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_Ready_0_document_1_medium.png differ
diff --git a/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_Ready_0_document_2_large.png b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_Ready_0_document_2_large.png
new file mode 100644
index 00000000000..6440c34a0ad
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_Ready_0_document_2_large.png differ
diff --git a/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_ZeroData_0_document_0_small.png b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_ZeroData_0_document_0_small.png
new file mode 100644
index 00000000000..a632e52bbb8
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_ZeroData_0_document_0_small.png differ
diff --git a/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_ZeroData_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_ZeroData_0_document_1_medium.png
new file mode 100644
index 00000000000..bcebd4f6aad
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_ZeroData_0_document_1_medium.png differ
diff --git a/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_ZeroData_0_document_2_large.png b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_ZeroData_0_document_2_large.png
new file mode 100644
index 00000000000..065050dfad2
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_KeyMetrics_TopCitiesWidget_ZeroData_0_document_2_large.png differ