Merge pull request #10133 from mabashian/ui-next-pendo

Adds support for pendo initialization across the app

SUMMARY
We were already bootstrapping pendo as part of the subscription code I just moved that code to a more general place.  When the app container mounts (after login or on refresh) we check to see if the pendo flag is turned on.  If it is, we initialize pendo.  If it's not then we do nothing.  If a user goes into settings and manually changes the pendo tracking setting then we trigger a hard reload of the browser tab (to take the new setting into account and either initialize or not).  This functionality existed in the old UI as well.
ISSUE TYPE

Feature Pull Request

COMPONENT NAME

UI

Reviewed-by: Michael Abashian <None>
Reviewed-by: Kersom <None>
Reviewed-by: Tiago Góes <tiago.goes2009@gmail.com>
This commit is contained in:
softwarefactory-project-zuul[bot] 2021-05-18 14:51:07 +00:00 committed by GitHub
commit df3bd2e082
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 122 additions and 59 deletions

View File

@ -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`;

View File

@ -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('<AppContainer />', () => {
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('<AppContainer />', () => {
{routeConfig.map(({ groupId }) => (
<div key={groupId} id={groupId} />
))}
</AppContainer>
</AppContainer>,
{
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('<AppContainer />', () => {
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(<AppContainer />, {
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 () => {

View File

@ -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 (

View File

@ -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
);

View File

@ -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('<SubscriptionEdit />', () => {
@ -269,7 +268,7 @@ describe('<SubscriptionEdit />', () => {
context: {
config: {
mockConfig,
setConfig: jest.fn(),
request: jest.fn(),
},
me: {
is_superuser: true,

View File

@ -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 () => {

View File

@ -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
);

View File

@ -111,6 +111,21 @@ describe('<UIEdit />', () => {
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 () => {

View File

@ -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