mirror of
https://github.com/ansible/awx.git
synced 2026-01-09 23:12:08 -03:30
commit
6d08c11506
@ -1,4 +1,4 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { number, shape } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
@ -32,40 +32,12 @@ function canLaunchWithoutPrompt(launchData) {
|
||||
);
|
||||
}
|
||||
|
||||
class LaunchButton extends React.Component {
|
||||
static propTypes = {
|
||||
resource: shape({
|
||||
id: number.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showLaunchPrompt: false,
|
||||
launchConfig: null,
|
||||
launchError: false,
|
||||
surveyConfig: null,
|
||||
};
|
||||
|
||||
this.handleLaunch = this.handleLaunch.bind(this);
|
||||
this.launchWithParams = this.launchWithParams.bind(this);
|
||||
this.handleRelaunch = this.handleRelaunch.bind(this);
|
||||
this.handleLaunchErrorClose = this.handleLaunchErrorClose.bind(this);
|
||||
this.handlePromptErrorClose = this.handlePromptErrorClose.bind(this);
|
||||
}
|
||||
|
||||
handleLaunchErrorClose() {
|
||||
this.setState({ launchError: null });
|
||||
}
|
||||
|
||||
handlePromptErrorClose() {
|
||||
this.setState({ showLaunchPrompt: false });
|
||||
}
|
||||
|
||||
async handleLaunch() {
|
||||
const { resource } = this.props;
|
||||
function LaunchButton({ resource, i18n, children, history }) {
|
||||
const [showLaunchPrompt, setShowLaunchPrompt] = useState(false);
|
||||
const [launchConfig, setLaunchConfig] = useState(null);
|
||||
const [surveyConfig, setSurveyConfig] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const handleLaunch = async () => {
|
||||
const readLaunch =
|
||||
resource.type === 'workflow_job_template'
|
||||
? WorkflowJobTemplatesAPI.readLaunch(resource.id)
|
||||
@ -75,33 +47,27 @@ class LaunchButton extends React.Component {
|
||||
? WorkflowJobTemplatesAPI.readSurvey(resource.id)
|
||||
: JobTemplatesAPI.readSurvey(resource.id);
|
||||
try {
|
||||
const { data: launchConfig } = await readLaunch;
|
||||
const { data: launch } = await readLaunch;
|
||||
setLaunchConfig(launch);
|
||||
|
||||
let surveyConfig = null;
|
||||
|
||||
if (launchConfig.survey_enabled) {
|
||||
if (launch.survey_enabled) {
|
||||
const { data } = await readSurvey;
|
||||
|
||||
surveyConfig = data;
|
||||
setSurveyConfig(data);
|
||||
}
|
||||
|
||||
if (canLaunchWithoutPrompt(launchConfig)) {
|
||||
this.launchWithParams({});
|
||||
if (canLaunchWithoutPrompt(launch)) {
|
||||
launchWithParams({});
|
||||
} else {
|
||||
this.setState({
|
||||
showLaunchPrompt: true,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
});
|
||||
setShowLaunchPrompt(true);
|
||||
}
|
||||
} catch (err) {
|
||||
this.setState({ launchError: err });
|
||||
setError(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async launchWithParams(params) {
|
||||
const launchWithParams = async params => {
|
||||
try {
|
||||
const { history, resource } = this.props;
|
||||
let jobPromise;
|
||||
|
||||
if (resource.type === 'job_template') {
|
||||
@ -117,13 +83,11 @@ class LaunchButton extends React.Component {
|
||||
const { data: job } = await jobPromise;
|
||||
history.push(`/jobs/${job.id}/output`);
|
||||
} catch (launchError) {
|
||||
this.setState({ launchError });
|
||||
setError(launchError);
|
||||
}
|
||||
}
|
||||
|
||||
async handleRelaunch() {
|
||||
const { history, resource } = this.props;
|
||||
};
|
||||
|
||||
const handleRelaunch = async () => {
|
||||
let readRelaunch;
|
||||
let relaunch;
|
||||
|
||||
@ -145,6 +109,7 @@ class LaunchButton extends React.Component {
|
||||
|
||||
try {
|
||||
const { data: relaunchConfig } = await readRelaunch;
|
||||
setLaunchConfig(relaunchConfig);
|
||||
if (
|
||||
!relaunchConfig.passwords_needed_to_start ||
|
||||
relaunchConfig.passwords_needed_to_start.length === 0
|
||||
@ -165,53 +130,47 @@ class LaunchButton extends React.Component {
|
||||
const { data: job } = await relaunch;
|
||||
history.push(`/jobs/${job.id}/output`);
|
||||
} else {
|
||||
this.setState({
|
||||
showLaunchPrompt: true,
|
||||
launchConfig: relaunchConfig,
|
||||
});
|
||||
setShowLaunchPrompt(true);
|
||||
}
|
||||
} catch (err) {
|
||||
this.setState({ launchError: err });
|
||||
setError(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
launchError,
|
||||
showLaunchPrompt,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
} = this.state;
|
||||
const { resource, i18n, children } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
{children({
|
||||
handleLaunch: this.handleLaunch,
|
||||
handleRelaunch: this.handleRelaunch,
|
||||
})}
|
||||
{launchError && (
|
||||
<AlertModal
|
||||
isOpen={launchError}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={this.handleLaunchErrorClose}
|
||||
>
|
||||
{i18n._(t`Failed to launch job.`)}
|
||||
<ErrorDetail error={launchError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{showLaunchPrompt && (
|
||||
<LaunchPrompt
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
resource={resource}
|
||||
onLaunch={this.launchWithParams}
|
||||
onCancel={() => this.setState({ showLaunchPrompt: false })}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
{children({
|
||||
handleLaunch,
|
||||
handleRelaunch,
|
||||
})}
|
||||
{error && (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => setError(null)}
|
||||
>
|
||||
{i18n._(t`Failed to launch job.`)}
|
||||
<ErrorDetail error={error} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{showLaunchPrompt && (
|
||||
<LaunchPrompt
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
resource={resource}
|
||||
onLaunch={launchWithParams}
|
||||
onCancel={() => setShowLaunchPrompt(false)}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
LaunchButton.propTypes = {
|
||||
resource: shape({
|
||||
id: number.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(withRouter(LaunchButton));
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import { sleep } from '../../../testUtils/testUtils';
|
||||
|
||||
@ -69,7 +70,7 @@ describe('LaunchButton', () => {
|
||||
}
|
||||
);
|
||||
const button = wrapper.find('button');
|
||||
button.prop('onClick')();
|
||||
await act(() => button.prop('onClick')());
|
||||
expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
||||
await sleep(0);
|
||||
expect(JobTemplatesAPI.launch).toHaveBeenCalledWith(1, {});
|
||||
@ -106,7 +107,7 @@ describe('LaunchButton', () => {
|
||||
}
|
||||
);
|
||||
const button = wrapper.find('button');
|
||||
button.prop('onClick')();
|
||||
await act(() => button.prop('onClick')());
|
||||
expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
||||
await sleep(0);
|
||||
expect(WorkflowJobTemplatesAPI.launch).toHaveBeenCalledWith(1, {});
|
||||
@ -143,7 +144,7 @@ describe('LaunchButton', () => {
|
||||
}
|
||||
);
|
||||
const button = wrapper.find('button');
|
||||
button.prop('onClick')();
|
||||
await act(() => button.prop('onClick')());
|
||||
expect(JobsAPI.readRelaunch).toHaveBeenCalledWith(1);
|
||||
await sleep(0);
|
||||
expect(JobsAPI.relaunch).toHaveBeenCalledWith(1);
|
||||
@ -180,7 +181,7 @@ describe('LaunchButton', () => {
|
||||
}
|
||||
);
|
||||
const button = wrapper.find('button');
|
||||
button.prop('onClick')();
|
||||
await act(() => button.prop('onClick')());
|
||||
expect(WorkflowJobsAPI.readRelaunch).toHaveBeenCalledWith(1);
|
||||
await sleep(0);
|
||||
expect(WorkflowJobsAPI.relaunch).toHaveBeenCalledWith(1);
|
||||
@ -218,7 +219,7 @@ describe('LaunchButton', () => {
|
||||
}
|
||||
);
|
||||
const button = wrapper.find('button');
|
||||
button.prop('onClick')();
|
||||
await act(() => button.prop('onClick')());
|
||||
expect(ProjectsAPI.readLaunchUpdate).toHaveBeenCalledWith(5);
|
||||
await sleep(0);
|
||||
expect(ProjectsAPI.launchUpdate).toHaveBeenCalledWith(5);
|
||||
@ -256,7 +257,7 @@ describe('LaunchButton', () => {
|
||||
}
|
||||
);
|
||||
const button = wrapper.find('button');
|
||||
button.prop('onClick')();
|
||||
await act(() => button.prop('onClick')());
|
||||
expect(InventorySourcesAPI.readLaunchUpdate).toHaveBeenCalledWith(5);
|
||||
await sleep(0);
|
||||
expect(InventorySourcesAPI.launchUpdate).toHaveBeenCalledWith(5);
|
||||
@ -280,7 +281,7 @@ describe('LaunchButton', () => {
|
||||
})
|
||||
);
|
||||
expect(wrapper.find('Modal').length).toBe(0);
|
||||
wrapper.find('button').prop('onClick')();
|
||||
await act(() => wrapper.find('button').prop('onClick')());
|
||||
await sleep(0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Modal').length).toBe(1);
|
||||
|
||||
@ -123,6 +123,7 @@ function Lookup(props) {
|
||||
</ChipGroup>
|
||||
</ChipHolder>
|
||||
</InputGroup>
|
||||
|
||||
<Modal
|
||||
variant="large"
|
||||
title={i18n._(t`Select ${header || i18n._(t`Items`)}`)}
|
||||
@ -138,7 +139,12 @@ function Lookup(props) {
|
||||
>
|
||||
{i18n._(t`Select`)}
|
||||
</Button>,
|
||||
<Button key="cancel" variant="link" onClick={closeModal}>
|
||||
<Button
|
||||
key="cancel"
|
||||
variant="link"
|
||||
onClick={closeModal}
|
||||
aria-label={i18n._(t`Cancel lookup`)}
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>,
|
||||
]}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'styled-components/macro';
|
||||
import React from 'react';
|
||||
import { oneOf } from 'prop-types';
|
||||
import { Label } from '@patternfly/react-core';
|
||||
import { Label, Tooltip } from '@patternfly/react-core';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationCircleIcon,
|
||||
@ -48,15 +48,19 @@ const icons = {
|
||||
canceled: ExclamationTriangleIcon,
|
||||
};
|
||||
|
||||
export default function StatusLabel({ status }) {
|
||||
export default function StatusLabel({ status, tooltipContent = '' }) {
|
||||
const label = status.charAt(0).toUpperCase() + status.slice(1);
|
||||
const color = colors[status] || 'grey';
|
||||
const Icon = icons[status];
|
||||
|
||||
return (
|
||||
<Label variant="outline" color={color} icon={Icon ? <Icon /> : null}>
|
||||
{label}
|
||||
</Label>
|
||||
<>
|
||||
<Tooltip content={tooltipContent} position="top">
|
||||
<Label variant="outline" color={color} icon={Icon ? <Icon /> : null}>
|
||||
{label}
|
||||
</Label>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PageSection, Card } from '@patternfly/react-core';
|
||||
import { CardBody } from '../../../components/Card';
|
||||
@ -14,9 +14,6 @@ import CredentialForm from '../shared/CredentialForm';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
|
||||
function CredentialAdd({ me }) {
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [credentialTypes, setCredentialTypes] = useState(null);
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
@ -85,34 +82,38 @@ function CredentialAdd({ me }) {
|
||||
history.push(`/credentials/${credentialId}/details`);
|
||||
}
|
||||
}, [credentialId, history]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const { isLoading, error, request: loadData, result } = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await CredentialTypesAPI.read({ page_size: 200 });
|
||||
const credTypes = data.results;
|
||||
if (data.next && data.next.includes('page=2')) {
|
||||
const {
|
||||
data: { results: loadedCredentialTypes },
|
||||
} = await CredentialTypesAPI.read();
|
||||
setCredentialTypes(
|
||||
loadedCredentialTypes.reduce((credentialTypesMap, credentialType) => {
|
||||
credentialTypesMap[credentialType.id] = credentialType;
|
||||
return credentialTypesMap;
|
||||
}, {})
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
data: { results },
|
||||
} = await CredentialTypesAPI.read({
|
||||
page_size: 200,
|
||||
page: 2,
|
||||
});
|
||||
credTypes.concat(results);
|
||||
}
|
||||
};
|
||||
|
||||
const creds = credTypes.reduce((credentialTypesMap, credentialType) => {
|
||||
credentialTypesMap[credentialType.id] = credentialType;
|
||||
return credentialTypesMap;
|
||||
}, {});
|
||||
return creds;
|
||||
}, []),
|
||||
{}
|
||||
);
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
}, [loadData]);
|
||||
|
||||
const handleCancel = () => {
|
||||
history.push('/credentials');
|
||||
};
|
||||
|
||||
const handleSubmit = async values => {
|
||||
await submitRequest(values, credentialTypes);
|
||||
await submitRequest(values, result);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
@ -126,7 +127,7 @@ function CredentialAdd({ me }) {
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
if (isLoading) {
|
||||
if (isLoading && !result) {
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
@ -144,7 +145,7 @@ function CredentialAdd({ me }) {
|
||||
<CredentialForm
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSubmit}
|
||||
credentialTypes={credentialTypes}
|
||||
credentialTypes={result}
|
||||
submitError={submitError}
|
||||
/>
|
||||
</CardBody>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { object } from 'prop-types';
|
||||
import { CardBody } from '../../../components/Card';
|
||||
import {
|
||||
@ -13,11 +13,8 @@ import CredentialForm from '../shared/CredentialForm';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
|
||||
function CredentialEdit({ credential, me }) {
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [credentialTypes, setCredentialTypes] = useState(null);
|
||||
const [inputSources, setInputSources] = useState({});
|
||||
const history = useHistory();
|
||||
const { id: credId } = useParams();
|
||||
|
||||
const { error: submitError, request: submitRequest, result } = useRequest(
|
||||
useCallback(
|
||||
@ -55,7 +52,7 @@ function CredentialEdit({ credential, me }) {
|
||||
input_field_name: fieldName,
|
||||
metadata: fieldValue.inputs,
|
||||
source_credential: fieldValue.credential.id,
|
||||
target_credential: credential.id,
|
||||
target_credential: credId,
|
||||
});
|
||||
}
|
||||
if (fieldValue.touched) {
|
||||
@ -88,7 +85,7 @@ function CredentialEdit({ credential, me }) {
|
||||
modifiedData.user = me.id;
|
||||
}
|
||||
const [{ data }] = await Promise.all([
|
||||
CredentialsAPI.update(credential.id, modifiedData),
|
||||
CredentialsAPI.update(credId, modifiedData),
|
||||
...destroyInputSources(),
|
||||
]);
|
||||
|
||||
@ -96,7 +93,7 @@ function CredentialEdit({ credential, me }) {
|
||||
|
||||
return data;
|
||||
},
|
||||
[me, credential.id]
|
||||
[me, credId]
|
||||
)
|
||||
);
|
||||
|
||||
@ -105,56 +102,63 @@ function CredentialEdit({ credential, me }) {
|
||||
history.push(`/credentials/${result.id}/details`);
|
||||
}
|
||||
}, [result, history]);
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
request: loadData,
|
||||
result: { credentialTypes, loadedInputSources },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const [
|
||||
{ data },
|
||||
{
|
||||
data: { results },
|
||||
},
|
||||
] = await Promise.all([
|
||||
CredentialTypesAPI.read({ page_size: 200 }),
|
||||
CredentialsAPI.readInputSources(credId, { page_size: 200 }),
|
||||
]);
|
||||
const credTypes = data.results;
|
||||
if (data.next && data.next.includes('page=2')) {
|
||||
const {
|
||||
data: { results: additionalCredTypes },
|
||||
} = await CredentialTypesAPI.read({
|
||||
page_size: 200,
|
||||
page: 2,
|
||||
});
|
||||
credTypes.concat([...additionalCredTypes]);
|
||||
}
|
||||
const creds = credTypes.reduce((credentialTypesMap, credentialType) => {
|
||||
credentialTypesMap[credentialType.id] = credentialType;
|
||||
return credentialTypesMap;
|
||||
}, {});
|
||||
const inputSources = results.reduce((inputSourcesMap, inputSource) => {
|
||||
inputSourcesMap[inputSource.input_field_name] = inputSource;
|
||||
return inputSourcesMap;
|
||||
}, {});
|
||||
return { credentialTypes: creds, loadedInputSources: inputSources };
|
||||
}, [credId]),
|
||||
{ credentialTypes: {}, loadedInputSources: {} }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [
|
||||
{
|
||||
data: { results: loadedCredentialTypes },
|
||||
},
|
||||
{
|
||||
data: { results: loadedInputSources },
|
||||
},
|
||||
] = await Promise.all([
|
||||
CredentialTypesAPI.read(),
|
||||
CredentialsAPI.readInputSources(credential.id, { page_size: 200 }),
|
||||
]);
|
||||
setCredentialTypes(
|
||||
loadedCredentialTypes.reduce((credentialTypesMap, credentialType) => {
|
||||
credentialTypesMap[credentialType.id] = credentialType;
|
||||
return credentialTypesMap;
|
||||
}, {})
|
||||
);
|
||||
setInputSources(
|
||||
loadedInputSources.reduce((inputSourcesMap, inputSource) => {
|
||||
inputSourcesMap[inputSource.input_field_name] = inputSource;
|
||||
return inputSourcesMap;
|
||||
}, {})
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, [credential.id]);
|
||||
}, [loadData]);
|
||||
|
||||
const handleCancel = () => {
|
||||
const url = `/credentials/${credential.id}/details`;
|
||||
const url = `/credentials/${credId}/details`;
|
||||
history.push(`${url}`);
|
||||
};
|
||||
|
||||
const handleSubmit = async values => {
|
||||
await submitRequest(values, credentialTypes, inputSources);
|
||||
await submitRequest(values, credentialTypes, loadedInputSources);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading && !credentialTypes) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
@ -165,7 +169,7 @@ function CredentialEdit({ credential, me }) {
|
||||
onSubmit={handleSubmit}
|
||||
credential={credential}
|
||||
credentialTypes={credentialTypes}
|
||||
inputSources={inputSources}
|
||||
inputSources={loadedInputSources}
|
||||
submitError={submitError}
|
||||
/>
|
||||
</CardBody>
|
||||
|
||||
@ -14,6 +14,12 @@ import {
|
||||
import CredentialEdit from './CredentialEdit';
|
||||
|
||||
jest.mock('../../../api');
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => ({
|
||||
id: 3,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockCredential = {
|
||||
id: 3,
|
||||
|
||||
@ -3,26 +3,41 @@ import { Formik, useField, useFormikContext } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { arrayOf, func, object, shape } from 'prop-types';
|
||||
import { ActionGroup, Button, Form, FormGroup } from '@patternfly/react-core';
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
Form,
|
||||
FormGroup,
|
||||
Select as PFSelect,
|
||||
SelectOption as PFSelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||
import {
|
||||
FormColumnLayout,
|
||||
FormFullWidthLayout,
|
||||
} from '../../../components/FormLayout';
|
||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||
import { required } from '../../../util/validators';
|
||||
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
|
||||
import TypeInputsSubForm from './TypeInputsSubForm';
|
||||
import ExternalTestModal from './ExternalTestModal';
|
||||
|
||||
function CredentialFormFields({
|
||||
i18n,
|
||||
credentialTypes,
|
||||
formik,
|
||||
initialValues,
|
||||
}) {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const Select = styled(PFSelect)`
|
||||
ul {
|
||||
max-width: 495px;
|
||||
}
|
||||
`;
|
||||
|
||||
const SelectOption = styled(PFSelectOption)`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
function CredentialFormFields({ i18n, credentialTypes }) {
|
||||
const { setFieldValue, initialValues, setFieldTouched } = useFormikContext();
|
||||
const [isSelectOpen, setIsSelectOpen] = useState(false);
|
||||
const [credTypeField, credTypeMeta, credTypeHelpers] = useField({
|
||||
name: 'credential_type',
|
||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||
@ -30,7 +45,7 @@ function CredentialFormFields({
|
||||
|
||||
const isGalaxyCredential =
|
||||
!!credTypeField.value &&
|
||||
credentialTypes[credTypeField.value].kind === 'galaxy';
|
||||
credentialTypes[credTypeField.value]?.kind === 'galaxy';
|
||||
|
||||
const [orgField, orgMeta, orgHelpers] = useField({
|
||||
name: 'organization',
|
||||
@ -52,16 +67,14 @@ function CredentialFormFields({
|
||||
})
|
||||
.sort((a, b) => (a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1));
|
||||
|
||||
const resetSubFormFields = (newCredentialType, form) => {
|
||||
const resetSubFormFields = newCredentialType => {
|
||||
const fields = credentialTypes[newCredentialType].inputs.fields || [];
|
||||
fields.forEach(
|
||||
({ ask_at_runtime, type, id, choices, default: defaultValue }) => {
|
||||
if (
|
||||
parseInt(newCredentialType, 10) === form.initialValues.credential_type
|
||||
) {
|
||||
form.setFieldValue(`inputs.${id}`, initialValues.inputs[id]);
|
||||
if (parseInt(newCredentialType, 10) === initialValues.credential_type) {
|
||||
setFieldValue(`inputs.${id}`, initialValues.inputs[id]);
|
||||
if (ask_at_runtime) {
|
||||
form.setFieldValue(
|
||||
setFieldValue(
|
||||
`passwordPrompts.${id}`,
|
||||
initialValues.passwordPrompts[id]
|
||||
);
|
||||
@ -69,24 +82,24 @@ function CredentialFormFields({
|
||||
} else {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
form.setFieldValue(`inputs.${id}`, defaultValue || '');
|
||||
setFieldValue(`inputs.${id}`, defaultValue || '');
|
||||
break;
|
||||
case 'boolean':
|
||||
form.setFieldValue(`inputs.${id}`, defaultValue || false);
|
||||
setFieldValue(`inputs.${id}`, defaultValue || false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (choices) {
|
||||
form.setFieldValue(`inputs.${id}`, defaultValue);
|
||||
setFieldValue(`inputs.${id}`, defaultValue);
|
||||
}
|
||||
|
||||
if (ask_at_runtime) {
|
||||
form.setFieldValue(`passwordPrompts.${id}`, false);
|
||||
setFieldValue(`passwordPrompts.${id}`, false);
|
||||
}
|
||||
}
|
||||
form.setFieldTouched(`inputs.${id}`, false);
|
||||
setFieldTouched(`inputs.${id}`, false);
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -133,23 +146,29 @@ function CredentialFormFields({
|
||||
}
|
||||
label={i18n._(t`Credential Type`)}
|
||||
>
|
||||
<AnsibleSelect
|
||||
{...credTypeField}
|
||||
id="credential-type"
|
||||
data={[
|
||||
{
|
||||
value: '',
|
||||
key: '',
|
||||
label: i18n._(t`Choose a Credential Type`),
|
||||
isDisabled: true,
|
||||
},
|
||||
...credentialTypeOptions,
|
||||
]}
|
||||
onChange={(event, value) => {
|
||||
<Select
|
||||
aria-label={i18n._(t`Credential Type`)}
|
||||
isOpen={isSelectOpen}
|
||||
variant={SelectVariant.typeahead}
|
||||
ouiaId="credential-select"
|
||||
onToggle={setIsSelectOpen}
|
||||
onSelect={(event, value) => {
|
||||
credTypeHelpers.setValue(value);
|
||||
resetSubFormFields(value, formik);
|
||||
resetSubFormFields(value);
|
||||
setIsSelectOpen(false);
|
||||
}}
|
||||
/>
|
||||
selections={credTypeField.value}
|
||||
placeholder={i18n._(t`Select a credential Type`)}
|
||||
isCreatable={false}
|
||||
maxHeight="300px"
|
||||
width="100%"
|
||||
>
|
||||
{credentialTypeOptions.map(credType => (
|
||||
<SelectOption key={credType.value} value={credType.value}>
|
||||
{credType.label}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
{credTypeField.value !== undefined &&
|
||||
credTypeField.value !== '' &&
|
||||
@ -177,7 +196,7 @@ function CredentialForm({
|
||||
name: credential.name || '',
|
||||
description: credential.description || '',
|
||||
organization: credential?.summary_fields?.organization || null,
|
||||
credential_type: credential.credential_type || '',
|
||||
credential_type: credential?.credential_type || '',
|
||||
inputs: {},
|
||||
passwordPrompts: {},
|
||||
};
|
||||
@ -235,8 +254,6 @@ function CredentialForm({
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormColumnLayout>
|
||||
<CredentialFormFields
|
||||
formik={formik}
|
||||
initialValues={initialValues}
|
||||
credentialTypes={credentialTypes}
|
||||
i18n={i18n}
|
||||
{...rest}
|
||||
|
||||
@ -137,15 +137,28 @@ describe('<CredentialForm />', () => {
|
||||
test('should display cred type subform when scm type select has a value', async () => {
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('AnsibleSelect[id="credential-type"]')
|
||||
.invoke('onChange')(null, 1);
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onToggle')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onSelect')(null, 1);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
machineFieldExpects();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('AnsibleSelect[id="credential-type"]')
|
||||
.invoke('onChange')(null, 2);
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onToggle')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onSelect')(null, 2);
|
||||
});
|
||||
wrapper.update();
|
||||
sourceFieldExpects();
|
||||
@ -154,8 +167,14 @@ describe('<CredentialForm />', () => {
|
||||
test('should update expected fields when gce service account json file uploaded', async () => {
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('AnsibleSelect[id="credential-type"]')
|
||||
.invoke('onChange')(null, 10);
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onToggle')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onSelect')(null, 10);
|
||||
});
|
||||
wrapper.update();
|
||||
gceFieldExpects();
|
||||
@ -215,8 +234,14 @@ describe('<CredentialForm />', () => {
|
||||
test('should show error when error thrown parsing JSON', async () => {
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('AnsibleSelect[id="credential-type"]')
|
||||
.invoke('onChange')(null, 10);
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onToggle')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onSelect')(null, 10);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('#credential-gce-file-helper').text()).toBe(
|
||||
@ -246,8 +271,14 @@ describe('<CredentialForm />', () => {
|
||||
test('should show Test button when external credential type is selected', async () => {
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('AnsibleSelect[id="credential-type"]')
|
||||
.invoke('onChange')(null, 21);
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onToggle')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onSelect')(null, 21);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Button[children="Test"]').length).toBe(1);
|
||||
|
||||
@ -55,6 +55,19 @@ function InventoryListItem({
|
||||
inventory.inventory_sources_with_failures > 0 ? 'error' : 'success';
|
||||
}
|
||||
|
||||
let tooltipContent = '';
|
||||
if (inventory.has_inventory_sources) {
|
||||
if (inventory.inventory_sources_with_failures > 0) {
|
||||
tooltipContent = i18n._(
|
||||
t`${inventory.inventory_sources_with_failures} sources with sync failures.`
|
||||
);
|
||||
} else {
|
||||
tooltipContent = i18n._(t`No inventory sync failures.`);
|
||||
}
|
||||
} else {
|
||||
tooltipContent = i18n._(t`Not configured for inventory sync.`);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tr id={inventory.id} aria-labelledby={labelId}>
|
||||
<Td
|
||||
@ -75,7 +88,9 @@ function InventoryListItem({
|
||||
)}
|
||||
</Td>
|
||||
<Td dataLabel={i18n._(t`Status`)}>
|
||||
{inventory.kind !== 'smart' && <StatusLabel status={syncStatus} />}
|
||||
{inventory.kind !== 'smart' && (
|
||||
<StatusLabel status={syncStatus} tooltipContent={tooltipContent} />
|
||||
)}
|
||||
</Td>
|
||||
<Td dataLabel={i18n._(t`Type`)}>
|
||||
{inventory.kind === 'smart'
|
||||
|
||||
@ -7,24 +7,33 @@ import InventoryListItem from './InventoryListItem';
|
||||
jest.mock('../../../api/models/Inventories');
|
||||
|
||||
describe('<InventoryListItem />', () => {
|
||||
test('initially renders succesfully', () => {
|
||||
const inventory = {
|
||||
id: 1,
|
||||
name: 'Inventory',
|
||||
kind: '',
|
||||
has_active_failures: true,
|
||||
total_hosts: 10,
|
||||
hosts_with_active_failures: 4,
|
||||
has_inventory_sources: true,
|
||||
total_inventory_sources: 4,
|
||||
inventory_sources_with_failures: 5,
|
||||
summary_fields: {
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
mountWithContexts(
|
||||
<table>
|
||||
<tbody>
|
||||
<InventoryListItem
|
||||
inventory={{
|
||||
id: 1,
|
||||
name: 'Inventory',
|
||||
summary_fields: {
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
inventory={inventory}
|
||||
detailUrl="/inventories/inventory/1"
|
||||
isSelected
|
||||
onSelect={() => {}}
|
||||
@ -34,25 +43,50 @@ describe('<InventoryListItem />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should render not configured tooltip', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<table>
|
||||
<tbody>
|
||||
<InventoryListItem
|
||||
inventory={{ ...inventory, has_inventory_sources: false }}
|
||||
detailUrl="/inventories/inventory/1"
|
||||
isSelected
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
expect(wrapper.find('StatusLabel').prop('tooltipContent')).toBe(
|
||||
'Not configured for inventory sync.'
|
||||
);
|
||||
});
|
||||
|
||||
test('should render success tooltip', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<table>
|
||||
<tbody>
|
||||
<InventoryListItem
|
||||
inventory={{ ...inventory, inventory_sources_with_failures: 0 }}
|
||||
detailUrl="/inventories/inventory/1"
|
||||
isSelected
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
expect(wrapper.find('StatusLabel').prop('tooltipContent')).toBe(
|
||||
'No inventory sync failures.'
|
||||
);
|
||||
});
|
||||
|
||||
test('should render prompt list item data', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<table>
|
||||
<tbody>
|
||||
<InventoryListItem
|
||||
inventory={{
|
||||
id: 1,
|
||||
name: 'Inventory',
|
||||
kind: '',
|
||||
summary_fields: {
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
inventory={inventory}
|
||||
detailUrl="/inventories/inventory/1"
|
||||
isSelected
|
||||
onSelect={() => {}}
|
||||
@ -61,6 +95,9 @@ describe('<InventoryListItem />', () => {
|
||||
</table>
|
||||
);
|
||||
expect(wrapper.find('StatusLabel').length).toBe(1);
|
||||
expect(wrapper.find('StatusLabel').prop('tooltipContent')).toBe(
|
||||
`${inventory.inventory_sources_with_failures} sources with sync failures.`
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Td')
|
||||
@ -72,7 +109,7 @@ describe('<InventoryListItem />', () => {
|
||||
.find('Td')
|
||||
.at(2)
|
||||
.text()
|
||||
).toBe('Disabled');
|
||||
).toBe('Error');
|
||||
expect(
|
||||
wrapper
|
||||
.find('Td')
|
||||
@ -92,19 +129,7 @@ describe('<InventoryListItem />', () => {
|
||||
<table>
|
||||
<tbody>
|
||||
<InventoryListItem
|
||||
inventory={{
|
||||
id: 1,
|
||||
name: 'Inventory',
|
||||
summary_fields: {
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
inventory={inventory}
|
||||
detailUrl="/inventories/inventory/1"
|
||||
isSelected
|
||||
onSelect={() => {}}
|
||||
|
||||
@ -281,6 +281,42 @@ function JobDetail({ job, i18n }) {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{job.job_tags && job.job_tags.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Job Tags`)}
|
||||
value={
|
||||
<ChipGroup
|
||||
numChips={5}
|
||||
totalChips={job.job_tags.split(',').length}
|
||||
>
|
||||
{job.job_tags.split(',').map(jobTag => (
|
||||
<Chip key={jobTag} isReadOnly>
|
||||
{jobTag}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{job.skip_tags && job.skip_tags.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Skip Tags`)}
|
||||
value={
|
||||
<ChipGroup
|
||||
numChips={5}
|
||||
totalChips={job.skip_tags.split(',').length}
|
||||
>
|
||||
{job.skip_tags.split(',').map(skipTag => (
|
||||
<Chip key={skipTag} isReadOnly>
|
||||
{skipTag}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Created`)}
|
||||
date={job.created}
|
||||
|
||||
@ -77,6 +77,18 @@ describe('<JobDetail />', () => {
|
||||
mockJobData.summary_fields.credentials[0]
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Job Tags"]')
|
||||
.containsAnyMatchingElements([<span>a</span>, <span>b</span>])
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Skip Tags"]')
|
||||
.containsAnyMatchingElements([<span>c</span>, <span>d</span>])
|
||||
).toEqual(true);
|
||||
|
||||
const statusDetail = wrapper.find('Detail[label="Status"]');
|
||||
expect(statusDetail.find('StatusIcon SuccessfulTop')).toHaveLength(1);
|
||||
expect(statusDetail.find('StatusIcon SuccessfulBottom')).toHaveLength(1);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
@ -578,7 +578,7 @@ class JobOutput extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { job, i18n } = this.props;
|
||||
const { job } = this.props;
|
||||
|
||||
const {
|
||||
contentError,
|
||||
@ -666,64 +666,72 @@ class JobOutput extends Component {
|
||||
</CardBody>
|
||||
{showCancelPrompt &&
|
||||
['pending', 'waiting', 'running'].includes(jobStatus) && (
|
||||
<AlertModal
|
||||
isOpen={showCancelPrompt}
|
||||
variant="danger"
|
||||
onClose={this.handleCancelClose}
|
||||
title={i18n._(t`Cancel Job`)}
|
||||
label={i18n._(t`Cancel Job`)}
|
||||
actions={[
|
||||
<Button
|
||||
id="cancel-job-confirm-button"
|
||||
key="delete"
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<AlertModal
|
||||
isOpen={showCancelPrompt}
|
||||
variant="danger"
|
||||
isDisabled={cancelInProgress}
|
||||
aria-label={i18n._(t`Cancel job`)}
|
||||
onClick={this.handleCancelConfirm}
|
||||
onClose={this.handleCancelClose}
|
||||
title={i18n._(t`Cancel Job`)}
|
||||
label={i18n._(t`Cancel Job`)}
|
||||
actions={[
|
||||
<Button
|
||||
id="cancel-job-confirm-button"
|
||||
key="delete"
|
||||
variant="danger"
|
||||
isDisabled={cancelInProgress}
|
||||
aria-label={i18n._(t`Cancel job`)}
|
||||
onClick={this.handleCancelConfirm}
|
||||
>
|
||||
{i18n._(t`Cancel job`)}
|
||||
</Button>,
|
||||
<Button
|
||||
id="cancel-job-return-button"
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Return`)}
|
||||
onClick={this.handleCancelClose}
|
||||
>
|
||||
{i18n._(t`Return`)}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{i18n._(t`Cancel job`)}
|
||||
</Button>,
|
||||
<Button
|
||||
id="cancel-job-return-button"
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Return`)}
|
||||
onClick={this.handleCancelClose}
|
||||
>
|
||||
{i18n._(t`Return`)}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{i18n._(
|
||||
t`Are you sure you want to submit the request to cancel this job?`
|
||||
{i18n._(
|
||||
t`Are you sure you want to submit the request to cancel this job?`
|
||||
)}
|
||||
</AlertModal>
|
||||
)}
|
||||
</AlertModal>
|
||||
</I18n>
|
||||
)}
|
||||
{cancelError && (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={cancelError}
|
||||
variant="danger"
|
||||
onClose={() => this.setState({ cancelError: null })}
|
||||
title={i18n._(t`Job Cancel Error`)}
|
||||
label={i18n._(t`Job Cancel Error`)}
|
||||
>
|
||||
<ErrorDetail error={cancelError} />
|
||||
</AlertModal>
|
||||
</>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<AlertModal
|
||||
isOpen={cancelError}
|
||||
variant="danger"
|
||||
onClose={() => this.setState({ cancelError: null })}
|
||||
title={i18n._(t`Job Cancel Error`)}
|
||||
label={i18n._(t`Job Cancel Error`)}
|
||||
>
|
||||
<ErrorDetail error={cancelError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</I18n>
|
||||
)}
|
||||
{deletionError && (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
variant="danger"
|
||||
onClose={() => this.setState({ deletionError: null })}
|
||||
title={i18n._(t`Job Delete Error`)}
|
||||
label={i18n._(t`Job Delete Error`)}
|
||||
>
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
</>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
variant="danger"
|
||||
onClose={() => this.setState({ deletionError: null })}
|
||||
title={i18n._(t`Job Delete Error`)}
|
||||
label={i18n._(t`Job Delete Error`)}
|
||||
>
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</I18n>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
@ -731,4 +739,4 @@ class JobOutput extends Component {
|
||||
}
|
||||
|
||||
export { JobOutput as _JobOutput };
|
||||
export default withI18n()(withRouter(JobOutput));
|
||||
export default withRouter(JobOutput);
|
||||
|
||||
@ -101,9 +101,9 @@
|
||||
"limit": "",
|
||||
"verbosity": 0,
|
||||
"extra_vars": "{\"num_messages\": 94}",
|
||||
"job_tags": "",
|
||||
"job_tags": "a,b",
|
||||
"force_handlers": false,
|
||||
"skip_tags": "",
|
||||
"skip_tags": "c,d",
|
||||
"start_at_task": "",
|
||||
"timeout": 0,
|
||||
"use_fact_cache": false,
|
||||
|
||||
@ -50,10 +50,23 @@ export function requiredEmail(i18n) {
|
||||
if (!value) {
|
||||
return i18n._(t`This field must not be blank`);
|
||||
}
|
||||
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) {
|
||||
return i18n._(t`Invalid email address`);
|
||||
|
||||
// This isn't a perfect validator. It's likely to let a few
|
||||
// invalid (though unlikely) email addresses through.
|
||||
|
||||
// This is ok, because the server will always do strict validation for us.
|
||||
|
||||
const splitVals = value.split('@');
|
||||
|
||||
if (splitVals.length >= 2) {
|
||||
if (splitVals[0] && splitVals[1]) {
|
||||
// We get here if the string has an '@' that is enclosed by
|
||||
// non-empty substrings
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
return i18n._(t`Invalid email address`);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
url,
|
||||
combine,
|
||||
regExp,
|
||||
requiredEmail,
|
||||
} from './validators';
|
||||
|
||||
const i18n = { _: val => val };
|
||||
@ -187,4 +188,14 @@ describe('validators', () => {
|
||||
expect(regExp(i18n)('ok')).toBeUndefined();
|
||||
expect(regExp(i18n)('[^a-zA-Z]')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('email validator rejects obviously invalid email ', () => {
|
||||
expect(requiredEmail(i18n)('foobar321')).toEqual({
|
||||
id: 'Invalid email address',
|
||||
});
|
||||
});
|
||||
|
||||
test('bob has email', () => {
|
||||
expect(requiredEmail(i18n)('bob@localhost')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -31,7 +31,6 @@ options:
|
||||
new_name:
|
||||
description:
|
||||
- Setting this option will change the existing name (looked up via the name field.
|
||||
required: True
|
||||
type: str
|
||||
credential:
|
||||
description:
|
||||
|
||||
@ -169,7 +169,7 @@ def test_falsy_value(run_module, admin_user, base_inventory):
|
||||
result = run_module('tower_inventory_source', dict(
|
||||
name='falsy-test',
|
||||
inventory=base_inventory.name,
|
||||
# source='ec2',
|
||||
source='ec2',
|
||||
update_on_launch=False
|
||||
), admin_user)
|
||||
|
||||
|
||||
@ -150,7 +150,8 @@ spec:
|
||||
virtualenv -p {{ custom_venv.python | default(custom_venvs_python) }} \
|
||||
{{ custom_venvs_path }}/{{ custom_venv.name }} &&
|
||||
source {{ custom_venvs_path }}/{{ custom_venv.name }}/bin/activate &&
|
||||
{{ custom_venvs_path }}/{{ custom_venv.name }}/bin/pip install {{ trusted_hosts }} -U psutil \
|
||||
{{ custom_venvs_path }}/{{ custom_venv.name }}/bin/pip install {{ trusted_hosts }} -U pip &&
|
||||
{{ custom_venvs_path }}/{{ custom_venv.name }}/bin/pip install {{ trusted_hosts }} -U psutil \
|
||||
"ansible=={{ custom_venv.python_ansible_version }}" &&
|
||||
{% if custom_venv.python_modules is defined %}
|
||||
{{ custom_venvs_path }}/{{ custom_venv.name }}/bin/pip install {{ trusted_hosts }} -U \
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
aiohttp
|
||||
ansible-runner>=1.4.6
|
||||
ansible-runner>=1.4.7
|
||||
ansiconv==1.0.0 # UPGRADE BLOCKER: from 2013, consider replacing instead of upgrading
|
||||
asciichartpy
|
||||
autobahn>=20.12.3 # CVE-2020-35678
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
adal==1.2.2 # via msrestazure
|
||||
aiohttp==3.6.2 # via -r /awx_devel/requirements/requirements.in
|
||||
aioredis==1.3.1 # via channels-redis
|
||||
ansible-runner==1.4.6 # via -r /awx_devel/requirements/requirements.in
|
||||
ansible-runner==1.4.7 # via -r /awx_devel/requirements/requirements.in
|
||||
ansiconv==1.0.0 # via -r /awx_devel/requirements/requirements.in
|
||||
asciichartpy==1.5.25 # via -r /awx_devel/requirements/requirements.in
|
||||
asgiref==3.2.5 # via channels, channels-redis, daphne
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user