Add a new key "unit" to api setting fields

* Add detail popover
* Fix broken redirects
* Add additional id and data-cy attributes to Detail components
* Remove galaxy fields from job settings
This commit is contained in:
Marliana Lara 2020-09-25 14:00:27 -04:00
parent a69a40a429
commit 558dfb685e
No known key found for this signature in database
GPG Key ID: 38C73B40DFA809EE
38 changed files with 403 additions and 290 deletions

View File

@ -16,6 +16,7 @@ register(
help_text=_('Number of seconds that a user is inactive before they will need to login again.'),
category=_('Authentication'),
category_slug='authentication',
unit=_('seconds'),
)
register(
'SESSIONS_PER_USER',
@ -49,6 +50,7 @@ register(
'in the number of seconds.'),
category=_('Authentication'),
category_slug='authentication',
unit=_('seconds'),
)
register(
'ALLOW_OAUTH2_FOR_EXTERNAL_USERS',

View File

@ -39,7 +39,7 @@ class Metadata(metadata.SimpleMetadata):
'min_length', 'max_length',
'min_value', 'max_value',
'category', 'category_slug',
'defined_in_file'
'defined_in_file', 'unit',
]
for attr in text_attrs:

View File

@ -129,12 +129,14 @@ class SettingsRegistry(object):
placeholder = field_kwargs.pop('placeholder', empty)
encrypted = bool(field_kwargs.pop('encrypted', False))
defined_in_file = bool(field_kwargs.pop('defined_in_file', False))
unit = field_kwargs.pop('unit', None)
if getattr(field_kwargs.get('child', None), 'source', None) is not None:
field_kwargs['child'].source = None
field_instance = field_class(**field_kwargs)
field_instance.category_slug = category_slug
field_instance.category = category
field_instance.depends_on = depends_on
field_instance.unit = unit
if placeholder is not empty:
field_instance.placeholder = placeholder
field_instance.defined_in_file = defined_in_file

View File

