mirror of
https://github.com/ansible/awx.git
synced 2026-03-23 11:55:04 -02: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:
@@ -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);
|
||||||
|
}
|
||||||
|
// 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,15 +22,7 @@ export const useConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ConfigProvider = ({ children }) => {
|
export const ConfigProvider = ({ children }) => {
|
||||||
const { pathname } = useLocation();
|
const { error: configError, isLoading, request, result: config } = useRequest(
|
||||||
|
|
||||||
const {
|
|
||||||
error: configError,
|
|
||||||
isLoading,
|
|
||||||
request,
|
|
||||||
result: config,
|
|
||||||
setValue: setConfig,
|
|
||||||
} = useRequest(
|
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const [
|
const [
|
||||||
{ data },
|
{ data },
|
||||||
@@ -48,10 +40,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) {
|
||||||
@@ -59,10 +49,10 @@ export const ConfigProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
const value = useMemo(() => ({ ...config, isLoading, setConfig }), [
|
const value = useMemo(() => ({ ...config, request, isLoading }), [
|
||||||
config,
|
config,
|
||||||
|
request,
|
||||||
isLoading,
|
isLoading,
|
||||||
setConfig,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,13 +12,12 @@ import {
|
|||||||
WizardContextConsumer,
|
WizardContextConsumer,
|
||||||
WizardFooter,
|
WizardFooter,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { ConfigAPI, SettingsAPI, MeAPI, RootAPI } from '../../../../api';
|
import { ConfigAPI, SettingsAPI, RootAPI } from '../../../../api';
|
||||||
import useRequest, { useDismissableError } from '../../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../../util/useRequest';
|
||||||
import ContentLoading from '../../../../components/ContentLoading';
|
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';
|
||||||
@@ -92,7 +91,7 @@ const CustomFooter = ({ isSubmitLoading }) => {
|
|||||||
|
|
||||||
function SubscriptionEdit() {
|
function SubscriptionEdit() {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { license_info, setConfig } = useConfig();
|
const { request: updateConfig, license_info } = useConfig();
|
||||||
const hasValidKey = Boolean(license_info?.valid_key);
|
const hasValidKey = Boolean(license_info?.valid_key);
|
||||||
const subscriptionMgmtRoute = useRouteMatch({
|
const subscriptionMgmtRoute = useRouteMatch({
|
||||||
path: '/subscription_management',
|
path: '/subscription_management',
|
||||||
@@ -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,9 @@ function SubscriptionEdit() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await updateConfig();
|
||||||
|
|
||||||
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 = {
|
||||||
@@ -53,7 +52,7 @@ const emptyConfig = {
|
|||||||
license_info: {
|
license_info: {
|
||||||
valid_key: false,
|
valid_key: false,
|
||||||
},
|
},
|
||||||
setConfig: jest.fn(),
|
request: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('<SubscriptionEdit />', () => {
|
describe('<SubscriptionEdit />', () => {
|
||||||
@@ -269,7 +268,7 @@ describe('<SubscriptionEdit />', () => {
|
|||||||
context: {
|
context: {
|
||||||
config: {
|
config: {
|
||||||
mockConfig,
|
mockConfig,
|
||||||
setConfig: jest.fn(),
|
request: jest.fn(),
|
||||||
},
|
},
|
||||||
me: {
|
me: {
|
||||||
is_superuser: true,
|
is_superuser: true,
|
||||||
|
|||||||
@@ -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