mirror of
https://github.com/ansible/awx.git
synced 2026-01-17 04:31:21 -03:30
Merge pull request #6662 from keithjgrant/5909-jt-launch-prompt-2
JT Launch Prompting (phase 2) Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
516a44ce73
@ -5,6 +5,28 @@ class CredentialTypes extends Base {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/credential_types/';
|
||||
}
|
||||
|
||||
async loadAllTypes(
|
||||
acceptableKinds = ['machine', 'cloud', 'net', 'ssh', 'vault']
|
||||
) {
|
||||
const pageSize = 200;
|
||||
// The number of credential types a user can have is unlimited. In practice, it is unlikely for
|
||||
// users to have more than a page at the maximum request size.
|
||||
const {
|
||||
data: { next, results },
|
||||
} = await this.read({ page_size: pageSize });
|
||||
let nextResults = [];
|
||||
if (next) {
|
||||
const { data } = await this.read({
|
||||
page_size: pageSize,
|
||||
page: 2,
|
||||
});
|
||||
nextResults = data.results;
|
||||
}
|
||||
return results
|
||||
.concat(nextResults)
|
||||
.filter(type => acceptableKinds.includes(type.kind));
|
||||
}
|
||||
}
|
||||
|
||||
export default CredentialTypes;
|
||||
|
||||
65
awx/ui_next/src/api/models/CredentialTypes.test.js
Normal file
65
awx/ui_next/src/api/models/CredentialTypes.test.js
Normal file
@ -0,0 +1,65 @@
|
||||
import CredentialTypes from './CredentialTypes';
|
||||
|
||||
const typesData = [{ id: 1, kind: 'machine' }, { id: 2, kind: 'cloud' }];
|
||||
|
||||
describe('CredentialTypesAPI', () => {
|
||||
test('should load all types', async () => {
|
||||
const getPromise = () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
results: typesData,
|
||||
},
|
||||
});
|
||||
const mockHttp = { get: jest.fn(getPromise) };
|
||||
const CredentialTypesAPI = new CredentialTypes(mockHttp);
|
||||
|
||||
const types = await CredentialTypesAPI.loadAllTypes();
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.get.mock.calls[0]).toEqual([
|
||||
`/api/v2/credential_types/`,
|
||||
{ params: { page_size: 200 } },
|
||||
]);
|
||||
expect(types).toEqual(typesData);
|
||||
});
|
||||
|
||||
test('should load all types (2 pages)', async () => {
|
||||
const getPromise = () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
results: typesData,
|
||||
next: 2,
|
||||
},
|
||||
});
|
||||
const mockHttp = { get: jest.fn(getPromise) };
|
||||
const CredentialTypesAPI = new CredentialTypes(mockHttp);
|
||||
|
||||
const types = await CredentialTypesAPI.loadAllTypes();
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(2);
|
||||
expect(mockHttp.get.mock.calls[0]).toEqual([
|
||||
`/api/v2/credential_types/`,
|
||||
{ params: { page_size: 200 } },
|
||||
]);
|
||||
expect(mockHttp.get.mock.calls[1]).toEqual([
|
||||
`/api/v2/credential_types/`,
|
||||
{ params: { page_size: 200, page: 2 } },
|
||||
]);
|
||||
expect(types).toHaveLength(4);
|
||||
});
|
||||
|
||||
test('should filter by acceptable kinds', async () => {
|
||||
const getPromise = () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
results: typesData,
|
||||
},
|
||||
});
|
||||
const mockHttp = { get: jest.fn(getPromise) };
|
||||
const CredentialTypesAPI = new CredentialTypes(mockHttp);
|
||||
|
||||
const types = await CredentialTypesAPI.loadAllTypes(['machine']);
|
||||
|
||||
expect(types).toEqual([typesData[0]]);
|
||||
});
|
||||
});
|
||||
@ -88,8 +88,8 @@ class LaunchButton extends React.Component {
|
||||
const { history, resource } = this.props;
|
||||
const jobPromise =
|
||||
resource.type === 'workflow_job_template'
|
||||
? WorkflowJobTemplatesAPI.launch(resource.id, params)
|
||||
: JobTemplatesAPI.launch(resource.id, params);
|
||||
? WorkflowJobTemplatesAPI.launch(resource.id, params || {})
|
||||
: JobTemplatesAPI.launch(resource.id, params || {});
|
||||
|
||||
const { data: job } = await jobPromise;
|
||||
history.push(
|
||||
|
||||
@ -1,7 +1,171 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import { ToolbarItem } from '@patternfly/react-core';
|
||||
import { CredentialsAPI, CredentialTypesAPI } from '@api';
|
||||
import AnsibleSelect from '@components/AnsibleSelect';
|
||||
import OptionsList from '@components/OptionsList';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import CredentialChip from '@components/CredentialChip';
|
||||
import ContentError from '@components/ContentError';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import useRequest from '@util/useRequest';
|
||||
|
||||
function CredentialsStep() {
|
||||
return <div />;
|
||||
const QS_CONFIG = getQSConfig('inventory', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function CredentialsStep({ i18n }) {
|
||||
const [field, , helpers] = useField('credentials');
|
||||
const [selectedType, setSelectedType] = useState(null);
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
result: types,
|
||||
error: typesError,
|
||||
isLoading: isTypesLoading,
|
||||
request: fetchTypes,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const loadedTypes = await CredentialTypesAPI.loadAllTypes();
|
||||
if (loadedTypes.length) {
|
||||
const match =
|
||||
loadedTypes.find(type => type.kind === 'ssh') || loadedTypes[0];
|
||||
setSelectedType(match);
|
||||
}
|
||||
return loadedTypes;
|
||||
}, []),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTypes();
|
||||
}, [fetchTypes]);
|
||||
|
||||
const {
|
||||
result: { credentials, count },
|
||||
error: credentialsError,
|
||||
isLoading: isCredentialsLoading,
|
||||
request: fetchCredentials,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
if (!selectedType) {
|
||||
return { credentials: [], count: 0 };
|
||||
}
|
||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||
const { data } = await CredentialsAPI.read({
|
||||
...params,
|
||||
credential_type: selectedType.id,
|
||||
});
|
||||
return {
|
||||
credentials: data.results,
|
||||
count: data.count,
|
||||
};
|
||||
}, [selectedType, history.location.search]),
|
||||
{ credentials: [], count: 0 }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCredentials();
|
||||
}, [fetchCredentials]);
|
||||
|
||||
if (isTypesLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (typesError || credentialsError) {
|
||||
return <ContentError error={typesError || credentialsError} />;
|
||||
}
|
||||
|
||||
const isVault = selectedType?.kind === 'vault';
|
||||
|
||||
const renderChip = ({ item, removeItem, canDelete }) => (
|
||||
<CredentialChip
|
||||
key={item.id}
|
||||
onClick={() => removeItem(item)}
|
||||
isReadOnly={!canDelete}
|
||||
credential={item}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{types && types.length > 0 && (
|
||||
<ToolbarItem css=" display: flex; align-items: center;">
|
||||
<div css="flex: 0 0 25%; margin-right: 32px">
|
||||
{i18n._(t`Selected Category`)}
|
||||
</div>
|
||||
<AnsibleSelect
|
||||
css="flex: 1 1 75%;"
|
||||
id="multiCredentialsLookUp-select"
|
||||
label={i18n._(t`Selected Category`)}
|
||||
data={types.map(type => ({
|
||||
key: type.id,
|
||||
value: type.id,
|
||||
label: type.name,
|
||||
isDisabled: false,
|
||||
}))}
|
||||
value={selectedType && selectedType.id}
|
||||
onChange={(e, id) => {
|
||||
setSelectedType(types.find(o => o.id === parseInt(id, 10)));
|
||||
}}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
)}
|
||||
{!isCredentialsLoading && (
|
||||
<OptionsList
|
||||
value={field.value || []}
|
||||
options={credentials}
|
||||
optionCount={count}
|
||||
searchColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
]}
|
||||
sortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
multiple={isVault}
|
||||
header={i18n._(t`Credentials`)}
|
||||
name="credentials"
|
||||
qsConfig={QS_CONFIG}
|
||||
readOnly={false}
|
||||
selectItem={item => {
|
||||
const hasSameVaultID = val =>
|
||||
val?.inputs?.vault_id !== undefined &&
|
||||
val?.inputs?.vault_id === item?.inputs?.vault_id;
|
||||
const hasSameKind = val => val.kind === item.kind;
|
||||
const newItems = field.value.filter(i =>
|
||||
isVault ? !hasSameVaultID(i) : !hasSameKind(i)
|
||||
);
|
||||
newItems.push(item);
|
||||
helpers.setValue(newItems);
|
||||
}}
|
||||
deselectItem={item => {
|
||||
helpers.setValue(field.value.filter(i => i.id !== item.id));
|
||||
}}
|
||||
renderItemChip={renderChip}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CredentialsStep;
|
||||
export default withI18n()(CredentialsStep);
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import CredentialsStep from './CredentialsStep';
|
||||
import { CredentialsAPI, CredentialTypesAPI } from '@api';
|
||||
|
||||
jest.mock('@api/models/CredentialTypes');
|
||||
jest.mock('@api/models/Credentials');
|
||||
|
||||
const types = [
|
||||
{ id: 1, kind: 'ssh', name: 'SSH' },
|
||||
{ id: 2, kind: 'cloud', name: 'Ansible Tower' },
|
||||
{ id: 3, kind: 'vault', name: 'Vault' },
|
||||
];
|
||||
|
||||
const credentials = [
|
||||
{ id: 1, kind: 'cloud', name: 'Cred 1', url: 'www.google.com' },
|
||||
{ id: 2, kind: 'ssh', name: 'Cred 2', url: 'www.google.com' },
|
||||
{ id: 3, kind: 'Ansible', name: 'Cred 3', url: 'www.google.com' },
|
||||
{ id: 4, kind: 'Machine', name: 'Cred 4', url: 'www.google.com' },
|
||||
{ id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' },
|
||||
];
|
||||
|
||||
describe('CredentialsStep', () => {
|
||||
beforeEach(() => {
|
||||
CredentialTypesAPI.loadAllTypes.mockResolvedValue(types);
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: credentials,
|
||||
count: 5,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should load credentials', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<CredentialsStep />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(CredentialsAPI.read).toHaveBeenCalled();
|
||||
expect(wrapper.find('OptionsList').prop('options')).toEqual(credentials);
|
||||
});
|
||||
|
||||
test('should load credentials for selected type', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<CredentialsStep />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||
credential_type: 1,
|
||||
order_by: 'name',
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect').invoke('onChange')({}, 2);
|
||||
});
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||
credential_type: 2,
|
||||
order_by: 'name',
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -8,6 +8,7 @@ import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import useRequest from '@util/useRequest';
|
||||
import OptionsList from '@components/OptionsList';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import ContentError from '@components/ContentError';
|
||||
|
||||
const QS_CONFIG = getQSConfig('inventory', {
|
||||
page: 1,
|
||||
@ -16,12 +17,12 @@ const QS_CONFIG = getQSConfig('inventory', {
|
||||
});
|
||||
|
||||
function InventoryStep({ i18n }) {
|
||||
const history = useHistory();
|
||||
const [field, , helpers] = useField('inventory');
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
// error,
|
||||
error,
|
||||
result: { inventories, count },
|
||||
request: fetchInventories,
|
||||
} = useRequest(
|
||||
@ -46,6 +47,9 @@ function InventoryStep({ i18n }) {
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
if (error) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionsList
|
||||
|
||||
@ -19,30 +19,49 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
||||
component: <InventoryStep />,
|
||||
});
|
||||
}
|
||||
// TODO: match old UI Logic:
|
||||
// if (vm.promptDataClone.launchConf.ask_credential_on_launch ||
|
||||
// (_.has(vm, 'promptDataClone.prompts.credentials.passwords.vault') &&
|
||||
// vm.promptDataClone.prompts.credentials.passwords.vault.length > 0) ||
|
||||
// _.has(vm, 'promptDataClone.prompts.credentials.passwords.ssh_key_unlock') ||
|
||||
// _.has(vm, 'promptDataClone.prompts.credentials.passwords.become_password') ||
|
||||
// _.has(vm, 'promptDataClone.prompts.credentials.passwords.ssh_password')
|
||||
// ) {
|
||||
if (config.ask_credential_on_launch) {
|
||||
initialValues.credentials = resource?.summary_fields?.credentials || [];
|
||||
steps.push({
|
||||
name: i18n._(t`Credential`),
|
||||
name: i18n._(t`Credentials`),
|
||||
component: <CredentialsStep />,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Add Credential Passwords step
|
||||
|
||||
if (config.ask_job_type_on_launch) {
|
||||
initialValues.job_type = resource.job_type || '';
|
||||
}
|
||||
if (config.ask_limit_on_launch) {
|
||||
initialValues.limit = resource.limit || '';
|
||||
}
|
||||
if (config.ask_verbosity_on_launch) {
|
||||
initialValues.verbosity = resource.verbosity || 0;
|
||||
}
|
||||
if (config.ask_tags_on_launch) {
|
||||
initialValues.job_tags = resource.job_tags || '';
|
||||
}
|
||||
if (config.ask_skip_tags_on_launch) {
|
||||
initialValues.skip_tags = resource.skip_tags || '';
|
||||
}
|
||||
if (config.ask_variables_on_launch) {
|
||||
initialValues.extra_vars = resource.extra_vars || '---';
|
||||
}
|
||||
if (config.ask_scm_branch_on_launch) {
|
||||
initialValues.scm_branch = resource.scm_branch || '';
|
||||
}
|
||||
if (config.ask_diff_mode_on_launch) {
|
||||
initialValues.diff_mode = resource.diff_mode || false;
|
||||
}
|
||||
if (
|
||||
config.ask_scm_branch_on_launch ||
|
||||
(config.ask_variables_on_launch && !config.ignore_ask_variables) ||
|
||||
config.ask_tags_on_launch ||
|
||||
config.ask_diff_mode_on_launch ||
|
||||
config.ask_skip_tags_on_launch ||
|
||||
config.ask_job_type_on_launch ||
|
||||
config.ask_limit_on_launch ||
|
||||
config.ask_verbosity_on_launch
|
||||
config.ask_verbosity_on_launch ||
|
||||
config.ask_tags_on_launch ||
|
||||
config.ask_skip_tags_on_launch ||
|
||||
config.ask_variables_on_launch ||
|
||||
config.ask_scm_branch_on_launch ||
|
||||
config.ask_diff_mode_on_launch
|
||||
) {
|
||||
steps.push({
|
||||
name: i18n._(t`Other Prompts`),
|
||||
@ -63,9 +82,18 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
||||
|
||||
const submit = values => {
|
||||
const postValues = {};
|
||||
if (values.inventory) {
|
||||
postValues.inventory_id = values.inventory.id;
|
||||
}
|
||||
const setValue = (key, value) => {
|
||||
if (typeof value !== 'undefined' && value !== null) {
|
||||
postValues[key] = value;
|
||||
}
|
||||
};
|
||||
setValue('inventory_id', values.inventory?.id);
|
||||
setValue('credentials', values.credentials?.map(c => c.id));
|
||||
setValue('job_type', values.job_type);
|
||||
setValue('limit', values.limit);
|
||||
setValue('job_tags', values.job_tags);
|
||||
setValue('skip_tags', values.skip_tags);
|
||||
setValue('extra_vars', values.extra_vars);
|
||||
onLaunch(postValues);
|
||||
};
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ import { act, isElementOfType } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import LaunchPrompt from './LaunchPrompt';
|
||||
import InventoryStep from './InventoryStep';
|
||||
import CredentialsStep from './CredentialsStep';
|
||||
import OtherPromptsStep from './OtherPromptsStep';
|
||||
import PreviewStep from './PreviewStep';
|
||||
import { InventoriesAPI } from '@api';
|
||||
|
||||
@ -69,7 +71,7 @@ describe('LaunchPrompt', () => {
|
||||
|
||||
expect(steps).toHaveLength(5);
|
||||
expect(steps[0].name).toEqual('Inventory');
|
||||
expect(steps[1].name).toEqual('Credential');
|
||||
expect(steps[1].name).toEqual('Credentials');
|
||||
expect(steps[2].name).toEqual('Other Prompts');
|
||||
expect(steps[3].name).toEqual('Survey');
|
||||
expect(steps[4].name).toEqual('Preview');
|
||||
@ -97,4 +99,50 @@ describe('LaunchPrompt', () => {
|
||||
expect(isElementOfType(steps[0].component, InventoryStep)).toEqual(true);
|
||||
expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true);
|
||||
});
|
||||
|
||||
test('should add credentials step', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<LaunchPrompt
|
||||
config={{
|
||||
...config,
|
||||
ask_credential_on_launch: true,
|
||||
}}
|
||||
resource={resource}
|
||||
onLaunch={noop}
|
||||
onCancel={noop}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const steps = wrapper.find('Wizard').prop('steps');
|
||||
|
||||
expect(steps).toHaveLength(2);
|
||||
expect(steps[0].name).toEqual('Credentials');
|
||||
expect(isElementOfType(steps[0].component, CredentialsStep)).toEqual(true);
|
||||
expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true);
|
||||
});
|
||||
|
||||
test('should add other prompts step', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<LaunchPrompt
|
||||
config={{
|
||||
...config,
|
||||
ask_verbosity_on_launch: true,
|
||||
}}
|
||||
resource={resource}
|
||||
onLaunch={noop}
|
||||
onCancel={noop}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const steps = wrapper.find('Wizard').prop('steps');
|
||||
|
||||
expect(steps).toHaveLength(2);
|
||||
expect(steps[0].name).toEqual('Other Prompts');
|
||||
expect(isElementOfType(steps[0].component, OtherPromptsStep)).toEqual(true);
|
||||
expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,7 +1,180 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import { Form, FormGroup, Switch } from '@patternfly/react-core';
|
||||
import FormField, { FieldTooltip } from '@components/FormField';
|
||||
import { TagMultiSelect } from '@components/MultiSelect';
|
||||
import AnsibleSelect from '@components/AnsibleSelect';
|
||||
import { VariablesField } from '@components/CodeMirrorInput';
|
||||
import styled from 'styled-components';
|
||||
|
||||
function InventoryStep() {
|
||||
return <div />;
|
||||
const FieldHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: var(--pf-c-form__label--PaddingBottom);
|
||||
|
||||
label {
|
||||
--pf-c-form__label--PaddingBottom: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
function OtherPromptsStep({ config, i18n }) {
|
||||
return (
|
||||
<Form>
|
||||
{config.ask_job_type_on_launch && <JobTypeField i18n={i18n} />}
|
||||
{config.ask_limit_on_launch && (
|
||||
<FormField
|
||||
id="prompt-limit"
|
||||
name="limit"
|
||||
label={i18n._(t`Limit`)}
|
||||
tooltip={i18n._(t`Provide a host pattern to further constrain the list
|
||||
of hosts that will be managed or affected by the playbook. Multiple
|
||||
patterns are allowed. Refer to Ansible documentation for more
|
||||
information and examples on patterns.`)}
|
||||
/>
|
||||
)}
|
||||
{config.ask_verbosity_on_launch && <VerbosityField i18n={i18n} />}
|
||||
{config.ask_diff_mode_on_launch && <ShowChangesToggle i18n={i18n} />}
|
||||
{config.ask_tags_on_launch && (
|
||||
<TagField
|
||||
id="prompt-job-tags"
|
||||
name="job_tags"
|
||||
label={i18n._(t`Job Tags`)}
|
||||
tooltip={i18n._(t`Tags are useful when you have a large
|
||||
playbook, and you want to run a specific part of a play or task.
|
||||
Use commas to separate multiple tags. Refer to Ansible Tower
|
||||
documentation for details on the usage of tags.`)}
|
||||
/>
|
||||
)}
|
||||
{config.ask_skip_tags_on_launch && (
|
||||
<TagField
|
||||
id="prompt-skip-tags"
|
||||
name="skip_tags"
|
||||
label={i18n._(t`Skip Tags`)}
|
||||
tooltip={i18n._(t`Skip tags are useful when you have a large
|
||||
playbook, and you want to skip specific parts of a play or task.
|
||||
Use commas to separate multiple tags. Refer to Ansible Tower
|
||||
documentation for details on the usage of tags.`)}
|
||||
/>
|
||||
)}
|
||||
{config.ask_variables_on_launch && (
|
||||
<VariablesField
|
||||
id="prompt-variables"
|
||||
name="extra_vars"
|
||||
label={i18n._(t`Variables`)}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default InventoryStep;
|
||||
function JobTypeField({ i18n }) {
|
||||
const [field, meta, helpers] = useField('job_type');
|
||||
const options = [
|
||||
{
|
||||
value: '',
|
||||
key: '',
|
||||
label: i18n._(t`Choose a job type`),
|
||||
isDisabled: true,
|
||||
},
|
||||
{ value: 'run', key: 'run', label: i18n._(t`Run`), isDisabled: false },
|
||||
{
|
||||
value: 'check',
|
||||
key: 'check',
|
||||
label: i18n._(t`Check`),
|
||||
isDisabled: false,
|
||||
},
|
||||
];
|
||||
const isValid = !(meta.touched && meta.error);
|
||||
return (
|
||||
<FormGroup
|
||||
fieldId="propmt-job-type"
|
||||
label={i18n._(t`Job Type`)}
|
||||
isValid={isValid}
|
||||
>
|
||||
<FieldTooltip
|
||||
content={i18n._(t`For job templates, select run to execute the playbook.
|
||||
Select check to only check playbook syntax, test environment setup,
|
||||
and report problems without executing the playbook.`)}
|
||||
/>
|
||||
<AnsibleSelect
|
||||
id="prompt-job-type"
|
||||
data={options}
|
||||
{...field}
|
||||
onChange={(event, value) => helpers.setValue(value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function VerbosityField({ i18n }) {
|
||||
const [field, meta, helpers] = useField('verbosity');
|
||||
const options = [
|
||||
{ value: '0', key: '0', label: i18n._(t`0 (Normal)`) },
|
||||
{ value: '1', key: '1', label: i18n._(t`1 (Verbose)`) },
|
||||
{ value: '2', key: '2', label: i18n._(t`2 (More Verbose)`) },
|
||||
{ value: '3', key: '3', label: i18n._(t`3 (Debug)`) },
|
||||
{ value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) },
|
||||
];
|
||||
const isValid = !(meta.touched && meta.error);
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
fieldId="prompt-verbosity"
|
||||
isValid={isValid}
|
||||
label={i18n._(t`Verbosity`)}
|
||||
>
|
||||
<FieldTooltip
|
||||
content={i18n._(t`Control the level of output ansible
|
||||
will produce as the playbook executes.`)}
|
||||
/>
|
||||
<AnsibleSelect
|
||||
id="prompt-verbosity"
|
||||
data={options}
|
||||
{...field}
|
||||
onChange={(event, value) => helpers.setValue(value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function ShowChangesToggle({ i18n }) {
|
||||
const [field, , helpers] = useField('diff_mode');
|
||||
return (
|
||||
<FormGroup fieldId="prompt-show-changes">
|
||||
<FieldHeader>
|
||||
{' '}
|
||||
<label className="pf-c-form__label" htmlFor="prompt-show-changes">
|
||||
<span className="pf-c-form__label-text">
|
||||
{i18n._(t`Show Changes`)}
|
||||
<FieldTooltip
|
||||
content={i18n._(t`If enabled, show the changes made
|
||||
by Ansible tasks, where supported. This is equivalent to Ansible’s
|
||||
--diff mode.`)}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
</FieldHeader>
|
||||
<Switch
|
||||
id="prompt-show-changes"
|
||||
label={i18n._(t`On`)}
|
||||
labelOff={i18n._(t`Off`)}
|
||||
isChecked={field.value}
|
||||
onChange={helpers.setValue}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function TagField({ id, name, label, tooltip }) {
|
||||
const [field, , helpers] = useField(name);
|
||||
return (
|
||||
<FormGroup fieldId={id} label={label}>
|
||||
<FieldTooltip content={tooltip} />
|
||||
<TagMultiSelect value={field.value} onChange={helpers.setValue} />
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(OtherPromptsStep);
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import OtherPromptsStep from './OtherPromptsStep';
|
||||
|
||||
describe('OtherPromptsStep', () => {
|
||||
test('should render job type field', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={{ job_type: 'run' }}>
|
||||
<OtherPromptsStep
|
||||
config={{
|
||||
ask_job_type_on_launch: true,
|
||||
}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
|
||||
expect(wrapper.find('JobTypeField')).toHaveLength(1);
|
||||
expect(
|
||||
wrapper.find('JobTypeField AnsibleSelect').prop('data')
|
||||
).toHaveLength(3);
|
||||
expect(wrapper.find('JobTypeField AnsibleSelect').prop('value')).toEqual(
|
||||
'run'
|
||||
);
|
||||
});
|
||||
|
||||
test('should render limit field', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<OtherPromptsStep
|
||||
config={{
|
||||
ask_limit_on_launch: true,
|
||||
}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
|
||||
expect(wrapper.find('FormField#prompt-limit')).toHaveLength(1);
|
||||
expect(wrapper.find('FormField#prompt-limit input').prop('name')).toEqual(
|
||||
'limit'
|
||||
);
|
||||
});
|
||||
|
||||
test('should render verbosity field', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={{ verbosity: '' }}>
|
||||
<OtherPromptsStep
|
||||
config={{
|
||||
ask_verbosity_on_launch: true,
|
||||
}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
|
||||
expect(wrapper.find('VerbosityField')).toHaveLength(1);
|
||||
expect(
|
||||
wrapper.find('VerbosityField AnsibleSelect').prop('data')
|
||||
).toHaveLength(5);
|
||||
});
|
||||
|
||||
test('should render show changes toggle', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={{ diff_mode: true }}>
|
||||
<OtherPromptsStep
|
||||
config={{
|
||||
ask_diff_mode_on_launch: true,
|
||||
}}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
|
||||
expect(wrapper.find('ShowChangesToggle')).toHaveLength(1);
|
||||
expect(wrapper.find('ShowChangesToggle Switch').prop('isChecked')).toEqual(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -17,27 +17,6 @@ const QS_CONFIG = getQSConfig('credentials', {
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
async function loadCredentialTypes() {
|
||||
const pageSize = 200;
|
||||
const acceptableKinds = ['machine', 'cloud', 'net', 'ssh', 'vault'];
|
||||
// The number of credential types a user can have is unlimited. In practice, it is unlikely for
|
||||
// users to have more than a page at the maximum request size.
|
||||
const {
|
||||
data: { next, results },
|
||||
} = await CredentialTypesAPI.read({ page_size: pageSize });
|
||||
let nextResults = [];
|
||||
if (next) {
|
||||
const { data } = await CredentialTypesAPI.read({
|
||||
page_size: pageSize,
|
||||
page: 2,
|
||||
});
|
||||
nextResults = data.results;
|
||||
}
|
||||
return results
|
||||
.concat(nextResults)
|
||||
.filter(type => acceptableKinds.includes(type.kind));
|
||||
}
|
||||
|
||||
async function loadCredentials(params, selectedCredentialTypeId) {
|
||||
params.credential_type = selectedCredentialTypeId || 1;
|
||||
const { data } = await CredentialsAPI.read(params);
|
||||
@ -54,7 +33,7 @@ function MultiCredentialsLookup(props) {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const types = await loadCredentialTypes();
|
||||
const types = await CredentialTypesAPI.loadAllTypes();
|
||||
setCredentialTypes(types);
|
||||
const match = types.find(type => type.kind === 'ssh') || types[0];
|
||||
setSelectedType(match);
|
||||
|
||||
@ -18,21 +18,16 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
CredentialTypesAPI.read.mockResolvedValueOnce({
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 400,
|
||||
kind: 'ssh',
|
||||
namespace: 'biz',
|
||||
name: 'Amazon Web Services',
|
||||
},
|
||||
{ id: 500, kind: 'vault', namespace: 'buzz', name: 'Vault' },
|
||||
{ id: 600, kind: 'machine', namespace: 'fuzz', name: 'Machine' },
|
||||
],
|
||||
count: 2,
|
||||
CredentialTypesAPI.loadAllTypes.mockResolvedValueOnce([
|
||||
{
|
||||
id: 400,
|
||||
kind: 'ssh',
|
||||
namespace: 'biz',
|
||||
name: 'Amazon Web Services',
|
||||
},
|
||||
});
|
||||
{ id: 500, kind: 'vault', namespace: 'buzz', name: 'Vault' },
|
||||
{ id: 600, kind: 'machine', namespace: 'fuzz', name: 'Machine' },
|
||||
]);
|
||||
CredentialsAPI.read.mockResolvedValueOnce({
|
||||
data: {
|
||||
results: [
|
||||
@ -52,7 +47,7 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('MultiCredentialsLookup renders properly', async () => {
|
||||
test('should load credential types', async () => {
|
||||
const onChange = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
@ -64,8 +59,9 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
/>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('MultiCredentialsLookup')).toHaveLength(1);
|
||||
expect(CredentialTypesAPI.read).toHaveBeenCalled();
|
||||
expect(CredentialTypesAPI.loadAllTypes).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('onChange is called when you click to remove a credential from input', async () => {
|
||||
@ -118,12 +114,12 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledTimes(2);
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledTimes(1);
|
||||
await act(async () => {
|
||||
select.invoke('onChange')({}, 500);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledTimes(3);
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledTimes(2);
|
||||
expect(wrapper.find('OptionsList').prop('options')).toEqual([
|
||||
{ id: 1, kind: 'cloud', name: 'New Cred', url: 'www.google.com' },
|
||||
]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user