mirror of
https://github.com/ansible/awx.git
synced 2026-05-11 03:17:38 -02:30
Merge pull request #144 from keithjgrant/120-add-org-form-cleanup
Refactor org add/edit forms with Formik
This commit is contained in:
5
__tests__/.eslintrc
Normal file
5
__tests__/.eslintrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"react/jsx-pascal-case": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
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 OrganizationForm from '../../../../src/pages/Organizations/components/OrganizationForm';
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
describe('<OrganizationForm />', () => {
|
||||||
|
let api;
|
||||||
|
|
||||||
|
const mockData = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Foo',
|
||||||
|
description: 'Bar',
|
||||||
|
custom_virtualenv: 'Fizz',
|
||||||
|
related: {
|
||||||
|
instance_groups: '/api/v2/organizations/1/instance_groups'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = {
|
||||||
|
getInstanceGroups: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should request related instance groups from api', () => {
|
||||||
|
const mockInstanceGroups = [
|
||||||
|
{ name: 'One', id: 1 },
|
||||||
|
{ name: 'Two', id: 2 }
|
||||||
|
];
|
||||||
|
api.getOrganizationInstanceGroups = jest.fn(() => (
|
||||||
|
Promise.resolve({ data: { results: mockInstanceGroups } })
|
||||||
|
));
|
||||||
|
mount(
|
||||||
|
<I18nProvider>
|
||||||
|
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||||
|
<OrganizationForm
|
||||||
|
api={api}
|
||||||
|
organization={mockData}
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</I18nProvider>
|
||||||
|
).find('OrganizationForm');
|
||||||
|
|
||||||
|
expect(api.getOrganizationInstanceGroups).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('componentDidMount should set instanceGroups to state', async () => {
|
||||||
|
const mockInstanceGroups = [
|
||||||
|
{ name: 'One', id: 1 },
|
||||||
|
{ name: 'Two', id: 2 }
|
||||||
|
];
|
||||||
|
api.getOrganizationInstanceGroups = jest.fn(() => (
|
||||||
|
Promise.resolve({ data: { results: mockInstanceGroups } })
|
||||||
|
));
|
||||||
|
const wrapper = mount(
|
||||||
|
<I18nProvider>
|
||||||
|
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||||
|
<OrganizationForm
|
||||||
|
organization={mockData}
|
||||||
|
api={api}
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</I18nProvider>
|
||||||
|
).find('OrganizationForm');
|
||||||
|
|
||||||
|
await wrapper.instance().componentDidMount();
|
||||||
|
expect(wrapper.state().instanceGroups).toEqual(mockInstanceGroups);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changing instance group successfully sets instanceGroups state', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<OrganizationForm
|
||||||
|
organization={mockData}
|
||||||
|
api={api}
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
).find('OrganizationForm');
|
||||||
|
|
||||||
|
const lookup = wrapper.find('InstanceGroupsLookup');
|
||||||
|
expect(lookup.length).toBe(1);
|
||||||
|
|
||||||
|
lookup.prop('onChange')([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'foo'
|
||||||
|
}
|
||||||
|
], 'instanceGroups');
|
||||||
|
expect(wrapper.state().instanceGroups).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'foo'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changing inputs should update form values', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<OrganizationForm
|
||||||
|
organization={mockData}
|
||||||
|
api={api}
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
).find('OrganizationForm');
|
||||||
|
|
||||||
|
const form = wrapper.find('Formik');
|
||||||
|
wrapper.find('input#edit-org-form-name').simulate('change', {
|
||||||
|
target: { value: 'new foo', name: 'name' }
|
||||||
|
});
|
||||||
|
expect(form.state('values').name).toEqual('new foo');
|
||||||
|
wrapper.find('input#edit-org-form-description').simulate('change', {
|
||||||
|
target: { value: 'new bar', name: 'description' }
|
||||||
|
});
|
||||||
|
expect(form.state('values').description).toEqual('new bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AnsibleSelect component renders if there are virtual environments', () => {
|
||||||
|
const config = {
|
||||||
|
custom_virtualenvs: ['foo', 'bar'],
|
||||||
|
};
|
||||||
|
const wrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<ConfigContext.Provider value={config}>
|
||||||
|
<OrganizationForm
|
||||||
|
organization={mockData}
|
||||||
|
api={api}
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
</ConfigContext.Provider>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('FormSelect')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('FormSelectOption')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls handleSubmit when form submitted', async () => {
|
||||||
|
const handleSubmit = jest.fn();
|
||||||
|
const wrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<OrganizationForm
|
||||||
|
organization={mockData}
|
||||||
|
api={api}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
).find('OrganizationForm');
|
||||||
|
expect(wrapper.prop('handleSubmit')).not.toHaveBeenCalled();
|
||||||
|
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||||
|
await sleep(1);
|
||||||
|
expect(handleSubmit).toHaveBeenCalledWith({
|
||||||
|
name: 'Foo',
|
||||||
|
description: 'Bar',
|
||||||
|
custom_virtualenv: 'Fizz',
|
||||||
|
}, [], []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleSubmit associates and disassociates instance groups', async () => {
|
||||||
|
const mockInstanceGroups = [
|
||||||
|
{ name: 'One', id: 1 },
|
||||||
|
{ name: 'Two', id: 2 }
|
||||||
|
];
|
||||||
|
api.getOrganizationInstanceGroups = jest.fn(() => (
|
||||||
|
Promise.resolve({ data: { results: mockInstanceGroups } })
|
||||||
|
));
|
||||||
|
const mockDataForm = {
|
||||||
|
name: 'Foo',
|
||||||
|
description: 'Bar',
|
||||||
|
custom_virtualenv: 'Fizz',
|
||||||
|
};
|
||||||
|
const handleSubmit = jest.fn();
|
||||||
|
api.updateOrganizationDetails = jest.fn().mockResolvedValue(1, mockDataForm);
|
||||||
|
api.associateInstanceGroup = jest.fn().mockResolvedValue('done');
|
||||||
|
api.disassociate = jest.fn().mockResolvedValue('done');
|
||||||
|
const wrapper = mount(
|
||||||
|
<I18nProvider>
|
||||||
|
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||||
|
<OrganizationForm
|
||||||
|
organization={mockData}
|
||||||
|
api={api}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</I18nProvider>
|
||||||
|
).find('OrganizationForm');
|
||||||
|
|
||||||
|
await wrapper.instance().componentDidMount();
|
||||||
|
|
||||||
|
wrapper.find('InstanceGroupsLookup').prop('onChange')([
|
||||||
|
{ name: 'One', id: 1 },
|
||||||
|
{ name: 'Three', id: 3 }
|
||||||
|
], 'instanceGroups');
|
||||||
|
|
||||||
|
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||||
|
await sleep(0);
|
||||||
|
expect(handleSubmit).toHaveBeenCalledWith(mockDataForm, [3], [2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls "handleCancel" when Cancel button is clicked', () => {
|
||||||
|
const handleCancel = jest.fn();
|
||||||
|
const wrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<OrganizationForm
|
||||||
|
organization={mockData}
|
||||||
|
api={api}
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
/>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(handleCancel).not.toHaveBeenCalled();
|
||||||
|
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
||||||
|
expect(handleCancel).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,11 +2,13 @@ import React from 'react';
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { ConfigContext } from '../../../../../src/context';
|
import OrganizationEdit, { _OrganizationEdit } from '../../../../../src/pages/Organizations/screens/Organization/OrganizationEdit';
|
||||||
import APIClient from '../../../../../src/api';
|
|
||||||
import OrganizationEdit from '../../../../../src/pages/Organizations/screens/Organization/OrganizationEdit';
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
describe('<OrganizationEdit />', () => {
|
describe('<OrganizationEdit />', () => {
|
||||||
|
let api;
|
||||||
|
|
||||||
const mockData = {
|
const mockData = {
|
||||||
name: 'Foo',
|
name: 'Foo',
|
||||||
description: 'Bar',
|
description: 'Bar',
|
||||||
@@ -17,228 +19,93 @@ describe('<OrganizationEdit />', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should request related instance groups from api', () => {
|
beforeEach(() => {
|
||||||
const mockInstanceGroups = [
|
api = {
|
||||||
{ name: 'One', id: 1 },
|
getInstanceGroups: jest.fn(),
|
||||||
{ name: 'Two', id: 2 }
|
updateOrganizationDetails: jest.fn(),
|
||||||
];
|
associateInstanceGroup: jest.fn(),
|
||||||
const getOrganizationInstanceGroups = jest.fn(() => (
|
disassociate: jest.fn(),
|
||||||
Promise.resolve({ data: { results: mockInstanceGroups } })
|
|
||||||
));
|
|
||||||
mount(
|
|
||||||
<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();
|
test('handleSubmit should call api update', () => {
|
||||||
const spy = jest.spyOn(OrganizationEdit.WrappedComponent.prototype, 'onSubmit');
|
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationEdit
|
<OrganizationEdit
|
||||||
match={{
|
|
||||||
path: '/organizations/:id',
|
|
||||||
url: '/organizations/1',
|
|
||||||
params: { id: '1' }
|
|
||||||
}}
|
|
||||||
organization={mockData}
|
organization={mockData}
|
||||||
api={api}
|
api={api}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
expect(spy).not.toHaveBeenCalled();
|
|
||||||
wrapper.find('button[aria-label="Save"]').prop('onClick')();
|
const updatedOrgData = {
|
||||||
expect(spy).toBeCalled();
|
name: 'new name',
|
||||||
});
|
description: 'new description',
|
||||||
test('onSubmit associates and disassociates instance groups', async () => {
|
custom_virtualenv: 'Buzz',
|
||||||
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);
|
wrapper.find('OrganizationForm').prop('handleSubmit')(updatedOrgData, [], []);
|
||||||
const associateInstanceGroupFn = jest.fn().mockResolvedValue('done');
|
|
||||||
const disassociateInstanceGroupFn = jest.fn().mockResolvedValue('done');
|
|
||||||
const api = {
|
|
||||||
getOrganizationInstanceGroups: getOrganizationInstanceGroupsFn,
|
|
||||||
updateOrganizationDetails: updateOrganizationDetailsFn,
|
|
||||||
associateInstanceGroup: associateInstanceGroupFn,
|
|
||||||
disassociate: 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();
|
expect(api.updateOrganizationDetails).toHaveBeenCalledWith(
|
||||||
|
1,
|
||||||
wrapper.instance().onLookupSave([
|
updatedOrgData
|
||||||
{ 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', () => {
|
test('handleSubmit associates and disassociates instance groups', async () => {
|
||||||
const api = jest.fn();
|
|
||||||
const spy = jest.spyOn(OrganizationEdit.WrappedComponent.prototype, 'onCancel');
|
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationEdit
|
<OrganizationEdit
|
||||||
match={{
|
|
||||||
path: '/organizations/:id',
|
|
||||||
url: '/organizations/1',
|
|
||||||
params: { id: '1' }
|
|
||||||
}}
|
|
||||||
organization={mockData}
|
organization={mockData}
|
||||||
api={api}
|
api={api}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
expect(spy).not.toHaveBeenCalled();
|
|
||||||
|
const updatedOrgData = {
|
||||||
|
name: 'new name',
|
||||||
|
description: 'new description',
|
||||||
|
custom_virtualenv: 'Buzz',
|
||||||
|
};
|
||||||
|
wrapper.find('OrganizationForm').prop('handleSubmit')(updatedOrgData, [3, 4], [2]);
|
||||||
|
await sleep(1);
|
||||||
|
|
||||||
|
expect(api.associateInstanceGroup).toHaveBeenCalledWith(
|
||||||
|
'/api/v2/organizations/1/instance_groups',
|
||||||
|
3
|
||||||
|
);
|
||||||
|
expect(api.associateInstanceGroup).toHaveBeenCalledWith(
|
||||||
|
'/api/v2/organizations/1/instance_groups',
|
||||||
|
4
|
||||||
|
);
|
||||||
|
expect(api.disassociate).toHaveBeenCalledWith(
|
||||||
|
'/api/v2/organizations/1/instance_groups',
|
||||||
|
2
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to organization detail when cancel is clicked', () => {
|
||||||
|
const history = {
|
||||||
|
push: jest.fn(),
|
||||||
|
};
|
||||||
|
const wrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<_OrganizationEdit
|
||||||
|
history={history}
|
||||||
|
organization={mockData}
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(history.push).not.toHaveBeenCalled();
|
||||||
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
||||||
expect(spy).toBeCalled();
|
|
||||||
|
expect(history.push).toHaveBeenCalledWith('/organizations/1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,184 +1,152 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { MemoryRouter, Router } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { ConfigContext } from '../../../../src/context';
|
import { ConfigContext } from '../../../../src/context';
|
||||||
import OrganizationAdd from '../../../../src/pages/Organizations/screens/OrganizationAdd';
|
import OrganizationAdd, { _OrganizationAdd } from '../../../../src/pages/Organizations/screens/OrganizationAdd';
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
describe('<OrganizationAdd />', () => {
|
describe('<OrganizationAdd />', () => {
|
||||||
test('initially renders succesfully', () => {
|
let api;
|
||||||
mount(
|
|
||||||
<MemoryRouter>
|
beforeEach(() => {
|
||||||
<I18nProvider>
|
api = {
|
||||||
<OrganizationAdd
|
getInstanceGroups: jest.fn(),
|
||||||
match={{ path: '/organizations/add', url: '/organizations/add' }}
|
createOrganization: jest.fn(),
|
||||||
location={{ search: '', pathname: '/organizations/add' }}
|
associateInstanceGroup: jest.fn(),
|
||||||
/>
|
disassociate: jest.fn(),
|
||||||
</I18nProvider>
|
};
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calls "onFieldChange" when input values change', () => {
|
test('handleSubmit should post to api', () => {
|
||||||
const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onFieldChange');
|
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationAdd
|
<OrganizationAdd
|
||||||
match={{ path: '/organizations/add', url: '/organizations/add' }}
|
api={api}
|
||||||
location={{ search: '', pathname: '/organizations/add' }}
|
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
expect(spy).not.toHaveBeenCalled();
|
|
||||||
wrapper.find('input#add-org-form-name').simulate('change', { target: { value: 'foo' } });
|
const updatedOrgData = {
|
||||||
wrapper.find('input#add-org-form-description').simulate('change', { target: { value: 'bar' } });
|
name: 'new name',
|
||||||
expect(spy).toHaveBeenCalledTimes(2);
|
description: 'new description',
|
||||||
|
custom_virtualenv: 'Buzz',
|
||||||
|
};
|
||||||
|
wrapper.find('OrganizationForm').prop('handleSubmit')(updatedOrgData, [], []);
|
||||||
|
|
||||||
|
expect(api.createOrganization).toHaveBeenCalledWith(updatedOrgData);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calls "onSubmit" when Save button is clicked', () => {
|
test('should navigate to organizations list when cancel is clicked', () => {
|
||||||
const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onSubmit');
|
const history = {
|
||||||
const wrapper = mount(
|
push: jest.fn(),
|
||||||
<MemoryRouter>
|
|
||||||
<I18nProvider>
|
|
||||||
<OrganizationAdd
|
|
||||||
match={{ path: '/organizations/add', url: '/organizations/add' }}
|
|
||||||
location={{ search: '', pathname: '/organizations/add' }}
|
|
||||||
/>
|
|
||||||
</I18nProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
expect(spy).not.toHaveBeenCalled();
|
|
||||||
wrapper.find('button[aria-label="Save"]').prop('onClick')();
|
|
||||||
expect(spy).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('calls "onCancel" when Cancel button is clicked', () => {
|
|
||||||
const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onCancel');
|
|
||||||
const wrapper = mount(
|
|
||||||
<MemoryRouter>
|
|
||||||
<I18nProvider>
|
|
||||||
<OrganizationAdd
|
|
||||||
match={{ path: '/organizations/add', url: '/organizations/add' }}
|
|
||||||
location={{ search: '', pathname: '/organizations/add' }}
|
|
||||||
/>
|
|
||||||
</I18nProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
expect(spy).not.toHaveBeenCalled();
|
|
||||||
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
|
||||||
expect(spy).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('calls "onCancel" when close button (x) is clicked', () => {
|
|
||||||
const wrapper = mount(
|
|
||||||
<MemoryRouter initialEntries={['/organizations/add']} initialIndex={0}>
|
|
||||||
<I18nProvider>
|
|
||||||
<OrganizationAdd
|
|
||||||
match={{ path: '/organizations/add', url: '/organizations/add' }}
|
|
||||||
location={{ search: '', pathname: '/organizations/add' }}
|
|
||||||
/>
|
|
||||||
</I18nProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
const history = wrapper.find(Router).prop('history');
|
|
||||||
expect(history.length).toBe(1);
|
|
||||||
expect(history.location.pathname).toEqual('/organizations/add');
|
|
||||||
wrapper.find('button[aria-label="Close"]').prop('onClick')();
|
|
||||||
expect(history.length).toBe(2);
|
|
||||||
expect(history.location.pathname).toEqual('/organizations');
|
|
||||||
});
|
|
||||||
|
|
||||||
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), associateInstanceGroup: jest.fn().mockResolvedValue('done') };
|
|
||||||
const wrapper = mount(
|
|
||||||
<MemoryRouter>
|
|
||||||
<I18nProvider>
|
|
||||||
<OrganizationAdd api={api} />
|
|
||||||
</I18nProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
wrapper.find('input#add-org-form-name').simulate('change', { target: { value: 'foo' } });
|
|
||||||
wrapper.find('button[aria-label="Save"]').prop('onClick')();
|
|
||||||
setImmediate(() => {
|
|
||||||
expect(onSuccess).toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onLookupSave successfully sets instanceGroups state', () => {
|
|
||||||
const wrapper = mount(
|
|
||||||
<MemoryRouter>
|
|
||||||
<I18nProvider>
|
|
||||||
<OrganizationAdd api={{}} />
|
|
||||||
</I18nProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
).find('OrganizationAdd');
|
|
||||||
wrapper.instance().onLookupSave([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo'
|
|
||||||
}
|
|
||||||
], 'instanceGroups');
|
|
||||||
expect(wrapper.state('instanceGroups')).toEqual([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onFieldChange successfully sets custom_virtualenv state', () => {
|
|
||||||
const wrapper = mount(
|
|
||||||
<MemoryRouter>
|
|
||||||
<I18nProvider>
|
|
||||||
<OrganizationAdd api={{}} />
|
|
||||||
</I18nProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
).find('OrganizationAdd');
|
|
||||||
wrapper.instance().onFieldChange('fooBar', { target: { name: 'custom_virtualenv' } });
|
|
||||||
expect(wrapper.state('custom_virtualenv')).toBe('fooBar');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onSubmit posts instance groups from selectedInstanceGroups', async () => {
|
|
||||||
const createOrganizationFn = jest.fn().mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
id: 1,
|
|
||||||
name: 'mock org',
|
|
||||||
related: {
|
|
||||||
instance_groups: '/api/v2/organizations/1/instance_groups'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const associateInstanceGroupFn = jest.fn().mockResolvedValue('done');
|
|
||||||
const api = {
|
|
||||||
createOrganization: createOrganizationFn,
|
|
||||||
associateInstanceGroup: associateInstanceGroupFn
|
|
||||||
};
|
};
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationAdd api={api} />
|
<_OrganizationAdd
|
||||||
|
history={history}
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
).find('OrganizationAdd');
|
);
|
||||||
wrapper.setState({
|
|
||||||
name: 'mock org',
|
expect(history.push).not.toHaveBeenCalled();
|
||||||
instanceGroups: [{
|
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
||||||
id: 1,
|
|
||||||
name: 'foo'
|
expect(history.push).toHaveBeenCalledWith('/organizations');
|
||||||
}]
|
});
|
||||||
|
|
||||||
|
test('should navigate to organizations list when close (x) is clicked', () => {
|
||||||
|
const history = {
|
||||||
|
push: jest.fn(),
|
||||||
|
};
|
||||||
|
const wrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<_OrganizationAdd
|
||||||
|
history={history}
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(history.push).not.toHaveBeenCalled();
|
||||||
|
wrapper.find('button[aria-label="Close"]').prop('onClick')();
|
||||||
|
|
||||||
|
expect(history.push).toHaveBeenCalledWith('/organizations');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successful form submission should trigger redirect', async () => {
|
||||||
|
const history = {
|
||||||
|
push: jest.fn(),
|
||||||
|
};
|
||||||
|
const orgData = {
|
||||||
|
name: 'new name',
|
||||||
|
description: 'new description',
|
||||||
|
custom_virtualenv: 'Buzz',
|
||||||
|
};
|
||||||
|
api.createOrganization.mockReturnValueOnce({
|
||||||
|
data: {
|
||||||
|
id: 5,
|
||||||
|
related: {
|
||||||
|
instance_groups: '/bar',
|
||||||
|
},
|
||||||
|
...orgData,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
await wrapper.instance().onSubmit();
|
const wrapper = mount(
|
||||||
expect(createOrganizationFn).toHaveBeenCalledWith({
|
<MemoryRouter>
|
||||||
custom_virtualenv: '',
|
<I18nProvider>
|
||||||
description: '',
|
<_OrganizationAdd
|
||||||
name: 'mock org'
|
history={history}
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [], []);
|
||||||
|
await sleep(0);
|
||||||
|
|
||||||
|
expect(history.push).toHaveBeenCalledWith('/organizations/5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleSubmit should post instance groups', async () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<OrganizationAdd
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
const orgData = {
|
||||||
|
name: 'new name',
|
||||||
|
description: 'new description',
|
||||||
|
custom_virtualenv: 'Buzz',
|
||||||
|
};
|
||||||
|
api.createOrganization.mockReturnValueOnce({
|
||||||
|
data: {
|
||||||
|
id: 5,
|
||||||
|
related: {
|
||||||
|
instance_groups: '/api/v2/organizations/5/instance_groups',
|
||||||
|
},
|
||||||
|
...orgData,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
expect(associateInstanceGroupFn).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1);
|
wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [3], []);
|
||||||
|
await sleep(0);
|
||||||
|
|
||||||
|
expect(api.associateInstanceGroup)
|
||||||
|
.toHaveBeenCalledWith('/api/v2/organizations/5/instance_groups', 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('AnsibleSelect component renders if there are virtual environments', () => {
|
test('AnsibleSelect component renders if there are virtual environments', () => {
|
||||||
@@ -189,7 +157,7 @@ describe('<OrganizationAdd />', () => {
|
|||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<ConfigContext.Provider value={config}>
|
<ConfigContext.Provider value={config}>
|
||||||
<OrganizationAdd api={{}} />
|
<OrganizationAdd api={api} />
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@@ -206,7 +174,7 @@ describe('<OrganizationAdd />', () => {
|
|||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<ConfigContext.Provider value={config}>
|
<ConfigContext.Provider value={config}>
|
||||||
<OrganizationAdd api={{}} />
|
<OrganizationAdd api={api} />
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
|
|||||||
34
__tests__/util/validators.test.js
Normal file
34
__tests__/util/validators.test.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { required, maxLength } from '../../src/util/validators';
|
||||||
|
|
||||||
|
describe('validators', () => {
|
||||||
|
test('required returns undefined if value given', () => {
|
||||||
|
expect(required()('some value')).toBeUndefined();
|
||||||
|
expect(required('oops')('some value')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('required returns default message if value missing', () => {
|
||||||
|
expect(required()('')).toEqual('This field must not be blank');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('required returns custom message if value missing', () => {
|
||||||
|
expect(required('oops')('')).toEqual('oops');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('required interprets white space as empty value', () => {
|
||||||
|
expect(required()(' ')).toEqual('This field must not be blank');
|
||||||
|
expect(required()('\t')).toEqual('This field must not be blank');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maxLength accepts value below max', () => {
|
||||||
|
expect(maxLength(10)('snazzy')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maxLength accepts value equal to max', () => {
|
||||||
|
expect(maxLength(10)('abracadbra')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maxLength rejects value above max', () => {
|
||||||
|
expect(maxLength(8)('abracadbra'))
|
||||||
|
.toEqual('This field must not exceed 8 characters');
|
||||||
|
});
|
||||||
|
});
|
||||||
126
package-lock.json
generated
126
package-lock.json
generated
@@ -1656,7 +1656,7 @@
|
|||||||
},
|
},
|
||||||
"ansi-colors": {
|
"ansi-colors": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "http://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz",
|
||||||
"integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==",
|
"integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-wrap": "^0.1.0"
|
"ansi-wrap": "^0.1.0"
|
||||||
@@ -2184,6 +2184,11 @@
|
|||||||
"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
|
"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"asap": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||||
|
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
|
||||||
|
},
|
||||||
"asn1": {
|
"asn1": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||||
@@ -2566,12 +2571,12 @@
|
|||||||
},
|
},
|
||||||
"babel-plugin-syntax-class-properties": {
|
"babel-plugin-syntax-class-properties": {
|
||||||
"version": "6.13.0",
|
"version": "6.13.0",
|
||||||
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz",
|
||||||
"integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94="
|
"integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94="
|
||||||
},
|
},
|
||||||
"babel-plugin-syntax-flow": {
|
"babel-plugin-syntax-flow": {
|
||||||
"version": "6.18.0",
|
"version": "6.18.0",
|
||||||
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz",
|
||||||
"integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0="
|
"integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0="
|
||||||
},
|
},
|
||||||
"babel-plugin-syntax-jsx": {
|
"babel-plugin-syntax-jsx": {
|
||||||
@@ -4139,6 +4144,15 @@
|
|||||||
"sha.js": "^2.4.8"
|
"sha.js": "^2.4.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"create-react-context": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.3.tgz",
|
||||||
|
"integrity": "sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==",
|
||||||
|
"requires": {
|
||||||
|
"fbjs": "^0.8.0",
|
||||||
|
"gud": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"cross-spawn": {
|
"cross-spawn": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
|
||||||
@@ -4379,6 +4393,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
|
||||||
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
|
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
|
||||||
},
|
},
|
||||||
|
"deepmerge": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA=="
|
||||||
|
},
|
||||||
"default-gateway": {
|
"default-gateway": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz",
|
||||||
@@ -4710,7 +4729,7 @@
|
|||||||
},
|
},
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
@@ -4851,7 +4870,6 @@
|
|||||||
"version": "0.1.12",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
|
||||||
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
|
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"iconv-lite": "~0.4.13"
|
"iconv-lite": "~0.4.13"
|
||||||
}
|
}
|
||||||
@@ -5685,6 +5703,27 @@
|
|||||||
"bser": "^2.0.0"
|
"bser": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fbjs": {
|
||||||
|
"version": "0.8.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
|
||||||
|
"integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
|
||||||
|
"requires": {
|
||||||
|
"core-js": "^1.0.0",
|
||||||
|
"isomorphic-fetch": "^2.1.1",
|
||||||
|
"loose-envify": "^1.0.0",
|
||||||
|
"object-assign": "^4.1.0",
|
||||||
|
"promise": "^7.1.1",
|
||||||
|
"setimmediate": "^1.0.5",
|
||||||
|
"ua-parser-js": "^0.7.18"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"core-js": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
|
||||||
|
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"fbjs-scripts": {
|
"fbjs-scripts": {
|
||||||
"version": "0.8.3",
|
"version": "0.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/fbjs-scripts/-/fbjs-scripts-0.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/fbjs-scripts/-/fbjs-scripts-0.8.3.tgz",
|
||||||
@@ -5735,7 +5774,7 @@
|
|||||||
},
|
},
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
@@ -6000,6 +6039,22 @@
|
|||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"formik": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/formik/-/formik-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-FBWGBKQkcCE4d5b5l2fKccD9d1QxNxw/0bQTRvp3EjzA8Bnjmsm9H/Oy0375UA8P3FPmfJkF4cXLLdEqK7fP5A==",
|
||||||
|
"requires": {
|
||||||
|
"create-react-context": "^0.2.2",
|
||||||
|
"deepmerge": "^2.1.1",
|
||||||
|
"hoist-non-react-statics": "^2.5.5",
|
||||||
|
"lodash": "^4.17.11",
|
||||||
|
"lodash-es": "^4.17.11",
|
||||||
|
"prop-types": "^15.6.1",
|
||||||
|
"react-fast-compare": "^2.0.1",
|
||||||
|
"tiny-warning": "^1.0.2",
|
||||||
|
"tslib": "^1.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"forwarded": {
|
"forwarded": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||||
@@ -6830,6 +6885,11 @@
|
|||||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"gud": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
|
||||||
|
},
|
||||||
"handle-thing": {
|
"handle-thing": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",
|
||||||
@@ -8030,8 +8090,7 @@
|
|||||||
"is-stream": {
|
"is-stream": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||||
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
|
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"is-string": {
|
"is-string": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
@@ -8102,6 +8161,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"isomorphic-fetch": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
|
||||||
|
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
|
||||||
|
"requires": {
|
||||||
|
"node-fetch": "^1.0.1",
|
||||||
|
"whatwg-fetch": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"isstream": {
|
"isstream": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||||
@@ -9263,7 +9331,7 @@
|
|||||||
},
|
},
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
|
||||||
"integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ="
|
"integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ="
|
||||||
},
|
},
|
||||||
"kleur": {
|
"kleur": {
|
||||||
@@ -9357,6 +9425,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
|
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
|
||||||
},
|
},
|
||||||
|
"lodash-es": {
|
||||||
|
"version": "4.17.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz",
|
||||||
|
"integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q=="
|
||||||
|
},
|
||||||
"lodash.assign": {
|
"lodash.assign": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
|
||||||
@@ -10116,7 +10189,6 @@
|
|||||||
"version": "1.6.3",
|
"version": "1.6.3",
|
||||||
"resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz",
|
"resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz",
|
||||||
"integrity": "sha1-3CNO3WSJmC1Y6PDbT2lQKavNjAQ=",
|
"integrity": "sha1-3CNO3WSJmC1Y6PDbT2lQKavNjAQ=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"encoding": "^0.1.11",
|
"encoding": "^0.1.11",
|
||||||
"is-stream": "^1.0.1"
|
"is-stream": "^1.0.1"
|
||||||
@@ -11304,6 +11376,14 @@
|
|||||||
"integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
|
"integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"promise": {
|
||||||
|
"version": "7.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||||
|
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||||
|
"requires": {
|
||||||
|
"asap": "~2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"promise-inflight": {
|
"promise-inflight": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
||||||
@@ -11553,6 +11633,11 @@
|
|||||||
"scheduler": "^0.10.0"
|
"scheduler": "^0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-fast-compare": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
|
||||||
|
},
|
||||||
"react-hot-loader": {
|
"react-hot-loader": {
|
||||||
"version": "4.3.11",
|
"version": "4.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.3.11.tgz",
|
||||||
@@ -13013,8 +13098,7 @@
|
|||||||
"setimmediate": {
|
"setimmediate": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
|
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"setprototypeof": {
|
"setprototypeof": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@@ -13938,6 +14022,11 @@
|
|||||||
"setimmediate": "^1.0.4"
|
"setimmediate": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tiny-warning": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q=="
|
||||||
|
},
|
||||||
"tippy.js": {
|
"tippy.js": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-3.4.1.tgz",
|
||||||
@@ -14110,8 +14199,7 @@
|
|||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
|
||||||
"integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
|
"integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"tty-browserify": {
|
"tty-browserify": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -14162,6 +14250,11 @@
|
|||||||
"integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==",
|
"integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ua-parser-js": {
|
||||||
|
"version": "0.7.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",
|
||||||
|
"integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
|
||||||
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "3.4.9",
|
"version": "3.4.9",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
|
||||||
@@ -15561,6 +15654,11 @@
|
|||||||
"iconv-lite": "0.4.24"
|
"iconv-lite": "0.4.24"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"whatwg-fetch": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
|
||||||
|
},
|
||||||
"whatwg-mimetype": {
|
"whatwg-mimetype": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz",
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"@patternfly/react-icons": "^3.6.1",
|
"@patternfly/react-icons": "^3.6.1",
|
||||||
"@patternfly/react-tokens": "^2.2.1",
|
"@patternfly/react-tokens": "^2.2.1",
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
|
"formik": "^1.5.1",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.4.1",
|
"react": "^16.4.1",
|
||||||
"react-dom": "^16.4.1",
|
"react-dom": "^16.4.1",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class AnsibleSelect extends React.Component {
|
|||||||
onSelectChange (val, event) {
|
onSelectChange (val, event) {
|
||||||
const { onChange, name } = this.props;
|
const { onChange, name } = this.props;
|
||||||
event.target.name = name;
|
event.target.name = name;
|
||||||
onChange(val, event);
|
onChange(event, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@@ -24,10 +24,22 @@ class AnsibleSelect extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
<FormSelect value={value} onChange={this.onSelectChange} aria-label={i18n._(t`Select Input`)}>
|
<FormSelect
|
||||||
{data.map((datum) => (datum === defaultSelected
|
value={value}
|
||||||
? (<FormSelectOption key="" value="" label={i18n._(t`Use Default ${label}`)} />) : (<FormSelectOption key={datum} value={datum} label={datum} />)))
|
onChange={this.onSelectChange}
|
||||||
}
|
aria-label={i18n._(t`Select Input`)}
|
||||||
|
>
|
||||||
|
{data.map((datum) => (
|
||||||
|
datum === defaultSelected ? (
|
||||||
|
<FormSelectOption
|
||||||
|
key=""
|
||||||
|
value=""
|
||||||
|
label={i18n._(t`Use Default ${label}`)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormSelectOption key={datum} value={datum} label={datum} />
|
||||||
|
)
|
||||||
|
))}
|
||||||
</FormSelect>
|
</FormSelect>
|
||||||
)}
|
)}
|
||||||
</I18n>
|
</I18n>
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ const FormActionGroup = ({ onSubmit, submitDisabled, onCancel }) => (
|
|||||||
<ActionGroup style={formActionGroupStyle}>
|
<ActionGroup style={formActionGroupStyle}>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarGroup style={buttonGroupStyle}>
|
<ToolbarGroup style={buttonGroupStyle}>
|
||||||
<Button aria-label={i18n._(t`Save`)} variant="primary" onClick={onSubmit} isDisabled={submitDisabled}>{i18n._(t`Save`)}</Button>
|
<Button aria-label={i18n._(t`Save`)} variant="primary" type="submit" onClick={onSubmit} isDisabled={submitDisabled}>{i18n._(t`Save`)}</Button>
|
||||||
</ToolbarGroup>
|
</ToolbarGroup>
|
||||||
<ToolbarGroup>
|
<ToolbarGroup>
|
||||||
<Button aria-label={i18n._(t`Cancel`)} variant="secondary" onClick={onCancel}>{i18n._(t`Cancel`)}</Button>
|
<Button aria-label={i18n._(t`Cancel`)} variant="secondary" type="button" onClick={onCancel}>{i18n._(t`Cancel`)}</Button>
|
||||||
</ToolbarGroup>
|
</ToolbarGroup>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</ActionGroup>
|
</ActionGroup>
|
||||||
|
|||||||
56
src/components/FormField.jsx
Normal file
56
src/components/FormField.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Field } from 'formik';
|
||||||
|
import { FormGroup, TextInput } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function FormField (props) {
|
||||||
|
const { id, name, label, validate, isRequired, ...rest } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
validate={validate}
|
||||||
|
render={({ field, form }) => {
|
||||||
|
const isValid = !form.touched[field.name] || !form.errors[field.name];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
fieldId={id}
|
||||||
|
helperTextInvalid={form.errors[field.name]}
|
||||||
|
isRequired={isRequired}
|
||||||
|
isValid={isValid}
|
||||||
|
label={label}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id={id}
|
||||||
|
isRequired={isRequired}
|
||||||
|
isValid={isValid}
|
||||||
|
{...rest}
|
||||||
|
{...field}
|
||||||
|
onChange={(value, event) => {
|
||||||
|
field.onChange(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FormField.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
type: PropTypes.string,
|
||||||
|
validate: PropTypes.func,
|
||||||
|
isRequired: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
FormField.defaultProps = {
|
||||||
|
type: 'text',
|
||||||
|
validate: () => {},
|
||||||
|
isRequired: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormField;
|
||||||
15
src/components/FormRow.jsx
Normal file
15
src/components/FormRow.jsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function FormRow ({ children }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridGap: '20px',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -157,7 +157,7 @@ class Lookup extends React.Component {
|
|||||||
sortedColumnKey,
|
sortedColumnKey,
|
||||||
sortOrder
|
sortOrder
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { lookupHeader = 'items', value, columns } = this.props;
|
const { id, lookupHeader = 'items', value, columns } = this.props;
|
||||||
|
|
||||||
const chips = value ? (
|
const chips = value ? (
|
||||||
<div className="pf-c-chip-group">
|
<div className="pf-c-chip-group">
|
||||||
@@ -173,7 +173,12 @@ class Lookup extends React.Component {
|
|||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
<div className="pf-c-input-group awx-lookup">
|
<div className="pf-c-input-group awx-lookup">
|
||||||
<Button className="pf-c-input-group__text" aria-label="search" id="search" onClick={this.handleModalToggle}>
|
<Button
|
||||||
|
className="pf-c-input-group__text"
|
||||||
|
aria-label="search"
|
||||||
|
id={id}
|
||||||
|
onClick={this.handleModalToggle}
|
||||||
|
>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="pf-c-form-control">{chips}</div>
|
<div className="pf-c-form-control">{chips}</div>
|
||||||
@@ -248,6 +253,7 @@ class Lookup extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Lookup.propTypes = {
|
Lookup.propTypes = {
|
||||||
|
id: PropTypes.string,
|
||||||
getItems: PropTypes.func.isRequired,
|
getItems: PropTypes.func.isRequired,
|
||||||
lookupHeader: PropTypes.string,
|
lookupHeader: PropTypes.string,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
@@ -256,6 +262,7 @@ Lookup.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Lookup.defaultProps = {
|
Lookup.defaultProps = {
|
||||||
|
id: 'lookup-search',
|
||||||
lookupHeader: 'items',
|
lookupHeader: 'items',
|
||||||
name: null,
|
name: null,
|
||||||
};
|
};
|
||||||
|
|||||||
84
src/pages/Organizations/components/InstanceGroupsLookup.jsx
Normal file
84
src/pages/Organizations/components/InstanceGroupsLookup.jsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { I18n, i18nMark } from '@lingui/react';
|
||||||
|
import { FormGroup, Tooltip } from '@patternfly/react-core';
|
||||||
|
import { QuestionCircleIcon } from '@patternfly/react-icons';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import Lookup from '../../../components/Lookup';
|
||||||
|
|
||||||
|
const INSTANCE_GROUPS_LOOKUP_COLUMNS = [
|
||||||
|
{ 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 }
|
||||||
|
];
|
||||||
|
|
||||||
|
class InstanceGroupsLookup extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.getInstanceGroups = this.getInstanceGroups.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInstanceGroups (params) {
|
||||||
|
const { api } = this.props;
|
||||||
|
const data = await api.getInstanceGroups(params);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { value, tooltip, onChange } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<I18n>
|
||||||
|
{({ i18n }) => (
|
||||||
|
<FormGroup
|
||||||
|
label={(
|
||||||
|
<Fragment>
|
||||||
|
{i18n._(t`Instance Groups`)}
|
||||||
|
{' '}
|
||||||
|
{
|
||||||
|
tooltip && (
|
||||||
|
<Tooltip
|
||||||
|
position="right"
|
||||||
|
content={tooltip}
|
||||||
|
>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
fieldId="org-instance-groups"
|
||||||
|
>
|
||||||
|
<Lookup
|
||||||
|
id="org-instance-groups"
|
||||||
|
lookupHeader={i18n._(t`Instance Groups`)}
|
||||||
|
name="instanceGroups"
|
||||||
|
value={value}
|
||||||
|
onLookupSave={onChange}
|
||||||
|
getItems={this.getInstanceGroups}
|
||||||
|
columns={INSTANCE_GROUPS_LOOKUP_COLUMNS}
|
||||||
|
sortedColumnKey="name"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</I18n>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InstanceGroupsLookup.propTypes = {
|
||||||
|
api: PropTypes.shape({
|
||||||
|
getInstanceGroups: PropTypes.func,
|
||||||
|
}).isRequired,
|
||||||
|
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
tooltip: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
InstanceGroupsLookup.defaultProps = {
|
||||||
|
tooltip: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InstanceGroupsLookup;
|
||||||
178
src/pages/Organizations/components/OrganizationForm.jsx
Normal file
178
src/pages/Organizations/components/OrganizationForm.jsx
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import { Formik, Field } from 'formik';
|
||||||
|
import { I18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormGroup,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import { ConfigContext } from '../../../context';
|
||||||
|
import FormRow from '../../../components/FormRow';
|
||||||
|
import FormField from '../../../components/FormField';
|
||||||
|
import FormActionGroup from '../../../components/FormActionGroup';
|
||||||
|
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||||
|
import InstanceGroupsLookup from './InstanceGroupsLookup';
|
||||||
|
import { required } from '../../../util/validators';
|
||||||
|
|
||||||
|
class OrganizationForm extends Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this);
|
||||||
|
this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
instanceGroups: [],
|
||||||
|
formIsValid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount () {
|
||||||
|
let instanceGroups = [];
|
||||||
|
|
||||||
|
if (!this.isEditingNewOrganization()) {
|
||||||
|
try {
|
||||||
|
instanceGroups = await this.getRelatedInstanceGroups();
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ error: err });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
instanceGroups,
|
||||||
|
initialInstanceGroups: [...instanceGroups],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRelatedInstanceGroups () {
|
||||||
|
const {
|
||||||
|
api,
|
||||||
|
organization: { id }
|
||||||
|
} = this.props;
|
||||||
|
const { data } = await api.getOrganizationInstanceGroups(id);
|
||||||
|
return data.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditingNewOrganization () {
|
||||||
|
const { organization } = this.props;
|
||||||
|
return !organization.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInstanceGroupsChange (instanceGroups) {
|
||||||
|
this.setState({ instanceGroups });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit (values) {
|
||||||
|
const { handleSubmit } = this.props;
|
||||||
|
const { instanceGroups, initialInstanceGroups } = this.state;
|
||||||
|
|
||||||
|
const initialIds = initialInstanceGroups.map(ig => ig.id);
|
||||||
|
const updatedIds = instanceGroups.map(ig => ig.id);
|
||||||
|
const groupsToAssociate = [...updatedIds]
|
||||||
|
.filter(x => !initialIds.includes(x));
|
||||||
|
const groupsToDisassociate = [...initialIds]
|
||||||
|
.filter(x => !updatedIds.includes(x));
|
||||||
|
|
||||||
|
handleSubmit(values, groupsToAssociate, groupsToDisassociate);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { api, organization, handleCancel } = this.props;
|
||||||
|
const { instanceGroups, formIsValid, error } = this.state;
|
||||||
|
const defaultVenv = '/venv/ansible/';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<I18n>
|
||||||
|
{({ i18n }) => (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
name: organization.name,
|
||||||
|
description: organization.description,
|
||||||
|
custom_virtualenv: organization.custom_virtualenv || '',
|
||||||
|
}}
|
||||||
|
onSubmit={this.handleSubmit}
|
||||||
|
render={formik => (
|
||||||
|
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||||
|
<FormRow>
|
||||||
|
<FormField
|
||||||
|
id="org-name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
label={i18n._(t`Name`)}
|
||||||
|
validate={required()}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="org-description"
|
||||||
|
name="description"
|
||||||
|
type="text"
|
||||||
|
label={i18n._(t`Description`)}
|
||||||
|
/>
|
||||||
|
<ConfigContext.Consumer>
|
||||||
|
{({ custom_virtualenvs }) => (
|
||||||
|
custom_virtualenvs && custom_virtualenvs.length > 1 && (
|
||||||
|
<Field
|
||||||
|
name="custom_virtualenv"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormGroup
|
||||||
|
fieldId="org-custom-virtualenv"
|
||||||
|
label={i18n._(t`Ansible Environment`)}
|
||||||
|
>
|
||||||
|
<AnsibleSelect
|
||||||
|
data={custom_virtualenvs}
|
||||||
|
defaultSelected={defaultVenv}
|
||||||
|
label={i18n._(t`Ansible Environment`)}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ConfigContext.Consumer>
|
||||||
|
</FormRow>
|
||||||
|
<InstanceGroupsLookup
|
||||||
|
api={api}
|
||||||
|
value={instanceGroups}
|
||||||
|
onChange={this.handleInstanceGroupsChange}
|
||||||
|
tooltip={i18n._(t`Select the Instance Groups for this Organization to run on.`)}
|
||||||
|
/>
|
||||||
|
<FormActionGroup
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
submitDisabled={!formIsValid}
|
||||||
|
/>
|
||||||
|
{error ? <div>error</div> : null}
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</I18n>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OrganizationForm.propTypes = {
|
||||||
|
api: PropTypes.shape().isRequired,
|
||||||
|
organization: PropTypes.shape(),
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
handleCancel: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
OrganizationForm.defaultProps = {
|
||||||
|
organization: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
custom_virtualenv: '',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
OrganizationForm.contextTypes = {
|
||||||
|
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withRouter(OrganizationForm);
|
||||||
@@ -1,171 +1,50 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { I18n, i18nMark } from '@lingui/react';
|
import { CardBody } from '@patternfly/react-core';
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import {
|
|
||||||
CardBody,
|
|
||||||
Form,
|
|
||||||
FormGroup,
|
|
||||||
TextInput,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { ConfigContext } from '../../../../context';
|
import OrganizationForm from '../../components/OrganizationForm';
|
||||||
import Lookup from '../../../../components/Lookup';
|
|
||||||
import FormActionGroup from '../../../../components/FormActionGroup';
|
|
||||||
import AnsibleSelect from '../../../../components/AnsibleSelect';
|
|
||||||
|
|
||||||
class OrganizationEdit extends Component {
|
class OrganizationEdit extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.getInstanceGroups = this.getInstanceGroups.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this);
|
this.submitInstanceGroups = this.submitInstanceGroups.bind(this);
|
||||||
this.checkValidity = this.checkValidity.bind(this);
|
this.handleCancel = this.handleCancel.bind(this);
|
||||||
this.onFieldChange = this.onFieldChange.bind(this);
|
this.handleSuccess = this.handleSuccess.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 = {
|
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: '',
|
error: '',
|
||||||
formIsValid: true
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount () {
|
async handleSubmit (values, groupsToAssociate, groupsToDisassociate) {
|
||||||
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 { 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 {
|
try {
|
||||||
await api.updateOrganizationDetails(organization.id, updatedData);
|
await api.updateOrganizationDetails(organization.id, values);
|
||||||
await this.postInstanceGroups();
|
await this.submitInstanceGroups(groupsToAssociate, groupsToDisassociate);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: err });
|
this.setState({ error: err });
|
||||||
} finally {
|
} finally {
|
||||||
this.onSuccess();
|
this.handleSuccess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onCancel () {
|
handleCancel () {
|
||||||
const { organization: { id }, history } = this.props;
|
const { organization: { id }, history } = this.props;
|
||||||
history.push(`/organizations/${id}`);
|
history.push(`/organizations/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuccess () {
|
handleSuccess () {
|
||||||
const { organization: { id }, history } = this.props;
|
const { organization: { id }, history } = this.props;
|
||||||
history.push(`/organizations/${id}`);
|
history.push(`/organizations/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInstanceGroups (params) {
|
async submitInstanceGroups (groupsToAssociate, groupsToDisassociate) {
|
||||||
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 { api, organization } = this.props;
|
||||||
const { form: { instanceGroups } } = this.state;
|
|
||||||
const url = organization.related.instance_groups;
|
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 {
|
try {
|
||||||
await Promise.all(groupsToAssociate.map(async id => {
|
await Promise.all(groupsToAssociate.map(async id => {
|
||||||
await api.associateInstanceGroup(url, id);
|
await api.associateInstanceGroup(url, id);
|
||||||
@@ -179,106 +58,31 @@ class OrganizationEdit extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const { api, organization } = this.props;
|
||||||
form: {
|
const { error } = this.state;
|
||||||
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 (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<I18n>
|
<OrganizationForm
|
||||||
{({ i18n }) => (
|
api={api}
|
||||||
<Form autoComplete="off">
|
organization={organization}
|
||||||
<div style={{ display: 'grid', gridGap: '20px', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))' }}>
|
handleSubmit={this.handleSubmit}
|
||||||
<FormGroup
|
handleCancel={this.handleCancel}
|
||||||
fieldId="edit-org-form-name"
|
/>
|
||||||
helperTextInvalid={name.helperTextInvalid}
|
{error ? <div>error</div> : null}
|
||||||
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>
|
</CardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OrganizationEdit.propTypes = {
|
||||||
|
api: PropTypes.shape().isRequired,
|
||||||
|
organization: PropTypes.shape().isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
OrganizationEdit.contextTypes = {
|
OrganizationEdit.contextTypes = {
|
||||||
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
|
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { OrganizationEdit as _OrganizationEdit };
|
||||||
export default withRouter(OrganizationEdit);
|
export default withRouter(OrganizationEdit);
|
||||||
|
|||||||
@@ -1,114 +1,65 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { I18n, i18nMark } from '@lingui/react';
|
import { I18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
PageSection,
|
PageSection,
|
||||||
Form,
|
|
||||||
FormGroup,
|
|
||||||
TextInput,
|
|
||||||
Gallery,
|
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardBody,
|
CardBody,
|
||||||
Button,
|
Button,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { QuestionCircleIcon, TimesIcon } from '@patternfly/react-icons';
|
import { TimesIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
import { ConfigContext } from '../../../context';
|
import OrganizationForm from '../components/OrganizationForm';
|
||||||
import Lookup from '../../../components/Lookup';
|
|
||||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
|
||||||
import FormActionGroup from '../../../components/FormActionGroup';
|
|
||||||
|
|
||||||
class OrganizationAdd extends React.Component {
|
class OrganizationAdd extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.getInstanceGroups = this.getInstanceGroups.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.onFieldChange = this.onFieldChange.bind(this);
|
this.handleCancel = this.handleCancel.bind(this);
|
||||||
this.onLookupSave = this.onLookupSave.bind(this);
|
this.handleSuccess = this.handleSuccess.bind(this);
|
||||||
this.onSubmit = this.onSubmit.bind(this);
|
|
||||||
this.onCancel = this.onCancel.bind(this);
|
|
||||||
this.onSuccess = this.onSuccess.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
custom_virtualenv: '',
|
|
||||||
instanceGroups: [],
|
|
||||||
error: '',
|
error: '',
|
||||||
defaultEnv: '/venv/ansible/',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onFieldChange (val, evt) {
|
async handleSubmit (values, groupsToAssociate) {
|
||||||
this.setState({ [evt.target.name]: val || evt.target.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onLookupSave (val, targetName) {
|
|
||||||
this.setState({ [targetName]: val });
|
|
||||||
}
|
|
||||||
|
|
||||||
async onSubmit () {
|
|
||||||
const { api } = this.props;
|
const { api } = this.props;
|
||||||
const { name, description, custom_virtualenv, instanceGroups } = this.state;
|
|
||||||
const data = {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
custom_virtualenv
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
const { data: response } = await api.createOrganization(data);
|
const { data: response } = await api.createOrganization(values);
|
||||||
const instanceGroupsUrl = response.related.instance_groups;
|
const instanceGroupsUrl = response.related.instance_groups;
|
||||||
try {
|
try {
|
||||||
if (instanceGroups.length > 0) {
|
await Promise.all(groupsToAssociate.map(async id => {
|
||||||
instanceGroups.forEach(async (select) => {
|
await api.associateInstanceGroup(instanceGroupsUrl, id);
|
||||||
await api.associateInstanceGroup(instanceGroupsUrl, select.id);
|
}));
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: err });
|
this.setState({ error: err });
|
||||||
} finally {
|
} finally {
|
||||||
this.onSuccess(response.id);
|
this.handleSuccess(response.id);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: err });
|
this.setState({ error: err });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onCancel () {
|
handleCancel () {
|
||||||
const { history } = this.props;
|
const { history } = this.props;
|
||||||
history.push('/organizations');
|
history.push('/organizations');
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuccess (id) {
|
handleSuccess (id) {
|
||||||
const { history } = this.props;
|
const { history } = this.props;
|
||||||
history.push(`/organizations/${id}`);
|
history.push(`/organizations/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInstanceGroups (params) {
|
|
||||||
const { api } = this.props;
|
|
||||||
const data = await api.getInstanceGroups(params);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const { api } = this.props;
|
||||||
name,
|
const { error } = this.state;
|
||||||
description,
|
|
||||||
custom_virtualenv,
|
|
||||||
defaultEnv,
|
|
||||||
instanceGroups,
|
|
||||||
error
|
|
||||||
} = this.state;
|
|
||||||
const enabled = name.length > 0; // TODO: add better form validation
|
|
||||||
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 (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
@@ -123,99 +74,19 @@ class OrganizationAdd extends React.Component {
|
|||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
aria-label={i18n._(t`Close`)}
|
aria-label={i18n._(t`Close`)}
|
||||||
onClick={this.onCancel}
|
onClick={this.handleCancel}
|
||||||
>
|
>
|
||||||
<TimesIcon />
|
<TimesIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Form autoComplete="off">
|
<OrganizationForm
|
||||||
<Gallery gutter="md">
|
api={api}
|
||||||
<FormGroup
|
handleSubmit={this.handleSubmit}
|
||||||
label={i18n._(t`Name`)}
|
handleCancel={this.handleCancel}
|
||||||
isRequired
|
/>
|
||||||
fieldId="add-org-form-name"
|
{error ? <div>error</div> : ''}
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
isRequired
|
|
||||||
id="add-org-form-name"
|
|
||||||
name="name"
|
|
||||||
value={name}
|
|
||||||
onChange={this.onFieldChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup label={i18n._(t`Description`)} fieldId="add-org-form-description">
|
|
||||||
<TextInput
|
|
||||||
id="add-org-form-description"
|
|
||||||
name="description"
|
|
||||||
value={description}
|
|
||||||
onChange={this.onFieldChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup
|
|
||||||
label={(
|
|
||||||
<Fragment>
|
|
||||||
{i18n._(t`Instance Groups`)}
|
|
||||||
{' '}
|
|
||||||
<Tooltip
|
|
||||||
position="right"
|
|
||||||
content={i18n._(t`Select the Instance Groups for this Organization to run on.`)}
|
|
||||||
>
|
|
||||||
<QuestionCircleIcon />
|
|
||||||
</Tooltip>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
fieldId="add-org-form-instance-groups"
|
|
||||||
>
|
|
||||||
<Lookup
|
|
||||||
lookupHeader={i18n._(t`Instance Groups`)}
|
|
||||||
name="instanceGroups"
|
|
||||||
value={instanceGroups}
|
|
||||||
onLookupSave={this.onLookupSave}
|
|
||||||
getItems={this.getInstanceGroups}
|
|
||||||
columns={instanceGroupsLookupColumns}
|
|
||||||
sortedColumnKey="name"
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<ConfigContext.Consumer>
|
|
||||||
{({ custom_virtualenvs }) => (
|
|
||||||
custom_virtualenvs && custom_virtualenvs.length > 1 && (
|
|
||||||
<FormGroup
|
|
||||||
label={(
|
|
||||||
<Fragment>
|
|
||||||
{i18n._(t`Ansible Environment`)}
|
|
||||||
{' '}
|
|
||||||
<Tooltip
|
|
||||||
position="right"
|
|
||||||
content={i18n._(t`Select the custom Python virtual environment for this organization to run on.`)}
|
|
||||||
>
|
|
||||||
<QuestionCircleIcon />
|
|
||||||
</Tooltip>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
fieldId="add-org-custom-virtualenv"
|
|
||||||
>
|
|
||||||
<AnsibleSelect
|
|
||||||
label={i18n._(t`Ansible Environment`)}
|
|
||||||
name="custom_virtualenv"
|
|
||||||
value={custom_virtualenv}
|
|
||||||
onChange={this.onFieldChange}
|
|
||||||
data={custom_virtualenvs}
|
|
||||||
defaultSelected={defaultEnv}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</ConfigContext.Consumer>
|
|
||||||
</Gallery>
|
|
||||||
<FormActionGroup
|
|
||||||
onSubmit={this.onSubmit}
|
|
||||||
submitDisabled={!enabled}
|
|
||||||
onCancel={this.onCancel}
|
|
||||||
/>
|
|
||||||
{error ? <div>error</div> : ''}
|
|
||||||
</Form>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
@@ -225,8 +96,13 @@ class OrganizationAdd extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OrganizationAdd.propTypes = {
|
||||||
|
api: PropTypes.shape().isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
OrganizationAdd.contextTypes = {
|
OrganizationAdd.contextTypes = {
|
||||||
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
|
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { OrganizationAdd as _OrganizationAdd };
|
||||||
export default withRouter(OrganizationAdd);
|
export default withRouter(OrganizationAdd);
|
||||||
|
|||||||
20
src/util/validators.js
Normal file
20
src/util/validators.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { i18nMark } from '@lingui/react';
|
||||||
|
|
||||||
|
export function required (message) {
|
||||||
|
return value => {
|
||||||
|
if (!value.trim()) {
|
||||||
|
return message || i18nMark('This field must not be blank');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function maxLength (max) {
|
||||||
|
return value => {
|
||||||
|
if (value.trim().length
|
||||||
|
> max) {
|
||||||
|
return i18nMark(`This field must not exceed ${max} characters`);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user