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:
softwarefactory-project-zuul[bot] 2020-04-13 17:04:41 +00:00 committed by GitHub
commit 516a44ce73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 718 additions and 70 deletions

View File

@ -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;

View 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]]);
});
});

View File

@ -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(

View File

@ -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);

View File

@ -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,
});
});
});

View File

@ -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

View File

@ -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);
};

View File

@ -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);
});
});

View File

@ -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 Ansibles
--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);

View File

@ -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
);
});
});

View File

@ -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);

View File

@ -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' },
]);