Merge remote-tracking branch 'origin/master' into add-select-default-option

This commit is contained in:
kialam
2019-02-14 10:38:28 -05:00
14 changed files with 391 additions and 285 deletions

View File

@@ -8,19 +8,22 @@ describe('<AnsibleSelect />', () => {
test('initially renders succesfully', async () => { test('initially renders succesfully', async () => {
mount( mount(
<AnsibleSelect <AnsibleSelect
selected="foo" value="foo"
selectChange={() => { }} name="bar"
onChange={() => { }}
labelName={label} labelName={label}
data={mockData} data={mockData}
/> />
); );
}); });
test('calls "onSelectChange" on dropdown select change', () => { test('calls "onSelectChange" on dropdown select change', () => {
const spy = jest.spyOn(AnsibleSelect.prototype, 'onSelectChange'); const spy = jest.spyOn(AnsibleSelect.prototype, 'onSelectChange');
const wrapper = mount( const wrapper = mount(
<AnsibleSelect <AnsibleSelect
selected="foo" value="foo"
selectChange={() => { }} name="bar"
onChange={() => { }}
labelName={label} labelName={label}
data={mockData} data={mockData}
/> />
@@ -29,11 +32,13 @@ describe('<AnsibleSelect />', () => {
wrapper.find('select').simulate('change'); wrapper.find('select').simulate('change');
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
test('content not rendered when data property is falsey', () => { test('content not rendered when data property is falsey', () => {
const wrapper = mount( const wrapper = mount(
<AnsibleSelect <AnsibleSelect
selected="foo" value="foo"
selectChange={() => { }} name="bar"
onChange={() => { }}
labelName={label} labelName={label}
data={null} data={null}
/> />

View File

@@ -7,14 +7,35 @@ let mockData = [{ name: 'foo', id: 1, isChecked: false }];
describe('<Lookup />', () => { describe('<Lookup />', () => {
test('initially renders succesfully', () => { test('initially renders succesfully', () => {
mount( mount(
<Lookup <I18nProvider>
lookup_header="Foo Bar" <Lookup
onLookupSave={() => { }} lookup_header="Foo Bar"
data={mockData} name="fooBar"
selected={[]} value={mockData}
/> onLookupSave={() => { }}
getItems={() => { }}
/>
</I18nProvider>
); );
}); });
test('API response is formatted properly', (done) => {
const wrapper = mount(
<I18nProvider>
<Lookup
lookup_header="Foo Bar"
name="fooBar"
value={mockData}
onLookupSave={() => { }}
getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })}
/>
</I18nProvider>
).find('Lookup');
setImmediate(() => {
expect(wrapper.state().results).toEqual([{ id: 1, name: 'test instance' }]);
done();
});
});
test('Opens modal when search icon is clicked', () => { test('Opens modal when search icon is clicked', () => {
const spy = jest.spyOn(Lookup.prototype, 'handleModalToggle'); const spy = jest.spyOn(Lookup.prototype, 'handleModalToggle');
const mockSelected = [{ name: 'foo', id: 1 }]; const mockSelected = [{ name: 'foo', id: 1 }];
@@ -22,9 +43,10 @@ describe('<Lookup />', () => {
<I18nProvider> <I18nProvider>
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
name="fooBar"
value={mockSelected}
onLookupSave={() => { }} onLookupSave={() => { }}
data={mockData} getItems={() => { }}
selected={mockSelected}
/> />
</I18nProvider> </I18nProvider>
).find('Lookup'); ).find('Lookup');
@@ -39,34 +61,39 @@ describe('<Lookup />', () => {
}]); }]);
expect(wrapper.state('isModalOpen')).toEqual(true); expect(wrapper.state('isModalOpen')).toEqual(true);
}); });
test('calls "toggleSelected" when a user changes a checkbox', () => { test('calls "toggleSelected" when a user changes a checkbox', (done) => {
const spy = jest.spyOn(Lookup.prototype, 'toggleSelected'); const spy = jest.spyOn(Lookup.prototype, 'toggleSelected');
const mockSelected = [{ name: 'foo', id: 1 }];
const wrapper = mount( const wrapper = mount(
<I18nProvider> <I18nProvider>
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
name="fooBar"
value={mockSelected}
onLookupSave={() => { }} onLookupSave={() => { }}
data={mockData} getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })}
selected={[]}
/> />
</I18nProvider> </I18nProvider>
); );
const searchItem = wrapper.find('.pf-c-input-group__text#search'); setImmediate(() => {
searchItem.first().simulate('click'); const searchItem = wrapper.find('.pf-c-input-group__text#search');
wrapper.find('input[type="checkbox"]').simulate('change'); searchItem.first().simulate('click');
expect(spy).toHaveBeenCalled(); wrapper.find('input[type="checkbox"]').simulate('change');
expect(spy).toHaveBeenCalled();
done();
});
}); });
test('calls "toggleSelected" when remove icon is clicked', () => { test('calls "toggleSelected" when remove icon is clicked', () => {
const spy = jest.spyOn(Lookup.prototype, 'toggleSelected'); const spy = jest.spyOn(Lookup.prototype, 'toggleSelected');
mockData = [{ name: 'foo', id: 1, isChecked: false }, { name: 'bar', id: 2, isChecked: true }]; mockData = [{ name: 'foo', id: 1, isChecked: false }, { name: 'bar', id: 2, isChecked: true }];
const mockSelected = [{ name: 'foo', id: 1 }, { name: 'bar', id: 2 }];
const wrapper = mount( const wrapper = mount(
<I18nProvider> <I18nProvider>
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
name="fooBar"
value={mockData}
onLookupSave={() => { }} onLookupSave={() => { }}
data={mockData} getItems={() => { }}
selected={mockSelected}
/> />
</I18nProvider> </I18nProvider>
); );
@@ -124,9 +151,10 @@ describe('<Lookup />', () => {
<I18nProvider> <I18nProvider>
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
name="fooBar"
value={mockData}
onLookupSave={onLookupSaveFn} onLookupSave={onLookupSaveFn}
data={mockData} getItems={() => { }}
selected={[]}
/> />
</I18nProvider> </I18nProvider>
).find('Lookup'); ).find('Lookup');
@@ -142,6 +170,6 @@ describe('<Lookup />', () => {
expect(onLookupSaveFn).toHaveBeenCalledWith([{ expect(onLookupSaveFn).toHaveBeenCalledWith([{
id: 1, id: 1,
name: 'foo' name: 'foo'
}]); }], 'fooBar');
}); });
}); });

View File

@@ -1,27 +1,32 @@
import React from 'react'; 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 OrganizationAdd from '../../../../src/pages/Organizations/screens/OrganizationAdd'; import OrganizationAdd from '../../../../src/pages/Organizations/screens/OrganizationAdd';
describe('<OrganizationAdd />', () => { describe('<OrganizationAdd />', () => {
test('initially renders succesfully', () => { test('initially renders succesfully', () => {
mount( mount(
<MemoryRouter> <MemoryRouter>
<OrganizationAdd <I18nProvider>
match={{ path: '/organizations/add', url: '/organizations/add' }} <OrganizationAdd
location={{ search: '', pathname: '/organizations/add' }} match={{ path: '/organizations/add', url: '/organizations/add' }}
/> location={{ search: '', pathname: '/organizations/add' }}
/>
</I18nProvider>
</MemoryRouter> </MemoryRouter>
); );
}); });
test('calls "handleChange" when input values change', () => { test('calls "onFieldChange" when input values change', () => {
const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'handleChange'); const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onFieldChange');
const wrapper = mount( const wrapper = mount(
<MemoryRouter> <MemoryRouter>
<OrganizationAdd <I18nProvider>
match={{ path: '/organizations/add', url: '/organizations/add' }} <OrganizationAdd
location={{ search: '', pathname: '/organizations/add' }} match={{ path: '/organizations/add', url: '/organizations/add' }}
/> location={{ search: '', pathname: '/organizations/add' }}
/>
</I18nProvider>
</MemoryRouter> </MemoryRouter>
); );
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
@@ -33,79 +38,69 @@ describe('<OrganizationAdd />', () => {
const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onSubmit'); const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onSubmit');
const wrapper = mount( const wrapper = mount(
<MemoryRouter> <MemoryRouter>
<OrganizationAdd <I18nProvider>
match={{ path: '/organizations/add', url: '/organizations/add' }} <OrganizationAdd
location={{ search: '', pathname: '/organizations/add' }} match={{ path: '/organizations/add', url: '/organizations/add' }}
/> location={{ search: '', pathname: '/organizations/add' }}
/>
</I18nProvider>
</MemoryRouter> </MemoryRouter>
); );
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
wrapper.find('button.at-C-SubmitButton').prop('onClick')(); wrapper.find('button[aria-label="Save"]').prop('onClick')();
expect(spy).toBeCalled(); expect(spy).toBeCalled();
}); });
test('calls "onCancel" when Cancel button is clicked', () => { test('calls "onCancel" when Cancel button is clicked', () => {
const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onCancel'); const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onCancel');
const wrapper = mount( const wrapper = mount(
<MemoryRouter> <MemoryRouter>
<OrganizationAdd <I18nProvider>
match={{ path: '/organizations/add', url: '/organizations/add' }} <OrganizationAdd
location={{ search: '', pathname: '/organizations/add' }} match={{ path: '/organizations/add', url: '/organizations/add' }}
/> location={{ search: '', pathname: '/organizations/add' }}
/>
</I18nProvider>
</MemoryRouter> </MemoryRouter>
); );
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
wrapper.find('button.at-C-CancelButton').prop('onClick')(); wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
expect(spy).toBeCalled(); expect(spy).toBeCalled();
}); });
test('API response is formatted properly', (done) => {
const mockedResp = { data: { results: [{ name: 'test instance', id: 1 }] } };
const api = { getInstanceGroups: jest.fn().mockResolvedValue(mockedResp) };
const wrapper = mount(
<MemoryRouter>
<OrganizationAdd api={api} />
</MemoryRouter>
);
setImmediate(() => {
const orgAddElem = wrapper.find('OrganizationAdd');
expect([{ id: 1, isChecked: false, name: 'test instance' }]).toEqual(orgAddElem.state().results);
done();
});
});
test('Successful form submission triggers redirect', (done) => { test('Successful form submission triggers redirect', (done) => {
const onSuccess = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onSuccess'); const onSuccess = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onSuccess');
const resetForm = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'resetForm');
const mockedResp = { data: { id: 1, related: { instance_groups: '/bar' } } }; 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), createInstanceGroups: jest.fn().mockResolvedValue('done') };
const wrapper = mount( const wrapper = mount(
<MemoryRouter> <MemoryRouter>
<OrganizationAdd api={api} /> <I18nProvider>
<OrganizationAdd api={api} />
</I18nProvider>
</MemoryRouter> </MemoryRouter>
); );
wrapper.find('input#add-org-form-name').simulate('change', { target: { value: 'foo' } }); wrapper.find('input#add-org-form-name').simulate('change', { target: { value: 'foo' } });
wrapper.find('button.at-C-SubmitButton').prop('onClick')(); wrapper.find('button[aria-label="Save"]').prop('onClick')();
setImmediate(() => { setImmediate(() => {
expect(onSuccess).toHaveBeenCalled(); expect(onSuccess).toHaveBeenCalled();
expect(resetForm).toHaveBeenCalled();
done(); done();
}); });
}); });
test('updateSelectedInstanceGroups successfully sets selectedInstanceGroups state', () => { test('onLookupSave successfully sets instanceGroups state', () => {
const wrapper = mount( const wrapper = mount(
<MemoryRouter> <MemoryRouter>
<OrganizationAdd api={{}} /> <I18nProvider>
<OrganizationAdd api={{}} />
</I18nProvider>
</MemoryRouter> </MemoryRouter>
).find('OrganizationAdd'); ).find('OrganizationAdd');
wrapper.instance().updateSelectedInstanceGroups([ wrapper.instance().onLookupSave([
{ {
id: 1, id: 1,
name: 'foo' name: 'foo'
} }
]); ], 'instanceGroups');
expect(wrapper.state('selectedInstanceGroups')).toEqual([ expect(wrapper.state('instanceGroups')).toEqual([
{ {
id: 1, id: 1,
name: 'foo' name: 'foo'
@@ -113,14 +108,16 @@ describe('<OrganizationAdd />', () => {
]); ]);
}); });
test('onSelectChange successfully sets custom_virtualenv state', () => { test('onFieldChange successfully sets custom_virtualenv state', () => {
const wrapper = mount( const wrapper = mount(
<MemoryRouter> <MemoryRouter>
<OrganizationAdd api={{}} /> <I18nProvider>
<OrganizationAdd api={{}} />
</I18nProvider>
</MemoryRouter> </MemoryRouter>
).find('OrganizationAdd'); ).find('OrganizationAdd');
wrapper.instance().onSelectChange('foobar'); wrapper.instance().onFieldChange('fooBar', { target: { name: 'custom_virtualenv' } });
expect(wrapper.state('custom_virtualenv')).toBe('foobar'); expect(wrapper.state('custom_virtualenv')).toBe('fooBar');
}); });
test('onSubmit posts instance groups from selectedInstanceGroups', async () => { test('onSubmit posts instance groups from selectedInstanceGroups', async () => {
@@ -140,12 +137,14 @@ describe('<OrganizationAdd />', () => {
}; };
const wrapper = mount( const wrapper = mount(
<MemoryRouter> <MemoryRouter>
<OrganizationAdd api={api} /> <I18nProvider>
<OrganizationAdd api={api} />
</I18nProvider>
</MemoryRouter> </MemoryRouter>
).find('OrganizationAdd'); ).find('OrganizationAdd');
wrapper.setState({ wrapper.setState({
name: 'mock org', name: 'mock org',
selectedInstanceGroups: [{ instanceGroups: [{
id: 1, id: 1,
name: 'foo' name: 'foo'
}] }]

View File

@@ -106,8 +106,8 @@ class APIClient {
return this.http.post(endpoint, data); return this.http.post(endpoint, data);
} }
getInstanceGroups () { getInstanceGroups (params) {
return this.http.get(API_INSTANCE_GROUPS); return this.http.get(API_INSTANCE_GROUPS, { params });
} }
createInstanceGroups (url, id) { createInstanceGroups (url, id) {

View File

@@ -157,8 +157,11 @@
// //
.pf-c-modal-box__footer { .pf-c-modal-box__footer {
--pf-c-modal-box__footer--PaddingTop: 0; --pf-c-modal-box__footer--PaddingTop: 20px;
--pf-c-modal-box__footer--PaddingBottom: 0; --pf-c-modal-box__footer--PaddingRight: 20px;
--pf-c-modal-box__footer--PaddingBottom: 20px;
--pf-c-modal-box__footer--PaddingLeft: 20px;
justify-content: flex-end;
} }
.pf-c-modal-box__header { .pf-c-modal-box__header {
@@ -171,6 +174,7 @@
.pf-c-modal-box__body { .pf-c-modal-box__body {
--pf-c-modal-box__body--PaddingLeft: 20px; --pf-c-modal-box__body--PaddingLeft: 20px;
--pf-c-modal-box__body--PaddingRight: 20px; --pf-c-modal-box__body--PaddingRight: 20px;
--pf-c-modal-box__body--PaddingBottom: 5px;
} }
// //
@@ -215,12 +219,6 @@
} }
} }
.at-align-right {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.awx-c-list { .awx-c-list {
border-top: 1px solid #d7d7d7; border-top: 1px solid #d7d7d7;
border-bottom: 1px solid #d7d7d7; border-bottom: 1px solid #d7d7d7;
@@ -240,4 +238,4 @@
--pf-c-card__footer--PaddingY: 0; --pf-c-card__footer--PaddingY: 0;
--pf-c-card__body--PaddingX: 0; --pf-c-card__body--PaddingX: 0;
--pf-c-card__body--PaddingY: 0; --pf-c-card__body--PaddingY: 0;
} }

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { import {
FormGroup,
Select, Select,
SelectOption, SelectOption,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
@@ -25,32 +24,28 @@ class AnsibleSelect extends React.Component {
return null; return null;
} }
onSelectChange (val) { onSelectChange (val, event) {
const { selectChange } = this.props; const { onChange, name } = this.props;
selectChange(val); event.target.name = name;
onChange(val, event);
} }
render () { render () {
const { count } = this.state; const { count } = this.state;
const { labelName, selected, data, defaultSelected } = this.props; const { label = '', value, data, defaultSelected } = this.props;
let elem; let elem;
if (count > 1) { if (count > 1) {
elem = ( elem = (
<FormGroup label={labelName} fieldId="ansible-select"> <Select value={value} onChange={this.onSelectChange} aria-label="Select Input">
<Select value={selected} onChange={this.onSelectChange} aria-label="Select Input"> {data.map((datum) => (datum === defaultSelected
<SelectOption key="" value="" label={`Use Default ${labelName}`} /> ? (<SelectOption key="" value="" label={`Use Default ${label}`} />) : (<SelectOption key={datum} value={datum} label={datum} />)))
{data.map((datum) => (datum !== defaultSelected }
? (<SelectOption key={datum} value={datum} label={datum} />) : null)) </Select>
}
</Select>
</FormGroup>
); );
} else { } else {
elem = null; elem = null;
} }
return elem; return elem;
} }
} }
export default AnsibleSelect; export default AnsibleSelect;

View File

@@ -1,6 +1,6 @@
.awx-toolbar { .awx-toolbar {
--awx-toolbar--BackgroundColor: var(--pf-global--BackgroundColor--light-100); --awx-toolbar--BackgroundColor: var(--pf-global--BackgroundColor--light-100);
--awx-toolbar--BorderColor: var(--pf-global--Color--light-200); --awx-toolbar--BorderColor: #ebebeb;
--awx-toolbar--BorderWidth: var(--pf-global--BorderWidth--sm); --awx-toolbar--BorderWidth: var(--pf-global--BorderWidth--sm);
border-bottom: var(--awx-toolbar--BorderWidth) solid var(--awx-toolbar--BorderColor); border-bottom: var(--awx-toolbar--BorderWidth) solid var(--awx-toolbar--BorderColor);

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
ActionGroup,
Toolbar,
ToolbarGroup,
Button
} from '@patternfly/react-core';
const formActionGroupStyle = {
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: '10px'
};
const buttonGroupStyle = {
marginRight: '20px'
};
export default ({ onSubmit, submitDisabled, onCancel }) => (
<I18n>
{({ i18n }) => (
<ActionGroup style={formActionGroupStyle}>
<Toolbar>
<ToolbarGroup style={buttonGroupStyle}>
<Button aria-label={i18n._(t`Save`)} variant="primary" onClick={onSubmit} isDisabled={submitDisabled}>{i18n._(t`Save`)}</Button>
</ToolbarGroup>
<ToolbarGroup>
<Button aria-label={i18n._(t`Cancel`)} variant="secondary" onClick={onCancel}>{i18n._(t`Cancel`)}</Button>
</ToolbarGroup>
</Toolbar>
</ActionGroup>
)}
</I18n>
);

View File

@@ -1,33 +1,82 @@
import React from 'react'; import React, { Fragment } from 'react';
import { SearchIcon } from '@patternfly/react-icons'; import { SearchIcon, CubesIcon } from '@patternfly/react-icons';
import { import {
Modal, Modal,
Button, Button,
ActionGroup, EmptyState,
Toolbar, EmptyStateIcon,
ToolbarGroup, EmptyStateBody,
Title
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { I18n } from '@lingui/react'; import { I18n } from '@lingui/react';
import { t, Trans } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import CheckboxListItem from '../ListItem'; import CheckboxListItem from '../ListItem';
import SelectedList from '../SelectedList'; import SelectedList from '../SelectedList';
import Pagination from '../Pagination';
const paginationStyling = {
paddingLeft: '0',
justifyContent: 'flex-end',
borderRight: '1px solid #ebebeb',
borderBottom: '1px solid #ebebeb',
borderTop: '0'
};
class Lookup extends React.Component { class Lookup extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
this.state = { this.state = {
isModalOpen: false, isModalOpen: false,
lookupSelectedItems: [] lookupSelectedItems: [],
results: [],
count: 0,
page: 1,
page_size: 5,
error: null
}; };
this.onSetPage = this.onSetPage.bind(this);
this.handleModalToggle = this.handleModalToggle.bind(this); this.handleModalToggle = this.handleModalToggle.bind(this);
this.wrapTags = this.wrapTags.bind(this); this.wrapTags = this.wrapTags.bind(this);
this.toggleSelected = this.toggleSelected.bind(this); this.toggleSelected = this.toggleSelected.bind(this);
this.saveModal = this.saveModal.bind(this); this.saveModal = this.saveModal.bind(this);
this.getData = this.getData.bind(this);
} }
componentDidMount () {
const { page_size, page } = this.state;
this.getData({ page_size, page });
}
async getData (queryParams) {
const { getItems } = this.props;
const { page } = queryParams;
this.setState({ error: false });
try {
const { data } = await getItems(queryParams);
const { results, count } = data;
const stateToUpdate = {
page,
results,
count
};
this.setState(stateToUpdate);
} catch (err) {
this.setState({ error: true });
}
}
onSetPage = async (pageNumber, pageSize) => {
const page = parseInt(pageNumber, 10);
const page_size = parseInt(pageSize, 10);
this.getData({ page_size, page });
};
toggleSelected (row) { toggleSelected (row) {
const { lookupSelectedItems } = this.state; const { lookupSelectedItems } = this.state;
const selectedIndex = lookupSelectedItems const selectedIndex = lookupSelectedItems
@@ -44,12 +93,12 @@ class Lookup extends React.Component {
handleModalToggle () { handleModalToggle () {
const { isModalOpen } = this.state; const { isModalOpen } = this.state;
const { selected } = this.props; const { value } = this.props;
// Resets the selected items from parent state whenever modal is opened // Resets the selected items from parent state whenever modal is opened
// This handles the case where the user closes/cancels the modal and // This handles the case where the user closes/cancels the modal and
// opens it again // opens it again
if (!isModalOpen) { if (!isModalOpen) {
this.setState({ lookupSelectedItems: [...selected] }); this.setState({ lookupSelectedItems: [...value] });
} }
this.setState((prevState) => ({ this.setState((prevState) => ({
isModalOpen: !prevState.isModalOpen, isModalOpen: !prevState.isModalOpen,
@@ -57,13 +106,13 @@ class Lookup extends React.Component {
} }
saveModal () { saveModal () {
const { onLookupSave } = this.props; const { onLookupSave, name } = this.props;
const { lookupSelectedItems } = this.state; const { lookupSelectedItems } = this.state;
onLookupSave(lookupSelectedItems); onLookupSave(lookupSelectedItems, name);
this.handleModalToggle(); this.handleModalToggle();
} }
wrapTags (tags) { wrapTags (tags = []) {
return tags.map(tag => ( return tags.map(tag => (
<span className="awx-c-tag--pill" key={tag.id}> <span className="awx-c-tag--pill" key={tag.id}>
{tag.name} {tag.name}
@@ -75,34 +124,61 @@ class Lookup extends React.Component {
} }
render () { render () {
const { isModalOpen, lookupSelectedItems } = this.state; const { isModalOpen, lookupSelectedItems, error, results, count, page, page_size } = this.state;
const { data, lookupHeader, selected } = this.props; const { lookupHeader = 'items', value } = this.props;
return ( return (
<div className="pf-c-input-group awx-lookup"> <I18n>
<Button className="pf-c-input-group__text" aria-label="search" id="search" onClick={this.handleModalToggle}> {({ i18n }) => (
<SearchIcon /> <div className="pf-c-input-group awx-lookup">
</Button> <Button className="pf-c-input-group__text" aria-label="search" id="search" onClick={this.handleModalToggle}>
<div className="pf-c-form-control">{this.wrapTags(selected)}</div> <SearchIcon />
<Modal </Button>
className="awx-c-modal" <div className="pf-c-form-control">{this.wrapTags(value)}</div>
title={`Select ${lookupHeader}`} <Modal
isOpen={isModalOpen} className="awx-c-modal"
onClose={this.handleModalToggle} title={`Select ${lookupHeader}`}
> isOpen={isModalOpen}
<ul className="pf-c-data-list awx-c-list"> onClose={this.handleModalToggle}
{data.map(i => ( actions={[
<CheckboxListItem <Button key="save" variant="primary" onClick={this.saveModal} style={(results.length === 0) ? { display: 'none' } : {}}>{i18n._(t`Save`)}</Button>,
key={i.id} <Button key="cancel" variant="secondary" onClick={this.handleModalToggle}>{(results.length === 0) ? i18n._(t`Close`) : i18n._(t`Cancel`)}</Button>
itemId={i.id} ]}
name={i.name} >
isSelected={lookupSelectedItems.some(item => item.id === i.id)} {(results.length === 0) ? (
onSelect={() => this.toggleSelected(i)} <EmptyState>
/> <EmptyStateIcon icon={CubesIcon} />
))} <Title size="lg">
</ul> <Trans>{`No ${lookupHeader} Found`}</Trans>
{lookupSelectedItems.length > 0 && ( </Title>
<I18n> <EmptyStateBody>
{({ i18n }) => ( <Trans>{`Please add ${lookupHeader.toLowerCase()} to populate this list`}</Trans>
</EmptyStateBody>
</EmptyState>
) : (
<Fragment>
<ul className="pf-c-data-list awx-c-list">
{results.map(i => (
<CheckboxListItem
key={i.id}
itemId={i.id}
name={i.name}
isSelected={lookupSelectedItems.some(item => item.id === i.id)}
onSelect={() => this.toggleSelected(i)}
/>
))}
</ul>
<Pagination
count={count}
page={page}
pageCount={Math.ceil(count / page_size)}
page_size={page_size}
onSetPage={this.onSetPage}
style={paginationStyling}
/>
</Fragment>
)}
{lookupSelectedItems.length > 0 && (
<SelectedList <SelectedList
label={i18n._(t`Selected`)} label={i18n._(t`Selected`)}
selected={lookupSelectedItems} selected={lookupSelectedItems}
@@ -110,24 +186,11 @@ class Lookup extends React.Component {
onRemove={this.toggleSelected} onRemove={this.toggleSelected}
/> />
)} )}
</I18n> { error ? <div>error</div> : '' }
)} </Modal>
<ActionGroup className="at-align-right"> </div>
<Toolbar> )}
<ToolbarGroup> </I18n>
<Button className="at-C-SubmitButton" variant="primary" onClick={this.saveModal}>
<Trans>Select</Trans>
</Button>
</ToolbarGroup>
<ToolbarGroup>
<Button className="at-C-CancelButton" variant="secondary" onClick={this.handleModalToggle}>
<Trans>Cancel</Trans>
</Button>
</ToolbarGroup>
</Toolbar>
</ActionGroup>
</Modal>
</div>
); );
} }
} }

View File

@@ -105,10 +105,13 @@ class Pagination extends Component {
pageCount, pageCount,
page_size, page_size,
pageSizeOptions, pageSizeOptions,
style
} = this.props; } = this.props;
const { value, isOpen } = this.state; const { value, isOpen } = this.state;
let opts;
const opts = pageSizeOptions.slice().reverse().filter(o => o !== page_size); if (pageSizeOptions) {
opts = pageSizeOptions.slice().reverse().filter(o => o !== page_size);
}
const isOnFirst = page === 1; const isOnFirst = page === 1;
const isOnLast = page === pageCount; const isOnLast = page === pageCount;
@@ -119,33 +122,35 @@ class Pagination extends Component {
return ( return (
<I18n> <I18n>
{({ i18n }) => ( {({ i18n }) => (
<div className="awx-pagination"> <div className="awx-pagination" style={style}>
<div className="awx-pagination__page-size-selection"> {opts && (
<Trans>Items Per Page</Trans> <div className="awx-pagination__page-size-selection">
<Dropdown <Trans>Items Per Page</Trans>
onToggle={this.onTogglePageSize} <Dropdown
onSelect={this.onSelectPageSize} onToggle={this.onTogglePageSize}
direction={up} onSelect={this.onSelectPageSize}
isOpen={isOpen} direction={up}
toggle={( isOpen={isOpen}
<DropdownToggle toggle={(
className="togglePageSize" <DropdownToggle
onToggle={this.onTogglePageSize} className="togglePageSize"
> onToggle={this.onTogglePageSize}
{page_size} >
</DropdownToggle> {page_size}
)} </DropdownToggle>
> )}
{opts.map(option => ( >
<DropdownItem {opts.map(option => (
key={option} <DropdownItem
component="button" key={option}
> component="button"
{option} >
</DropdownItem> {option}
))} </DropdownItem>
</Dropdown> ))}
</div> </Dropdown>
</div>
)}
<div className="awx-pagination__counts"> <div className="awx-pagination__counts">
<div className="awx-pagination__item-count"> <div className="awx-pagination__item-count">
<Trans>{`Items ${itemMin} - ${itemMax} of ${count}`}</Trans> <Trans>{`Items ${itemMin} - ${itemMax} of ${count}`}</Trans>

View File

@@ -1,6 +1,6 @@
.awx-pagination { .awx-pagination {
--awx-pagination--BackgroundColor: var(--pf-global--BackgroundColor--light-100); --awx-pagination--BackgroundColor: var(--pf-global--BackgroundColor--light-100);
--awx-pagination--BorderColor: var(--pf-global--BackgroundColor--light-300); --awx-pagination--BorderColor: #dbdbdb;
--awx-pagination--disabled-BackgroundColor: #f2f2f2; --awx-pagination--disabled-BackgroundColor: #f2f2f2;
--awx-pagination--disabled-Color: #C2C2CA; --awx-pagination--disabled-Color: #C2C2CA;

View File

@@ -3,6 +3,18 @@ import {
Chip Chip
} from '@patternfly/react-core'; } from '@patternfly/react-core';
const selectedRowStyling = {
paddingTop: '15px',
paddingBottom: '5px',
borderLeft: '0',
borderRight: '0'
};
const selectedLabelStyling = {
fontSize: '14px',
fontWeight: 'bold'
};
class SelectedList extends Component { class SelectedList extends Component {
constructor (props) { constructor (props) {
super(props); super(props);
@@ -23,8 +35,8 @@ class SelectedList extends Component {
const { showOverflow } = this.state; const { showOverflow } = this.state;
return ( return (
<div className="awx-selectedList"> <div className="awx-selectedList">
<div className="pf-l-split"> <div className="pf-l-split" style={selectedRowStyling}>
<div className="pf-l-split__item pf-u-align-items-center"> <div className="pf-l-split__item pf-u-align-items-center" style={selectedLabelStyling}>
{label} {label}
</div> </div>
<div className="pf-l-split__item"> <div className="pf-l-split__item">

View File

@@ -1,6 +1,6 @@
.awx-selectedList { .awx-selectedList {
--awx-selectedList--BackgroundColor: var(--pf-global--BackgroundColor--light-100); --awx-selectedList--BackgroundColor: var(--pf-global--BackgroundColor--light-100);
--awx-selectedList--BorderColor: #d7d7d7; --awx-selectedList--BorderColor: #ebebeb;
--awx-selectedList--BorderWidth: var(--pf-global--BorderWidth--sm); --awx-selectedList--BorderWidth: var(--pf-global--BorderWidth--sm);
--awx-selectedList--FontSize: var(--pf-c-chip__text--FontSize); --awx-selectedList--FontSize: var(--pf-c-chip__text--FontSize);

View File

@@ -6,10 +6,6 @@ import {
Form, Form,
FormGroup, FormGroup,
TextInput, TextInput,
ActionGroup,
Toolbar,
ToolbarGroup,
Button,
Gallery, Gallery,
Card, Card,
CardBody, CardBody,
@@ -18,76 +14,57 @@ import {
import { ConfigContext } from '../../../context'; import { ConfigContext } from '../../../context';
import Lookup from '../../../components/Lookup'; import Lookup from '../../../components/Lookup';
import AnsibleSelect from '../../../components/AnsibleSelect'; import AnsibleSelect from '../../../components/AnsibleSelect';
import FormActionGroup from '../../../components/FormActionGroup';
const format = (data) => {
const results = data.results.map((result) => ({
id: result.id,
name: result.name,
isChecked: false
}));
return results;
};
class OrganizationAdd extends React.Component { class OrganizationAdd extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
this.handleChange = this.handleChange.bind(this); this.getInstanceGroups = this.getInstanceGroups.bind(this);
this.onSelectChange = this.onSelectChange.bind(this); this.onFieldChange = this.onFieldChange.bind(this);
this.onLookupSave = this.onLookupSave.bind(this);
this.onSubmit = this.onSubmit.bind(this); this.onSubmit = this.onSubmit.bind(this);
this.resetForm = this.resetForm.bind(this);
this.onSuccess = this.onSuccess.bind(this);
this.onCancel = this.onCancel.bind(this); this.onCancel = this.onCancel.bind(this);
this.updateSelectedInstanceGroups = this.updateSelectedInstanceGroups.bind(this); this.onSuccess = this.onSuccess.bind(this);
this.state = {
name: '',
description: '',
custom_virtualenv: '',
instanceGroups: [],
error: '',
defaultEnv: '/venv/ansible/',
};
} }
state = { onFieldChange (val, evt) {
name: '', this.setState({ [evt.target.name]: val || evt.target.value });
description: '',
results: [],
custom_virtualenv: '',
error: '',
selectedInstanceGroups: [],
defaultEnv: '/venv/ansible/'
};
async componentDidMount () {
const { api } = this.props;
try {
const { data } = await api.getInstanceGroups();
const results = format(data);
this.setState({ results });
} catch (error) {
this.setState({ error });
}
} }
onSelectChange (value) { onLookupSave (val, targetName) {
this.setState({ custom_virtualenv: value }); this.setState({ [targetName]: val });
} }
async onSubmit () { async onSubmit () {
const { api } = this.props; const { api } = this.props;
const { name, description, custom_virtualenv } = this.state; const { name, description, custom_virtualenv, instanceGroups } = this.state;
const data = { const data = {
name, name,
description, description,
custom_virtualenv custom_virtualenv
}; };
const { selectedInstanceGroups } = this.state;
try { try {
const { data: response } = await api.createOrganization(data); const { data: response } = await api.createOrganization(data);
const url = response.related.instance_groups; const instanceGroupsUrl = response.related.instance_groups;
try { try {
if (selectedInstanceGroups.length > 0) { if (instanceGroups.length > 0) {
selectedInstanceGroups.forEach(async (select) => { instanceGroups.forEach(async (select) => {
await api.createInstanceGroups(url, select.id); await api.createInstanceGroups(instanceGroupsUrl, select.id);
}); });
} }
} catch (err) { } catch (err) {
this.setState({ error: err }); this.setState({ error: err });
} finally { } finally {
this.resetForm();
this.onSuccess(response.id); this.onSuccess(response.id);
} }
} catch (err) { } catch (err) {
@@ -105,32 +82,19 @@ class OrganizationAdd extends React.Component {
history.push(`/organizations/${id}`); history.push(`/organizations/${id}`);
} }
updateSelectedInstanceGroups (selectedInstanceGroups) { async getInstanceGroups (params) {
this.setState({ selectedInstanceGroups }); const { api } = this.props;
} const data = await api.getInstanceGroups(params);
return data;
handleChange (_, evt) {
this.setState({ [evt.target.name]: evt.target.value });
}
resetForm () {
this.setState({
name: '',
description: '',
});
const { results } = this.state;
const reset = results.map((result) => ({ id: result.id, name: result.name, isChecked: false }));
this.setState({ results: reset });
} }
render () { render () {
const { const {
name, name,
results,
description, description,
custom_virtualenv, custom_virtualenv,
selectedInstanceGroups,
defaultEnv, defaultEnv,
instanceGroups,
error error
} = this.state; } = this.state;
const enabled = name.length > 0; // TODO: add better form validation const enabled = name.length > 0; // TODO: add better form validation
@@ -148,11 +112,10 @@ class OrganizationAdd extends React.Component {
> >
<TextInput <TextInput
isRequired isRequired
type="text"
id="add-org-form-name" id="add-org-form-name"
name="name" name="name"
value={name} value={name}
onChange={this.handleChange} onChange={this.onFieldChange}
/> />
</FormGroup> </FormGroup>
<FormGroup label="Description" fieldId="add-org-form-description"> <FormGroup label="Description" fieldId="add-org-form-description">
@@ -160,40 +123,39 @@ class OrganizationAdd extends React.Component {
id="add-org-form-description" id="add-org-form-description"
name="description" name="description"
value={description} value={description}
onChange={this.handleChange} onChange={this.onFieldChange}
/> />
</FormGroup> </FormGroup>
<FormGroup label="Instance Groups" fieldId="simple-form-instance-groups"> <FormGroup label="Instance Groups" fieldId="add-org-form-instance-groups">
<Lookup <Lookup
lookupHeader="Instance Groups" lookupHeader="Instance Groups"
onLookupSave={this.updateSelectedInstanceGroups} name="instanceGroups"
data={results} value={instanceGroups}
selected={selectedInstanceGroups} onLookupSave={this.onLookupSave}
getItems={this.getInstanceGroups}
/> />
</FormGroup> </FormGroup>
<ConfigContext.Consumer> <ConfigContext.Consumer>
{({ custom_virtualenvs }) => ( {({ custom_virtualenvs }) => (
<AnsibleSelect <FormGroup label="Ansible Environment" fieldId="add-org-form-custom-virtualenv">
labelName="Ansible Environment" <AnsibleSelect
selected={custom_virtualenv} label="Ansible Environment"
selectChange={this.onSelectChange} name="custom_virtualenv"
data={custom_virtualenvs} value={custom_virtualenv}
defaultSelected={defaultEnv} onChange={this.onFieldChange}
/> data={custom_virtualenvs}
defaultSelected={defaultEnv}
/>
</FormGroup>
)} )}
</ConfigContext.Consumer> </ConfigContext.Consumer>
</Gallery> </Gallery>
<ActionGroup className="at-align-right"> <FormActionGroup
<Toolbar> onSubmit={this.onSubmit}
<ToolbarGroup> submitDisabled={!enabled}
<Button className="at-C-SubmitButton" variant="primary" onClick={this.onSubmit} isDisabled={!enabled}>Save</Button> onCancel={this.onCancel}
</ToolbarGroup> />
<ToolbarGroup> {error ? <div>error</div> : ''}
<Button className="at-C-CancelButton" variant="secondary" onClick={this.onCancel}>Cancel</Button>
</ToolbarGroup>
</Toolbar>
</ActionGroup>
{ error ? <div>error</div> : '' }
</Form> </Form>
</CardBody> </CardBody>
</Card> </Card>