mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 01:57:35 -03:30
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:
commit
df3bd2e082
@ -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`;
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user