mirror of
https://github.com/ansible/awx.git
synced 2026-02-28 16:28:43 -03:30
Add setting system category forms and tests
* Add activity stream, logging, and misc system forms * Hookup logging test alert * Hookup revert buttons * Add useModal helper hook * Swap VariablesDetail for CodeDetail within setting detail views * Update SettingDetail import path in setting detail views
This commit is contained in:
@@ -21,6 +21,10 @@ class Settings extends Base {
|
|||||||
readCategoryOptions(category) {
|
readCategoryOptions(category) {
|
||||||
return this.http.options(`${this.baseUrl}${category}/`);
|
return this.http.options(`${this.baseUrl}${category}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test(category, data) {
|
||||||
|
return this.http.post(`${this.baseUrl}${category}/test/`, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import 'styled-components/macro';
|
import 'styled-components/macro';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shape, node, number, oneOf, string } from 'prop-types';
|
import {
|
||||||
|
arrayOf,
|
||||||
|
oneOf,
|
||||||
|
oneOfType,
|
||||||
|
node,
|
||||||
|
number,
|
||||||
|
shape,
|
||||||
|
string,
|
||||||
|
} from 'prop-types';
|
||||||
import { TextListItemVariants } from '@patternfly/react-core';
|
import { TextListItemVariants } from '@patternfly/react-core';
|
||||||
import { DetailName, DetailValue } from './Detail';
|
import { DetailName, DetailValue } from './Detail';
|
||||||
import CodeMirrorInput from '../CodeMirrorInput';
|
import CodeMirrorInput from '../CodeMirrorInput';
|
||||||
@@ -57,12 +65,12 @@ function CodeDetail({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
CodeDetail.propTypes = {
|
CodeDetail.propTypes = {
|
||||||
value: shape.isRequired,
|
value: oneOfType([shape({}), arrayOf(string), string]).isRequired,
|
||||||
label: node.isRequired,
|
label: node.isRequired,
|
||||||
dataCy: string,
|
dataCy: string,
|
||||||
helpText: string,
|
helpText: string,
|
||||||
rows: number,
|
rows: number,
|
||||||
mode: oneOf(['json', 'yaml', 'jinja2']).isRequired,
|
mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired,
|
||||||
};
|
};
|
||||||
CodeDetail.defaultProps = {
|
CodeDetail.defaultProps = {
|
||||||
rows: null,
|
rows: null,
|
||||||
|
|||||||
@@ -5,13 +5,7 @@ import { t } from '@lingui/macro';
|
|||||||
import { ActionGroup, Button } from '@patternfly/react-core';
|
import { ActionGroup, Button } from '@patternfly/react-core';
|
||||||
import { FormFullWidthLayout } from '../FormLayout';
|
import { FormFullWidthLayout } from '../FormLayout';
|
||||||
|
|
||||||
const FormActionGroup = ({
|
const FormActionGroup = ({ onCancel, onSubmit, submitDisabled, i18n }) => {
|
||||||
onCancel,
|
|
||||||
onRevert,
|
|
||||||
onSubmit,
|
|
||||||
submitDisabled,
|
|
||||||
i18n,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<FormFullWidthLayout>
|
<FormFullWidthLayout>
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
@@ -24,16 +18,6 @@ const FormActionGroup = ({
|
|||||||
>
|
>
|
||||||
{i18n._(t`Save`)}
|
{i18n._(t`Save`)}
|
||||||
</Button>
|
</Button>
|
||||||
{onRevert && (
|
|
||||||
<Button
|
|
||||||
aria-label={i18n._(t`Revert`)}
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
onClick={onRevert}
|
|
||||||
>
|
|
||||||
{i18n._(t`Revert`)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
aria-label={i18n._(t`Cancel`)}
|
aria-label={i18n._(t`Cancel`)}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -49,13 +33,11 @@ const FormActionGroup = ({
|
|||||||
|
|
||||||
FormActionGroup.propTypes = {
|
FormActionGroup.propTypes = {
|
||||||
onCancel: PropTypes.func.isRequired,
|
onCancel: PropTypes.func.isRequired,
|
||||||
onRevert: PropTypes.func,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
submitDisabled: PropTypes.bool,
|
submitDisabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
FormActionGroup.defaultProps = {
|
FormActionGroup.defaultProps = {
|
||||||
onRevert: null,
|
|
||||||
submitDisabled: false,
|
submitDisabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,19 @@ import styled from 'styled-components';
|
|||||||
const PopoverButton = styled.button`
|
const PopoverButton = styled.button`
|
||||||
padding: var(--pf-global--spacer--xs);
|
padding: var(--pf-global--spacer--xs);
|
||||||
margin: -(var(--pf-global--spacer--xs));
|
margin: -(var(--pf-global--spacer--xs));
|
||||||
|
font-size: var(--pf-global--FontSize--sm);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Popover({ i18n, ariaLabel, content, header, id, maxWidth, ...rest }) {
|
function Popover({
|
||||||
|
i18n,
|
||||||
|
i18nHash,
|
||||||
|
ariaLabel,
|
||||||
|
content,
|
||||||
|
header,
|
||||||
|
id,
|
||||||
|
maxWidth,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
import ContentError from '../../../components/ContentError';
|
import ContentError from '../../../components/ContentError';
|
||||||
|
import { useConfig } from '../../../contexts/Config';
|
||||||
import ActivityStreamDetail from './ActivityStreamDetail';
|
import ActivityStreamDetail from './ActivityStreamDetail';
|
||||||
import ActivityStreamEdit from './ActivityStreamEdit';
|
import ActivityStreamEdit from './ActivityStreamEdit';
|
||||||
|
|
||||||
function ActivityStream({ i18n }) {
|
function ActivityStream({ i18n }) {
|
||||||
const baseURL = '/settings/activity_stream';
|
const baseURL = '/settings/activity_stream';
|
||||||
|
const { me } = useConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -18,7 +21,11 @@ function ActivityStream({ i18n }) {
|
|||||||
<ActivityStreamDetail />
|
<ActivityStreamDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseURL}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<ActivityStreamEdit />
|
{me?.is_superuser ? (
|
||||||
|
<ActivityStreamEdit />
|
||||||
|
) : (
|
||||||
|
<Redirect to={`${baseURL}/details`} />
|
||||||
|
)}
|
||||||
</Route>
|
</Route>
|
||||||
<Route key="not-found" path={`${baseURL}/*`}>
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
<ContentError isNotFound>
|
<ContentError isNotFound>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import useRequest from '../../../../util/useRequest';
|
|||||||
import { useConfig } from '../../../../contexts/Config';
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import { SettingsAPI } from '../../../../api';
|
import { SettingsAPI } from '../../../../api';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
|
|
||||||
function ActivityStreamDetail({ i18n }) {
|
function ActivityStreamDetail({ i18n }) {
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
|
|||||||
@@ -1,25 +1,137 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { Formik } from 'formik';
|
||||||
import { t } from '@lingui/macro';
|
import { Form } from '@patternfly/react-core';
|
||||||
import { Button } from '@patternfly/react-core';
|
import { CardBody } from '../../../../components/Card';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import { FormSubmitError } from '../../../../components/FormField';
|
||||||
|
import { FormColumnLayout } from '../../../../components/FormLayout';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import {
|
||||||
|
BooleanField,
|
||||||
|
RevertAllAlert,
|
||||||
|
RevertFormActionGroup,
|
||||||
|
} from '../../shared';
|
||||||
|
import useModal from '../../../../util/useModal';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
|
||||||
|
function ActivityStreamEdit() {
|
||||||
|
const history = useHistory();
|
||||||
|
const { isModalOpen, toggleModal, closeModal } = useModal();
|
||||||
|
const { PUT: options } = useSettings();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
request,
|
||||||
|
result: {
|
||||||
|
ACTIVITY_STREAM_ENABLED,
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC,
|
||||||
|
},
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('system');
|
||||||
|
return {
|
||||||
|
ACTIVITY_STREAM_ENABLED: {
|
||||||
|
...options.ACTIVITY_STREAM_ENABLED,
|
||||||
|
value: data.ACTIVITY_STREAM_ENABLED,
|
||||||
|
},
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: {
|
||||||
|
...options.ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC,
|
||||||
|
value: data.ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [options]),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: submitError,
|
||||||
|
request: submitForm,
|
||||||
|
result: submitResult,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async values => {
|
||||||
|
const result = await SettingsAPI.updateAll(values);
|
||||||
|
return result;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submitResult) {
|
||||||
|
history.push('/settings/activity_stream/details');
|
||||||
|
}
|
||||||
|
}, [submitResult, history]);
|
||||||
|
|
||||||
|
const handleSubmit = async form => {
|
||||||
|
await submitForm(form);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.push('/settings/activity_stream/details');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevertAll = async () => {
|
||||||
|
const defaultValues = {
|
||||||
|
ACTIVITY_STREAM_ENABLED: ACTIVITY_STREAM_ENABLED.default,
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC:
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC.default,
|
||||||
|
};
|
||||||
|
await submitForm(defaultValues);
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
function ActivityStreamEdit({ i18n }) {
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{i18n._(t`Edit form coming soon :)`)}
|
{isLoading && <ContentLoading />}
|
||||||
<CardActionsRow>
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
<Button
|
{!isLoading && ACTIVITY_STREAM_ENABLED && (
|
||||||
aria-label={i18n._(t`Cancel`)}
|
<Formik
|
||||||
component={Link}
|
initialValues={{
|
||||||
to="/settings/activity_stream/details"
|
ACTIVITY_STREAM_ENABLED: ACTIVITY_STREAM_ENABLED.value,
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC:
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC.value,
|
||||||
|
}}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{formik => {
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||||
|
<FormColumnLayout>
|
||||||
|
<BooleanField
|
||||||
|
name="ACTIVITY_STREAM_ENABLED"
|
||||||
|
config={ACTIVITY_STREAM_ENABLED}
|
||||||
|
/>
|
||||||
|
<BooleanField
|
||||||
|
name="ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC"
|
||||||
|
config={ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC}
|
||||||
|
/>
|
||||||
|
{submitError && <FormSubmitError error={submitError} />}
|
||||||
|
</FormColumnLayout>
|
||||||
|
<RevertFormActionGroup
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onRevert={toggleModal}
|
||||||
|
/>
|
||||||
|
{isModalOpen && (
|
||||||
|
<RevertAllAlert
|
||||||
|
onClose={closeModal}
|
||||||
|
onRevertAll={handleRevertAll}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(ActivityStreamEdit);
|
export default ActivityStreamEdit;
|
||||||
|
|||||||
@@ -1,16 +1,135 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
import ActivityStreamEdit from './ActivityStreamEdit';
|
import ActivityStreamEdit from './ActivityStreamEdit';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
ACTIVITY_STREAM_ENABLED: false,
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
SettingsAPI.updateAll.mockResolvedValue({});
|
||||||
describe('<ActivityStreamEdit />', () => {
|
describe('<ActivityStreamEdit />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
let history;
|
||||||
wrapper = mountWithContexts(<ActivityStreamEdit />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/activity_stream/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<ActivityStreamEdit />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('ActivityStreamEdit').length).toBe(1);
|
expect(wrapper.find('ActivityStreamEdit').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should navigate to activity stream detail when cancel is clicked', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||||
|
});
|
||||||
|
expect(history.location.pathname).toEqual(
|
||||||
|
'/settings/activity_stream/details'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to activity stream detail on successful submission', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
expect(history.location.pathname).toEqual(
|
||||||
|
'/settings/activity_stream/details'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully send request to api on form submission', async () => {
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
|
||||||
|
ACTIVITY_STREAM_ENABLED: false,
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully send default values to api on form revert all', async () => {
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
|
||||||
|
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('button[aria-label="Revert all to default"]')
|
||||||
|
.invoke('onClick')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('RevertAllAlert')).toHaveLength(1);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('RevertAllAlert button[aria-label="Confirm revert all"]')
|
||||||
|
.invoke('onClick')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
|
||||||
|
ACTIVITY_STREAM_ENABLED: true,
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display error message on unsuccessful submission', async () => {
|
||||||
|
const error = {
|
||||||
|
response: {
|
||||||
|
data: { detail: 'An error occurred' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error));
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(0);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(1);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display ContentError on throw', async () => {
|
||||||
|
SettingsAPI.readCategory.mockImplementationOnce(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<ActivityStreamEdit />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,25 @@ import React from 'react';
|
|||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../contexts/Settings';
|
||||||
import { SettingsAPI } from '../../../api';
|
import { SettingsAPI } from '../../../api';
|
||||||
|
import mockAllOptions from '../shared/data.allSettingOptions.json';
|
||||||
import AzureAD from './AzureAD';
|
import AzureAD from './AzureAD';
|
||||||
|
|
||||||
jest.mock('../../../api/models/Settings');
|
jest.mock('../../../api/models/Settings');
|
||||||
SettingsAPI.readCategory.mockResolvedValue({
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
data: {},
|
data: {
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_CALLBACK_URL:
|
||||||
|
'https://towerhost/sso/complete/azuread-oauth2/',
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_KEY: 'mock key',
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET: '$encrypted$',
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP: {},
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP: {
|
||||||
|
'My Team': {
|
||||||
|
users: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('<AzureAD />', () => {
|
describe('<AzureAD />', () => {
|
||||||
@@ -23,9 +36,14 @@ describe('<AzureAD />', () => {
|
|||||||
initialEntries: ['/settings/azure/details'],
|
initialEntries: ['/settings/azure/details'],
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<AzureAD />, {
|
wrapper = mountWithContexts(
|
||||||
context: { router: { history } },
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
});
|
<AzureAD />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('AzureADDetail').length).toBe(1);
|
expect(wrapper.find('AzureADDetail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useConfig } from '../../../../contexts/Config';
|
|||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import useRequest from '../../../../util/useRequest';
|
import useRequest from '../../../../util/useRequest';
|
||||||
import { SettingsAPI } from '../../../../api';
|
import { SettingsAPI } from '../../../../api';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
|
|
||||||
function AzureADDetail({ i18n }) {
|
function AzureADDetail({ i18n }) {
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
|
|||||||
@@ -1,25 +1,146 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { Formik } from 'formik';
|
||||||
import { t } from '@lingui/macro';
|
import { Form } from '@patternfly/react-core';
|
||||||
import { Button } from '@patternfly/react-core';
|
import { CardBody } from '../../../../components/Card';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import { FormSubmitError } from '../../../../components/FormField';
|
||||||
|
import { FormColumnLayout } from '../../../../components/FormLayout';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import { RevertAllAlert, RevertFormActionGroup } from '../../shared';
|
||||||
|
import {
|
||||||
|
EncryptedField,
|
||||||
|
InputField,
|
||||||
|
ObjectField,
|
||||||
|
} from '../../shared/SharedFields';
|
||||||
|
import useModal from '../../../../util/useModal';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
|
||||||
|
function AzureADEdit() {
|
||||||
|
const history = useHistory();
|
||||||
|
const { isModalOpen, toggleModal, closeModal } = useModal();
|
||||||
|
const { PUT: options } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request: fetchAzureAD, result: azure } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('azuread-oauth2');
|
||||||
|
const mergedData = {};
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
if (!options[key]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mergedData[key] = options[key];
|
||||||
|
mergedData[key].value = data[key];
|
||||||
|
});
|
||||||
|
return mergedData;
|
||||||
|
}, [options]),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAzureAD();
|
||||||
|
}, [fetchAzureAD]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: submitError,
|
||||||
|
request: submitForm,
|
||||||
|
result: submitResult,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async values => {
|
||||||
|
const result = await SettingsAPI.updateAll(values);
|
||||||
|
return result;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submitResult) {
|
||||||
|
history.push('/settings/azure/details');
|
||||||
|
}
|
||||||
|
}, [submitResult, history]);
|
||||||
|
|
||||||
|
const handleSubmit = async form => {
|
||||||
|
await submitForm({
|
||||||
|
...form,
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP: JSON.parse(
|
||||||
|
form.SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP
|
||||||
|
),
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP: JSON.parse(
|
||||||
|
form.SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevertAll = async () => {
|
||||||
|
const defaultValues = Object.assign(
|
||||||
|
...Object.entries(azure).map(([key, value]) => ({
|
||||||
|
[key]: value.default,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
await submitForm(defaultValues);
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.push('/settings/azure/details');
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialValues = fields =>
|
||||||
|
Object.keys(fields).reduce((acc, key) => {
|
||||||
|
if (fields[key].type === 'list' || fields[key].type === 'nested object') {
|
||||||
|
acc[key] = JSON.stringify(fields[key].value, null, 2);
|
||||||
|
} else {
|
||||||
|
acc[key] = fields[key].value ?? '';
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
function AzureADEdit({ i18n }) {
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{i18n._(t`Edit form coming soon :)`)}
|
{isLoading && <ContentLoading />}
|
||||||
<CardActionsRow>
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
<Button
|
{!isLoading && azure && (
|
||||||
aria-label={i18n._(t`Cancel`)}
|
<Formik initialValues={initialValues(azure)} onSubmit={handleSubmit}>
|
||||||
component={Link}
|
{formik => (
|
||||||
to="/settings/azure/details"
|
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||||
>
|
<FormColumnLayout>
|
||||||
{i18n._(t`Cancel`)}
|
<InputField
|
||||||
</Button>
|
name="SOCIAL_AUTH_AZUREAD_OAUTH2_KEY"
|
||||||
</CardActionsRow>
|
config={azure.SOCIAL_AUTH_AZUREAD_OAUTH2_KEY}
|
||||||
|
/>
|
||||||
|
<EncryptedField
|
||||||
|
name="SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET"
|
||||||
|
config={azure.SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET}
|
||||||
|
/>
|
||||||
|
<ObjectField
|
||||||
|
name="SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP"
|
||||||
|
config={azure.SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP}
|
||||||
|
/>
|
||||||
|
<ObjectField
|
||||||
|
name="SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP"
|
||||||
|
config={azure.SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP}
|
||||||
|
/>
|
||||||
|
{submitError && <FormSubmitError error={submitError} />}
|
||||||
|
</FormColumnLayout>
|
||||||
|
<RevertFormActionGroup
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onRevert={toggleModal}
|
||||||
|
/>
|
||||||
|
{isModalOpen && (
|
||||||
|
<RevertAllAlert
|
||||||
|
onClose={closeModal}
|
||||||
|
onRevertAll={handleRevertAll}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(AzureADEdit);
|
export default AzureADEdit;
|
||||||
|
|||||||
@@ -1,16 +1,147 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
import AzureADEdit from './AzureADEdit';
|
import AzureADEdit from './AzureADEdit';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.updateAll.mockResolvedValue({});
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_CALLBACK_URL:
|
||||||
|
'https://towerhost/sso/complete/azuread-oauth2/',
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_KEY: 'mock key',
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET: '$encrypted$',
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP: {},
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP: {
|
||||||
|
'My Team': {
|
||||||
|
organization: 'foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<AzureADEdit />', () => {
|
describe('<AzureADEdit />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
let history;
|
||||||
wrapper = mountWithContexts(<AzureADEdit />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/azure/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<AzureADEdit />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('AzureADEdit').length).toBe(1);
|
expect(wrapper.find('AzureADEdit').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should successfully send default values to api on form revert all', async () => {
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
|
||||||
|
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('button[aria-label="Revert all to default"]')
|
||||||
|
.invoke('onClick')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('RevertAllAlert')).toHaveLength(1);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('RevertAllAlert button[aria-label="Confirm revert all"]')
|
||||||
|
.invoke('onClick')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_KEY: '',
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET: '',
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP: null,
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully send request to api on form submission', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_KEY: 'mock key',
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET: '$encrypted$',
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP: {},
|
||||||
|
SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP: {
|
||||||
|
'My Team': {
|
||||||
|
organization: 'foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to azure detail on successful submission', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
expect(history.location.pathname).toEqual('/settings/azure/details');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to azure detail when cancel is clicked', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||||
|
});
|
||||||
|
expect(history.location.pathname).toEqual('/settings/azure/details');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display error message on unsuccessful submission', async () => {
|
||||||
|
const error = {
|
||||||
|
response: {
|
||||||
|
data: { detail: 'An error occurred' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error));
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(0);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(1);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display ContentError on throw', async () => {
|
||||||
|
SettingsAPI.readCategory.mockImplementationOnce(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<AzureADEdit />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useConfig } from '../../../../contexts/Config';
|
|||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import useRequest from '../../../../util/useRequest';
|
import useRequest from '../../../../util/useRequest';
|
||||||
import { SettingsAPI } from '../../../../api';
|
import { SettingsAPI } from '../../../../api';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
|
|
||||||
function GitHubDetail({ i18n }) {
|
function GitHubDetail({ i18n }) {
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import useRequest from '../../../../util/useRequest';
|
|||||||
import { DetailList } from '../../../../components/DetailList';
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
import { useConfig } from '../../../../contexts/Config';
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
|
|
||||||
function GoogleOAuth2Detail({ i18n }) {
|
function GoogleOAuth2Detail({ i18n }) {
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useConfig } from '../../../../contexts/Config';
|
|||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import { SettingsAPI } from '../../../../api';
|
import { SettingsAPI } from '../../../../api';
|
||||||
import { sortNestedDetails } from '../../shared/settingUtils';
|
import { sortNestedDetails } from '../../shared/settingUtils';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
|
|
||||||
function JobsDetail({ i18n }) {
|
function JobsDetail({ i18n }) {
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { SettingsAPI } from '../../../../api';
|
|||||||
import useRequest from '../../../../util/useRequest';
|
import useRequest from '../../../../util/useRequest';
|
||||||
import { useConfig } from '../../../../contexts/Config';
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
import { sortNestedDetails } from '../../shared/settingUtils';
|
import { sortNestedDetails } from '../../shared/settingUtils';
|
||||||
|
|
||||||
function filterByPrefix(data, prefix) {
|
function filterByPrefix(data, prefix) {
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
import ContentError from '../../../components/ContentError';
|
import ContentError from '../../../components/ContentError';
|
||||||
|
import { useConfig } from '../../../contexts/Config';
|
||||||
import LoggingDetail from './LoggingDetail';
|
import LoggingDetail from './LoggingDetail';
|
||||||
import LoggingEdit from './LoggingEdit';
|
import LoggingEdit from './LoggingEdit';
|
||||||
|
|
||||||
function Logging({ i18n }) {
|
function Logging({ i18n }) {
|
||||||
const baseURL = '/settings/logging';
|
const baseURL = '/settings/logging';
|
||||||
|
const { me } = useConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -18,11 +21,17 @@ function Logging({ i18n }) {
|
|||||||
<LoggingDetail />
|
<LoggingDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseURL}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<LoggingEdit />
|
{me?.is_superuser ? (
|
||||||
|
<LoggingEdit />
|
||||||
|
) : (
|
||||||
|
<Redirect to={`${baseURL}/details`} />
|
||||||
|
)}
|
||||||
</Route>
|
</Route>
|
||||||
<Route key="not-found" path={`${baseURL}/*`}>
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
<ContentError isNotFound>
|
<ContentError isNotFound>
|
||||||
<Link to={`${baseURL}`}>{i18n._(t`View Logging settings`)}</Link>
|
<Link to={`${baseURL}/details`}>
|
||||||
|
{i18n._(t`View Logging settings`)}
|
||||||
|
</Link>
|
||||||
</ContentError>
|
</ContentError>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -10,7 +10,29 @@ import Logging from './Logging';
|
|||||||
|
|
||||||
jest.mock('../../../api/models/Settings');
|
jest.mock('../../../api/models/Settings');
|
||||||
SettingsAPI.readCategory.mockResolvedValue({
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
data: {},
|
data: {
|
||||||
|
LOG_AGGREGATOR_HOST: null,
|
||||||
|
LOG_AGGREGATOR_PORT: null,
|
||||||
|
LOG_AGGREGATOR_TYPE: null,
|
||||||
|
LOG_AGGREGATOR_USERNAME: '',
|
||||||
|
LOG_AGGREGATOR_PASSWORD: '',
|
||||||
|
LOG_AGGREGATOR_LOGGERS: [
|
||||||
|
'awx',
|
||||||
|
'activity_stream',
|
||||||
|
'job_events',
|
||||||
|
'system_tracking',
|
||||||
|
],
|
||||||
|
LOG_AGGREGATOR_INDIVIDUAL_FACTS: false,
|
||||||
|
LOG_AGGREGATOR_ENABLED: false,
|
||||||
|
LOG_AGGREGATOR_TOWER_UUID: '',
|
||||||
|
LOG_AGGREGATOR_PROTOCOL: 'https',
|
||||||
|
LOG_AGGREGATOR_TCP_TIMEOUT: 5,
|
||||||
|
LOG_AGGREGATOR_VERIFY_CERT: true,
|
||||||
|
LOG_AGGREGATOR_LEVEL: 'INFO',
|
||||||
|
LOG_AGGREGATOR_MAX_DISK_USAGE_GB: 1,
|
||||||
|
LOG_AGGREGATOR_MAX_DISK_USAGE_PATH: '/var/lib/awx',
|
||||||
|
LOG_AGGREGATOR_RSYSLOGD_DEBUG: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('<Logging />', () => {
|
describe('<Logging />', () => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import useRequest from '../../../../util/useRequest';
|
|||||||
import { DetailList } from '../../../../components/DetailList';
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
import { useConfig } from '../../../../contexts/Config';
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
import { sortNestedDetails, pluck } from '../../shared/settingUtils';
|
import { sortNestedDetails, pluck } from '../../shared/settingUtils';
|
||||||
|
|
||||||
function LoggingDetail({ i18n }) {
|
function LoggingDetail({ i18n }) {
|
||||||
|
|||||||
@@ -1,23 +1,272 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Button } from '@patternfly/react-core';
|
import { Formik } from 'formik';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { Button, Form, Tooltip } from '@patternfly/react-core';
|
||||||
|
import { CardBody } from '../../../../components/Card';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import { FormSubmitError } from '../../../../components/FormField';
|
||||||
|
import { FormColumnLayout } from '../../../../components/FormLayout';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import {
|
||||||
|
BooleanField,
|
||||||
|
ChoiceField,
|
||||||
|
EncryptedField,
|
||||||
|
InputField,
|
||||||
|
ObjectField,
|
||||||
|
RevertAllAlert,
|
||||||
|
RevertFormActionGroup,
|
||||||
|
LoggingTestAlert,
|
||||||
|
} from '../../shared';
|
||||||
|
import useModal from '../../../../util/useModal';
|
||||||
|
import useRequest, { useDismissableError } from '../../../../util/useRequest';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
|
||||||
function LoggingEdit({ i18n }) {
|
function LoggingEdit({ i18n }) {
|
||||||
|
const history = useHistory();
|
||||||
|
const { isModalOpen, toggleModal, closeModal } = useModal();
|
||||||
|
const { PUT: options } = useSettings();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
request: fetchLogging,
|
||||||
|
result: logging,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('logging');
|
||||||
|
const mergedData = {};
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
mergedData[key] = options[key];
|
||||||
|
mergedData[key].value = data[key];
|
||||||
|
});
|
||||||
|
return mergedData;
|
||||||
|
}, [options]),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLogging();
|
||||||
|
}, [fetchLogging]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: submitError,
|
||||||
|
request: submitForm,
|
||||||
|
result: submitResult,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async values => {
|
||||||
|
const result = await SettingsAPI.updateAll(values);
|
||||||
|
return result;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submitResult) {
|
||||||
|
history.push('/settings/logging/details');
|
||||||
|
}
|
||||||
|
}, [submitResult, history]);
|
||||||
|
|
||||||
|
const handleSubmit = async form => {
|
||||||
|
await submitForm({
|
||||||
|
...form,
|
||||||
|
LOG_AGGREGATOR_LOGGERS: JSON.parse(form.LOG_AGGREGATOR_LOGGERS),
|
||||||
|
LOG_AGGREGATOR_HOST: form.LOG_AGGREGATOR_HOST || null,
|
||||||
|
LOG_AGGREGATOR_TYPE: form.LOG_AGGREGATOR_TYPE || null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevertAll = async () => {
|
||||||
|
const defaultValues = {};
|
||||||
|
Object.entries(logging).forEach(([key, value]) => {
|
||||||
|
defaultValues[key] = value.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
await submitForm(defaultValues);
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: testLoggingError,
|
||||||
|
request: testLogging,
|
||||||
|
result: testSuccess,
|
||||||
|
setValue: setTestLogging,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const result = await SettingsAPI.test('logging', {});
|
||||||
|
return result;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: testError,
|
||||||
|
dismissError: dismissTestError,
|
||||||
|
} = useDismissableError(testLoggingError);
|
||||||
|
|
||||||
|
const handleTest = async () => {
|
||||||
|
await testLogging();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseAlert = () => {
|
||||||
|
setTestLogging(null);
|
||||||
|
dismissTestError();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.push('/settings/logging/details');
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialValues = fields =>
|
||||||
|
Object.keys(fields).reduce((acc, key) => {
|
||||||
|
if (fields[key].type === 'list') {
|
||||||
|
acc[key] = JSON.stringify(fields[key].value, null, 2);
|
||||||
|
} else {
|
||||||
|
acc[key] = fields[key].value ?? '';
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{i18n._(t`Edit form coming soon :)`)}
|
{isLoading && <ContentLoading />}
|
||||||
<CardActionsRow>
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
<Button
|
{!isLoading && logging && (
|
||||||
aria-label={i18n._(t`Cancel`)}
|
<Formik
|
||||||
component={Link}
|
initialValues={{ ...initialValues(logging) }}
|
||||||
to="/settings/logging/details"
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{formik => {
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||||
|
<FormColumnLayout>
|
||||||
|
<BooleanField
|
||||||
|
name="LOG_AGGREGATOR_ENABLED"
|
||||||
|
config={logging.LOG_AGGREGATOR_ENABLED}
|
||||||
|
ariaLabel={i18n._(t`Enable external logging`)}
|
||||||
|
disabled={
|
||||||
|
!formik.values.LOG_AGGREGATOR_ENABLED &&
|
||||||
|
(!formik.values.LOG_AGGREGATOR_HOST ||
|
||||||
|
!formik.values.LOG_AGGREGATOR_TYPE)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="LOG_AGGREGATOR_HOST"
|
||||||
|
config={logging.LOG_AGGREGATOR_HOST}
|
||||||
|
isRequired={Boolean(formik.values.LOG_AGGREGATOR_ENABLED)}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="LOG_AGGREGATOR_PORT"
|
||||||
|
config={logging.LOG_AGGREGATOR_PORT}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<ChoiceField
|
||||||
|
name="LOG_AGGREGATOR_TYPE"
|
||||||
|
config={logging.LOG_AGGREGATOR_TYPE}
|
||||||
|
isRequired={Boolean(formik.values.LOG_AGGREGATOR_ENABLED)}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="LOG_AGGREGATOR_USERNAME"
|
||||||
|
config={logging.LOG_AGGREGATOR_USERNAME}
|
||||||
|
/>
|
||||||
|
<EncryptedField
|
||||||
|
name="LOG_AGGREGATOR_PASSWORD"
|
||||||
|
config={logging.LOG_AGGREGATOR_PASSWORD}
|
||||||
|
/>
|
||||||
|
<BooleanField
|
||||||
|
name="LOG_AGGREGATOR_INDIVIDUAL_FACTS"
|
||||||
|
ariaLabel={i18n._(
|
||||||
|
t`Enable log system tracking facts individually`
|
||||||
|
)}
|
||||||
|
config={logging.LOG_AGGREGATOR_INDIVIDUAL_FACTS}
|
||||||
|
/>
|
||||||
|
<ChoiceField
|
||||||
|
name="LOG_AGGREGATOR_PROTOCOL"
|
||||||
|
config={logging.LOG_AGGREGATOR_PROTOCOL}
|
||||||
|
/>
|
||||||
|
<ChoiceField
|
||||||
|
name="LOG_AGGREGATOR_LEVEL"
|
||||||
|
config={logging.LOG_AGGREGATOR_LEVEL}
|
||||||
|
/>
|
||||||
|
{['tcp', 'https'].includes(
|
||||||
|
formik.values.LOG_AGGREGATOR_PROTOCOL
|
||||||
|
) && (
|
||||||
|
<InputField
|
||||||
|
name="LOG_AGGREGATOR_TCP_TIMEOUT"
|
||||||
|
config={logging.LOG_AGGREGATOR_TCP_TIMEOUT}
|
||||||
|
type="number"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{formik.values.LOG_AGGREGATOR_PROTOCOL === 'https' && (
|
||||||
|
<BooleanField
|
||||||
|
name="LOG_AGGREGATOR_VERIFY_CERT"
|
||||||
|
ariaLabel={i18n._(
|
||||||
|
t`Enable HTTPS certificate verification`
|
||||||
|
)}
|
||||||
|
config={logging.LOG_AGGREGATOR_VERIFY_CERT}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ObjectField
|
||||||
|
name="LOG_AGGREGATOR_LOGGERS"
|
||||||
|
config={logging.LOG_AGGREGATOR_LOGGERS}
|
||||||
|
/>
|
||||||
|
{submitError && <FormSubmitError error={submitError} />}
|
||||||
|
<RevertFormActionGroup
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onRevert={toggleModal}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
formik.dirty || !formik.values.LOG_AGGREGATOR_ENABLED
|
||||||
|
? i18n._(
|
||||||
|
t`Save and enable log aggregation before testing the log aggregator.`
|
||||||
|
)
|
||||||
|
: i18n._(
|
||||||
|
t`Send a test log message to the configured log aggregator.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Test logging`)}
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleTest}
|
||||||
|
isDisabled={
|
||||||
|
formik.dirty ||
|
||||||
|
!formik.values.LOG_AGGREGATOR_ENABLED ||
|
||||||
|
testSuccess ||
|
||||||
|
testError
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{i18n._(t`Test`)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</RevertFormActionGroup>
|
||||||
|
</FormColumnLayout>
|
||||||
|
{isModalOpen && (
|
||||||
|
<RevertAllAlert
|
||||||
|
onClose={closeModal}
|
||||||
|
onRevertAll={handleRevertAll}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(testSuccess || testError) && (
|
||||||
|
<LoggingTestAlert
|
||||||
|
successResponse={testSuccess}
|
||||||
|
errorResponse={testError}
|
||||||
|
onClose={handleCloseAlert}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,318 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
import LoggingEdit from './LoggingEdit';
|
import LoggingEdit from './LoggingEdit';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
const mockSettings = {
|
||||||
|
LOG_AGGREGATOR_HOST: 'https://logstash',
|
||||||
|
LOG_AGGREGATOR_PORT: 1234,
|
||||||
|
LOG_AGGREGATOR_TYPE: 'logstash',
|
||||||
|
LOG_AGGREGATOR_USERNAME: '',
|
||||||
|
LOG_AGGREGATOR_PASSWORD: '',
|
||||||
|
LOG_AGGREGATOR_LOGGERS: [
|
||||||
|
'awx',
|
||||||
|
'activity_stream',
|
||||||
|
'job_events',
|
||||||
|
'system_tracking',
|
||||||
|
],
|
||||||
|
LOG_AGGREGATOR_INDIVIDUAL_FACTS: false,
|
||||||
|
LOG_AGGREGATOR_ENABLED: true,
|
||||||
|
LOG_AGGREGATOR_TOWER_UUID: '',
|
||||||
|
LOG_AGGREGATOR_PROTOCOL: 'https',
|
||||||
|
LOG_AGGREGATOR_TCP_TIMEOUT: 123,
|
||||||
|
LOG_AGGREGATOR_VERIFY_CERT: true,
|
||||||
|
LOG_AGGREGATOR_LEVEL: 'ERROR',
|
||||||
|
LOG_AGGREGATOR_MAX_DISK_USAGE_GB: 1,
|
||||||
|
LOG_AGGREGATOR_MAX_DISK_USAGE_PATH: '/var/lib/awx',
|
||||||
|
LOG_AGGREGATOR_RSYSLOGD_DEBUG: false,
|
||||||
|
};
|
||||||
|
const mockDefaultSettings = {
|
||||||
|
LOG_AGGREGATOR_HOST: null,
|
||||||
|
LOG_AGGREGATOR_PORT: null,
|
||||||
|
LOG_AGGREGATOR_TYPE: null,
|
||||||
|
LOG_AGGREGATOR_USERNAME: '',
|
||||||
|
LOG_AGGREGATOR_PASSWORD: '',
|
||||||
|
LOG_AGGREGATOR_LOGGERS: [
|
||||||
|
'awx',
|
||||||
|
'activity_stream',
|
||||||
|
'job_events',
|
||||||
|
'system_tracking',
|
||||||
|
],
|
||||||
|
LOG_AGGREGATOR_INDIVIDUAL_FACTS: false,
|
||||||
|
LOG_AGGREGATOR_ENABLED: false,
|
||||||
|
LOG_AGGREGATOR_TOWER_UUID: '',
|
||||||
|
LOG_AGGREGATOR_PROTOCOL: 'https',
|
||||||
|
LOG_AGGREGATOR_TCP_TIMEOUT: 5,
|
||||||
|
LOG_AGGREGATOR_VERIFY_CERT: true,
|
||||||
|
LOG_AGGREGATOR_LEVEL: 'INFO',
|
||||||
|
LOG_AGGREGATOR_MAX_DISK_USAGE_GB: 1,
|
||||||
|
LOG_AGGREGATOR_MAX_DISK_USAGE_PATH: '/var/lib/awx',
|
||||||
|
LOG_AGGREGATOR_RSYSLOGD_DEBUG: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
SettingsAPI.updateAll.mockResolvedValue({});
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: mockSettings,
|
||||||
|
});
|
||||||
|
|
||||||
describe('<LoggingEdit />', () => {
|
describe('<LoggingEdit />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
let history;
|
||||||
wrapper = mountWithContexts(<LoggingEdit />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/logging/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<LoggingEdit />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('LoggingEdit').length).toBe(1);
|
expect(wrapper.find('LoggingEdit').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Enable External Logging toggle should be disabled when it is off and there is no Logging Aggregator or no Logging Aggregator Type', async () => {
|
||||||
|
const enableLoggingField =
|
||||||
|
"FormGroup[label='Enable External Logging'] Switch";
|
||||||
|
const loggingAggregatorField =
|
||||||
|
"FormGroup[label='Logging Aggregator'] TextInputBase";
|
||||||
|
expect(wrapper.find(enableLoggingField).prop('isChecked')).toBe(true);
|
||||||
|
expect(wrapper.find(enableLoggingField).prop('isDisabled')).toBe(false);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find(enableLoggingField).invoke('onChange')(false);
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find(loggingAggregatorField).invoke('onChange')(null, {
|
||||||
|
target: {
|
||||||
|
name: 'LOG_AGGREGATOR_HOST',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find(enableLoggingField)
|
||||||
|
.find('Switch')
|
||||||
|
.prop('isChecked')
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find(enableLoggingField)
|
||||||
|
.find('Switch')
|
||||||
|
.prop('isDisabled')
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Logging Aggregator and Logging Aggregator Type should be required when External Logging toggle is enabled', () => {
|
||||||
|
const enableLoggingField = wrapper.find(
|
||||||
|
"FormGroup[label='Enable External Logging']"
|
||||||
|
);
|
||||||
|
const loggingAggregatorField = wrapper.find(
|
||||||
|
"FormGroup[label='Logging Aggregator']"
|
||||||
|
);
|
||||||
|
const loggingAggregatorTypeField = wrapper.find(
|
||||||
|
"FormGroup[label='Logging Aggregator Type']"
|
||||||
|
);
|
||||||
|
expect(enableLoggingField.find('RevertButton').text()).toEqual('Revert');
|
||||||
|
expect(
|
||||||
|
loggingAggregatorField.find('.pf-c-form__label-required')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
loggingAggregatorTypeField.find('.pf-c-form__label-required')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Logging Aggregator and Logging Aggregator Type should not be required when External Logging toggle is disabled', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find("FormGroup[label='Enable External Logging'] Switch")
|
||||||
|
.invoke('onChange')(false);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
const enableLoggingField = wrapper.find(
|
||||||
|
"FormGroup[label='Enable External Logging']"
|
||||||
|
);
|
||||||
|
const loggingAggregatorField = wrapper.find(
|
||||||
|
"FormGroup[label='Logging Aggregator']"
|
||||||
|
);
|
||||||
|
const loggingAggregatorTypeField = wrapper.find(
|
||||||
|
"FormGroup[label='Logging Aggregator Type']"
|
||||||
|
);
|
||||||
|
expect(enableLoggingField.find('RevertButton').text()).toEqual('Undo');
|
||||||
|
expect(
|
||||||
|
loggingAggregatorField.find('.pf-c-form__label-required')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
loggingAggregatorTypeField.find('.pf-c-form__label-required')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('HTTPS certificate toggle should be shown when protocol is https', () => {
|
||||||
|
const httpsField = wrapper.find(
|
||||||
|
"FormGroup[label='Enable/disable HTTPS certificate verification']"
|
||||||
|
);
|
||||||
|
expect(httpsField).toHaveLength(1);
|
||||||
|
expect(httpsField.find('Switch').prop('isChecked')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TCP connection timeout should be required when protocol is tcp', () => {
|
||||||
|
const tcpTimeoutField = wrapper.find(
|
||||||
|
"FormGroup[label='TCP Connection Timeout']"
|
||||||
|
);
|
||||||
|
expect(tcpTimeoutField).toHaveLength(1);
|
||||||
|
expect(tcpTimeoutField.find('.pf-c-form__label-required')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TCP connection timeout and https certificate toggle should be hidden when protocol is udp', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('AnsibleSelect[name="LOG_AGGREGATOR_PROTOCOL"]')
|
||||||
|
.invoke('onChange')({
|
||||||
|
target: {
|
||||||
|
name: 'LOG_AGGREGATOR_PROTOCOL',
|
||||||
|
value: 'udp',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper.find(
|
||||||
|
"FormGroup[label='Enable/disable HTTPS certificate verification']"
|
||||||
|
)
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find("FormGroup[label='TCP Connection Timeout']")
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find("FormGroup[label='Logging Aggregator Level Threshold']")
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display successful toast when test button is clicked', async () => {
|
||||||
|
SettingsAPI.test.mockResolvedValue({});
|
||||||
|
expect(SettingsAPI.test).toHaveBeenCalledTimes(0);
|
||||||
|
expect(wrapper.find('LoggingTestAlert')).toHaveLength(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Test logging"]').invoke('onClick')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
await waitForElement(wrapper, 'LoggingTestAlert');
|
||||||
|
expect(SettingsAPI.test).toHaveBeenCalledTimes(1);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('AlertActionCloseButton button').invoke('onClick')();
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'LoggingTestAlert', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully send default values to api on form revert all', async () => {
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
|
||||||
|
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('button[aria-label="Revert all to default"]')
|
||||||
|
.invoke('onClick')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('RevertAllAlert')).toHaveLength(1);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('RevertAllAlert button[aria-label="Confirm revert all"]')
|
||||||
|
.invoke('onClick')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledWith(mockDefaultSettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully send request to api on form submission', async () => {
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
|
||||||
|
const loggingAggregatorField =
|
||||||
|
"FormGroup[label='Logging Aggregator'] TextInputBase";
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find(loggingAggregatorField).invoke('onChange')(null, {
|
||||||
|
target: {
|
||||||
|
name: 'LOG_AGGREGATOR_PORT',
|
||||||
|
value: 1010,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
|
||||||
|
...mockSettings,
|
||||||
|
LOG_AGGREGATOR_PORT: 1010,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to logging detail on successful submission', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
expect(history.location.pathname).toEqual('/settings/logging/details');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to logging detail when cancel is clicked', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||||
|
});
|
||||||
|
expect(history.location.pathname).toEqual('/settings/logging/details');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display error message on unsuccessful submission', async () => {
|
||||||
|
const error = {
|
||||||
|
response: {
|
||||||
|
data: { detail: 'An error occurred' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error));
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(0);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(1);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display ContentError on throw', async () => {
|
||||||
|
SettingsAPI.readCategory.mockImplementationOnce(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<LoggingEdit />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
import ContentError from '../../../components/ContentError';
|
import ContentError from '../../../components/ContentError';
|
||||||
|
import { useConfig } from '../../../contexts/Config';
|
||||||
import MiscSystemDetail from './MiscSystemDetail';
|
import MiscSystemDetail from './MiscSystemDetail';
|
||||||
import MiscSystemEdit from './MiscSystemEdit';
|
import MiscSystemEdit from './MiscSystemEdit';
|
||||||
|
|
||||||
function MiscSystem({ i18n }) {
|
function MiscSystem({ i18n }) {
|
||||||
const baseURL = '/settings/miscellaneous_system';
|
const baseURL = '/settings/miscellaneous_system';
|
||||||
|
const { me } = useConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -18,7 +21,11 @@ function MiscSystem({ i18n }) {
|
|||||||
<MiscSystemDetail />
|
<MiscSystemDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseURL}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<MiscSystemEdit />
|
{me?.is_superuser ? (
|
||||||
|
<MiscSystemEdit />
|
||||||
|
) : (
|
||||||
|
<Redirect to={`${baseURL}/details`} />
|
||||||
|
)}
|
||||||
</Route>
|
</Route>
|
||||||
<Route key="not-found" path={`${baseURL}/*`}>
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
<ContentError isNotFound>
|
<ContentError isNotFound>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { SettingsAPI } from '../../../../api';
|
|||||||
import useRequest from '../../../../util/useRequest';
|
import useRequest from '../../../../util/useRequest';
|
||||||
import { useConfig } from '../../../../contexts/Config';
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
import { sortNestedDetails, pluck } from '../../shared/settingUtils';
|
import { sortNestedDetails, pluck } from '../../shared/settingUtils';
|
||||||
|
|
||||||
function MiscSystemDetail({ i18n }) {
|
function MiscSystemDetail({ i18n }) {
|
||||||
|
|||||||
@@ -1,23 +1,305 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Button } from '@patternfly/react-core';
|
import { Formik } from 'formik';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { Form } from '@patternfly/react-core';
|
||||||
|
import { CardBody } from '../../../../components/Card';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import { FormSubmitError } from '../../../../components/FormField';
|
||||||
|
import { FormColumnLayout } from '../../../../components/FormLayout';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import {
|
||||||
|
BooleanField,
|
||||||
|
EncryptedField,
|
||||||
|
InputField,
|
||||||
|
ObjectField,
|
||||||
|
RevertAllAlert,
|
||||||
|
RevertFormActionGroup,
|
||||||
|
} from '../../shared';
|
||||||
|
import useModal from '../../../../util/useModal';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import { pluck } from '../../shared/settingUtils';
|
||||||
|
|
||||||
function MiscSystemEdit({ i18n }) {
|
function MiscSystemEdit({ i18n }) {
|
||||||
|
const history = useHistory();
|
||||||
|
const { isModalOpen, toggleModal, closeModal } = useModal();
|
||||||
|
const { PUT: options } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request: fetchSystem, result: system } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('all');
|
||||||
|
const {
|
||||||
|
OAUTH2_PROVIDER: {
|
||||||
|
ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||||
|
REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||||
|
AUTHORIZATION_CODE_EXPIRE_SECONDS,
|
||||||
|
},
|
||||||
|
...pluckedSystemData
|
||||||
|
} = pluck(
|
||||||
|
data,
|
||||||
|
'ALLOW_OAUTH2_FOR_EXTERNAL_USERS',
|
||||||
|
'AUTH_BASIC_ENABLED',
|
||||||
|
'AUTOMATION_ANALYTICS_GATHER_INTERVAL',
|
||||||
|
'AUTOMATION_ANALYTICS_URL',
|
||||||
|
'CUSTOM_VENV_PATHS',
|
||||||
|
'INSIGHTS_TRACKING_STATE',
|
||||||
|
'LOGIN_REDIRECT_OVERRIDE',
|
||||||
|
'MANAGE_ORGANIZATION_AUTH',
|
||||||
|
'OAUTH2_PROVIDER',
|
||||||
|
'ORG_ADMINS_CAN_SEE_ALL_USERS',
|
||||||
|
'REDHAT_PASSWORD',
|
||||||
|
'REDHAT_USERNAME',
|
||||||
|
'REMOTE_HOST_HEADERS',
|
||||||
|
'SESSIONS_PER_USER',
|
||||||
|
'SESSION_COOKIE_AGE',
|
||||||
|
'TOWER_URL_BASE'
|
||||||
|
);
|
||||||
|
|
||||||
|
const systemData = {
|
||||||
|
...pluckedSystemData,
|
||||||
|
ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||||
|
REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||||
|
AUTHORIZATION_CODE_EXPIRE_SECONDS,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
OAUTH2_PROVIDER: OAUTH2_PROVIDER_OPTIONS,
|
||||||
|
...restOptions
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const systemOptions = {
|
||||||
|
...restOptions,
|
||||||
|
ACCESS_TOKEN_EXPIRE_SECONDS: {
|
||||||
|
...OAUTH2_PROVIDER_OPTIONS,
|
||||||
|
default: OAUTH2_PROVIDER_OPTIONS.default.ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||||
|
type: OAUTH2_PROVIDER_OPTIONS.child.type,
|
||||||
|
label: i18n._(t`Access Token Expiration`),
|
||||||
|
},
|
||||||
|
REFRESH_TOKEN_EXPIRE_SECONDS: {
|
||||||
|
...OAUTH2_PROVIDER_OPTIONS,
|
||||||
|
default: OAUTH2_PROVIDER_OPTIONS.default.REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||||
|
type: OAUTH2_PROVIDER_OPTIONS.child.type,
|
||||||
|
label: i18n._(t`Refresh Token Expiration`),
|
||||||
|
},
|
||||||
|
AUTHORIZATION_CODE_EXPIRE_SECONDS: {
|
||||||
|
...OAUTH2_PROVIDER_OPTIONS,
|
||||||
|
default:
|
||||||
|
OAUTH2_PROVIDER_OPTIONS.default.AUTHORIZATION_CODE_EXPIRE_SECONDS,
|
||||||
|
type: OAUTH2_PROVIDER_OPTIONS.child.type,
|
||||||
|
label: i18n._(t`Authorization Code Expiration`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergedData = {};
|
||||||
|
Object.keys(systemData).forEach(key => {
|
||||||
|
mergedData[key] = systemOptions[key];
|
||||||
|
mergedData[key].value = systemData[key];
|
||||||
|
});
|
||||||
|
return mergedData;
|
||||||
|
}, [options, i18n]),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSystem();
|
||||||
|
}, [fetchSystem]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: submitError,
|
||||||
|
request: submitForm,
|
||||||
|
result: submitResult,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async values => {
|
||||||
|
const result = await SettingsAPI.updateAll(values);
|
||||||
|
return result;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = async form => {
|
||||||
|
const {
|
||||||
|
ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||||
|
REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||||
|
AUTHORIZATION_CODE_EXPIRE_SECONDS,
|
||||||
|
...formData
|
||||||
|
} = form;
|
||||||
|
await submitForm({
|
||||||
|
...formData,
|
||||||
|
CUSTOM_VENV_PATHS: JSON.parse(formData.CUSTOM_VENV_PATHS),
|
||||||
|
REMOTE_HOST_HEADERS: JSON.parse(formData.REMOTE_HOST_HEADERS),
|
||||||
|
OAUTH2_PROVIDER: {
|
||||||
|
ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||||
|
REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||||
|
AUTHORIZATION_CODE_EXPIRE_SECONDS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submitResult) {
|
||||||
|
history.push('/settings/miscellaneous_system/details');
|
||||||
|
}
|
||||||
|
}, [submitResult, history]);
|
||||||
|
|
||||||
|
const handleRevertAll = async () => {
|
||||||
|
const {
|
||||||
|
ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||||
|
REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||||
|
AUTHORIZATION_CODE_EXPIRE_SECONDS,
|
||||||
|
...systemData
|
||||||
|
} = system;
|
||||||
|
|
||||||
|
const defaultValues = {};
|
||||||
|
Object.entries(systemData).forEach(([key, value]) => {
|
||||||
|
defaultValues[key] = value.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
await submitForm({
|
||||||
|
...defaultValues,
|
||||||
|
OAUTH2_PROVIDER: {
|
||||||
|
ACCESS_TOKEN_EXPIRE_SECONDS: ACCESS_TOKEN_EXPIRE_SECONDS.default,
|
||||||
|
REFRESH_TOKEN_EXPIRE_SECONDS: REFRESH_TOKEN_EXPIRE_SECONDS.default,
|
||||||
|
AUTHORIZATION_CODE_EXPIRE_SECONDS:
|
||||||
|
AUTHORIZATION_CODE_EXPIRE_SECONDS.default,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.push('/settings/miscellaneous_system/details');
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialValues = fields =>
|
||||||
|
Object.keys(fields).reduce((acc, key) => {
|
||||||
|
if (fields[key].type === 'list') {
|
||||||
|
acc[key] = JSON.stringify(fields[key].value, null, 2);
|
||||||
|
} else {
|
||||||
|
acc[key] = fields[key].value ?? '';
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{i18n._(t`Edit form coming soon :)`)}
|
{isLoading && <ContentLoading />}
|
||||||
<CardActionsRow>
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
<Button
|
{!isLoading && system && (
|
||||||
aria-label={i18n._(t`Cancel`)}
|
<Formik
|
||||||
component={Link}
|
initialValues={{ ...initialValues(system) }}
|
||||||
to="/settings/miscellaneous_system/details"
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{formik => {
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||||
|
<FormColumnLayout>
|
||||||
|
<InputField
|
||||||
|
name="TOWER_URL_BASE"
|
||||||
|
config={system.TOWER_URL_BASE}
|
||||||
|
isRequired
|
||||||
|
type="url"
|
||||||
|
/>
|
||||||
|
<BooleanField
|
||||||
|
name="ORG_ADMINS_CAN_SEE_ALL_USERS"
|
||||||
|
config={system.ORG_ADMINS_CAN_SEE_ALL_USERS}
|
||||||
|
/>
|
||||||
|
<BooleanField
|
||||||
|
name="MANAGE_ORGANIZATION_AUTH"
|
||||||
|
config={system.MANAGE_ORGANIZATION_AUTH}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="SESSION_COOKIE_AGE"
|
||||||
|
config={system.SESSION_COOKIE_AGE}
|
||||||
|
type="number"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="SESSIONS_PER_USER"
|
||||||
|
config={system.SESSIONS_PER_USER}
|
||||||
|
type="number"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<BooleanField
|
||||||
|
name="AUTH_BASIC_ENABLED"
|
||||||
|
config={system.AUTH_BASIC_ENABLED}
|
||||||
|
/>
|
||||||
|
<BooleanField
|
||||||
|
name="ALLOW_OAUTH2_FOR_EXTERNAL_USERS"
|
||||||
|
config={system.ALLOW_OAUTH2_FOR_EXTERNAL_USERS}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="LOGIN_REDIRECT_OVERRIDE"
|
||||||
|
config={system.LOGIN_REDIRECT_OVERRIDE}
|
||||||
|
type="url"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="ACCESS_TOKEN_EXPIRE_SECONDS"
|
||||||
|
config={system.ACCESS_TOKEN_EXPIRE_SECONDS}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="REFRESH_TOKEN_EXPIRE_SECONDS"
|
||||||
|
config={system.REFRESH_TOKEN_EXPIRE_SECONDS}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="AUTHORIZATION_CODE_EXPIRE_SECONDS"
|
||||||
|
config={system.AUTHORIZATION_CODE_EXPIRE_SECONDS}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<BooleanField
|
||||||
|
name="INSIGHTS_TRACKING_STATE"
|
||||||
|
config={system.INSIGHTS_TRACKING_STATE}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="REDHAT_USERNAME"
|
||||||
|
config={system.REDHAT_USERNAME}
|
||||||
|
/>
|
||||||
|
<EncryptedField
|
||||||
|
name="REDHAT_PASSWORD"
|
||||||
|
config={system.REDHAT_PASSWORD}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="AUTOMATION_ANALYTICS_URL"
|
||||||
|
config={system.AUTOMATION_ANALYTICS_URL}
|
||||||
|
type="url"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="AUTOMATION_ANALYTICS_GATHER_INTERVAL"
|
||||||
|
config={system.AUTOMATION_ANALYTICS_GATHER_INTERVAL}
|
||||||
|
type="number"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<ObjectField
|
||||||
|
name="REMOTE_HOST_HEADERS"
|
||||||
|
config={system.REMOTE_HOST_HEADERS}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<ObjectField
|
||||||
|
name="CUSTOM_VENV_PATHS"
|
||||||
|
config={system.CUSTOM_VENV_PATHS}
|
||||||
|
/>
|
||||||
|
{submitError && <FormSubmitError error={submitError} />}
|
||||||
|
</FormColumnLayout>
|
||||||
|
<RevertFormActionGroup
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onRevert={toggleModal}
|
||||||
|
/>
|
||||||
|
{isModalOpen && (
|
||||||
|
<RevertAllAlert
|
||||||
|
onClose={closeModal}
|
||||||
|
onRevertAll={handleRevertAll}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,124 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
|
import mockAllSettings from '../../shared/data.allSettings.json';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
import MiscSystemEdit from './MiscSystemEdit';
|
import MiscSystemEdit from './MiscSystemEdit';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.updateAll.mockResolvedValue({});
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: mockAllSettings,
|
||||||
|
});
|
||||||
describe('<MiscSystemEdit />', () => {
|
describe('<MiscSystemEdit />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
let history;
|
||||||
wrapper = mountWithContexts(<MiscSystemEdit />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/miscellaneous_system/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<MiscSystemEdit />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('MiscSystemEdit').length).toBe(1);
|
expect(wrapper.find('MiscSystemEdit').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should successfully send default values to api on form revert all', async () => {
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
|
||||||
|
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('button[aria-label="Revert all to default"]')
|
||||||
|
.invoke('onClick')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('RevertAllAlert')).toHaveLength(1);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('RevertAllAlert button[aria-label="Confirm revert all"]')
|
||||||
|
.invoke('onClick')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully send request to api on form submission', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to miscellaneous detail on successful submission', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
expect(history.location.pathname).toEqual(
|
||||||
|
'/settings/miscellaneous_system/details'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to miscellaneous detail when cancel is clicked', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||||
|
});
|
||||||
|
expect(history.location.pathname).toEqual(
|
||||||
|
'/settings/miscellaneous_system/details'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display error message on unsuccessful submission', async () => {
|
||||||
|
const error = {
|
||||||
|
response: {
|
||||||
|
data: { detail: 'An error occurred' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error));
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(0);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(1);
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display ContentError on throw', async () => {
|
||||||
|
SettingsAPI.readCategory.mockImplementationOnce(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<MiscSystemEdit />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import useRequest from '../../../../util/useRequest';
|
|||||||
import { DetailList } from '../../../../components/DetailList';
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
import { useConfig } from '../../../../contexts/Config';
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
|
|
||||||
function RADIUSDetail({ i18n }) {
|
function RADIUSDetail({ i18n }) {
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import useRequest from '../../../../util/useRequest';
|
|||||||
import { DetailList } from '../../../../components/DetailList';
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
import { useConfig } from '../../../../contexts/Config';
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
|
|
||||||
function SAMLDetail({ i18n }) {
|
function SAMLDetail({ i18n }) {
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
|
|||||||
@@ -1,16 +1,78 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsAPI } from '../../api';
|
||||||
|
import mockAllOptions from './shared/data.allSettingOptions.json';
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
|
|
||||||
|
jest.mock('../../api/models/Settings');
|
||||||
|
SettingsAPI.readAllOptions.mockResolvedValue({
|
||||||
|
data: mockAllOptions,
|
||||||
|
});
|
||||||
|
|
||||||
describe('<Settings />', () => {
|
describe('<Settings />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<Settings />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.length).toBe(1);
|
test('should render Redirect for users without system admin or auditor permissions', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Settings />, {
|
||||||
|
context: {
|
||||||
|
router: {
|
||||||
|
history,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
is_system_auditor: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Redirect').length).toBe(1);
|
||||||
|
expect(wrapper.find('SettingList').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render Settings for users with permissions system admin or auditor permissions', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Settings />, {
|
||||||
|
context: {
|
||||||
|
router: {
|
||||||
|
history,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
is_superuser: true,
|
||||||
|
is_system_auditor: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('SettingList').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render content error on throw', async () => {
|
||||||
|
SettingsAPI.readAllOptions.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Settings />);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import useRequest from '../../../../util/useRequest';
|
|||||||
import { DetailList } from '../../../../components/DetailList';
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
import { useConfig } from '../../../../contexts/Config';
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
|
|
||||||
function TACACSDetail({ i18n }) {
|
function TACACSDetail({ i18n }) {
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { DetailList } from '../../../../components/DetailList';
|
|||||||
import { useConfig } from '../../../../contexts/Config';
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
import { useSettings } from '../../../../contexts/Settings';
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
import { pluck } from '../../shared/settingUtils';
|
import { pluck } from '../../shared/settingUtils';
|
||||||
import SettingDetail from '../../shared';
|
import { SettingDetail } from '../../shared';
|
||||||
|
|
||||||
function UIDetail({ i18n }) {
|
function UIDetail({ i18n }) {
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
|
|||||||
59
awx/ui_next/src/screens/Setting/shared/LoggingTestAlert.jsx
Normal file
59
awx/ui_next/src/screens/Setting/shared/LoggingTestAlert.jsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { func, shape } from 'prop-types';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertActionCloseButton,
|
||||||
|
AlertGroup,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function LoggingTestAlert({ i18n, successResponse, errorResponse, onClose }) {
|
||||||
|
let testMessage = null;
|
||||||
|
if (successResponse) {
|
||||||
|
testMessage = i18n._(t`Log aggregator test sent successfully.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorData = null;
|
||||||
|
if (errorResponse) {
|
||||||
|
testMessage = i18n._(t`There was an error testing the log aggregator.`);
|
||||||
|
if (
|
||||||
|
errorResponse?.response?.statusText &&
|
||||||
|
errorResponse?.response?.status
|
||||||
|
) {
|
||||||
|
testMessage = i18n._(
|
||||||
|
t`${errorResponse.response.statusText}: ${errorResponse.response.status}`
|
||||||
|
);
|
||||||
|
errorData = i18n._(t`${errorResponse.response?.data?.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertGroup isToast>
|
||||||
|
{testMessage && (
|
||||||
|
<Alert
|
||||||
|
actionClose={<AlertActionCloseButton onClose={onClose} />}
|
||||||
|
title={successResponse ? i18n._(t`Success`) : i18n._(t`Error`)}
|
||||||
|
variant={successResponse ? 'success' : 'danger'}
|
||||||
|
>
|
||||||
|
<b id="test-message">{testMessage}</b>
|
||||||
|
<p id="test-error">{errorData}</p>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</AlertGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggingTestAlert.propTypes = {
|
||||||
|
successResponse: shape({}),
|
||||||
|
errorResponse: shape({}),
|
||||||
|
onClose: func,
|
||||||
|
};
|
||||||
|
|
||||||
|
LoggingTestAlert.defaultProps = {
|
||||||
|
successResponse: null,
|
||||||
|
errorResponse: null,
|
||||||
|
onClose: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(LoggingTestAlert);
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import LoggingTestAlert from './LoggingTestAlert';
|
||||||
|
|
||||||
|
describe('LoggingTestAlert', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders expected content when test is successful', () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<LoggingTestAlert
|
||||||
|
successResponse={{}}
|
||||||
|
errorResponse={null}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('b#test-message').text()).toBe(
|
||||||
|
'Log aggregator test sent successfully.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders expected content when test is unsuccessful', () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<LoggingTestAlert
|
||||||
|
successResponse={null}
|
||||||
|
errorResponse={{
|
||||||
|
response: {
|
||||||
|
data: {
|
||||||
|
error: 'Name or service not known',
|
||||||
|
},
|
||||||
|
status: 400,
|
||||||
|
statusText: 'Bad Response',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('b#test-message').text()).toBe('Bad Response: 400');
|
||||||
|
expect(wrapper.find('p#test-error').text()).toBe(
|
||||||
|
'Name or service not known'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('close button should call "onClose"', () => {
|
||||||
|
const onClose = jest.fn();
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(0);
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<LoggingTestAlert
|
||||||
|
successResponse={{}}
|
||||||
|
errorResponse={null}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
wrapper.find('AlertActionCloseButton').invoke('onClose')();
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
39
awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx
Normal file
39
awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Button } from '@patternfly/react-core';
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
|
||||||
|
function RevertAllAlert({ i18n, onClose, onRevertAll }) {
|
||||||
|
return (
|
||||||
|
<AlertModal
|
||||||
|
isOpen
|
||||||
|
title={i18n._(t`Revert settings`)}
|
||||||
|
variant="info"
|
||||||
|
onClose={onClose}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
key="revert"
|
||||||
|
variant="primary"
|
||||||
|
aria-label={i18n._(t`Confirm revert all`)}
|
||||||
|
onClick={onRevertAll}
|
||||||
|
>
|
||||||
|
{i18n._(t`Revert all`)}
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="cancel"
|
||||||
|
variant="secondary"
|
||||||
|
aria-label={i18n._(t`Cancel revert`)}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{i18n._(t`Cancel`)}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{i18n._(t`This will revert all configuration values to their
|
||||||
|
factory defaults. Are you sure you want to proceed?`)}
|
||||||
|
</AlertModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(RevertAllAlert);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import RevertAllAlert from './RevertAllAlert';
|
||||||
|
|
||||||
|
describe('RevertAllAlert', () => {
|
||||||
|
test('renders the expected content', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<RevertAllAlert onClose={() => {}} onRevertAll={() => {}} />
|
||||||
|
);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
66
awx/ui_next/src/screens/Setting/shared/RevertButton.jsx
Normal file
66
awx/ui_next/src/screens/Setting/shared/RevertButton.jsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { Button, Tooltip } from '@patternfly/react-core';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const ButtonWrapper = styled.div`
|
||||||
|
margin-left: auto;
|
||||||
|
&&& {
|
||||||
|
--pf-c-button--FontSize: var(--pf-c-button--m-small--FontSize);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function RevertButton({ i18n, id, defaultValue, isDisabled = false }) {
|
||||||
|
const [field, meta, helpers] = useField(id);
|
||||||
|
const initialValue = meta.initialValue ?? '';
|
||||||
|
const currentValue = field.value;
|
||||||
|
let isRevertable = true;
|
||||||
|
let isMatch = false;
|
||||||
|
|
||||||
|
if (currentValue === defaultValue && currentValue !== initialValue) {
|
||||||
|
isRevertable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentValue === defaultValue && currentValue === initialValue) {
|
||||||
|
isMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
helpers.setValue(isRevertable ? defaultValue : initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const revertTooltipContent = isRevertable
|
||||||
|
? i18n._(t`Revert to factory default.`)
|
||||||
|
: i18n._(t`Restore initial value.`);
|
||||||
|
const tooltipContent =
|
||||||
|
isDisabled || isMatch
|
||||||
|
? i18n._(t`Setting matches factory default.`)
|
||||||
|
: revertTooltipContent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip entryDelay={700} content={tooltipContent}>
|
||||||
|
<ButtonWrapper>
|
||||||
|
<Button
|
||||||
|
aria-label={isRevertable ? i18n._(t`Revert`) : i18n._(t`Undo`)}
|
||||||
|
isInline
|
||||||
|
isSmall
|
||||||
|
onClick={handleConfirm}
|
||||||
|
type="button"
|
||||||
|
variant="link"
|
||||||
|
isDisabled={isDisabled || isMatch}
|
||||||
|
>
|
||||||
|
{isRevertable ? i18n._(t`Revert`) : i18n._(t`Undo`)}
|
||||||
|
</Button>
|
||||||
|
</ButtonWrapper>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RevertButton.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(RevertButton);
|
||||||
77
awx/ui_next/src/screens/Setting/shared/RevertButton.test.jsx
Normal file
77
awx/ui_next/src/screens/Setting/shared/RevertButton.test.jsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import RevertButton from './RevertButton';
|
||||||
|
|
||||||
|
describe('RevertButton', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('button text should display "Revert"', async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
test_input: 'foo',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => <RevertButton id="test_input" defaultValue="" />}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('button').text()).toEqual('Revert');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('button text should display "Undo"', async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
test_input: 'foo',
|
||||||
|
}}
|
||||||
|
values={{
|
||||||
|
test_input: 'bar',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => <RevertButton id="test_input" defaultValue="bar" />}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('button').text()).toEqual('Revert');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should revert value to default on button click', async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
test_input: 'foo',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => <RevertButton id="test_input" defaultValue="bar" />}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('button').text()).toEqual('Revert');
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Revert"]').invoke('onClick')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('button').text()).toEqual('Undo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be disabled when current value equals the initial and default values', async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
test_input: 'bar',
|
||||||
|
}}
|
||||||
|
values={{
|
||||||
|
test_input: 'bar',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => <RevertButton id="test_input" defaultValue="bar" />}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('button').text()).toEqual('Revert');
|
||||||
|
expect(wrapper.find('button').props().disabled).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { ActionGroup, Button } from '@patternfly/react-core';
|
||||||
|
import { FormFullWidthLayout } from '../../../components/FormLayout';
|
||||||
|
|
||||||
|
const RevertFormActionGroup = ({
|
||||||
|
children,
|
||||||
|
onCancel,
|
||||||
|
onRevert,
|
||||||
|
onSubmit,
|
||||||
|
i18n,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<FormFullWidthLayout>
|
||||||
|
<ActionGroup>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Save`)}
|
||||||
|
variant="primary"
|
||||||
|
type="button"
|
||||||
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
|
{i18n._(t`Save`)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Revert all to default`)}
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={onRevert}
|
||||||
|
>
|
||||||
|
{i18n._(t`Revert all to default`)}
|
||||||
|
</Button>
|
||||||
|
{children}
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Cancel`)}
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
{i18n._(t`Cancel`)}
|
||||||
|
</Button>
|
||||||
|
</ActionGroup>
|
||||||
|
</FormFullWidthLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RevertFormActionGroup.propTypes = {
|
||||||
|
onCancel: PropTypes.func.isRequired,
|
||||||
|
onRevert: PropTypes.func.isRequired,
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(RevertFormActionGroup);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import RevertFormActionGroup from './RevertFormActionGroup';
|
||||||
|
|
||||||
|
describe('RevertFormActionGroup', () => {
|
||||||
|
test('should render the expected content', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<RevertFormActionGroup
|
||||||
|
onSubmit={() => {}}
|
||||||
|
onCancel={() => {}}
|
||||||
|
onRevert={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Detail } from '../../../components/DetailList';
|
import { Detail } from '../../../components/DetailList';
|
||||||
import { VariablesDetail } from '../../../components/CodeMirrorInput';
|
import CodeDetail from '../../../components/DetailList/CodeDetail';
|
||||||
|
|
||||||
export default withI18n()(
|
export default withI18n()(
|
||||||
({ i18n, helpText, id, label, type, unit = '', value }) => {
|
({ i18n, helpText, id, label, type, unit = '', value }) => {
|
||||||
@@ -12,10 +12,11 @@ export default withI18n()(
|
|||||||
switch (dataType) {
|
switch (dataType) {
|
||||||
case 'nested object':
|
case 'nested object':
|
||||||
detail = (
|
detail = (
|
||||||
<VariablesDetail
|
<CodeDetail
|
||||||
dataCy={id}
|
dataCy={id}
|
||||||
label={label}
|
|
||||||
helpText={helpText}
|
helpText={helpText}
|
||||||
|
label={label}
|
||||||
|
mode="javascript"
|
||||||
rows={4}
|
rows={4}
|
||||||
value={JSON.stringify(value || {}, undefined, 2)}
|
value={JSON.stringify(value || {}, undefined, 2)}
|
||||||
/>
|
/>
|
||||||
@@ -23,12 +24,13 @@ export default withI18n()(
|
|||||||
break;
|
break;
|
||||||
case 'list':
|
case 'list':
|
||||||
detail = (
|
detail = (
|
||||||
<VariablesDetail
|
<CodeDetail
|
||||||
dataCy={id}
|
dataCy={id}
|
||||||
helpText={helpText}
|
helpText={helpText}
|
||||||
rows={4}
|
|
||||||
label={label}
|
label={label}
|
||||||
value={value}
|
mode="javascript"
|
||||||
|
rows={4}
|
||||||
|
value={JSON.stringify(value || [], undefined, 2)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|||||||
152
awx/ui_next/src/screens/Setting/shared/ShareFields.test.jsx
Normal file
152
awx/ui_next/src/screens/Setting/shared/ShareFields.test.jsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import {
|
||||||
|
BooleanField,
|
||||||
|
ChoiceField,
|
||||||
|
EncryptedField,
|
||||||
|
InputField,
|
||||||
|
ObjectField,
|
||||||
|
} from './SharedFields';
|
||||||
|
|
||||||
|
describe('Setting form fields', () => {
|
||||||
|
test('BooleanField renders the expected content', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
boolean: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<BooleanField
|
||||||
|
name="boolean"
|
||||||
|
config={{
|
||||||
|
label: 'test',
|
||||||
|
help_text: 'test',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('Switch')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('Switch').prop('isChecked')).toBe(true);
|
||||||
|
expect(wrapper.find('Switch').prop('isDisabled')).toBe(false);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Switch').invoke('onChange')(false);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('Switch').prop('isChecked')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ChoiceField renders unrequired form field', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
choice: 'one',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<ChoiceField
|
||||||
|
name="choice"
|
||||||
|
config={{
|
||||||
|
label: 'test',
|
||||||
|
help_text: 'test',
|
||||||
|
choices: [
|
||||||
|
['one', 'One'],
|
||||||
|
['two', 'Two'],
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('FormSelect')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('.pf-c-form__label-required')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EncryptedField renders the expected content', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
encrypted: '',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<EncryptedField
|
||||||
|
name="encrypted"
|
||||||
|
config={{
|
||||||
|
label: 'test',
|
||||||
|
help_text: 'test',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('PasswordInput')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('InputField renders the expected content', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
text: '',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<InputField
|
||||||
|
name="text"
|
||||||
|
config={{
|
||||||
|
label: 'test',
|
||||||
|
help_text: 'test',
|
||||||
|
default: '',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('TextInputBase')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('TextInputBase').prop('value')).toEqual('');
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('TextInputBase').invoke('onChange')(null, {
|
||||||
|
target: {
|
||||||
|
name: 'text',
|
||||||
|
value: 'foo',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('TextInputBase').prop('value')).toEqual('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ObjectField renders the expected content', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
object: '["one", "two", "three"]',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<ObjectField
|
||||||
|
name="object"
|
||||||
|
config={{
|
||||||
|
label: 'test',
|
||||||
|
help_text: 'test',
|
||||||
|
default: '[]',
|
||||||
|
type: 'list',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('CodeMirrorInput')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('CodeMirrorInput').prop('value')).toBe(
|
||||||
|
'["one", "two", "three"]'
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('CodeMirrorInput').invoke('onChange')('[]');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('CodeMirrorInput').prop('value')).toBe('[]');
|
||||||
|
});
|
||||||
|
});
|
||||||
258
awx/ui_next/src/screens/Setting/shared/SharedFields.jsx
Normal file
258
awx/ui_next/src/screens/Setting/shared/SharedFields.jsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { bool, oneOf, shape, string } from 'prop-types';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import {
|
||||||
|
FormGroup as PFFormGroup,
|
||||||
|
InputGroup,
|
||||||
|
TextInput,
|
||||||
|
Switch,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||||
|
import CodeMirrorInput from '../../../components/CodeMirrorInput';
|
||||||
|
import { PasswordInput } from '../../../components/FormField';
|
||||||
|
import { FormFullWidthLayout } from '../../../components/FormLayout';
|
||||||
|
import Popover from '../../../components/Popover';
|
||||||
|
import { combine, required, url, minMaxValue } from '../../../util/validators';
|
||||||
|
import RevertButton from './RevertButton';
|
||||||
|
|
||||||
|
const FormGroup = styled(PFFormGroup)`
|
||||||
|
.pf-c-form__group-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SettingGroup = withI18n()(
|
||||||
|
({
|
||||||
|
i18n,
|
||||||
|
children,
|
||||||
|
defaultValue,
|
||||||
|
fieldId,
|
||||||
|
helperTextInvalid,
|
||||||
|
isDisabled,
|
||||||
|
isRequired,
|
||||||
|
label,
|
||||||
|
popoverContent,
|
||||||
|
validated,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
fieldId={fieldId}
|
||||||
|
helperTextInvalid={helperTextInvalid}
|
||||||
|
isRequired={isRequired}
|
||||||
|
label={label}
|
||||||
|
validated={validated}
|
||||||
|
labelIcon={
|
||||||
|
<>
|
||||||
|
<Popover
|
||||||
|
content={popoverContent}
|
||||||
|
ariaLabel={`${i18n._(t`More information for`)} ${label}`}
|
||||||
|
/>
|
||||||
|
<RevertButton
|
||||||
|
id={fieldId}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const BooleanField = withI18n()(
|
||||||
|
({ i18n, ariaLabel = '', name, config, disabled = false }) => {
|
||||||
|
const [field, meta, helpers] = useField(name);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingGroup
|
||||||
|
defaultValue={config.default ?? false}
|
||||||
|
fieldId={name}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
isDisabled={disabled}
|
||||||
|
label={config.label}
|
||||||
|
popoverContent={config.help_text}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
id={name}
|
||||||
|
isChecked={field.value}
|
||||||
|
isDisabled={disabled}
|
||||||
|
label={i18n._(t`On`)}
|
||||||
|
labelOff={i18n._(t`Off`)}
|
||||||
|
onChange={checked => helpers.setValue(checked)}
|
||||||
|
aria-label={ariaLabel || config.label}
|
||||||
|
/>
|
||||||
|
</SettingGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
BooleanField.propTypes = {
|
||||||
|
name: string.isRequired,
|
||||||
|
config: shape({}).isRequired,
|
||||||
|
ariaLabel: string,
|
||||||
|
disabled: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChoiceField = withI18n()(({ i18n, name, config, isRequired = false }) => {
|
||||||
|
const validate = isRequired ? required(null, i18n) : null;
|
||||||
|
const [field, meta] = useField({ name, validate });
|
||||||
|
const isValid = !meta.error || !meta.touched;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingGroup
|
||||||
|
defaultValue={config.default ?? ''}
|
||||||
|
fieldId={name}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
isRequired={isRequired}
|
||||||
|
label={config.label}
|
||||||
|
popoverContent={config.help_text}
|
||||||
|
validated={isValid ? 'default' : 'error'}
|
||||||
|
>
|
||||||
|
<AnsibleSelect
|
||||||
|
id={name}
|
||||||
|
{...field}
|
||||||
|
data={[
|
||||||
|
...config.choices.map(([value, label], index) => ({
|
||||||
|
label,
|
||||||
|
value: value ?? '',
|
||||||
|
key: value ?? index,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingGroup>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ChoiceField.propTypes = {
|
||||||
|
name: string.isRequired,
|
||||||
|
config: shape({}).isRequired,
|
||||||
|
isRequired: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EncryptedField = withI18n()(
|
||||||
|
({ i18n, name, config, isRequired = false }) => {
|
||||||
|
const validate = isRequired ? required(null, i18n) : null;
|
||||||
|
const [, meta] = useField({ name, validate });
|
||||||
|
const isValid = !(meta.touched && meta.error);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingGroup
|
||||||
|
defaultValue={config.default ?? ''}
|
||||||
|
fieldId={name}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
isRequired={isRequired}
|
||||||
|
label={config.label}
|
||||||
|
popoverContent={config.help_text}
|
||||||
|
validated={isValid ? 'default' : 'error'}
|
||||||
|
>
|
||||||
|
<InputGroup>
|
||||||
|
<PasswordInput
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
label={config.label}
|
||||||
|
validate={validate}
|
||||||
|
isRequired={isRequired}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</SettingGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
EncryptedField.propTypes = {
|
||||||
|
name: string.isRequired,
|
||||||
|
config: shape({}).isRequired,
|
||||||
|
isRequired: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const InputField = withI18n()(
|
||||||
|
({ i18n, name, config, type = 'text', isRequired = false }) => {
|
||||||
|
const {
|
||||||
|
min_value = Number.MIN_SAFE_INTEGER,
|
||||||
|
max_value = Number.MAX_SAFE_INTEGER,
|
||||||
|
} = config;
|
||||||
|
const validators = [
|
||||||
|
isRequired ? required(null, i18n) : null,
|
||||||
|
type === 'url' ? url(i18n) : null,
|
||||||
|
type === 'number' ? minMaxValue(min_value, max_value, i18n) : null,
|
||||||
|
];
|
||||||
|
const [field, meta] = useField({ name, validate: combine(validators) });
|
||||||
|
const isValid = !(meta.touched && meta.error);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingGroup
|
||||||
|
defaultValue={config.default || ''}
|
||||||
|
fieldId={name}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
isRequired={isRequired}
|
||||||
|
label={config.label}
|
||||||
|
popoverContent={config.help_text}
|
||||||
|
validated={isValid ? 'default' : 'error'}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id={name}
|
||||||
|
isRequired={isRequired}
|
||||||
|
placeholder={config.placeholder}
|
||||||
|
type={type}
|
||||||
|
validated={isValid ? 'default' : 'error'}
|
||||||
|
value={field.value}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
onChange={(value, event) => {
|
||||||
|
field.onChange(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
InputField.propTypes = {
|
||||||
|
name: string.isRequired,
|
||||||
|
config: shape({}).isRequired,
|
||||||
|
type: oneOf(['text', 'number', 'url']),
|
||||||
|
isRequired: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ObjectField = withI18n()(({ i18n, name, config, isRequired = false }) => {
|
||||||
|
const validate = isRequired ? required(null, i18n) : null;
|
||||||
|
const [field, meta, helpers] = useField({ name, validate });
|
||||||
|
const isValid = !(meta.touched && meta.error);
|
||||||
|
|
||||||
|
const emptyDefault = config.type === 'list' ? '[]' : '{}';
|
||||||
|
const defaultRevertValue = config.default
|
||||||
|
? JSON.stringify(config.default, null, 2)
|
||||||
|
: emptyDefault;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormFullWidthLayout>
|
||||||
|
<SettingGroup
|
||||||
|
defaultValue={defaultRevertValue}
|
||||||
|
fieldId={name}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
isRequired={isRequired}
|
||||||
|
label={config.label}
|
||||||
|
popoverContent={config.help_text}
|
||||||
|
validated={isValid ? 'default' : 'error'}
|
||||||
|
>
|
||||||
|
<CodeMirrorInput
|
||||||
|
{...field}
|
||||||
|
id={name}
|
||||||
|
onChange={value => {
|
||||||
|
helpers.setValue(value);
|
||||||
|
}}
|
||||||
|
mode="javascript"
|
||||||
|
/>
|
||||||
|
</SettingGroup>
|
||||||
|
</FormFullWidthLayout>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ObjectField.propTypes = {
|
||||||
|
name: string.isRequired,
|
||||||
|
config: shape({}).isRequired,
|
||||||
|
isRequired: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BooleanField, ChoiceField, EncryptedField, InputField, ObjectField };
|
||||||
309
awx/ui_next/src/screens/Setting/shared/data.allSettings.json
Normal file
309
awx/ui_next/src/screens/Setting/shared/data.allSettings.json
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
{
|
||||||
|
"ACTIVITY_STREAM_ENABLED":true,
|
||||||
|
"ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC":false,
|
||||||
|
"ORG_ADMINS_CAN_SEE_ALL_USERS":true,
|
||||||
|
"MANAGE_ORGANIZATION_AUTH":true,
|
||||||
|
"TOWER_URL_BASE":"https://localhost:3000",
|
||||||
|
"REMOTE_HOST_HEADERS":["REMOTE_ADDR","REMOTE_HOST"],
|
||||||
|
"PROXY_IP_ALLOWED_LIST":[],
|
||||||
|
"LICENSE":{},
|
||||||
|
"REDHAT_USERNAME":"",
|
||||||
|
"REDHAT_PASSWORD":"",
|
||||||
|
"AUTOMATION_ANALYTICS_URL":"https://example.com",
|
||||||
|
"INSTALL_UUID":"3f5a4d68-3a94-474c-a3c0-f23a33122ce6",
|
||||||
|
"CUSTOM_VENV_PATHS":[],
|
||||||
|
"AD_HOC_COMMANDS":[
|
||||||
|
"command",
|
||||||
|
"shell",
|
||||||
|
"yum",
|
||||||
|
"apt",
|
||||||
|
"apt_key",
|
||||||
|
"apt_repository",
|
||||||
|
"apt_rpm",
|
||||||
|
"service",
|
||||||
|
"group",
|
||||||
|
"user",
|
||||||
|
"mount",
|
||||||
|
"ping",
|
||||||
|
"selinux",
|
||||||
|
"setup",
|
||||||
|
"win_ping",
|
||||||
|
"win_service",
|
||||||
|
"win_updates",
|
||||||
|
"win_group",
|
||||||
|
"win_user"
|
||||||
|
],
|
||||||
|
"ALLOW_JINJA_IN_EXTRA_VARS":"template",
|
||||||
|
"AWX_PROOT_ENABLED":true,
|
||||||
|
"AWX_PROOT_BASE_PATH":"/tmp",
|
||||||
|
"AWX_PROOT_HIDE_PATHS":[],
|
||||||
|
"AWX_PROOT_SHOW_PATHS":[],
|
||||||
|
"AWX_ISOLATED_CHECK_INTERVAL":1,
|
||||||
|
"AWX_ISOLATED_LAUNCH_TIMEOUT":600,
|
||||||
|
"AWX_ISOLATED_CONNECTION_TIMEOUT":10,
|
||||||
|
"AWX_ISOLATED_HOST_KEY_CHECKING":false,
|
||||||
|
"AWX_ISOLATED_KEY_GENERATION":true,
|
||||||
|
"AWX_ISOLATED_PRIVATE_KEY":"",
|
||||||
|
"AWX_ISOLATED_PUBLIC_KEY":"",
|
||||||
|
"AWX_RESOURCE_PROFILING_ENABLED":false,
|
||||||
|
"AWX_RESOURCE_PROFILING_CPU_POLL_INTERVAL":0.25,
|
||||||
|
"AWX_RESOURCE_PROFILING_MEMORY_POLL_INTERVAL":0.25,
|
||||||
|
"AWX_RESOURCE_PROFILING_PID_POLL_INTERVAL":0.25,
|
||||||
|
"AWX_TASK_ENV":{},
|
||||||
|
"INSIGHTS_TRACKING_STATE":false,
|
||||||
|
"PROJECT_UPDATE_VVV":false,
|
||||||
|
"AWX_ROLES_ENABLED":true,
|
||||||
|
"AWX_COLLECTIONS_ENABLED":true,
|
||||||
|
"AWX_SHOW_PLAYBOOK_LINKS":false,
|
||||||
|
"GALAXY_IGNORE_CERTS":false,
|
||||||
|
"STDOUT_MAX_BYTES_DISPLAY":1048576,
|
||||||
|
"EVENT_STDOUT_MAX_BYTES_DISPLAY":1024,
|
||||||
|
"SCHEDULE_MAX_JOBS":10,
|
||||||
|
"AWX_ANSIBLE_CALLBACK_PLUGINS":[],
|
||||||
|
"DEFAULT_JOB_TIMEOUT":0,
|
||||||
|
"DEFAULT_INVENTORY_UPDATE_TIMEOUT":0,
|
||||||
|
"DEFAULT_PROJECT_UPDATE_TIMEOUT":0,
|
||||||
|
"ANSIBLE_FACT_CACHE_TIMEOUT":0,
|
||||||
|
"MAX_FORKS":200,
|
||||||
|
"LOG_AGGREGATOR_HOST":null,
|
||||||
|
"LOG_AGGREGATOR_PORT":null,
|
||||||
|
"LOG_AGGREGATOR_TYPE":null,
|
||||||
|
"LOG_AGGREGATOR_USERNAME":"",
|
||||||
|
"LOG_AGGREGATOR_PASSWORD":"",
|
||||||
|
"LOG_AGGREGATOR_LOGGERS":["awx","activity_stream","job_events","system_tracking"],
|
||||||
|
"LOG_AGGREGATOR_INDIVIDUAL_FACTS":false,
|
||||||
|
"LOG_AGGREGATOR_ENABLED":true,
|
||||||
|
"LOG_AGGREGATOR_TOWER_UUID":"",
|
||||||
|
"LOG_AGGREGATOR_PROTOCOL":"https",
|
||||||
|
"LOG_AGGREGATOR_TCP_TIMEOUT":5,
|
||||||
|
"LOG_AGGREGATOR_VERIFY_CERT":true,
|
||||||
|
"LOG_AGGREGATOR_LEVEL":"INFO",
|
||||||
|
"LOG_AGGREGATOR_MAX_DISK_USAGE_GB":1,
|
||||||
|
"LOG_AGGREGATOR_MAX_DISK_USAGE_PATH":"/var/lib/awx",
|
||||||
|
"LOG_AGGREGATOR_RSYSLOGD_DEBUG":false,
|
||||||
|
"AUTOMATION_ANALYTICS_LAST_GATHER":null,
|
||||||
|
"AUTOMATION_ANALYTICS_GATHER_INTERVAL":14400,
|
||||||
|
"SESSION_COOKIE_AGE":1800,
|
||||||
|
"SESSIONS_PER_USER":-1,
|
||||||
|
"AUTH_BASIC_ENABLED":true,
|
||||||
|
"OAUTH2_PROVIDER":{
|
||||||
|
"ACCESS_TOKEN_EXPIRE_SECONDS":31536000000,
|
||||||
|
"REFRESH_TOKEN_EXPIRE_SECONDS":2628000,
|
||||||
|
"AUTHORIZATION_CODE_EXPIRE_SECONDS":600
|
||||||
|
},
|
||||||
|
"ALLOW_OAUTH2_FOR_EXTERNAL_USERS":false,
|
||||||
|
"LOGIN_REDIRECT_OVERRIDE":"",
|
||||||
|
"PENDO_TRACKING_STATE":"off",
|
||||||
|
"CUSTOM_LOGIN_INFO":"",
|
||||||
|
"CUSTOM_LOGO":"",
|
||||||
|
"MAX_UI_JOB_EVENTS":4000,
|
||||||
|
"UI_LIVE_UPDATES_ENABLED":true,
|
||||||
|
"AUTHENTICATION_BACKENDS":[
|
||||||
|
"awx.sso.backends.LDAPBackend",
|
||||||
|
"awx.sso.backends.RADIUSBackend",
|
||||||
|
"awx.sso.backends.TACACSPlusBackend",
|
||||||
|
"social_core.backends.github.GithubTeamOAuth2",
|
||||||
|
"django.contrib.auth.backends.ModelBackend"
|
||||||
|
],
|
||||||
|
"SOCIAL_AUTH_ORGANIZATION_MAP":null,
|
||||||
|
"SOCIAL_AUTH_TEAM_MAP":null,
|
||||||
|
"SOCIAL_AUTH_USER_FIELDS":null,
|
||||||
|
"AUTH_LDAP_SERVER_URI":"ldap://ldap.example.com",
|
||||||
|
"AUTH_LDAP_BIND_DN":"cn=eng_user1",
|
||||||
|
"AUTH_LDAP_BIND_PASSWORD":"$encrypted$",
|
||||||
|
"AUTH_LDAP_START_TLS":false,
|
||||||
|
"AUTH_LDAP_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30},
|
||||||
|
"AUTH_LDAP_USER_SEARCH":[],
|
||||||
|
"AUTH_LDAP_USER_DN_TEMPLATE":"uid=%(user)s,OU=Users,DC=example,DC=com",
|
||||||
|
"AUTH_LDAP_USER_ATTR_MAP":{},
|
||||||
|
"AUTH_LDAP_GROUP_SEARCH":["DC=example,DC=com","SCOPE_SUBTREE","(objectClass=group)"],
|
||||||
|
"AUTH_LDAP_GROUP_TYPE":"MemberDNGroupType",
|
||||||
|
"AUTH_LDAP_GROUP_TYPE_PARAMS":{"name_attr":"cn","member_attr":"member"},
|
||||||
|
"AUTH_LDAP_REQUIRE_GROUP":"CN=Tower Users,OU=Users,DC=example,DC=com",
|
||||||
|
"AUTH_LDAP_DENY_GROUP":null,
|
||||||
|
"AUTH_LDAP_USER_FLAGS_BY_GROUP":{"is_superuser":["cn=superusers"]},
|
||||||
|
"AUTH_LDAP_ORGANIZATION_MAP":{},
|
||||||
|
"AUTH_LDAP_TEAM_MAP":{},
|
||||||
|
"AUTH_LDAP_1_SERVER_URI":"",
|
||||||
|
"AUTH_LDAP_1_BIND_DN":"",
|
||||||
|
"AUTH_LDAP_1_BIND_PASSWORD":"",
|
||||||
|
"AUTH_LDAP_1_START_TLS":true,
|
||||||
|
"AUTH_LDAP_1_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30},
|
||||||
|
"AUTH_LDAP_1_USER_SEARCH":[],
|
||||||
|
"AUTH_LDAP_1_USER_DN_TEMPLATE":null,
|
||||||
|
"AUTH_LDAP_1_USER_ATTR_MAP":{},
|
||||||
|
"AUTH_LDAP_1_GROUP_SEARCH":[],
|
||||||
|
"AUTH_LDAP_1_GROUP_TYPE":"MemberDNGroupType",
|
||||||
|
"AUTH_LDAP_1_GROUP_TYPE_PARAMS":{"member_attr":"member","name_attr":"cn"},
|
||||||
|
"AUTH_LDAP_1_REQUIRE_GROUP":null,
|
||||||
|
"AUTH_LDAP_1_DENY_GROUP":"CN=Disabled1",
|
||||||
|
"AUTH_LDAP_1_USER_FLAGS_BY_GROUP":{},
|
||||||
|
"AUTH_LDAP_1_ORGANIZATION_MAP":{},
|
||||||
|
"AUTH_LDAP_1_TEAM_MAP":{},
|
||||||
|
"AUTH_LDAP_2_SERVER_URI":"",
|
||||||
|
"AUTH_LDAP_2_BIND_DN":"",
|
||||||
|
"AUTH_LDAP_2_BIND_PASSWORD":"",
|
||||||
|
"AUTH_LDAP_2_START_TLS":false,
|
||||||
|
"AUTH_LDAP_2_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30},
|
||||||
|
"AUTH_LDAP_2_USER_SEARCH":[],
|
||||||
|
"AUTH_LDAP_2_USER_DN_TEMPLATE":null,
|
||||||
|
"AUTH_LDAP_2_USER_ATTR_MAP":{},
|
||||||
|
"AUTH_LDAP_2_GROUP_SEARCH":[],
|
||||||
|
"AUTH_LDAP_2_GROUP_TYPE":"MemberDNGroupType",
|
||||||
|
"AUTH_LDAP_2_GROUP_TYPE_PARAMS":{"member_attr":"member","name_attr":"cn"},
|
||||||
|
"AUTH_LDAP_2_REQUIRE_GROUP":null,
|
||||||
|
"AUTH_LDAP_2_DENY_GROUP":"CN=Disabled2",
|
||||||
|
"AUTH_LDAP_2_USER_FLAGS_BY_GROUP":{},
|
||||||
|
"AUTH_LDAP_2_ORGANIZATION_MAP":{},
|
||||||
|
"AUTH_LDAP_2_TEAM_MAP":{},
|
||||||
|
"AUTH_LDAP_3_SERVER_URI":"",
|
||||||
|
"AUTH_LDAP_3_BIND_DN":"",
|
||||||
|
"AUTH_LDAP_3_BIND_PASSWORD":"",
|
||||||
|
"AUTH_LDAP_3_START_TLS":false,
|
||||||
|
"AUTH_LDAP_3_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30},
|
||||||
|
"AUTH_LDAP_3_USER_SEARCH":[],
|
||||||
|
"AUTH_LDAP_3_USER_DN_TEMPLATE":null,
|
||||||
|
"AUTH_LDAP_3_USER_ATTR_MAP":{},
|
||||||
|
"AUTH_LDAP_3_GROUP_SEARCH":[],
|
||||||
|
"AUTH_LDAP_3_GROUP_TYPE":"MemberDNGroupType",
|
||||||
|
"AUTH_LDAP_3_GROUP_TYPE_PARAMS":{"member_attr":"member","name_attr":"cn"},
|
||||||
|
"AUTH_LDAP_3_REQUIRE_GROUP":null,
|
||||||
|
"AUTH_LDAP_3_DENY_GROUP":null,
|
||||||
|
"AUTH_LDAP_3_USER_FLAGS_BY_GROUP":{},
|
||||||
|
"AUTH_LDAP_3_ORGANIZATION_MAP":{},
|
||||||
|
"AUTH_LDAP_3_TEAM_MAP":{},
|
||||||
|
"AUTH_LDAP_4_SERVER_URI":"",
|
||||||
|
"AUTH_LDAP_4_BIND_DN":"",
|
||||||
|
"AUTH_LDAP_4_BIND_PASSWORD":"",
|
||||||
|
"AUTH_LDAP_4_START_TLS":false,
|
||||||
|
"AUTH_LDAP_4_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30},
|
||||||
|
"AUTH_LDAP_4_USER_SEARCH":[],
|
||||||
|
"AUTH_LDAP_4_USER_DN_TEMPLATE":null,
|
||||||
|
"AUTH_LDAP_4_USER_ATTR_MAP":{},
|
||||||
|
"AUTH_LDAP_4_GROUP_SEARCH":[],
|
||||||
|
"AUTH_LDAP_4_GROUP_TYPE":"MemberDNGroupType",
|
||||||
|
"AUTH_LDAP_4_GROUP_TYPE_PARAMS":{"member_attr":"member","name_attr":"cn"},
|
||||||
|
"AUTH_LDAP_4_REQUIRE_GROUP":null,
|
||||||
|
"AUTH_LDAP_4_DENY_GROUP":null,
|
||||||
|
"AUTH_LDAP_4_USER_FLAGS_BY_GROUP":{},
|
||||||
|
"AUTH_LDAP_4_ORGANIZATION_MAP":{},
|
||||||
|
"AUTH_LDAP_4_TEAM_MAP":{},
|
||||||
|
"AUTH_LDAP_5_SERVER_URI":"",
|
||||||
|
"AUTH_LDAP_5_BIND_DN":"",
|
||||||
|
"AUTH_LDAP_5_BIND_PASSWORD":"",
|
||||||
|
"AUTH_LDAP_5_START_TLS":false,
|
||||||
|
"AUTH_LDAP_5_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30},
|
||||||
|
"AUTH_LDAP_5_USER_SEARCH":[],
|
||||||
|
"AUTH_LDAP_5_USER_DN_TEMPLATE":null,
|
||||||
|
"AUTH_LDAP_5_USER_ATTR_MAP":{},
|
||||||
|
"AUTH_LDAP_5_GROUP_SEARCH":[],
|
||||||
|
"AUTH_LDAP_5_GROUP_TYPE":"MemberDNGroupType",
|
||||||
|
"AUTH_LDAP_5_GROUP_TYPE_PARAMS":{"member_attr":"member","name_attr":"cn"},
|
||||||
|
"AUTH_LDAP_5_REQUIRE_GROUP":null,
|
||||||
|
"AUTH_LDAP_5_DENY_GROUP":null,
|
||||||
|
"AUTH_LDAP_5_USER_FLAGS_BY_GROUP":{},
|
||||||
|
"AUTH_LDAP_5_ORGANIZATION_MAP":{},
|
||||||
|
"AUTH_LDAP_5_TEAM_MAP":{},
|
||||||
|
"RADIUS_SERVER":"example.org",
|
||||||
|
"RADIUS_PORT":1812,
|
||||||
|
"RADIUS_SECRET":"$encrypted$",
|
||||||
|
"TACACSPLUS_HOST":"",
|
||||||
|
"TACACSPLUS_PORT":49,
|
||||||
|
"TACACSPLUS_SECRET":"",
|
||||||
|
"TACACSPLUS_SESSION_TIMEOUT":5,
|
||||||
|
"TACACSPLUS_AUTH_PROTOCOL":"ascii",
|
||||||
|
"SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL":"https://localhost:3000/sso/complete/google-oauth2/",
|
||||||
|
"SOCIAL_AUTH_GOOGLE_OAUTH2_KEY":"",
|
||||||
|
"SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET":"",
|
||||||
|
"SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS":[],
|
||||||
|
"SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS":{},
|
||||||
|
"SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP":null,
|
||||||
|
"SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP":null,
|
||||||
|
"SOCIAL_AUTH_GITHUB_CALLBACK_URL":"https://localhost:3000/sso/complete/github/",
|
||||||
|
"SOCIAL_AUTH_GITHUB_KEY":"",
|
||||||
|
"SOCIAL_AUTH_GITHUB_SECRET":"",
|
||||||
|
"SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP":null,
|
||||||
|
"SOCIAL_AUTH_GITHUB_TEAM_MAP":null,
|
||||||
|
"SOCIAL_AUTH_GITHUB_ORG_CALLBACK_URL":"https://localhost:3000/sso/complete/github-org/",
|
||||||
|
"SOCIAL_AUTH_GITHUB_ORG_KEY":"",
|
||||||
|
"SOCIAL_AUTH_GITHUB_ORG_SECRET":"",
|
||||||
|
"SOCIAL_AUTH_GITHUB_ORG_NAME":"",
|
||||||
|
"SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP":null,
|
||||||
|
"SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP":null,
|
||||||
|
"SOCIAL_AUTH_GITHUB_TEAM_CALLBACK_URL":"https://localhost:3000/sso/complete/github-team/",
|
||||||
|
"SOCIAL_AUTH_GITHUB_TEAM_KEY":"OAuth2 key (Client ID)",
|
||||||
|
"SOCIAL_AUTH_GITHUB_TEAM_SECRET":"$encrypted$",
|
||||||
|
"SOCIAL_AUTH_GITHUB_TEAM_ID":"team_id",
|
||||||
|
"SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP":{},
|
||||||
|
"SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP":{},
|
||||||
|
"SOCIAL_AUTH_AZUREAD_OAUTH2_CALLBACK_URL":"https://localhost:3000/sso/complete/azuread-oauth2/",
|
||||||
|
"SOCIAL_AUTH_AZUREAD_OAUTH2_KEY":"",
|
||||||
|
"SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET":"",
|
||||||
|
"SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP":null,
|
||||||
|
"SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP":null,
|
||||||
|
"SAML_AUTO_CREATE_OBJECTS":true,
|
||||||
|
"SOCIAL_AUTH_SAML_CALLBACK_URL":"https://localhost:3000/sso/complete/saml/",
|
||||||
|
"SOCIAL_AUTH_SAML_METADATA_URL":"https://localhost:3000/sso/metadata/saml/",
|
||||||
|
"SOCIAL_AUTH_SAML_SP_ENTITY_ID":"",
|
||||||
|
"SOCIAL_AUTH_SAML_SP_PUBLIC_CERT":"",
|
||||||
|
"SOCIAL_AUTH_SAML_SP_PRIVATE_KEY":"",
|
||||||
|
"SOCIAL_AUTH_SAML_ORG_INFO":{},
|
||||||
|
"SOCIAL_AUTH_SAML_TECHNICAL_CONTACT":{},
|
||||||
|
"SOCIAL_AUTH_SAML_SUPPORT_CONTACT":{},
|
||||||
|
"SOCIAL_AUTH_SAML_ENABLED_IDPS":{},
|
||||||
|
"SOCIAL_AUTH_SAML_SECURITY_CONFIG":{"requestedAuthnContext":false},
|
||||||
|
"SOCIAL_AUTH_SAML_SP_EXTRA":null,
|
||||||
|
"SOCIAL_AUTH_SAML_EXTRA_DATA":null,
|
||||||
|
"SOCIAL_AUTH_SAML_ORGANIZATION_MAP":null,
|
||||||
|
"SOCIAL_AUTH_SAML_TEAM_MAP":null,
|
||||||
|
"SOCIAL_AUTH_SAML_ORGANIZATION_ATTR":{},
|
||||||
|
"SOCIAL_AUTH_SAML_TEAM_ATTR":{},
|
||||||
|
"NAMED_URL_FORMATS":{
|
||||||
|
"organizations":"<name>",
|
||||||
|
"teams":"<name>++<organization.name>",
|
||||||
|
"credential_types":"<name>+<kind>",
|
||||||
|
"credentials":"<name>++<credential_type.name>+<credential_type.kind>++<organization.name>",
|
||||||
|
"notification_templates":"<name>++<organization.name>",
|
||||||
|
"job_templates":"<name>++<organization.name>",
|
||||||
|
"projects":"<name>++<organization.name>",
|
||||||
|
"inventories":"<name>++<organization.name>",
|
||||||
|
"hosts":"<name>++<inventory.name>++<organization.name>",
|
||||||
|
"groups":"<name>++<inventory.name>++<organization.name>",
|
||||||
|
"inventory_sources":"<name>++<inventory.name>++<organization.name>",
|
||||||
|
"inventory_scripts":"<name>++<organization.name>",
|
||||||
|
"instance_groups":"<name>",
|
||||||
|
"labels":"<name>++<organization.name>",
|
||||||
|
"workflow_job_templates":"<name>++<organization.name>",
|
||||||
|
"workflow_job_template_nodes":"<identifier>++<workflow_job_template.name>++<organization.name>",
|
||||||
|
"applications":"<name>++<organization.name>",
|
||||||
|
"users":"<username>",
|
||||||
|
"instances":"<hostname>"
|
||||||
|
},
|
||||||
|
"NAMED_URL_GRAPH_NODES":{
|
||||||
|
"organizations":{"fields":["name"],"adj_list":[]},
|
||||||
|
"teams":{"fields":["name"],"adj_list":[["organization","organizations"]]},
|
||||||
|
"credential_types":{"fields":["name","kind"],"adj_list":[]},
|
||||||
|
"credentials":{
|
||||||
|
"fields":["name"],
|
||||||
|
"adj_list":[["credential_type","credential_types"],["organization","organizations"]]
|
||||||
|
},
|
||||||
|
"notification_templates":{"fields":["name"],"adj_list":[["organization","organizations"]]},
|
||||||
|
"job_templates":{"fields":["name"],"adj_list":[["organization","organizations"]]},
|
||||||
|
"projects":{"fields":["name"],"adj_list":[["organization","organizations"]]},
|
||||||
|
"inventories":{"fields":["name"],"adj_list":[["organization","organizations"]]},
|
||||||
|
"hosts":{"fields":["name"],"adj_list":[["inventory","inventories"]]},
|
||||||
|
"groups":{"fields":["name"],"adj_list":[["inventory","inventories"]]},
|
||||||
|
"inventory_sources":{"fields":["name"],"adj_list":[["inventory","inventories"]]},
|
||||||
|
"inventory_scripts":{"fields":["name"],"adj_list":[["organization","organizations"]]},
|
||||||
|
"instance_groups":{"fields":["name"],"adj_list":[]},
|
||||||
|
"labels":{"fields":["name"],"adj_list":[["organization","organizations"]]},
|
||||||
|
"workflow_job_templates":{"fields":["name"],"adj_list":[["organization","organizations"]]},
|
||||||
|
"workflow_job_template_nodes":{
|
||||||
|
"fields":["identifier"],
|
||||||
|
"adj_list":[["workflow_job_template","workflow_job_templates"]]
|
||||||
|
},
|
||||||
|
"applications":{"fields":["name"],"adj_list":[["organization","organizations"]]},
|
||||||
|
"users":{"fields":["username"],"adj_list":[]},
|
||||||
|
"instances":{"fields":["hostname"],"adj_list":[]}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,11 @@
|
|||||||
export { default } from './SettingDetail';
|
export { default as LoggingTestAlert } from './LoggingTestAlert';
|
||||||
|
export { default as SettingDetail } from './SettingDetail';
|
||||||
|
export { default as RevertAllAlert } from './RevertAllAlert';
|
||||||
|
export { default as RevertFormActionGroup } from './RevertFormActionGroup';
|
||||||
|
export {
|
||||||
|
BooleanField,
|
||||||
|
ChoiceField,
|
||||||
|
EncryptedField,
|
||||||
|
InputField,
|
||||||
|
ObjectField,
|
||||||
|
} from './SharedFields';
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ export function assertDetail(wrapper, label, value) {
|
|||||||
|
|
||||||
export function assertVariableDetail(wrapper, label, value) {
|
export function assertVariableDetail(wrapper, label, value) {
|
||||||
expect(
|
expect(
|
||||||
wrapper.find(`VariablesDetail[label="${label}"] .pf-c-form__label`).text()
|
wrapper.find(`CodeDetail[label="${label}"] .pf-c-form__label`).text()
|
||||||
).toBe(label);
|
).toBe(label);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper.find(`CodeDetail[label="${label}"] CodeMirrorInput`).prop('value')
|
||||||
.find(`VariablesDetail[label="${label}"] CodeMirrorInput`)
|
|
||||||
.prop('value')
|
|
||||||
).toBe(value);
|
).toBe(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
export function sortNestedDetails(obj = {}) {
|
export function sortNestedDetails(obj = {}) {
|
||||||
const nestedTypes = ['nested object', 'list'];
|
const nestedTypes = ['nested object', 'list', 'boolean'];
|
||||||
const notNested = Object.entries(obj).filter(
|
const notNested = Object.entries(obj).filter(
|
||||||
([, value]) => !nestedTypes.includes(value.type)
|
([, value]) => !nestedTypes.includes(value.type)
|
||||||
);
|
);
|
||||||
|
const booleanList = Object.entries(obj).filter(
|
||||||
|
([, value]) => value.type === 'boolean'
|
||||||
|
);
|
||||||
const nestedList = Object.entries(obj).filter(
|
const nestedList = Object.entries(obj).filter(
|
||||||
([, value]) => value.type === 'list'
|
([, value]) => value.type === 'list'
|
||||||
);
|
);
|
||||||
const nestedObject = Object.entries(obj).filter(
|
const nestedObject = Object.entries(obj).filter(
|
||||||
([, value]) => value.type === 'nested object'
|
([, value]) => value.type === 'nested object'
|
||||||
);
|
);
|
||||||
return [...notNested, ...nestedList, ...nestedObject];
|
return [...notNested, ...booleanList, ...nestedList, ...nestedObject];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pluck(sourceObject, ...keys) {
|
export function pluck(sourceObject, ...keys) {
|
||||||
|
|||||||
28
awx/ui_next/src/util/useModal.js
Normal file
28
awx/ui_next/src/util/useModal.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useModal hook provides a way to read and update modal visibility
|
||||||
|
* Returns: {
|
||||||
|
* isModalOpen: boolean that indicates if modal is open
|
||||||
|
* toggleModal: function that toggles the modal open and close
|
||||||
|
* closeModal: function that closes modal
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function useModal() {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
function toggleModal() {
|
||||||
|
setIsModalOpen(!isModalOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isModalOpen,
|
||||||
|
toggleModal,
|
||||||
|
closeModal,
|
||||||
|
};
|
||||||
|
}
|
||||||
54
awx/ui_next/src/util/useModal.test.jsx
Normal file
54
awx/ui_next/src/util/useModal.test.jsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import useModal from './useModal';
|
||||||
|
|
||||||
|
const TestHook = ({ callback }) => {
|
||||||
|
callback();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const testHook = callback => {
|
||||||
|
mount(<TestHook callback={callback} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('useModal hook', () => {
|
||||||
|
let closeModal;
|
||||||
|
let isModalOpen;
|
||||||
|
let toggleModal;
|
||||||
|
|
||||||
|
test('should return expected initial values', () => {
|
||||||
|
testHook(() => {
|
||||||
|
({ isModalOpen, toggleModal, closeModal } = useModal());
|
||||||
|
});
|
||||||
|
expect(isModalOpen).toEqual(false);
|
||||||
|
expect(toggleModal).toBeInstanceOf(Function);
|
||||||
|
expect(closeModal).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return expected isModalOpen value after modal toggle', () => {
|
||||||
|
testHook(() => {
|
||||||
|
({ isModalOpen, toggleModal, closeModal } = useModal());
|
||||||
|
});
|
||||||
|
expect(isModalOpen).toEqual(false);
|
||||||
|
act(() => {
|
||||||
|
toggleModal();
|
||||||
|
});
|
||||||
|
expect(isModalOpen).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isModalOpen should be false after closeModal is called', () => {
|
||||||
|
testHook(() => {
|
||||||
|
({ isModalOpen, toggleModal, closeModal } = useModal());
|
||||||
|
});
|
||||||
|
expect(isModalOpen).toEqual(false);
|
||||||
|
act(() => {
|
||||||
|
toggleModal();
|
||||||
|
});
|
||||||
|
expect(isModalOpen).toEqual(true);
|
||||||
|
act(() => {
|
||||||
|
closeModal();
|
||||||
|
});
|
||||||
|
expect(isModalOpen).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user