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
commit f09eb182c2
No known key found for this signature in database
GPG Key ID: 2D0E60E4B8C7EA0F
14 changed files with 391 additions and 285 deletions

View File

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

View File

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

View File

@ -1,27 +1,32 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import OrganizationAdd from '../../../../src/pages/Organizations/screens/OrganizationAdd';
describe('<OrganizationAdd />', () => {
test('initially renders succesfully', () => {
mount(
<MemoryRouter>
<OrganizationAdd
match={{ path: '/organizations/add', url: '/organizations/add' }}
location={{ search: '', pathname: '/organizations/add' }}
/>
<I18nProvider>
<OrganizationAdd
match={{ path: '/organizations/add', url: '/organizations/add' }}
location={{ search: '', pathname: '/organizations/add' }}
/>
</I18nProvider>
</MemoryRouter>
);
});
test('calls "handleChange" when input values change', () => {
const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'handleChange');
test('calls "onFieldChange" when input values change', () => {
const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onFieldChange');
const wrapper = mount(
<MemoryRouter>
<OrganizationAdd
match={{ path: '/organizations/add', url: '/organizations/add' }}
location={{ search: '', pathname: '/organizations/add' }}
/>
<I18nProvider>
<OrganizationAdd
match={{ path: '/organizations/add', url: '/organizations/add' }}
location={{ search: '', pathname: '/organizations/add' }}
/>
</I18nProvider>
</MemoryRouter>
);
expect(spy).not.toHaveBeenCalled();
@ -33,79 +38,69 @@ describe('<OrganizationAdd />', () => {
const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onSubmit');
const wrapper = mount(
<MemoryRouter>
<OrganizationAdd
match={{ path: '/organizations/add', url: '/organizations/add' }}
location={{ search: '', pathname: '/organizations/add' }}
/>
<I18nProvider>
<OrganizationAdd
match={{ path: '/organizations/add', url: '/organizations/add' }}
location={{ search: '', pathname: '/organizations/add' }}
/>
</I18nProvider>
</MemoryRouter>
);
expect(spy).not.toHaveBeenCalled();
wrapper.find('button.at-C-SubmitButton').prop('onClick')();
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>
<OrganizationAdd
match={{ path: '/organizations/add', url: '/organizations/add' }}
location={{ search: '', pathname: '/organizations/add' }}
/>
<I18nProvider>
<OrganizationAdd
match={{ path: '/organizations/add', url: '/organizations/add' }}
location={{ search: '', pathname: '/organizations/add' }}
/>
</I18nProvider>
</MemoryRouter>
);
expect(spy).not.toHaveBeenCalled();
wrapper.find('button.at-C-CancelButton').prop('onClick')();
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
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) => {
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 api = { createOrganization: jest.fn().mockResolvedValue(mockedResp), createInstanceGroups: jest.fn().mockResolvedValue('done') };
const wrapper = mount(
<MemoryRouter>
<OrganizationAdd api={api} />
<I18nProvider>
<OrganizationAdd api={api} />
</I18nProvider>
</MemoryRouter>
);
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(() => {
expect(onSuccess).toHaveBeenCalled();
expect(resetForm).toHaveBeenCalled();
done();
});
});
test('updateSelectedInstanceGroups successfully sets selectedInstanceGroups state', () => {
test('onLookupSave successfully sets instanceGroups state', () => {
const wrapper = mount(
<MemoryRouter>
<OrganizationAdd api={{}} />
<I18nProvider>
<OrganizationAdd api={{}} />
</I18nProvider>
</MemoryRouter>
).find('OrganizationAdd');
wrapper.instance().updateSelectedInstanceGroups([
wrapper.instance().onLookupSave([
{
id: 1,
name: 'foo'
}
]);
expect(wrapper.state('selectedInstanceGroups')).toEqual([
], 'instanceGroups');
expect(wrapper.state('instanceGroups')).toEqual([
{
id: 1,
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(
<MemoryRouter>
<OrganizationAdd api={{}} />
<I18nProvider>
<OrganizationAdd api={{}} />
</I18nProvider>
</MemoryRouter>
).find('OrganizationAdd');
wrapper.instance().onSelectChange('foobar');
expect(wrapper.state('custom_virtualenv')).toBe('foobar');
wrapper.instance().onFieldChange('fooBar', { target: { name: 'custom_virtualenv' } });
expect(wrapper.state('custom_virtualenv')).toBe('fooBar');
});
test('onSubmit posts instance groups from selectedInstanceGroups', async () => {
@ -140,12 +137,14 @@ describe('<OrganizationAdd />', () => {
};
const wrapper = mount(
<MemoryRouter>
<OrganizationAdd api={api} />
<I18nProvider>
<OrganizationAdd api={api} />
</I18nProvider>
</MemoryRouter>
).find('OrganizationAdd');
wrapper.setState({
name: 'mock org',
selectedInstanceGroups: [{
instanceGroups: [{
id: 1,
name: 'foo'
}]

View File

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

View File

@ -157,8 +157,11 @@
//
.pf-c-modal-box__footer {
--pf-c-modal-box__footer--PaddingTop: 0;
--pf-c-modal-box__footer--PaddingBottom: 0;
--pf-c-modal-box__footer--PaddingTop: 20px;
--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 {
@ -171,6 +174,7 @@
.pf-c-modal-box__body {
--pf-c-modal-box__body--PaddingLeft: 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 {
border-top: 1px solid #d7d7d7;
border-bottom: 1px solid #d7d7d7;
@ -240,4 +238,4 @@
--pf-c-card__footer--PaddingY: 0;
--pf-c-card__body--PaddingX: 0;
--pf-c-card__body--PaddingY: 0;
}
}

View File

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

View File

@ -1,6 +1,6 @@
.awx-toolbar {
--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);
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 {
Modal,
Button,
ActionGroup,
Toolbar,
ToolbarGroup,
EmptyState,
EmptyStateIcon,
EmptyStateBody,
Title
} from '@patternfly/react-core';
import { I18n } from '@lingui/react';
import { t, Trans } from '@lingui/macro';
import { Trans, t } from '@lingui/macro';
import CheckboxListItem from '../ListItem';
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 {
constructor (props) {
super(props);
this.state = {
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.wrapTags = this.wrapTags.bind(this);
this.toggleSelected = this.toggleSelected.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) {
const { lookupSelectedItems } = this.state;
const selectedIndex = lookupSelectedItems
@ -44,12 +93,12 @@ class Lookup extends React.Component {
handleModalToggle () {
const { isModalOpen } = this.state;
const { selected } = this.props;
const { value } = this.props;
// Resets the selected items from parent state whenever modal is opened
// This handles the case where the user closes/cancels the modal and
// opens it again
if (!isModalOpen) {
this.setState({ lookupSelectedItems: [...selected] });
this.setState({ lookupSelectedItems: [...value] });
}
this.setState((prevState) => ({
isModalOpen: !prevState.isModalOpen,
@ -57,13 +106,13 @@ class Lookup extends React.Component {
}
saveModal () {
const { onLookupSave } = this.props;
const { onLookupSave, name } = this.props;
const { lookupSelectedItems } = this.state;
onLookupSave(lookupSelectedItems);
onLookupSave(lookupSelectedItems, name);
this.handleModalToggle();
}
wrapTags (tags) {
wrapTags (tags = []) {
return tags.map(tag => (
<span className="awx-c-tag--pill" key={tag.id}>
{tag.name}
@ -75,34 +124,61 @@ class Lookup extends React.Component {
}
render () {
const { isModalOpen, lookupSelectedItems } = this.state;
const { data, lookupHeader, selected } = this.props;
const { isModalOpen, lookupSelectedItems, error, results, count, page, page_size } = this.state;
const { lookupHeader = 'items', value } = this.props;
return (
<div className="pf-c-input-group awx-lookup">
<Button className="pf-c-input-group__text" aria-label="search" id="search" onClick={this.handleModalToggle}>
<SearchIcon />
</Button>
<div className="pf-c-form-control">{this.wrapTags(selected)}</div>
<Modal
className="awx-c-modal"
title={`Select ${lookupHeader}`}
isOpen={isModalOpen}
onClose={this.handleModalToggle}
>
<ul className="pf-c-data-list awx-c-list">
{data.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>
{lookupSelectedItems.length > 0 && (
<I18n>
{({ i18n }) => (
<I18n>
{({ i18n }) => (
<div className="pf-c-input-group awx-lookup">
<Button className="pf-c-input-group__text" aria-label="search" id="search" onClick={this.handleModalToggle}>
<SearchIcon />
</Button>
<div className="pf-c-form-control">{this.wrapTags(value)}</div>
<Modal
className="awx-c-modal"
title={`Select ${lookupHeader}`}
isOpen={isModalOpen}
onClose={this.handleModalToggle}
actions={[
<Button key="save" variant="primary" onClick={this.saveModal} style={(results.length === 0) ? { display: 'none' } : {}}>{i18n._(t`Save`)}</Button>,
<Button key="cancel" variant="secondary" onClick={this.handleModalToggle}>{(results.length === 0) ? i18n._(t`Close`) : i18n._(t`Cancel`)}</Button>
]}
>
{(results.length === 0) ? (
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
<Title size="lg">
<Trans>{`No ${lookupHeader} Found`}</Trans>
</Title>
<EmptyStateBody>
<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
label={i18n._(t`Selected`)}
selected={lookupSelectedItems}
@ -110,24 +186,11 @@ class Lookup extends React.Component {
onRemove={this.toggleSelected}
/>
)}
</I18n>
)}
<ActionGroup className="at-align-right">
<Toolbar>
<ToolbarGroup>
<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>
{ error ? <div>error</div> : '' }
</Modal>
</div>
)}
</I18n>
);
}
}

View File

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

View File

@ -1,6 +1,6 @@
.awx-pagination {
--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-Color: #C2C2CA;

View File

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

View File

@ -1,6 +1,6 @@
.awx-selectedList {
--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--FontSize: var(--pf-c-chip__text--FontSize);

View File

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