diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx index 9a24316c74..81601a687d 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx @@ -3,7 +3,7 @@ import { withRouter } from 'react-router-dom'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { ToolbarItem } from '@patternfly/react-core'; +import { ToolbarItem, Alert } from '@patternfly/react-core'; import { CredentialsAPI, CredentialTypesAPI } from '@api'; import AnsibleSelect from '@components/AnsibleSelect'; import CredentialChip from '@components/CredentialChip'; @@ -77,7 +77,7 @@ function MultiCredentialsLookup(props) { /> ); - const isMultiple = selectedType && selectedType.kind === 'vault'; + const isVault = selectedType?.kind === 'vault'; return ( { return ( + {isVault && ( + + )} {credentialTypes && credentialTypes.length > 0 && (
@@ -140,17 +150,18 @@ function MultiCredentialsLookup(props) { key: 'name', }, ]} - multiple={isMultiple} + multiple={isVault} header={i18n._(t`Credentials`)} name="credentials" qsConfig={QS_CONFIG} readOnly={!canDelete} selectItem={item => { - if (isMultiple) { - return dispatch({ type: 'SELECT_ITEM', item }); - } - const selectedItems = state.selectedItems.filter( - i => i.kind !== item.kind + const hasSameVaultID = val => + val?.inputs?.vault_id !== undefined && + val?.inputs?.vault_id === item?.inputs?.vault_id; + const hasSameKind = val => val.kind === item.kind; + const selectedItems = state.selectedItems.filter(i => + isVault ? !hasSameVaultID(i) : !hasSameKind(i) ); selectedItems.push(item); return dispatch({ diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx index fa73edad3a..fa8eb5a15f 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx @@ -12,7 +12,8 @@ describe('', () => { const credentials = [ { id: 1, kind: 'cloud', name: 'Foo', url: 'www.google.com' }, { id: 2, kind: 'ssh', name: 'Alex', url: 'www.google.com' }, - { name: 'Gatsby', id: 21, kind: 'vault' }, + { name: 'Gatsby', id: 21, kind: 'vault', inputs: { vault_id: '1' } }, + { name: 'Gatsby 2', id: 23, kind: 'vault' }, { name: 'Gatsby', id: 8, kind: 'Machine' }, ]; @@ -80,14 +81,15 @@ describe('', () => { ); }); const chip = wrapper.find('CredentialChip'); - expect(chip).toHaveLength(4); + expect(chip).toHaveLength(5); const button = chip.at(1).find('ChipButton'); await act(async () => { button.invoke('onClick')(); }); expect(onChange).toBeCalledWith([ { id: 1, kind: 'cloud', name: 'Foo', url: 'www.google.com' }, - { id: 21, kind: 'vault', name: 'Gatsby' }, + { id: 21, inputs: { vault_id: '1' }, kind: 'vault', name: 'Gatsby' }, + { id: 23, kind: 'vault', name: 'Gatsby 2' }, { id: 8, kind: 'Machine', name: 'Gatsby' }, ]); }); @@ -161,12 +163,13 @@ describe('', () => { expect(onChange).toBeCalledWith([ { id: 1, kind: 'cloud', name: 'Foo', url: 'www.google.com' }, { id: 2, kind: 'ssh', name: 'Alex', url: 'www.google.com' }, - { id: 21, kind: 'vault', name: 'Gatsby' }, + { id: 21, inputs: { vault_id: '1' }, kind: 'vault', name: 'Gatsby' }, + { id: 23, kind: 'vault', name: 'Gatsby 2' }, { id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' }, ]); }); - test('should allow multiple vault credentials', async () => { + test('should allow multiple vault credentials with no vault id', async () => { const onChange = jest.fn(); await act(async () => { wrapper = mountWithContexts( @@ -193,7 +196,7 @@ describe('', () => { act(() => { optionsList.invoke('selectItem')({ id: 5, - kind: 'Machine', + kind: 'vault', name: 'Cred 5', url: 'www.google.com', }); @@ -205,9 +208,115 @@ describe('', () => { expect(onChange).toBeCalledWith([ { id: 1, kind: 'cloud', name: 'Foo', url: 'www.google.com' }, { id: 2, kind: 'ssh', name: 'Alex', url: 'www.google.com' }, - { id: 21, kind: 'vault', name: 'Gatsby' }, + { id: 21, kind: 'vault', name: 'Gatsby', inputs: { vault_id: '1' } }, + { id: 23, kind: 'vault', name: 'Gatsby 2' }, { id: 8, kind: 'Machine', name: 'Gatsby' }, - { id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' }, + { id: 5, kind: 'vault', name: 'Cred 5', url: 'www.google.com' }, + ]); + }); + + test('should allow multiple vault credentials with different vault ids', async () => { + const onChange = jest.fn(); + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + const searchButton = await waitForElement(wrapper, 'SearchButton'); + await act(async () => { + searchButton.invoke('onClick')(); + }); + wrapper.update(); + const typeSelect = wrapper.find('AnsibleSelect'); + act(() => { + typeSelect.invoke('onChange')({}, 500); + }); + wrapper.update(); + const optionsList = wrapper.find('OptionsList'); + expect(optionsList.prop('multiple')).toEqual(true); + act(() => { + optionsList.invoke('selectItem')({ + id: 5, + kind: 'vault', + name: 'Cred 5', + url: 'www.google.com', + inputs: { vault_id: '2' }, + }); + }); + wrapper.update(); + act(() => { + wrapper.find('Button[variant="primary"]').invoke('onClick')(); + }); + expect(onChange).toBeCalledWith([ + { id: 1, kind: 'cloud', name: 'Foo', url: 'www.google.com' }, + { id: 2, kind: 'ssh', name: 'Alex', url: 'www.google.com' }, + { id: 21, kind: 'vault', name: 'Gatsby', inputs: { vault_id: '1' } }, + { id: 23, kind: 'vault', name: 'Gatsby 2' }, + { id: 8, kind: 'Machine', name: 'Gatsby' }, + { + id: 5, + kind: 'vault', + name: 'Cred 5', + url: 'www.google.com', + inputs: { vault_id: '2' }, + }, + ]); + }); + + test('should not select multiple vault credentials with same vault id', async () => { + const onChange = jest.fn(); + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + const searchButton = await waitForElement(wrapper, 'SearchButton'); + await act(async () => { + searchButton.invoke('onClick')(); + }); + wrapper.update(); + const typeSelect = wrapper.find('AnsibleSelect'); + act(() => { + typeSelect.invoke('onChange')({}, 500); + }); + wrapper.update(); + const optionsList = wrapper.find('OptionsList'); + expect(optionsList.prop('multiple')).toEqual(true); + act(() => { + optionsList.invoke('selectItem')({ + id: 24, + kind: 'vault', + name: 'Cred 5', + url: 'www.google.com', + inputs: { vault_id: '1' }, + }); + }); + wrapper.update(); + act(() => { + wrapper.find('Button[variant="primary"]').invoke('onClick')(); + }); + expect(onChange).toBeCalledWith([ + { id: 1, kind: 'cloud', name: 'Foo', url: 'www.google.com' }, + { id: 2, kind: 'ssh', name: 'Alex', url: 'www.google.com' }, + { id: 23, kind: 'vault', name: 'Gatsby 2' }, + { id: 8, kind: 'Machine', name: 'Gatsby' }, + { + id: 24, + kind: 'vault', + name: 'Cred 5', + url: 'www.google.com', + inputs: { vault_id: '1' }, + }, ]); }); });