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 a68a931270..48e598759d 100644 --- a/packages/marketplace/src/components/pages/__tests__/__snapshots__/analytics.tsx.snap +++ b/packages/marketplace/src/components/pages/__tests__/__snapshots__/analytics.tsx.snap @@ -1,571 +1,331 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Analytics should match a snapshot when app detail data undefined 1`] = ` +exports[`AnalyticsPage should match snapshot 1`] = ` - Analytics + Dashboard - - Total number of installations: - 1 - - - To see specific installations please select an App from the list below: - - + - - - - -`; - -exports[`Analytics should match a snapshot when app detail loading false 1`] = ` - - - - Analytics - - - Total number of installations: - 1 - - - To see specific installations please select an App from the list below: - - - - + /> `; -exports[`Analytics should match a snapshot when app detail loading true 1`] = ` - +exports[`AnalyticsPage should match when loading 1`] = ``; + +exports[`InstallationTable should match snapshot 1`] = ` +
+ + Installations + +

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

+
+

+ Total current installation for + + test + + : + 0 +

+

+ Total current installation for + + asd + + : + 0 +

+
- - Analytics - - - Total number of installations: - 1 - - - To see specific installations please select an App from the list below: - - - - - - -`; - -exports[`Analytics should match a snapshot when appInstallations data null 1`] = ` - - -
-
-
-
-
-
- - + loading={false} + scrollable={true} + /> +
+ +
`; -exports[`Analytics should match a snapshot when appInstallations loading false 1`] = ` - +exports[`InstallationTable should match with null developerData 1`] = ` +
+ + Installations + +

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

+
- - Analytics - - - Total number of installations: - 1 - - - To see specific installations please select an App from the list below: - - - - - - -`; - -exports[`Analytics should match a snapshot when appInstallations loading true 1`] = ` - - -
-
-
-
-
-
- - -`; - -exports[`Analytics should match a snapshot when appsOfDeveloper data undefined 1`] = ` - + loading={false} + scrollable={true} + /> +
- - Analytics - - - Total number of installations: - 1 - - - To see specific installations please select an App from the list below: - - - - - -
+ onChange={[Function]} + pageNumber={1} + pageSize={5} + totalCount={1} + /> +
`; -exports[`Analytics should match a snapshot when appsOfDeveloper loading false 1`] = ` - - - - Analytics - - - Total number of installations: - 1 - - - To see specific installations please select an App from the list below: - - - - +exports[`InstallationTable should match with null installationsAppData 1`] = ` +
+ + Installations - -`; - -exports[`Analytics should match a snapshot when appsOfDeveloper loading true 1`] = ` - - - - Analytics - - - Total number of installations: - 1 - - - To see specific installations please select an App from the list below: - - + The installations table below shows the individuals per client with a total number of installations per app +

+
+

- - - - -`; - -exports[`Analytics should match a snapshot when no appId params 1`] = ` - + Total current installation for + + test + + : + 0 +

+

+ Total current installation for + + asd + + : + 0 +

