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