mirror of
https://github.com/ansible/awx.git
synced 2024-11-01 08:21:15 +03:00
commit
6f26383e06
@ -138,4 +138,36 @@ describe('APIClient (api.js)', () => {
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('associateInstanceGroup calls expected http method with expected data', async (done) => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = ({ post: jest.fn(createPromise) });
|
||||
|
||||
const api = new APIClient(mockHttp);
|
||||
const url = 'foo/bar/';
|
||||
const id = 1;
|
||||
await api.associateInstanceGroup(url, id);
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0][0]).toEqual(url);
|
||||
expect(mockHttp.post.mock.calls[0][1]).toEqual({ id });
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('disassociateInstanceGroup calls expected http method with expected data', async (done) => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = ({ post: jest.fn(createPromise) });
|
||||
|
||||
const api = new APIClient(mockHttp);
|
||||
const url = 'foo/bar/';
|
||||
const id = 1;
|
||||
await api.disassociateInstanceGroup(url, id);
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0][0]).toEqual(url);
|
||||
expect(mockHttp.post.mock.calls[0][1]).toEqual({ id, disassociate: true });
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -60,7 +60,7 @@ describe('<Lookup />', () => {
|
||||
</I18nProvider>
|
||||
).find('Lookup');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(wrapper.state('lookupSelectedItems')).toEqual([]);
|
||||
expect(wrapper.state('lookupSelectedItems')).toEqual(mockSelected);
|
||||
const searchItem = wrapper.find('.pf-c-input-group__text#search');
|
||||
searchItem.first().simulate('click');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
@ -110,12 +110,11 @@ describe('<Lookup />', () => {
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
const removeIcon = wrapper.find('.awx-c-icon--remove').first();
|
||||
const removeIcon = wrapper.find('button[aria-label="close"]').first();
|
||||
removeIcon.simulate('click');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
test('"wrapTags" method properly handles data', () => {
|
||||
const spy = jest.spyOn(Lookup.prototype, 'wrapTags');
|
||||
test('renders chips from prop value', () => {
|
||||
mockData = [{ name: 'foo', id: 0 }, { name: 'bar', id: 1 }];
|
||||
const wrapper = mount(
|
||||
<I18nProvider>
|
||||
@ -129,20 +128,18 @@ describe('<Lookup />', () => {
|
||||
sortedColumnKey="name"
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
const pill = wrapper.find('span.awx-c-tag--pill');
|
||||
expect(pill).toHaveLength(2);
|
||||
).find('Lookup');
|
||||
const chip = wrapper.find('li.pf-c-chip');
|
||||
expect(chip).toHaveLength(2);
|
||||
});
|
||||
test('toggleSelected successfully adds/removes row from lookupSelectedItems state', () => {
|
||||
mockData = [{ name: 'foo', id: 1 }];
|
||||
mockData = [];
|
||||
const wrapper = mount(
|
||||
<I18nProvider>
|
||||
<Lookup
|
||||
lookup_header="Foo Bar"
|
||||
onLookupSave={() => { }}
|
||||
value={mockData}
|
||||
selected={[]}
|
||||
getItems={() => { }}
|
||||
columns={mockColumns}
|
||||
sortedColumnKey="name"
|
||||
@ -164,7 +161,7 @@ describe('<Lookup />', () => {
|
||||
expect(wrapper.state('lookupSelectedItems')).toEqual([]);
|
||||
});
|
||||
test('saveModal calls callback with selected items', () => {
|
||||
mockData = [{ name: 'foo', id: 1 }];
|
||||
mockData = [];
|
||||
const onLookupSaveFn = jest.fn();
|
||||
const wrapper = mount(
|
||||
<I18nProvider>
|
||||
@ -198,10 +195,10 @@ describe('<Lookup />', () => {
|
||||
<Lookup
|
||||
lookup_header="Foo Bar"
|
||||
onLookupSave={() => { }}
|
||||
data={mockData}
|
||||
selected={[]}
|
||||
value={mockData}
|
||||
columns={mockColumns}
|
||||
sortedColumnKey="name"
|
||||
getItems={() => { }}
|
||||
/>
|
||||
</I18nProvider>
|
||||
).find('Lookup');
|
||||
@ -217,10 +214,10 @@ describe('<Lookup />', () => {
|
||||
<Lookup
|
||||
lookup_header="Foo Bar"
|
||||
onLookupSave={() => { }}
|
||||
data={mockData}
|
||||
selected={[]}
|
||||
value={mockData}
|
||||
columns={mockColumns}
|
||||
sortedColumnKey="name"
|
||||
getItems={() => { }}
|
||||
/>
|
||||
</I18nProvider>
|
||||
).find('Lookup');
|
||||
|
@ -1,16 +1,244 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { ConfigContext } from '../../../../../src/context';
|
||||
import APIClient from '../../../../../src/api';
|
||||
import OrganizationEdit from '../../../../../src/pages/Organizations/screens/Organization/OrganizationEdit';
|
||||
|
||||
describe('<OrganizationEdit />', () => {
|
||||
test('initially renders succesfully', () => {
|
||||
const mockData = {
|
||||
name: 'Foo',
|
||||
description: 'Bar',
|
||||
custom_virtualenv: 'Fizz',
|
||||
id: 1,
|
||||
related: {
|
||||
instance_groups: '/api/v2/organizations/1/instance_groups'
|
||||
}
|
||||
};
|
||||
|
||||
test('should request related instance groups from api', () => {
|
||||
const mockInstanceGroups = [
|
||||
{ name: 'One', id: 1 },
|
||||
{ name: 'Two', id: 2 }
|
||||
];
|
||||
const getOrganizationInstanceGroups = jest.fn(() => (
|
||||
Promise.resolve({ data: { results: mockInstanceGroups } })
|
||||
));
|
||||
mount(
|
||||
<MemoryRouter initialEntries={['/organizations/1/edit']} initialIndex={0}>
|
||||
<OrganizationEdit
|
||||
match={{ path: '/organizations/:id/edit', url: '/organizations/1/edit', params: { id: 1 } }}
|
||||
/>
|
||||
<I18nProvider>
|
||||
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||
<OrganizationEdit
|
||||
match={{ params: { id: '1' } }}
|
||||
api={{
|
||||
getOrganizationInstanceGroups
|
||||
}}
|
||||
organization={mockData}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</I18nProvider>
|
||||
).find('OrganizationEdit');
|
||||
|
||||
expect(getOrganizationInstanceGroups).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('componentDidMount should set instanceGroups to state', async () => {
|
||||
const mockInstanceGroups = [
|
||||
{ name: 'One', id: 1 },
|
||||
{ name: 'Two', id: 2 }
|
||||
];
|
||||
const getOrganizationInstanceGroups = jest.fn(() => (
|
||||
Promise.resolve({ data: { results: mockInstanceGroups } })
|
||||
));
|
||||
const wrapper = mount(
|
||||
<I18nProvider>
|
||||
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||
<OrganizationEdit
|
||||
match={{
|
||||
path: '/organizations/:id',
|
||||
url: '/organizations/1',
|
||||
params: { id: '1' }
|
||||
}}
|
||||
organization={mockData}
|
||||
api={{ getOrganizationInstanceGroups }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</I18nProvider>
|
||||
).find('OrganizationEdit');
|
||||
|
||||
await wrapper.instance().componentDidMount();
|
||||
expect(wrapper.state().form.instanceGroups.value).toEqual(mockInstanceGroups);
|
||||
});
|
||||
test('onLookupSave successfully sets instanceGroups state', () => {
|
||||
const api = jest.fn();
|
||||
const wrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<OrganizationEdit
|
||||
organization={mockData}
|
||||
api={api}
|
||||
match={{ path: '/organizations/:id/edit', url: '/organizations/1/edit' }}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
).find('OrganizationEdit');
|
||||
|
||||
wrapper.instance().onLookupSave([
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo'
|
||||
}
|
||||
], 'instanceGroups');
|
||||
expect(wrapper.state().form.instanceGroups.value).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo'
|
||||
}
|
||||
]);
|
||||
});
|
||||
test('calls "onFieldChange" when input values change', () => {
|
||||
const api = new APIClient();
|
||||
const spy = jest.spyOn(OrganizationEdit.WrappedComponent.prototype, 'onFieldChange');
|
||||
const wrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<OrganizationEdit
|
||||
organization={mockData}
|
||||
api={api}
|
||||
match={{ path: '/organizations/:id/edit', url: '/organizations/1/edit' }}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
).find('OrganizationEdit');
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
wrapper.instance().onFieldChange('foo', { target: { name: 'name' } });
|
||||
wrapper.instance().onFieldChange('bar', { target: { name: 'description' } });
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
test('AnsibleSelect component renders if there are virtual environments', () => {
|
||||
const api = jest.fn();
|
||||
const config = {
|
||||
custom_virtualenvs: ['foo', 'bar'],
|
||||
};
|
||||
const wrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<ConfigContext.Provider value={config}>
|
||||
<OrganizationEdit
|
||||
match={{
|
||||
path: '/organizations/:id',
|
||||
url: '/organizations/1',
|
||||
params: { id: '1' }
|
||||
}}
|
||||
organization={mockData}
|
||||
api={api}
|
||||
/>
|
||||
</ConfigContext.Provider>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(wrapper.find('FormSelect')).toHaveLength(1);
|
||||
expect(wrapper.find('FormSelectOption')).toHaveLength(2);
|
||||
});
|
||||
test('calls onSubmit when Save button is clicked', () => {
|
||||
const api = jest.fn();
|
||||
const spy = jest.spyOn(OrganizationEdit.WrappedComponent.prototype, 'onSubmit');
|
||||
const wrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<OrganizationEdit
|
||||
match={{
|
||||
path: '/organizations/:id',
|
||||
url: '/organizations/1',
|
||||
params: { id: '1' }
|
||||
}}
|
||||
organization={mockData}
|
||||
api={api}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
wrapper.find('button[aria-label="Save"]').prop('onClick')();
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
test('onSubmit associates and disassociates instance groups', async () => {
|
||||
const mockInstanceGroups = [
|
||||
{ name: 'One', id: 1 },
|
||||
{ name: 'Two', id: 2 }
|
||||
];
|
||||
const getOrganizationInstanceGroupsFn = jest.fn(() => (
|
||||
Promise.resolve({ data: { results: mockInstanceGroups } })
|
||||
));
|
||||
const mockDataForm = {
|
||||
name: 'Foo',
|
||||
description: 'Bar',
|
||||
custom_virtualenv: 'Fizz',
|
||||
};
|
||||
const updateOrganizationDetailsFn = jest.fn().mockResolvedValue(1, mockDataForm);
|
||||
const associateInstanceGroupFn = jest.fn().mockResolvedValue('done');
|
||||
const disassociateInstanceGroupFn = jest.fn().mockResolvedValue('done');
|
||||
const api = {
|
||||
getOrganizationInstanceGroups: getOrganizationInstanceGroupsFn,
|
||||
updateOrganizationDetails: updateOrganizationDetailsFn,
|
||||
associateInstanceGroup: associateInstanceGroupFn,
|
||||
disassociateInstanceGroup: disassociateInstanceGroupFn
|
||||
};
|
||||
const wrapper = mount(
|
||||
<I18nProvider>
|
||||
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||
<OrganizationEdit
|
||||
match={{
|
||||
path: '/organizations/:id',
|
||||
url: '/organizations/1',
|
||||
params: { id: '1' }
|
||||
}}
|
||||
organization={mockData}
|
||||
api={api}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</I18nProvider>
|
||||
).find('OrganizationEdit');
|
||||
|
||||
await wrapper.instance().componentDidMount();
|
||||
|
||||
wrapper.instance().onLookupSave([
|
||||
{ name: 'One', id: 1 },
|
||||
{ name: 'Three', id: 3 }
|
||||
], 'instanceGroups');
|
||||
|
||||
await wrapper.instance().onSubmit();
|
||||
expect(updateOrganizationDetailsFn).toHaveBeenCalledWith(1, mockDataForm);
|
||||
expect(associateInstanceGroupFn).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 3);
|
||||
expect(associateInstanceGroupFn).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1);
|
||||
expect(associateInstanceGroupFn).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 2);
|
||||
|
||||
expect(disassociateInstanceGroupFn).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 2);
|
||||
expect(disassociateInstanceGroupFn).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1);
|
||||
expect(disassociateInstanceGroupFn).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 3);
|
||||
});
|
||||
|
||||
test('calls "onCancel" when Cancel button is clicked', () => {
|
||||
const api = jest.fn();
|
||||
const spy = jest.spyOn(OrganizationEdit.WrappedComponent.prototype, 'onCancel');
|
||||
const wrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<OrganizationEdit
|
||||
match={{
|
||||
path: '/organizations/:id',
|
||||
url: '/organizations/1',
|
||||
params: { id: '1' }
|
||||
}}
|
||||
organization={mockData}
|
||||
api={api}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
@ -71,7 +71,7 @@ describe('<OrganizationAdd />', () => {
|
||||
test('Successful form submission triggers redirect', (done) => {
|
||||
const onSuccess = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onSuccess');
|
||||
const mockedResp = { data: { id: 1, related: { instance_groups: '/bar' } } };
|
||||
const api = { createOrganization: jest.fn().mockResolvedValue(mockedResp), createInstanceGroups: jest.fn().mockResolvedValue('done') };
|
||||
const api = { createOrganization: jest.fn().mockResolvedValue(mockedResp), associateInstanceGroup: jest.fn().mockResolvedValue('done') };
|
||||
const wrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
@ -131,10 +131,10 @@ describe('<OrganizationAdd />', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
const createInstanceGroupsFn = jest.fn().mockResolvedValue('done');
|
||||
const associateInstanceGroupFn = jest.fn().mockResolvedValue('done');
|
||||
const api = {
|
||||
createOrganization: createOrganizationFn,
|
||||
createInstanceGroups: createInstanceGroupsFn
|
||||
associateInstanceGroup: associateInstanceGroupFn
|
||||
};
|
||||
const wrapper = mount(
|
||||
<MemoryRouter>
|
||||
@ -156,7 +156,7 @@ describe('<OrganizationAdd />', () => {
|
||||
description: '',
|
||||
name: 'mock org'
|
||||
});
|
||||
expect(createInstanceGroupsFn).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1);
|
||||
expect(associateInstanceGroupFn).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1);
|
||||
});
|
||||
|
||||
test('AnsibleSelect component renders if there are virtual environments', () => {
|
||||
|
12
src/api.js
12
src/api.js
@ -70,6 +70,12 @@ class APIClient {
|
||||
return this.http.get(endpoint);
|
||||
}
|
||||
|
||||
updateOrganizationDetails (id, data) {
|
||||
const endpoint = `${API_ORGANIZATIONS}${id}/`;
|
||||
|
||||
return this.http.patch(endpoint, data);
|
||||
}
|
||||
|
||||
getOrganizationInstanceGroups (id, params = {}) {
|
||||
const endpoint = `${API_ORGANIZATIONS}${id}/instance_groups/`;
|
||||
|
||||
@ -110,9 +116,13 @@ class APIClient {
|
||||
return this.http.get(API_INSTANCE_GROUPS, { params });
|
||||
}
|
||||
|
||||
createInstanceGroups (url, id) {
|
||||
associateInstanceGroup (url, id) {
|
||||
return this.http.post(url, { id });
|
||||
}
|
||||
|
||||
disassociateInstanceGroup (url, id) {
|
||||
return this.http.post(url, { id, disassociate: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default APIClient;
|
||||
|
16
src/app.scss
16
src/app.scss
@ -228,26 +228,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.awx-c-icon--remove {
|
||||
padding-left: 10px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.awx-c-list {
|
||||
border-bottom: 1px solid #d7d7d7;
|
||||
}
|
||||
|
||||
.awx-c-tag--pill {
|
||||
color: var(--pf-global--BackgroundColor--light-100);
|
||||
background-color: rgb(0, 123, 186);
|
||||
border-radius: 3px;
|
||||
margin: 1px 2px;
|
||||
padding: 0 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.at-c-listCardBody {
|
||||
--pf-c-card__footer--PaddingX: 0;
|
||||
--pf-c-card__footer--PaddingY: 0;
|
||||
|
@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SearchIcon, CubesIcon } from '@patternfly/react-icons';
|
||||
import {
|
||||
Chip,
|
||||
Modal,
|
||||
Button,
|
||||
EmptyState,
|
||||
@ -28,9 +29,10 @@ const paginationStyling = {
|
||||
class Lookup extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isModalOpen: false,
|
||||
lookupSelectedItems: [],
|
||||
lookupSelectedItems: [...props.value] || [],
|
||||
results: [],
|
||||
count: 0,
|
||||
page: 1,
|
||||
@ -41,7 +43,6 @@ class Lookup extends React.Component {
|
||||
};
|
||||
this.onSetPage = this.onSetPage.bind(this);
|
||||
this.handleModalToggle = this.handleModalToggle.bind(this);
|
||||
this.wrapTags = this.wrapTags.bind(this);
|
||||
this.toggleSelected = this.toggleSelected.bind(this);
|
||||
this.saveModal = this.saveModal.bind(this);
|
||||
this.getData = this.getData.bind(this);
|
||||
@ -100,17 +101,27 @@ class Lookup extends React.Component {
|
||||
};
|
||||
|
||||
toggleSelected (row) {
|
||||
const { lookupSelectedItems } = this.state;
|
||||
const selectedIndex = lookupSelectedItems
|
||||
const { name, onLookupSave } = this.props;
|
||||
const { lookupSelectedItems: updatedSelectedItems, isModalOpen } = this.state;
|
||||
|
||||
const selectedIndex = updatedSelectedItems
|
||||
.findIndex(selectedRow => selectedRow.id === row.id);
|
||||
|
||||
if (selectedIndex > -1) {
|
||||
lookupSelectedItems.splice(selectedIndex, 1);
|
||||
this.setState({ lookupSelectedItems });
|
||||
updatedSelectedItems.splice(selectedIndex, 1);
|
||||
this.setState({ lookupSelectedItems: updatedSelectedItems });
|
||||
} else {
|
||||
this.setState(prevState => ({
|
||||
lookupSelectedItems: [...prevState.lookupSelectedItems, row]
|
||||
}));
|
||||
}
|
||||
|
||||
// Updates the selected items from parent state
|
||||
// This handles the case where the user removes chips from the lookup input
|
||||
// while the modal is closed
|
||||
if (!isModalOpen) {
|
||||
onLookupSave(updatedSelectedItems, name);
|
||||
}
|
||||
}
|
||||
|
||||
handleModalToggle () {
|
||||
@ -134,17 +145,6 @@ class Lookup extends React.Component {
|
||||
this.handleModalToggle();
|
||||
}
|
||||
|
||||
wrapTags (tags = []) {
|
||||
return tags.map(tag => (
|
||||
<span className="awx-c-tag--pill" key={tag.id}>
|
||||
{tag.name}
|
||||
<Button className="awx-c-icon--remove" id={tag.id} onClick={() => this.toggleSelected(tag)}>
|
||||
x
|
||||
</Button>
|
||||
</span>
|
||||
));
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
isModalOpen,
|
||||
@ -159,6 +159,16 @@ class Lookup extends React.Component {
|
||||
} = this.state;
|
||||
const { lookupHeader = 'items', value, columns } = this.props;
|
||||
|
||||
const chips = value ? (
|
||||
<div className="pf-c-chip-group">
|
||||
{value.map(chip => (
|
||||
<Chip key={chip.id} onClick={() => this.toggleSelected(chip)}>
|
||||
{chip.name}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@ -166,7 +176,7 @@ class Lookup extends React.Component {
|
||||
<Button className="pf-c-input-group__text" aria-label="search" id="search" onClick={this.handleModalToggle}>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
<div className="pf-c-form-control">{this.wrapTags(value)}</div>
|
||||
<div className="pf-c-form-control">{chips}</div>
|
||||
<Modal
|
||||
className="awx-c-modal"
|
||||
title={`Select ${lookupHeader}`}
|
||||
|
@ -119,14 +119,18 @@ class Organization extends Component {
|
||||
to="/organizations/:id/details"
|
||||
exact
|
||||
/>
|
||||
<Route
|
||||
path="/organizations/:id/edit"
|
||||
render={() => (
|
||||
<OrganizationEdit
|
||||
match={match}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{organization && (
|
||||
<Route
|
||||
path="/organizations/:id/edit"
|
||||
render={() => (
|
||||
<OrganizationEdit
|
||||
api={api}
|
||||
match={match}
|
||||
organization={organization}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{organization && (
|
||||
<Route
|
||||
path="/organizations/:id/details"
|
||||
|
@ -1,17 +1,284 @@
|
||||
import React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { I18n, i18nMark } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Link
|
||||
} from 'react-router-dom';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
CardBody,
|
||||
Form,
|
||||
FormGroup,
|
||||
TextInput,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
const OrganizationEdit = ({ match }) => (
|
||||
<CardBody>
|
||||
<h1><Trans>edit view</Trans></h1>
|
||||
<Link to={`/organizations/${match.params.id}`}>
|
||||
<Trans>save/cancel and go back to view</Trans>
|
||||
</Link>
|
||||
</CardBody>
|
||||
);
|
||||
import { ConfigContext } from '../../../../context';
|
||||
import Lookup from '../../../../components/Lookup';
|
||||
import FormActionGroup from '../../../../components/FormActionGroup';
|
||||
import AnsibleSelect from '../../../../components/AnsibleSelect';
|
||||
|
||||
export default OrganizationEdit;
|
||||
class OrganizationEdit extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.getInstanceGroups = this.getInstanceGroups.bind(this);
|
||||
this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this);
|
||||
this.checkValidity = this.checkValidity.bind(this);
|
||||
this.onFieldChange = this.onFieldChange.bind(this);
|
||||
this.onLookupSave = this.onLookupSave.bind(this);
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.postInstanceGroups = this.postInstanceGroups.bind(this);
|
||||
this.onCancel = this.onCancel.bind(this);
|
||||
this.onSuccess = this.onSuccess.bind(this);
|
||||
|
||||
this.state = {
|
||||
form: {
|
||||
name: {
|
||||
value: '',
|
||||
isValid: true,
|
||||
validation: {
|
||||
required: true
|
||||
},
|
||||
helperTextInvalid: i18nMark('This field must not be blank')
|
||||
},
|
||||
description: {
|
||||
value: ''
|
||||
},
|
||||
instanceGroups: {
|
||||
value: [],
|
||||
initialValue: []
|
||||
},
|
||||
custom_virtualenv: {
|
||||
value: '',
|
||||
defaultValue: '/venv/ansible/'
|
||||
}
|
||||
},
|
||||
error: '',
|
||||
formIsValid: true
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount () {
|
||||
const { organization } = this.props;
|
||||
const { form: formData } = this.state;
|
||||
|
||||
formData.name.value = organization.name;
|
||||
formData.description.value = organization.description;
|
||||
formData.custom_virtualenv.value = organization.custom_virtualenv;
|
||||
|
||||
try {
|
||||
formData.instanceGroups.value = await this.getRelatedInstanceGroups();
|
||||
formData.instanceGroups.initialValue = [...formData.instanceGroups.value];
|
||||
} catch (err) {
|
||||
this.setState({ error: err });
|
||||
}
|
||||
|
||||
this.setState({ form: formData });
|
||||
}
|
||||
|
||||
onFieldChange (val, evt) {
|
||||
const targetName = evt.target.name;
|
||||
const value = val;
|
||||
|
||||
const { form: updatedForm } = this.state;
|
||||
const updatedFormEl = { ...updatedForm[targetName] };
|
||||
|
||||
updatedFormEl.value = value;
|
||||
updatedForm[targetName] = updatedFormEl;
|
||||
|
||||
updatedFormEl.isValid = (updatedFormEl.validation)
|
||||
? this.checkValidity(updatedFormEl.value, updatedFormEl.validation) : true;
|
||||
|
||||
const formIsValid = (updatedFormEl.validation) ? updatedFormEl.isValid : true;
|
||||
|
||||
this.setState({ form: updatedForm, formIsValid });
|
||||
}
|
||||
|
||||
onLookupSave (val, targetName) {
|
||||
const { form: updatedForm } = this.state;
|
||||
updatedForm[targetName].value = val;
|
||||
|
||||
this.setState({ form: updatedForm });
|
||||
}
|
||||
|
||||
async onSubmit () {
|
||||
const { api, organization } = this.props;
|
||||
const { form: { name, description, custom_virtualenv } } = this.state;
|
||||
const formData = { name, description, custom_virtualenv };
|
||||
|
||||
const updatedData = {};
|
||||
Object.keys(formData)
|
||||
.forEach(formId => {
|
||||
updatedData[formId] = formData[formId].value;
|
||||
});
|
||||
|
||||
try {
|
||||
await api.updateOrganizationDetails(organization.id, updatedData);
|
||||
await this.postInstanceGroups();
|
||||
} catch (err) {
|
||||
this.setState({ error: err });
|
||||
} finally {
|
||||
this.onSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
onCancel () {
|
||||
const { organization: { id }, history } = this.props;
|
||||
history.push(`/organizations/${id}`);
|
||||
}
|
||||
|
||||
onSuccess () {
|
||||
const { organization: { id }, history } = this.props;
|
||||
history.push(`/organizations/${id}`);
|
||||
}
|
||||
|
||||
async getInstanceGroups (params) {
|
||||
const { api } = this.props;
|
||||
const data = await api.getInstanceGroups(params);
|
||||
return data;
|
||||
}
|
||||
|
||||
async getRelatedInstanceGroups () {
|
||||
const {
|
||||
api,
|
||||
organization: { id }
|
||||
} = this.props;
|
||||
const { data } = await api.getOrganizationInstanceGroups(id);
|
||||
const { results } = data;
|
||||
return results;
|
||||
}
|
||||
|
||||
checkValidity = (value, validation) => {
|
||||
const isValid = (validation.required)
|
||||
? (value.trim() !== '') : true;
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
async postInstanceGroups () {
|
||||
const { api, organization } = this.props;
|
||||
const { form: { instanceGroups } } = this.state;
|
||||
const url = organization.related.instance_groups;
|
||||
|
||||
const initialInstanceGroups = instanceGroups.initialValue.map(ig => ig.id);
|
||||
const updatedInstanceGroups = instanceGroups.value.map(ig => ig.id);
|
||||
|
||||
const groupsToAssociate = [...updatedInstanceGroups]
|
||||
.filter(x => !initialInstanceGroups.includes(x));
|
||||
const groupsToDisassociate = [...initialInstanceGroups]
|
||||
.filter(x => !updatedInstanceGroups.includes(x));
|
||||
|
||||
try {
|
||||
await Promise.all(groupsToAssociate.map(async id => {
|
||||
await api.associateInstanceGroup(url, id);
|
||||
}));
|
||||
await Promise.all(groupsToDisassociate.map(async id => {
|
||||
await api.disassociateInstanceGroup(url, id);
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setState({ error: err });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
form: {
|
||||
name,
|
||||
description,
|
||||
instanceGroups,
|
||||
custom_virtualenv
|
||||
},
|
||||
formIsValid,
|
||||
error
|
||||
} = this.state;
|
||||
|
||||
const instanceGroupsLookupColumns = [
|
||||
{ name: i18nMark('Name'), key: 'name', isSortable: true },
|
||||
{ name: i18nMark('Modified'), key: 'modified', isSortable: false, isNumeric: true },
|
||||
{ name: i18nMark('Created'), key: 'created', isSortable: false, isNumeric: true }
|
||||
];
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Form autoComplete="off">
|
||||
<div style={{ display: 'grid', gridGap: '20px', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))' }}>
|
||||
<FormGroup
|
||||
fieldId="edit-org-form-name"
|
||||
helperTextInvalid={name.helperTextInvalid}
|
||||
isRequired
|
||||
isValid={name.isValid}
|
||||
label={i18n._(t`Name`)}
|
||||
>
|
||||
<TextInput
|
||||
id="edit-org-form-name"
|
||||
isRequired
|
||||
isValid={name.isValid}
|
||||
name="name"
|
||||
onChange={this.onFieldChange}
|
||||
value={name.value || ''}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
fieldId="edit-org-form-description"
|
||||
label={i18n._(t`Description`)}
|
||||
>
|
||||
<TextInput
|
||||
id="edit-org-form-description"
|
||||
name="description"
|
||||
onChange={this.onFieldChange}
|
||||
value={description.value || ''}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ConfigContext.Consumer>
|
||||
{({ custom_virtualenvs }) => (
|
||||
custom_virtualenvs && custom_virtualenvs.length > 1 && (
|
||||
<FormGroup
|
||||
fieldId="edit-org-custom-virtualenv"
|
||||
label={i18n._(t`Ansible Environment`)}
|
||||
>
|
||||
<AnsibleSelect
|
||||
data={custom_virtualenvs}
|
||||
defaultSelected={custom_virtualenv.defaultEnv}
|
||||
label={i18n._(t`Ansible Environment`)}
|
||||
name="custom_virtualenv"
|
||||
onChange={this.onFieldChange}
|
||||
value={custom_virtualenv.value || ''}
|
||||
/>
|
||||
</FormGroup>
|
||||
)
|
||||
)}
|
||||
</ConfigContext.Consumer>
|
||||
</div>
|
||||
<FormGroup
|
||||
fieldId="edit-org-form-instance-groups"
|
||||
label={i18n._(t`Instance Groups`)}
|
||||
>
|
||||
<Lookup
|
||||
columns={instanceGroupsLookupColumns}
|
||||
getItems={this.getInstanceGroups}
|
||||
lookupHeader={i18n._(t`Instance Groups`)}
|
||||
name="instanceGroups"
|
||||
onLookupSave={this.onLookupSave}
|
||||
sortedColumnKey="name"
|
||||
value={instanceGroups.value}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormActionGroup
|
||||
onCancel={this.onCancel}
|
||||
onSubmit={this.onSubmit}
|
||||
submitDisabled={!formIsValid}
|
||||
/>
|
||||
{ error ? <div>error</div> : '' }
|
||||
</Form>
|
||||
)}
|
||||
</I18n>
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OrganizationEdit.contextTypes = {
|
||||
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
|
||||
};
|
||||
|
||||
export default withRouter(OrganizationEdit);
|
||||
|
@ -61,7 +61,7 @@ class OrganizationAdd extends React.Component {
|
||||
try {
|
||||
if (instanceGroups.length > 0) {
|
||||
instanceGroups.forEach(async (select) => {
|
||||
await api.createInstanceGroups(instanceGroupsUrl, select.id);
|
||||
await api.associateInstanceGroup(instanceGroupsUrl, select.id);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
|
Loading…
Reference in New Issue
Block a user