mirror of
https://github.com/ansible/awx.git
synced 2026-05-08 01:47:35 -02:30
Add setting details and unit tests
This commit is contained in:
@@ -24,6 +24,7 @@ import Projects from './models/Projects';
|
|||||||
import Roles from './models/Roles';
|
import Roles from './models/Roles';
|
||||||
import Root from './models/Root';
|
import Root from './models/Root';
|
||||||
import Schedules from './models/Schedules';
|
import Schedules from './models/Schedules';
|
||||||
|
import Settings from './models/Settings';
|
||||||
import SystemJobs from './models/SystemJobs';
|
import SystemJobs from './models/SystemJobs';
|
||||||
import Teams from './models/Teams';
|
import Teams from './models/Teams';
|
||||||
import Tokens from './models/Tokens';
|
import Tokens from './models/Tokens';
|
||||||
@@ -61,6 +62,7 @@ const ProjectsAPI = new Projects();
|
|||||||
const RolesAPI = new Roles();
|
const RolesAPI = new Roles();
|
||||||
const RootAPI = new Root();
|
const RootAPI = new Root();
|
||||||
const SchedulesAPI = new Schedules();
|
const SchedulesAPI = new Schedules();
|
||||||
|
const SettingsAPI = new Settings();
|
||||||
const SystemJobsAPI = new SystemJobs();
|
const SystemJobsAPI = new SystemJobs();
|
||||||
const TeamsAPI = new Teams();
|
const TeamsAPI = new Teams();
|
||||||
const TokensAPI = new Tokens();
|
const TokensAPI = new Tokens();
|
||||||
@@ -99,6 +101,7 @@ export {
|
|||||||
RolesAPI,
|
RolesAPI,
|
||||||
RootAPI,
|
RootAPI,
|
||||||
SchedulesAPI,
|
SchedulesAPI,
|
||||||
|
SettingsAPI,
|
||||||
SystemJobsAPI,
|
SystemJobsAPI,
|
||||||
TeamsAPI,
|
TeamsAPI,
|
||||||
TokensAPI,
|
TokensAPI,
|
||||||
|
|||||||
26
awx/ui_next/src/api/models/Settings.js
Normal file
26
awx/ui_next/src/api/models/Settings.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import Base from '../Base';
|
||||||
|
|
||||||
|
class Settings extends Base {
|
||||||
|
constructor(http) {
|
||||||
|
super(http);
|
||||||
|
this.baseUrl = '/api/v2/settings/';
|
||||||
|
}
|
||||||
|
|
||||||
|
readAllOptions() {
|
||||||
|
return this.http.options(`${this.baseUrl}all/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAll(data) {
|
||||||
|
return this.http.patch(`${this.baseUrl}all/`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
readCategory(category) {
|
||||||
|
return this.http.get(`${this.baseUrl}${category}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
readCategoryOptions(category) {
|
||||||
|
return this.http.options(`${this.baseUrl}${category}/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'styled-components/macro';
|
import 'styled-components/macro';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { node, number, oneOfType, shape, string } from 'prop-types';
|
import { node, number, oneOfType, shape, string, arrayOf } from 'prop-types';
|
||||||
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
|
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
|
||||||
import { DetailName, DetailValue } from '../DetailList';
|
import { DetailName, DetailValue } from '../DetailList';
|
||||||
import MultiButtonToggle from '../MultiButtonToggle';
|
import MultiButtonToggle from '../MultiButtonToggle';
|
||||||
@@ -109,7 +109,7 @@ function VariablesDetail({ value, label, rows, fullHeight }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
VariablesDetail.propTypes = {
|
VariablesDetail.propTypes = {
|
||||||
value: oneOfType([shape({}), string]).isRequired,
|
value: oneOfType([shape({}), arrayOf(string), string]).isRequired,
|
||||||
label: node.isRequired,
|
label: node.isRequired,
|
||||||
rows: number,
|
rows: number,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ const DetailName = styled(({ fullWidth, ...props }) => (
|
|||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DetailValue = styled(({ fullWidth, isEncrypted, ...props }) => (
|
const DetailValue = styled(
|
||||||
<TextListItem {...props} />
|
({ fullWidth, isEncrypted, isUnconfigured, ...props }) => (
|
||||||
))`
|
<TextListItem {...props} />
|
||||||
|
)
|
||||||
|
)`
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
${props =>
|
${props =>
|
||||||
props.fullWidth &&
|
props.fullWidth &&
|
||||||
@@ -24,9 +26,8 @@ const DetailValue = styled(({ fullWidth, isEncrypted, ...props }) => (
|
|||||||
grid-column: 2 / -1;
|
grid-column: 2 / -1;
|
||||||
`}
|
`}
|
||||||
${props =>
|
${props =>
|
||||||
props.isEncrypted &&
|
(props.isEncrypted || props.isUnconfigured) &&
|
||||||
`
|
`
|
||||||
text-transform: uppercase
|
|
||||||
color: var(--pf-global--Color--400);
|
color: var(--pf-global--Color--400);
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
@@ -39,6 +40,7 @@ const Detail = ({
|
|||||||
dataCy,
|
dataCy,
|
||||||
alwaysVisible,
|
alwaysVisible,
|
||||||
isEncrypted,
|
isEncrypted,
|
||||||
|
isUnconfigured,
|
||||||
}) => {
|
}) => {
|
||||||
if (!value && typeof value !== 'number' && !alwaysVisible) {
|
if (!value && typeof value !== 'number' && !alwaysVisible) {
|
||||||
return null;
|
return null;
|
||||||
@@ -63,6 +65,7 @@ const Detail = ({
|
|||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
data-cy={valueCy}
|
data-cy={valueCy}
|
||||||
isEncrypted={isEncrypted}
|
isEncrypted={isEncrypted}
|
||||||
|
isUnconfigured={isUnconfigured}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</DetailValue>
|
</DetailValue>
|
||||||
|
|||||||
@@ -5,37 +5,57 @@ 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 = ({ onSubmit, submitDisabled, onCancel, i18n }) => (
|
const FormActionGroup = ({
|
||||||
<FormFullWidthLayout>
|
onCancel,
|
||||||
<ActionGroup>
|
onRevert,
|
||||||
<Button
|
onSubmit,
|
||||||
aria-label={i18n._(t`Save`)}
|
submitDisabled,
|
||||||
variant="primary"
|
i18n,
|
||||||
type="button"
|
}) => {
|
||||||
onClick={onSubmit}
|
return (
|
||||||
isDisabled={submitDisabled}
|
<FormFullWidthLayout>
|
||||||
>
|
<ActionGroup>
|
||||||
{i18n._(t`Save`)}
|
<Button
|
||||||
</Button>
|
aria-label={i18n._(t`Save`)}
|
||||||
<Button
|
variant="primary"
|
||||||
aria-label={i18n._(t`Cancel`)}
|
type="button"
|
||||||
variant="secondary"
|
onClick={onSubmit}
|
||||||
type="button"
|
isDisabled={submitDisabled}
|
||||||
onClick={onCancel}
|
>
|
||||||
>
|
{i18n._(t`Save`)}
|
||||||
{i18n._(t`Cancel`)}
|
</Button>
|
||||||
</Button>
|
{onRevert && (
|
||||||
</ActionGroup>
|
<Button
|
||||||
</FormFullWidthLayout>
|
aria-label={i18n._(t`Revert`)}
|
||||||
);
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={onRevert}
|
||||||
|
>
|
||||||
|
{i18n._(t`Revert`)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Cancel`)}
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
{i18n._(t`Cancel`)}
|
||||||
|
</Button>
|
||||||
|
</ActionGroup>
|
||||||
|
</FormFullWidthLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
6
awx/ui_next/src/contexts/Settings.jsx
Normal file
6
awx/ui_next/src/contexts/Settings.jsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
|
export const SettingsContext = React.createContext({});
|
||||||
|
export const SettingsProvider = SettingsContext.Provider;
|
||||||
|
|
||||||
|
export const useSettings = () => useContext(SettingsContext);
|
||||||
@@ -1,25 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
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';
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`Activity stream settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/details`}>
|
||||||
<ActivityStreamDetail />
|
<ActivityStreamDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<ActivityStreamEdit />
|
<ActivityStreamEdit />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}/details`}>
|
||||||
|
{i18n._(t`View Activity Stream settings`)}
|
||||||
|
</Link>
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,16 +1,59 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
import ActivityStream from './ActivityStream';
|
import ActivityStream from './ActivityStream';
|
||||||
|
import { SettingsAPI } from '../../../api';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
ACTIVITY_STREAM_ENABLED: true,
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<ActivityStream />', () => {
|
describe('<ActivityStream />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<ActivityStream />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain('Activity stream settings');
|
test('should render activity stream details', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/activity_stream/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<ActivityStream />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ActivityStreamDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render activity stream edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/activity_stream/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<ActivityStream />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ActivityStreamEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/activity_stream/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<ActivityStream />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,96 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } 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 { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
|
||||||
function ActivityStreamDetail({ i18n }) {
|
function ActivityStreamDetail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: options } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request, result: activityStream } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
ACTIVITY_STREAM_ENABLED,
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC,
|
||||||
|
},
|
||||||
|
} = await SettingsAPI.readCategory('system');
|
||||||
|
return {
|
||||||
|
ACTIVITY_STREAM_ENABLED,
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC,
|
||||||
|
};
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/settings/activity_stream/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
{isLoading && <ContentLoading />}
|
||||||
aria-label={i18n._(t`Edit`)}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
component={Link}
|
{!isLoading && activityStream && (
|
||||||
to="/settings/activity_stream/edit"
|
<DetailList>
|
||||||
>
|
{Object.keys(activityStream).map(key => {
|
||||||
{i18n._(t`Edit`)}
|
const record = options?.[key];
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<SettingDetail
|
||||||
</CardBody>
|
key={key}
|
||||||
|
label={record?.label}
|
||||||
|
type={record?.type}
|
||||||
|
value={activityStream?.[key]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to="/settings/activity_stream/edit"
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,88 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import { assertDetail } from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
import ActivityStreamDetail from './ActivityStreamDetail';
|
import ActivityStreamDetail from './ActivityStreamDetail';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
ACTIVITY_STREAM_ENABLED: true,
|
||||||
|
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<ActivityStreamDetail />', () => {
|
describe('<ActivityStreamDetail />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<ActivityStreamDetail />);
|
beforeAll(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<ActivityStreamDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
|
afterAll(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('ActivityStreamDetail').length).toBe(1);
|
expect(wrapper.find('ActivityStreamDetail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', () => {
|
||||||
|
const expectedTabs = ['Back to Settings', 'Details'];
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(wrapper, 'Enable Activity Stream', 'On');
|
||||||
|
assertDetail(wrapper, 'Enable Activity Stream for Inventory Sync', 'Off');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<ActivityStreamDetail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<ActivityStreamDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
import AzureADDetail from './AzureADDetail';
|
import AzureADDetail from './AzureADDetail';
|
||||||
import AzureADEdit from './AzureADEdit';
|
import AzureADEdit from './AzureADEdit';
|
||||||
|
|
||||||
function AzureAD({ i18n }) {
|
function AzureAD({ i18n }) {
|
||||||
const baseUrl = '/settings/azure';
|
const baseURL = '/settings/azure';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`Azure AD settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/details`}>
|
||||||
<AzureADDetail />
|
<AzureADDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<AzureADEdit />
|
<AzureADEdit />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}/details`}>
|
||||||
|
{i18n._(t`View Azure AD settings`)}
|
||||||
|
</Link>
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,16 +1,56 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsAPI } from '../../../api';
|
||||||
import AzureAD from './AzureAD';
|
import AzureAD from './AzureAD';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<AzureAD />', () => {
|
describe('<AzureAD />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<AzureAD />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain('Azure AD settings');
|
test('should render azure details', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/azure/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<AzureAD />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('AzureADDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render azure edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/azure/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<AzureAD />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('AzureADEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/azure/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<AzureAD />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,88 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } 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 { Button } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
|
||||||
function AzureADDetail({ i18n }) {
|
function AzureADDetail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: options } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request, result: azure } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('azuread-oauth2');
|
||||||
|
return data;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/settings/azure/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
{isLoading && <ContentLoading />}
|
||||||
aria-label={i18n._(t`Edit`)}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
component={Link}
|
{!isLoading && azure && (
|
||||||
to="/settings/azure/edit"
|
<DetailList>
|
||||||
>
|
{Object.keys(azure).map(key => {
|
||||||
{i18n._(t`Edit`)}
|
const record = options?.[key];
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<SettingDetail
|
||||||
</CardBody>
|
key={key}
|
||||||
|
label={record?.label}
|
||||||
|
type={record?.type}
|
||||||
|
value={azure?.[key]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to="/settings/azure/edit"
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,110 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import {
|
||||||
|
assertDetail,
|
||||||
|
assertVariableDetail,
|
||||||
|
} from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
import AzureADDetail from './AzureADDetail';
|
import AzureADDetail from './AzureADDetail';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
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': {
|
||||||
|
users: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<AzureADDetail />', () => {
|
describe('<AzureADDetail />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<AzureADDetail />);
|
beforeAll(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<AzureADDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
|
afterAll(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('AzureADDetail').length).toBe(1);
|
expect(wrapper.find('AzureADDetail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', () => {
|
||||||
|
const expectedTabs = ['Back to Settings', 'Details'];
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Azure AD OAuth2 Callback URL',
|
||||||
|
'https://towerhost/sso/complete/azuread-oauth2/'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'Azure AD OAuth2 Key', 'mock key');
|
||||||
|
assertDetail(wrapper, 'Azure AD OAuth2 Secret', 'Encrypted');
|
||||||
|
assertVariableDetail(wrapper, 'Azure AD OAuth2 Organization Map', '{}');
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'Azure AD OAuth2 Team Map',
|
||||||
|
'{\n "My Team": {\n "users": []\n }\n}'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<AzureADDetail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<AzureADDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
import GitHubDetail from './GitHubDetail';
|
import GitHubDetail from './GitHubDetail';
|
||||||
import GitHubEdit from './GitHubEdit';
|
import GitHubEdit from './GitHubEdit';
|
||||||
|
|
||||||
function GitHub({ i18n }) {
|
function GitHub({ i18n }) {
|
||||||
const baseUrl = '/settings/github';
|
const baseURL = '/settings/github';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`GitHub settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/default/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/:category/details`}>
|
||||||
<GitHubDetail />
|
<GitHubDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/:category/edit`}>
|
||||||
<GitHubEdit />
|
<GitHubEdit />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}/default/details`}>
|
||||||
|
{i18n._(t`View GitHub Settings`)}
|
||||||
|
</Link>
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,16 +1,61 @@
|
|||||||
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 GitHub from './GitHub';
|
import GitHub from './GitHub';
|
||||||
|
import { SettingsAPI } from '../../../api';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<GitHub />', () => {
|
describe('<GitHub />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<GitHub />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain('GitHub settings');
|
test('should render github details', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/github/'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<GitHub />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('GitHubDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render github edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/github/default/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<GitHub />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('GitHubEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/github/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<GitHub />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,124 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, Redirect, useRouteMatch } 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 { Button } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
|
||||||
function GitHubDetail({ i18n }) {
|
function GitHubDetail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: options } = useSettings();
|
||||||
|
|
||||||
|
const baseURL = '/settings/github';
|
||||||
|
const {
|
||||||
|
path,
|
||||||
|
params: { category },
|
||||||
|
} = useRouteMatch(`${baseURL}/:category/details`);
|
||||||
|
|
||||||
|
const { isLoading, error, request, result: gitHubDetails } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const [
|
||||||
|
{ data: gitHubDefault },
|
||||||
|
{ data: gitHubOrganization },
|
||||||
|
{ data: gitHubTeam },
|
||||||
|
] = await Promise.all([
|
||||||
|
SettingsAPI.readCategory('github'),
|
||||||
|
SettingsAPI.readCategory('github-org'),
|
||||||
|
SettingsAPI.readCategory('github-team'),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
default: gitHubDefault,
|
||||||
|
organization: gitHubOrganization,
|
||||||
|
team: gitHubTeam,
|
||||||
|
};
|
||||||
|
}, []),
|
||||||
|
{
|
||||||
|
default: null,
|
||||||
|
organization: null,
|
||||||
|
team: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`GitHub Default`),
|
||||||
|
link: `${baseURL}/default/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`GitHub Organization`),
|
||||||
|
link: `${baseURL}/organization/details`,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`GitHub Team`),
|
||||||
|
link: `${baseURL}/team/details`,
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!Object.keys(gitHubDetails).includes(category)) {
|
||||||
|
return <Redirect from={path} to={`${baseURL}/default/details`} exact />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
{isLoading && <ContentLoading />}
|
||||||
aria-label={i18n._(t`Edit`)}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
component={Link}
|
{!isLoading && !Object.values(gitHubDetails)?.includes(null) && (
|
||||||
to="/settings/github/edit"
|
<DetailList>
|
||||||
>
|
{Object.keys(gitHubDetails[category]).map(key => {
|
||||||
{i18n._(t`Edit`)}
|
const record = options?.[key];
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<SettingDetail
|
||||||
</CardBody>
|
key={key}
|
||||||
|
label={record?.label}
|
||||||
|
type={record?.type}
|
||||||
|
value={gitHubDetails[category][key]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to={`${baseURL}/${category}/edit`}
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,257 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { useRouteMatch } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import {
|
||||||
|
assertDetail,
|
||||||
|
assertVariableDetail,
|
||||||
|
} from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
import GitHubDetail from './GitHubDetail';
|
import GitHubDetail from './GitHubDetail';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useRouteMatch: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
|
||||||
|
const mockDefault = {
|
||||||
|
data: {
|
||||||
|
SOCIAL_AUTH_GITHUB_CALLBACK_URL: 'https://towerhost/sso/complete/github/',
|
||||||
|
SOCIAL_AUTH_GITHUB_KEY: 'mock github key',
|
||||||
|
SOCIAL_AUTH_GITHUB_SECRET: '$encrypted$',
|
||||||
|
SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP: null,
|
||||||
|
SOCIAL_AUTH_GITHUB_TEAM_MAP: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockOrg = {
|
||||||
|
data: {
|
||||||
|
SOCIAL_AUTH_GITHUB_ORG_CALLBACK_URL:
|
||||||
|
'https://towerhost/sso/complete/github-org/',
|
||||||
|
SOCIAL_AUTH_GITHUB_ORG_KEY: '',
|
||||||
|
SOCIAL_AUTH_GITHUB_ORG_SECRET: '$encrypted$',
|
||||||
|
SOCIAL_AUTH_GITHUB_ORG_NAME: '',
|
||||||
|
SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP: null,
|
||||||
|
SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockTeam = {
|
||||||
|
data: {
|
||||||
|
SOCIAL_AUTH_GITHUB_TEAM_CALLBACK_URL:
|
||||||
|
'https://towerhost/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: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe('<GitHubDetail />', () => {
|
describe('<GitHubDetail />', () => {
|
||||||
let wrapper;
|
describe('Default', () => {
|
||||||
beforeEach(() => {
|
let wrapper;
|
||||||
wrapper = mountWithContexts(<GitHubDetail />);
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault);
|
||||||
|
SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg);
|
||||||
|
SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam);
|
||||||
|
useRouteMatch.mockImplementation(() => ({
|
||||||
|
url: '/settings/github/default/details',
|
||||||
|
path: '/settings/github/:category/details',
|
||||||
|
params: { category: 'default' },
|
||||||
|
}));
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<GitHubDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initially renders without crashing', () => {
|
||||||
|
expect(wrapper.find('GitHubDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', () => {
|
||||||
|
const expectedTabs = [
|
||||||
|
'Back to Settings',
|
||||||
|
'GitHub Default',
|
||||||
|
'GitHub Organization',
|
||||||
|
'GitHub Team',
|
||||||
|
];
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'GitHub OAuth2 Callback URL',
|
||||||
|
'https://towerhost/sso/complete/github/'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'GitHub OAuth2 Key', 'mock github key');
|
||||||
|
assertDetail(wrapper, 'GitHub OAuth2 Secret', 'Encrypted');
|
||||||
|
assertVariableDetail(wrapper, 'GitHub OAuth2 Organization Map', '{}');
|
||||||
|
assertVariableDetail(wrapper, 'GitHub OAuth2 Team Map', '{}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<GitHubDetail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<GitHubDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
wrapper.unmount();
|
describe('Organization', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault);
|
||||||
|
SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg);
|
||||||
|
SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam);
|
||||||
|
useRouteMatch.mockImplementation(() => ({
|
||||||
|
url: '/settings/github/organization/details',
|
||||||
|
path: '/settings/github/:category/details',
|
||||||
|
params: { category: 'organization' },
|
||||||
|
}));
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<GitHubDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'GitHub Organization OAuth2 Callback URL',
|
||||||
|
'https://towerhost/sso/complete/github-org/'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'GitHub Organization OAuth2 Key', 'Unconfigured');
|
||||||
|
assertDetail(wrapper, 'GitHub Organization OAuth2 Secret', 'Encrypted');
|
||||||
|
assertDetail(wrapper, 'GitHub Organization Name', 'Unconfigured');
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'GitHub Organization OAuth2 Organization Map',
|
||||||
|
'{}'
|
||||||
|
);
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'GitHub Organization OAuth2 Team Map',
|
||||||
|
'{}'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('GitHubDetail').length).toBe(1);
|
describe('Team', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault);
|
||||||
|
SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg);
|
||||||
|
SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam);
|
||||||
|
useRouteMatch.mockImplementation(() => ({
|
||||||
|
url: '/settings/github/team/details',
|
||||||
|
path: '/settings/github/:category/details',
|
||||||
|
params: { category: 'team' },
|
||||||
|
}));
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<GitHubDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'GitHub Team OAuth2 Callback URL',
|
||||||
|
'https://towerhost/sso/complete/github-team/'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'GitHub Team OAuth2 Key', 'OAuth2 key (Client ID)');
|
||||||
|
assertDetail(wrapper, 'GitHub Team OAuth2 Secret', 'Encrypted');
|
||||||
|
assertDetail(wrapper, 'GitHub Team ID', 'team_id');
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'GitHub Team OAuth2 Organization Map',
|
||||||
|
'{}'
|
||||||
|
);
|
||||||
|
assertVariableDetail(wrapper, 'GitHub Team OAuth2 Team Map', '{}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Redirect', () => {
|
||||||
|
test('should render redirect when user navigates to erroneous category', async () => {
|
||||||
|
let wrapper;
|
||||||
|
useRouteMatch.mockImplementation(() => ({
|
||||||
|
url: '/settings/github/foo/details',
|
||||||
|
path: '/settings/github/:category/details',
|
||||||
|
params: { category: 'foo' },
|
||||||
|
}));
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<GitHubDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'Redirect');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
import GoogleOAuth2Detail from './GoogleOAuth2Detail';
|
import GoogleOAuth2Detail from './GoogleOAuth2Detail';
|
||||||
import GoogleOAuth2Edit from './GoogleOAuth2Edit';
|
import GoogleOAuth2Edit from './GoogleOAuth2Edit';
|
||||||
|
|
||||||
function GoogleOAuth2({ i18n }) {
|
function GoogleOAuth2({ i18n }) {
|
||||||
const baseUrl = '/settings/google_oauth2';
|
const baseURL = '/settings/google_oauth2';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`Google OAuth 2.0 settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/details`}>
|
||||||
<GoogleOAuth2Detail />
|
<GoogleOAuth2Detail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<GoogleOAuth2Edit />
|
<GoogleOAuth2Edit />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}/details`}>
|
||||||
|
{i18n._(t`View Google OAuth 2.0 settings`)}
|
||||||
|
</Link>
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,16 +1,57 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
import GoogleOAuth2 from './GoogleOAuth2';
|
import GoogleOAuth2 from './GoogleOAuth2';
|
||||||
|
|
||||||
|
import { SettingsAPI } from '../../../api';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<GoogleOAuth2 />', () => {
|
describe('<GoogleOAuth2 />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<GoogleOAuth2 />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain('Google OAuth 2.0 settings');
|
test('should render Google OAuth 2.0 details', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/google_oauth2/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<GoogleOAuth2 />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('GoogleOAuth2Detail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render Google OAuth 2.0 edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/google_oauth2/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<GoogleOAuth2 />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('GoogleOAuth2Edit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/google_oauth2/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<GoogleOAuth2 />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,88 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } 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 { Button } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
|
||||||
function GoogleOAuth2Detail({ i18n }) {
|
function GoogleOAuth2Detail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: options } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request, result: googleOAuth2 } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('google-oauth2');
|
||||||
|
return data;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/settings/google_oauth2/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
{isLoading && <ContentLoading />}
|
||||||
aria-label={i18n._(t`Edit`)}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
component={Link}
|
{!isLoading && googleOAuth2 && (
|
||||||
to="/settings/google_oauth2/edit"
|
<DetailList>
|
||||||
>
|
{Object.keys(googleOAuth2).map(key => {
|
||||||
{i18n._(t`Edit`)}
|
const record = options?.[key];
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<SettingDetail
|
||||||
</CardBody>
|
key={key}
|
||||||
|
label={record?.label}
|
||||||
|
type={record?.type}
|
||||||
|
value={googleOAuth2?.[key]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to="/settings/google_oauth2/edit"
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,119 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import {
|
||||||
|
assertDetail,
|
||||||
|
assertVariableDetail,
|
||||||
|
} from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
import GoogleOAuth2Detail from './GoogleOAuth2Detail';
|
import GoogleOAuth2Detail from './GoogleOAuth2Detail';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL:
|
||||||
|
'https://towerhost/sso/complete/google-oauth2/',
|
||||||
|
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: 'mock key',
|
||||||
|
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: '$encrypted$',
|
||||||
|
SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: [
|
||||||
|
'example.com',
|
||||||
|
'example_2.com',
|
||||||
|
],
|
||||||
|
SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: {},
|
||||||
|
SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP: {
|
||||||
|
Default: {},
|
||||||
|
},
|
||||||
|
SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<GoogleOAuth2Detail />', () => {
|
describe('<GoogleOAuth2Detail />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<GoogleOAuth2Detail />);
|
beforeAll(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<GoogleOAuth2Detail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
|
afterAll(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('GoogleOAuth2Detail').length).toBe(1);
|
expect(wrapper.find('GoogleOAuth2Detail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', () => {
|
||||||
|
const expectedTabs = ['Back to Settings', 'Details'];
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Google OAuth2 Callback URL',
|
||||||
|
'https://towerhost/sso/complete/google-oauth2/'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'Google OAuth2 Key', 'mock key');
|
||||||
|
assertDetail(wrapper, 'Google OAuth2 Secret', 'Encrypted');
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'Google OAuth2 Whitelisted Domains',
|
||||||
|
'[\n "example.com",\n "example_2.com"\n]'
|
||||||
|
);
|
||||||
|
assertVariableDetail(wrapper, 'Google OAuth2 Extra Arguments', '{}');
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'Google OAuth2 Organization Map',
|
||||||
|
'{\n "Default": {}\n}'
|
||||||
|
);
|
||||||
|
assertVariableDetail(wrapper, 'Google OAuth2 Team Map', '{}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<GoogleOAuth2Detail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<GoogleOAuth2Detail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
import JobsDetail from './JobsDetail';
|
import JobsDetail from './JobsDetail';
|
||||||
import JobsEdit from './JobsEdit';
|
import JobsEdit from './JobsEdit';
|
||||||
|
|
||||||
function Jobs({ i18n }) {
|
function Jobs({ i18n }) {
|
||||||
const baseUrl = '/settings/jobs';
|
const baseURL = '/settings/jobs';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`Jobs settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/details`}>
|
||||||
<JobsDetail />
|
<JobsDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<JobsEdit />
|
<JobsEdit />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}/details`}>
|
||||||
|
{i18n._(t`View Jobs settings`)}
|
||||||
|
</Link>
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,16 +1,57 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
import Jobs from './Jobs';
|
import Jobs from './Jobs';
|
||||||
|
|
||||||
|
import { SettingsAPI } from '../../../api';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<Jobs />', () => {
|
describe('<Jobs />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<Jobs />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain('Jobs settings');
|
test('should render jobs details', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/jobs/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Jobs />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('JobsDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render jobs edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/jobs/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Jobs />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('JobsEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/jobs/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Jobs />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,106 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } 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 { Button } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import { sortNestedDetails } from '../../shared/settingUtils';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
|
||||||
function JobsDetail({ i18n }) {
|
function JobsDetail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: options } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request, result: jobs } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('jobs');
|
||||||
|
|
||||||
|
const {
|
||||||
|
ALLOW_JINJA_IN_EXTRA_VARS,
|
||||||
|
AWX_ISOLATED_KEY_GENERATION,
|
||||||
|
AWX_ISOLATED_PRIVATE_KEY,
|
||||||
|
AWX_ISOLATED_PUBLIC_KEY,
|
||||||
|
GALAXY_IGNORE_CERTS,
|
||||||
|
STDOUT_MAX_BYTES_DISPLAY,
|
||||||
|
EVENT_STDOUT_MAX_BYTES_DISPLAY,
|
||||||
|
...jobsData
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const mergedData = {};
|
||||||
|
Object.keys(jobsData).forEach(key => {
|
||||||
|
mergedData[key] = options[key];
|
||||||
|
mergedData[key].value = jobsData[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortNestedDetails(mergedData);
|
||||||
|
}, [options]),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/settings/jobs/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
{isLoading && <ContentLoading />}
|
||||||
aria-label={i18n._(t`Edit`)}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
component={Link}
|
{!isLoading && jobs && (
|
||||||
to="/settings/jobs/edit"
|
<DetailList>
|
||||||
>
|
{Array.from(jobs).map(([, detail]) => {
|
||||||
{i18n._(t`Edit`)}
|
return (
|
||||||
</Button>
|
<SettingDetail
|
||||||
</CardActionsRow>
|
key={detail?.label}
|
||||||
</CardBody>
|
label={detail?.label}
|
||||||
|
type={detail?.type}
|
||||||
|
value={detail?.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to="/settings/jobs/edit"
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import {
|
||||||
|
assertDetail,
|
||||||
|
assertVariableDetail,
|
||||||
|
} from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
|
import mockJobSettings from '../../shared/data.jobSettings.json';
|
||||||
import JobsDetail from './JobsDetail';
|
import JobsDetail from './JobsDetail';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: mockJobSettings,
|
||||||
|
});
|
||||||
|
|
||||||
describe('<JobsDetail />', () => {
|
describe('<JobsDetail />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<JobsDetail />);
|
beforeAll(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<JobsDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
|
afterAll(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('JobsDetail').length).toBe(1);
|
expect(wrapper.find('JobsDetail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', () => {
|
||||||
|
const expectedTabs = ['Back to Settings', 'Details'];
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(wrapper, 'Enable job isolation', 'On');
|
||||||
|
assertDetail(wrapper, 'Job execution path', '/tmp');
|
||||||
|
assertDetail(wrapper, 'Isolated status check interval', '1');
|
||||||
|
assertDetail(wrapper, 'Isolated launch timeout', '600');
|
||||||
|
assertDetail(wrapper, 'Isolated connection timeout', '10');
|
||||||
|
assertDetail(wrapper, 'Isolated host key checking', 'Off');
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Enable detailed resource profiling on all playbook runs',
|
||||||
|
'Off'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'Run Project Updates With Higher Verbosity', 'Off');
|
||||||
|
assertDetail(wrapper, 'Enable Role Download', 'On');
|
||||||
|
assertDetail(wrapper, 'Enable Collection(s) Download', 'On');
|
||||||
|
assertDetail(wrapper, 'Follow symlinks', 'Off');
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Primary Galaxy Server URL',
|
||||||
|
'https://galaxy.server.com'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'Primary Galaxy Server Username', 'Unconfigured');
|
||||||
|
assertDetail(wrapper, 'Primary Galaxy Server Password', 'Unconfigured');
|
||||||
|
assertDetail(wrapper, 'Primary Galaxy Server Token', 'Encrypted');
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Primary Galaxy Authentication URL',
|
||||||
|
'https://galaxy.auth.com'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'Allow Access to Public Galaxy', 'On');
|
||||||
|
assertDetail(wrapper, 'Maximum Scheduled Jobs', '10');
|
||||||
|
assertDetail(wrapper, 'Default Job Timeout', 'Unconfigured');
|
||||||
|
assertDetail(wrapper, 'Default Inventory Update Timeout', 'Unconfigured');
|
||||||
|
assertDetail(wrapper, 'Default Project Update Timeout', 'Unconfigured');
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Per-Host Ansible Fact Cache Timeout',
|
||||||
|
'Unconfigured'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'Maximum number of forks per job.', '200');
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'Ansible Modules Allowed for Ad Hoc Jobs',
|
||||||
|
'[\n "command"\n]'
|
||||||
|
);
|
||||||
|
assertVariableDetail(wrapper, 'Paths to hide from isolated jobs', '[]');
|
||||||
|
assertVariableDetail(wrapper, 'Paths to expose to isolated jobs', '[]');
|
||||||
|
assertVariableDetail(wrapper, 'Extra Environment Variables', '{}');
|
||||||
|
assertVariableDetail(wrapper, 'Ansible Callback Plugins', '[]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<JobsDetail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<JobsDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
import LDAPDetail from './LDAPDetail';
|
import LDAPDetail from './LDAPDetail';
|
||||||
import LDAPEdit from './LDAPEdit';
|
import LDAPEdit from './LDAPEdit';
|
||||||
|
|
||||||
function LDAP({ i18n }) {
|
function LDAP({ i18n }) {
|
||||||
const baseUrl = '/settings/ldap';
|
const baseURL = '/settings/ldap';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`LDAP settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/default/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/:category/details`}>
|
||||||
<LDAPDetail />
|
<LDAPDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/:category/edit`}>
|
||||||
<LDAPEdit />
|
<LDAPEdit />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}/default/details`}>
|
||||||
|
{i18n._(t`View LDAP Settings`)}
|
||||||
|
</Link>
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,16 +1,61 @@
|
|||||||
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 LDAP from './LDAP';
|
import LDAP from './LDAP';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<LDAP />', () => {
|
describe('<LDAP />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<LDAP />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain('LDAP settings');
|
test('should render ldap details', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/ldap/'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<LDAP />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('LDAPDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render ldap edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/ldap/default/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<LDAP />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('LDAPEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/ldap/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<LDAP />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,169 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, Redirect, useRouteMatch } 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 { Button } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
import { sortNestedDetails } from '../../shared/settingUtils';
|
||||||
|
|
||||||
|
function filterByPrefix(data, prefix) {
|
||||||
|
return Object.keys(data)
|
||||||
|
.filter(key => key.includes(prefix))
|
||||||
|
.reduce((obj, key) => {
|
||||||
|
obj[key] = data[key];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
function LDAPDetail({ i18n }) {
|
function LDAPDetail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: options } = useSettings();
|
||||||
|
const {
|
||||||
|
path,
|
||||||
|
params: { category },
|
||||||
|
} = useRouteMatch('/settings/ldap/:category/details');
|
||||||
|
|
||||||
|
const { isLoading, error, request, result: LDAPDetails } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('ldap');
|
||||||
|
|
||||||
|
const mergedData = {};
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
if (key.includes('_CONNECTION_OPTIONS')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mergedData[key] = options[key];
|
||||||
|
mergedData[key].value = data[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
const ldap1 = filterByPrefix(mergedData, 'AUTH_LDAP_1_');
|
||||||
|
const ldap2 = filterByPrefix(mergedData, 'AUTH_LDAP_2_');
|
||||||
|
const ldap3 = filterByPrefix(mergedData, 'AUTH_LDAP_3_');
|
||||||
|
const ldap4 = filterByPrefix(mergedData, 'AUTH_LDAP_4_');
|
||||||
|
const ldap5 = filterByPrefix(mergedData, 'AUTH_LDAP_5_');
|
||||||
|
const ldapDefault = Object.assign({}, mergedData);
|
||||||
|
Object.keys({ ...ldap1, ...ldap2, ...ldap3, ...ldap4, ...ldap5 }).forEach(
|
||||||
|
keyToOmit => {
|
||||||
|
delete ldapDefault[keyToOmit];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
default: sortNestedDetails(ldapDefault),
|
||||||
|
1: sortNestedDetails(ldap1),
|
||||||
|
2: sortNestedDetails(ldap2),
|
||||||
|
3: sortNestedDetails(ldap3),
|
||||||
|
4: sortNestedDetails(ldap4),
|
||||||
|
5: sortNestedDetails(ldap5),
|
||||||
|
};
|
||||||
|
}, [options]),
|
||||||
|
{
|
||||||
|
default: null,
|
||||||
|
1: null,
|
||||||
|
2: null,
|
||||||
|
3: null,
|
||||||
|
4: null,
|
||||||
|
5: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const baseURL = '/settings/ldap';
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Default`),
|
||||||
|
link: `${baseURL}/default/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`LDAP1`),
|
||||||
|
link: `${baseURL}/1/details`,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`LDAP2`),
|
||||||
|
link: `${baseURL}/2/details`,
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`LDAP3`),
|
||||||
|
link: `${baseURL}/3/details`,
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`LDAP4`),
|
||||||
|
link: `${baseURL}/4/details`,
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`LDAP5`),
|
||||||
|
link: `${baseURL}/5/details`,
|
||||||
|
id: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!Object.keys(LDAPDetails).includes(category)) {
|
||||||
|
return <Redirect from={path} to={`${baseURL}/default/details`} exact />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
<>
|
||||||
aria-label={i18n._(t`Edit`)}
|
{isLoading && <ContentLoading />}
|
||||||
component={Link}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
to="/settings/ldap/edit"
|
{!isLoading && !Object.values(LDAPDetails)?.includes(null) && (
|
||||||
>
|
<DetailList>
|
||||||
{i18n._(t`Edit`)}
|
{Array.from(LDAPDetails[category]).map(([, detail]) => {
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<SettingDetail
|
||||||
</CardBody>
|
key={detail?.label}
|
||||||
|
label={detail?.label}
|
||||||
|
type={detail?.type}
|
||||||
|
value={detail?.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to={`${baseURL}/${category}/edit`}
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,151 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { useRouteMatch } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import {
|
||||||
|
assertDetail,
|
||||||
|
assertVariableDetail,
|
||||||
|
} from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
|
import mockLDAP from '../../shared/data.ldapSettings.json';
|
||||||
import LDAPDetail from './LDAPDetail';
|
import LDAPDetail from './LDAPDetail';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useRouteMatch: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({ data: mockLDAP });
|
||||||
|
|
||||||
describe('<LDAPDetail />', () => {
|
describe('<LDAPDetail />', () => {
|
||||||
let wrapper;
|
describe('Default', () => {
|
||||||
beforeEach(() => {
|
let wrapper;
|
||||||
wrapper = mountWithContexts(<LDAPDetail />);
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
useRouteMatch.mockImplementation(() => ({
|
||||||
|
url: '/settings/ldap/default/details',
|
||||||
|
path: '/settings/ldap/:category/details',
|
||||||
|
params: { category: 'default' },
|
||||||
|
}));
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<LDAPDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initially renders without crashing', () => {
|
||||||
|
expect(wrapper.find('LDAPDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', () => {
|
||||||
|
const expectedTabs = [
|
||||||
|
'Back to Settings',
|
||||||
|
'Default',
|
||||||
|
'LDAP1',
|
||||||
|
'LDAP2',
|
||||||
|
'LDAP3',
|
||||||
|
'LDAP4',
|
||||||
|
'LDAP5',
|
||||||
|
];
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(wrapper, 'LDAP Server URI', 'ldap://ldap.example.com');
|
||||||
|
assertDetail(wrapper, 'LDAP Bind DN', 'cn=eng_user');
|
||||||
|
assertDetail(wrapper, 'LDAP Bind Password', 'Encrypted');
|
||||||
|
assertDetail(wrapper, 'LDAP Start TLS', 'Off');
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'LDAP User DN Template',
|
||||||
|
'uid=%(user)s,OU=Users,DC=example,DC=com'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'LDAP Group Type', 'MemberDNGroupType');
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'LDAP Require Group',
|
||||||
|
'CN=Tower Users,OU=Users,DC=example,DC=com'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'LDAP Deny Group', 'Unconfigured');
|
||||||
|
assertVariableDetail(wrapper, 'LDAP User Search', '[]');
|
||||||
|
assertVariableDetail(wrapper, 'LDAP User Attribute Map', '{}');
|
||||||
|
assertVariableDetail(wrapper, 'LDAP Group Search', '[]');
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'LDAP Group Type Parameters',
|
||||||
|
'{\n "name_attr": "cn",\n "member_attr": "member"\n}'
|
||||||
|
);
|
||||||
|
assertVariableDetail(wrapper, 'LDAP User Flags By Group', '{}');
|
||||||
|
assertVariableDetail(wrapper, 'LDAP Organization Map', '{}');
|
||||||
|
assertVariableDetail(wrapper, 'LDAP Team Map', '{}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<LDAPDetail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<LDAPDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
wrapper.unmount();
|
describe('Redirect', () => {
|
||||||
});
|
test('should render redirect when user navigates to erroneous category', async () => {
|
||||||
test('initially renders without crashing', () => {
|
let wrapper;
|
||||||
expect(wrapper.find('LDAPDetail').length).toBe(1);
|
useRouteMatch.mockImplementation(() => ({
|
||||||
|
url: '/settings/ldap/foo/details',
|
||||||
|
path: '/settings/ldap/:category/details',
|
||||||
|
params: { category: 'foo' },
|
||||||
|
}));
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<LDAPDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'Redirect');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
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';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`Logging settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/details`}>
|
||||||
<LoggingDetail />
|
<LoggingDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<LoggingEdit />
|
<LoggingEdit />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}`}>{i18n._(t`View Logging settings`)}</Link>
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,16 +1,61 @@
|
|||||||
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 Logging from './Logging';
|
import Logging from './Logging';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<Logging />', () => {
|
describe('<Logging />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<Logging />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain('Logging settings');
|
test('should render logging details', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/logging/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Logging />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('LoggingDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render logging edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/logging/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Logging />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('LoggingEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/logging/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Logging />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,109 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } 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 { Button } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
import { sortNestedDetails, pluck } from '../../shared/settingUtils';
|
||||||
|
|
||||||
function LoggingDetail({ i18n }) {
|
function LoggingDetail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: options } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request, result: logging } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('logging');
|
||||||
|
|
||||||
|
const loggingData = pluck(
|
||||||
|
data,
|
||||||
|
'LOG_AGGREGATOR_ENABLED',
|
||||||
|
'LOG_AGGREGATOR_HOST',
|
||||||
|
'LOG_AGGREGATOR_INDIVIDUAL_FACTS',
|
||||||
|
'LOG_AGGREGATOR_LEVEL',
|
||||||
|
'LOG_AGGREGATOR_LOGGERS',
|
||||||
|
'LOG_AGGREGATOR_PASSWORD',
|
||||||
|
'LOG_AGGREGATOR_PORT',
|
||||||
|
'LOG_AGGREGATOR_PROTOCOL',
|
||||||
|
'LOG_AGGREGATOR_TCP_TIMEOUT',
|
||||||
|
'LOG_AGGREGATOR_TYPE',
|
||||||
|
'LOG_AGGREGATOR_USERNAME',
|
||||||
|
'LOG_AGGREGATOR_VERIFY_CERT'
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedData = {};
|
||||||
|
Object.keys(loggingData).forEach(key => {
|
||||||
|
mergedData[key] = options[key];
|
||||||
|
mergedData[key].value = loggingData[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortNestedDetails(mergedData);
|
||||||
|
}, [options]),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/settings/logging/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
{isLoading && <ContentLoading />}
|
||||||
aria-label={i18n._(t`Edit`)}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
component={Link}
|
{!isLoading && logging && (
|
||||||
to="/settings/logging/edit"
|
<DetailList>
|
||||||
>
|
{logging.map(([key, detail]) => (
|
||||||
{i18n._(t`Edit`)}
|
<SettingDetail
|
||||||
</Button>
|
key={key}
|
||||||
</CardActionsRow>
|
label={detail?.label}
|
||||||
</CardBody>
|
type={detail?.type}
|
||||||
|
value={detail?.value}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to="/settings/logging/edit"
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,107 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import {
|
||||||
|
assertDetail,
|
||||||
|
assertVariableDetail,
|
||||||
|
} from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
|
import mockLogSettings from '../../shared/data.logSettings.json';
|
||||||
import LoggingDetail from './LoggingDetail';
|
import LoggingDetail from './LoggingDetail';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: mockLogSettings,
|
||||||
|
});
|
||||||
|
|
||||||
describe('<LoggingDetail />', () => {
|
describe('<LoggingDetail />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<LoggingDetail />);
|
beforeAll(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<LoggingDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
|
afterAll(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('LoggingDetail').length).toBe(1);
|
expect(wrapper.find('LoggingDetail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', () => {
|
||||||
|
const expectedTabs = ['Back to Settings', 'Details'];
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(wrapper, 'Enable External Logging', 'Off');
|
||||||
|
assertDetail(wrapper, 'Logging Aggregator', 'https://mocklog');
|
||||||
|
assertDetail(wrapper, 'Logging Aggregator Port', '1234');
|
||||||
|
assertDetail(wrapper, 'Logging Aggregator Type', 'logstash');
|
||||||
|
assertDetail(wrapper, 'Logging Aggregator Username', 'logging_name');
|
||||||
|
assertDetail(wrapper, 'Logging Aggregator Password/Token', 'Encrypted');
|
||||||
|
assertDetail(wrapper, 'Log System Tracking Facts Individually', 'Off');
|
||||||
|
assertDetail(wrapper, 'Logging Aggregator Protocol', 'https');
|
||||||
|
assertDetail(wrapper, 'TCP Connection Timeout', '5');
|
||||||
|
assertDetail(wrapper, 'Logging Aggregator Level Threshold', 'INFO');
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Enable/disable HTTPS certificate verification',
|
||||||
|
'On'
|
||||||
|
);
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'Loggers Sending Data to Log Aggregator Form',
|
||||||
|
'[\n "activity_stream",\n "system_tracking"\n]'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<LoggingDetail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<LoggingDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
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';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`Miscellaneous system settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/details`}>
|
||||||
<MiscSystemDetail />
|
<MiscSystemDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<MiscSystemEdit />
|
<MiscSystemEdit />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}/details`}>
|
||||||
|
{i18n._(t`View Miscellaneous System settings`)}
|
||||||
|
</Link>
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,18 +1,61 @@
|
|||||||
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 MiscSystem from './MiscSystem';
|
import MiscSystem from './MiscSystem';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<MiscSystem />', () => {
|
describe('<MiscSystem />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<MiscSystem />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain(
|
test('should render miscellaneous system details', async () => {
|
||||||
'Miscellaneous system settings'
|
const history = createMemoryHistory({
|
||||||
);
|
initialEntries: ['/settings/miscellaneous_system/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<MiscSystem />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('MiscSystemDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render miscellaneous system edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/miscellaneous_system/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<MiscSystem />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('MiscSystemEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/miscellaneous_system/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<MiscSystem />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,150 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } 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 { Button } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
import { sortNestedDetails, pluck } from '../../shared/settingUtils';
|
||||||
|
|
||||||
function MiscSystemDetail({ i18n }) {
|
function MiscSystemDetail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: allOptions } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request, 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,
|
||||||
|
...options
|
||||||
|
} = allOptions;
|
||||||
|
|
||||||
|
const systemOptions = {
|
||||||
|
...options,
|
||||||
|
ACCESS_TOKEN_EXPIRE_SECONDS: {
|
||||||
|
...OAUTH2_PROVIDER_OPTIONS,
|
||||||
|
type: OAUTH2_PROVIDER_OPTIONS.child.type,
|
||||||
|
label: i18n._(t`Access Token Expiration`),
|
||||||
|
},
|
||||||
|
REFRESH_TOKEN_EXPIRE_SECONDS: {
|
||||||
|
...OAUTH2_PROVIDER_OPTIONS,
|
||||||
|
type: OAUTH2_PROVIDER_OPTIONS.child.type,
|
||||||
|
label: i18n._(t`Refresh Token Expiration`),
|
||||||
|
},
|
||||||
|
AUTHORIZATION_CODE_EXPIRE_SECONDS: {
|
||||||
|
...OAUTH2_PROVIDER_OPTIONS,
|
||||||
|
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 sortNestedDetails(mergedData);
|
||||||
|
}, [allOptions, i18n]),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/settings/miscellaneous_system/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
{isLoading && <ContentLoading />}
|
||||||
aria-label={i18n._(t`Edit`)}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
component={Link}
|
{!isLoading && system && (
|
||||||
to="/settings/miscellaneous_system/edit"
|
<DetailList>
|
||||||
>
|
{system.map(([key, detail]) => (
|
||||||
{i18n._(t`Edit`)}
|
<SettingDetail
|
||||||
</Button>
|
key={key}
|
||||||
</CardActionsRow>
|
label={detail?.label}
|
||||||
</CardBody>
|
type={detail?.type}
|
||||||
|
value={detail?.value}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to="/settings/miscellaneous_system/edit"
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,144 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import {
|
||||||
|
assertDetail,
|
||||||
|
assertVariableDetail,
|
||||||
|
} from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
import MiscSystemDetail from './MiscSystemDetail';
|
import MiscSystemDetail from './MiscSystemDetail';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
ALLOW_OAUTH2_FOR_EXTERNAL_USERS: false,
|
||||||
|
AUTH_BASIC_ENABLED: true,
|
||||||
|
AUTOMATION_ANALYTICS_GATHER_INTERVAL: 14400,
|
||||||
|
AUTOMATION_ANALYTICS_URL: 'https://example.com',
|
||||||
|
CUSTOM_VENV_PATHS: [],
|
||||||
|
INSIGHTS_TRACKING_STATE: false,
|
||||||
|
LOGIN_REDIRECT_OVERRIDE: 'https://redirect.com',
|
||||||
|
MANAGE_ORGANIZATION_AUTH: true,
|
||||||
|
OAUTH2_PROVIDER: {
|
||||||
|
ACCESS_TOKEN_EXPIRE_SECONDS: 1,
|
||||||
|
AUTHORIZATION_CODE_EXPIRE_SECONDS: 2,
|
||||||
|
REFRESH_TOKEN_EXPIRE_SECONDS: 3,
|
||||||
|
},
|
||||||
|
ORG_ADMINS_CAN_SEE_ALL_USERS: true,
|
||||||
|
REDHAT_PASSWORD: '$encrypted$',
|
||||||
|
REDHAT_USERNAME: 'mock name',
|
||||||
|
REMOTE_HOST_HEADERS: [],
|
||||||
|
SESSIONS_PER_USER: -1,
|
||||||
|
SESSION_COOKIE_AGE: 30000000000,
|
||||||
|
TOWER_URL_BASE: 'https://towerhost',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<MiscSystemDetail />', () => {
|
describe('<MiscSystemDetail />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<MiscSystemDetail />);
|
beforeAll(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<MiscSystemDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
|
afterAll(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('MiscSystemDetail').length).toBe(1);
|
expect(wrapper.find('MiscSystemDetail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', () => {
|
||||||
|
const expectedTabs = ['Back to Settings', 'Details'];
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(wrapper, 'Access Token Expiration', '1');
|
||||||
|
assertDetail(wrapper, 'All Users Visible to Organization Admins', 'On');
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Allow External Users to Create OAuth2 Tokens',
|
||||||
|
'Off'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'Authorization Code Expiration', '2');
|
||||||
|
assertDetail(wrapper, 'Automation Analytics Gather Interval', '14400');
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Automation Analytics upload URL.',
|
||||||
|
'https://example.com'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'Base URL of the Tower host', 'https://towerhost');
|
||||||
|
assertDetail(wrapper, 'Enable HTTP Basic Auth', 'On');
|
||||||
|
assertDetail(wrapper, 'Gather data for Automation Analytics', 'Off');
|
||||||
|
assertDetail(wrapper, 'Idle Time Force Log Out', '30000000000');
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Login redirect override URL',
|
||||||
|
'https://redirect.com'
|
||||||
|
);
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Maximum number of simultaneous logged in sessions',
|
||||||
|
'-1'
|
||||||
|
);
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'Organization Admins Can Manage Users and Teams',
|
||||||
|
'On'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'Red Hat customer password', 'Encrypted');
|
||||||
|
assertDetail(wrapper, 'Red Hat customer username', 'mock name');
|
||||||
|
assertDetail(wrapper, 'Refresh Token Expiration', '3');
|
||||||
|
assertVariableDetail(wrapper, 'Remote Host Headers', '[]');
|
||||||
|
assertVariableDetail(wrapper, 'Custom virtual environment paths', '[]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<MiscSystemDetail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<MiscSystemDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
import RadiusDetail from './RadiusDetail';
|
import ContentError from '../../../components/ContentError';
|
||||||
import RadiusEdit from './RadiusEdit';
|
import RADIUSDetail from './RADIUSDetail';
|
||||||
|
import RADIUSEdit from './RADIUSEdit';
|
||||||
function Radius({ i18n }) {
|
|
||||||
const baseUrl = '/settings/radius';
|
|
||||||
|
|
||||||
|
function RADIUS({ i18n }) {
|
||||||
|
const baseURL = '/settings/radius';
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`Radius settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/details`}>
|
||||||
<RadiusDetail />
|
<RADIUSDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<RadiusEdit />
|
<RADIUSEdit />
|
||||||
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}/details`}>
|
||||||
|
{i18n._(t`View RADIUS settings`)}
|
||||||
|
</Link>
|
||||||
|
</ContentError>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -27,4 +33,4 @@ function Radius({ i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(Radius);
|
export default withI18n()(RADIUS);
|
||||||
|
|||||||
@@ -1,16 +1,56 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
import Radius from './Radius';
|
import RADIUS from './RADIUS';
|
||||||
|
import { SettingsAPI } from '../../../api';
|
||||||
|
|
||||||
describe('<Radius />', () => {
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('<RADIUS />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<Radius />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain('Radius settings');
|
test('should render RADIUS details', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/radius/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<RADIUS />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('RADIUSDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render RADIUS edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/radius/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<RADIUS />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('RADIUSEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/radius/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<RADIUS />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +1,89 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } 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 { Button } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
|
||||||
|
function RADIUSDetail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: options } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request, result: radius } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('radius');
|
||||||
|
return data;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/settings/radius/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function RadiusDetail({ i18n }) {
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
{isLoading && <ContentLoading />}
|
||||||
aria-label={i18n._(t`Edit`)}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
component={Link}
|
{!isLoading && radius && (
|
||||||
to="/settings/radius/edit"
|
<DetailList>
|
||||||
>
|
{Object.keys(radius).map(key => {
|
||||||
{i18n._(t`Edit`)}
|
const record = options?.[key];
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<SettingDetail
|
||||||
</CardBody>
|
key={key}
|
||||||
|
label={record?.label}
|
||||||
|
type={record?.type}
|
||||||
|
value={radius?.[key]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to="/settings/radius/edit"
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(RadiusDetail);
|
export default withI18n()(RADIUSDetail);
|
||||||
|
|||||||
@@ -1,16 +1,90 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
import RadiusDetail from './RadiusDetail';
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import { assertDetail } from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
|
import RADIUSDetail from './RADIUSDetail';
|
||||||
|
|
||||||
describe('<RadiusDetail />', () => {
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
RADIUS_SERVER: 'example.org',
|
||||||
|
RADIUS_PORT: 1812,
|
||||||
|
RADIUS_SECRET: '$encrypted$',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('<RADIUSDetail />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<RadiusDetail />);
|
beforeAll(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<RADIUSDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
|
afterAll(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('RadiusDetail').length).toBe(1);
|
expect(wrapper.find('RADIUSDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', () => {
|
||||||
|
const expectedTabs = ['Back to Settings', 'Details'];
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(wrapper, 'RADIUS Server', 'example.org');
|
||||||
|
assertDetail(wrapper, 'RADIUS Port', '1812');
|
||||||
|
assertDetail(wrapper, 'RADIUS Secret', 'Encrypted');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<RADIUSDetail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<RADIUSDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default } from './RadiusDetail';
|
export { default } from './RADIUSDetail';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { t } from '@lingui/macro';
|
|||||||
import { Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
|
||||||
function RadiusEdit({ i18n }) {
|
function RADIUSEdit({ i18n }) {
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{i18n._(t`Edit form coming soon :)`)}
|
{i18n._(t`Edit form coming soon :)`)}
|
||||||
@@ -22,4 +22,4 @@ function RadiusEdit({ i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(RadiusEdit);
|
export default withI18n()(RADIUSEdit);
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
||||||
import RadiusEdit from './RadiusEdit';
|
import RADIUSEdit from './RADIUSEdit';
|
||||||
|
|
||||||
describe('<RadiusEdit />', () => {
|
describe('<RADIUSEdit />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = mountWithContexts(<RadiusEdit />);
|
wrapper = mountWithContexts(<RADIUSEdit />);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('RadiusEdit').length).toBe(1);
|
expect(wrapper.find('RADIUSEdit').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default } from './RadiusEdit';
|
export { default } from './RADIUSEdit';
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default } from './Radius';
|
export { default } from './RADIUS';
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
import SAMLDetail from './SAMLDetail';
|
import SAMLDetail from './SAMLDetail';
|
||||||
import SAMLEdit from './SAMLEdit';
|
import SAMLEdit from './SAMLEdit';
|
||||||
|
|
||||||
function SAML({ i18n }) {
|
function SAML({ i18n }) {
|
||||||
const baseUrl = '/settings/saml';
|
const baseURL = '/settings/saml';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`SAML settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/details`}>
|
||||||
<SAMLDetail />
|
<SAMLDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<SAMLEdit />
|
<SAMLEdit />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}/details`}>
|
||||||
|
{i18n._(t`View SAML settings`)}
|
||||||
|
</Link>
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,16 +1,56 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsAPI } from '../../../api';
|
||||||
import SAML from './SAML';
|
import SAML from './SAML';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<SAML />', () => {
|
describe('<SAML />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<SAML />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain('SAML settings');
|
test('should render SAML details', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/saml/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<SAML />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('SAMLDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render SAML edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/saml/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<SAML />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('SAMLEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/saml/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<SAML />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,88 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } 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 { Button } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
|
||||||
function SAMLDetail({ i18n }) {
|
function SAMLDetail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: options } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request, result: saml } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('saml');
|
||||||
|
return data;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/settings/saml/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
{isLoading && <ContentLoading />}
|
||||||
aria-label={i18n._(t`Edit`)}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
component={Link}
|
{!isLoading && saml && (
|
||||||
to="/settings/saml/edit"
|
<DetailList>
|
||||||
>
|
{Object.keys(saml).map(key => {
|
||||||
{i18n._(t`Edit`)}
|
const record = options?.[key];
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<SettingDetail
|
||||||
</CardBody>
|
key={key}
|
||||||
|
label={record?.label}
|
||||||
|
type={record?.type}
|
||||||
|
value={saml?.[key]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to="/settings/saml/edit"
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,144 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import {
|
||||||
|
assertDetail,
|
||||||
|
assertVariableDetail,
|
||||||
|
} from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
import SAMLDetail from './SAMLDetail';
|
import SAMLDetail from './SAMLDetail';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
SOCIAL_AUTH_SAML_CALLBACK_URL: 'https://towerhost/sso/complete/saml/',
|
||||||
|
SOCIAL_AUTH_SAML_METADATA_URL: 'https://towerhost/sso/metadata/saml/',
|
||||||
|
SOCIAL_AUTH_SAML_SP_ENTITY_ID: 'mock_id',
|
||||||
|
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT: 'mock_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: {},
|
||||||
|
SOCIAL_AUTH_SAML_SP_EXTRA: {},
|
||||||
|
SOCIAL_AUTH_SAML_EXTRA_DATA: [],
|
||||||
|
SOCIAL_AUTH_SAML_ORGANIZATION_MAP: {},
|
||||||
|
SOCIAL_AUTH_SAML_TEAM_MAP: {},
|
||||||
|
SOCIAL_AUTH_SAML_ORGANIZATION_ATTR: {},
|
||||||
|
SOCIAL_AUTH_SAML_TEAM_ATTR: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<SAMLDetail />', () => {
|
describe('<SAMLDetail />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<SAMLDetail />);
|
beforeAll(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<SAMLDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
|
afterAll(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('SAMLDetail').length).toBe(1);
|
expect(wrapper.find('SAMLDetail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'SAML Assertion Consumer Service (ACS) URL',
|
||||||
|
'https://towerhost/sso/complete/saml/'
|
||||||
|
);
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'SAML Service Provider Metadata URL',
|
||||||
|
'https://towerhost/sso/metadata/saml/'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'SAML Service Provider Entity ID', 'mock_id');
|
||||||
|
assertDetail(
|
||||||
|
wrapper,
|
||||||
|
'SAML Service Provider Public Certificate',
|
||||||
|
'mock_cert'
|
||||||
|
);
|
||||||
|
assertDetail(wrapper, 'SAML Service Provider Private Key', 'Unconfigured');
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'SAML Service Provider Organization Info',
|
||||||
|
'{}'
|
||||||
|
);
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'SAML Service Provider Technical Contact',
|
||||||
|
'{}'
|
||||||
|
);
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'SAML Service Provider Support Contact',
|
||||||
|
'{}'
|
||||||
|
);
|
||||||
|
assertVariableDetail(wrapper, 'SAML Enabled Identity Providers', '{}');
|
||||||
|
assertVariableDetail(wrapper, 'SAML Security Config', '{}');
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'SAML Service Provider extra configuration data',
|
||||||
|
'{}'
|
||||||
|
);
|
||||||
|
assertVariableDetail(
|
||||||
|
wrapper,
|
||||||
|
'SAML IDP to extra_data attribute mapping',
|
||||||
|
'[]'
|
||||||
|
);
|
||||||
|
assertVariableDetail(wrapper, 'SAML Organization Map', '{}');
|
||||||
|
assertVariableDetail(wrapper, 'SAML Team Map', '{}');
|
||||||
|
assertVariableDetail(wrapper, 'SAML Organization Attribute Mapping', '{}');
|
||||||
|
assertVariableDetail(wrapper, 'SAML Team Attribute Mapping', '{}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<SAMLDetail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<SAMLDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function SettingList({ i18n }) {
|
|||||||
path: '/settings/ldap',
|
path: '/settings/ldap',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Radius settings`),
|
title: i18n._(t`RADIUS settings`),
|
||||||
path: '/settings/radius',
|
path: '/settings/radius',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -107,11 +107,11 @@ function SettingList({ i18n }) {
|
|||||||
id: 'system',
|
id: 'system',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
title: i18n._(t`Miscellaneous system settings`),
|
title: i18n._(t`Miscellaneous System settings`),
|
||||||
path: '/settings/miscellaneous_system',
|
path: '/settings/miscellaneous_system',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Activity stream settings`),
|
title: i18n._(t`Activity Stream settings`),
|
||||||
path: '/settings/activity_stream',
|
path: '/settings/activity_stream',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -121,15 +121,15 @@ function SettingList({ i18n }) {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: i18n._(t`User interface`),
|
header: i18n._(t`User Interface`),
|
||||||
description: i18n._(
|
description: i18n._(
|
||||||
t`Set preferences for data collection, logos, and logins`
|
t`Set preferences for data collection, logos, and logins`
|
||||||
),
|
),
|
||||||
id: 'user_interface',
|
id: 'ui',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
title: i18n._(t`User interface settings`),
|
title: i18n._(t`User Interface settings`),
|
||||||
path: '/settings/user_interface',
|
path: '/settings/ui',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { Link, Route, Switch, Redirect } from 'react-router-dom';
|
import { Link, Route, Switch, Redirect } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
import ContentError from '../../components/ContentError';
|
import ContentError from '../../components/ContentError';
|
||||||
|
import ContentLoading from '../../components/ContentLoading';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import Breadcrumbs from '../../components/Breadcrumbs';
|
||||||
import ActivityStream from './ActivityStream';
|
import ActivityStream from './ActivityStream';
|
||||||
import AzureAD from './AzureAD';
|
import AzureAD from './AzureAD';
|
||||||
@@ -14,34 +15,120 @@ import LDAP from './LDAP';
|
|||||||
import License from './License';
|
import License from './License';
|
||||||
import Logging from './Logging';
|
import Logging from './Logging';
|
||||||
import MiscSystem from './MiscSystem';
|
import MiscSystem from './MiscSystem';
|
||||||
import Radius from './Radius';
|
import RADIUS from './RADIUS';
|
||||||
import SAML from './SAML';
|
import SAML from './SAML';
|
||||||
import SettingList from './SettingList';
|
import SettingList from './SettingList';
|
||||||
import TACACS from './TACACS';
|
import TACACS from './TACACS';
|
||||||
import UI from './UI';
|
import UI from './UI';
|
||||||
|
import { SettingsProvider } from '../../contexts/Settings';
|
||||||
import { useConfig } from '../../contexts/Config';
|
import { useConfig } from '../../contexts/Config';
|
||||||
|
import { SettingsAPI } from '../../api';
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
|
|
||||||
function Settings({ i18n }) {
|
function Settings({ i18n }) {
|
||||||
const { license_info = {} } = useConfig();
|
const { license_info = {}, me } = useConfig();
|
||||||
|
|
||||||
|
const { request, result, isLoading, error } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const response = await SettingsAPI.readAllOptions();
|
||||||
|
return response.data.actions;
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
const breadcrumbConfig = {
|
const breadcrumbConfig = {
|
||||||
'/settings': i18n._(t`Settings`),
|
'/settings': i18n._(t`Settings`),
|
||||||
'/settings/activity_stream': i18n._(t`Activity stream`),
|
'/settings/activity_stream': i18n._(t`Activity Stream`),
|
||||||
|
'/settings/activity_stream/details': i18n._(t`Details`),
|
||||||
|
'/settings/activity_stream/edit': i18n._(t`Edit Details`),
|
||||||
'/settings/azure': i18n._(t`Azure AD`),
|
'/settings/azure': i18n._(t`Azure AD`),
|
||||||
'/settings/github': i18n._(t`GitHub`),
|
'/settings/azure/details': i18n._(t`Details`),
|
||||||
|
'/settings/azure/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/github': null,
|
||||||
|
'/settings/github/default': i18n._(t`GitHub Default`),
|
||||||
|
'/settings/github/default/details': i18n._(t`Details`),
|
||||||
|
'/settings/github/default/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/github/organization': i18n._(t`GitHub Organization`),
|
||||||
|
'/settings/github/organization/details': i18n._(t`Details`),
|
||||||
|
'/settings/github/organization/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/github/team': i18n._(t`GitHub Team`),
|
||||||
|
'/settings/github/team/details': i18n._(t`Details`),
|
||||||
|
'/settings/github/team/edit': i18n._(t`Edit Details`),
|
||||||
'/settings/google_oauth2': i18n._(t`Google OAuth2`),
|
'/settings/google_oauth2': i18n._(t`Google OAuth2`),
|
||||||
|
'/settings/google_oauth2/details': i18n._(t`Details`),
|
||||||
|
'/settings/google_oauth2/edit': i18n._(t`Edit Details`),
|
||||||
'/settings/jobs': i18n._(t`Jobs`),
|
'/settings/jobs': i18n._(t`Jobs`),
|
||||||
'/settings/ldap': i18n._(t`LDAP`),
|
'/settings/jobs/details': i18n._(t`Details`),
|
||||||
|
'/settings/jobs/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/ldap': null,
|
||||||
|
'/settings/ldap/default': i18n._(t`LDAP Default`),
|
||||||
|
'/settings/ldap/1': i18n._(t`LDAP 1`),
|
||||||
|
'/settings/ldap/2': i18n._(t`LDAP 2`),
|
||||||
|
'/settings/ldap/3': i18n._(t`LDAP 3`),
|
||||||
|
'/settings/ldap/4': i18n._(t`LDAP 4`),
|
||||||
|
'/settings/ldap/5': i18n._(t`LDAP 5`),
|
||||||
|
'/settings/ldap/default/details': i18n._(t`Details`),
|
||||||
|
'/settings/ldap/1/details': i18n._(t`Details`),
|
||||||
|
'/settings/ldap/2/details': i18n._(t`Details`),
|
||||||
|
'/settings/ldap/3/details': i18n._(t`Details`),
|
||||||
|
'/settings/ldap/4/details': i18n._(t`Details`),
|
||||||
|
'/settings/ldap/5/details': i18n._(t`Details`),
|
||||||
|
'/settings/ldap/default/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/ldap/1/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/ldap/2/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/ldap/3/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/ldap/4/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/ldap/5/edit': i18n._(t`Edit Details`),
|
||||||
'/settings/license': i18n._(t`License`),
|
'/settings/license': i18n._(t`License`),
|
||||||
'/settings/logging': i18n._(t`Logging`),
|
'/settings/logging': i18n._(t`Logging`),
|
||||||
'/settings/miscellaneous_system': i18n._(t`Miscellaneous system`),
|
'/settings/logging/details': i18n._(t`Details`),
|
||||||
'/settings/radius': i18n._(t`Radius`),
|
'/settings/logging/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/miscellaneous_system': i18n._(t`Miscellaneous System`),
|
||||||
|
'/settings/miscellaneous_system/details': i18n._(t`Details`),
|
||||||
|
'/settings/miscellaneous_system/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/radius': i18n._(t`RADIUS`),
|
||||||
|
'/settings/radius/details': i18n._(t`Details`),
|
||||||
|
'/settings/radius/edit': i18n._(t`Edit Details`),
|
||||||
'/settings/saml': i18n._(t`SAML`),
|
'/settings/saml': i18n._(t`SAML`),
|
||||||
|
'/settings/saml/details': i18n._(t`Details`),
|
||||||
|
'/settings/saml/edit': i18n._(t`Edit Details`),
|
||||||
'/settings/tacacs': i18n._(t`TACACS+`),
|
'/settings/tacacs': i18n._(t`TACACS+`),
|
||||||
'/settings/user_interface': i18n._(t`User interface`),
|
'/settings/tacacs/details': i18n._(t`Details`),
|
||||||
|
'/settings/tacacs/edit': i18n._(t`Edit Details`),
|
||||||
|
'/settings/ui': i18n._(t`User Interface`),
|
||||||
|
'/settings/ui/details': i18n._(t`Details`),
|
||||||
|
'/settings/ui/edit': i18n._(t`Edit Details`),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<ContentError error={error} />
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading || !result || !me) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<ContentLoading />
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!me?.is_superuser && !me?.is_system_auditor) {
|
||||||
|
return <Redirect to="/" />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SettingsProvider value={result}>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/settings/activity_stream">
|
<Route path="/settings/activity_stream">
|
||||||
@@ -76,7 +163,7 @@ function Settings({ i18n }) {
|
|||||||
<MiscSystem />
|
<MiscSystem />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/settings/radius">
|
<Route path="/settings/radius">
|
||||||
<Radius />
|
<RADIUS />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/settings/saml">
|
<Route path="/settings/saml">
|
||||||
<SAML />
|
<SAML />
|
||||||
@@ -84,7 +171,7 @@ function Settings({ i18n }) {
|
|||||||
<Route path="/settings/tacacs">
|
<Route path="/settings/tacacs">
|
||||||
<TACACS />
|
<TACACS />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/settings/user_interface">
|
<Route path="/settings/ui">
|
||||||
<UI />
|
<UI />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/settings" exact>
|
<Route path="/settings" exact>
|
||||||
@@ -100,7 +187,7 @@ function Settings({ i18n }) {
|
|||||||
</PageSection>
|
</PageSection>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</>
|
</SettingsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
import TACACSDetail from './TACACSDetail';
|
import TACACSDetail from './TACACSDetail';
|
||||||
import TACACSEdit from './TACACSEdit';
|
import TACACSEdit from './TACACSEdit';
|
||||||
|
|
||||||
function TACACS({ i18n }) {
|
function TACACS({ i18n }) {
|
||||||
const baseUrl = '/settings/tacacs';
|
const baseURL = '/settings/tacacs';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`TACACS+ settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/details`}>
|
||||||
<TACACSDetail />
|
<TACACSDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<TACACSEdit />
|
<TACACSEdit />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}/details`}>
|
||||||
|
{i18n._(t`View TACACS+ settings`)}
|
||||||
|
</Link>
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,16 +1,56 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsAPI } from '../../../api';
|
||||||
import TACACS from './TACACS';
|
import TACACS from './TACACS';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<TACACS />', () => {
|
describe('<TACACS />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<TACACS />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain('TACACS+ settings');
|
test('should render TACACS+ details', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/tacacs/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<TACACS />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('TACACSDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render TACACS+ edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/tacacs/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<TACACS />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('TACACSEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/tacacs/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<TACACS />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,88 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } 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 { Button } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
|
||||||
function TACACSDetail({ i18n }) {
|
function TACACSDetail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: options } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request, result: tacacs } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('tacacsplus');
|
||||||
|
return data;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/settings/tacacs/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
{isLoading && <ContentLoading />}
|
||||||
aria-label={i18n._(t`Edit`)}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
component={Link}
|
{!isLoading && tacacs && (
|
||||||
to="/settings/tacacs/edit"
|
<DetailList>
|
||||||
>
|
{Object.keys(tacacs).map(key => {
|
||||||
{i18n._(t`Edit`)}
|
const record = options?.[key];
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<SettingDetail
|
||||||
</CardBody>
|
key={key}
|
||||||
|
label={record?.label}
|
||||||
|
type={record?.type}
|
||||||
|
value={tacacs?.[key]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to="/settings/tacacs/edit"
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,94 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import { assertDetail } from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
import TACACSDetail from './TACACSDetail';
|
import TACACSDetail from './TACACSDetail';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
TACACSPLUS_HOST: 'mockhost',
|
||||||
|
TACACSPLUS_PORT: 49,
|
||||||
|
TACACSPLUS_SECRET: '$encrypted$',
|
||||||
|
TACACSPLUS_SESSION_TIMEOUT: 5,
|
||||||
|
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<TACACSDetail />', () => {
|
describe('<TACACSDetail />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<TACACSDetail />);
|
beforeAll(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<TACACSDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
|
afterAll(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('TACACSDetail').length).toBe(1);
|
expect(wrapper.find('TACACSDetail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', () => {
|
||||||
|
const expectedTabs = ['Back to Settings', 'Details'];
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(wrapper, 'TACACS+ Server', 'mockhost');
|
||||||
|
assertDetail(wrapper, 'TACACS+ Port', '49');
|
||||||
|
assertDetail(wrapper, 'TACACS+ Secret', 'Encrypted');
|
||||||
|
assertDetail(wrapper, 'TACACS+ Auth Session Timeout', '5');
|
||||||
|
assertDetail(wrapper, 'TACACS+ Authentication Protocol', 'ascii');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<TACACSDetail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<TACACSDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } 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 { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
import UIDetail from './UIDetail';
|
import UIDetail from './UIDetail';
|
||||||
import UIEdit from './UIEdit';
|
import UIEdit from './UIEdit';
|
||||||
|
|
||||||
function UI({ i18n }) {
|
function UI({ i18n }) {
|
||||||
const baseUrl = '/settings/ui';
|
const baseURL = '/settings/ui';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{i18n._(t`User interface settings`)}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from={baseUrl} to={`${baseUrl}/details`} exact />
|
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
|
||||||
<Route path={`${baseUrl}/details`}>
|
<Route path={`${baseURL}/details`}>
|
||||||
<UIDetail />
|
<UIDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${baseUrl}/edit`}>
|
<Route path={`${baseURL}/edit`}>
|
||||||
<UIEdit />
|
<UIEdit />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route key="not-found" path={`${baseURL}/*`}>
|
||||||
|
<ContentError isNotFound>
|
||||||
|
<Link to={`${baseURL}/details`}>
|
||||||
|
{i18n._(t`View User Interface settings`)}
|
||||||
|
</Link>
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,16 +1,61 @@
|
|||||||
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 UI from './UI';
|
import UI from './UI';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<UI />', () => {
|
describe('<UI />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<UI />);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(wrapper.find('Card').text()).toContain('User interface settings');
|
test('should render user interface details', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/ui/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<UI />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('UIDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render user interface edit', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/ui/edit'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<UI />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('UIEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user navigates to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/settings/ui/foo'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<UI />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,103 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } 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 { Button } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../../components/Card';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import RoutedTabs from '../../../../components/RoutedTabs';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import useRequest from '../../../../util/useRequest';
|
||||||
|
import { DetailList } from '../../../../components/DetailList';
|
||||||
|
import { useConfig } from '../../../../contexts/Config';
|
||||||
|
import { useSettings } from '../../../../contexts/Settings';
|
||||||
|
import { pluck } from '../../shared/settingUtils';
|
||||||
|
import SettingDetail from '../../shared';
|
||||||
|
|
||||||
function UIDetail({ i18n }) {
|
function UIDetail({ i18n }) {
|
||||||
|
const { me } = useConfig();
|
||||||
|
const { GET: options } = useSettings();
|
||||||
|
|
||||||
|
const { isLoading, error, request, result: ui } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SettingsAPI.readCategory('ui');
|
||||||
|
|
||||||
|
const uiData = pluck(
|
||||||
|
data,
|
||||||
|
'PENDO_TRACKING_STATE',
|
||||||
|
'CUSTOM_LOGO',
|
||||||
|
'CUSTOM_LOGIN_INFO'
|
||||||
|
);
|
||||||
|
|
||||||
|
return uiData;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request();
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Settings`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/settings`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/settings/ui/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Change CUSTOM_LOGO type from string to image
|
||||||
|
// to help SettingDetail render it as an <img>
|
||||||
|
if (options?.CUSTOM_LOGO) {
|
||||||
|
options.CUSTOM_LOGO.type = 'image';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
{i18n._(t`Detail coming soon :)`)}
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<CardActionsRow>
|
<CardBody>
|
||||||
<Button
|
{isLoading && <ContentLoading />}
|
||||||
aria-label={i18n._(t`Edit`)}
|
{!isLoading && error && <ContentError error={error} />}
|
||||||
component={Link}
|
{!isLoading && ui && (
|
||||||
to="/settings/ui/edit"
|
<DetailList>
|
||||||
>
|
{Object.keys(ui).map(key => {
|
||||||
{i18n._(t`Edit`)}
|
const record = options?.[key];
|
||||||
</Button>
|
return (
|
||||||
</CardActionsRow>
|
<SettingDetail
|
||||||
</CardBody>
|
key={key}
|
||||||
|
label={record?.label}
|
||||||
|
type={record?.type}
|
||||||
|
value={ui?.[key]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
{me?.is_superuser && (
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to="/settings/ui/edit"
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,93 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import { SettingsProvider } from '../../../../contexts/Settings';
|
||||||
|
import { SettingsAPI } from '../../../../api';
|
||||||
|
import { assertDetail } from '../../shared/settingTestUtils';
|
||||||
|
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||||
import UIDetail from './UIDetail';
|
import UIDetail from './UIDetail';
|
||||||
|
|
||||||
|
jest.mock('../../../../api/models/Settings');
|
||||||
|
SettingsAPI.readCategory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
CUSTOM_LOGIN_INFO: 'mock info',
|
||||||
|
CUSTOM_LOGO: 'data:image/png',
|
||||||
|
PENDO_TRACKING_STATE: 'off',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('<UIDetail />', () => {
|
describe('<UIDetail />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountWithContexts(<UIDetail />);
|
beforeAll(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<UIDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
|
||||||
|
afterAll(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(wrapper.find('UIDetail').length).toBe(1);
|
expect(wrapper.find('UIDetail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', () => {
|
||||||
|
const expectedTabs = ['Back to Settings', 'Details'];
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected details', () => {
|
||||||
|
assertDetail(wrapper, 'User Analytics Tracking State', 'off');
|
||||||
|
assertDetail(wrapper, 'Custom Login Info', 'mock info');
|
||||||
|
expect(wrapper.find('Detail[label="Custom Logo"] dt').text()).toBe(
|
||||||
|
'Custom Logo'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('Detail[label="Custom Logo"] dd img').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button from non-superusers', async () => {
|
||||||
|
const config = {
|
||||||
|
me: {
|
||||||
|
is_superuser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<UIDetail />
|
||||||
|
</SettingsProvider>,
|
||||||
|
{
|
||||||
|
context: { config },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display content error when api throws error on initial render', async () => {
|
||||||
|
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockAllOptions.actions}>
|
||||||
|
<UIDetail />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
86
awx/ui_next/src/screens/Setting/shared/SettingDetail.jsx
Normal file
86
awx/ui_next/src/screens/Setting/shared/SettingDetail.jsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Detail } from '../../../components/DetailList';
|
||||||
|
import { VariablesDetail } from '../../../components/CodeMirrorInput';
|
||||||
|
|
||||||
|
export default withI18n()(({ i18n, label, type, value }) => {
|
||||||
|
const dataType = value === '$encrypted$' ? 'encrypted' : type;
|
||||||
|
let detail = null;
|
||||||
|
|
||||||
|
switch (dataType) {
|
||||||
|
case 'nested object':
|
||||||
|
detail = (
|
||||||
|
<VariablesDetail
|
||||||
|
label={label}
|
||||||
|
rows={4}
|
||||||
|
value={JSON.stringify(value || {}, undefined, 2)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'list':
|
||||||
|
detail = <VariablesDetail rows={4} label={label} value={value} />;
|
||||||
|
break;
|
||||||
|
case 'image':
|
||||||
|
detail = (
|
||||||
|
<Detail
|
||||||
|
alwaysVisible
|
||||||
|
label={label}
|
||||||
|
value={<img src={value} alt={label} height="40" width="40" />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'encrypted':
|
||||||
|
detail = (
|
||||||
|
<Detail
|
||||||
|
alwaysVisible
|
||||||
|
isEncrypted
|
||||||
|
label={label}
|
||||||
|
value={i18n._(t`Encrypted`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
detail = (
|
||||||
|
<Detail
|
||||||
|
alwaysVisible
|
||||||
|
label={label}
|
||||||
|
value={value ? i18n._(t`On`) : i18n._(t`Off`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'choice':
|
||||||
|
detail = (
|
||||||
|
<Detail
|
||||||
|
alwaysVisible
|
||||||
|
label={label}
|
||||||
|
value={!value ? i18n._(t`Unconfigured`) : value}
|
||||||
|
isUnconfigured={!value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'integer':
|
||||||
|
detail = (
|
||||||
|
<Detail
|
||||||
|
alwaysVisible
|
||||||
|
label={label}
|
||||||
|
value={!value ? i18n._(t`Unconfigured`) : value}
|
||||||
|
isUnconfigured={!value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
detail = (
|
||||||
|
<Detail
|
||||||
|
alwaysVisible
|
||||||
|
label={label}
|
||||||
|
value={!value ? i18n._(t`Unconfigured`) : value}
|
||||||
|
isUnconfigured={!value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
detail = null;
|
||||||
|
}
|
||||||
|
return detail;
|
||||||
|
});
|
||||||
6562
awx/ui_next/src/screens/Setting/shared/data.allSettingOptions.json
Normal file
6562
awx/ui_next/src/screens/Setting/shared/data.allSettingOptions.json
Normal file
File diff suppressed because it is too large
Load Diff
42
awx/ui_next/src/screens/Setting/shared/data.jobSettings.json
Normal file
42
awx/ui_next/src/screens/Setting/shared/data.jobSettings.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"AD_HOC_COMMANDS": [
|
||||||
|
"command"
|
||||||
|
],
|
||||||
|
"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": {},
|
||||||
|
"PROJECT_UPDATE_VVV": false,
|
||||||
|
"AWX_ROLES_ENABLED": true,
|
||||||
|
"AWX_COLLECTIONS_ENABLED": true,
|
||||||
|
"AWX_SHOW_PLAYBOOK_LINKS": false,
|
||||||
|
"PRIMARY_GALAXY_URL": "https://galaxy.server.com",
|
||||||
|
"PRIMARY_GALAXY_USERNAME": "",
|
||||||
|
"PRIMARY_GALAXY_PASSWORD": "",
|
||||||
|
"PRIMARY_GALAXY_TOKEN": "$encrypted$",
|
||||||
|
"PRIMARY_GALAXY_AUTH_URL": "https://galaxy.auth.com",
|
||||||
|
"PUBLIC_GALAXY_ENABLED": true,
|
||||||
|
"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
|
||||||
|
}
|
||||||
134
awx/ui_next/src/screens/Setting/shared/data.ldapSettings.json
Normal file
134
awx/ui_next/src/screens/Setting/shared/data.ldapSettings.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"AUTH_LDAP_SERVER_URI": "ldap://ldap.example.com",
|
||||||
|
"AUTH_LDAP_BIND_DN": "cn=eng_user",
|
||||||
|
"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": [],
|
||||||
|
"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": {},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
21
awx/ui_next/src/screens/Setting/shared/data.logSettings.json
Normal file
21
awx/ui_next/src/screens/Setting/shared/data.logSettings.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"LOG_AGGREGATOR_HOST": "https://mocklog",
|
||||||
|
"LOG_AGGREGATOR_PORT": 1234,
|
||||||
|
"LOG_AGGREGATOR_TYPE": "logstash",
|
||||||
|
"LOG_AGGREGATOR_USERNAME": "logging_name",
|
||||||
|
"LOG_AGGREGATOR_PASSWORD": "$encrypted$",
|
||||||
|
"LOG_AGGREGATOR_LOGGERS": [
|
||||||
|
"activity_stream",
|
||||||
|
"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
|
||||||
|
}
|
||||||
1
awx/ui_next/src/screens/Setting/shared/index.js
Normal file
1
awx/ui_next/src/screens/Setting/shared/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './SettingDetail';
|
||||||
15
awx/ui_next/src/screens/Setting/shared/settingTestUtils.js
Normal file
15
awx/ui_next/src/screens/Setting/shared/settingTestUtils.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export function assertDetail(wrapper, label, value) {
|
||||||
|
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
|
||||||
|
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertVariableDetail(wrapper, label, value) {
|
||||||
|
expect(
|
||||||
|
wrapper.find(`VariablesDetail[label="${label}"] .pf-c-form__label`).text()
|
||||||
|
).toBe(label);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find(`VariablesDetail[label="${label}"] CodeMirrorInput`)
|
||||||
|
.prop('value')
|
||||||
|
).toBe(value);
|
||||||
|
}
|
||||||
14
awx/ui_next/src/screens/Setting/shared/settingUtils.js
Normal file
14
awx/ui_next/src/screens/Setting/shared/settingUtils.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export function sortNestedDetails(obj = {}) {
|
||||||
|
const nestedTypes = ['nested object', 'list'];
|
||||||
|
const notNested = Object.entries(obj).filter(
|
||||||
|
([, value]) => !nestedTypes.includes(value.type)
|
||||||
|
);
|
||||||
|
const nested = Object.entries(obj).filter(([, value]) =>
|
||||||
|
nestedTypes.includes(value.type)
|
||||||
|
);
|
||||||
|
return [...notNested, ...nested];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pluck(sourceObject, ...keys) {
|
||||||
|
return Object.assign({}, ...keys.map(key => ({ [key]: sourceObject[key] })));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user