@ -148,7 +148,7 @@ register(
default='https://example.com',
schemes=('http', 'https'),
allow_plain_hostname=True, # Allow hostname only without TLD.
label=_('Automation Analytics upload URL.'),
label=_('Automation Analytics upload URL'),
help_text=_('This setting is used to to configure data collection for the Automation Analytics dashboard'),
category=_('System'),
category_slug='system',
@ -253,6 +253,7 @@ register(
help_text=_('The number of seconds to sleep between status checks for jobs running on isolated instances.'),
category=_('Jobs'),
category_slug='jobs',
unit=_('seconds'),
)
register(
@ -264,6 +265,7 @@ register(
'This includes the time needed to copy source control files (playbooks) to the isolated instance.'),
category=_('Jobs'),
category_slug='jobs',
unit=_('seconds'),
)
register(
@ -276,6 +278,7 @@ register(
'Value should be substantially greater than expected network latency.'),
category=_('Jobs'),
category_slug='jobs',
unit=_('seconds'),
)
register(
@ -497,6 +500,7 @@ register(
'timeout should be imposed. A timeout set on an individual job template will override this.'),
category=_('Jobs'),
category_slug='jobs',
unit=_('seconds'),
)
register(
@ -509,6 +513,7 @@ register(
'timeout should be imposed. A timeout set on an individual inventory source will override this.'),
category=_('Jobs'),
category_slug='jobs',
unit=_('seconds'),
)
register(
@ -521,6 +526,7 @@ register(
'timeout should be imposed. A timeout set on an individual project will override this.'),
category=_('Jobs'),
category_slug='jobs',
unit=_('seconds'),
)
register(
@ -535,6 +541,7 @@ register(
'Use a value of 0 to indicate that no timeout should be imposed.'),
category=_('Jobs'),
category_slug='jobs',
unit=_('seconds'),
)
register(
@ -542,7 +549,7 @@ register(
field_class=fields.IntegerField,
allow_null=False,
default=200,
label=_('Maximum number of forks per job.'),
label=_('Maximum number of forks per job'),
help_text=_('Saving a Job Template with more than this number of forks will result in an error. '
'When set to 0, no limit is applied.'),
category=_('Jobs'),
@ -672,6 +679,7 @@ register(
'aggregator protocols.'),
category=_('Logging'),
category_slug='logging',
unit=_('seconds'),
)
register(
'LOG_AGGREGATOR_VERIFY_CERT',
@ -752,7 +760,8 @@ register(
default=14400, # every 4 hours
min_value=1800, # every 30 minutes
category=_('System'),
category_slug='system'
category_slug='system',
unit=_('seconds'),
)

View File

@ -515,6 +515,7 @@ register(
help_text=_('TACACS+ session timeout value in seconds, 0 disables timeout.'),
category=_('TACACS+'),
category_slug='tacacsplus',
unit=_('seconds'),
)
register(

View File

@ -4,6 +4,7 @@ import { node, number, oneOfType, shape, string, arrayOf } from 'prop-types';
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
import { DetailName, DetailValue } from '../DetailList';
import MultiButtonToggle from '../MultiButtonToggle';
import DetailPopover from '../DetailPopover';
import {
yamlToJson,
jsonToYaml,
@ -27,7 +28,7 @@ function getValueAsMode(value, mode) {
return mode === YAML_MODE ? jsonToYaml(value) : yamlToJson(value);
}
function VariablesDetail({ value, label, rows, fullHeight }) {
function VariablesDetail({ dataCy, helpText, value, label, rows, fullHeight }) {
const [mode, setMode] = useState(
isJsonObject(value) || isJsonString(value) ? JSON_MODE : YAML_MODE
);
@ -46,9 +47,14 @@ function VariablesDetail({ value, label, rows, fullHeight }) {
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [value]);
const labelCy = dataCy ? `${dataCy}-label` : null;
const valueCy = dataCy ? `${dataCy}-value` : null;
return (
<>
<DetailName
data-cy={labelCy}
id={dataCy}
component={TextListItemVariants.dt}
fullWidth
css="grid-column: 1 / -1"
@ -62,6 +68,9 @@ function VariablesDetail({ value, label, rows, fullHeight }) {
>
{label}
</span>
{helpText && (
<DetailPopover header={label} content={helpText} id={dataCy} />
)}
</div>
</SplitItem>
<SplitItem>
@ -84,6 +93,7 @@ function VariablesDetail({ value, label, rows, fullHeight }) {
</Split>
</DetailName>
<DetailValue
data-cy={valueCy}
component={TextListItemVariants.dd}
fullWidth
css="grid-column: 1 / -1; margin-top: -20px"

View File

@ -1,7 +1,8 @@
import React from 'react';
import { node, bool } from 'prop-types';
import { node, bool, string } from 'prop-types';
import { TextListItem, TextListItemVariants } from '@patternfly/react-core';
import styled from 'styled-components';
import DetailPopover from '../DetailPopover';
const DetailName = styled(({ fullWidth, ...props }) => (
<TextListItem {...props} />
@ -15,7 +16,7 @@ const DetailName = styled(({ fullWidth, ...props }) => (
`;
const DetailValue = styled(
({ fullWidth, isEncrypted, isUnconfigured, ...props }) => (
({ fullWidth, isEncrypted, isNotConfigured, ...props }) => (
<TextListItem {...props} />
)
)`
@ -26,7 +27,7 @@ const DetailValue = styled(
grid-column: 2 / -1;
`}
${props =>
(props.isEncrypted || props.isUnconfigured) &&
(props.isEncrypted || props.isNotConfigured) &&
`
color: var(--pf-global--Color--400);
`}
@ -39,8 +40,9 @@ const Detail = ({
className,
dataCy,
alwaysVisible,
helpText,
isEncrypted,
isUnconfigured,
isNotConfigured,
}) => {
if (!value && typeof value !== 'number' && !alwaysVisible) {
return null;
@ -56,8 +58,12 @@ const Detail = ({
component={TextListItemVariants.dt}
fullWidth={fullWidth}
data-cy={labelCy}
id={dataCy}
>
{label}
{helpText && (
<DetailPopover header={label} content={helpText} id={dataCy} />
)}
</DetailName>
<DetailValue
className={className}
@ -65,7 +71,7 @@ const Detail = ({
fullWidth={fullWidth}
data-cy={valueCy}
isEncrypted={isEncrypted}
isUnconfigured={isUnconfigured}
isNotConfigured={isNotConfigured}
>
{value}
</DetailValue>
@ -77,11 +83,13 @@ Detail.propTypes = {
value: node,
fullWidth: bool,
alwaysVisible: bool,
helpText: string,
};
Detail.defaultProps = {
value: null,
fullWidth: false,
alwaysVisible: false,
helpText: null,
};
export default Detail;

View File

@ -0,0 +1,51 @@
import React, { useState } from 'react';
import { node, string } from 'prop-types';
import { Button as _Button, Popover } from '@patternfly/react-core';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
const Button = styled(_Button)`
--pf-c-button--PaddingTop: 0;
--pf-c-button--PaddingBottom: 0;
`;
function DetailPopover({ header, content, id }) {
const [showPopover, setShowPopover] = useState(false);
if (!content) {
return null;
}
return (
<Popover
bodyContent={content}
headerContent={header}
hideOnOutsideClick
id={id}
isVisible={showPopover}
shouldClose={() => setShowPopover(false)}
>
<Button
onClick={() => setShowPopover(!showPopover)}
variant="plain"
aria-haspopup="true"
aria-expanded={showPopover}
>
<OutlinedQuestionCircleIcon
onClick={() => setShowPopover(!showPopover)}
/>
</Button>
</Popover>
);
}
DetailPopover.propTypes = {
content: node,
header: node,
id: string,
};
DetailPopover.defaultProps = {
content: null,
header: null,
id: 'detail-popover',
};
export default DetailPopover;

View File

@ -0,0 +1 @@
export { default } from './DetailPopover';

View File

@ -74,6 +74,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
<Detail
alwaysVisible={false}
fullWidth={false}
helpText={null}
label="Name"
value="jane brown"
/>
@ -86,6 +87,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
<Detail
alwaysVisible={false}
fullWidth={false}
helpText={null}
label="Team Roles"
value={
<WithI18n
@ -138,6 +140,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
<Detail
alwaysVisible={false}
fullWidth={false}
helpText={null}
label="Name"
value="jane brown"
/>
@ -150,6 +153,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
<Detail
alwaysVisible={false}
fullWidth={false}
helpText={null}
label="Team Roles"
value={
<WithI18n
@ -225,6 +229,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
<Detail
alwaysVisible={false}
fullWidth={false}
helpText={null}
label="Name"
value="jane brown"
/>
@ -237,6 +242,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
<Detail
alwaysVisible={false}
fullWidth={false}
helpText={null}
label="Team Roles"
value={
<WithI18n
@ -447,6 +453,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
<Detail
alwaysVisible={false}
fullWidth={false}
helpText={null}
label="Name"
value="jane brown"
>
@ -463,9 +470,9 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
"$$typeof": Symbol(react.forward_ref),
"attrs": Array [],
"componentStyle": ComponentStyle {
"componentId": "sc-htpNat",
"componentId": "sc-bxivhb",
"isStatic": false,
"lastClassName": "iYJcPm",
"lastClassName": "gQwVdc",
"rules": Array [
"
font-weight: var(--pf-global--FontWeight--bold);
@ -478,7 +485,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
"displayName": "Styled(Component)",
"foldedComponentIds": Array [],
"render": [Function],
"styledComponentId": "sc-htpNat",
"styledComponentId": "sc-bxivhb",
"target": [Function],
"toString": [Function],
"warnTooManyClasses": [Function],
@ -489,18 +496,18 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
fullWidth={false}
>
<Component
className="sc-htpNat iYJcPm"
className="sc-bxivhb gQwVdc"
component="dt"
data-cy={null}
fullWidth={false}
>
<TextListItem
className="sc-htpNat iYJcPm"
className="sc-bxivhb gQwVdc"
component="dt"
data-cy={null}
>
<dt
className="sc-htpNat iYJcPm"
className="sc-bxivhb gQwVdc"
data-cy={null}
data-pf-content={true}
>
@ -523,9 +530,9 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
"$$typeof": Symbol(react.forward_ref),
"attrs": Array [],
"componentStyle": ComponentStyle {
"componentId": "sc-bxivhb",
"componentId": "sc-ifAKCX",
"isStatic": false,
"lastClassName": "gxmPlV",
"lastClassName": "boHWLt",
"rules": Array [
"
word-break: break-all;
@ -541,7 +548,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
"displayName": "Styled(Component)",
"foldedComponentIds": Array [],
"render": [Function],
"styledComponentId": "sc-bxivhb",
"styledComponentId": "sc-ifAKCX",
"target": [Function],
"toString": [Function],
"warnTooManyClasses": [Function],
@ -552,18 +559,18 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
fullWidth={false}
>
<Component
className="sc-bxivhb gxmPlV"
className="sc-ifAKCX boHWLt"
component="dd"
data-cy={null}
fullWidth={false}
>
<TextListItem
className="sc-bxivhb gxmPlV"
className="sc-ifAKCX boHWLt"
component="dd"
data-cy={null}
>
<dd
className="sc-bxivhb gxmPlV"
className="sc-ifAKCX boHWLt"
data-cy={null}
data-pf-content={true}
>
@ -670,6 +677,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
<Detail
alwaysVisible={false}
fullWidth={false}
helpText={null}
label="Team Roles"
value={
<WithI18n
@ -703,9 +711,9 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
"$$typeof": Symbol(react.forward_ref),
"attrs": Array [],
"componentStyle": ComponentStyle {
"componentId": "sc-htpNat",
"componentId": "sc-bxivhb",
"isStatic": false,
"lastClassName": "iYJcPm",
"lastClassName": "gQwVdc",
"rules": Array [
"
font-weight: var(--pf-global--FontWeight--bold);
@ -718,7 +726,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
"displayName": "Styled(Component)",
"foldedComponentIds": Array [],
"render": [Function],
"styledComponentId": "sc-htpNat",
"styledComponentId": "sc-bxivhb",
"target": [Function],
"toString": [Function],
"warnTooManyClasses": [Function],
@ -729,18 +737,18 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
fullWidth={false}
>
<Component
className="sc-htpNat iYJcPm"
className="sc-bxivhb gQwVdc"
component="dt"
data-cy={null}
fullWidth={false}
>
<TextListItem
className="sc-htpNat iYJcPm"
className="sc-bxivhb gQwVdc"
component="dt"
data-cy={null}
>
<dt
className="sc-htpNat iYJcPm"
className="sc-bxivhb gQwVdc"
data-cy={null}
data-pf-content={true}
>
@ -763,9 +771,9 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
"$$typeof": Symbol(react.forward_ref),
"attrs": Array [],
"componentStyle": ComponentStyle {
"componentId": "sc-bxivhb",
"componentId": "sc-ifAKCX",
"isStatic": false,
"lastClassName": "gxmPlV",
"lastClassName": "boHWLt",
"rules": Array [
"
word-break: break-all;
@ -781,7 +789,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
"displayName": "Styled(Component)",
"foldedComponentIds": Array [],
"render": [Function],
"styledComponentId": "sc-bxivhb",
"styledComponentId": "sc-ifAKCX",
"target": [Function],
"toString": [Function],
"warnTooManyClasses": [Function],
@ -792,18 +800,18 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
fullWidth={false}
>
<Component
className="sc-bxivhb gxmPlV"
className="sc-ifAKCX boHWLt"
component="dd"
data-cy={null}
fullWidth={false}
>
<TextListItem
className="sc-bxivhb gxmPlV"
className="sc-ifAKCX boHWLt"
component="dd"
data-cy={null}
>
<dd
className="sc-bxivhb gxmPlV"
className="sc-ifAKCX boHWLt"
data-cy={null}
data-pf-content={true}
>

View File

@ -70,8 +70,11 @@ function ActivityStreamDetail({ i18n }) {
return (
<SettingDetail
key={key}
id={key}
helpText={record?.help_text}
label={record?.label}
type={record?.type}
unit={record?.unit}
value={activityStream?.[key]}
/>
);

View File

@ -62,8 +62,11 @@ function AzureADDetail({ i18n }) {
return (
<SettingDetail
key={key}
id={key}
helpText={record?.help_text}
label={record?.label}
type={record?.type}
unit={record?.unit}
value={azure?.[key]}
/>
);

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Link, Redirect, Route, Switch } from 'react-router-dom';
import { Link, Redirect, Route, Switch, useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { PageSection, Card } from '@patternfly/react-core';
@ -9,11 +9,23 @@ import GitHubEdit from './GitHubEdit';
function GitHub({ i18n }) {
const baseURL = '/settings/github';
const baseRoute = useRouteMatch({ path: '/settings/github', exact: true });
const categoryRoute = useRouteMatch({
path: '/settings/github/:category',
exact: true,
});
return (
<PageSection>
<Card>
<Switch>
<Redirect from={baseURL} to={`${baseURL}/default/details`} exact />
{baseRoute && <Redirect to={`${baseURL}/default/details`} exact />}
{categoryRoute && (
<Redirect
to={`${baseURL}/${categoryRoute.params.category}/details`}
exact
/>
)}
<Route path={`${baseURL}/:category/details`}>
<GitHubDetail />
</Route>

View File

@ -49,7 +49,7 @@ describe('<GitHub />', () => {
test('should show content error when user navigates to erroneous route', async () => {
const history = createMemoryHistory({
initialEntries: ['/settings/github/foo'],
initialEntries: ['/settings/github/foo/bar'],
});
await act(async () => {
wrapper = mountWithContexts(<GitHub />, {

View File

@ -98,8 +98,11 @@ function GitHubDetail({ i18n }) {
return (
<SettingDetail
key={key}
id={key}
helpText={record?.help_text}
label={record?.label}
type={record?.type}
unit={record?.unit}
value={gitHubDetails[category][key]}
/>
);

View File

@ -175,9 +175,9 @@ describe('<GitHubDetail />', () => {
'GitHub Organization OAuth2 Callback URL',
'https://towerhost/sso/complete/github-org/'
);
assertDetail(wrapper, 'GitHub Organization OAuth2 Key', 'Unconfigured');
assertDetail(wrapper, 'GitHub Organization OAuth2 Key', 'Not configured');
assertDetail(wrapper, 'GitHub Organization OAuth2 Secret', 'Encrypted');
assertDetail(wrapper, 'GitHub Organization Name', 'Unconfigured');
assertDetail(wrapper, 'GitHub Organization Name', 'Not configured');
assertVariableDetail(
wrapper,
'GitHub Organization OAuth2 Organization Map',

View File

@ -62,8 +62,11 @@ function GoogleOAuth2Detail({ i18n }) {
return (
<SettingDetail
key={key}
id={key}
helpText={record?.help_text}
label={record?.label}
type={record?.type}
unit={record?.unit}
value={googleOAuth2?.[key]}
/>
);

View File

@ -72,7 +72,7 @@ describe('<GoogleOAuth2Detail />', () => {
assertDetail(wrapper, 'Google OAuth2 Secret', 'Encrypted');
assertVariableDetail(
wrapper,
'Google OAuth2 Whitelisted Domains',
'Google OAuth2 Allowed Domains',
'[\n "example.com",\n "example_2.com"\n]'
);
assertVariableDetail(wrapper, 'Google OAuth2 Extra Arguments', '{}');

View File

@ -29,7 +29,6 @@ function JobsDetail({ i18n }) {
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
@ -76,12 +75,15 @@ function JobsDetail({ i18n }) {
{!isLoading && error && <ContentError error={error} />}
{!isLoading && jobs && (
<DetailList>
{Array.from(jobs).map(([, detail]) => {
{jobs.map(([key, detail]) => {
return (
<SettingDetail
key={detail?.label}
key={key}
id={key}
helpText={detail?.help_text}
label={detail?.label}
type={detail?.type}
unit={detail?.unit}
value={detail?.value}
/>
);

View File

@ -52,9 +52,9 @@ describe('<JobsDetail />', () => {
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 status check interval', '1 seconds');
assertDetail(wrapper, 'Isolated launch timeout', '600 seconds');
assertDetail(wrapper, 'Isolated connection timeout', '10 seconds');
assertDetail(wrapper, 'Isolated host key checking', 'Off');
assertDetail(
wrapper,
@ -67,28 +67,15 @@ describe('<JobsDetail />', () => {
assertDetail(wrapper, 'Follow symlinks', 'Off');
assertDetail(
wrapper,
'Primary Galaxy Server URL',
'https://galaxy.server.com'
'Ignore Ansible Galaxy SSL Certificate Verification',
'Off'
);
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');
assertDetail(wrapper, 'Default Job Timeout', '0 seconds');
assertDetail(wrapper, 'Default Inventory Update Timeout', '0 seconds');
assertDetail(wrapper, 'Default Project Update Timeout', '0 seconds');
assertDetail(wrapper, 'Per-Host Ansible Fact Cache Timeout', '0 seconds');
assertDetail(wrapper, 'Maximum number of forks per job', '200');
assertVariableDetail(
wrapper,
'Ansible Modules Allowed for Ad Hoc Jobs',

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Link, Redirect, Route, Switch } from 'react-router-dom';
import { Link, Redirect, Route, Switch, useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { PageSection, Card } from '@patternfly/react-core';
@ -9,11 +9,23 @@ import LDAPEdit from './LDAPEdit';
function LDAP({ i18n }) {
const baseURL = '/settings/ldap';
const baseRoute = useRouteMatch({ path: '/settings/ldap', exact: true });
const categoryRoute = useRouteMatch({
path: '/settings/ldap/:category',
exact: true,
});
return (
<PageSection>
<Card>
<Switch>
<Redirect from={baseURL} to={`${baseURL}/default/details`} exact />
{baseRoute && <Redirect to={`${baseURL}/default/details`} exact />}
{categoryRoute && (
<Redirect
to={`${baseURL}/${categoryRoute.params.category}/details`}
exact
/>
)}
<Route path={`${baseURL}/:category/details`}>
<LDAPDetail />
</Route>

View File

@ -49,7 +49,7 @@ describe('<LDAP />', () => {
test('should show content error when user navigates to erroneous route', async () => {
const history = createMemoryHistory({
initialEntries: ['/settings/ldap/foo'],
initialEntries: ['/settings/ldap/foo/bar'],
});
await act(async () => {
wrapper = mountWithContexts(<LDAP />, {

View File

@ -138,12 +138,15 @@ function LDAPDetail({ i18n }) {
{!isLoading && error && <ContentError error={error} />}
{!isLoading && !Object.values(LDAPDetails)?.includes(null) && (
<DetailList>
{Array.from(LDAPDetails[category]).map(([, detail]) => {
{LDAPDetails[category].map(([key, detail]) => {
return (
<SettingDetail
key={detail?.label}
key={key}
id={key}
helpText={detail?.help_text}
label={detail?.label}
type={detail?.type}
unit={detail?.unit}
value={detail?.value}
/>
);

View File

@ -82,7 +82,7 @@ describe('<LDAPDetail />', () => {
'LDAP Require Group',
'CN=Tower Users,OU=Users,DC=example,DC=com'
);
assertDetail(wrapper, 'LDAP Deny Group', 'Unconfigured');
assertDetail(wrapper, 'LDAP Deny Group', 'Not configured');
assertVariableDetail(wrapper, 'LDAP User Search', '[]');
assertVariableDetail(wrapper, 'LDAP User Attribute Map', '{}');
assertVariableDetail(wrapper, 'LDAP Group Search', '[]');

View File

@ -84,8 +84,11 @@ function LoggingDetail({ i18n }) {
{logging.map(([key, detail]) => (
<SettingDetail
key={key}
id={key}
helpText={detail?.help_text}
label={detail?.label}
type={detail?.type}
unit={detail?.unit}
value={detail?.value}
/>
))}

View File

@ -58,7 +58,7 @@ describe('<LoggingDetail />', () => {
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, 'TCP Connection Timeout', '5 seconds');
assertDetail(wrapper, 'Logging Aggregator Level Threshold', 'INFO');
assertDetail(
wrapper,

View File

@ -125,8 +125,11 @@ function MiscSystemDetail({ i18n }) {
{system.map(([key, detail]) => (
<SettingDetail
key={key}
id={key}
helpText={detail?.help_text}
label={detail?.label}
type={detail?.type}
unit={detail?.unit}
value={detail?.value}
/>
))}

View File

@ -69,24 +69,28 @@ describe('<MiscSystemDetail />', () => {
});
test('should render expected details', () => {
assertDetail(wrapper, 'Access Token Expiration', '1');
assertDetail(wrapper, 'Access Token Expiration', '1 seconds');
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, 'Authorization Code Expiration', '2 seconds');
assertDetail(
wrapper,
'Automation Analytics upload URL.',
'Automation Analytics Gather Interval',
'14400 seconds'
);
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, 'Idle Time Force Log Out', '30000000000 seconds');
assertDetail(
wrapper,
'Login redirect override URL',
@ -104,7 +108,7 @@ describe('<MiscSystemDetail />', () => {
);
assertDetail(wrapper, 'Red Hat customer password', 'Encrypted');
assertDetail(wrapper, 'Red Hat customer username', 'mock name');
assertDetail(wrapper, 'Refresh Token Expiration', '3');
assertDetail(wrapper, 'Refresh Token Expiration', '3 seconds');
assertVariableDetail(wrapper, 'Remote Host Headers', '[]');
assertVariableDetail(wrapper, 'Custom virtual environment paths', '[]');
});

View File

@ -62,8 +62,11 @@ function RADIUSDetail({ i18n }) {
return (
<SettingDetail
key={key}
id={key}
helpText={record?.help_text}
label={record?.label}
type={record?.type}
unit={record?.unit}
value={radius?.[key]}
/>
);

View File

@ -62,8 +62,11 @@ function SAMLDetail({ i18n }) {
return (
<SettingDetail
key={key}
id={key}
helpText={record?.help_text}
label={record?.label}
type={record?.type}
unit={record?.unit}
value={saml?.[key]}
/>
);

View File

@ -75,7 +75,11 @@ describe('<SAMLDetail />', () => {
'SAML Service Provider Public Certificate',
'mock_cert'
);
assertDetail(wrapper, 'SAML Service Provider Private Key', 'Unconfigured');
assertDetail(
wrapper,
'SAML Service Provider Private Key',
'Not configured'
);
assertVariableDetail(
wrapper,
'SAML Service Provider Organization Info',

View File

@ -62,8 +62,11 @@ function TACACSDetail({ i18n }) {
return (
<SettingDetail
key={key}
id={key}
helpText={record?.help_text}
label={record?.label}
type={record?.type}
unit={record?.unit}
value={tacacs?.[key]}
/>
);

View File

@ -55,7 +55,7 @@ describe('<TACACSDetail />', () => {
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+ Auth Session Timeout', '5 seconds');
assertDetail(wrapper, 'TACACS+ Authentication Protocol', 'ascii');
});

View File

@ -77,8 +77,11 @@ function UIDetail({ i18n }) {
return (
<SettingDetail
key={key}
id={key}
helpText={record?.help_text}
label={record?.label}
type={record?.type}
unit={record?.unit}
value={ui?.[key]}
/>
);

View File

@ -3,84 +3,115 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Detail } from '../../../components/DetailList';
import { VariablesDetail } from '../../../components/CodeMirrorInput';
// import DetailPopover from '../../../components/DetailList/DetailPopover';
export default withI18n()(({ i18n, label, type, value }) => {
const dataType = value === '$encrypted$' ? 'encrypted' : type;
let detail = null;
export default withI18n()(
({ i18n, helpText, id, label, type, unit = '', 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;
switch (dataType) {
case 'nested object':
detail = (
<VariablesDetail
dataCy={id}
label={label}
helpText={helpText}
rows={4}
value={JSON.stringify(value || {}, undefined, 2)}
/>
);
break;
case 'list':
detail = (
<VariablesDetail
dataCy={id}
helpText={helpText}
rows={4}
label={label}
value={value}
/>
);
break;
case 'image':
detail = (
<Detail
alwaysVisible
dataCy={id}
helpText={helpText}
isNotConfigured={!value}
label={label}
value={
!value ? (
i18n._(t`Not configured`)
) : (
<img src={value} alt={label} height="40" width="40" />
)
}
/>
);
break;
case 'encrypted':
detail = (
<Detail
alwaysVisible
dataCy={id}
helpText={helpText}
isEncrypted
label={label}
value={i18n._(t`Encrypted`)}
/>
);
break;
case 'boolean':
detail = (
<Detail
alwaysVisible
dataCy={id}
helpText={helpText}
label={label}
value={value ? i18n._(t`On`) : i18n._(t`Off`)}
/>
);
break;
case 'choice':
detail = (
<Detail
alwaysVisible
dataCy={id}
helpText={helpText}
isNotConfigured={!value}
label={label}
value={!value ? i18n._(t`Not configured`) : value}
/>
);
break;
case 'integer':
detail = (
<Detail
alwaysVisible
dataCy={id}
helpText={helpText}
label={label}
value={unit ? `${value} ${unit}` : `${value}`}
/>
);
break;
case 'string':
detail = (
<Detail
alwaysVisible
dataCy={id}
helpText={helpText}
isNotConfigured={!value}
label={label}
value={!value ? i18n._(t`Not configured`) : value}
/>
);
break;
default:
detail = null;
}
return detail;
}
return detail;
});
);

View File

@ -1,4 +1,3 @@
{
"name": "Setting Detail",
"actions": {
@ -94,7 +93,7 @@
},
"AUTOMATION_ANALYTICS_URL": {
"type": "string",
"label": "Automation Analytics upload URL.",
"label": "Automation Analytics upload URL",
"help_text": "This setting is used to to configure data collection for the Automation Analytics dashboard",
"category": "System",
"category_slug": "system",
@ -196,7 +195,8 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
"defined_in_file": false,
"unit": "seconds"
},
"AWX_ISOLATED_LAUNCH_TIMEOUT": {
"type": "integer",
@ -205,7 +205,8 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
"defined_in_file": false,
"unit": "seconds"
},
"AWX_ISOLATED_CONNECTION_TIMEOUT": {
"type": "integer",
@ -214,7 +215,8 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
"defined_in_file": false,
"unit": "seconds"
},
"AWX_ISOLATED_HOST_KEY_CHECKING": {
"type": "boolean",
@ -331,58 +333,10 @@
"category_slug": "jobs",
"defined_in_file": false
},
"PRIMARY_GALAXY_URL": {
"type": "string",
"label": "Primary Galaxy Server URL",
"help_text": "For organizations that run their own Galaxy service, this gives the option to specify a host as the primary galaxy server. Requirements will be downloaded from the primary if the specific role or collection is available there. If the content is not avilable in the primary, or if this field is left blank, it will default to galaxy.ansible.com.",
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
},
"PRIMARY_GALAXY_USERNAME": {
"type": "string",
"label": "Primary Galaxy Server Username",
"help_text": "(This setting is deprecated and will be removed in a future release) For using a galaxy server at higher precedence than the public Ansible Galaxy. The username to use for basic authentication against the Galaxy instance, this is mutually exclusive with PRIMARY_GALAXY_TOKEN.",
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
},
"PRIMARY_GALAXY_PASSWORD": {
"type": "string",
"label": "Primary Galaxy Server Password",
"help_text": "(This setting is deprecated and will be removed in a future release) For using a galaxy server at higher precedence than the public Ansible Galaxy. The password to use for basic authentication against the Galaxy instance, this is mutually exclusive with PRIMARY_GALAXY_TOKEN.",
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
},
"PRIMARY_GALAXY_TOKEN": {
"type": "string",
"label": "Primary Galaxy Server Token",
"help_text": "For using a galaxy server at higher precedence than the public Ansible Galaxy. The token to use for connecting with the Galaxy instance, this is mutually exclusive with corresponding username and password settings.",
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
},
"PRIMARY_GALAXY_AUTH_URL": {
"type": "string",
"label": "Primary Galaxy Authentication URL",
"help_text": "For using a galaxy server at higher precedence than the public Ansible Galaxy. The token_endpoint of a Keycloak server.",
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
},
"PUBLIC_GALAXY_ENABLED": {
"type": "boolean",
"label": "Allow Access to Public Galaxy",
"help_text": "Allow or deny access to the public Ansible Galaxy during project updates.",
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
},
"GALAXY_IGNORE_CERTS": {
"type": "boolean",
"label": "Ignore Ansible Galaxy SSL Certificate Verification",
"help_text": "If set to true, certificate validation will not be done wheninstalling content from any Galaxy server.",
"help_text": "If set to true, certificate validation will not be done when installing content from any Galaxy server.",
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
@ -432,7 +386,8 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
"defined_in_file": false,
"unit": "seconds"
},
"DEFAULT_INVENTORY_UPDATE_TIMEOUT": {
"type": "integer",
@ -441,7 +396,8 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
"defined_in_file": false,
"unit": "seconds"
},
"DEFAULT_PROJECT_UPDATE_TIMEOUT": {
"type": "integer",
@ -450,7 +406,8 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
"defined_in_file": false,
"unit": "seconds"
},
"ANSIBLE_FACT_CACHE_TIMEOUT": {
"type": "integer",
@ -459,11 +416,12 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"defined_in_file": false
"defined_in_file": false,
"unit": "seconds"
},
"MAX_FORKS": {
"type": "integer",
"label": "Maximum number of forks per job.",
"label": "Maximum number of forks per job",
"help_text": "Saving a Job Template with more than this number of forks will result in an error. When set to 0, no limit is applied.",
"category": "Jobs",
"category_slug": "jobs",
@ -598,7 +556,8 @@
"help_text": "Number of seconds for a TCP connection to external log aggregator to timeout. Applies to HTTPS and TCP log aggregator protocols.",
"category": "Logging",
"category_slug": "logging",
"defined_in_file": false
"defined_in_file": false,
"unit": "seconds"
},
"LOG_AGGREGATOR_VERIFY_CERT": {
"type": "boolean",
@ -677,7 +636,8 @@
"min_value": 1800,
"category": "System",
"category_slug": "system",
"defined_in_file": false
"defined_in_file": false,
"unit": "seconds"
},
"SESSION_COOKIE_AGE": {
"type": "integer",
@ -687,7 +647,8 @@
"max_value": 30000000000,
"category": "Authentication",
"category_slug": "authentication",
"defined_in_file": false
"defined_in_file": false,
"unit": "seconds"
},
"SESSIONS_PER_USER": {
"type": "integer",
@ -713,6 +674,7 @@
"category": "Authentication",
"category_slug": "authentication",
"defined_in_file": false,
"unit": "seconds",
"child": {
"type": "integer",
"min_value": 1
@ -2204,7 +2166,8 @@
"min_value": 0,
"category": "TACACS+",
"category_slug": "tacacsplus",
"defined_in_file": false
"defined_in_file": false,
"unit": "seconds"
},
"TACACSPLUS_AUTH_PROTOCOL": {
"type": "choice",
@ -2250,7 +2213,7 @@
},
"SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS": {
"type": "list",
"label": "Google OAuth2 Whitelisted Domains",
"label": "Google OAuth2 Allowed Domains",
"help_text": "Update this setting to restrict the domains who are allowed to login using Google OAuth2.",
"category": "Google OAuth2",
"category_slug": "google-oauth2",
@ -2542,6 +2505,14 @@
}
}
},
"SAML_AUTO_CREATE_OBJECTS": {
"type": "boolean",
"label": "Automatically Create Organizations and Teams on SAML Login",
"help_text": "When enabled (the default), mapped Organizations and Teams will be created automatically on successful SAML login.",
"category": "SAML",
"category_slug": "saml",
"defined_in_file": false
},
"SOCIAL_AUTH_SAML_CALLBACK_URL": {
"type": "string",
"label": "SAML Assertion Consumer Service (ACS) URL",
@ -2844,7 +2815,7 @@
"AUTOMATION_ANALYTICS_URL": {
"type": "string",
"required": false,
"label": "Automation Analytics upload URL.",
"label": "Automation Analytics upload URL",
"help_text": "This setting is used to to configure data collection for the Automation Analytics dashboard",
"category": "System",
"category_slug": "system",
@ -2975,6 +2946,7 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"unit": "seconds",
"default": 1
},
"AWX_ISOLATED_LAUNCH_TIMEOUT": {
@ -2985,6 +2957,7 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"unit": "seconds",
"default": 600
},
"AWX_ISOLATED_CONNECTION_TIMEOUT": {
@ -2995,6 +2968,7 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"unit": "seconds",
"default": 10
},
"AWX_ISOLATED_HOST_KEY_CHECKING": {
@ -3104,65 +3078,11 @@
"category_slug": "jobs",
"default": false
},
"PRIMARY_GALAXY_URL": {
"type": "string",
"required": false,
"label": "Primary Galaxy Server URL",
"help_text": "For organizations that run their own Galaxy service, this gives the option to specify a host as the primary galaxy server. Requirements will be downloaded from the primary if the specific role or collection is available there. If the content is not avilable in the primary, or if this field is left blank, it will default to galaxy.ansible.com.",
"category": "Jobs",
"category_slug": "jobs",
"default": ""
},
"PRIMARY_GALAXY_USERNAME": {
"type": "string",
"required": false,
"label": "Primary Galaxy Server Username",
"help_text": "(This setting is deprecated and will be removed in a future release) For using a galaxy server at higher precedence than the public Ansible Galaxy. The username to use for basic authentication against the Galaxy instance, this is mutually exclusive with PRIMARY_GALAXY_TOKEN.",
"category": "Jobs",
"category_slug": "jobs",
"default": ""
},
"PRIMARY_GALAXY_PASSWORD": {
"type": "string",
"required": false,
"label": "Primary Galaxy Server Password",
"help_text": "(This setting is deprecated and will be removed in a future release) For using a galaxy server at higher precedence than the public Ansible Galaxy. The password to use for basic authentication against the Galaxy instance, this is mutually exclusive with PRIMARY_GALAXY_TOKEN.",
"category": "Jobs",
"category_slug": "jobs",
"default": ""
},
"PRIMARY_GALAXY_TOKEN": {
"type": "string",
"required": false,
"label": "Primary Galaxy Server Token",
"help_text": "For using a galaxy server at higher precedence than the public Ansible Galaxy. The token to use for connecting with the Galaxy instance, this is mutually exclusive with corresponding username and password settings.",
"category": "Jobs",
"category_slug": "jobs",
"default": ""
},
"PRIMARY_GALAXY_AUTH_URL": {
"type": "string",
"required": false,
"label": "Primary Galaxy Authentication URL",
"help_text": "For using a galaxy server at higher precedence than the public Ansible Galaxy. The token_endpoint of a Keycloak server.",
"category": "Jobs",
"category_slug": "jobs",
"default": ""
},
"PUBLIC_GALAXY_ENABLED": {
"type": "boolean",
"required": false,
"label": "Allow Access to Public Galaxy",
"help_text": "Allow or deny access to the public Ansible Galaxy during project updates.",
"category": "Jobs",
"category_slug": "jobs",
"default": true
},
"GALAXY_IGNORE_CERTS": {
"type": "boolean",
"required": false,
"label": "Ignore Ansible Galaxy SSL Certificate Verification",
"help_text": "If set to true, certificate validation will not be done wheninstalling content from any Galaxy server.",
"help_text": "If set to true, certificate validation will not be done when installing content from any Galaxy server.",
"category": "Jobs",
"category_slug": "jobs",
"default": false
@ -3219,6 +3139,7 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"unit": "seconds",
"default": 0
},
"DEFAULT_INVENTORY_UPDATE_TIMEOUT": {
@ -3229,6 +3150,7 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"unit": "seconds",
"default": 0
},
"DEFAULT_PROJECT_UPDATE_TIMEOUT": {
@ -3239,6 +3161,7 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"unit": "seconds",
"default": 0
},
"ANSIBLE_FACT_CACHE_TIMEOUT": {
@ -3249,12 +3172,13 @@
"min_value": 0,
"category": "Jobs",
"category_slug": "jobs",
"unit": "seconds",
"default": 0
},
"MAX_FORKS": {
"type": "integer",
"required": false,
"label": "Maximum number of forks per job.",
"label": "Maximum number of forks per job",
"help_text": "Saving a Job Template with more than this number of forks will result in an error. When set to 0, no limit is applied.",
"category": "Jobs",
"category_slug": "jobs",
@ -3407,6 +3331,7 @@
"help_text": "Number of seconds for a TCP connection to external log aggregator to timeout. Applies to HTTPS and TCP log aggregator protocols.",
"category": "Logging",
"category_slug": "logging",
"unit": "seconds",
"default": 5
},
"LOG_AGGREGATOR_VERIFY_CERT": {
@ -3493,6 +3418,7 @@
"min_value": 1800,
"category": "System",
"category_slug": "system",
"unit": "seconds",
"default": 14400
},
"SESSION_COOKIE_AGE": {
@ -3504,6 +3430,7 @@
"max_value": 30000000000,
"category": "Authentication",
"category_slug": "authentication",
"unit": "seconds",
"default": 1800
},
"SESSIONS_PER_USER": {
@ -3532,6 +3459,7 @@
"help_text": "Dictionary for customizing OAuth 2 timeouts, available items are `ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number of seconds, `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of authorization codes in the number of seconds, and `REFRESH_TOKEN_EXPIRE_SECONDS`, the duration of refresh tokens, after expired access tokens, in the number of seconds.",
"category": "Authentication",
"category_slug": "authentication",
"unit": "seconds",
"default": {
"ACCESS_TOKEN_EXPIRE_SECONDS": 31536000000,
"AUTHORIZATION_CODE_EXPIRE_SECONDS": 600,
@ -5668,6 +5596,7 @@
"min_value": 0,
"category": "TACACS+",
"category_slug": "tacacsplus",
"unit": "seconds",
"default": 5
},
"TACACSPLUS_AUTH_PROTOCOL": {
@ -5712,7 +5641,7 @@
"SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS": {
"type": "list",
"required": false,
"label": "Google OAuth2 Whitelisted Domains",
"label": "Google OAuth2 Allowed Domains",
"help_text": "Update this setting to restrict the domains who are allowed to login using Google OAuth2.",
"category": "Google OAuth2",
"category_slug": "google-oauth2",
@ -6208,6 +6137,15 @@
}
}
},
"SAML_AUTO_CREATE_OBJECTS": {
"type": "boolean",
"required": false,
"label": "Automatically Create Organizations and Teams on SAML Login",
"help_text": "When enabled (the default), mapped Organizations and Teams will be created automatically on successful SAML login.",
"category": "SAML",
"category_slug": "saml",
"default": true
},
"SOCIAL_AUTH_SAML_SP_ENTITY_ID": {
"type": "string",
"required": false,

View File

@ -1,3 +1,4 @@
{
"AD_HOC_COMMANDS": [
"command"
@ -23,12 +24,6 @@
"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,

View File

@ -3,10 +3,13 @@ export function sortNestedDetails(obj = {}) {
const notNested = Object.entries(obj).filter(
([, value]) => !nestedTypes.includes(value.type)
);
const nested = Object.entries(obj).filter(([, value]) =>
nestedTypes.includes(value.type)
const nestedList = Object.entries(obj).filter(
([, value]) => value.type === 'list'
);
return [...notNested, ...nested];
const nestedObject = Object.entries(obj).filter(
([, value]) => value.type === 'nested object'
);
return [...notNested, ...nestedList, ...nestedObject];
}
export function pluck(sourceObject, ...keys) {