mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 19:10:07 -03:30
Adds support for typing values into single select lookups
This commit is contained in:
parent
4e129d3d04
commit
4ec7ba0107
@ -82,7 +82,8 @@
|
||||
"rows",
|
||||
"href",
|
||||
"modifier",
|
||||
"data-cy"
|
||||
"data-cy",
|
||||
"fieldName"
|
||||
],
|
||||
"ignore": ["Ansible", "Tower", "JSON", "YAML", "lg"],
|
||||
"ignoreComponent": [
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { bool, func, shape } from 'prop-types';
|
||||
import { Formik, useField } from 'formik';
|
||||
|
||||
import { Formik, useField, useFormikContext } from 'formik';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { Form, FormGroup } from '@patternfly/react-core';
|
||||
import FormField, { FormSubmitError } from '../FormField';
|
||||
import FormActionGroup from '../FormActionGroup/FormActionGroup';
|
||||
@ -13,15 +11,19 @@ import { FormColumnLayout, FormFullWidthLayout } from '../FormLayout';
|
||||
import Popover from '../Popover';
|
||||
import { required } from '../../util/validators';
|
||||
|
||||
const InventoryLookupField = ({ host }) => {
|
||||
const [inventory, setInventory] = useState(
|
||||
host ? host.summary_fields.inventory : ''
|
||||
const InventoryLookupField = () => {
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [inventoryField, inventoryMeta, inventoryHelpers] = useField(
|
||||
'inventory'
|
||||
);
|
||||
|
||||
const [, inventoryMeta, inventoryHelpers] = useField({
|
||||
name: 'inventory',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const handleInventoryUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('inventory', value);
|
||||
setFieldTouched('inventory', true, false);
|
||||
},
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
@ -40,18 +42,16 @@ const InventoryLookupField = ({ host }) => {
|
||||
>
|
||||
<InventoryLookup
|
||||
fieldId="inventory-lookup"
|
||||
value={inventory}
|
||||
value={inventoryField.value}
|
||||
onBlur={() => inventoryHelpers.setTouched()}
|
||||
tooltip={t`Select the inventory that this host will belong to.`}
|
||||
isValid={!inventoryMeta.touched || !inventoryMeta.error}
|
||||
helperTextInvalid={inventoryMeta.error}
|
||||
onChange={value => {
|
||||
inventoryHelpers.setValue(value.id);
|
||||
setInventory(value);
|
||||
}}
|
||||
onChange={handleInventoryUpdate}
|
||||
required
|
||||
touched={inventoryMeta.touched}
|
||||
error={inventoryMeta.error}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
@ -62,7 +62,6 @@ const HostForm = ({
|
||||
handleSubmit,
|
||||
host,
|
||||
isInventoryVisible,
|
||||
|
||||
submitError,
|
||||
}) => {
|
||||
return (
|
||||
@ -70,7 +69,7 @@ const HostForm = ({
|
||||
initialValues={{
|
||||
name: host.name,
|
||||
description: host.description,
|
||||
inventory: host.inventory || '',
|
||||
inventory: host.summary_fields?.inventory || null,
|
||||
variables: host.variables,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
@ -92,7 +91,7 @@ const HostForm = ({
|
||||
type="text"
|
||||
label={t`Description`}
|
||||
/>
|
||||
{isInventoryVisible && <InventoryLookupField host={host} />}
|
||||
{isInventoryVisible && <InventoryLookupField />}
|
||||
<FormFullWidthLayout>
|
||||
<VariablesField
|
||||
id="host-variables"
|
||||
|
||||
@ -23,9 +23,7 @@ const QS_CONFIG = getQSConfig('inventory', {
|
||||
});
|
||||
|
||||
function InventoryStep({ warningMessage = null }) {
|
||||
const [field, meta, helpers] = useField({
|
||||
name: 'inventory',
|
||||
});
|
||||
const [field, meta, helpers] = useField('inventory');
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { func, node } from 'prop-types';
|
||||
import { func, node, string } from 'prop-types';
|
||||
import { withRouter, useLocation } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import { ApplicationsAPI } from '../../api';
|
||||
@ -18,7 +17,7 @@ const QS_CONFIG = getQSConfig('applications', {
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function ApplicationLookup({ onChange, value, label }) {
|
||||
function ApplicationLookup({ onChange, value, label, fieldName, validate }) {
|
||||
const location = useLocation();
|
||||
const {
|
||||
error,
|
||||
@ -55,6 +54,25 @@ function ApplicationLookup({ onChange, value, label }) {
|
||||
searchableKeys: [],
|
||||
}
|
||||
);
|
||||
|
||||
const checkApplicationName = useCallback(
|
||||
async name => {
|
||||
if (name && name !== '') {
|
||||
try {
|
||||
const {
|
||||
data: { results: nameMatchResults, count: nameMatchCount },
|
||||
} = await ApplicationsAPI.read({ name });
|
||||
onChange(nameMatchCount ? nameMatchResults[0] : null);
|
||||
} catch {
|
||||
onChange(null);
|
||||
}
|
||||
} else {
|
||||
onChange(null);
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchApplications();
|
||||
}, [fetchApplications]);
|
||||
@ -65,6 +83,9 @@ function ApplicationLookup({ onChange, value, label }) {
|
||||
header={t`Application`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onDebounce={checkApplicationName}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<OptionsList
|
||||
@ -119,10 +140,14 @@ ApplicationLookup.propTypes = {
|
||||
label: node.isRequired,
|
||||
onChange: func.isRequired,
|
||||
value: Application,
|
||||
validate: func,
|
||||
fieldName: string,
|
||||
};
|
||||
|
||||
ApplicationLookup.defaultProps = {
|
||||
value: null,
|
||||
validate: () => undefined,
|
||||
fieldName: 'application',
|
||||
};
|
||||
|
||||
export default withRouter(ApplicationLookup);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import ApplicationLookup from './ApplicationLookup';
|
||||
import { ApplicationsAPI } from '../../api';
|
||||
@ -41,11 +42,13 @@ describe('ApplicationLookup', () => {
|
||||
test('should render successfully', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ApplicationLookup
|
||||
label="Application"
|
||||
value={application}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<ApplicationLookup
|
||||
label="Application"
|
||||
value={application}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('ApplicationLookup')).toHaveLength(1);
|
||||
@ -54,11 +57,13 @@ describe('ApplicationLookup', () => {
|
||||
test('should fetch applications', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ApplicationLookup
|
||||
label="Application"
|
||||
value={application}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<ApplicationLookup
|
||||
label="Application"
|
||||
value={application}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(ApplicationsAPI.read).toHaveBeenCalledTimes(1);
|
||||
@ -67,11 +72,13 @@ describe('ApplicationLookup', () => {
|
||||
test('should display label', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ApplicationLookup
|
||||
label="Application"
|
||||
value={application}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<ApplicationLookup
|
||||
label="Application"
|
||||
value={application}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
const title = wrapper.find('FormGroup .pf-c-form__label-text');
|
||||
|
||||
@ -39,11 +39,12 @@ function CredentialLookup({
|
||||
credentialTypeKind,
|
||||
credentialTypeNamespace,
|
||||
value,
|
||||
|
||||
tooltip,
|
||||
isDisabled,
|
||||
autoPopulate,
|
||||
multiple,
|
||||
validate,
|
||||
fieldName,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const autoPopulateLookup = useAutoPopulateLookup(onChange);
|
||||
@ -111,6 +112,39 @@ function CredentialLookup({
|
||||
}
|
||||
);
|
||||
|
||||
const checkCredentialName = useCallback(
|
||||
async name => {
|
||||
if (name && name !== '') {
|
||||
try {
|
||||
const typeIdParams = credentialTypeId
|
||||
? { credential_type: credentialTypeId }
|
||||
: {};
|
||||
const typeKindParams = credentialTypeKind
|
||||
? { credential_type__kind: credentialTypeKind }
|
||||
: {};
|
||||
const typeNamespaceParams = credentialTypeNamespace
|
||||
? { credential_type__namespace: credentialTypeNamespace }
|
||||
: {};
|
||||
|
||||
const {
|
||||
data: { results: nameMatchResults, count: nameMatchCount },
|
||||
} = await CredentialsAPI.read({
|
||||
name,
|
||||
...typeIdParams,
|
||||
...typeKindParams,
|
||||
...typeNamespaceParams,
|
||||
});
|
||||
onChange(nameMatchCount ? nameMatchResults[0] : null);
|
||||
} catch {
|
||||
onChange(null);
|
||||
}
|
||||
} else {
|
||||
onChange(null);
|
||||
}
|
||||
},
|
||||
[onChange, credentialTypeId, credentialTypeKind, credentialTypeNamespace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCredentials();
|
||||
}, [fetchCredentials]);
|
||||
@ -132,6 +166,9 @@ function CredentialLookup({
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
onDebounce={checkCredentialName}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
required={required}
|
||||
qsConfig={QS_CONFIG}
|
||||
isDisabled={isDisabled}
|
||||
@ -212,6 +249,8 @@ CredentialLookup.propTypes = {
|
||||
value: oneOfType([Credential, arrayOf(Credential)]),
|
||||
isDisabled: bool,
|
||||
autoPopulate: bool,
|
||||
validate: func,
|
||||
fieldName: string,
|
||||
};
|
||||
|
||||
CredentialLookup.defaultProps = {
|
||||
@ -225,6 +264,8 @@ CredentialLookup.defaultProps = {
|
||||
value: null,
|
||||
isDisabled: false,
|
||||
autoPopulate: false,
|
||||
validate: () => undefined,
|
||||
fieldName: 'credential',
|
||||
};
|
||||
|
||||
export { CredentialLookup as _CredentialLookup };
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import CredentialLookup, { _CredentialLookup } from './CredentialLookup';
|
||||
import { CredentialsAPI } from '../../api';
|
||||
@ -31,11 +32,13 @@ describe('CredentialLookup', () => {
|
||||
test('should render successfully', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('CredentialLookup')).toHaveLength(1);
|
||||
@ -44,11 +47,13 @@ describe('CredentialLookup', () => {
|
||||
test('should fetch credentials', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledTimes(1);
|
||||
@ -63,11 +68,13 @@ describe('CredentialLookup', () => {
|
||||
test('should display label', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
const title = wrapper.find('FormGroup .pf-c-form__label-text');
|
||||
@ -77,11 +84,13 @@ describe('CredentialLookup', () => {
|
||||
test('should define default value for function props', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(_CredentialLookup.defaultProps.onBlur).toBeInstanceOf(Function);
|
||||
@ -98,11 +107,13 @@ describe('CredentialLookup', () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Formik>
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
@ -118,12 +129,14 @@ describe('CredentialLookup', () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
autoPopulate
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Formik>
|
||||
<CredentialLookup
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
autoPopulate
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
@ -141,12 +154,14 @@ describe('CredentialLookup auto select', () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
mountWithContexts(
|
||||
<CredentialLookup
|
||||
autoPopulate
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Formik>
|
||||
<CredentialLookup
|
||||
autoPopulate
|
||||
credentialTypeId={1}
|
||||
label="Foo"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(onChange).toHaveBeenCalledWith({ id: 1 });
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { string, func, bool, oneOfType, number } from 'prop-types';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { FormGroup, Tooltip } from '@patternfly/react-core';
|
||||
|
||||
import { ExecutionEnvironmentsAPI, ProjectsAPI } from '../../api';
|
||||
import { ExecutionEnvironment } from '../../types';
|
||||
import { getQSConfig, parseQueryString, mergeParams } from '../../util/qs';
|
||||
@ -23,17 +21,20 @@ const QS_CONFIG = getQSConfig('execution_environments', {
|
||||
|
||||
function ExecutionEnvironmentLookup({
|
||||
globallyAvailable,
|
||||
|
||||
helperTextInvalid,
|
||||
isDefaultEnvironment,
|
||||
isGlobalDefaultEnvironment,
|
||||
isDisabled,
|
||||
isGlobalDefaultEnvironment,
|
||||
isValid,
|
||||
onBlur,
|
||||
onChange,
|
||||
organizationId,
|
||||
popoverContent,
|
||||
projectId,
|
||||
tooltip,
|
||||
validate,
|
||||
value,
|
||||
fieldName,
|
||||
}) {
|
||||
const location = useLocation();
|
||||
|
||||
@ -113,6 +114,24 @@ function ExecutionEnvironmentLookup({
|
||||
}
|
||||
);
|
||||
|
||||
const checkExecutionEnvironmentName = useCallback(
|
||||
async name => {
|
||||
if (name && name !== '') {
|
||||
try {
|
||||
const {
|
||||
data: { results: nameMatchResults, count: nameMatchCount },
|
||||
} = await ExecutionEnvironmentsAPI.read({ name });
|
||||
onChange(nameMatchCount ? nameMatchResults[0] : null);
|
||||
} catch {
|
||||
onChange(null);
|
||||
}
|
||||
} else {
|
||||
onChange(null);
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchExecutionEnvironments();
|
||||
}, [fetchExecutionEnvironments]);
|
||||
@ -125,6 +144,9 @@ function ExecutionEnvironmentLookup({
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
onDebounce={checkExecutionEnvironmentName}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
qsConfig={QS_CONFIG}
|
||||
isLoading={isLoading || fetchProjectLoading}
|
||||
isDisabled={isDisabled}
|
||||
@ -179,6 +201,8 @@ function ExecutionEnvironmentLookup({
|
||||
fieldId="execution-environment-lookup"
|
||||
label={renderLabel(isGlobalDefaultEnvironment, isDefaultEnvironment)}
|
||||
labelIcon={popoverContent && <Popover content={popoverContent} />}
|
||||
helperTextInvalid={helperTextInvalid}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
>
|
||||
{tooltip && isDisabled ? (
|
||||
<Tooltip content={tooltip}>{renderLookup()}</Tooltip>
|
||||
@ -199,6 +223,8 @@ ExecutionEnvironmentLookup.propTypes = {
|
||||
isGlobalDefaultEnvironment: bool,
|
||||
projectId: oneOfType([number, string]),
|
||||
organizationId: oneOfType([number, string]),
|
||||
validate: func,
|
||||
fieldName: string,
|
||||
};
|
||||
|
||||
ExecutionEnvironmentLookup.defaultProps = {
|
||||
@ -208,6 +234,8 @@ ExecutionEnvironmentLookup.defaultProps = {
|
||||
value: null,
|
||||
projectId: null,
|
||||
organizationId: null,
|
||||
validate: () => undefined,
|
||||
fieldName: 'execution_environment',
|
||||
};
|
||||
|
||||
export default ExecutionEnvironmentLookup;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import ExecutionEnvironmentLookup from './ExecutionEnvironmentLookup';
|
||||
import { ExecutionEnvironmentsAPI, ProjectsAPI } from '../../api';
|
||||
@ -52,11 +53,13 @@ describe('ExecutionEnvironmentLookup', () => {
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ExecutionEnvironmentLookup
|
||||
isDefaultEnvironment
|
||||
value={executionEnvironment}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<ExecutionEnvironmentLookup
|
||||
isDefaultEnvironment
|
||||
value={executionEnvironment}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
@ -73,10 +76,12 @@ describe('ExecutionEnvironmentLookup', () => {
|
||||
test('should fetch execution environments', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ExecutionEnvironmentLookup
|
||||
value={executionEnvironment}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<ExecutionEnvironmentLookup
|
||||
value={executionEnvironment}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledTimes(2);
|
||||
@ -91,12 +96,14 @@ describe('ExecutionEnvironmentLookup', () => {
|
||||
test('should call api with organization id', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ExecutionEnvironmentLookup
|
||||
value={executionEnvironment}
|
||||
onChange={() => {}}
|
||||
organizationId={1}
|
||||
globallyAvailable
|
||||
/>
|
||||
<Formik>
|
||||
<ExecutionEnvironmentLookup
|
||||
value={executionEnvironment}
|
||||
onChange={() => {}}
|
||||
organizationId={1}
|
||||
globallyAvailable
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledWith({
|
||||
@ -111,12 +118,14 @@ describe('ExecutionEnvironmentLookup', () => {
|
||||
test('should call api with organization id from the related project', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ExecutionEnvironmentLookup
|
||||
value={executionEnvironment}
|
||||
onChange={() => {}}
|
||||
projectId={12}
|
||||
globallyAvailable
|
||||
/>
|
||||
<Formik>
|
||||
<ExecutionEnvironmentLookup
|
||||
value={executionEnvironment}
|
||||
onChange={() => {}}
|
||||
projectId={12}
|
||||
globallyAvailable
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(ProjectsAPI.readDetail).toHaveBeenCalledWith(12);
|
||||
|
||||
@ -19,9 +19,16 @@ const QS_CONFIG = getQSConfig('instance-groups', {
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function InstanceGroupsLookup(props) {
|
||||
const { value, onChange, tooltip, className, required, history } = props;
|
||||
|
||||
function InstanceGroupsLookup({
|
||||
value,
|
||||
onChange,
|
||||
tooltip,
|
||||
className,
|
||||
required,
|
||||
history,
|
||||
fieldName,
|
||||
validate,
|
||||
}) {
|
||||
const {
|
||||
result: { instanceGroups, count, relatedSearchableKeys, searchableKeys },
|
||||
request: fetchInstanceGroups,
|
||||
@ -69,6 +76,8 @@ function InstanceGroupsLookup(props) {
|
||||
header={t`Instance Groups`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
qsConfig={QS_CONFIG}
|
||||
multiple
|
||||
required={required}
|
||||
@ -118,12 +127,16 @@ InstanceGroupsLookup.propTypes = {
|
||||
onChange: func.isRequired,
|
||||
className: string,
|
||||
required: bool,
|
||||
validate: func,
|
||||
fieldName: string,
|
||||
};
|
||||
|
||||
InstanceGroupsLookup.defaultProps = {
|
||||
tooltip: '',
|
||||
className: '',
|
||||
required: false,
|
||||
validate: () => undefined,
|
||||
fieldName: 'instance_groups',
|
||||
};
|
||||
|
||||
export default withRouter(InstanceGroupsLookup);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { func, bool } from 'prop-types';
|
||||
import { func, bool, string } from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { InventoriesAPI } from '../../api';
|
||||
import { Inventory } from '../../types';
|
||||
@ -23,7 +22,6 @@ function InventoryLookup({
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
|
||||
history,
|
||||
required,
|
||||
isPromptableField,
|
||||
@ -31,6 +29,8 @@ function InventoryLookup({
|
||||
promptId,
|
||||
promptName,
|
||||
isOverrideDisabled,
|
||||
validate,
|
||||
fieldName,
|
||||
}) {
|
||||
const {
|
||||
result: {
|
||||
@ -50,6 +50,7 @@ function InventoryLookup({
|
||||
InventoriesAPI.read(params),
|
||||
InventoriesAPI.readOptions(),
|
||||
]);
|
||||
|
||||
return {
|
||||
inventories: data.results,
|
||||
count: data.count,
|
||||
@ -73,6 +74,24 @@ function InventoryLookup({
|
||||
}
|
||||
);
|
||||
|
||||
const checkInventoryName = useCallback(
|
||||
async name => {
|
||||
if (name && name !== '') {
|
||||
try {
|
||||
const {
|
||||
data: { results: nameMatchResults, count: nameMatchCount },
|
||||
} = await InventoriesAPI.read({ name });
|
||||
onChange(nameMatchCount ? nameMatchResults[0] : null);
|
||||
} catch {
|
||||
onChange(null);
|
||||
}
|
||||
} else {
|
||||
onChange(null);
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInventories();
|
||||
}, [fetchInventories]);
|
||||
@ -96,6 +115,9 @@ function InventoryLookup({
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
required={required}
|
||||
onDebounce={checkInventoryName}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
isLoading={isLoading}
|
||||
isDisabled={!canEdit}
|
||||
qsConfig={QS_CONFIG}
|
||||
@ -147,6 +169,9 @@ function InventoryLookup({
|
||||
header={t`Inventory`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onDebounce={checkInventoryName}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
onBlur={onBlur}
|
||||
required={required}
|
||||
isLoading={isLoading}
|
||||
@ -200,12 +225,16 @@ InventoryLookup.propTypes = {
|
||||
onChange: func.isRequired,
|
||||
required: bool,
|
||||
isOverrideDisabled: bool,
|
||||
validate: func,
|
||||
fieldName: string,
|
||||
};
|
||||
|
||||
InventoryLookup.defaultProps = {
|
||||
value: null,
|
||||
required: false,
|
||||
isOverrideDisabled: false,
|
||||
validate: () => {},
|
||||
fieldName: 'inventory',
|
||||
};
|
||||
|
||||
export default withRouter(InventoryLookup);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import InventoryLookup from './InventoryLookup';
|
||||
import { InventoriesAPI } from '../../api';
|
||||
@ -39,7 +40,11 @@ describe('InventoryLookup', () => {
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryLookup onChange={() => {}} />);
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<InventoryLookup onChange={() => {}} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(InventoriesAPI.read).toHaveBeenCalledTimes(1);
|
||||
@ -58,7 +63,9 @@ describe('InventoryLookup', () => {
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryLookup isOverrideDisabled onChange={() => {}} />
|
||||
<Formik>
|
||||
<InventoryLookup isOverrideDisabled onChange={() => {}} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
@ -77,7 +84,11 @@ describe('InventoryLookup', () => {
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryLookup onChange={() => {}} />);
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<InventoryLookup onChange={() => {}} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(InventoriesAPI.read).toHaveBeenCalledTimes(1);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { Fragment, useReducer, useEffect } from 'react';
|
||||
import React, { Fragment, useReducer, useEffect, useState } from 'react';
|
||||
import {
|
||||
string,
|
||||
bool,
|
||||
@ -9,6 +9,7 @@ import {
|
||||
shape,
|
||||
} from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { useField } from 'formik';
|
||||
import { SearchIcon } from '@patternfly/react-icons';
|
||||
import {
|
||||
Button,
|
||||
@ -16,12 +17,12 @@ import {
|
||||
Chip,
|
||||
InputGroup,
|
||||
Modal,
|
||||
TextInput,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import useDebounce from '../../util/useDebounce';
|
||||
import ChipGroup from '../ChipGroup';
|
||||
|
||||
import reducer, { initReducer } from './shared/reducer';
|
||||
import { QSConfig } from '../../types';
|
||||
|
||||
@ -44,9 +45,23 @@ function Lookup(props) {
|
||||
renderItemChip,
|
||||
renderOptionsList,
|
||||
history,
|
||||
|
||||
isDisabled,
|
||||
onDebounce,
|
||||
fieldName,
|
||||
validate,
|
||||
} = props;
|
||||
const [typedText, setTypedText] = useState('');
|
||||
const debounceRequest = useDebounce(onDebounce, 1000);
|
||||
|
||||
useField({
|
||||
name: fieldName,
|
||||
validate: val => {
|
||||
if (!multiple && !val && typedText && typedText !== '') {
|
||||
return t`That value was not found. Please enter or select a valid value.`;
|
||||
}
|
||||
return validate(val);
|
||||
},
|
||||
});
|
||||
|
||||
const [state, dispatch] = useReducer(
|
||||
reducer,
|
||||
@ -60,7 +75,16 @@ function Lookup(props) {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'SET_VALUE', value });
|
||||
}, [value]);
|
||||
if (value?.name) {
|
||||
setTypedText(value.name);
|
||||
}
|
||||
}, [value, multiple]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!multiple) {
|
||||
setTypedText(state.selectedItems[0] ? state.selectedItems[0].name : '');
|
||||
}
|
||||
}, [state.selectedItems, multiple]);
|
||||
|
||||
const clearQSParams = () => {
|
||||
const parts = history.location.search.replace(/^\?/, '').split('&');
|
||||
@ -71,19 +95,16 @@ function Lookup(props) {
|
||||
|
||||
const save = () => {
|
||||
const { selectedItems } = state;
|
||||
const val = multiple ? selectedItems : selectedItems[0] || null;
|
||||
onChange(val);
|
||||
if (multiple) {
|
||||
onChange(selectedItems);
|
||||
} else {
|
||||
onChange(selectedItems[0] || null);
|
||||
}
|
||||
clearQSParams();
|
||||
dispatch({ type: 'CLOSE_MODAL' });
|
||||
};
|
||||
|
||||
const removeItem = item => {
|
||||
if (multiple) {
|
||||
onChange(value.filter(i => i.id !== item.id));
|
||||
} else {
|
||||
onChange(null);
|
||||
}
|
||||
};
|
||||
const removeItem = item => onChange(value.filter(i => i.id !== item.id));
|
||||
|
||||
const closeModal = () => {
|
||||
clearQSParams();
|
||||
@ -99,6 +120,7 @@ function Lookup(props) {
|
||||
} else if (value) {
|
||||
items.push(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<InputGroup onBlur={onBlur}>
|
||||
@ -111,17 +133,31 @@ function Lookup(props) {
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
<ChipHolder isDisabled={isDisabled} className="pf-c-form-control">
|
||||
<ChipGroup numChips={5} totalChips={items.length}>
|
||||
{items.map(item =>
|
||||
renderItemChip({
|
||||
item,
|
||||
removeItem,
|
||||
canDelete,
|
||||
})
|
||||
)}
|
||||
</ChipGroup>
|
||||
</ChipHolder>
|
||||
{multiple ? (
|
||||
<ChipHolder isDisabled={isDisabled} className="pf-c-form-control">
|
||||
<ChipGroup numChips={5} totalChips={items.length}>
|
||||
{items.map(item =>
|
||||
renderItemChip({
|
||||
item,
|
||||
removeItem,
|
||||
canDelete,
|
||||
})
|
||||
)}
|
||||
</ChipGroup>
|
||||
</ChipHolder>
|
||||
) : (
|
||||
<TextInput
|
||||
id={`${id}-input`}
|
||||
value={typedText}
|
||||
onChange={inputValue => {
|
||||
setTypedText(inputValue);
|
||||
if (value?.name !== inputValue) {
|
||||
debounceRequest(inputValue);
|
||||
}
|
||||
}}
|
||||
isDisabled={isLoading || isDisabled}
|
||||
/>
|
||||
)}
|
||||
</InputGroup>
|
||||
|
||||
<Modal
|
||||
@ -176,6 +212,9 @@ Lookup.propTypes = {
|
||||
qsConfig: QSConfig.isRequired,
|
||||
renderItemChip: func,
|
||||
renderOptionsList: func.isRequired,
|
||||
fieldName: string.isRequired,
|
||||
validate: func,
|
||||
onDebounce: func,
|
||||
};
|
||||
|
||||
Lookup.defaultProps = {
|
||||
@ -194,6 +233,8 @@ Lookup.defaultProps = {
|
||||
{item.name}
|
||||
</Chip>
|
||||
),
|
||||
validate: () => undefined,
|
||||
onDebounce: () => undefined,
|
||||
};
|
||||
|
||||
export { Lookup as _Lookup };
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* eslint-disable react/jsx-pascal-case */
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
@ -56,22 +57,25 @@ describe('<Lookup />', () => {
|
||||
const mockSelected = [{ name: 'foo', id: 1, url: '/api/v2/item/1' }];
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Lookup
|
||||
id="test"
|
||||
multiple
|
||||
header="Foo Bar"
|
||||
value={mockSelected}
|
||||
onChange={onChange}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<TestList
|
||||
id="options-list"
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Formik>
|
||||
<Lookup
|
||||
id="test"
|
||||
multiple
|
||||
header="Foo Bar"
|
||||
value={mockSelected}
|
||||
onChange={onChange}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<TestList
|
||||
id="options-list"
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
)}
|
||||
fieldName="foo"
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
return wrapper;
|
||||
@ -137,22 +141,25 @@ describe('<Lookup />', () => {
|
||||
await act(async () => {
|
||||
const mockSelected = { name: 'foo', id: 1, url: '/api/v2/item/1' };
|
||||
wrapper = mountWithContexts(
|
||||
<Lookup
|
||||
id="test"
|
||||
header="Foo Bar"
|
||||
required
|
||||
value={mockSelected}
|
||||
onChange={onChange}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<TestList
|
||||
id="options-list"
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Formik>
|
||||
<Lookup
|
||||
id="test"
|
||||
header="Foo Bar"
|
||||
required
|
||||
value={mockSelected}
|
||||
onChange={onChange}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<TestList
|
||||
id="options-list"
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
)}
|
||||
fieldName="foo"
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.find('button[aria-label="Search"]').simulate('click');
|
||||
@ -163,23 +170,26 @@ describe('<Lookup />', () => {
|
||||
test('should be disabled while isLoading is true', async () => {
|
||||
const mockSelected = [{ name: 'foo', id: 1, url: '/api/v2/item/1' }];
|
||||
wrapper = mountWithContexts(
|
||||
<Lookup
|
||||
id="test"
|
||||
multiple
|
||||
header="Foo Bar"
|
||||
value={mockSelected}
|
||||
onChange={onChange}
|
||||
qsConfig={QS_CONFIG}
|
||||
isLoading
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<TestList
|
||||
id="options-list"
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Formik>
|
||||
<Lookup
|
||||
id="test"
|
||||
multiple
|
||||
header="Foo Bar"
|
||||
value={mockSelected}
|
||||
onChange={onChange}
|
||||
qsConfig={QS_CONFIG}
|
||||
isLoading
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<TestList
|
||||
id="options-list"
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
)}
|
||||
fieldName="foo"
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
checkRootElementNotPresent('body div[role="dialog"]');
|
||||
const button = wrapper.find('button[aria-label="Search"]');
|
||||
|
||||
@ -2,7 +2,6 @@ import 'styled-components/macro';
|
||||
import React, { Fragment, useState, useCallback, useEffect } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { ToolbarItem, Alert } from '@patternfly/react-core';
|
||||
import { CredentialsAPI, CredentialTypesAPI } from '../../api';
|
||||
@ -26,8 +25,14 @@ async function loadCredentials(params, selectedCredentialTypeId) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function MultiCredentialsLookup(props) {
|
||||
const { value, onChange, onError, history } = props;
|
||||
function MultiCredentialsLookup({
|
||||
value,
|
||||
onChange,
|
||||
onError,
|
||||
history,
|
||||
fieldName,
|
||||
validate,
|
||||
}) {
|
||||
const [selectedType, setSelectedType] = useState(null);
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
@ -68,9 +73,12 @@ function MultiCredentialsLookup(props) {
|
||||
if (!selectedType) {
|
||||
return {
|
||||
credentials: [],
|
||||
count: 0,
|
||||
credentialsCount: 0,
|
||||
relatedSearchableKeys: [],
|
||||
searchableKeys: [],
|
||||
};
|
||||
}
|
||||
|
||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||
const [{ results, count }, actionsResponse] = await Promise.all([
|
||||
loadCredentials(params, selectedType.id),
|
||||
@ -130,6 +138,8 @@ function MultiCredentialsLookup(props) {
|
||||
id="multiCredential"
|
||||
header={t`Credentials`}
|
||||
value={value}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
multiple
|
||||
onChange={onChange}
|
||||
qsConfig={QS_CONFIG}
|
||||
@ -240,10 +250,14 @@ MultiCredentialsLookup.propTypes = {
|
||||
),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onError: PropTypes.func.isRequired,
|
||||
validate: PropTypes.func,
|
||||
fieldName: PropTypes.string,
|
||||
};
|
||||
|
||||
MultiCredentialsLookup.defaultProps = {
|
||||
value: [],
|
||||
validate: () => undefined,
|
||||
fieldName: 'credentials',
|
||||
};
|
||||
|
||||
export { MultiCredentialsLookup as _MultiCredentialsLookup };
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
@ -9,7 +10,7 @@ import { CredentialsAPI, CredentialTypesAPI } from '../../api';
|
||||
|
||||
jest.mock('../../api');
|
||||
|
||||
describe('<MultiCredentialsLookup />', () => {
|
||||
describe('<Formik><MultiCredentialsLookup /></Formik>', () => {
|
||||
let wrapper;
|
||||
|
||||
const credentials = [
|
||||
@ -128,12 +129,14 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
@ -145,12 +148,14 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
const chip = wrapper.find('CredentialChip');
|
||||
@ -182,12 +187,14 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
test('should change credential types', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={() => {}}
|
||||
onError={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={() => {}}
|
||||
onError={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
const searchButton = await waitForElement(
|
||||
@ -227,12 +234,14 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
const searchButton = await waitForElement(
|
||||
@ -294,12 +303,14 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
test('should properly render vault credential labels', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={() => {}}
|
||||
onError={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={() => {}}
|
||||
onError={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
const searchButton = await waitForElement(
|
||||
@ -325,12 +336,14 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
const searchButton = await waitForElement(
|
||||
@ -392,12 +405,14 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
const searchButton = await waitForElement(
|
||||
@ -466,12 +481,14 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<MultiCredentialsLookup
|
||||
value={credentials}
|
||||
tooltip="This is credentials look up"
|
||||
onChange={onChange}
|
||||
onError={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
const searchButton = await waitForElement(
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { node, func, bool } from 'prop-types';
|
||||
import { node, func, bool, string } from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import { OrganizationsAPI } from '../../api';
|
||||
@ -21,7 +20,6 @@ const QS_CONFIG = getQSConfig('organizations', {
|
||||
|
||||
function OrganizationLookup({
|
||||
helperTextInvalid,
|
||||
|
||||
isValid,
|
||||
onBlur,
|
||||
onChange,
|
||||
@ -31,6 +29,8 @@ function OrganizationLookup({
|
||||
autoPopulate,
|
||||
isDisabled,
|
||||
helperText,
|
||||
validate,
|
||||
fieldName,
|
||||
}) {
|
||||
const autoPopulateLookup = useAutoPopulateLookup(onChange);
|
||||
|
||||
@ -69,6 +69,24 @@ function OrganizationLookup({
|
||||
}
|
||||
);
|
||||
|
||||
const checkOrganizationName = useCallback(
|
||||
async name => {
|
||||
if (name && name !== '') {
|
||||
try {
|
||||
const {
|
||||
data: { results: nameMatchResults, count: nameMatchCount },
|
||||
} = await OrganizationsAPI.read({ name });
|
||||
onChange(nameMatchCount ? nameMatchResults[0] : null);
|
||||
} catch {
|
||||
onChange(null);
|
||||
}
|
||||
} else {
|
||||
onChange(null);
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrganizations();
|
||||
}, [fetchOrganizations]);
|
||||
@ -89,6 +107,9 @@ function OrganizationLookup({
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
onDebounce={checkOrganizationName}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
qsConfig={QS_CONFIG}
|
||||
required={required}
|
||||
sortedColumnKey="name"
|
||||
@ -144,6 +165,8 @@ OrganizationLookup.propTypes = {
|
||||
value: Organization,
|
||||
autoPopulate: bool,
|
||||
isDisabled: bool,
|
||||
validate: func,
|
||||
fieldName: string,
|
||||
};
|
||||
|
||||
OrganizationLookup.defaultProps = {
|
||||
@ -154,6 +177,8 @@ OrganizationLookup.defaultProps = {
|
||||
value: null,
|
||||
autoPopulate: false,
|
||||
isDisabled: false,
|
||||
validate: () => undefined,
|
||||
fieldName: 'organization',
|
||||
};
|
||||
|
||||
export { OrganizationLookup as _OrganizationLookup };
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import OrganizationLookup, { _OrganizationLookup } from './OrganizationLookup';
|
||||
import { OrganizationsAPI } from '../../api';
|
||||
@ -16,14 +17,22 @@ describe('OrganizationLookup', () => {
|
||||
|
||||
test('should render successfully', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<OrganizationLookup onChange={() => {}} />);
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<OrganizationLookup onChange={() => {}} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should fetch organizations', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<OrganizationLookup onChange={() => {}} />);
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<OrganizationLookup onChange={() => {}} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1);
|
||||
expect(OrganizationsAPI.read).toHaveBeenCalledWith({
|
||||
@ -35,7 +44,11 @@ describe('OrganizationLookup', () => {
|
||||
|
||||
test('should display "Organization" label', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<OrganizationLookup onChange={() => {}} />);
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<OrganizationLookup onChange={() => {}} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
const title = wrapper.find('FormGroup .pf-c-form__label-text');
|
||||
expect(title.text()).toEqual('Organization');
|
||||
@ -43,7 +56,11 @@ describe('OrganizationLookup', () => {
|
||||
|
||||
test('should define default value for function props', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<OrganizationLookup onChange={() => {}} />);
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<OrganizationLookup onChange={() => {}} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(_OrganizationLookup.defaultProps.onBlur).toBeInstanceOf(Function);
|
||||
expect(_OrganizationLookup.defaultProps.onBlur).not.toThrow();
|
||||
@ -59,7 +76,9 @@ describe('OrganizationLookup', () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<OrganizationLookup autoPopulate onChange={onChange} />
|
||||
<Formik>
|
||||
<OrganizationLookup autoPopulate onChange={onChange} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(onChange).toHaveBeenCalledWith({ id: 1 });
|
||||
@ -74,7 +93,11 @@ describe('OrganizationLookup', () => {
|
||||
});
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<OrganizationLookup onChange={onChange} />);
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<OrganizationLookup onChange={onChange} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -89,7 +112,9 @@ describe('OrganizationLookup', () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<OrganizationLookup autoPopulate onChange={onChange} />
|
||||
<Formik>
|
||||
<OrganizationLookup autoPopulate onChange={onChange} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { node, string, func, bool } from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import { ProjectsAPI } from '../../api';
|
||||
@ -32,6 +31,8 @@ function ProjectLookup({
|
||||
onBlur,
|
||||
history,
|
||||
isOverrideDisabled,
|
||||
validate,
|
||||
fieldName,
|
||||
}) {
|
||||
const autoPopulateLookup = useAutoPopulateLookup(onChange);
|
||||
const {
|
||||
@ -72,6 +73,24 @@ function ProjectLookup({
|
||||
}
|
||||
);
|
||||
|
||||
const checkProjectName = useCallback(
|
||||
async name => {
|
||||
if (name && name !== '') {
|
||||
try {
|
||||
const {
|
||||
data: { results: nameMatchResults, count: nameMatchCount },
|
||||
} = await ProjectsAPI.read({ name });
|
||||
onChange(nameMatchCount ? nameMatchResults[0] : null);
|
||||
} catch {
|
||||
onChange(null);
|
||||
}
|
||||
} else {
|
||||
onChange(null);
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
}, [fetchProjects]);
|
||||
@ -92,6 +111,9 @@ function ProjectLookup({
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
onDebounce={checkProjectName}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
required={required}
|
||||
isLoading={isLoading}
|
||||
isDisabled={!canEdit}
|
||||
@ -164,6 +186,8 @@ ProjectLookup.propTypes = {
|
||||
tooltip: string,
|
||||
value: Project,
|
||||
isOverrideDisabled: bool,
|
||||
validate: func,
|
||||
fieldName: string,
|
||||
};
|
||||
|
||||
ProjectLookup.defaultProps = {
|
||||
@ -175,6 +199,8 @@ ProjectLookup.defaultProps = {
|
||||
tooltip: '',
|
||||
value: null,
|
||||
isOverrideDisabled: false,
|
||||
validate: () => undefined,
|
||||
fieldName: 'project',
|
||||
};
|
||||
|
||||
export { ProjectLookup as _ProjectLookup };
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import { ProjectsAPI } from '../../api';
|
||||
import ProjectLookup from './ProjectLookup';
|
||||
@ -14,27 +15,35 @@ describe('<ProjectLookup />', () => {
|
||||
test('should auto-select project when only one available and autoPopulate prop is true', async () => {
|
||||
ProjectsAPI.read.mockReturnValue({
|
||||
data: {
|
||||
results: [{ id: 1 }],
|
||||
results: [{ id: 1, name: 'Test' }],
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
mountWithContexts(<ProjectLookup autoPopulate onChange={onChange} />);
|
||||
mountWithContexts(
|
||||
<Formik>
|
||||
<ProjectLookup autoPopulate onChange={onChange} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(onChange).toHaveBeenCalledWith({ id: 1 });
|
||||
expect(onChange).toHaveBeenCalledWith({ id: 1, name: 'Test' });
|
||||
});
|
||||
|
||||
test('should not auto-select project when autoPopulate prop is false', async () => {
|
||||
ProjectsAPI.read.mockReturnValue({
|
||||
data: {
|
||||
results: [{ id: 1 }],
|
||||
results: [{ id: 1, name: 'Test' }],
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
mountWithContexts(<ProjectLookup onChange={onChange} />);
|
||||
mountWithContexts(
|
||||
<Formik>
|
||||
<ProjectLookup onChange={onChange} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -42,13 +51,20 @@ describe('<ProjectLookup />', () => {
|
||||
test('should not auto-select project when multiple available', async () => {
|
||||
ProjectsAPI.read.mockReturnValue({
|
||||
data: {
|
||||
results: [{ id: 1 }, { id: 2 }],
|
||||
results: [
|
||||
{ id: 1, name: 'Test' },
|
||||
{ id: 2, name: 'Test 2' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
mountWithContexts(<ProjectLookup autoPopulate onChange={onChange} />);
|
||||
mountWithContexts(
|
||||
<Formik>
|
||||
<ProjectLookup autoPopulate onChange={onChange} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -57,7 +73,7 @@ describe('<ProjectLookup />', () => {
|
||||
let wrapper;
|
||||
ProjectsAPI.read.mockReturnValue({
|
||||
data: {
|
||||
results: [{ id: 1 }],
|
||||
results: [{ id: 1, name: 'Test' }],
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
@ -71,7 +87,9 @@ describe('<ProjectLookup />', () => {
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectLookup isOverrideDisabled onChange={() => {}} />
|
||||
<Formik>
|
||||
<ProjectLookup isOverrideDisabled onChange={() => {}} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
@ -92,7 +110,11 @@ describe('<ProjectLookup />', () => {
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<ProjectLookup onChange={() => {}} />);
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<ProjectLookup onChange={() => {}} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(ProjectsAPI.read).toHaveBeenCalledTimes(1);
|
||||
@ -113,11 +135,13 @@ describe('<ProjectLookup />', () => {
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectLookup
|
||||
isValid
|
||||
helperTextInvalid="select value"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<ProjectLookup
|
||||
isValid
|
||||
helperTextInvalid="select value"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
@ -138,11 +162,13 @@ describe('<ProjectLookup />', () => {
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectLookup
|
||||
isValid={false}
|
||||
helperTextInvalid="select value"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Formik>
|
||||
<ProjectLookup
|
||||
isValid={false}
|
||||
helperTextInvalid="select value"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
@ -41,7 +41,6 @@ function OptionsList({
|
||||
deselectItem,
|
||||
renderItemChip,
|
||||
isLoading,
|
||||
|
||||
displayKey,
|
||||
}) {
|
||||
return (
|
||||
|
||||
@ -98,8 +98,9 @@ describe('<ApplicationAdd/>', () => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#name').prop('value')).toBe('new foo');
|
||||
expect(wrapper.find('input#description').prop('value')).toBe('new bar');
|
||||
expect(wrapper.find('Chip').length).toBe(1);
|
||||
expect(wrapper.find('Chip').text()).toBe('organization');
|
||||
expect(wrapper.find('input#organization-input').prop('value')).toBe(
|
||||
'organization'
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('AnsibleSelect[name="authorization_grant_type"]')
|
||||
|
||||
@ -188,8 +188,9 @@ describe('<ApplicationEdit/>', () => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#name').prop('value')).toBe('new foo');
|
||||
expect(wrapper.find('input#description').prop('value')).toBe('new bar');
|
||||
expect(wrapper.find('Chip').length).toBe(1);
|
||||
expect(wrapper.find('Chip').text()).toBe('organization');
|
||||
expect(wrapper.find('input#organization-input').prop('value')).toBe(
|
||||
'organization'
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('AnsibleSelect[name="authorization_grant_type"]')
|
||||
|
||||
@ -20,11 +20,10 @@ function ApplicationFormFields({
|
||||
clientTypeOptions,
|
||||
}) {
|
||||
const match = useRouteMatch();
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [organizationField, organizationMeta, organizationHelpers] = useField({
|
||||
name: 'organization',
|
||||
validate: required(null),
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [organizationField, organizationMeta, organizationHelpers] = useField(
|
||||
'organization'
|
||||
);
|
||||
const [
|
||||
authorizationTypeField,
|
||||
authorizationTypeMeta,
|
||||
@ -39,11 +38,12 @@ function ApplicationFormFields({
|
||||
validate: required(null),
|
||||
});
|
||||
|
||||
const onOrganizationChange = useCallback(
|
||||
const handleOrganizationUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('organization', value);
|
||||
setFieldTouched('organization', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -66,10 +66,11 @@ function ApplicationFormFields({
|
||||
helperTextInvalid={organizationMeta.error}
|
||||
isValid={!organizationMeta.touched || !organizationMeta.error}
|
||||
onBlur={() => organizationHelpers.setTouched()}
|
||||
onChange={onOrganizationChange}
|
||||
onChange={handleOrganizationUpdate}
|
||||
value={organizationField.value}
|
||||
required
|
||||
autoPopulate={!application?.id}
|
||||
validate={required(null)}
|
||||
/>
|
||||
<FormGroup
|
||||
fieldId="authType"
|
||||
|
||||
@ -107,8 +107,9 @@ describe('<ApplicationForm', () => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#name').prop('value')).toBe('new foo');
|
||||
expect(wrapper.find('input#description').prop('value')).toBe('new bar');
|
||||
expect(wrapper.find('Chip').length).toBe(1);
|
||||
expect(wrapper.find('Chip').text()).toBe('organization');
|
||||
expect(wrapper.find('input#organization-input').prop('value')).toBe(
|
||||
'organization'
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('AnsibleSelect[name="authorization_grant_type"]')
|
||||
|
||||
@ -52,12 +52,7 @@ function CredentialFormFields({ initialTypeId, credentialTypes }) {
|
||||
const isGalaxyCredential =
|
||||
!!credentialTypeId && credentialTypes[credentialTypeId]?.kind === 'galaxy';
|
||||
|
||||
const [orgField, orgMeta, orgHelpers] = useField({
|
||||
name: 'organization',
|
||||
validate:
|
||||
isGalaxyCredential &&
|
||||
required(t`Galaxy credentials must be owned by an Organization.`),
|
||||
});
|
||||
const [orgField, orgMeta, orgHelpers] = useField('organization');
|
||||
|
||||
const credentialTypeOptions = Object.keys(credentialTypes)
|
||||
.map(key => {
|
||||
@ -122,11 +117,12 @@ function CredentialFormFields({ initialTypeId, credentialTypes }) {
|
||||
}
|
||||
}, [resetSubFormFields, credentialTypeId]);
|
||||
|
||||
const onOrganizationChange = useCallback(
|
||||
const handleOrganizationUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('organization', value);
|
||||
setFieldTouched('organization', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const isCredentialTypeDisabled = pathname.includes('edit');
|
||||
@ -182,12 +178,17 @@ function CredentialFormFields({ initialTypeId, credentialTypes }) {
|
||||
helperTextInvalid={orgMeta.error}
|
||||
isValid={!orgMeta.touched || !orgMeta.error}
|
||||
onBlur={() => orgHelpers.setTouched()}
|
||||
onChange={onOrganizationChange}
|
||||
onChange={handleOrganizationUpdate}
|
||||
value={orgField.value}
|
||||
touched={orgMeta.touched}
|
||||
error={orgMeta.error}
|
||||
required={isGalaxyCredential}
|
||||
isDisabled={initialValues.isOrgLookupDisabled}
|
||||
validate={
|
||||
isGalaxyCredential
|
||||
? required(t`Galaxy credentials must be owned by an Organization.`)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<FormGroup
|
||||
fieldId="credential-Type"
|
||||
|
||||
@ -60,6 +60,7 @@ const containerRegistryCredentialResolve = {
|
||||
kind: 'registry',
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -53,6 +53,7 @@ const containerRegistryCredentialResolve = {
|
||||
kind: 'registry',
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { func, shape, bool } from 'prop-types';
|
||||
import { Formik, useField, useFormikContext } from 'formik';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { Form, FormGroup, Tooltip } from '@patternfly/react-core';
|
||||
|
||||
import { ExecutionEnvironmentsAPI } from '../../../api';
|
||||
import CredentialLookup from '../../../components/Lookup/CredentialLookup';
|
||||
import FormActionGroup from '../../../components/FormActionGroup';
|
||||
@ -26,14 +24,13 @@ function ExecutionEnvironmentFormFields({
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField(
|
||||
'credential'
|
||||
);
|
||||
const [organizationField, organizationMeta, organizationHelpers] = useField({
|
||||
name: 'organization',
|
||||
validate: !me?.is_superuser && required(t`Select a value for this field`),
|
||||
});
|
||||
const [organizationField, organizationMeta, organizationHelpers] = useField(
|
||||
'organization'
|
||||
);
|
||||
|
||||
const isGloballyAvailable = useRef(!organizationField.value);
|
||||
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
|
||||
const onCredentialChange = useCallback(
|
||||
value => {
|
||||
@ -42,20 +39,19 @@ function ExecutionEnvironmentFormFields({
|
||||
[setFieldValue]
|
||||
);
|
||||
|
||||
const onOrganizationChange = useCallback(
|
||||
const handleOrganizationUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('organization', value);
|
||||
setFieldTouched('organization', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const [
|
||||
containerOptionsField,
|
||||
containerOptionsMeta,
|
||||
containerOptionsHelpers,
|
||||
] = useField({
|
||||
name: 'pull',
|
||||
});
|
||||
] = useField('pull');
|
||||
|
||||
const containerPullChoices = options?.actions?.POST?.pull?.choices.map(
|
||||
([value, label]) => ({ value, label, key: value })
|
||||
@ -67,7 +63,7 @@ function ExecutionEnvironmentFormFields({
|
||||
helperTextInvalid={organizationMeta.error}
|
||||
isValid={!organizationMeta.touched || !organizationMeta.error}
|
||||
onBlur={() => organizationHelpers.setTouched()}
|
||||
onChange={onOrganizationChange}
|
||||
onChange={handleOrganizationUpdate}
|
||||
value={organizationField.value}
|
||||
required={!me.is_superuser}
|
||||
helperText={
|
||||
@ -79,6 +75,11 @@ function ExecutionEnvironmentFormFields({
|
||||
}
|
||||
autoPopulate={!me?.is_superuser ? !executionEnvironment?.id : null}
|
||||
isDisabled={!!isOrgLookupDisabled && isGloballyAvailable.current}
|
||||
validate={
|
||||
!me?.is_superuser
|
||||
? required(t`Select a value for this field`)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -113,6 +113,7 @@ const containerRegistryCredentialResolve = {
|
||||
kind: 'registry',
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PageSection, Card } from '@patternfly/react-core';
|
||||
|
||||
import HostForm from '../../../components/HostForm';
|
||||
import { CardBody } from '../../../components/Card';
|
||||
import { HostsAPI } from '../../../api';
|
||||
@ -12,7 +11,11 @@ function HostAdd() {
|
||||
|
||||
const handleSubmit = async formData => {
|
||||
try {
|
||||
const { data: response } = await HostsAPI.create(formData);
|
||||
const dataToSend = { ...formData };
|
||||
if (dataToSend.inventory) {
|
||||
dataToSend.inventory = dataToSend.inventory.id;
|
||||
}
|
||||
const { data: response } = await HostsAPI.create(dataToSend);
|
||||
history.push(`/hosts/${response.id}/details`);
|
||||
} catch (error) {
|
||||
setFormError(error);
|
||||
|
||||
@ -10,7 +10,10 @@ jest.mock('../../../api');
|
||||
const hostData = {
|
||||
name: 'new name',
|
||||
description: 'new description',
|
||||
inventory: 1,
|
||||
inventory: {
|
||||
id: 1,
|
||||
name: 'Demo Inventory',
|
||||
},
|
||||
variables: '---\nfoo: bar',
|
||||
};
|
||||
|
||||
@ -44,7 +47,7 @@ describe('<HostAdd />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('HostForm').prop('handleSubmit')(hostData);
|
||||
});
|
||||
expect(HostsAPI.create).toHaveBeenCalledWith(hostData);
|
||||
expect(HostsAPI.create).toHaveBeenCalledWith({ ...hostData, inventory: 1 });
|
||||
});
|
||||
|
||||
test('should navigate to hosts list when cancel is clicked', async () => {
|
||||
|
||||
@ -12,7 +12,11 @@ function HostEdit({ host }) {
|
||||
|
||||
const handleSubmit = async values => {
|
||||
try {
|
||||
await HostsAPI.update(host.id, values);
|
||||
const dataToSend = { ...values };
|
||||
if (dataToSend.inventory) {
|
||||
dataToSend.inventory = dataToSend.inventory.id;
|
||||
}
|
||||
await HostsAPI.update(host.id, dataToSend);
|
||||
history.push(detailsUrl);
|
||||
} catch (error) {
|
||||
setFormError(error);
|
||||
|
||||
@ -22,18 +22,19 @@ import CredentialLookup from '../../../components/Lookup/CredentialLookup';
|
||||
import { VariablesField } from '../../../components/CodeEditor';
|
||||
|
||||
function ContainerGroupFormFields({ instanceGroup }) {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField({
|
||||
name: 'credential',
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField(
|
||||
'credential'
|
||||
);
|
||||
|
||||
const [overrideField] = useField('override');
|
||||
|
||||
const onCredentialChange = useCallback(
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -52,7 +53,7 @@ function ContainerGroupFormFields({ instanceGroup }) {
|
||||
helperTextInvalid={credentialMeta.error}
|
||||
isValid={!credentialMeta.touched || !credentialMeta.error}
|
||||
onBlur={() => credentialHelpers.setTouched()}
|
||||
onChange={onCredentialChange}
|
||||
onChange={handleCredentialUpdate}
|
||||
value={credentialField.value}
|
||||
tooltip={t`Credential to authenticate with Kubernetes or OpenShift. Must be of type "Kubernetes/OpenShift API Bearer Token". If left blank, the underlying Pod's service account will be used.`}
|
||||
autoPopulate={!instanceGroup?.id}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Formik, useField, useFormikContext } from 'formik';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, number, shape } from 'prop-types';
|
||||
|
||||
import { Form } from '@patternfly/react-core';
|
||||
import { VariablesField } from '../../../components/CodeEditor';
|
||||
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||
@ -18,26 +16,30 @@ import {
|
||||
} from '../../../components/FormLayout';
|
||||
|
||||
function InventoryFormFields({ credentialTypeId, inventory }) {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [organizationField, organizationMeta, organizationHelpers] = useField({
|
||||
name: 'organization',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [organizationField, organizationMeta, organizationHelpers] = useField(
|
||||
'organization'
|
||||
);
|
||||
const [instanceGroupsField, , instanceGroupsHelpers] = useField(
|
||||
'instanceGroups'
|
||||
);
|
||||
const [insightsCredentialField] = useField('insights_credential');
|
||||
const onOrganizationChange = useCallback(
|
||||
const [insightsCredentialField, insightsCredentialMeta] = useField(
|
||||
'insights_credential'
|
||||
);
|
||||
const handleOrganizationUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('organization', value);
|
||||
setFieldTouched('organization', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
const onCredentialChange = useCallback(
|
||||
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('insights_credential', value);
|
||||
setFieldTouched('insights_credential', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -60,24 +62,31 @@ function InventoryFormFields({ credentialTypeId, inventory }) {
|
||||
helperTextInvalid={organizationMeta.error}
|
||||
isValid={!organizationMeta.touched || !organizationMeta.error}
|
||||
onBlur={() => organizationHelpers.setTouched()}
|
||||
onChange={onOrganizationChange}
|
||||
onChange={handleOrganizationUpdate}
|
||||
value={organizationField.value}
|
||||
touched={organizationMeta.touched}
|
||||
error={organizationMeta.error}
|
||||
required
|
||||
autoPopulate={!inventory?.id}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<CredentialLookup
|
||||
helperTextInvalid={insightsCredentialMeta.error}
|
||||
isValid={
|
||||
!insightsCredentialMeta.touched || !insightsCredentialMeta.error
|
||||
}
|
||||
label={t`Insights Credential`}
|
||||
credentialTypeId={credentialTypeId}
|
||||
onChange={onCredentialChange}
|
||||
onChange={handleCredentialUpdate}
|
||||
value={insightsCredentialField.value}
|
||||
fieldName="insights_credential"
|
||||
/>
|
||||
<InstanceGroupsLookup
|
||||
value={instanceGroupsField.value}
|
||||
onChange={value => {
|
||||
instanceGroupsHelpers.setValue(value);
|
||||
}}
|
||||
fieldName="instanceGroups"
|
||||
/>
|
||||
<FormFullWidthLayout>
|
||||
<VariablesField
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { Formik, useField, useFormikContext } from 'formik';
|
||||
import { func, shape } from 'prop-types';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { Form, FormGroup, Title } from '@patternfly/react-core';
|
||||
import { InventorySourcesAPI } from '../../../api';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import { required } from '../../../util/validators';
|
||||
|
||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||
import ContentError from '../../../components/ContentError';
|
||||
import ContentLoading from '../../../components/ContentLoading';
|
||||
@ -58,9 +56,7 @@ const InventorySourceFormFields = ({
|
||||
executionEnvironmentField,
|
||||
executionEnvironmentMeta,
|
||||
executionEnvironmentHelpers,
|
||||
] = useField({
|
||||
name: 'execution_environment',
|
||||
});
|
||||
] = useField('execution_environment');
|
||||
|
||||
const resetSubFormFields = sourceType => {
|
||||
if (sourceType === initialValues.source) {
|
||||
@ -97,6 +93,14 @@ const InventorySourceFormFields = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleExecutionEnvironmentUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('execution_environment', value);
|
||||
setFieldTouched('execution_environment', true, false);
|
||||
},
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
@ -120,7 +124,7 @@ const InventorySourceFormFields = ({
|
||||
}
|
||||
onBlur={() => executionEnvironmentHelpers.setTouched()}
|
||||
value={executionEnvironmentField.value}
|
||||
onChange={value => executionEnvironmentHelpers.setValue(value)}
|
||||
onChange={handleExecutionEnvironmentUpdate}
|
||||
globallyAvailable
|
||||
organizationId={organizationId}
|
||||
/>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useField, useFormikContext } from 'formik';
|
||||
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
|
||||
import {
|
||||
@ -16,18 +15,18 @@ import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
|
||||
import { useConfig } from '../../../../contexts/Config';
|
||||
|
||||
const AzureSubForm = ({ autoPopulateCredential }) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField({
|
||||
name: 'credential',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField(
|
||||
'credential'
|
||||
);
|
||||
const config = useConfig();
|
||||
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const pluginLink = `${getDocsBaseUrl(
|
||||
@ -48,6 +47,7 @@ const AzureSubForm = ({ autoPopulateCredential }) => {
|
||||
value={credentialField.value}
|
||||
required
|
||||
autoPopulate={autoPopulateCredential}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<VerbosityField />
|
||||
<HostFilterField />
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useField, useFormikContext } from 'formik';
|
||||
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
|
||||
import {
|
||||
@ -15,15 +14,16 @@ import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
|
||||
import { useConfig } from '../../../../contexts/Config';
|
||||
|
||||
const EC2SubForm = () => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [credentialField] = useField('credential');
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [credentialField, credentialMeta] = useField('credential');
|
||||
const config = useConfig();
|
||||
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const pluginLink = `${getDocsBaseUrl(
|
||||
@ -35,6 +35,8 @@ const EC2SubForm = () => {
|
||||
return (
|
||||
<>
|
||||
<CredentialLookup
|
||||
helperTextInvalid={credentialMeta.error}
|
||||
isValid={!credentialMeta.touched || !credentialMeta.error}
|
||||
credentialTypeNamespace="aws"
|
||||
label={t`Credential`}
|
||||
value={credentialField.value}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useField, useFormikContext } from 'formik';
|
||||
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
|
||||
import {
|
||||
@ -16,18 +15,18 @@ import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
|
||||
import { useConfig } from '../../../../contexts/Config';
|
||||
|
||||
const GCESubForm = ({ autoPopulateCredential }) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField({
|
||||
name: 'credential',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField(
|
||||
'credential'
|
||||
);
|
||||
const config = useConfig();
|
||||
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const pluginLink = `${getDocsBaseUrl(
|
||||
@ -48,6 +47,7 @@ const GCESubForm = ({ autoPopulateCredential }) => {
|
||||
value={credentialField.value}
|
||||
required
|
||||
autoPopulate={autoPopulateCredential}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<VerbosityField />
|
||||
<HostFilterField />
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useField, useFormikContext } from 'formik';
|
||||
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
|
||||
import {
|
||||
@ -16,18 +15,18 @@ import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
|
||||
import { useConfig } from '../../../../contexts/Config';
|
||||
|
||||
const OpenStackSubForm = ({ autoPopulateCredential }) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField({
|
||||
name: 'credential',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField(
|
||||
'credential'
|
||||
);
|
||||
const config = useConfig();
|
||||
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const pluginLink = `${getDocsBaseUrl(
|
||||
@ -48,6 +47,7 @@ const OpenStackSubForm = ({ autoPopulateCredential }) => {
|
||||
value={credentialField.value}
|
||||
required
|
||||
autoPopulate={autoPopulateCredential}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<VerbosityField />
|
||||
<HostFilterField />
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useField, useFormikContext } from 'formik';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
FormGroup,
|
||||
@ -11,7 +10,6 @@ import {
|
||||
import { ProjectsAPI } from '../../../../api';
|
||||
import useRequest from '../../../../util/useRequest';
|
||||
import { required } from '../../../../util/validators';
|
||||
|
||||
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
|
||||
import ProjectLookup from '../../../../components/Lookup/ProjectLookup';
|
||||
import Popover from '../../../../components/Popover';
|
||||
@ -29,10 +27,9 @@ const SCMSubForm = ({ autoPopulateProject }) => {
|
||||
const [sourcePath, setSourcePath] = useState([]);
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [credentialField] = useField('credential');
|
||||
const [projectField, projectMeta, projectHelpers] = useField({
|
||||
name: 'source_project',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const [projectField, projectMeta, projectHelpers] = useField(
|
||||
'source_project'
|
||||
);
|
||||
const [sourcePathField, sourcePathMeta, sourcePathHelpers] = useField({
|
||||
name: 'source_path',
|
||||
validate: required(t`Select a value for this field`),
|
||||
@ -60,7 +57,10 @@ const SCMSubForm = ({ autoPopulateProject }) => {
|
||||
setFieldValue('source_project', value);
|
||||
setFieldValue('source_path', '');
|
||||
setFieldTouched('source_path', false);
|
||||
fetchSourcePath(value.id);
|
||||
setFieldTouched('source_project', true, false);
|
||||
if (value) {
|
||||
fetchSourcePath(value.id);
|
||||
}
|
||||
},
|
||||
[fetchSourcePath, setFieldValue, setFieldTouched]
|
||||
);
|
||||
@ -68,8 +68,9 @@ const SCMSubForm = ({ autoPopulateProject }) => {
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -88,6 +89,8 @@ const SCMSubForm = ({ autoPopulateProject }) => {
|
||||
onChange={handleProjectUpdate}
|
||||
required
|
||||
autoPopulate={autoPopulateProject}
|
||||
fieldName="source_project"
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<FormGroup
|
||||
fieldId="source_path"
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useField, useFormikContext } from 'formik';
|
||||
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
|
||||
import {
|
||||
@ -16,18 +15,18 @@ import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
|
||||
import { useConfig } from '../../../../contexts/Config';
|
||||
|
||||
const SatelliteSubForm = ({ autoPopulateCredential }) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField({
|
||||
name: 'credential',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField(
|
||||
'credential'
|
||||
);
|
||||
const config = useConfig();
|
||||
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const pluginLink = `${getDocsBaseUrl(
|
||||
@ -48,6 +47,7 @@ const SatelliteSubForm = ({ autoPopulateCredential }) => {
|
||||
value={credentialField.value}
|
||||
required
|
||||
autoPopulate={autoPopulateCredential}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<VerbosityField />
|
||||
<HostFilterField />
|
||||
|
||||
@ -16,18 +16,18 @@ import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
|
||||
import { useConfig } from '../../../../contexts/Config';
|
||||
|
||||
const TowerSubForm = ({ autoPopulateCredential }) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField({
|
||||
name: 'credential',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField(
|
||||
'credential'
|
||||
);
|
||||
const config = useConfig();
|
||||
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const pluginLink = `${getDocsBaseUrl(
|
||||
@ -48,6 +48,7 @@ const TowerSubForm = ({ autoPopulateCredential }) => {
|
||||
value={credentialField.value}
|
||||
required
|
||||
autoPopulate={autoPopulateCredential}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<VerbosityField />
|
||||
<HostFilterField />
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useField, useFormikContext } from 'formik';
|
||||
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
|
||||
import {
|
||||
@ -16,18 +15,18 @@ import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
|
||||
import { useConfig } from '../../../../contexts/Config';
|
||||
|
||||
const VMwareSubForm = ({ autoPopulateCredential }) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField({
|
||||
name: 'credential',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField(
|
||||
'credential'
|
||||
);
|
||||
const config = useConfig();
|
||||
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const pluginLink = `${getDocsBaseUrl(
|
||||
@ -48,6 +47,7 @@ const VMwareSubForm = ({ autoPopulateCredential }) => {
|
||||
value={credentialField.value}
|
||||
required
|
||||
autoPopulate={autoPopulateCredential}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<VerbosityField />
|
||||
<HostFilterField />
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useField, useFormikContext } from 'formik';
|
||||
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
|
||||
import {
|
||||
@ -16,18 +15,18 @@ import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
|
||||
import { useConfig } from '../../../../contexts/Config';
|
||||
|
||||
const VirtualizationSubForm = ({ autoPopulateCredential }) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField({
|
||||
name: 'credential',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField(
|
||||
'credential'
|
||||
);
|
||||
const config = useConfig();
|
||||
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const pluginLink = `${getDocsBaseUrl(
|
||||
@ -48,6 +47,7 @@ const VirtualizationSubForm = ({ autoPopulateCredential }) => {
|
||||
value={credentialField.value}
|
||||
required
|
||||
autoPopulate={autoPopulateCredential}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<VerbosityField />
|
||||
<HostFilterField />
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { Formik, useField, useFormikContext } from 'formik';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { func, shape, arrayOf } from 'prop-types';
|
||||
@ -27,23 +26,23 @@ import { required } from '../../../util/validators';
|
||||
import { InventoriesAPI } from '../../../api';
|
||||
|
||||
const SmartInventoryFormFields = ({ inventory }) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [organizationField, organizationMeta, organizationHelpers] = useField({
|
||||
name: 'organization',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const [instanceGroupsField, , instanceGroupsHelpers] = useField({
|
||||
name: 'instance_groups',
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [organizationField, organizationMeta, organizationHelpers] = useField(
|
||||
'organization'
|
||||
);
|
||||
const [instanceGroupsField, , instanceGroupsHelpers] = useField(
|
||||
'instance_groups'
|
||||
);
|
||||
const [hostFilterField, hostFilterMeta, hostFilterHelpers] = useField({
|
||||
name: 'host_filter',
|
||||
validate: required(null),
|
||||
});
|
||||
const onOrganizationChange = useCallback(
|
||||
const handleOrganizationUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('organization', value);
|
||||
setFieldTouched('organization', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -66,10 +65,11 @@ const SmartInventoryFormFields = ({ inventory }) => {
|
||||
helperTextInvalid={organizationMeta.error}
|
||||
isValid={!organizationMeta.touched || !organizationMeta.error}
|
||||
onBlur={() => organizationHelpers.setTouched()}
|
||||
onChange={onOrganizationChange}
|
||||
onChange={handleOrganizationUpdate}
|
||||
value={organizationField.value}
|
||||
required
|
||||
autoPopulate={!inventory?.id}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<HostFilterLookup
|
||||
value={hostFilterField.value}
|
||||
|
||||
@ -17,18 +17,19 @@ import hasCustomMessages from './hasCustomMessages';
|
||||
import typeFieldNames, { initialConfigValues } from './typeFieldNames';
|
||||
|
||||
function NotificationTemplateFormFields({ defaultMessages, template }) {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [orgField, orgMeta, orgHelpers] = useField('organization');
|
||||
const [typeField, typeMeta] = useField({
|
||||
name: 'notification_type',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
|
||||
const onOrganizationChange = useCallback(
|
||||
const handleOrganizationUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('organization', value);
|
||||
setFieldTouched('organization', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -51,12 +52,13 @@ function NotificationTemplateFormFields({ defaultMessages, template }) {
|
||||
helperTextInvalid={orgMeta.error}
|
||||
isValid={!orgMeta.touched || !orgMeta.error}
|
||||
onBlur={() => orgHelpers.setTouched()}
|
||||
onChange={onOrganizationChange}
|
||||
onChange={handleOrganizationUpdate}
|
||||
value={orgField.value}
|
||||
touched={orgMeta.touched}
|
||||
error={orgMeta.error}
|
||||
required
|
||||
autoPopulate={!template?.id}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<FormGroup
|
||||
fieldId="notification-type"
|
||||
|
||||
@ -39,9 +39,7 @@ function OrganizationFormFields({
|
||||
executionEnvironmentField,
|
||||
executionEnvironmentMeta,
|
||||
executionEnvironmentHelpers,
|
||||
] = useField({
|
||||
name: 'default_environment',
|
||||
});
|
||||
] = useField('default_environment');
|
||||
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
@ -97,6 +95,7 @@ function OrganizationFormFields({
|
||||
globallyAvailable
|
||||
organizationId={organizationId}
|
||||
isDefaultEnvironment
|
||||
fieldName="default_environment"
|
||||
/>
|
||||
<CredentialLookup
|
||||
credentialTypeNamespace="galaxy_api_token"
|
||||
@ -107,6 +106,7 @@ function OrganizationFormFields({
|
||||
onChange={handleCredentialUpdate}
|
||||
value={galaxyCredentialsField.value}
|
||||
multiple
|
||||
fieldName="galaxy_credentials"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -19,6 +19,8 @@ function ProjectAdd() {
|
||||
// has a zero-length string as its credential field. As a work-around,
|
||||
// normalize falsey credential fields by deleting them.
|
||||
delete values.credential;
|
||||
} else {
|
||||
values.credential = values.credential.id;
|
||||
}
|
||||
setFormSubmitError(null);
|
||||
try {
|
||||
|
||||
@ -56,6 +56,7 @@ describe('<ProjectAdd />', () => {
|
||||
kind: 'scm',
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
},
|
||||
};
|
||||
|
||||
@ -68,6 +69,7 @@ describe('<ProjectAdd />', () => {
|
||||
kind: 'insights',
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -19,6 +19,8 @@ function ProjectEdit({ project }) {
|
||||
// has a zero-length string as its credential field. As a work-around,
|
||||
// normalize falsey credential fields by deleting them.
|
||||
delete values.credential;
|
||||
} else {
|
||||
values.credential = values.credential.id;
|
||||
}
|
||||
try {
|
||||
const {
|
||||
|
||||
@ -89,24 +89,21 @@ function ProjectFormFields({
|
||||
scm_update_cache_timeout: 0,
|
||||
};
|
||||
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
|
||||
const [scmTypeField, scmTypeMeta, scmTypeHelpers] = useField({
|
||||
name: 'scm_type',
|
||||
validate: required(t`Set a value for this field`),
|
||||
});
|
||||
const [organizationField, organizationMeta, organizationHelpers] = useField({
|
||||
name: 'organization',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const [organizationField, organizationMeta, organizationHelpers] = useField(
|
||||
'organization'
|
||||
);
|
||||
|
||||
const [
|
||||
executionEnvironmentField,
|
||||
executionEnvironmentMeta,
|
||||
executionEnvironmentHelpers,
|
||||
] = useField({
|
||||
name: 'default_environment',
|
||||
});
|
||||
] = useField('default_environment');
|
||||
|
||||
/* Save current scm subform field values to state */
|
||||
const saveSubFormState = form => {
|
||||
@ -153,11 +150,20 @@ function ProjectFormFields({
|
||||
[credentials, setCredentials]
|
||||
);
|
||||
|
||||
const onOrganizationChange = useCallback(
|
||||
const handleOrganizationUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('organization', value);
|
||||
setFieldTouched('organization', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const handleExecutionEnvironmentUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('default_environment', value);
|
||||
setFieldTouched('default_environment', true, false);
|
||||
},
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -180,10 +186,11 @@ function ProjectFormFields({
|
||||
helperTextInvalid={organizationMeta.error}
|
||||
isValid={!organizationMeta.touched || !organizationMeta.error}
|
||||
onBlur={() => organizationHelpers.setTouched()}
|
||||
onChange={onOrganizationChange}
|
||||
onChange={handleOrganizationUpdate}
|
||||
value={organizationField.value}
|
||||
required
|
||||
autoPopulate={!project?.id}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<ExecutionEnvironmentLookup
|
||||
helperTextInvalid={executionEnvironmentMeta.error}
|
||||
@ -192,13 +199,14 @@ function ProjectFormFields({
|
||||
}
|
||||
onBlur={() => executionEnvironmentHelpers.setTouched()}
|
||||
value={executionEnvironmentField.value}
|
||||
onChange={value => executionEnvironmentHelpers.setValue(value)}
|
||||
popoverContent={t`The execution environment that will be used for jobs that use this project. This will be used as fallback when an execution environment has not been explicitly assigned at the job template or workflow level.`}
|
||||
onChange={handleExecutionEnvironmentUpdate}
|
||||
tooltip={t`Select an organization before editing the default execution environment.`}
|
||||
globallyAvailable
|
||||
isDisabled={!organizationField.value}
|
||||
organizationId={organizationField.value?.id}
|
||||
isDefaultEnvironment
|
||||
fieldName="default_environment"
|
||||
/>
|
||||
<FormGroup
|
||||
fieldId="project-scm-type"
|
||||
|
||||
@ -12,18 +12,16 @@ const InsightsSubForm = ({
|
||||
scmUpdateOnLaunch,
|
||||
autoPopulateCredential,
|
||||
}) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [, credMeta, credHelpers] = useField({
|
||||
name: 'credential',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [, credMeta, credHelpers] = useField('credential');
|
||||
|
||||
const onCredentialChange = useCallback(
|
||||
value => {
|
||||
onCredentialSelection('insights', value);
|
||||
setFieldValue('credential', value.id);
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[onCredentialSelection, setFieldValue]
|
||||
[onCredentialSelection, setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -38,6 +36,7 @@ const InsightsSubForm = ({
|
||||
value={credential.value}
|
||||
required
|
||||
autoPopulate={autoPopulateCredential}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
<ScmTypeOptions hideAllowOverride scmUpdateOnLaunch={scmUpdateOnLaunch} />
|
||||
</>
|
||||
|
||||
@ -41,14 +41,15 @@ export const ScmCredentialFormField = ({
|
||||
credential,
|
||||
onCredentialSelection,
|
||||
}) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
|
||||
const onCredentialChange = useCallback(
|
||||
value => {
|
||||
onCredentialSelection('scm', value);
|
||||
setFieldValue('credential', value ? value.id : '');
|
||||
setFieldValue('credential', value);
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
[onCredentialSelection, setFieldValue]
|
||||
[onCredentialSelection, setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { Formik } from 'formik';
|
||||
import { Form } from '@patternfly/react-core';
|
||||
@ -238,14 +237,20 @@ function MiscSystemEdit() {
|
||||
formik.setFieldTouched('DEFAULT_EXECUTION_ENVIRONMENT')
|
||||
}
|
||||
value={formik.values.DEFAULT_EXECUTION_ENVIRONMENT}
|
||||
onChange={value =>
|
||||
onChange={value => {
|
||||
formik.setFieldValue(
|
||||
'DEFAULT_EXECUTION_ENVIRONMENT',
|
||||
value
|
||||
)
|
||||
}
|
||||
);
|
||||
formik.setFieldTouched(
|
||||
'DEFAULT_EXECUTION_ENVIRONMENT',
|
||||
true,
|
||||
false
|
||||
);
|
||||
}}
|
||||
popoverContent={t`The Execution Environment to be used when one has not been configured for a job template.`}
|
||||
isGlobalDefaultEnvironment
|
||||
fieldName="DEFAULT_EXECUTION_ENVIRONMENT"
|
||||
/>
|
||||
<InputField
|
||||
name="TOWER_URL_BASE"
|
||||
|
||||
@ -15,18 +15,10 @@ const ANALYTICSLINK = 'https://www.ansible.com/products/automation-analytics';
|
||||
|
||||
function AnalyticsStep() {
|
||||
const config = useConfig();
|
||||
const [manifest] = useField({
|
||||
name: 'manifest_file',
|
||||
});
|
||||
const [insights] = useField({
|
||||
name: 'insights',
|
||||
});
|
||||
const [, , usernameHelpers] = useField({
|
||||
name: 'username',
|
||||
});
|
||||
const [, , passwordHelpers] = useField({
|
||||
name: 'password',
|
||||
});
|
||||
const [manifest] = useField('manifest_file');
|
||||
const [insights] = useField('insights');
|
||||
const [, , usernameHelpers] = useField('username');
|
||||
const [, , passwordHelpers] = useField('password');
|
||||
const requireCredentialFields = manifest.value && insights.value;
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -40,21 +40,13 @@ function SubscriptionStep() {
|
||||
values.subscription ? 'selectSubscription' : 'uploadManifest'
|
||||
);
|
||||
const { isModalOpen, toggleModal, closeModal } = useModal();
|
||||
const [manifest, manifestMeta, manifestHelpers] = useField({
|
||||
name: 'manifest_file',
|
||||
});
|
||||
const [manifestFilename, , manifestFilenameHelpers] = useField({
|
||||
name: 'manifest_filename',
|
||||
});
|
||||
const [subscription, , subscriptionHelpers] = useField({
|
||||
name: 'subscription',
|
||||
});
|
||||
const [username, usernameMeta, usernameHelpers] = useField({
|
||||
name: 'username',
|
||||
});
|
||||
const [password, passwordMeta, passwordHelpers] = useField({
|
||||
name: 'password',
|
||||
});
|
||||
const [manifest, manifestMeta, manifestHelpers] = useField('manifest_file');
|
||||
const [manifestFilename, , manifestFilenameHelpers] = useField(
|
||||
'manifest_filename'
|
||||
);
|
||||
const [subscription, , subscriptionHelpers] = useField('subscription');
|
||||
const [username, usernameMeta, usernameHelpers] = useField('username');
|
||||
const [password, passwordMeta, passwordHelpers] = useField('password');
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
||||
@ -18,7 +18,9 @@ class TeamAdd extends React.Component {
|
||||
async handleSubmit(values) {
|
||||
const { history } = this.props;
|
||||
try {
|
||||
const { data: response } = await TeamsAPI.create(values);
|
||||
const valuesToSend = { ...values };
|
||||
valuesToSend.organization = valuesToSend.organization.id;
|
||||
const { data: response } = await TeamsAPI.create(valuesToSend);
|
||||
history.push(`/teams/${response.id}`);
|
||||
} catch (error) {
|
||||
this.setState({ error });
|
||||
|
||||
@ -17,12 +17,18 @@ describe('<TeamAdd />', () => {
|
||||
const updatedTeamData = {
|
||||
name: 'new name',
|
||||
description: 'new description',
|
||||
organization: 1,
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
},
|
||||
};
|
||||
await act(async () => {
|
||||
wrapper.find('TeamForm').invoke('handleSubmit')(updatedTeamData);
|
||||
});
|
||||
expect(TeamsAPI.create).toHaveBeenCalledWith(updatedTeamData);
|
||||
expect(TeamsAPI.create).toHaveBeenCalledWith({
|
||||
...updatedTeamData,
|
||||
organization: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('should navigate to teams list when cancel is clicked', async () => {
|
||||
@ -41,7 +47,10 @@ describe('<TeamAdd />', () => {
|
||||
const teamData = {
|
||||
name: 'new name',
|
||||
description: 'new description',
|
||||
organization: 1,
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
},
|
||||
};
|
||||
TeamsAPI.create.mockResolvedValueOnce({
|
||||
data: {
|
||||
|
||||
@ -14,7 +14,11 @@ function TeamEdit({ team }) {
|
||||
|
||||
const handleSubmit = async values => {
|
||||
try {
|
||||
await TeamsAPI.update(team.id, values);
|
||||
const valuesToSend = { ...values };
|
||||
if (valuesToSend.organization) {
|
||||
valuesToSend.organization = valuesToSend.organization.id;
|
||||
}
|
||||
await TeamsAPI.update(team.id, valuesToSend);
|
||||
history.push(`/teams/${team.id}/details`);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
|
||||
@ -30,12 +30,19 @@ describe('<TeamEdit />', () => {
|
||||
const updatedTeamData = {
|
||||
name: 'new name',
|
||||
description: 'new description',
|
||||
organization: {
|
||||
id: 2,
|
||||
name: 'Other Org',
|
||||
},
|
||||
};
|
||||
await act(async () => {
|
||||
wrapper.find('TeamForm').invoke('handleSubmit')(updatedTeamData);
|
||||
});
|
||||
|
||||
expect(TeamsAPI.update).toHaveBeenCalledWith(1, updatedTeamData);
|
||||
expect(TeamsAPI.update).toHaveBeenCalledWith(1, {
|
||||
...updatedTeamData,
|
||||
organization: 2,
|
||||
});
|
||||
expect(history.location.pathname).toEqual('/teams/1/details');
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
@ -11,21 +11,15 @@ import { required } from '../../../util/validators';
|
||||
import { FormColumnLayout } from '../../../components/FormLayout';
|
||||
|
||||
function TeamFormFields({ team }) {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [organization, setOrganization] = useState(
|
||||
team.summary_fields ? team.summary_fields.organization : null
|
||||
);
|
||||
const [, orgMeta, orgHelpers] = useField({
|
||||
name: 'organization',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [orgField, orgMeta, orgHelpers] = useField('organization');
|
||||
|
||||
const onOrganizationChange = useCallback(
|
||||
const handleOrganizationUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('organization', value.id);
|
||||
setOrganization(value);
|
||||
setFieldValue('organization', value);
|
||||
setFieldTouched('organization', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -48,10 +42,11 @@ function TeamFormFields({ team }) {
|
||||
helperTextInvalid={orgMeta.error}
|
||||
isValid={!orgMeta.touched || !orgMeta.error}
|
||||
onBlur={() => orgHelpers.setTouched('organization')}
|
||||
onChange={onOrganizationChange}
|
||||
value={organization}
|
||||
onChange={handleOrganizationUpdate}
|
||||
value={orgField.value}
|
||||
required
|
||||
autoPopulate={!team?.id}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -65,7 +60,7 @@ function TeamForm(props) {
|
||||
initialValues={{
|
||||
description: team.description || '',
|
||||
name: team.name || '',
|
||||
organization: team.organization || '',
|
||||
organization: team.summary_fields?.organization || null,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
|
||||
@ -22,8 +22,10 @@ describe('<TeamForm />', () => {
|
||||
description: 'Bar',
|
||||
organization: 1,
|
||||
summary_fields: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@ function JobTemplateAdd() {
|
||||
labels,
|
||||
instanceGroups,
|
||||
initialInstanceGroups,
|
||||
inventory,
|
||||
project,
|
||||
credentials,
|
||||
webhook_credential,
|
||||
webhook_key,
|
||||
@ -22,8 +24,9 @@ function JobTemplateAdd() {
|
||||
} = values;
|
||||
|
||||
setFormSubmitError(null);
|
||||
remainingValues.project = remainingValues.project.id;
|
||||
remainingValues.project = project.id;
|
||||
remainingValues.webhook_credential = webhook_credential?.id;
|
||||
remainingValues.inventory = inventory?.id || null;
|
||||
try {
|
||||
const {
|
||||
data: { id, type },
|
||||
|
||||
@ -46,6 +46,8 @@ function JobTemplateEdit({ template }) {
|
||||
instanceGroups,
|
||||
initialInstanceGroups,
|
||||
credentials,
|
||||
inventory,
|
||||
project,
|
||||
webhook_credential,
|
||||
webhook_key,
|
||||
webhook_url,
|
||||
@ -55,8 +57,9 @@ function JobTemplateEdit({ template }) {
|
||||
|
||||
setFormSubmitError(null);
|
||||
setIsLoading(true);
|
||||
remainingValues.project = values.project.id;
|
||||
remainingValues.project = project.id;
|
||||
remainingValues.webhook_credential = webhook_credential?.id || null;
|
||||
remainingValues.inventory = inventory?.id || null;
|
||||
remainingValues.execution_environment = execution_environment?.id || null;
|
||||
try {
|
||||
await JobTemplatesAPI.update(template.id, remainingValues);
|
||||
|
||||
@ -13,10 +13,20 @@ import {
|
||||
ProjectsAPI,
|
||||
InventoriesAPI,
|
||||
ExecutionEnvironmentsAPI,
|
||||
InstanceGroupsAPI,
|
||||
} from '../../../api';
|
||||
import JobTemplateEdit from './JobTemplateEdit';
|
||||
import useDebounce from '../../../util/useDebounce';
|
||||
|
||||
jest.mock('../../../api');
|
||||
jest.mock('../../../util/useDebounce');
|
||||
jest.mock('../../../api/models/Credentials');
|
||||
jest.mock('../../../api/models/CredentialTypes');
|
||||
jest.mock('../../../api/models/JobTemplates');
|
||||
jest.mock('../../../api/models/Labels');
|
||||
jest.mock('../../../api/models/Projects');
|
||||
jest.mock('../../../api/models/Inventories');
|
||||
jest.mock('../../../api/models/ExecutionEnvironments');
|
||||
jest.mock('../../../api/models/InstanceGroups');
|
||||
|
||||
const mockJobTemplate = {
|
||||
allow_callbacks: false,
|
||||
@ -66,6 +76,7 @@ const mockJobTemplate = {
|
||||
},
|
||||
inventory: {
|
||||
id: 2,
|
||||
name: 'Demo Inventory',
|
||||
organization_id: 1,
|
||||
},
|
||||
credentials: [
|
||||
@ -195,22 +206,55 @@ describe('<JobTemplateEdit />', () => {
|
||||
JobTemplatesAPI.readCredentials.mockResolvedValue({
|
||||
data: mockRelatedCredentials,
|
||||
});
|
||||
ProjectsAPI.readPlaybooks.mockResolvedValue({
|
||||
data: mockRelatedProjectPlaybooks,
|
||||
JobTemplatesAPI.readInstanceGroups.mockReturnValue({
|
||||
data: { results: mockInstanceGroups },
|
||||
});
|
||||
|
||||
InventoriesAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [],
|
||||
count: 0,
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
|
||||
InstanceGroupsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [],
|
||||
count: 0,
|
||||
},
|
||||
});
|
||||
InstanceGroupsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
|
||||
ProjectsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [],
|
||||
count: 0,
|
||||
},
|
||||
});
|
||||
ProjectsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
ProjectsAPI.readPlaybooks.mockResolvedValue({
|
||||
data: mockRelatedProjectPlaybooks,
|
||||
});
|
||||
|
||||
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
||||
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [],
|
||||
count: 0,
|
||||
},
|
||||
});
|
||||
CredentialsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
|
||||
CredentialTypesAPI.loadAllTypes.mockResolvedValue([]);
|
||||
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
@ -219,18 +263,11 @@ describe('<JobTemplateEdit />', () => {
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
||||
JobTemplatesAPI.readCredentials.mockResolvedValue({
|
||||
data: mockRelatedCredentials,
|
||||
});
|
||||
JobTemplatesAPI.readInstanceGroups.mockReturnValue({
|
||||
data: { results: mockInstanceGroups },
|
||||
});
|
||||
ProjectsAPI.readDetail.mockReturnValue({
|
||||
id: 1,
|
||||
allow_override: true,
|
||||
name: 'foo',
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
|
||||
useDebounce.mockImplementation(fn => fn);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -262,7 +299,10 @@ describe('<JobTemplateEdit />', () => {
|
||||
const updatedTemplateData = {
|
||||
job_type: 'check',
|
||||
name: 'new name',
|
||||
inventory: 1,
|
||||
inventory: {
|
||||
id: 1,
|
||||
name: 'Other Inventory',
|
||||
},
|
||||
};
|
||||
const labels = [
|
||||
{ id: 3, name: 'Foo' },
|
||||
@ -280,20 +320,24 @@ describe('<JobTemplateEdit />', () => {
|
||||
null,
|
||||
'check'
|
||||
);
|
||||
wrapper.update();
|
||||
});
|
||||
wrapper.update();
|
||||
act(() => {
|
||||
wrapper.find('InventoryLookup').invoke('onChange')({
|
||||
id: 1,
|
||||
organization: 1,
|
||||
name: 'Other Inventory',
|
||||
});
|
||||
|
||||
wrapper.find('ExecutionEnvironmentLookup').invoke('onChange')(null);
|
||||
wrapper.update();
|
||||
wrapper.find('TextInput#execution-environments-input').invoke('onChange')(
|
||||
''
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
wrapper.find('input#template-name').simulate('change', {
|
||||
target: { value: 'new name', name: 'name' },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
wrapper.update();
|
||||
@ -301,8 +345,9 @@ describe('<JobTemplateEdit />', () => {
|
||||
|
||||
const expected = {
|
||||
...mockJobTemplate,
|
||||
project: mockJobTemplate.project,
|
||||
...updatedTemplateData,
|
||||
inventory: 1,
|
||||
project: 3,
|
||||
execution_environment: null,
|
||||
};
|
||||
delete expected.summary_fields;
|
||||
@ -372,6 +417,7 @@ describe('<JobTemplateEdit />', () => {
|
||||
},
|
||||
inventory: {
|
||||
id: 2,
|
||||
name: 'Demo Inventory',
|
||||
organization_id: 1,
|
||||
},
|
||||
credentials: [
|
||||
|
||||
@ -8,14 +8,22 @@ import {
|
||||
LabelsAPI,
|
||||
ExecutionEnvironmentsAPI,
|
||||
UsersAPI,
|
||||
InventoriesAPI,
|
||||
} from '../../../api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit';
|
||||
import useDebounce from '../../../util/useDebounce';
|
||||
|
||||
jest.mock('../../../api');
|
||||
jest.mock('../../../util/useDebounce');
|
||||
jest.mock('../../../api/models/WorkflowJobTemplates');
|
||||
jest.mock('../../../api/models/Organizations');
|
||||
jest.mock('../../../api/models/Labels');
|
||||
jest.mock('../../../api/models/ExecutionEnvironments');
|
||||
jest.mock('../../../api/models/Users');
|
||||
jest.mock('../../../api/models/Inventories');
|
||||
|
||||
const mockTemplate = {
|
||||
id: 6,
|
||||
@ -66,18 +74,40 @@ describe('<WorkflowJobTemplateEdit/>', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] });
|
||||
|
||||
InventoriesAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [],
|
||||
count: 0,
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
|
||||
OrganizationsAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1, name: 'Default' }], count: 1 },
|
||||
});
|
||||
OrganizationsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: mockExecutionEnvironment,
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
|
||||
UsersAPI.readAdminOfOrganizations.mockResolvedValue({
|
||||
data: { count: 1, results: [{ id: 1 }] },
|
||||
data: { count: 1, results: [{ id: 1, name: 'Default' }] },
|
||||
});
|
||||
|
||||
useDebounce.mockImplementation(fn => fn);
|
||||
|
||||
await act(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/templates/workflow_job_template/6/edit'],
|
||||
@ -120,7 +150,9 @@ describe('<WorkflowJobTemplateEdit/>', () => {
|
||||
.find('SelectToggle')
|
||||
.simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('ExecutionEnvironmentLookup').invoke('onChange')(null);
|
||||
wrapper.find('TextInput#execution-environments-input').invoke('onChange')(
|
||||
''
|
||||
);
|
||||
wrapper.find('input#wfjt-description').simulate('change', {
|
||||
target: { value: 'main', name: 'scm_branch' },
|
||||
});
|
||||
@ -130,6 +162,7 @@ describe('<WorkflowJobTemplateEdit/>', () => {
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'SelectOption button[aria-label="Label 3"]',
|
||||
|
||||
@ -54,14 +54,12 @@ function JobTemplateForm({
|
||||
handleCancel,
|
||||
handleSubmit,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
submitError,
|
||||
|
||||
validateField,
|
||||
isOverrideDisabledLookup,
|
||||
}) {
|
||||
const [contentError, setContentError] = useState(false);
|
||||
const [inventory, setInventory] = useState(
|
||||
template?.summary_fields?.inventory
|
||||
);
|
||||
const [allowCallbacks, setAllowCallbacks] = useState(
|
||||
Boolean(template?.host_config_key)
|
||||
);
|
||||
@ -75,15 +73,14 @@ function JobTemplateForm({
|
||||
name: 'job_type',
|
||||
validate: required(null),
|
||||
});
|
||||
const [, inventoryMeta, inventoryHelpers] = useField('inventory');
|
||||
const [projectField, projectMeta, projectHelpers] = useField({
|
||||
name: 'project',
|
||||
validate: project => handleProjectValidation(project),
|
||||
});
|
||||
const [inventoryField, inventoryMeta, inventoryHelpers] = useField(
|
||||
'inventory'
|
||||
);
|
||||
const [projectField, projectMeta, projectHelpers] = useField('project');
|
||||
const [scmField, , scmHelpers] = useField('scm_branch');
|
||||
const [playbookField, playbookMeta, playbookHelpers] = useField({
|
||||
name: 'playbook',
|
||||
validate: required(t`Select a value for this field`),
|
||||
validate: required(null),
|
||||
});
|
||||
const [credentialField, , credentialHelpers] = useField('credentials');
|
||||
const [labelsField, , labelsHelpers] = useField('labels');
|
||||
@ -109,7 +106,7 @@ function JobTemplateForm({
|
||||
executionEnvironmentField,
|
||||
executionEnvironmentMeta,
|
||||
executionEnvironmentHelpers,
|
||||
] = useField({ name: 'execution_environment' });
|
||||
] = useField('execution_environment');
|
||||
|
||||
const {
|
||||
request: loadRelatedInstanceGroups,
|
||||
@ -149,24 +146,52 @@ function JobTemplateForm({
|
||||
}, [enableWebhooks]);
|
||||
|
||||
const handleProjectValidation = project => {
|
||||
if (!project && projectMeta.touched) {
|
||||
return t`Select a value for this field`;
|
||||
if (!project) {
|
||||
return t`This field must not be blank`;
|
||||
}
|
||||
if (project?.value?.status === 'never updated') {
|
||||
return t`This project needs to be updated`;
|
||||
if (project?.status === 'never updated') {
|
||||
return t`This Project needs to be updated`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const handleProjectUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('playbook', '');
|
||||
setFieldValue('scm_branch', '');
|
||||
setFieldValue('project', value);
|
||||
setFieldValue('playbook', '', false);
|
||||
setFieldValue('scm_branch', '', false);
|
||||
setFieldTouched('project', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const handleInventoryValidation = inventory => {
|
||||
if (!inventory && !askInventoryOnLaunchField.value) {
|
||||
return t`Please select an Inventory or check the Prompt on Launch option`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const handleInventoryUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('inventory', value);
|
||||
setFieldTouched('inventory', true, false);
|
||||
},
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const handleExecutionEnvironmentUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('execution_environment', value);
|
||||
setFieldTouched('execution_environment', true, false);
|
||||
},
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
validateField('inventory');
|
||||
}, [askInventoryOnLaunchField.value, validateField]);
|
||||
|
||||
const jobTypeOptions = [
|
||||
{
|
||||
value: '',
|
||||
@ -254,21 +279,19 @@ function JobTemplateForm({
|
||||
>
|
||||
<InventoryLookup
|
||||
fieldId="template-inventory"
|
||||
value={inventory}
|
||||
value={inventoryField.value}
|
||||
promptId="template-ask-inventory-on-launch"
|
||||
promptName="ask_inventory_on_launch"
|
||||
isPromptableField
|
||||
tooltip={t`Select the inventory containing the hosts
|
||||
you want this job to manage.`}
|
||||
onBlur={() => inventoryHelpers.setTouched()}
|
||||
onChange={value => {
|
||||
inventoryHelpers.setValue(value ? value.id : null);
|
||||
setInventory(value);
|
||||
}}
|
||||
onChange={handleInventoryUpdate}
|
||||
required={!askInventoryOnLaunchField.value}
|
||||
touched={inventoryMeta.touched}
|
||||
error={inventoryMeta.error}
|
||||
isOverrideDisabled={isOverrideDisabledLookup}
|
||||
validate={handleInventoryValidation}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@ -277,14 +300,15 @@ function JobTemplateForm({
|
||||
onBlur={() => projectHelpers.setTouched()}
|
||||
tooltip={t`Select the project containing the playbook
|
||||
you want this job to execute.`}
|
||||
isValid={
|
||||
!projectMeta.touched || !projectMeta.error || projectField.value
|
||||
}
|
||||
isValid={Boolean(
|
||||
!projectMeta.touched || (!projectMeta.error && projectField.value)
|
||||
)}
|
||||
helperTextInvalid={projectMeta.error}
|
||||
onChange={handleProjectUpdate}
|
||||
required
|
||||
autoPopulate={!template?.id}
|
||||
isOverrideDisabled={isOverrideDisabledLookup}
|
||||
validate={handleProjectValidation}
|
||||
/>
|
||||
|
||||
<ExecutionEnvironmentLookup
|
||||
@ -294,7 +318,7 @@ function JobTemplateForm({
|
||||
}
|
||||
onBlur={() => executionEnvironmentHelpers.setTouched()}
|
||||
value={executionEnvironmentField.value}
|
||||
onChange={value => executionEnvironmentHelpers.setValue(value)}
|
||||
onChange={handleExecutionEnvironmentUpdate}
|
||||
popoverContent={t`Select the execution environment for this job template.`}
|
||||
tooltip={t`Select a project before editing the execution environment.`}
|
||||
globallyAvailable
|
||||
@ -489,6 +513,7 @@ function JobTemplateForm({
|
||||
onChange={value => instanceGroupsHelpers.setValue(value)}
|
||||
tooltip={t`Select the Instance Groups for this Organization
|
||||
to run on.`}
|
||||
fieldName="instanceGroups"
|
||||
/>
|
||||
<FieldWithPrompt
|
||||
fieldId="template-tags"
|
||||
@ -679,7 +704,7 @@ const FormikApp = withFormik({
|
||||
const {
|
||||
summary_fields = {
|
||||
labels: { results: [] },
|
||||
inventory: { organization: null },
|
||||
inventory: null,
|
||||
},
|
||||
} = template;
|
||||
|
||||
@ -705,7 +730,7 @@ const FormikApp = withFormik({
|
||||
host_config_key: template.host_config_key || '',
|
||||
initialInstanceGroups: [],
|
||||
instanceGroups: [],
|
||||
inventory: template.inventory || null,
|
||||
inventory: summary_fields?.inventory || null,
|
||||
job_slice_count: template.job_slice_count || 1,
|
||||
job_tags: template.job_tags || '',
|
||||
job_type: template.job_type || 'run',
|
||||
@ -738,18 +763,6 @@ const FormikApp = withFormik({
|
||||
setErrors(errors);
|
||||
}
|
||||
},
|
||||
validate: values => {
|
||||
const errors = {};
|
||||
|
||||
if (
|
||||
(!values.inventory || values.inventory === '') &&
|
||||
!values.ask_inventory_on_launch
|
||||
) {
|
||||
errors.inventory = t`Please select an Inventory or check the Prompt on Launch option.`;
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
})(JobTemplateForm);
|
||||
|
||||
export { JobTemplateForm as _JobTemplateForm };
|
||||
|
||||
@ -213,6 +213,7 @@ function WebhookSubForm({ templateType }) {
|
||||
isValid={!webhookCredentialMeta.error}
|
||||
helperTextInvalid={webhookCredentialMeta.error}
|
||||
value={webhookCredentialField.value}
|
||||
fieldName="webhook_credential"
|
||||
/>
|
||||
)}
|
||||
</FormColumnLayout>
|
||||
|
||||
@ -69,12 +69,9 @@ describe('<WebhookSubForm />', () => {
|
||||
.find('TextInputBase[aria-label="workflow job template webhook key"]')
|
||||
.prop('value')
|
||||
).toBe('webhook key');
|
||||
expect(
|
||||
wrapper
|
||||
.find('Chip')
|
||||
.find('span')
|
||||
.text()
|
||||
).toBe('Github credential');
|
||||
expect(wrapper.find('input#credential-input').prop('value')).toBe(
|
||||
'Github credential'
|
||||
);
|
||||
});
|
||||
test('should make other credential type available', async () => {
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import PropTypes, { shape } from 'prop-types';
|
||||
|
||||
import { useField, useFormikContext, withFormik } from 'formik';
|
||||
import {
|
||||
Form,
|
||||
@ -11,7 +10,6 @@ import {
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
import { required } from '../../../util/validators';
|
||||
|
||||
import FieldWithPrompt from '../../../components/FieldWithPrompt';
|
||||
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||
import {
|
||||
@ -43,7 +41,7 @@ function WorkflowJobTemplateForm({
|
||||
submitError,
|
||||
isOrgAdmin,
|
||||
}) {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [enableWebhooks, setEnableWebhooks] = useState(
|
||||
Boolean(template.webhook_service)
|
||||
);
|
||||
@ -71,9 +69,7 @@ function WorkflowJobTemplateForm({
|
||||
executionEnvironmentField,
|
||||
executionEnvironmentMeta,
|
||||
executionEnvironmentHelpers,
|
||||
] = useField({
|
||||
name: 'execution_environment',
|
||||
});
|
||||
] = useField('execution_environment');
|
||||
|
||||
useEffect(() => {
|
||||
if (enableWebhooks) {
|
||||
@ -90,11 +86,28 @@ function WorkflowJobTemplateForm({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [enableWebhooks]);
|
||||
|
||||
const onOrganizationChange = useCallback(
|
||||
const handleOrganizationChange = useCallback(
|
||||
value => {
|
||||
setFieldValue('organization', value);
|
||||
setFieldTouched('organization', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const handleInventoryUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('inventory', value);
|
||||
setFieldTouched('inventory', true, false);
|
||||
},
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
const handleExecutionEnvironmentUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('execution_environment', value);
|
||||
setFieldTouched('execution_environment', true, false);
|
||||
},
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
if (hasContentError) {
|
||||
@ -122,14 +135,26 @@ function WorkflowJobTemplateForm({
|
||||
helperTextInvalid={organizationMeta.error}
|
||||
isValid={!organizationMeta.touched || !organizationMeta.error}
|
||||
onBlur={() => organizationHelpers.setTouched()}
|
||||
onChange={onOrganizationChange}
|
||||
onChange={handleOrganizationChange}
|
||||
value={organizationField.value}
|
||||
touched={organizationMeta.touched}
|
||||
error={organizationMeta.error}
|
||||
required={isOrgAdmin}
|
||||
autoPopulate={isOrgAdmin}
|
||||
validate={
|
||||
isOrgAdmin ? required(t`Select a value for this field`) : undefined
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<FormGroup
|
||||
fieldId="inventory-lookup"
|
||||
validated={
|
||||
!(inventoryMeta.touched || askInventoryOnLaunchField.value) ||
|
||||
!inventoryMeta.error
|
||||
? 'default'
|
||||
: 'error'
|
||||
}
|
||||
helperTextInvalid={inventoryMeta.error}
|
||||
>
|
||||
<InventoryLookup
|
||||
promptId="wfjt-ask-inventory-on-launch"
|
||||
promptName="ask_inventory_on_launch"
|
||||
@ -138,22 +163,11 @@ function WorkflowJobTemplateForm({
|
||||
isPromptableField
|
||||
value={inventoryField.value}
|
||||
onBlur={() => inventoryHelpers.setTouched()}
|
||||
onChange={value => {
|
||||
inventoryHelpers.setValue(value);
|
||||
}}
|
||||
onChange={handleInventoryUpdate}
|
||||
touched={inventoryMeta.touched}
|
||||
error={inventoryMeta.error}
|
||||
/>
|
||||
{(inventoryMeta.touched || askInventoryOnLaunchField.value) &&
|
||||
inventoryMeta.error && (
|
||||
<div
|
||||
className="pf-c-form__helper-text pf-m-error"
|
||||
aria-live="polite"
|
||||
>
|
||||
{inventoryMeta.error}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</FormGroup>
|
||||
<FieldWithPrompt
|
||||
fieldId="wfjt-limit"
|
||||
label={t`Limit`}
|
||||
@ -199,7 +213,7 @@ function WorkflowJobTemplateForm({
|
||||
}
|
||||
onBlur={() => executionEnvironmentHelpers.setTouched()}
|
||||
value={executionEnvironmentField.value}
|
||||
onChange={value => executionEnvironmentHelpers.setValue(value)}
|
||||
onChange={handleExecutionEnvironmentUpdate}
|
||||
tooltip={t`Select the default execution environment for this organization to run on.`}
|
||||
globallyAvailable
|
||||
organizationId={organizationField.value?.id}
|
||||
|
||||
@ -67,6 +67,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
|
||||
{ name: 'Label 2', id: 2 },
|
||||
{ name: 'Label 3', id: 3 },
|
||||
],
|
||||
count: 3,
|
||||
},
|
||||
});
|
||||
OrganizationsAPI.read.mockResolvedValue({
|
||||
@ -75,16 +76,20 @@ describe('<WorkflowJobTemplateForm/>', () => {
|
||||
{ id: 1, name: 'Organization 1' },
|
||||
{ id: 2, name: 'Organization 2' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
InventoriesAPI.read.mockResolvedValue({
|
||||
results: [
|
||||
{ id: 1, name: 'Foo' },
|
||||
{ id: 2, name: 'Bar' },
|
||||
],
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'Foo' },
|
||||
{ id: 2, name: 'Bar' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1 }] },
|
||||
data: { results: [{ id: 1 }], count: 1 },
|
||||
});
|
||||
InventoriesAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
@ -93,13 +98,13 @@ describe('<WorkflowJobTemplateForm/>', () => {
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: { results: [] },
|
||||
data: { results: [], count: 0 },
|
||||
});
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: { results: [] },
|
||||
data: { results: [], count: 0 },
|
||||
});
|
||||
CredentialsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
|
||||
@ -15,7 +15,7 @@ function UserAdd() {
|
||||
try {
|
||||
const {
|
||||
data: { id },
|
||||
} = await OrganizationsAPI.createUser(organization, userValues);
|
||||
} = await OrganizationsAPI.createUser(organization.id, userValues);
|
||||
history.push(`/users/${id}/details`);
|
||||
} catch (error) {
|
||||
setFormSubmitError(error);
|
||||
|
||||
@ -23,7 +23,10 @@ describe('<UserAdd />', () => {
|
||||
first_name: 'System',
|
||||
last_name: 'Administrator',
|
||||
password: 'password',
|
||||
organization: 1,
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
},
|
||||
is_superuser: true,
|
||||
is_system_auditor: false,
|
||||
};
|
||||
@ -33,7 +36,7 @@ describe('<UserAdd />', () => {
|
||||
|
||||
const { organization, ...userData } = updatedUserData;
|
||||
expect(OrganizationsAPI.createUser.mock.calls).toEqual([
|
||||
[organization, userData],
|
||||
[organization.id, userData],
|
||||
]);
|
||||
});
|
||||
|
||||
@ -58,7 +61,10 @@ describe('<UserAdd />', () => {
|
||||
first_name: 'System',
|
||||
last_name: 'Administrator',
|
||||
password: 'password',
|
||||
organization: 1,
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
},
|
||||
is_superuser: true,
|
||||
is_system_auditor: false,
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ function UserEdit({ user }) {
|
||||
const handleSubmit = async values => {
|
||||
setFormSubmitError(null);
|
||||
try {
|
||||
delete values.organization;
|
||||
await UsersAPI.update(user.id, values);
|
||||
history.push(`/users/${user.id}/details`);
|
||||
} catch (error) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
@ -15,8 +15,7 @@ import { required } from '../../../util/validators';
|
||||
import { FormColumnLayout } from '../../../components/FormLayout';
|
||||
|
||||
function UserFormFields({ user }) {
|
||||
const [organization, setOrganization] = useState(null);
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
|
||||
const ldapUser = user.ldap_dn;
|
||||
const socialAuthUser = user.auth?.length > 0;
|
||||
@ -43,21 +42,18 @@ function UserFormFields({ user }) {
|
||||
},
|
||||
];
|
||||
|
||||
const [, organizationMeta, organizationHelpers] = useField({
|
||||
name: 'organization',
|
||||
validate: !user.id
|
||||
? required(t`Select a value for this field`)
|
||||
: () => undefined,
|
||||
});
|
||||
const [organizationField, organizationMeta, organizationHelpers] = useField(
|
||||
'organization'
|
||||
);
|
||||
|
||||
const [userTypeField, userTypeMeta] = useField('user_type');
|
||||
|
||||
const onOrganizationChange = useCallback(
|
||||
const handleOrganizationUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('organization', value.id);
|
||||
setOrganization(value);
|
||||
setFieldValue('organization', value);
|
||||
setFieldTouched('organization', true, false);
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -116,10 +112,11 @@ function UserFormFields({ user }) {
|
||||
helperTextInvalid={organizationMeta.error}
|
||||
isValid={!organizationMeta.touched || !organizationMeta.error}
|
||||
onBlur={() => organizationHelpers.setTouched()}
|
||||
onChange={onOrganizationChange}
|
||||
value={organization}
|
||||
onChange={handleOrganizationUpdate}
|
||||
value={organizationField.value}
|
||||
required
|
||||
autoPopulate={!user?.id}
|
||||
validate={required(t`Select a value for this field`)}
|
||||
/>
|
||||
)}
|
||||
<FormGroup
|
||||
@ -173,7 +170,7 @@ function UserForm({ user, handleCancel, handleSubmit, submitError }) {
|
||||
initialValues={{
|
||||
first_name: user.first_name || '',
|
||||
last_name: user.last_name || '',
|
||||
organization: user.organization || '',
|
||||
organization: null,
|
||||
email: user.email || '',
|
||||
username: user.username || '',
|
||||
password: '',
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Formik, useField } from 'formik';
|
||||
import { Formik, useField, useFormikContext } from 'formik';
|
||||
import { Form, FormGroup } from '@patternfly/react-core';
|
||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
||||
@ -9,19 +8,25 @@ import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||
import ApplicationLookup from '../../../components/Lookup/ApplicationLookup';
|
||||
import Popover from '../../../components/Popover';
|
||||
import { required } from '../../../util/validators';
|
||||
|
||||
import { FormColumnLayout } from '../../../components/FormLayout';
|
||||
|
||||
function UserTokenFormFields() {
|
||||
const [applicationField, applicationMeta, applicationHelpers] = useField(
|
||||
'application'
|
||||
);
|
||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||
const [applicationField, applicationMeta] = useField('application');
|
||||
|
||||
const [scopeField, scopeMeta, scopeHelpers] = useField({
|
||||
name: 'scope',
|
||||
validate: required(t`Please enter a value.`),
|
||||
});
|
||||
|
||||
const handleApplicationUpdate = useCallback(
|
||||
value => {
|
||||
setFieldValue('application', value);
|
||||
setFieldTouched('application', true, false);
|
||||
},
|
||||
[setFieldValue, setFieldTouched]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup
|
||||
@ -36,9 +41,7 @@ function UserTokenFormFields() {
|
||||
>
|
||||
<ApplicationLookup
|
||||
value={applicationField.value}
|
||||
onChange={value => {
|
||||
applicationHelpers.setValue(value);
|
||||
}}
|
||||
onChange={handleApplicationUpdate}
|
||||
label={
|
||||
<span>
|
||||
{t`Application`}
|
||||
@ -89,7 +92,6 @@ function UserTokenForm({
|
||||
handleCancel,
|
||||
handleSubmit,
|
||||
submitError,
|
||||
|
||||
token = {},
|
||||
}) {
|
||||
return (
|
||||
|
||||
16
awx/ui_next/src/util/useInterval.js
Normal file
16
awx/ui_next/src/util/useInterval.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function useInterval(callback, delay) {
|
||||
const savedCallbackRef = useRef();
|
||||
useEffect(() => {
|
||||
savedCallbackRef.current = callback;
|
||||
}, [callback]);
|
||||
useEffect(() => {
|
||||
const handler = (...args) => savedCallbackRef.current(...args);
|
||||
if (delay !== null) {
|
||||
const intervalId = setInterval(handler, delay);
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
return () => undefined;
|
||||
}, [delay]);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user