+
- - Analytics - - - Total number of installations: - 1 - - - To see specific installations please select an App from the list below: - - - - - -
+ "Header": "App Name", + "accessor": "appName", + }, + Object { + "Header": "Client", + "accessor": "client", + }, + Object { + "Header": "Date of installation", + "accessor": [Function], + }, + Object { + "Header": "Date of Uninstallation", + "accessor": [Function], + }, + ] + } + data={Array []} + loading={false} + scrollable={true} + /> +
+ +
`; diff --git a/packages/marketplace/src/components/pages/__tests__/analytics.tsx b/packages/marketplace/src/components/pages/__tests__/analytics.tsx index a1dab22d4a..e7cb6d8448 100644 --- a/packages/marketplace/src/components/pages/__tests__/analytics.tsx +++ b/packages/marketplace/src/components/pages/__tests__/analytics.tsx @@ -1,361 +1,538 @@ import * as React from 'react' -import { mount, shallow } from 'enzyme' +import { shallow } from 'enzyme' import { AnalyticsPage, - transformListAppToSelectBoxOptions, - transformAppInstalationsToTableColumsCompatible, - handleSubmit, + InstallationTable, + handleSetPageNumber, + installationTableColumn, + handleMapAppNameToInstallation, + handleUseMemoData, + handleCountCurrentInstallationForEachApp, + sortAppByDateInstalled, + countAppHasInstallation, + countAppNoInstallation, } from '../analytics' import { installationsStub } from '@/sagas/__stubs__/installations' -import { mockLoginSession, mockRefreshParams } from '@/core/__mocks__/store' -import { AnalyticsPageProps, mapStateToProps, mapDispatchToProps } from '@/components/pages/analytics' +import { mapStateToProps } from '@/components/pages/analytics' import { appsDataStub } from '@/sagas/__stubs__/apps' import { ReduxState } from '@/types/core' -import { appDetailDataStub } from '@/sagas/__stubs__/app-detail' -import { defaultAppAuthState } from '@/reducers/app-detail' -import { RouteComponentProps, StaticContext } from 'react-router' -import Routes from '@/constants/routes' +import { DeveloperState } from '@/reducers/developer' +import { AppInstallationsState } from '@/reducers/app-installations' +jest.mock('@reapit/elements', () => ({ + ...jest.requireActual('@reapit/elements'), + toLocalTime: jest.fn().mockReturnValue('localtime'), +})) jest.mock('../../../core/store') -const routerProps = appId => - ({ - match: { - params: { - page: '2', - }, - }, - location: { - search: `page=1${appId ? `&appId=${appId}` : ''}`, - }, - } as RouteComponentProps) - -const mockProps = ( - appId, - appInstallations, - appInstallationsLoading, - appsOfDeveloper, - appsOfDeveloperLoading, - appDetail, - appDetailLoading, -) => - ({ - appInstallations: { - installationsAppData: appInstallations, - formState: 'SUCCESS', - loading: appInstallationsLoading, - }, - appsOfDeveloper: { - developerData: { - data: appsOfDeveloper, - scopes: [], - }, - formState: 'SUCCESS', - isVisible: true, - loading: appsOfDeveloperLoading, - }, - appDetail: { - appDetailData: appDetail, - authentication: defaultAppAuthState, - error: false, - isStale: false, - loading: appDetailLoading, - }, - requestAppDetailData: jest.fn(), - ...routerProps(appId), - } as AnalyticsPageProps) +const developer = { + developerData: appsDataStub, + loading: false, +} as DeveloperState -describe('Analytics', () => { - it('should match a snapshot when appInstallations loading false', () => { - expect( - shallow( - , - ), - ).toMatchSnapshot() - }) - - it('should match a snapshot when appInstallations loading true', () => { - expect( - mount( - , - ), - ).toMatchSnapshot() - }) +const installations = { + loading: false, + installationsAppData: { + ...installationsStub, + }, +} as AppInstallationsState - it('should match a snapshot when appInstallations data null', () => { - expect( - mount( - , - ), - ).toMatchSnapshot() +describe('AnalyticsPage', () => { + it('should match snapshot', () => { + expect(shallow()).toMatchSnapshot() }) - it('should match a snapshot when appsOfDeveloper loading true', () => { + it('should match when loading', () => { + const installationsLoading = { ...installations, loading: true } + const developerLoading = { ...developer, loading: true } expect( - shallow( - , - ), + shallow(), ).toMatchSnapshot() }) +}) - it('should match a snapshot when appsOfDeveloper loading false', () => { - expect( - shallow( - , - ), - ).toMatchSnapshot() +describe('mapStateToProps', () => { + it('should return correct value', () => { + expect(mapStateToProps({ installations, developer } as ReduxState)).toEqual({ installations, developer }) }) +}) - it('should match a snapshot when appsOfDeveloper data undefined', () => { - expect( - shallow( - , - ), - ).toMatchSnapshot() +describe('InstallationTable', () => { + it('should match snapshot', () => { + expect(shallow()).toMatchSnapshot() }) - it('should match a snapshot when app detail loading true', () => { + it('should match with null installationsAppData', () => { + const installationsWithoutData = { ...installations, installationsAppData: null } expect( - shallow( - , - ), + shallow(), ).toMatchSnapshot() }) - it('should match a snapshot when app detail loading false', () => { + it('should match with null developerData', () => { + const developerWithoutData = { ...developer, developerData: null } expect( - shallow( - , - ), + shallow(), ).toMatchSnapshot() }) +}) - it('should match a snapshot when app detail data undefined', () => { - expect( - shallow( - , - ), - ).toMatchSnapshot() +describe('handleSetPageNumber', () => { + it('should call setPageNumber', () => { + const setPageNumber = jest.fn() + const fn = handleSetPageNumber(setPageNumber) + fn(1) + expect(setPageNumber).toHaveBeenCalledWith(1) }) +}) - it('should match a snapshot when no appId params', () => { - expect( - shallow( - , - ), - ).toMatchSnapshot() +describe('installationTableColumn', () => { + it('should call accessor', () => { + installationTableColumn.forEach(({ Header, accessor }) => { + if (typeof accessor === 'function') { + if (Header === 'Date of installation') { + const result = accessor({ created: 'date' }) + expect(result).toEqual('localtime') + } + if (Header === 'Date of Uninstallation') { + expect(accessor({ created: 'date', terminatesOn: 'terminated date' })).toEqual('localtime') + expect(accessor({})).toEqual('') + } + } + }) }) }) -describe('mapStateToProps', () => { +describe('handleMapAppNameToInstallation', () => { + const installationsArray = [ + { + id: 'b3c2f644-3241-4298-b320-b0398ff492f9', + appId: '062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + created: '2019-12-03T05:33:20', + client: 'DXX', + status: 'Active', + links: [ + { + rel: 'self', + href: 'http://dev.platformmarketplace.reapit.net/installations/b3c2f644-3241-4298-b320-b0398ff492f9', + action: 'GET', + }, + { + rel: 'app', + href: 'http://dev.platformmarketplace.reapit.net/apps/062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + action: 'GET', + }, + ], + }, + ] + const developerDataArray = [ + { + id: '062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + developerId: '28c9ea52-7f73-4814-9e00-4e3714b8adeb', + name: 'test', + summary: + 'nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsic' + + 'r8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz', + developer: "Pete's Proptech World Ltd", + homePage: 'http://google.com/abc', + iconUri: 'https://reapit-app-store-app-media.s3.eu-west-2.amazonaws.com/d10e790c-2bf2-40ae-9c43-82c1534bde31.png', + links: [ + { + rel: 'self', + href: 'http://platformdemo.reapit.net/marketplace/apps/09043eb8-9e5e-4650-b7f1-f0cb62699027', + action: 'GET', + }, + { + rel: 'developer', + href: 'http://platformdemo.reapit.net/marketplace/developers/28c9ea52-7f73-4814-9e00-4e3714b8adeb', + action: 'GET', + }, + ], + }, + ] it('should return correctly', () => { - const mockState = { - installations: { - installationsAppData: { - ...installationsStub, - data: [], - }, - formState: 'SUCCESS', - loading: true, - }, - developer: { - developerData: { - data: appsDataStub.data, - scopes: [], - }, - formState: 'SUCCESS', - isVisible: true, - loading: false, - }, - auth: { - error: false, - loginType: 'DEVELOPER', - refreshSession: mockRefreshParams, - loginSession: mockLoginSession, - }, - appDetail: { - appDetailData: appDetailDataStub, - authentication: defaultAppAuthState, - error: false, - isStale: false, - loading: false, - }, - } as Partial - - const output = { - appInstallations: { - installationsAppData: { - ...installationsStub, - data: [], - }, - formState: 'SUCCESS', - loading: true, - }, - appsOfDeveloper: { - developerData: { - data: appsDataStub.data, - scopes: [], - }, - formState: 'SUCCESS', - isVisible: true, - loading: false, - }, - appDetail: { - appDetailData: appDetailDataStub, - authentication: defaultAppAuthState, - error: false, - isStale: false, - loading: false, - }, - } - const result = mapStateToProps(mockState as ReduxState) - expect(result).toEqual(output) + const fn = handleMapAppNameToInstallation(installationsArray, developerDataArray) + const result = fn() + expect(result).toEqual([{ ...installationsArray[0], appName: 'test' }]) }) -}) - -describe('mapDispatchToProps', () => { - it('should run dispatch', () => { - const mockDispatch = jest.fn() - const result = mapDispatchToProps(mockDispatch) - result.requestAppDetailData({ id: 'id' }) - - expect(mockDispatch).toBeCalled() + it('should return correctly when not found app in developerDataArray', () => { + const fn = handleMapAppNameToInstallation(installationsArray, [{ ...developerDataArray[0], id: 'fake id' }]) + const result = fn() + expect(result).toEqual(installationsArray) }) }) -describe('transformListAppToSelectBoxOptions', () => { - it('should transform normally', () => { - const list = appsDataStub.data.data! - const selectBoxOptions = list?.map(transformListAppToSelectBoxOptions) - list?.forEach((original, index) => { - const transformed = selectBoxOptions[index] - expect(transformed.label).toBe(original.name) - expect(transformed.value).toBe(original.id) - }) +describe('handleUseMemoData', () => { + const installationAppDataArrayWithName = [ + { + appName: 'app2', + id: 'id2', + appId: 'id2', + created: '2019-12-02T05:33:20', + client: 'DXX', + status: 'Active', + links: [ + { + rel: 'self', + href: 'http://dev.platformmarketplace.reapit.net/installations/b3c2f644-3241-4298-b320-b0398ff492f9', + action: 'GET', + }, + { + rel: 'app', + href: 'http://dev.platformmarketplace.reapit.net/apps/062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + action: 'GET', + }, + ], + }, + { + appName: 'app1', + id: 'b3c2f644-3241-4298-b320-b0398ff492f9', + appId: 'id1', + created: '2019-12-03T05:33:20', + client: 'DXX', + status: 'Active', + links: [ + { + rel: 'self', + href: 'http://dev.platformmarketplace.reapit.net/installations/b3c2f644-3241-4298-b320-b0398ff492f9', + action: 'GET', + }, + { + rel: 'app', + href: 'http://dev.platformmarketplace.reapit.net/apps/062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + action: 'GET', + }, + ], + }, + ] + it('should return correctly', () => { + const fn = handleUseMemoData(installationAppDataArrayWithName, 1) + const result = fn() + expect(result).toEqual([installationAppDataArrayWithName[1], installationAppDataArrayWithName[0]]) }) - - it('should transform the label into "Error"', () => { - const list = appsDataStub.data.data!.map(app => ({ ...app, name: undefined, id: undefined })) - const selectBoxOptions = list?.map(transformListAppToSelectBoxOptions) - list?.forEach((original, index) => { - const transformed = selectBoxOptions[index] - expect(transformed.label).toBe('Error') - expect(transformed.value).toBe('Error') - }) + it('should return correctly when reorder', () => { + const fn = handleUseMemoData([installationAppDataArrayWithName[1], installationAppDataArrayWithName[0]], 1) + const result = fn() + expect(result).toEqual([installationAppDataArrayWithName[1], installationAppDataArrayWithName[0]]) + }) + it('should return correctly when created undefined', () => { + const fn = handleUseMemoData( + [installationAppDataArrayWithName[0], { ...installationAppDataArrayWithName[1], created: undefined }], + 1, + ) + const result = fn() + expect(result).toEqual([ + installationAppDataArrayWithName[0], + { ...installationAppDataArrayWithName[1], created: undefined }, + ]) }) }) -describe('transformAppInstalationsToTableColumsCompatible', () => { - const appName = 'App Name Test' - const list = installationsStub.data! - const transformed = list.map(transformAppInstalationsToTableColumsCompatible(appName)) - transformed.forEach(installations => { - expect(installations.appName).toBe(appName) +describe('handleCountCurrentInstallationForEachApp', () => { + const installationAppDataArrayWithName = [ + { + terminatesOn: '2019-12-05T05:33:20', + appName: 'app2', + id: 'id2', + appId: 'id2', + created: '2019-12-02T05:33:20', + client: 'DXX', + status: 'Active', + links: [ + { + rel: 'self', + href: 'http://dev.platformmarketplace.reapit.net/installations/b3c2f644-3241-4298-b320-b0398ff492f9', + action: 'GET', + }, + { + rel: 'app', + href: 'http://dev.platformmarketplace.reapit.net/apps/062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + action: 'GET', + }, + ], + }, + { + appName: 'app1', + id: 'id2', + appId: 'id2', + created: '2019-12-02T05:33:20', + client: 'DXX', + status: 'Active', + links: [ + { + rel: 'self', + href: 'http://dev.platformmarketplace.reapit.net/installations/b3c2f644-3241-4298-b320-b0398ff492f9', + action: 'GET', + }, + { + rel: 'app', + href: 'http://dev.platformmarketplace.reapit.net/apps/062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + action: 'GET', + }, + ], + }, + { + appName: 'app1', + id: 'b3c2f644-3241-4298-b320-b0398ff492f9', + appId: 'id1', + created: '2019-12-03T05:33:20', + client: 'DXX', + status: 'Active', + links: [ + { + rel: 'self', + href: 'http://dev.platformmarketplace.reapit.net/installations/b3c2f644-3241-4298-b320-b0398ff492f9', + action: 'GET', + }, + { + rel: 'app', + href: 'http://dev.platformmarketplace.reapit.net/apps/062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + action: 'GET', + }, + ], + }, + ] + const developerDataArray = [ + { + id: 'id1', + developerId: '28c9ea52-7f73-4814-9e00-4e3714b8adeb', + name: 'app2', + summary: + 'nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsic' + + 'r8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz', + developer: "Pete's Proptech World Ltd", + homePage: 'http://google.com/abc', + iconUri: 'https://reapit-app-store-app-media.s3.eu-west-2.amazonaws.com/d10e790c-2bf2-40ae-9c43-82c1534bde31.png', + links: [ + { + rel: 'self', + href: 'http://platformdemo.reapit.net/marketplace/apps/09043eb8-9e5e-4650-b7f1-f0cb62699027', + action: 'GET', + }, + { + rel: 'developer', + href: 'http://platformdemo.reapit.net/marketplace/developers/28c9ea52-7f73-4814-9e00-4e3714b8adeb', + action: 'GET', + }, + ], + }, + { + id: 'id1', + developerId: '28c9ea52-7f73-4814-9e00-4e3714b8adeb', + summary: + 'nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsic' + + 'r8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz', + developer: "Pete's Proptech World Ltd", + homePage: 'http://google.com/abc', + iconUri: 'https://reapit-app-store-app-media.s3.eu-west-2.amazonaws.com/d10e790c-2bf2-40ae-9c43-82c1534bde31.png', + links: [ + { + rel: 'self', + href: 'http://platformdemo.reapit.net/marketplace/apps/09043eb8-9e5e-4650-b7f1-f0cb62699027', + action: 'GET', + }, + { + rel: 'developer', + href: 'http://platformdemo.reapit.net/marketplace/developers/28c9ea52-7f73-4814-9e00-4e3714b8adeb', + action: 'GET', + }, + ], + }, + ] + it('should return correctly', () => { + const fn = handleCountCurrentInstallationForEachApp(installationAppDataArrayWithName, developerDataArray) + const result = fn() + expect(result).toEqual({ app1: 2, app2: 0 }) }) }) -describe('handleSubmit', () => { - it('should run correctly', () => { - const mockHistory = { - push: jest.fn(str => str), - } +describe('sortAppByDateInstalled', () => { + const installationAppDataArrayWithName = [ + { + appName: 'app2', + id: 'id2', + appId: 'id2', + created: '2019-12-02T05:33:20', + client: 'DXX', + status: 'Active', + links: [ + { + rel: 'self', + href: 'http://dev.platformmarketplace.reapit.net/installations/b3c2f644-3241-4298-b320-b0398ff492f9', + action: 'GET', + }, + { + rel: 'app', + href: 'http://dev.platformmarketplace.reapit.net/apps/062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + action: 'GET', + }, + ], + }, + { + appName: 'app1', + id: 'b3c2f644-3241-4298-b320-b0398ff492f9', + appId: 'id1', + created: '2019-12-03T05:33:20', + client: 'DXX', + status: 'Active', + links: [ + { + rel: 'self', + href: 'http://dev.platformmarketplace.reapit.net/installations/b3c2f644-3241-4298-b320-b0398ff492f9', + action: 'GET', + }, + { + rel: 'app', + href: 'http://dev.platformmarketplace.reapit.net/apps/062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + action: 'GET', + }, + ], + }, + ] - handleSubmit(mockHistory, 'appId', 1) - expect(mockHistory.push).toHaveBeenCalledWith(`${Routes.DEVELOPER_ANALYTICS}/1?appId=appId`) + it('should return correctly', () => { + const result = sortAppByDateInstalled(installationAppDataArrayWithName) + expect(result).toEqual([installationAppDataArrayWithName[1], installationAppDataArrayWithName[0]]) }) - it('should run correctly', () => { - const mockHistory = { - push: jest.fn(str => str), - } + it('should return correctly when reorder', () => { + const result = sortAppByDateInstalled([installationAppDataArrayWithName[1], installationAppDataArrayWithName[0]]) + expect(result).toEqual([installationAppDataArrayWithName[1], installationAppDataArrayWithName[0]]) + }) - handleSubmit(mockHistory, '', 1) - expect(mockHistory.push).toHaveBeenCalledWith(`${Routes.DEVELOPER_ANALYTICS}/1`) + it('should return correctly when created undefined', () => { + const result = sortAppByDateInstalled([ + installationAppDataArrayWithName[0], + { ...installationAppDataArrayWithName[1], created: undefined }, + ]) + expect(result).toEqual([ + installationAppDataArrayWithName[0], + { ...installationAppDataArrayWithName[1], created: undefined }, + ]) }) +}) - it('should run correctly', () => { - const mockHistory = { - push: jest.fn(str => str), - } +describe('countAppHasInstallation', () => { + const installationAppDataArrayWithName = [ + { + terminatesOn: '2019-12-05T05:33:20', + appName: 'app2', + id: 'id2', + appId: 'id2', + created: '2019-12-02T05:33:20', + client: 'DXX', + status: 'Active', + links: [ + { + rel: 'self', + href: 'http://dev.platformmarketplace.reapit.net/installations/b3c2f644-3241-4298-b320-b0398ff492f9', + action: 'GET', + }, + { + rel: 'app', + href: 'http://dev.platformmarketplace.reapit.net/apps/062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + action: 'GET', + }, + ], + }, + { + appName: 'app1', + id: 'id2', + appId: 'id2', + created: '2019-12-02T05:33:20', + client: 'DXX', + status: 'Active', + links: [ + { + rel: 'self', + href: 'http://dev.platformmarketplace.reapit.net/installations/b3c2f644-3241-4298-b320-b0398ff492f9', + action: 'GET', + }, + { + rel: 'app', + href: 'http://dev.platformmarketplace.reapit.net/apps/062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + action: 'GET', + }, + ], + }, + { + appName: 'app1', + id: 'b3c2f644-3241-4298-b320-b0398ff492f9', + appId: 'id1', + created: '2019-12-03T05:33:20', + client: 'DXX', + status: 'Active', + links: [ + { + rel: 'self', + href: 'http://dev.platformmarketplace.reapit.net/installations/b3c2f644-3241-4298-b320-b0398ff492f9', + action: 'GET', + }, + { + rel: 'app', + href: 'http://dev.platformmarketplace.reapit.net/apps/062a376c-f5a3-46a0-a64b-e4bc6e5af2c1', + action: 'GET', + }, + ], + }, + ] + it('should return correctly', () => { + const result = countAppHasInstallation(installationAppDataArrayWithName) + expect(result).toEqual({ app1: 2 }) + }) +}) - handleSubmit(mockHistory, '') - expect(mockHistory.push).toHaveBeenCalledWith(`${Routes.DEVELOPER_ANALYTICS}/1`) +describe('countAppNoInstallation', () => { + const developerDataArray = [ + { + id: 'id1', + developerId: '28c9ea52-7f73-4814-9e00-4e3714b8adeb', + name: 'app2', + summary: + 'nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsic' + + 'r8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz', + developer: "Pete's Proptech World Ltd", + homePage: 'http://google.com/abc', + iconUri: 'https://reapit-app-store-app-media.s3.eu-west-2.amazonaws.com/d10e790c-2bf2-40ae-9c43-82c1534bde31.png', + links: [ + { + rel: 'self', + href: 'http://platformdemo.reapit.net/marketplace/apps/09043eb8-9e5e-4650-b7f1-f0cb62699027', + action: 'GET', + }, + { + rel: 'developer', + href: 'http://platformdemo.reapit.net/marketplace/developers/28c9ea52-7f73-4814-9e00-4e3714b8adeb', + action: 'GET', + }, + ], + }, + { + id: 'id1', + developerId: '28c9ea52-7f73-4814-9e00-4e3714b8adeb', + summary: + 'nXXT2zaK807ysWgy8F0WEhIYRP3TgosAtfuiLtQCImoSx0kynxbIF0nkGHU36Oz13kM3DG0Bcsic' + + 'r8L6eWFKLBg4axlmiOEWcvwHAbBP9LRvoFkCl58k1wjhOExnpaZItEyOT1AXVKv8PE44aMGtVz', + developer: "Pete's Proptech World Ltd", + homePage: 'http://google.com/abc', + iconUri: 'https://reapit-app-store-app-media.s3.eu-west-2.amazonaws.com/d10e790c-2bf2-40ae-9c43-82c1534bde31.png', + links: [ + { + rel: 'self', + href: 'http://platformdemo.reapit.net/marketplace/apps/09043eb8-9e5e-4650-b7f1-f0cb62699027', + action: 'GET', + }, + { + rel: 'developer', + href: 'http://platformdemo.reapit.net/marketplace/developers/28c9ea52-7f73-4814-9e00-4e3714b8adeb', + action: 'GET', + }, + ], + }, + ] + it('should return correctly', () => { + const result = countAppNoInstallation(developerDataArray) + expect(result).toEqual({ app2: 0 }) }) }) diff --git a/packages/marketplace/src/components/pages/analytics.tsx b/packages/marketplace/src/components/pages/analytics.tsx index 8a66fbea98..44f573a422 100644 --- a/packages/marketplace/src/components/pages/analytics.tsx +++ b/packages/marketplace/src/components/pages/analytics.tsx @@ -1,34 +1,17 @@ import * as React from 'react' import ErrorBoundary from '@/components/hocs/error-boundary' -import { - Table, - FlexContainerBasic, - H3, - H5, - SubTitleH5, - SelectBox, - SelectBoxOptions, - Formik, - Form, - Loader, - toLocalTime, - Pagination, - setQueryParams, -} from '@reapit/elements' +import { Table, FlexContainerBasic, H3, H4, Loader, toLocalTime, Pagination } from '@reapit/elements' import { connect } from 'react-redux' import { ReduxState } from '@/types/core' -import { AppSummaryModel, InstallationModel, PagedResultInstallationModel_ } from '@reapit/foundations-ts-definitions' +import { InstallationModel, AppSummaryModel } from '@reapit/foundations-ts-definitions' import { DeveloperState } from '@/reducers/developer' import { AppInstallationsState } from '@/reducers/app-installations' -import { fetchInstallations } from '@/sagas/app-installations' import { INSTALLATIONS_PER_PAGE } from '@/constants/paginator' -import { AppDetailState } from '@/reducers/app-detail' -import { RouteComponentProps, withRouter } from 'react-router' -import Routes from '@/constants/routes' -import { Dispatch } from 'redux' -import { appDetailRequestData } from '@/actions/app-detail' +import { withRouter } from 'react-router' +import styles from '@/styles/pages/analytics.scss?mod' -const COLUMNS = [ +export const installationTableColumn = [ + { Header: 'App Name', accessor: 'appName' }, { Header: 'Client', accessor: 'client', @@ -46,145 +29,170 @@ const COLUMNS = [ ] export interface AnalyticsPageMappedProps { - appsOfDeveloper?: DeveloperState - appInstallations?: AppInstallationsState | null - appDetail: AppDetailState + developer: DeveloperState + installations: AppInstallationsState } export interface AnalyticsPageMappedActions { requestAppDetailData: (data) => void } -export type AnalyticsPageProps = AnalyticsPageMappedProps & - AnalyticsPageMappedActions & - RouteComponentProps<{ page: any }> +export interface InstallationModelWithAppName extends InstallationModel { + appName?: string +} + +export type AnalyticsPageProps = AnalyticsPageMappedProps + +export const handleMapAppNameToInstallation = ( + installationsAppDataArray: InstallationModel[], + developerDataArray: AppSummaryModel[], +) => (): InstallationModelWithAppName[] => + installationsAppDataArray.map(installation => { + const app = developerDataArray.find(app => app.id === installation.appId) + return { + ...installation, + appName: app?.name, + } + }) -export const transformListAppToSelectBoxOptions = (app: AppSummaryModel) => { - return { label: app.name ?? 'Error', value: app.id ?? 'Error' } +export const sortAppByDateInstalled = ( + installationsAppDataArrayWithName: InstallationModelWithAppName[], +): InstallationModelWithAppName[] => { + const newAppData = [...installationsAppDataArrayWithName].sort( + (a: InstallationModelWithAppName, b: InstallationModelWithAppName) => { + if (!a.created || !b.created) { + return 0 + } + const dateA = new Date(a.created).getTime() + const dateB = new Date(b.created).getTime() + return dateA < dateB ? 1 : -1 + }, + ) + return newAppData } -export const transformAppInstalationsToTableColumsCompatible = (appName?: string) => ( - appInstalls: InstallationModel, -) => { - return { ...appInstalls, appName } +export const handleUseMemoData = ( + installationsAppDataArrayWithName: InstallationModelWithAppName[], + pageNumber: number, +) => (): InstallationModelWithAppName[] => { + const sortedInstallationsAppDataArray = sortAppByDateInstalled(installationsAppDataArrayWithName) + const slicedInstallationAppDataArray = sortedInstallationsAppDataArray.slice( + (pageNumber - 1) * INSTALLATIONS_PER_PAGE, + pageNumber * INSTALLATIONS_PER_PAGE, + ) + return slicedInstallationAppDataArray } -export const handleSubmit = (history, appId, pageNumber = 1) => { - const query = setQueryParams({ appId }) ? `?${setQueryParams({ appId })}` : '' - history.push(`${Routes.DEVELOPER_ANALYTICS}/${pageNumber}${query}`) +/** + * Count installations for each app has installation + * E.g. return {appName1: 1, appName2: 0, appName3: 15} + */ +export const countAppHasInstallation = ( + installationsAppDataArrayWithName: InstallationModelWithAppName[], +): { [appName: string]: number } => + installationsAppDataArrayWithName.reduce((prevValue, { appName, terminatesOn }) => { + if (!appName || terminatesOn) { + return prevValue + } + const newValue = { ...prevValue } + if (prevValue[appName]) { + newValue[appName]++ + return newValue + } + newValue[appName] = 1 + return newValue + }, {}) as { [appName: string]: number } + +/** + * All app in here will show 0 because it's not installed + */ +export const countAppNoInstallation = (developerDataArray: AppSummaryModel[]): { [appName: string]: number } => + developerDataArray.reduce((prevValue, { name }) => { + if (!name) { + return prevValue + } + const newValue = { ...prevValue } + newValue[name] = 0 + return newValue + }, {}) as { [appName: string]: number } + +export const handleCountCurrentInstallationForEachApp = ( + installationAppDataArrayWithName: InstallationModelWithAppName[], + developerDataArray: AppSummaryModel[], +) => (): { [appName: string]: number } => { + const appHasInstallation = countAppHasInstallation(installationAppDataArrayWithName) + const appNoInstallation = countAppNoInstallation(developerDataArray) + return { + ...appNoInstallation, + ...appHasInstallation, + } } -export const AnalyticsPage: React.FC = ({ - appInstallations, - appsOfDeveloper, - appDetail, - requestAppDetailData, +export const handleSetPageNumber = setPageNumber => (pageNumber: number) => setPageNumber(pageNumber) + +export const InstallationTable: React.FC<{ installations: AppInstallationsState; developer: DeveloperState }> = ({ + installations, + developer, }) => { - // const pageNumber = match.params.page ? Number(match.params.page) : 1 - const [appId, setAppId] = React.useState('') const [pageNumber, setPageNumber] = React.useState(1) - const [installations, setInstallations] = React.useState() - const [fetchingInstallations, setFetchingInstallations] = React.useState(false) - - React.useEffect(() => { - if (appId) { - setFetchingInstallations(true) - requestAppDetailData({ id: appId }) - fetchInstallations({ appId: [appId], pageNumber, pageSize: INSTALLATIONS_PER_PAGE }).then(response => { - const isOnWrongPage = pageNumber > Math.ceil(response.totalCount / response.pageSize) - const notEmptyData = response.totalCount > 0 - setFetchingInstallations(false) - if (isOnWrongPage && notEmptyData) { - setPageNumber(1) - } else { - setInstallations(response) - } - }) - } else { - setInstallations({}) - } - }, [appId, pageNumber]) - if (appInstallations?.loading || !appInstallations?.installationsAppData) { + const installationAppDataArray = installations.installationsAppData?.data ?? [] + const developerDataArray = developer.developerData?.data?.data ?? [] + + const installationAppDataArrayWithName = React.useMemo( + handleMapAppNameToInstallation(installationAppDataArray, developerDataArray), + [installationAppDataArray, developerDataArray], + ) + const appCountEntries = React.useMemo( + handleCountCurrentInstallationForEachApp(installationAppDataArrayWithName, developerDataArray), + [installationAppDataArrayWithName, developerDataArray], + ) + const memoizedData = React.useMemo(handleUseMemoData(installationAppDataArrayWithName, pageNumber), [ + installationAppDataArray, + pageNumber, + ]) + return ( +
+

Installations

+

The installations table below shows the individuals 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 } return ( - -

Analytics

-
Total number of installations: {appInstallations.installationsAppData.totalCount}
- To see specific installations please select an App from the list below: - { - setAppId(values.appId) - }} - > - {({ submitForm }) => ( - <> -
submitForm()}> -
- ( - transformListAppToSelectBoxOptions, - ) ?? [] - } - /> -
- - {fetchingInstallations ? ( - - ) : ( - installations?.data && ( - <> -
-
- { - setPageNumber(pageNumber) - }} - pageSize={INSTALLATIONS_PER_PAGE} - totalCount={installations?.totalCount} - /> -
- Total number of installations for {appDetail?.appDetailData?.data?.name}:{' '} - {installations?.totalCount} -
- - ) - )} - - - )} - + +

Dashboard

+
+
) } export const mapStateToProps: (state: ReduxState) => AnalyticsPageMappedProps = state => ({ - appInstallations: state.installations, - appsOfDeveloper: state.developer, - appDetail: state.appDetail, -}) - -export const mapDispatchToProps = (dispatch: Dispatch) => ({ - requestAppDetailData: data => dispatch(appDetailRequestData(data)), + installations: state.installations, + developer: state.developer, }) -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AnalyticsPage)) +export default withRouter(connect(mapStateToProps)(AnalyticsPage)) diff --git a/packages/marketplace/src/constants/paginator.ts b/packages/marketplace/src/constants/paginator.ts index f6d7013d9d..12acf93adf 100644 --- a/packages/marketplace/src/constants/paginator.ts +++ b/packages/marketplace/src/constants/paginator.ts @@ -2,4 +2,5 @@ export const APPS_PER_PAGE = 12 export const REVISIONS_PER_PAGE = 12 export const FEATURED_APPS = 3 export const INSTALLED_APPS_PERPAGE = 20 -export const INSTALLATIONS_PER_PAGE = 10 +export const INSTALLATIONS_PER_PAGE = 5 +export const GET_ALL_PAGE_SIZE = 9999 diff --git a/packages/marketplace/src/styles/pages/analytics.scss b/packages/marketplace/src/styles/pages/analytics.scss new file mode 100644 index 0000000000..e850d9c21a --- /dev/null +++ b/packages/marketplace/src/styles/pages/analytics.scss @@ -0,0 +1,8 @@ +.hr { + margin: 0; + border: 1px solid rgba(0, 0, 0, 0.5); +} +.totalCount { + font-size: 1.2rem; + margin: 1rem 0; +} diff --git a/packages/marketplace/src/utils/__tests__/route-dispatcher.ts b/packages/marketplace/src/utils/__tests__/route-dispatcher.ts index 3375334e32..c4d81469f7 100644 --- a/packages/marketplace/src/utils/__tests__/route-dispatcher.ts +++ b/packages/marketplace/src/utils/__tests__/route-dispatcher.ts @@ -1,6 +1,7 @@ import routeDispatcher from '../route-dispatcher' import store from '../../core/store' import Routes from '../../constants/routes' +import { GET_ALL_PAGE_SIZE } from '../../constants/paginator' import { RouteValue } from '../../types/core' import { clientRequestData } from '../../actions/client' import { developerRequestData } from '../../actions/developer' @@ -74,13 +75,9 @@ describe('routeDispatcher', () => { expect(store.dispatch).toHaveBeenCalledWith(requestDeveloperData()) }) - it('should dispatch to appInstallationsRequestData for the analytics route', async () => { + it('should dispatch to appInstallationsRequestData & developerRequestData for the analytics route', async () => { await routeDispatcher(Routes.DEVELOPER_ANALYTICS as RouteValue) - expect(store.dispatch).toHaveBeenCalledWith(appInstallationsRequestData({})) - }) - - it('should dispatch to appInstallationsRequestData for the analytics paginate route', async () => { - await routeDispatcher(Routes.DEVELOPER_ANALYTICS as RouteValue) - expect(store.dispatch).toHaveBeenCalledWith(developerRequestData({ page: 1 })) + expect(store.dispatch).toHaveBeenCalledWith(appInstallationsRequestData({ pageSize: GET_ALL_PAGE_SIZE })) + expect(store.dispatch).toHaveBeenCalledWith(developerRequestData({ appsPerPage: GET_ALL_PAGE_SIZE, page: 1 })) }) }) diff --git a/packages/marketplace/src/utils/route-dispatcher.ts b/packages/marketplace/src/utils/route-dispatcher.ts index c76358e585..5ac9452b98 100644 --- a/packages/marketplace/src/utils/route-dispatcher.ts +++ b/packages/marketplace/src/utils/route-dispatcher.ts @@ -2,6 +2,7 @@ import { AdminDevManagementRequestDataValues } from './../actions/admin-dev-mana import { appDetailRequestData } from './../actions/app-detail' import { RouteValue, StringMap } from '../types/core' import Routes from '../constants/routes' +import { GET_ALL_PAGE_SIZE } from '../constants/paginator' import store from '../core/store' import { clientRequestData } from '../actions/client' import { myAppsRequestData } from '../actions/my-apps' @@ -46,8 +47,10 @@ const routeDispatcher = async (route: RouteValue, params?: StringMap, search?: s break case Routes.DEVELOPER_ANALYTICS_PAGINATE: case Routes.DEVELOPER_ANALYTICS: - store.dispatch(appInstallationsRequestData({})) - store.dispatch(developerRequestData({ page: 1, appsPerPage: 9999 })) + // 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 + store.dispatch(developerRequestData({ page: 1, appsPerPage: GET_ALL_PAGE_SIZE })) if (appId) { const clientId = selectClientId(store.state) store.dispatch(appDetailRequestData({ id: appId, clientId }))