diff --git a/packages/marketplace/package.json b/packages/marketplace/package.json index ea503000a6..7958ef1272 100644 --- a/packages/marketplace/package.json +++ b/packages/marketplace/package.json @@ -38,4 +38,4 @@ "last 2 versions", "not ie <= 10" ] -} +} \ No newline at end of file diff --git a/packages/marketplace/src/actions/__tests__/app-usage-stats.ts b/packages/marketplace/src/actions/__tests__/app-usage-stats.ts new file mode 100644 index 0000000000..e2cb9209e5 --- /dev/null +++ b/packages/marketplace/src/actions/__tests__/app-usage-stats.ts @@ -0,0 +1,17 @@ +import { appUsageStatsReceiveData, appUsageStatsRequestData, appUsageStatsRequestDataFailure } from '../app-usage-stats' +import ActionTypes from '@/constants/action-types' +import { usageStatsDataStub } from '@/sagas/__stubs__/app-usage-stats' + +describe('app-usage-stats actions', () => { + it('should create a appUsageStatsRequestData action', () => { + expect(appUsageStatsRequestData.type).toEqual(ActionTypes.APP_USAGE_STATS_REQUEST_DATA) + expect(appUsageStatsRequestData({ appId: ['1'] }).data).toEqual({ appId: ['1'] }) + }) + it('should create a appUsageStatsReceiveData action', () => { + expect(appUsageStatsReceiveData.type).toEqual(ActionTypes.APP_USAGE_STATS_RECEIVE_DATA) + expect(appUsageStatsReceiveData(usageStatsDataStub).data).toEqual(usageStatsDataStub) + }) + it('should create a appUsageStatsRequestDataFailure action', () => { + expect(appUsageStatsRequestDataFailure.type).toEqual(ActionTypes.APP_USAGE_STATS_REQUEST_DATA_FAILURE) + }) +}) diff --git a/packages/marketplace/src/actions/app-usage-stats.ts b/packages/marketplace/src/actions/app-usage-stats.ts new file mode 100644 index 0000000000..7b2b8b727e --- /dev/null +++ b/packages/marketplace/src/actions/app-usage-stats.ts @@ -0,0 +1,14 @@ +import { actionCreator } from '../utils/actions' +import ActionTypes from '../constants/action-types' + +const { APP_USAGE_STATS_REQUEST_DATA, APP_USAGE_STATS_RECEIVE_DATA, APP_USAGE_STATS_REQUEST_DATA_FAILURE } = ActionTypes + +export interface AppUsageStatsParams { + appId?: string[] + dateFrom?: string + dateTo?: string +} + +export const appUsageStatsRequestData = actionCreator(APP_USAGE_STATS_REQUEST_DATA) +export const appUsageStatsReceiveData = actionCreator(APP_USAGE_STATS_RECEIVE_DATA) +export const appUsageStatsRequestDataFailure = actionCreator(APP_USAGE_STATS_REQUEST_DATA_FAILURE) diff --git a/packages/marketplace/src/components/pages/__tests__/__snapshots__/admin-apps.tsx.snap b/packages/marketplace/src/components/pages/__tests__/__snapshots__/admin-apps.tsx.snap index e13d0c9dff..b3906d0ee2 100644 --- a/packages/marketplace/src/components/pages/__tests__/__snapshots__/admin-apps.tsx.snap +++ b/packages/marketplace/src/components/pages/__tests__/__snapshots__/admin-apps.tsx.snap @@ -126,6 +126,7 @@ exports[`AdminApps should match a snapshot when LOADING false 1`] = ` data={ Array [ Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://google.com/abc", @@ -147,6 +148,7 @@ exports[`AdminApps should match a snapshot when LOADING false 1`] = ` "summary": "nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsicr8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz", }, Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://www.contoso.com/path", @@ -266,6 +268,7 @@ exports[`AdminApps should match a snapshot when LOADING true 1`] = ` data={ Array [ Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://google.com/abc", @@ -287,6 +290,7 @@ exports[`AdminApps should match a snapshot when LOADING true 1`] = ` "summary": "nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsicr8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz", }, Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://www.contoso.com/path", diff --git a/packages/marketplace/src/components/pages/__tests__/__snapshots__/analytics.tsx.snap b/packages/marketplace/src/components/pages/__tests__/__snapshots__/analytics.tsx.snap index 68910cbf11..723ac422b0 100644 --- a/packages/marketplace/src/components/pages/__tests__/__snapshots__/analytics.tsx.snap +++ b/packages/marketplace/src/components/pages/__tests__/__snapshots__/analytics.tsx.snap @@ -39,12 +39,128 @@ exports[`AnalyticsPage should match snapshot 1`] = ` }, ] } + loading={false} /> - + + `; -exports[`AnalyticsPage should match when loading 1`] = ``; +exports[`AnalyticsPage should match when loading 1`] = ` + + + + Dashboard + +
+ + + + + + + + + + + +
+`; exports[`InstallationTable should match snapshot 1`] = `
diff --git a/packages/marketplace/src/components/pages/__tests__/__snapshots__/client.tsx.snap b/packages/marketplace/src/components/pages/__tests__/__snapshots__/client.tsx.snap index ed87977a7a..8e90363876 100644 --- a/packages/marketplace/src/components/pages/__tests__/__snapshots__/client.tsx.snap +++ b/packages/marketplace/src/components/pages/__tests__/__snapshots__/client.tsx.snap @@ -13,6 +13,7 @@ exports[`Client should match a snapshot when LOADING false 1`] = ` Object { "data": Array [ Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://google.com/abc", @@ -34,6 +35,7 @@ exports[`Client should match a snapshot when LOADING false 1`] = ` "summary": "nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsicr8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz", }, Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://www.contoso.com/path", @@ -118,6 +120,7 @@ exports[`Client should match a snapshot when featured apps is empty [] 1`] = ` Object { "data": Array [ Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://google.com/abc", @@ -139,6 +142,7 @@ exports[`Client should match a snapshot when featured apps is empty [] 1`] = ` "summary": "nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsicr8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz", }, Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://www.contoso.com/path", @@ -204,6 +208,7 @@ exports[`Client should match a snapshot when featured apps is undefined 1`] = ` Object { "data": Array [ Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://google.com/abc", @@ -225,6 +230,7 @@ exports[`Client should match a snapshot when featured apps is undefined 1`] = ` "summary": "nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsicr8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz", }, Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://www.contoso.com/path", diff --git a/packages/marketplace/src/components/pages/__tests__/__snapshots__/developer-home.tsx.snap b/packages/marketplace/src/components/pages/__tests__/__snapshots__/developer-home.tsx.snap index 527d041d76..46ba7e35eb 100644 --- a/packages/marketplace/src/components/pages/__tests__/__snapshots__/developer-home.tsx.snap +++ b/packages/marketplace/src/components/pages/__tests__/__snapshots__/developer-home.tsx.snap @@ -10,6 +10,7 @@ exports[`DeveloperHome should match a snapshot 1`] = ` list={ Array [ Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://google.com/abc", @@ -31,6 +32,7 @@ exports[`DeveloperHome should match a snapshot 1`] = ` "summary": "nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsicr8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz", }, Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://www.contoso.com/path", @@ -87,6 +89,7 @@ exports[`DeveloperHome should match a snapshot 3`] = ` list={ Array [ Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://google.com/abc", @@ -108,6 +111,7 @@ exports[`DeveloperHome should match a snapshot 3`] = ` "summary": "nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsicr8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz", }, Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://www.contoso.com/path", diff --git a/packages/marketplace/src/components/pages/__tests__/__snapshots__/installed-apps.tsx.snap b/packages/marketplace/src/components/pages/__tests__/__snapshots__/installed-apps.tsx.snap index d822586f70..6ccfca5c8c 100644 --- a/packages/marketplace/src/components/pages/__tests__/__snapshots__/installed-apps.tsx.snap +++ b/packages/marketplace/src/components/pages/__tests__/__snapshots__/installed-apps.tsx.snap @@ -7,6 +7,7 @@ exports[`InstalledApps should match a snapshot when LOADING false 1`] = ` list={ Array [ Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://google.com/abc", @@ -28,6 +29,7 @@ exports[`InstalledApps should match a snapshot when LOADING false 1`] = ` "summary": "nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsicr8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz", }, Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://www.contoso.com/path", diff --git a/packages/marketplace/src/components/pages/__tests__/__snapshots__/my-apps.tsx.snap b/packages/marketplace/src/components/pages/__tests__/__snapshots__/my-apps.tsx.snap index 8e01f5384e..026fb66f84 100644 --- a/packages/marketplace/src/components/pages/__tests__/__snapshots__/my-apps.tsx.snap +++ b/packages/marketplace/src/components/pages/__tests__/__snapshots__/my-apps.tsx.snap @@ -7,6 +7,7 @@ exports[`MyApps should match a snapshot when LOADING false 1`] = ` list={ Array [ Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://google.com/abc", @@ -28,6 +29,7 @@ exports[`MyApps should match a snapshot when LOADING false 1`] = ` "summary": "nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsicr8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz", }, Object { + "created": "2020-02-02T10:45:57", "developer": "Pete's Proptech World Ltd", "developerId": "28c9ea52-7f73-4814-9e00-4e3714b8adeb", "homePage": "http://www.contoso.com/path", diff --git a/packages/marketplace/src/components/pages/__tests__/analytics.tsx b/packages/marketplace/src/components/pages/__tests__/analytics.tsx index a0b4b35b09..5b48c72ec8 100644 --- a/packages/marketplace/src/components/pages/__tests__/analytics.tsx +++ b/packages/marketplace/src/components/pages/__tests__/analytics.tsx @@ -18,6 +18,7 @@ import { appsDataStub } from '@/sagas/__stubs__/apps' import { ReduxState } from '@/types/core' import { DeveloperState } from '@/reducers/developer' import { AppInstallationsState } from '@/reducers/app-installations' +import { AppUsageStatsState } from '@/reducers/app-usage-stats' jest.mock('@reapit/elements', () => ({ ...jest.requireActual('@reapit/elements'), @@ -37,23 +38,40 @@ const installations = { }, } as AppInstallationsState +const appUsageStats: AppUsageStatsState = { + loading: false, + appUsageStatsData: {}, +} + describe('AnalyticsPage', () => { it('should match snapshot', () => { - expect(shallow()).toMatchSnapshot() + expect( + shallow(), + ).toMatchSnapshot() }) it('should match when loading', () => { const installationsLoading = { ...installations, loading: true } const developerLoading = { ...developer, loading: true } expect( - shallow(), + shallow( + , + ), ).toMatchSnapshot() }) }) describe('mapStateToProps', () => { it('should return correct value', () => { - expect(mapStateToProps({ installations, developer } as ReduxState)).toEqual({ installations, developer }) + expect(mapStateToProps({ installations, developer, appUsageStats } as ReduxState)).toEqual({ + installations, + developer, + appUsageStats, + }) }) }) diff --git a/packages/marketplace/src/components/pages/analytics.tsx b/packages/marketplace/src/components/pages/analytics.tsx index 28f3dd5f92..b508a68b31 100644 --- a/packages/marketplace/src/components/pages/analytics.tsx +++ b/packages/marketplace/src/components/pages/analytics.tsx @@ -11,6 +11,8 @@ import { AppInstallationsState } from '@/reducers/app-installations' import { INSTALLATIONS_PER_PAGE } from '@/constants/paginator' import { withRouter } from 'react-router' import styles from '@/styles/pages/analytics.scss?mod' +import DeveloperTrafficTable from '../ui/developer-traffic-table' +import { AppUsageStatsState } from '@/reducers/app-usage-stats' export const installationTableColumn = [ { Header: 'App Name', accessor: 'appName' }, @@ -33,6 +35,7 @@ export const installationTableColumn = [ export interface AnalyticsPageMappedProps { developer: DeveloperState installations: AppInstallationsState + appUsageStats: AppUsageStatsState } export interface AnalyticsPageMappedActions { @@ -136,7 +139,8 @@ export const InstallationTable: React.FC<{ installedApps: InstallationModelWithAppName[] installations: AppInstallationsState developer: DeveloperState -}> = ({ installedApps, installations, developer }) => { + loading?: boolean +}> = ({ installedApps, installations, developer, loading }) => { const [pageNumber, setPageNumber] = React.useState(1) const installationAppDataArray = installations.installationsAppData?.data ?? [] @@ -152,34 +156,47 @@ export const InstallationTable: React.FC<{ ]) return (
-

