mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 15:36:04 -03:30
Add draggable selected list to galaxy credential lookup
This commit is contained in:
committed by
Shane McDonald
parent
673f722b71
commit
9fddf7c5cf
@@ -63,6 +63,7 @@
|
|||||||
"aria-labelledby",
|
"aria-labelledby",
|
||||||
"aria-hidden",
|
"aria-hidden",
|
||||||
"aria-controls",
|
"aria-controls",
|
||||||
|
"aria-pressed",
|
||||||
"sortKey",
|
"sortKey",
|
||||||
"ouiaId",
|
"ouiaId",
|
||||||
"credentialTypeNamespace",
|
"credentialTypeNamespace",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import useRequest from '../../util/useRequest';
|
|||||||
import { SearchColumns, SortColumns } from '../../types';
|
import { SearchColumns, SortColumns } from '../../types';
|
||||||
import DataListToolbar from '../DataListToolbar';
|
import DataListToolbar from '../DataListToolbar';
|
||||||
import CheckboxListItem from '../CheckboxListItem';
|
import CheckboxListItem from '../CheckboxListItem';
|
||||||
import SelectedList from '../SelectedList';
|
import { SelectedList } from '../SelectedList';
|
||||||
import { getQSConfig, parseQueryString } from '../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../util/qs';
|
||||||
import PaginatedTable, { HeaderCell, HeaderRow } from '../PaginatedTable';
|
import PaginatedTable, { HeaderCell, HeaderRow } from '../PaginatedTable';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import CheckboxCard from './CheckboxCard';
|
import CheckboxCard from './CheckboxCard';
|
||||||
import SelectedList from '../SelectedList';
|
import { SelectedList } from '../SelectedList';
|
||||||
|
|
||||||
function RolesStep({
|
function RolesStep({
|
||||||
onRolesClick,
|
onRolesClick,
|
||||||
|
|||||||
@@ -29,22 +29,24 @@ const QS_CONFIG = getQSConfig('credentials', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function CredentialLookup({
|
function CredentialLookup({
|
||||||
helperTextInvalid,
|
autoPopulate,
|
||||||
label,
|
|
||||||
isValid,
|
|
||||||
onBlur,
|
|
||||||
onChange,
|
|
||||||
required,
|
|
||||||
credentialTypeId,
|
credentialTypeId,
|
||||||
credentialTypeKind,
|
credentialTypeKind,
|
||||||
credentialTypeNamespace,
|
credentialTypeNamespace,
|
||||||
value,
|
draggable,
|
||||||
tooltip,
|
|
||||||
isDisabled,
|
|
||||||
autoPopulate,
|
|
||||||
multiple,
|
|
||||||
validate,
|
|
||||||
fieldName,
|
fieldName,
|
||||||
|
helperTextInvalid,
|
||||||
|
isDisabled,
|
||||||
|
isValid,
|
||||||
|
label,
|
||||||
|
modalDescription,
|
||||||
|
multiple,
|
||||||
|
onBlur,
|
||||||
|
onChange,
|
||||||
|
required,
|
||||||
|
tooltip,
|
||||||
|
validate,
|
||||||
|
value,
|
||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const autoPopulateLookup = useAutoPopulateLookup(onChange);
|
const autoPopulateLookup = useAutoPopulateLookup(onChange);
|
||||||
@@ -174,6 +176,7 @@ function CredentialLookup({
|
|||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
|
modalDescription={modalDescription}
|
||||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||||
<OptionsList
|
<OptionsList
|
||||||
value={state.selectedItems}
|
value={state.selectedItems}
|
||||||
@@ -208,7 +211,11 @@ function CredentialLookup({
|
|||||||
name="credential"
|
name="credential"
|
||||||
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
||||||
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
|
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
|
||||||
|
sortSelectedItems={selectedItems =>
|
||||||
|
dispatch({ type: 'SET_SELECTED_ITEMS', selectedItems })
|
||||||
|
}
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
|
draggable={draggable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ function Lookup(props) {
|
|||||||
onDebounce,
|
onDebounce,
|
||||||
fieldName,
|
fieldName,
|
||||||
validate,
|
validate,
|
||||||
|
modalDescription,
|
||||||
} = props;
|
} = props;
|
||||||
const [typedText, setTypedText] = useState('');
|
const [typedText, setTypedText] = useState('');
|
||||||
const debounceRequest = useDebounce(onDebounce, 1000);
|
const debounceRequest = useDebounce(onDebounce, 1000);
|
||||||
@@ -166,6 +167,7 @@ function Lookup(props) {
|
|||||||
aria-label={t`Lookup modal`}
|
aria-label={t`Lookup modal`}
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
|
description={state?.selectedItems?.length > 0 && modalDescription}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
ouiaId="modal-select-button"
|
ouiaId="modal-select-button"
|
||||||
@@ -204,6 +206,7 @@ const Item = shape({
|
|||||||
Lookup.propTypes = {
|
Lookup.propTypes = {
|
||||||
id: string,
|
id: string,
|
||||||
header: string,
|
header: string,
|
||||||
|
modalDescription: string,
|
||||||
onChange: func.isRequired,
|
onChange: func.isRequired,
|
||||||
value: oneOfType([Item, arrayOf(Item)]),
|
value: oneOfType([Item, arrayOf(Item)]),
|
||||||
multiple: bool,
|
multiple: bool,
|
||||||
@@ -224,6 +227,7 @@ Lookup.defaultProps = {
|
|||||||
value: null,
|
value: null,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
required: false,
|
required: false,
|
||||||
|
modalDescription: '',
|
||||||
onBlur: () => {},
|
onBlur: () => {},
|
||||||
renderItemChip: ({ item, removeItem, canDelete }) => (
|
renderItemChip: ({ item, removeItem, canDelete }) => (
|
||||||
<Chip
|
<Chip
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from 'prop-types';
|
} from 'prop-types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import SelectedList from '../SelectedList';
|
import { SelectedList, DraggableSelectedList } from '../SelectedList';
|
||||||
import CheckboxListItem from '../CheckboxListItem';
|
import CheckboxListItem from '../CheckboxListItem';
|
||||||
import DataListToolbar from '../DataListToolbar';
|
import DataListToolbar from '../DataListToolbar';
|
||||||
import { QSConfig, SearchColumns, SortColumns } from '../../types';
|
import { QSConfig, SearchColumns, SortColumns } from '../../types';
|
||||||
@@ -23,28 +23,39 @@ const ModalList = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function OptionsList({
|
function OptionsList({
|
||||||
value,
|
|
||||||
contentError,
|
contentError,
|
||||||
options,
|
deselectItem,
|
||||||
optionCount,
|
displayKey,
|
||||||
searchColumns,
|
draggable,
|
||||||
sortColumns,
|
|
||||||
searchableKeys,
|
|
||||||
relatedSearchableKeys,
|
|
||||||
multiple,
|
|
||||||
header,
|
header,
|
||||||
|
isLoading,
|
||||||
|
multiple,
|
||||||
name,
|
name,
|
||||||
|
optionCount,
|
||||||
|
options,
|
||||||
qsConfig,
|
qsConfig,
|
||||||
readOnly,
|
readOnly,
|
||||||
selectItem,
|
relatedSearchableKeys,
|
||||||
deselectItem,
|
|
||||||
renderItemChip,
|
renderItemChip,
|
||||||
isLoading,
|
searchColumns,
|
||||||
displayKey,
|
searchableKeys,
|
||||||
|
selectItem,
|
||||||
|
sortColumns,
|
||||||
|
sortSelectedItems,
|
||||||
|
value,
|
||||||
}) {
|
}) {
|
||||||
return (
|
let selectionPreview = null;
|
||||||
<ModalList>
|
if (value.length > 0) {
|
||||||
{value.length > 0 && (
|
if (draggable) {
|
||||||
|
selectionPreview = (
|
||||||
|
<DraggableSelectedList
|
||||||
|
onRemove={deselectItem}
|
||||||
|
onRowDrag={sortSelectedItems}
|
||||||
|
selected={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
selectionPreview = (
|
||||||
<SelectedList
|
<SelectedList
|
||||||
label={t`Selected`}
|
label={t`Selected`}
|
||||||
selected={value}
|
selected={value}
|
||||||
@@ -53,7 +64,13 @@ function OptionsList({
|
|||||||
renderItemChip={renderItemChip}
|
renderItemChip={renderItemChip}
|
||||||
displayKey={displayKey}
|
displayKey={displayKey}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalList>
|
||||||
|
{selectionPreview}
|
||||||
<PaginatedTable
|
<PaginatedTable
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
items={options}
|
items={options}
|
||||||
@@ -99,6 +116,7 @@ const Item = shape({
|
|||||||
OptionsList.propTypes = {
|
OptionsList.propTypes = {
|
||||||
deselectItem: func.isRequired,
|
deselectItem: func.isRequired,
|
||||||
displayKey: string,
|
displayKey: string,
|
||||||
|
draggable: bool,
|
||||||
multiple: bool,
|
multiple: bool,
|
||||||
optionCount: number.isRequired,
|
optionCount: number.isRequired,
|
||||||
options: arrayOf(Item).isRequired,
|
options: arrayOf(Item).isRequired,
|
||||||
@@ -110,6 +128,7 @@ OptionsList.propTypes = {
|
|||||||
value: arrayOf(Item).isRequired,
|
value: arrayOf(Item).isRequired,
|
||||||
};
|
};
|
||||||
OptionsList.defaultProps = {
|
OptionsList.defaultProps = {
|
||||||
|
draggable: false,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
renderItemChip: null,
|
renderItemChip: null,
|
||||||
searchColumns: [],
|
searchColumns: [],
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DataList,
|
||||||
|
DataListAction,
|
||||||
|
DataListItem,
|
||||||
|
DataListCell,
|
||||||
|
DataListItemRow,
|
||||||
|
DataListControl,
|
||||||
|
DataListDragButton,
|
||||||
|
DataListItemCells,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { TimesIcon } from '@patternfly/react-icons';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
const RemoveActionSection = styled(DataListAction)`
|
||||||
|
&& {
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function DraggableSelectedList({ selected, onRemove, onRowDrag }) {
|
||||||
|
const [liveText, setLiveText] = useState('');
|
||||||
|
const [id, setId] = React.useState('');
|
||||||
|
|
||||||
|
const onDragStart = newId => {
|
||||||
|
setId(newId);
|
||||||
|
setLiveText(t`Dragging tarted for item id: ${newId}.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragMove = (oldIndex, newIndex) => {
|
||||||
|
setLiveText(
|
||||||
|
t`Dragging item ${id}. Item with index ${oldIndex} in now ${newIndex}.`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragCancel = () => {
|
||||||
|
setLiveText(t`Dragging cancelled. List is unchanged.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragFinish = newItemOrder => {
|
||||||
|
const selectedItems = newItemOrder.map(item =>
|
||||||
|
selected.find(i => i.name === item)
|
||||||
|
);
|
||||||
|
onRowDrag(selectedItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeItem = item => {
|
||||||
|
onRemove(selected.find(i => i.name === item));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selected.length <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderedList = selected.map(item => item.name);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DataList
|
||||||
|
aria-label={t`Draggable list to reorder and remove selected items.`}
|
||||||
|
itemOrder={orderedList}
|
||||||
|
onDragCancel={onDragCancel}
|
||||||
|
onDragFinish={onDragFinish}
|
||||||
|
onDragMove={onDragMove}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
>
|
||||||
|
{orderedList.map((label, index) => {
|
||||||
|
const rowPosition = index + 1;
|
||||||
|
return (
|
||||||
|
<DataListItem id={label} key={rowPosition}>
|
||||||
|
<DataListItemRow>
|
||||||
|
<DataListControl>
|
||||||
|
<DataListDragButton
|
||||||
|
aria-label={t`Reorder`}
|
||||||
|
aria-labelledby={rowPosition}
|
||||||
|
aria-describedby={t`Press space or enter to begin dragging,
|
||||||
|
and use the arrow keys to navigate up or down.
|
||||||
|
Press enter to confirm the drag, or any other key to
|
||||||
|
cancel the drag operation.`}
|
||||||
|
aria-pressed="false"
|
||||||
|
/>
|
||||||
|
</DataListControl>
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell key={label}>
|
||||||
|
<span id={rowPosition}>
|
||||||
|
{rowPosition}. {label}
|
||||||
|
</span>
|
||||||
|
</DataListCell>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<RemoveActionSection aria-label={t`Actions`} id={rowPosition}>
|
||||||
|
<Button
|
||||||
|
onClick={() => removeItem(label)}
|
||||||
|
variant="plain"
|
||||||
|
aria-label={t`Remove`}
|
||||||
|
ouiaId="draggable-list-remove"
|
||||||
|
>
|
||||||
|
<TimesIcon />
|
||||||
|
</Button>
|
||||||
|
</RemoveActionSection>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DataList>
|
||||||
|
<div className="pf-screen-reader" aria-live="assertive">
|
||||||
|
{liveText}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItem = PropTypes.shape({
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
});
|
||||||
|
DraggableSelectedList.propTypes = {
|
||||||
|
onRemove: PropTypes.func,
|
||||||
|
onRowDrag: PropTypes.func,
|
||||||
|
selected: PropTypes.arrayOf(ListItem),
|
||||||
|
};
|
||||||
|
DraggableSelectedList.defaultProps = {
|
||||||
|
onRemove: () => null,
|
||||||
|
onRowDrag: () => null,
|
||||||
|
selected: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DraggableSelectedList;
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
|
import DraggableSelectedList from './DraggableSelectedList';
|
||||||
|
|
||||||
|
describe('<DraggableSelectedList />', () => {
|
||||||
|
let wrapper;
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
test('should render expected rows', () => {
|
||||||
|
const mockSelected = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'bar',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<DraggableSelectedList
|
||||||
|
selected={mockSelected}
|
||||||
|
onRemove={() => {}}
|
||||||
|
onRowDrag={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('DraggableSelectedList').length).toBe(1);
|
||||||
|
expect(wrapper.find('DataListItem').length).toBe(2);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('DataListItem DataListCell')
|
||||||
|
.first()
|
||||||
|
.containsMatchingElement(<span>1. foo</span>)
|
||||||
|
).toEqual(true);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('DataListItem DataListCell')
|
||||||
|
.last()
|
||||||
|
.containsMatchingElement(<span>2. bar</span>)
|
||||||
|
).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not render when selected list is empty', () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<DraggableSelectedList
|
||||||
|
selected={[]}
|
||||||
|
onRemove={() => {}}
|
||||||
|
onRowDrag={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('DataList').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call onRemove callback prop on remove button click', () => {
|
||||||
|
const onRemove = jest.fn();
|
||||||
|
const mockSelected = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<DraggableSelectedList selected={mockSelected} onRemove={onRemove} />
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('DataListItem[id="foo"] Button[aria-label="Remove"]')
|
||||||
|
.simulate('click');
|
||||||
|
expect(onRemove).toBeCalledWith({
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1 +1,2 @@
|
|||||||
export { default } from './SelectedList';
|
export { default as SelectedList } from './SelectedList';
|
||||||
|
export { default as DraggableSelectedList } from './DraggableSelectedList';
|
||||||
|
|||||||
@@ -42,14 +42,20 @@ function OrganizationAdd() {
|
|||||||
default_environment: values.default_environment?.id,
|
default_environment: values.default_environment?.id,
|
||||||
});
|
});
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
groupsToAssociate
|
groupsToAssociate.map(id =>
|
||||||
.map(id => OrganizationsAPI.associateInstanceGroup(response.id, id))
|
OrganizationsAPI.associateInstanceGroup(response.id, id)
|
||||||
.concat(
|
)
|
||||||
values.galaxy_credentials.map(({ id: credId }) =>
|
|
||||||
OrganizationsAPI.associateGalaxyCredential(response.id, credId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
/* eslint-disable no-await-in-loop, no-restricted-syntax */
|
||||||
|
// Resolve Promises sequentially to maintain order and avoid race condition
|
||||||
|
for (const credential of values.galaxy_credentials) {
|
||||||
|
await OrganizationsAPI.associateGalaxyCredential(
|
||||||
|
response.id,
|
||||||
|
credential.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* eslint-enable no-await-in-loop, no-restricted-syntax */
|
||||||
|
|
||||||
history.push(`/organizations/${response.id}`);
|
history.push(`/organizations/${response.id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFormError(error);
|
setFormError(error);
|
||||||
|
|||||||
@@ -3,9 +3,14 @@ import PropTypes from 'prop-types';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { CardBody } from '../../../components/Card';
|
import { CardBody } from '../../../components/Card';
|
||||||
import { OrganizationsAPI } from '../../../api';
|
import { OrganizationsAPI } from '../../../api';
|
||||||
import { getAddedAndRemoved } from '../../../util/lists';
|
|
||||||
import OrganizationForm from '../shared/OrganizationForm';
|
import OrganizationForm from '../shared/OrganizationForm';
|
||||||
|
|
||||||
|
const isEqual = (array1, array2) =>
|
||||||
|
array1.length === array2.length &&
|
||||||
|
array1.every((element, index) => {
|
||||||
|
return element.id === array2[index].id;
|
||||||
|
});
|
||||||
|
|
||||||
function OrganizationEdit({ organization }) {
|
function OrganizationEdit({ organization }) {
|
||||||
const detailsUrl = `/organizations/${organization.id}/details`;
|
const detailsUrl = `/organizations/${organization.id}/details`;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -17,43 +22,39 @@ function OrganizationEdit({ organization }) {
|
|||||||
groupsToDisassociate
|
groupsToDisassociate
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const {
|
|
||||||
added: addedCredentials,
|
|
||||||
removed: removedCredentials,
|
|
||||||
} = getAddedAndRemoved(
|
|
||||||
organization.galaxy_credentials,
|
|
||||||
values.galaxy_credentials
|
|
||||||
);
|
|
||||||
|
|
||||||
const addedCredentialIds = addedCredentials.map(({ id }) => id);
|
|
||||||
const removedCredentialIds = removedCredentials.map(({ id }) => id);
|
|
||||||
|
|
||||||
await OrganizationsAPI.update(organization.id, {
|
await OrganizationsAPI.update(organization.id, {
|
||||||
...values,
|
...values,
|
||||||
default_environment: values.default_environment?.id || null,
|
default_environment: values.default_environment?.id || null,
|
||||||
});
|
});
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
groupsToAssociate
|
groupsToAssociate.map(id =>
|
||||||
.map(id =>
|
OrganizationsAPI.associateInstanceGroup(organization.id, id)
|
||||||
OrganizationsAPI.associateInstanceGroup(organization.id, id)
|
)
|
||||||
)
|
|
||||||
.concat(
|
|
||||||
addedCredentialIds.map(id =>
|
|
||||||
OrganizationsAPI.associateGalaxyCredential(organization.id, id)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
groupsToDisassociate
|
groupsToDisassociate.map(id =>
|
||||||
.map(id =>
|
OrganizationsAPI.disassociateInstanceGroup(organization.id, id)
|
||||||
OrganizationsAPI.disassociateInstanceGroup(organization.id, id)
|
)
|
||||||
)
|
|
||||||
.concat(
|
|
||||||
removedCredentialIds.map(id =>
|
|
||||||
OrganizationsAPI.disassociateGalaxyCredential(organization.id, id)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
/* eslint-disable no-await-in-loop, no-restricted-syntax */
|
||||||
|
// Resolve Promises sequentially to avoid race condition
|
||||||
|
if (
|
||||||
|
!isEqual(organization.galaxy_credentials, values.galaxy_credentials)
|
||||||
|
) {
|
||||||
|
for (const credential of organization.galaxy_credentials) {
|
||||||
|
await OrganizationsAPI.disassociateGalaxyCredential(
|
||||||
|
organization.id,
|
||||||
|
credential.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const credential of values.galaxy_credentials) {
|
||||||
|
await OrganizationsAPI.associateGalaxyCredential(
|
||||||
|
organization.id,
|
||||||
|
credential.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable no-await-in-loop, no-restricted-syntax */
|
||||||
history.push(detailsUrl);
|
history.push(detailsUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFormError(error);
|
setFormError(error);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Formik, useField, useFormikContext } from 'formik';
|
import { Formik, useField, useFormikContext } from 'formik';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { Form } from '@patternfly/react-core';
|
import { Form } from '@patternfly/react-core';
|
||||||
|
|
||||||
import { OrganizationsAPI } from '../../../api';
|
import { OrganizationsAPI } from '../../../api';
|
||||||
@@ -106,7 +106,20 @@ function OrganizationFormFields({
|
|||||||
onChange={handleCredentialUpdate}
|
onChange={handleCredentialUpdate}
|
||||||
value={galaxyCredentialsField.value}
|
value={galaxyCredentialsField.value}
|
||||||
multiple
|
multiple
|
||||||
|
draggable
|
||||||
fieldName="galaxy_credentials"
|
fieldName="galaxy_credentials"
|
||||||
|
modalDescription={
|
||||||
|
<>
|
||||||
|
<b>
|
||||||
|
<Trans>Selected</Trans>
|
||||||
|
</b>
|
||||||
|
<br />
|
||||||
|
<Trans>
|
||||||
|
Note: The order of these credentials sets precedence for the sync
|
||||||
|
and lookup of the content.
|
||||||
|
</Trans>
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user