diff --git a/awx/ui_next/src/components/AppContainer/AppContainer.jsx b/awx/ui_next/src/components/AppContainer/AppContainer.jsx index 18db51495b..e697d27de7 100644 --- a/awx/ui_next/src/components/AppContainer/AppContainer.jsx +++ b/awx/ui_next/src/components/AppContainer/AppContainer.jsx @@ -19,6 +19,7 @@ import { MeAPI, RootAPI } from '../../api'; import { useConfig, useAuthorizedPath } from '../../contexts/Config'; import { SESSION_TIMEOUT_KEY } from '../../constants'; import { isAuthenticated } from '../../util/auth'; +import issuePendoIdentity from '../../util/issuePendoIdentity'; import About from '../About'; import AlertModal from '../AlertModal'; import BrandLogo from './BrandLogo'; @@ -138,6 +139,13 @@ function AppContainer({ navRouteConfig = [], children }) { } }, [handleLogout, timeRemaining]); + useEffect(() => { + if ('analytics_status' in config) { + issuePendoIdentity(config); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config.analytics_status]); + const brandName = config?.license_info?.product_name; const alt = brandName ? t`${brandName} logo` : t`brand logo`; diff --git a/awx/ui_next/src/components/AppContainer/AppContainer.test.jsx b/awx/ui_next/src/components/AppContainer/AppContainer.test.jsx index 8ba1260dbe..fdbde02a75 100644 --- a/awx/ui_next/src/components/AppContainer/AppContainer.test.jsx +++ b/awx/ui_next/src/components/AppContainer/AppContainer.test.jsx @@ -4,19 +4,25 @@ import { mountWithContexts, waitForElement, } from '../../../testUtils/enzymeHelpers'; -import { ConfigAPI, MeAPI, RootAPI } from '../../api'; +import { MeAPI, RootAPI } from '../../api'; import { useAuthorizedPath } from '../../contexts/Config'; import AppContainer from './AppContainer'; jest.mock('../../api'); +jest.mock('../../util/bootstrapPendo'); + +global.pendo = { + initialize: jest.fn(), +}; describe('', () => { const version = '222'; beforeEach(() => { - ConfigAPI.read.mockResolvedValue({ + RootAPI.readAssetVariables.mockResolvedValue({ data: { - version, + BRAND_NAME: 'AWX', + PENDO_API_KEY: '', }, }); MeAPI.read.mockResolvedValue({ data: { results: [{}] } }); @@ -52,7 +58,22 @@ describe('', () => { {routeConfig.map(({ groupId }) => (
))} - + , + { + context: { + config: { + analytics_status: 'detailed', + ansible_version: null, + custom_virtualenvs: [], + version: '9000', + me: { is_superuser: true }, + toJSON: () => '/config/', + license_info: { + valid_key: true, + }, + }, + }, + } ); }); wrapper.update(); @@ -70,6 +91,31 @@ describe('', () => { expect(wrapper.find('#group_one').length).toBe(1); expect(wrapper.find('#group_two').length).toBe(1); + + expect(global.pendo.initialize).toHaveBeenCalledTimes(1); + }); + + test('expected content is rendered', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts(, { + context: { + config: { + analytics_status: 'off', + ansible_version: null, + custom_virtualenvs: [], + version: '9000', + me: { is_superuser: true }, + toJSON: () => '/config/', + license_info: { + valid_key: true, + }, + }, + }, + }); + }); + wrapper.update(); + expect(global.pendo.initialize).toHaveBeenCalledTimes(0); }); test('opening the about modal renders prefetched config data', async () => { diff --git a/awx/ui_next/src/contexts/Config.jsx b/awx/ui_next/src/contexts/Config.jsx index d2e184f32f..f95e5017a5 100644 --- a/awx/ui_next/src/contexts/Config.jsx +++ b/awx/ui_next/src/contexts/Config.jsx @@ -1,5 +1,5 @@ import React, { useCallback, useContext, useEffect, useMemo } from 'react'; -import { useLocation, useRouteMatch } from 'react-router-dom'; +import { useRouteMatch } from 'react-router-dom'; import { t } from '@lingui/macro'; @@ -22,15 +22,7 @@ export const useConfig = () => { }; export const ConfigProvider = ({ children }) => { - const { pathname } = useLocation(); - - const { - error: configError, - isLoading, - request, - result: config, - setValue: setConfig, - } = useRequest( + const { error: configError, isLoading, request, result: config } = useRequest( useCallback(async () => { const [ { data }, @@ -48,10 +40,8 @@ export const ConfigProvider = ({ children }) => { const { error, dismissError } = useDismissableError(configError); useEffect(() => { - if (pathname !== '/login') { - request(); - } - }, [request, pathname]); + request(); + }, [request]); useEffect(() => { if (error?.response?.status === 401) { @@ -59,10 +49,10 @@ export const ConfigProvider = ({ children }) => { } }, [error]); - const value = useMemo(() => ({ ...config, isLoading, setConfig }), [ + const value = useMemo(() => ({ ...config, request, isLoading }), [ config, + request, isLoading, - setConfig, ]); return ( diff --git a/awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionEdit.jsx b/awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionEdit.jsx index 272e808d35..ef0c8680f9 100644 --- a/awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionEdit.jsx +++ b/awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionEdit.jsx @@ -12,13 +12,12 @@ import { WizardContextConsumer, WizardFooter, } from '@patternfly/react-core'; -import { ConfigAPI, SettingsAPI, MeAPI, RootAPI } from '../../../../api'; +import { ConfigAPI, SettingsAPI, RootAPI } from '../../../../api'; import useRequest, { useDismissableError } from '../../../../util/useRequest'; import ContentLoading from '../../../../components/ContentLoading'; import ContentError from '../../../../components/ContentError'; import { FormSubmitError } from '../../../../components/FormField'; import { useConfig } from '../../../../contexts/Config'; -import issuePendoIdentity from './pendoUtils'; import SubscriptionStep from './SubscriptionStep'; import AnalyticsStep from './AnalyticsStep'; import EulaStep from './EulaStep'; @@ -92,7 +91,7 @@ const CustomFooter = ({ isSubmitLoading }) => { function SubscriptionEdit() { const history = useHistory(); - const { license_info, setConfig } = useConfig(); + const { request: updateConfig, license_info } = useConfig(); const hasValidKey = Boolean(license_info?.valid_key); const subscriptionMgmtRoute = useRouteMatch({ path: '/subscription_management', @@ -102,20 +101,18 @@ function SubscriptionEdit() { isLoading: isContentLoading, error: contentError, request: fetchContent, - result: { brandName, pendoApiKey }, + result: { brandName }, } = useRequest( useCallback(async () => { const { - data: { BRAND_NAME, PENDO_API_KEY }, + data: { BRAND_NAME }, } = await RootAPI.readAssetVariables(); return { brandName: BRAND_NAME, - pendoApiKey: PENDO_API_KEY, }; }, []), { brandName: null, - pendoApiKey: null, } ); @@ -145,23 +142,11 @@ function SubscriptionEdit() { }); } - const [ - { data }, - { - data: { - results: [me], - }, - }, - ] = await Promise.all([ConfigAPI.read(), MeAPI.read()]); - const newConfig = { ...data, me }; - setConfig(newConfig); - if (!hasValidKey) { if (form.pendo) { await SettingsAPI.updateCategory('ui', { PENDO_TRACKING_STATE: 'detailed', }); - await issuePendoIdentity(newConfig, pendoApiKey); } else { await SettingsAPI.updateCategory('ui', { PENDO_TRACKING_STATE: 'off', @@ -178,6 +163,9 @@ function SubscriptionEdit() { }); } } + + await updateConfig(); + return true; }, []) // eslint-disable-line react-hooks/exhaustive-deps ); diff --git a/awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionEdit.test.jsx b/awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionEdit.test.jsx index ee3d3d3395..9f69eb9fd5 100644 --- a/awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionEdit.test.jsx +++ b/awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionEdit.test.jsx @@ -14,7 +14,6 @@ import { } from '../../../../api'; import SubscriptionEdit from './SubscriptionEdit'; -jest.mock('./bootstrapPendo'); jest.mock('../../../../api'); const mockConfig = { @@ -53,7 +52,7 @@ const emptyConfig = { license_info: { valid_key: false, }, - setConfig: jest.fn(), + request: jest.fn(), }; describe('', () => { @@ -269,7 +268,7 @@ describe('', () => { context: { config: { mockConfig, - setConfig: jest.fn(), + request: jest.fn(), }, me: { is_superuser: true, diff --git a/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx b/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx index d397a66c2d..803a56f9f7 100644 --- a/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx +++ b/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useCallback } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useHistory, useLocation } from 'react-router-dom'; import { t } from '@lingui/macro'; import { Button } from '@patternfly/react-core'; @@ -19,6 +19,12 @@ import { SettingDetail } from '../../shared'; function UIDetail() { const { me } = useConfig(); const { GET: options } = useSettings(); + const history = useHistory(); + const { hardReload } = useLocation(); + + if (hardReload) { + history.go(); + } const { isLoading, error, request, result: ui } = useRequest( useCallback(async () => { diff --git a/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.jsx b/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.jsx index 42a85f57cb..f1b6e64309 100644 --- a/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.jsx +++ b/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.jsx @@ -49,9 +49,18 @@ function UIEdit() { useCallback( async values => { await SettingsAPI.updateAll(values); - history.push('/settings/ui/details'); + if ( + values?.PENDO_TRACKING_STATE !== uiData?.PENDO_TRACKING_STATE?.value + ) { + history.push({ + pathname: '/settings/ui/details', + hardReload: true, + }); + } else { + history.push('/settings/ui/details'); + } }, - [history] + [history, uiData] ), null ); diff --git a/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.test.jsx b/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.test.jsx index 753075936a..7a607f476b 100644 --- a/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.test.jsx +++ b/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.test.jsx @@ -111,6 +111,21 @@ describe('', () => { wrapper.find('Form').invoke('onSubmit')(); }); expect(history.location.pathname).toEqual('/settings/ui/details'); + expect(history.location.hardReload).toEqual(undefined); + }); + + test('should navigate to ui detail with reload param on successful submission where PENDO_TRACKING_STATE changes', async () => { + act(() => { + wrapper.find('select#PENDO_TRACKING_STATE').simulate('change', { + target: { value: 'off', name: 'CUSTOM_LOGIN_INFO' }, + }); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual('/settings/ui/details'); + expect(history.location.hardReload).toEqual(true); }); test('should navigate to ui detail when cancel is clicked', async () => { diff --git a/awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/bootstrapPendo.js b/awx/ui_next/src/util/bootstrapPendo.js similarity index 100% rename from awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/bootstrapPendo.js rename to awx/ui_next/src/util/bootstrapPendo.js diff --git a/awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/pendoUtils.js b/awx/ui_next/src/util/issuePendoIdentity.js similarity index 72% rename from awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/pendoUtils.js rename to awx/ui_next/src/util/issuePendoIdentity.js index e03cb4c53c..380a796113 100644 --- a/awx/ui_next/src/screens/Setting/Subscription/SubscriptionEdit/pendoUtils.js +++ b/awx/ui_next/src/util/issuePendoIdentity.js @@ -1,30 +1,26 @@ -import { UsersAPI } from '../../../../api'; +import { RootAPI, UsersAPI } from '../api'; import bootstrapPendo from './bootstrapPendo'; function buildPendoOptions(config, pendoApiKey) { - const tower_version = config.version.split('-')[0]; + const towerVersion = config.version.split('-')[0]; const trial = config.trial ? config.trial : false; - const options = { + + return { apiKey: pendoApiKey, visitor: { - id: null, + id: 0, role: null, }, account: { - id: null, + id: 'tower.ansible.com', planLevel: config.license_type, planPrice: config.instance_count, creationDate: config.license_date, trial, - tower_version, + tower_version: towerVersion, ansible_version: config.ansible_version, }, }; - - options.visitor.id = 0; - options.account.id = 'tower.ansible.com'; - - return options; } async function buildPendoOptionsRole(options, config) { @@ -45,14 +41,20 @@ async function buildPendoOptionsRole(options, config) { } } -async function issuePendoIdentity(config, pendoApiKey) { +async function issuePendoIdentity(config) { + if (!('license_info' in config)) { + config.license_info = {}; + } config.license_info.analytics_status = config.analytics_status; config.license_info.version = config.version; config.license_info.ansible_version = config.ansible_version; if (config.analytics_status !== 'off') { - bootstrapPendo(pendoApiKey); - const pendoOptions = buildPendoOptions(config, pendoApiKey); + const { + data: { PENDO_API_KEY }, + } = await RootAPI.readAssetVariables(); + bootstrapPendo(PENDO_API_KEY); + const pendoOptions = buildPendoOptions(config, PENDO_API_KEY); const pendoOptionsWithRole = await buildPendoOptionsRole( pendoOptions, config