mirror of
https://github.com/ansible/awx.git
synced 2026-03-18 09:27:31 -02:30
Adds support for pendo initialization across the app
This commit is contained in:
@@ -19,6 +19,7 @@ import { MeAPI, RootAPI } from '../../api';
|
|||||||
import { useConfig, useAuthorizedPath } from '../../contexts/Config';
|
import { useConfig, useAuthorizedPath } from '../../contexts/Config';
|
||||||
import { SESSION_TIMEOUT_KEY } from '../../constants';
|
import { SESSION_TIMEOUT_KEY } from '../../constants';
|
||||||
import { isAuthenticated } from '../../util/auth';
|
import { isAuthenticated } from '../../util/auth';
|
||||||
|
import issuePendoIdentity from '../../util/issuePendoIdentity';
|
||||||
import About from '../About';
|
import About from '../About';
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import BrandLogo from './BrandLogo';
|
import BrandLogo from './BrandLogo';
|
||||||
@@ -138,6 +139,13 @@ function AppContainer({ navRouteConfig = [], children }) {
|
|||||||
}
|
}
|
||||||
}, [handleLogout, timeRemaining]);
|
}, [handleLogout, timeRemaining]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ('analytics_status' in config) {
|
||||||
|
issuePendoIdentity(config, 'foobar');
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [config.analytics_status]);
|
||||||
|
|
||||||
const brandName = config?.license_info?.product_name;
|
const brandName = config?.license_info?.product_name;
|
||||||
const alt = brandName ? t`${brandName} logo` : t`brand logo`;
|
const alt = brandName ? t`${brandName} logo` : t`brand logo`;
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,25 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../testUtils/enzymeHelpers';
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
import { ConfigAPI, MeAPI, RootAPI } from '../../api';
|
import { MeAPI, RootAPI } from '../../api';
|
||||||
import { useAuthorizedPath } from '../../contexts/Config';
|
import { useAuthorizedPath } from '../../contexts/Config';
|
||||||
import AppContainer from './AppContainer';
|
import AppContainer from './AppContainer';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
|
jest.mock('../../util/bootstrapPendo');
|
||||||
|
|
||||||
|
global.pendo = {
|
||||||
|
initialize: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
describe('<AppContainer />', () => {
|
describe('<AppContainer />', () => {
|
||||||
const version = '222';
|
const version = '222';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ConfigAPI.read.mockResolvedValue({
|
RootAPI.readAssetVariables.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
version,
|
BRAND_NAME: 'AWX',
|
||||||
|
PENDO_API_KEY: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
MeAPI.read.mockResolvedValue({ data: { results: [{}] } });
|
MeAPI.read.mockResolvedValue({ data: { results: [{}] } });
|
||||||
@@ -52,7 +58,22 @@ describe('<AppContainer />', () => {
|
|||||||
{routeConfig.map(({ groupId }) => (
|
{routeConfig.map(({ groupId }) => (
|
||||||
<div key={groupId} id={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();
|
wrapper.update();
|
||||||
@@ -70,6 +91,31 @@ describe('<AppContainer />', () => {
|
|||||||
|
|
||||||
expect(wrapper.find('#group_one').length).toBe(1);
|
expect(wrapper.find('#group_one').length).toBe(1);
|
||||||
expect(wrapper.find('#group_two').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 () => {
|
test('opening the about modal renders prefetched config data', async () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useContext, useEffect, useMemo } from 'react';
|
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';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
@@ -22,8 +22,6 @@ export const useConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ConfigProvider = ({ children }) => {
|
export const ConfigProvider = ({ children }) => {
|
||||||
const { pathname } = useLocation();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
error: configError,
|
error: configError,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -48,10 +46,8 @@ export const ConfigProvider = ({ children }) => {
|
|||||||
const { error, dismissError } = useDismissableError(configError);
|
const { error, dismissError } = useDismissableError(configError);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pathname !== '/login') {
|
request();
|
||||||
request();
|
}, [request]);
|
||||||
}
|
|
||||||
}, [request, pathname]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error?.response?.status === 401) {
|
if (error?.response?.status === 401) {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import ContentLoading from '../../../../components/ContentLoading';
|
|||||||
import ContentError from '../../../../components/ContentError';
|
import ContentError from '../../../../components/ContentError';
|
||||||
import { FormSubmitError } from '../../../../components/FormField';
|
import { FormSubmitError } from '../../../../components/FormField';
|
||||||
import { useConfig } from '../../../../contexts/Config';
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
import issuePendoIdentity from './pendoUtils';
|
|
||||||
import SubscriptionStep from './SubscriptionStep';
|
import SubscriptionStep from './SubscriptionStep';
|
||||||
import AnalyticsStep from './AnalyticsStep';
|
import AnalyticsStep from './AnalyticsStep';
|
||||||
import EulaStep from './EulaStep';
|
import EulaStep from './EulaStep';
|
||||||
@@ -102,20 +101,18 @@ function SubscriptionEdit() {
|
|||||||
isLoading: isContentLoading,
|
isLoading: isContentLoading,
|
||||||
error: contentError,
|
error: contentError,
|
||||||
request: fetchContent,
|
request: fetchContent,
|
||||||
result: { brandName, pendoApiKey },
|
result: { brandName },
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const {
|
const {
|
||||||
data: { BRAND_NAME, PENDO_API_KEY },
|
data: { BRAND_NAME },
|
||||||
} = await RootAPI.readAssetVariables();
|
} = await RootAPI.readAssetVariables();
|
||||||
return {
|
return {
|
||||||
brandName: BRAND_NAME,
|
brandName: BRAND_NAME,
|
||||||
pendoApiKey: PENDO_API_KEY,
|
|
||||||
};
|
};
|
||||||
}, []),
|
}, []),
|
||||||
{
|
{
|
||||||
brandName: null,
|
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 (!hasValidKey) {
|
||||||
if (form.pendo) {
|
if (form.pendo) {
|
||||||
await SettingsAPI.updateCategory('ui', {
|
await SettingsAPI.updateCategory('ui', {
|
||||||
PENDO_TRACKING_STATE: 'detailed',
|
PENDO_TRACKING_STATE: 'detailed',
|
||||||
});
|
});
|
||||||
await issuePendoIdentity(newConfig, pendoApiKey);
|
|
||||||
} else {
|
} else {
|
||||||
await SettingsAPI.updateCategory('ui', {
|
await SettingsAPI.updateCategory('ui', {
|
||||||
PENDO_TRACKING_STATE: 'off',
|
PENDO_TRACKING_STATE: 'off',
|
||||||
@@ -178,6 +163,18 @@ function SubscriptionEdit() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
{ data },
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
results: [me],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
|
||||||
|
const newConfig = { ...data, me };
|
||||||
|
setConfig(newConfig);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from '../../../../api';
|
} from '../../../../api';
|
||||||
import SubscriptionEdit from './SubscriptionEdit';
|
import SubscriptionEdit from './SubscriptionEdit';
|
||||||
|
|
||||||
jest.mock('./bootstrapPendo');
|
|
||||||
jest.mock('../../../../api');
|
jest.mock('../../../../api');
|
||||||
|
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useCallback } from 'react';
|
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 { t } from '@lingui/macro';
|
||||||
import { Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
@@ -19,6 +19,12 @@ import { SettingDetail } from '../../shared';
|
|||||||
function UIDetail() {
|
function UIDetail() {
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
const { GET: options } = useSettings();
|
const { GET: options } = useSettings();
|
||||||
|
const history = useHistory();
|
||||||
|
const { hardReload } = useLocation();
|
||||||
|
|
||||||
|
if (hardReload) {
|
||||||
|
history.go();
|
||||||
|
}
|
||||||
|
|
||||||
const { isLoading, error, request, result: ui } = useRequest(
|
const { isLoading, error, request, result: ui } = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
|
|||||||
@@ -49,9 +49,18 @@ function UIEdit() {
|
|||||||
useCallback(
|
useCallback(
|
||||||
async values => {
|
async values => {
|
||||||
await SettingsAPI.updateAll(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
|
null
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -111,6 +111,21 @@ describe('<UIEdit />', () => {
|
|||||||
wrapper.find('Form').invoke('onSubmit')();
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
});
|
});
|
||||||
expect(history.location.pathname).toEqual('/settings/ui/details');
|
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 () => {
|
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';
|
import bootstrapPendo from './bootstrapPendo';
|
||||||
|
|
||||||
function buildPendoOptions(config, pendoApiKey) {
|
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 trial = config.trial ? config.trial : false;
|
||||||
const options = {
|
|
||||||
|
return {
|
||||||
apiKey: pendoApiKey,
|
apiKey: pendoApiKey,
|
||||||
visitor: {
|
visitor: {
|
||||||
id: null,
|
id: 0,
|
||||||
role: null,
|
role: null,
|
||||||
},
|
},
|
||||||
account: {
|
account: {
|
||||||
id: null,
|
id: 'tower.ansible.com',
|
||||||
planLevel: config.license_type,
|
planLevel: config.license_type,
|
||||||
planPrice: config.instance_count,
|
planPrice: config.instance_count,
|
||||||
creationDate: config.license_date,
|
creationDate: config.license_date,
|
||||||
trial,
|
trial,
|
||||||
tower_version,
|
tower_version: towerVersion,
|
||||||
ansible_version: config.ansible_version,
|
ansible_version: config.ansible_version,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
options.visitor.id = 0;
|
|
||||||
options.account.id = 'tower.ansible.com';
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildPendoOptionsRole(options, config) {
|
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.analytics_status = config.analytics_status;
|
||||||
config.license_info.version = config.version;
|
config.license_info.version = config.version;
|
||||||
config.license_info.ansible_version = config.ansible_version;
|
config.license_info.ansible_version = config.ansible_version;
|
||||||
|
|
||||||
if (config.analytics_status !== 'off') {
|
if (config.analytics_status !== 'off') {
|
||||||
bootstrapPendo(pendoApiKey);
|
const {
|
||||||
const pendoOptions = buildPendoOptions(config, pendoApiKey);
|
data: { PENDO_API_KEY },
|
||||||
|
} = await RootAPI.readAssetVariables();
|
||||||
|
bootstrapPendo(PENDO_API_KEY);
|
||||||
|
const pendoOptions = buildPendoOptions(config, PENDO_API_KEY);
|
||||||
const pendoOptionsWithRole = await buildPendoOptionsRole(
|
const pendoOptionsWithRole = await buildPendoOptionsRole(
|
||||||
pendoOptions,
|
pendoOptions,
|
||||||
config
|
config
|
||||||
Reference in New Issue
Block a user