Installations

-

- The installations table below shows the individual installations per client with a total number of installations - per app -

-
- {Object.entries(appCountEntries).map(([appName, count]) => ( -

- Total current installation for {appName}: {count} + {loading ? ( + + ) : ( + <> +

Installations

+

+ The installations table below shows the individual installations per client with a total number of + installations per app

- ))} -
- -
- +
+ {Object.entries(appCountEntries).map(([appName, count]) => ( +

+ Total current installation for {appName}: {count} +

+ ))} +
+
+
+ + + )} ) } -export const AnalyticsPage: React.FC = ({ installations, developer }) => { - if (installations.loading || !installations.installationsAppData || developer.loading || !developer.developerData) { - return - } +export const AnalyticsPage: React.FC = ({ installations, developer, appUsageStats }) => { + // if ( + // installations.loading || + // !installations.installationsAppData || + // developer.loading || + // !developer.developerData || + // appUsageStats.loading || + // !appUsageStats.appUsageStatsData + // ) { + // return + // } const installationAppDataArray = installations.installationsAppData?.data ?? [] const developerDataArray = developer.developerData?.data?.data ?? [] @@ -189,6 +206,11 @@ export const AnalyticsPage: React.FC = ({ installations, dev [installationAppDataArray, developerDataArray], ) + const appUsageStatsLoading = appUsageStats.loading + const appUsageStatsData = appUsageStats.appUsageStatsData || {} + const developerAppsData = developer?.developerData?.data || {} + const installationsAppLoading = installations.loading + return ( @@ -196,16 +218,18 @@ export const AnalyticsPage: React.FC = ({ installations, dev
- + - + +
@@ -215,6 +239,7 @@ export const AnalyticsPage: React.FC = ({ installations, dev export const mapStateToProps: (state: ReduxState) => AnalyticsPageMappedProps = state => ({ installations: state.installations, developer: state.developer, + appUsageStats: state.appUsageStats, }) export default withRouter(connect(mapStateToProps)(AnalyticsPage)) diff --git a/packages/marketplace/src/components/ui/__tests__/__snapshots__/app-list.tsx.snap b/packages/marketplace/src/components/ui/__tests__/__snapshots__/app-list.tsx.snap index 2c9ef9e03f..f0a38af0a2 100644 --- a/packages/marketplace/src/components/ui/__tests__/__snapshots__/app-list.tsx.snap +++ b/packages/marketplace/src/components/ui/__tests__/__snapshots__/app-list.tsx.snap @@ -18,6 +18,7 @@ exports[`AppList should match a snapshot 1`] = ` +
+ + Traffic (API Count) + + +
+ +`; diff --git a/packages/marketplace/src/components/ui/__tests__/__snapshots__/developer-traffic-table.tsx.snap b/packages/marketplace/src/components/ui/__tests__/__snapshots__/developer-traffic-table.tsx.snap new file mode 100644 index 0000000000..64fc486521 --- /dev/null +++ b/packages/marketplace/src/components/ui/__tests__/__snapshots__/developer-traffic-table.tsx.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DeveloperTrafficTable should match a snapshot 1`] = ` +
+ + Traffic + +

+ The traffic table below shows all API calls made against each of your applications since the date your app was created +

+ +
+`; diff --git a/packages/marketplace/src/components/ui/__tests__/__snapshots__/installed-app-list.tsx.snap b/packages/marketplace/src/components/ui/__tests__/__snapshots__/installed-app-list.tsx.snap index f65de28b8e..2316936cfa 100644 --- a/packages/marketplace/src/components/ui/__tests__/__snapshots__/installed-app-list.tsx.snap +++ b/packages/marketplace/src/components/ui/__tests__/__snapshots__/installed-app-list.tsx.snap @@ -11,6 +11,7 @@ exports[`InstalledAppList should match a snappshot ListDesktopScreen 1`] = ` { + it('should match a snapshot', () => { + expect(shallow()).toMatchSnapshot() + }) + + describe('getAppUsageStatsChartData', () => { + it('should run correctly', () => { + const { stats, apps } = props + const result = getAppUsageStatsChartData(stats.appUsage, apps.data) + const expected = { + labels: ['15/11/2019'], + data: [5], + appUsageStatsGroupedByDate: { + '15/11/2019': { + '09043eb8-9e5e-4650-b7f1-f0cb62699027': { appName: 'test', requests: 5 }, + date: new Date('2019-11-15T00:00:00+00:00'), + totalRequests: 5, + }, + }, + } + expect(result).toEqual(expected) + }) + + describe('getChartOptions', () => { + it('should run correctly', () => { + const { stats, apps } = props + const result = getAppUsageStatsChartData(stats.appUsage, apps.data) + const options = getChartOptions(result?.appUsageStatsGroupedByDate) + expect(options.tooltips).not.toBeNull() + }) + }) + + describe('getChartConfig', () => { + it('should run correctly', () => { + const { stats, apps } = props + const result = getAppUsageStatsChartData(stats.appUsage, apps.data) + const configs = getChartConfig(result?.labels, result?.data) + expect(configs).not.toBeNull() + }) + }) + }) +}) diff --git a/packages/marketplace/src/components/ui/__tests__/developer-traffic-table.tsx b/packages/marketplace/src/components/ui/__tests__/developer-traffic-table.tsx new file mode 100644 index 0000000000..c6525e7537 --- /dev/null +++ b/packages/marketplace/src/components/ui/__tests__/developer-traffic-table.tsx @@ -0,0 +1,44 @@ +import * as React from 'react' +import { shallow } from 'enzyme' +import DeveloperTrafficTable, { + DeveloperAppTrafficProps, + generateUsageStatsData, + AppUsageStats, +} from '../developer-traffic-table' +import { appsDataStub } from '@/sagas/__stubs__/apps' +import { usageStatsDataStub } from '@/sagas/__stubs__/app-usage-stats' + +const props: DeveloperAppTrafficProps = { + apps: appsDataStub.data, + stats: usageStatsDataStub, +} + +describe('DeveloperTrafficTable', () => { + it('should match a snapshot', () => { + expect(shallow()).toMatchSnapshot() + }) + + describe('generateUsageStatsData', () => { + it('should run correctly', () => { + const props = { + apps: appsDataStub.data, + stats: usageStatsDataStub, + } + const result = generateUsageStatsData(props)() + const expected: AppUsageStats[] = [ + { appName: 'test', created: '2020-02-02T10:45:57', requests: 5 }, + { appName: 'asd', created: '2020-02-02T10:45:57', requests: 0 }, + ] + expect(result).toEqual(expected) + }) + + it('should run correctly when not found apps data', () => { + const props: DeveloperAppTrafficProps = { + apps: {}, + stats: {}, + } + const result = generateUsageStatsData(props)() + expect(result).toEqual(undefined) + }) + }) +}) diff --git a/packages/marketplace/src/components/ui/developer-installations-chart.tsx b/packages/marketplace/src/components/ui/developer-installations-chart.tsx index 004efc43c0..d38af4194a 100644 --- a/packages/marketplace/src/components/ui/developer-installations-chart.tsx +++ b/packages/marketplace/src/components/ui/developer-installations-chart.tsx @@ -1,11 +1,12 @@ import * as React from 'react' import { Line } from 'react-chartjs-2' -import { H4 } from '@reapit/elements' +import { H4, Loader } from '@reapit/elements' import { InstallationModelWithAppName } from '@/components/pages/analytics' import { groupInstalledAppsByDate, getChartData, groupAppsByNameAndCount } from '@/utils/developer-analytics' export interface DeveloperInstallationsChartProps { data: Array + loading?: boolean } export const getChartOptions = data => { @@ -27,7 +28,7 @@ export const getChartOptions = data => { } } -const DeveloperInstallationsChart = ({ data }: DeveloperInstallationsChartProps) => { +const DeveloperInstallationsChart = ({ data, loading }: DeveloperInstallationsChartProps) => { const grouppedAppsByDate = groupInstalledAppsByDate(data) const labels = Object.keys(grouppedAppsByDate) @@ -59,8 +60,14 @@ const DeveloperInstallationsChart = ({ data }: DeveloperInstallationsChartProps) return (
-

Installations

- + {loading ? ( + + ) : ( + <> +

Installations

+ + + )}
) } diff --git a/packages/marketplace/src/components/ui/developer-traffic-chart.tsx b/packages/marketplace/src/components/ui/developer-traffic-chart.tsx index c9042fdd56..f7008be324 100644 --- a/packages/marketplace/src/components/ui/developer-traffic-chart.tsx +++ b/packages/marketplace/src/components/ui/developer-traffic-chart.tsx @@ -1,41 +1,35 @@ import * as React from 'react' +import { H4, Alert, Loader } from '@reapit/elements' import { Line } from 'react-chartjs-2' -import { H4 } from '@reapit/elements' +import { UsageStatsModel, PagedResultAppSummaryModel_ } from '@reapit/foundations-ts-definitions' +import { getAppUsageStatsChartData, getChartConfig, getChartOptions } from '@/utils/app-usage-stats.ts' -export interface DeveloperTrafficChartProps {} - -const data = { - labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], - datasets: [ - { - label: 'My First dataset', - fill: false, - lineTension: 0.1, - backgroundColor: 'rgba(75,192,192,0.4)', - borderColor: 'rgba(75,192,192,1)', - borderCapStyle: 'butt', - borderDash: [], - borderDashOffset: 0.0, - borderJoinStyle: 'miter', - pointBorderColor: 'rgba(75,192,192,1)', - pointBackgroundColor: '#fff', - pointBorderWidth: 1, - pointHoverRadius: 5, - pointHoverBackgroundColor: 'rgba(75,192,192,1)', - pointHoverBorderColor: 'rgba(220,220,220,1)', - pointHoverBorderWidth: 2, - pointRadius: 1, - pointHitRadius: 10, - data: [65, 59, 80, 81, 23, 55, 40], - }, - ], +export type DeveloperTrafficChartProps = { + stats: UsageStatsModel + apps: PagedResultAppSummaryModel_ + loading?: Boolean | false } -const DeveloperTrafficChart = () => { +export const DeveloperTrafficChart: React.FC = ({ stats, apps, loading }) => { + const { appUsage } = stats || {} + const appUsageStatsChartData = getAppUsageStatsChartData(appUsage, apps.data) + if (!appUsageStatsChartData) { + return + } + + const { labels, data, appUsageStatsGroupedByDate } = appUsageStatsChartData + const chartData = getChartConfig(labels, data) + const chartOptions = getChartOptions(appUsageStatsGroupedByDate) return (
-

Traffic (API Count)

- + {loading ? ( + + ) : ( +
+

Traffic (API Count)

+ +
+ )}
) } diff --git a/packages/marketplace/src/components/ui/developer-traffic-table.tsx b/packages/marketplace/src/components/ui/developer-traffic-table.tsx new file mode 100644 index 0000000000..85b47c4010 --- /dev/null +++ b/packages/marketplace/src/components/ui/developer-traffic-table.tsx @@ -0,0 +1,72 @@ +import React, { useMemo } from 'react' +import { UsageStatsModel, PagedResultAppSummaryModel_, AppUsageStatsModel } from '@reapit/foundations-ts-definitions' +import { H4, Table, toLocalTime, Alert, Loader } from '@reapit/elements' + +export interface DeveloperAppTrafficProps { + stats: UsageStatsModel + apps: PagedResultAppSummaryModel_ + loading?: Boolean +} + +export interface AppUsageStats { + appName?: string + created?: string + requests?: number +} + +export const generateUsageStatsData = ({ apps, stats }: DeveloperAppTrafficProps) => () => { + return apps.data?.reduce((prev, app) => { + const appUsage = stats.appUsage?.find((item: AppUsageStatsModel) => item.appId === app.id) + const result = { + appName: app.name, + created: app.created, + requests: appUsage ? appUsage.requestsForPeriod : 0, + } + return [...prev, result] + }, []) +} + +export const generateUsageStatsColumns = () => () => { + return [ + { + Header: 'App Name', + accessor: 'appName', + }, + + { + Header: 'Date Created', + accessor: row => toLocalTime(row.created, 'DD/MM/YYYY'), + }, + { + Header: 'Total API Calls', + accessor: 'requests', + }, + ] +} + +const DeveloperTrafficTable: React.FC = ({ stats, apps, loading }) => { + const usageStatsData = useMemo(generateUsageStatsData({ apps, stats }), [stats, apps]) + const usageStatsColumns = useMemo(generateUsageStatsColumns(), [usageStatsData]) + return ( +
+ {loading ? ( + + ) : ( + <> +

Traffic

+

+ The traffic table below shows all API calls made against each of your applications since the date your app + was created +

+ {usageStatsData && usageStatsData.length > 0 ? ( +
+ ) : ( + + )} + + )} + + ) +} + +export default DeveloperTrafficTable diff --git a/packages/marketplace/src/constants/action-types.ts b/packages/marketplace/src/constants/action-types.ts index 394fe0d915..09da50186f 100644 --- a/packages/marketplace/src/constants/action-types.ts +++ b/packages/marketplace/src/constants/action-types.ts @@ -51,6 +51,13 @@ const ActionTypes = { INSTALLED_APPS_RECEIVE_DATA: 'INSTALLED_APPS_RECEIVE_DATA', INSTALLED_APPS_CLEAR_DATA: 'INSTALLED_APPS_CLEAR_DATA', + // app statistics actions + APP_USAGE_STATS_REQUEST_DATA: 'APP_USAGE_STATS_REQUEST_DATA', + APP_USAGE_STATS_LOADING: 'APP_USAGE_STATS_LOADING', + APP_USAGE_STATS_REQUEST_DATA_FAILURE: 'APP_USAGE_STATS_REQUEST_DATA_FAILURE', + APP_USAGE_STATS_RECEIVE_DATA: 'APP_USAGE_STATS_RECEIVE_DATA', + APP_USAGE_STATS_CLEAR_DATA: 'APP_USAGE_STATS_CLEAR_DATA', + // My apps actions MY_APPS_REQUEST_DATA: 'MY_APPS_REQUEST_DATA', MY_APPS_LOADING: 'MY_APPS_LOADING', diff --git a/packages/marketplace/src/constants/api.ts b/packages/marketplace/src/constants/api.ts index 54c1fc74ed..671ddb8189 100644 --- a/packages/marketplace/src/constants/api.ts +++ b/packages/marketplace/src/constants/api.ts @@ -21,4 +21,5 @@ export const URLS = { scopes: '/scopes', categories: '/categories', docs: '/docs', + statistics: '/statistics', } diff --git a/packages/marketplace/src/core/store.ts b/packages/marketplace/src/core/store.ts index d862ae0623..1a4c4190f0 100644 --- a/packages/marketplace/src/core/store.ts +++ b/packages/marketplace/src/core/store.ts @@ -1,4 +1,8 @@ import { createStore, applyMiddleware, compose, combineReducers, Store as ReduxStore, Dispatch } from 'redux' +import createSagaMiddleware from 'redux-saga' +import { fork, all } from '@redux-saga/core/effects' +import { ReduxState } from '../types/core' + import auth from '../reducers/auth' import client from '../reducers/client' import installedApps from '../reducers/installed-apps' @@ -18,13 +22,14 @@ import appCategories from '../reducers/app-categories' import settingsReducer from '../reducers/settings' import adminApps from '../reducers/admin-apps' import appInstallationsReducer from '../reducers/app-installations' -import { ReduxState } from '../types/core' -import createSagaMiddleware from 'redux-saga' -import { fork, all } from '@redux-saga/core/effects' +import appUsageStatsReducer from '../reducers/app-usage-stats' +import adminStatsReducer from '../reducers/admin-stats' + import authSagas from '../sagas/auth' import clientSagas from '../sagas/client' import appDetailSagas from '../sagas/app-detail' import installedAppsSagas from '../sagas/installed-apps' +import appUsageStatsSagas from '../sagas/app-usage-stats' import myAppsSagas from '../sagas/my-apps' import developerSagas from '../sagas/developer' import submitAppSagas from '../sagas/submit-app' @@ -43,7 +48,6 @@ import resetPasswordSagas from '../sagas/reset-password' import appInstallationsSagas from '../sagas/app-installations' import noticationMessage from '../reducers/notification-message' import adminStatsSaga from '../sagas/admin-stats' -import adminStatsReducer from '../reducers/admin-stats' export class Store { static _instance: Store @@ -82,6 +86,7 @@ export class Store { settings: settingsReducer, resetPassword: resetPasswordReducer, installations: appInstallationsReducer, + appUsageStats: appUsageStatsReducer, noticationMessage, adminStats: adminStatsReducer, }) @@ -91,6 +96,7 @@ export class Store { fork(authSagas), fork(clientSagas), fork(installedAppsSagas), + fork(appUsageStatsSagas), fork(myAppsSagas), fork(developerSagas), fork(appDetailSagas), diff --git a/packages/marketplace/src/reducers/__tests__/app-usage-stats.ts b/packages/marketplace/src/reducers/__tests__/app-usage-stats.ts new file mode 100644 index 0000000000..3e1ef9b9e8 --- /dev/null +++ b/packages/marketplace/src/reducers/__tests__/app-usage-stats.ts @@ -0,0 +1,47 @@ +import appUsageStatsReducer, { defaultState } from '../app-usage-stats' +import { ActionType } from '@/types/core' +import ActionTypes from '@/constants/action-types' +import { usageStatsDataStub } from '@/sagas/__stubs__/app-usage-stats' + +describe('app-usage-stats reducer', () => { + it('should return default state if action not matched', () => { + const newState = appUsageStatsReducer(undefined, { type: 'UNKNOWN' as ActionType, data: undefined }) + expect(newState).toEqual(defaultState) + }) + + it('should return loading true when APP_USAGE_STATS_REQUEST_DATA action is called', () => { + const newState = appUsageStatsReducer(defaultState, { + type: ActionTypes.APP_USAGE_STATS_REQUEST_DATA as ActionType, + data: { id: ['123'] }, + }) + const expected = { + ...defaultState, + loading: true, + } + expect(newState).toEqual(expected) + }) + + it('should return installationsAppData when APP_USAGE_STATS_RECEIVE_DATA action is called', () => { + const newState = appUsageStatsReducer(defaultState, { + type: ActionTypes.APP_USAGE_STATS_RECEIVE_DATA as ActionType, + data: usageStatsDataStub, + }) + const expected = { + ...defaultState, + appUsageStatsData: usageStatsDataStub, + } + expect(newState).toEqual(expected) + }) + + it('should return loading false when APP_USAGE_STATS_REQUEST_DATA_FAILURE action is called', () => { + const newState = appUsageStatsReducer(defaultState, { + type: ActionTypes.APP_USAGE_STATS_REQUEST_DATA_FAILURE as ActionType, + data: null, + }) + const expected = { + ...defaultState, + loading: false, + } + expect(newState).toEqual(expected) + }) +}) diff --git a/packages/marketplace/src/reducers/app-usage-stats.ts b/packages/marketplace/src/reducers/app-usage-stats.ts new file mode 100644 index 0000000000..9b235f276f --- /dev/null +++ b/packages/marketplace/src/reducers/app-usage-stats.ts @@ -0,0 +1,36 @@ +import { Action } from '../types/core' +import { isType } from '../utils/actions' +import { UsageStatsModel } from '@reapit/foundations-ts-definitions' +import { + appUsageStatsRequestData, + appUsageStatsReceiveData, + appUsageStatsRequestDataFailure, +} from '@/actions/app-usage-stats' + +export interface AppUsageStatsState { + loading: boolean + appUsageStatsData: UsageStatsModel | null +} + +export const defaultState: AppUsageStatsState = { + loading: false, + appUsageStatsData: null, +} + +const appUsageStatsReducer = (state: AppUsageStatsState = defaultState, action: Action): AppUsageStatsState => { + if (isType(action, appUsageStatsRequestData)) { + return { ...state, loading: true } + } + + if (isType(action, appUsageStatsReceiveData)) { + return { ...state, loading: false, appUsageStatsData: action.data } + } + + if (isType(action, appUsageStatsRequestDataFailure)) { + return { ...state, loading: false } + } + + return state +} + +export default appUsageStatsReducer diff --git a/packages/marketplace/src/sagas/__stubs__/app-usage-stats.ts b/packages/marketplace/src/sagas/__stubs__/app-usage-stats.ts new file mode 100644 index 0000000000..b2bff97978 --- /dev/null +++ b/packages/marketplace/src/sagas/__stubs__/app-usage-stats.ts @@ -0,0 +1,63 @@ +import { UsageStatsModel } from '@reapit/foundations-ts-definitions' + +export const usageStatsDataStub: UsageStatsModel = { + dateFrom: '2019-11-09T00:00:00+00:00', + dateTo: '2020-02-07T00:00:00+00:00', + totalRequestsForPeriod: 5, + appUsage: [ + { + appId: '09043eb8-9e5e-4650-b7f1-f0cb62699027', + requestsForPeriod: 5, + usage: [ + { + date: '2019-11-15T00:00:00+00:00', + requests: 5, + }, + ], + }, + ], +} + +export const usageStatsForMultipleAppsDataStub: UsageStatsModel = { + dateFrom: '2020-02-09T09:18:23.957Z', + dateTo: '2020-02-09T09:18:23.957Z', + totalRequestsForPeriod: 13, + appUsage: [ + { + appId: '09043eb8-9e5e-4650-b7f1-f0cb62699027', + requestsForPeriod: 7, + usage: [ + { + date: '2020-02-09T09:18:23.957Z', + requests: 2, + }, + { + date: '2020-02-01T09:18:23.957Z', + requests: 5, + }, + { + date: '2020-02-05T09:18:23.957Z', + requests: 0, + }, + ], + }, + { + appId: '261da083-cee2-4f5c-a18f-8f9375f1f5af', + requestsForPeriod: 6, + usage: [ + { + date: '2020-02-09T09:18:23.957Z', + requests: 3, + }, + { + date: '2020-02-01T09:18:23.957Z', + requests: 3, + }, + { + date: '2020-02-05T09:18:23.957Z', + requests: 0, + }, + ], + }, + ], +} diff --git a/packages/marketplace/src/sagas/__stubs__/apps.ts b/packages/marketplace/src/sagas/__stubs__/apps.ts index 51d16d11bf..bdaeb97b6d 100644 --- a/packages/marketplace/src/sagas/__stubs__/apps.ts +++ b/packages/marketplace/src/sagas/__stubs__/apps.ts @@ -7,6 +7,7 @@ export const appsDataStub: { data: PagedResultAppSummaryModel_ } = { id: '09043eb8-9e5e-4650-b7f1-f0cb62699027', developerId: '28c9ea52-7f73-4814-9e00-4e3714b8adeb', name: 'test', + created: '2020-02-02T10:45:57', summary: 'nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsic' + 'r8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz', @@ -31,6 +32,7 @@ export const appsDataStub: { data: PagedResultAppSummaryModel_ } = { id: '261da083-cee2-4f5c-a18f-8f9375f1f5af', developerId: '28c9ea52-7f73-4814-9e00-4e3714b8adeb', name: 'asd', + created: '2020-02-02T10:45:57', summary: 'asdQiiAJTmXGxPin7pwUiCsepZWXz1EJS71eGlvgPKH4hpE6J8DRDpzP2kDdOwpQPr4aHCCw' + 'WwxBJwzARLa7wMpJh5J61GhmQjLfKZkcDd47L9WEfQYVYAj0DTPJP0BuUMAAg2', diff --git a/packages/marketplace/src/sagas/__tests__/app-usage-stats.ts b/packages/marketplace/src/sagas/__tests__/app-usage-stats.ts new file mode 100644 index 0000000000..e5a1f9d545 --- /dev/null +++ b/packages/marketplace/src/sagas/__tests__/app-usage-stats.ts @@ -0,0 +1,72 @@ +import appUsageStatsSagas, { appUsageStatsListen, fetchAppUsageStats, appUsageStatsSaga } from '../app-usage-stats' +import { errorThrownServer } from '@/actions/error' +import errorMessages from '@/constants/error-messages' +import ActionTypes from '@/constants/action-types' +import { put, takeLatest, all, fork, call } from '@redux-saga/core/effects' +import { Action, ActionType } from '@/types/core' +import { cloneableGenerator } from '@redux-saga/testing-utils' +import { + AppUsageStatsParams, + appUsageStatsReceiveData, + appUsageStatsRequestDataFailure, +} from '@/actions/app-usage-stats' +import { usageStatsDataStub } from '../__stubs__/app-usage-stats' + +jest.mock('@reapit/elements') + +const params = { + type: 'APP_USAGE_STATS_REQUEST_DATA' as ActionType, + data: { + appId: ['1'], + }, +} + +describe('app-usage-stats sagas', () => { + describe('appUsageStatsSaga', () => { + const gen = cloneableGenerator(appUsageStatsSaga)(params) + expect(gen.next().value).toEqual(call(fetchAppUsageStats, params.data)) + + test('api call success', () => { + const clone = gen.clone() + expect(clone.next(usageStatsDataStub).value).toEqual(put(appUsageStatsReceiveData(usageStatsDataStub))) + }) + + test('api fail sagas', () => { + const clone = gen.clone() + if (clone.throw) { + expect(clone.throw(new Error('')).value).toEqual(put(appUsageStatsRequestDataFailure())) + expect(clone.next().value).toEqual( + put( + errorThrownServer({ + type: 'SERVER', + message: errorMessages.DEFAULT_SERVER_ERROR, + }), + ), + ) + } + expect(clone.next().done).toBe(true) + }) + }) + + describe('app-usage-stats thunks', () => { + describe('appUsageStatsListen', () => { + it('should trigger saga function when called', () => { + const gen = appUsageStatsListen() + expect(gen.next().value).toEqual( + takeLatest>(ActionTypes.APP_USAGE_STATS_REQUEST_DATA, appUsageStatsSaga), + ) + + expect(gen.next().done).toBe(true) + }) + }) + + describe('appUsageStatsSagas', () => { + it('should listen data request', () => { + const gen = appUsageStatsSagas() + + expect(gen.next().value).toEqual(all([fork(appUsageStatsListen)])) + expect(gen.next().done).toBe(true) + }) + }) + }) +}) diff --git a/packages/marketplace/src/sagas/app-usage-stats.ts b/packages/marketplace/src/sagas/app-usage-stats.ts new file mode 100644 index 0000000000..4d8a58c281 --- /dev/null +++ b/packages/marketplace/src/sagas/app-usage-stats.ts @@ -0,0 +1,49 @@ +import { fetcher, setQueryParams } from '@reapit/elements' +import { URLS, MARKETPLACE_HEADERS } from '../constants/api' +import { put, fork, all, call, takeLatest } from '@redux-saga/core/effects' +import ActionTypes from '../constants/action-types' +import { Action } from '../types/core' +import { errorThrownServer } from '../actions/error' +import errorMessages from '../constants/error-messages' +import { + AppUsageStatsParams, + appUsageStatsReceiveData, + appUsageStatsRequestDataFailure, +} from '@/actions/app-usage-stats' + +const { APP_USAGE_STATS_REQUEST_DATA } = ActionTypes + +export const fetchAppUsageStats = async (data: AppUsageStatsParams) => { + const response = await fetcher({ + url: `${URLS.statistics}?${setQueryParams({ ...data })}`, + api: process.env.MARKETPLACE_API_BASE_URL as string, + method: 'GET', + headers: MARKETPLACE_HEADERS, + }) + return response +} + +export const appUsageStatsSaga = function*({ data }: Action) { + try { + const response = yield call(fetchAppUsageStats, { ...data }) + yield put(appUsageStatsReceiveData(response)) + } catch (err) { + yield put(appUsageStatsRequestDataFailure()) + yield put( + errorThrownServer({ + type: 'SERVER', + message: errorMessages.DEFAULT_SERVER_ERROR, + }), + ) + } +} + +export const appUsageStatsListen = function*() { + yield takeLatest>(APP_USAGE_STATS_REQUEST_DATA, appUsageStatsSaga) +} + +export const appUsageStatsSagas = function*() { + yield all([fork(appUsageStatsListen)]) +} + +export default appUsageStatsSagas diff --git a/packages/marketplace/src/selector/__tests__/developer.ts b/packages/marketplace/src/selector/__tests__/developer.ts index 96cfac0262..537c60dc49 100644 --- a/packages/marketplace/src/selector/__tests__/developer.ts +++ b/packages/marketplace/src/selector/__tests__/developer.ts @@ -19,7 +19,7 @@ describe('selectDeveloperId', () => { }) it('should run correctly and return undefined', () => { - const input = {} as ReduxState + const input = { auth: {} } as ReduxState const result = selectDeveloperId(input) expect(result).toEqual(undefined) }) diff --git a/packages/marketplace/src/selector/developer.ts b/packages/marketplace/src/selector/developer.ts index 0ed63a8c4b..e2c26bdc3b 100644 --- a/packages/marketplace/src/selector/developer.ts +++ b/packages/marketplace/src/selector/developer.ts @@ -1,7 +1,7 @@ import { ReduxState } from '@/types/core' export const selectDeveloperId = (state: ReduxState) => { - return state?.auth?.loginSession?.loginIdentity.developerId + return state.auth.loginSession?.loginIdentity.developerId } export const selectDeveloperEmail = (state: ReduxState) => { diff --git a/packages/marketplace/src/tests/badges/badge-branches.svg b/packages/marketplace/src/tests/badges/badge-branches.svg index cef94f9ad5..2bb3e604c8 100644 --- a/packages/marketplace/src/tests/badges/badge-branches.svg +++ b/packages/marketplace/src/tests/badges/badge-branches.svg @@ -1 +1 @@ - Coverage:branchesCoverage:branches69.81%69.81% \ No newline at end of file + Coverage:branchesCoverage:branches69.66%69.66% \ No newline at end of file diff --git a/packages/marketplace/src/tests/badges/badge-functions.svg b/packages/marketplace/src/tests/badges/badge-functions.svg index f856493b95..c34b90cbf9 100644 --- a/packages/marketplace/src/tests/badges/badge-functions.svg +++ b/packages/marketplace/src/tests/badges/badge-functions.svg @@ -1 +1 @@ - Coverage:functionsCoverage:functions73.48%73.48% \ No newline at end of file + Coverage:functionsCoverage:functions73.8%73.8% \ No newline at end of file diff --git a/packages/marketplace/src/tests/badges/badge-lines.svg b/packages/marketplace/src/tests/badges/badge-lines.svg index 6d5e3f10f5..246131366d 100644 --- a/packages/marketplace/src/tests/badges/badge-lines.svg +++ b/packages/marketplace/src/tests/badges/badge-lines.svg @@ -1 +1 @@ - Coverage:linesCoverage:lines90.4%90.4% \ No newline at end of file + Coverage:linesCoverage:lines90.32%90.32% \ No newline at end of file diff --git a/packages/marketplace/src/tests/badges/badge-statements.svg b/packages/marketplace/src/tests/badges/badge-statements.svg index c90015f145..e0e462e5c3 100644 --- a/packages/marketplace/src/tests/badges/badge-statements.svg +++ b/packages/marketplace/src/tests/badges/badge-statements.svg @@ -1 +1 @@ - Coverage:statementsCoverage:statements89.51%89.51% \ No newline at end of file + Coverage:statementsCoverage:statements89.48%89.48% \ No newline at end of file diff --git a/packages/marketplace/src/types/core.ts b/packages/marketplace/src/types/core.ts index cac30c4a68..8ac6dbdd38 100644 --- a/packages/marketplace/src/types/core.ts +++ b/packages/marketplace/src/types/core.ts @@ -19,6 +19,7 @@ import { SettingsState } from '@/reducers/settings' import { AdminAppsState } from '@/reducers/admin-apps' import { ResetPasswordState } from '@/reducers/reset-password' import { AppInstallationsState } from '@/reducers/app-installations' +import { AppUsageStatsState } from '@/reducers/app-usage-stats' import { NotificationMessageState } from '@/reducers/notification-message' import { AdminStatsState } from '@/reducers/admin-stats' @@ -79,6 +80,7 @@ export interface ReduxState { settings: SettingsState resetPassword: ResetPasswordState installations: AppInstallationsState + appUsageStats: AppUsageStatsState noticationMessage: NotificationMessageState adminStats: AdminStatsState } diff --git a/packages/marketplace/src/utils/__tests__/app-usage-stats.ts b/packages/marketplace/src/utils/__tests__/app-usage-stats.ts new file mode 100644 index 0000000000..ab2d44d7e4 --- /dev/null +++ b/packages/marketplace/src/utils/__tests__/app-usage-stats.ts @@ -0,0 +1,83 @@ +import { appsDataStub } from '@/sagas/__stubs__/apps' +import { usageStatsDataStub, usageStatsForMultipleAppsDataStub } from '@/sagas/__stubs__/app-usage-stats' +import { getAppUsageStatsChartData, getChartConfig, getChartOptions } from '@/utils/app-usage-stats.ts' +import { DeveloperTrafficChartProps } from '@/components/ui/developer-traffic-chart' + +const props: DeveloperTrafficChartProps = { + apps: appsDataStub.data, + stats: usageStatsDataStub, +} + +const multipeAppStatsprops: DeveloperTrafficChartProps = { + apps: appsDataStub.data, + stats: usageStatsForMultipleAppsDataStub, +} + +describe('DeveloperTrafficChart', () => { + describe('getAppUsageStatsChartData', () => { + it('should run correctly with stats for 1 app', () => { + const { stats, apps } = props + const result = getAppUsageStatsChartData(stats.appUsage, apps.data) + const expected = { + labels: ['15/11/2019'], + data: [5], + appUsageStatsGroupedByDate: { + '15/11/2019': { + '09043eb8-9e5e-4650-b7f1-f0cb62699027': { appName: 'test', requests: 5 }, + date: new Date('2019-11-15T00:00:00+00:00'), + totalRequests: 5, + }, + }, + } + expect(result).toEqual(expected) + }) + + it('should run correctly with stats for multiple apps', () => { + const { stats, apps } = multipeAppStatsprops + const result = getAppUsageStatsChartData(stats.appUsage, apps.data) + const expected = { + labels: ['01/02/2020', '05/02/2020', '09/02/2020'], + data: [8, 0, 5], + appUsageStatsGroupedByDate: { + '09/02/2020': { + '09043eb8-9e5e-4650-b7f1-f0cb62699027': { appName: 'test', requests: 2 }, + '261da083-cee2-4f5c-a18f-8f9375f1f5af': { appName: 'asd', requests: 3 }, + date: new Date('2020-02-09T09:18:23.957Z'), + totalRequests: 5, + }, + '01/02/2020': { + '09043eb8-9e5e-4650-b7f1-f0cb62699027': { appName: 'test', requests: 5 }, + '261da083-cee2-4f5c-a18f-8f9375f1f5af': { appName: 'asd', requests: 3 }, + date: new Date('2020-02-01T09:18:23.957Z'), + totalRequests: 8, + }, + '05/02/2020': { + '09043eb8-9e5e-4650-b7f1-f0cb62699027': { appName: 'test', requests: 0 }, + '261da083-cee2-4f5c-a18f-8f9375f1f5af': { appName: 'asd', requests: 0 }, + date: new Date('2020-02-05T09:18:23.957Z'), + totalRequests: 0, + }, + }, + } + expect(result).toEqual(expected) + }) + + describe('getChartOptions', () => { + it('should run correctly', () => { + const { stats, apps } = props + const result = getAppUsageStatsChartData(stats.appUsage, apps.data) + const options = getChartOptions(result?.appUsageStatsGroupedByDate) + expect(options.tooltips).not.toBeNull() + }) + }) + + describe('getChartConfig', () => { + it('should run correctly', () => { + const { stats, apps } = props + const result = getAppUsageStatsChartData(stats.appUsage, apps.data) + const configs = getChartConfig(result?.labels, result?.data) + expect(configs).not.toBeNull() + }) + }) + }) +}) diff --git a/packages/marketplace/src/utils/app-usage-stats.ts b/packages/marketplace/src/utils/app-usage-stats.ts new file mode 100644 index 0000000000..3b9bed3ca2 --- /dev/null +++ b/packages/marketplace/src/utils/app-usage-stats.ts @@ -0,0 +1,113 @@ +import { AppUsageStatsModel, AppSummaryModel } from '@reapit/foundations-ts-definitions' +import orderBy from 'lodash.orderby' +import { toLocalTime } from '@reapit/elements' + +export interface AppTooltipLabel { + appName: string + requests: number +} + +export const getAppUsageStatsChartData = (appUsageStats?: AppUsageStatsModel[], developerApps?: AppSummaryModel[]) => { + const appUsageStatsGroupedByDate = appUsageStats?.reduce((accumulator, currentValue) => { + const { appId, usage } = currentValue || {} + const developerApp = developerApps?.find(app => app.id === appId) + if (!developerApp?.id) { + return accumulator + } + const { id: developerAppId, name: developerAppName } = developerApp + usage?.forEach(usageByDate => { + const { date, requests } = usageByDate + if (!date) { + return null + } + const formattedDate = toLocalTime(date, 'DD/MM/YYYY') + if (!accumulator[formattedDate]) { + accumulator[formattedDate] = { + [developerAppId]: { + appName: developerAppName, + requests, + }, + date: new Date(date), + totalRequests: requests, + } + } else { + accumulator[formattedDate] = { + ...accumulator[formattedDate], + ...{ + [developerAppId]: { + appName: developerAppName, + requests, + }, + }, + date: new Date(date), + totalRequests: accumulator[formattedDate].totalRequests + requests, + } + } + }) + return accumulator + }, {}) + + if (!appUsageStatsGroupedByDate || Object.keys(appUsageStatsGroupedByDate).length === 0) { + return null + } + + const orderedAppUsageStats = orderBy(appUsageStatsGroupedByDate, ['date'], ['asc']) + const labels = orderedAppUsageStats.map(item => toLocalTime(item.date, 'DD/MM/YYYY')) + const data = orderedAppUsageStats.map(item => item.totalRequests) + + return { + labels, + data, + appUsageStatsGroupedByDate, + } +} + +export const getChartOptions = data => { + return { + legend: null, + tooltips: { + mode: 'label', + callbacks: { + label: function(tooltipItem) { + const appUsage: [AppTooltipLabel] = data[tooltipItem.label] + if (!appUsage) { + return 'No Data' + } + return Object.values(appUsage) + .filter((app: AppTooltipLabel) => app.appName) + .map((app: AppTooltipLabel) => { + return `${app.appName}: ${app.requests}` + }) + }, + }, + }, + } +} + +export const getChartConfig = (labels: string[], data: number[]) => { + return { + labels, + datasets: [ + { + fill: false, + lineTension: 0.1, + backgroundColor: 'rgba(75,192,192,0.4)', + borderColor: 'rgba(75,192,192,1)', + borderCapStyle: 'butt', + borderDash: [], + borderDashOffset: 0.0, + borderJoinStyle: 'miter', + pointBorderColor: 'rgba(75,192,192,1)', + pointBackgroundColor: '#fff', + pointBorderWidth: 1, + pointHoverRadius: 5, + pointHoverBackgroundColor: 'rgba(75,192,192,1)', + pointHoverBorderColor: 'rgba(220,220,220,1)', + pointHoverBorderWidth: 2, + pointRadius: 1, + pointHitRadius: 10, + data, + }, + ], + } +} diff --git a/packages/marketplace/src/utils/developer-analytics.ts b/packages/marketplace/src/utils/developer-analytics.ts index bfe95127b9..36b9a9b56e 100644 --- a/packages/marketplace/src/utils/developer-analytics.ts +++ b/packages/marketplace/src/utils/developer-analytics.ts @@ -41,6 +41,9 @@ export const groupInstalledAppsByDate = (apps: InstallationModelWithAppName[]): const orderedApps: InstallationModelWithDateObject[] = orderBy(formatedApps, ['createdDate'], ['asc']) const tmpgrouppedApps = {} let tmpApp: InstallationModelWithDateObject, tmpLabel + if (orderedApps.length === 0) { + return grouppedApps + } for (let i = 0; i < orderedApps.length; i++) { tmpApp = orderedApps[i] tmpLabel = dayjs(tmpApp.createdDate).format('DD/MM/YYYY') diff --git a/packages/marketplace/src/utils/route-dispatcher.ts b/packages/marketplace/src/utils/route-dispatcher.ts index 5ac9452b98..42b454044a 100644 --- a/packages/marketplace/src/utils/route-dispatcher.ts +++ b/packages/marketplace/src/utils/route-dispatcher.ts @@ -18,6 +18,7 @@ import { checkFirstTimeLogin } from '@/actions/auth' import { adminAppsRequestData } from '@/actions/admin-apps' import { appInstallationsRequestData } from '@/actions/app-installations' import { selectClientId } from '@/selector/client' +import { appUsageStatsRequestData } from '@/actions/app-usage-stats' const routeDispatcher = async (route: RouteValue, params?: StringMap, search?: string) => { await getAccessToken() @@ -46,7 +47,9 @@ const routeDispatcher = async (route: RouteValue, params?: StringMap, search?: s store.dispatch(developerRequestData({ page: 1 })) break case Routes.DEVELOPER_ANALYTICS_PAGINATE: - case Routes.DEVELOPER_ANALYTICS: + case Routes.DEVELOPER_ANALYTICS: { + // Need to fetch statistics to traffic table + store.dispatch(appUsageStatsRequestData({})) // Need to fetch all apps to count Total current installations for each app store.dispatch(appInstallationsRequestData({ pageSize: GET_ALL_PAGE_SIZE })) // Fetch all apps to map app name to installations @@ -56,6 +59,7 @@ const routeDispatcher = async (route: RouteValue, params?: StringMap, search?: s store.dispatch(appDetailRequestData({ id: appId, clientId })) } break + } case Routes.DEVELOPER_MY_APPS_EDIT: store.dispatch(submitAppRequestData()) store.dispatch(appDetailRequestData({ id })) diff --git a/packages/web-components/src/components/search-widget/map/__mocks__/mock-property.ts b/packages/web-components/src/components/search-widget/map/__mocks__/mock-property.ts index fe397ccf3c..9b8ca37c16 100644 --- a/packages/web-components/src/components/search-widget/map/__mocks__/mock-property.ts +++ b/packages/web-components/src/components/search-widget/map/__mocks__/mock-property.ts @@ -12,7 +12,7 @@ export const property: PropertyModel = { line3: 'London', line4: '', postcode: 'N19 4JF', - country: 'GB', + countryId: 'GB', geolocation: { latitude: 51.56449, longitude: -0.121057