Merge pull request #112 from mabashian/88-sort-filter-ig-modal

Adds sorting to IG lookup on org form
This commit is contained in:
Michael Abashian 2019-02-20 15:53:47 -05:00 committed by GitHub
commit de3cc4637e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 243 additions and 108 deletions

View File

@ -162,6 +162,7 @@ describe('<DataListToolbar />', () => {
onSearch={onSearch}
onSort={onSort}
onSelectAll={onSelectAll}
showDelete
/>
</I18nProvider>
);

View File

@ -3,7 +3,10 @@ import { mount } from 'enzyme';
import { I18nProvider } from '@lingui/react';
import Lookup from '../../src/components/Lookup';
let mockData = [{ name: 'foo', id: 1 }];
let mockData = [{ name: 'foo', id: 1, isChecked: false }];
const mockColumns = [
{ name: 'Name', key: 'name', isSortable: true }
];
describe('<Lookup />', () => {
test('initially renders succesfully', () => {
mount(
@ -14,6 +17,8 @@ describe('<Lookup />', () => {
value={mockData}
onLookupSave={() => { }}
getItems={() => { }}
columns={mockColumns}
sortedColumnKey="name"
/>
</I18nProvider>
);
@ -27,6 +32,8 @@ describe('<Lookup />', () => {
value={mockData}
onLookupSave={() => { }}
getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })}
columns={mockColumns}
sortedColumnKey="name"
/>
</I18nProvider>
).find('Lookup');
@ -47,6 +54,8 @@ describe('<Lookup />', () => {
value={mockSelected}
onLookupSave={() => { }}
getItems={() => { }}
columns={mockColumns}
sortedColumnKey="name"
/>
</I18nProvider>
).find('Lookup');
@ -72,6 +81,8 @@ describe('<Lookup />', () => {
value={mockSelected}
onLookupSave={() => { }}
getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })}
columns={mockColumns}
sortedColumnKey="name"
/>
</I18nProvider>
);
@ -94,6 +105,8 @@ describe('<Lookup />', () => {
value={mockData}
onLookupSave={() => { }}
getItems={() => { }}
columns={mockColumns}
sortedColumnKey="name"
/>
</I18nProvider>
);
@ -112,6 +125,8 @@ describe('<Lookup />', () => {
value={mockData}
selected={[]}
getItems={() => { }}
columns={mockColumns}
sortedColumnKey="name"
/>
</I18nProvider>
);
@ -129,6 +144,8 @@ describe('<Lookup />', () => {
value={mockData}
selected={[]}
getItems={() => { }}
columns={mockColumns}
sortedColumnKey="name"
/>
</I18nProvider>
).find('Lookup');
@ -174,4 +191,42 @@ describe('<Lookup />', () => {
name: 'foo'
}], 'fooBar');
});
test('onSort sets state and calls getData ', () => {
const spy = jest.spyOn(Lookup.prototype, 'getData');
const wrapper = mount(
<I18nProvider>
<Lookup
lookup_header="Foo Bar"
onLookupSave={() => { }}
data={mockData}
selected={[]}
columns={mockColumns}
sortedColumnKey="name"
/>
</I18nProvider>
).find('Lookup');
wrapper.instance().onSort('id', 'descending');
expect(wrapper.state('sortedColumnKey')).toEqual('id');
expect(wrapper.state('sortOrder')).toEqual('descending');
expect(spy).toHaveBeenCalled();
});
test('onSetPage sets state and calls getData ', () => {
const spy = jest.spyOn(Lookup.prototype, 'getData');
const wrapper = mount(
<I18nProvider>
<Lookup
lookup_header="Foo Bar"
onLookupSave={() => { }}
data={mockData}
selected={[]}
columns={mockColumns}
sortedColumnKey="name"
/>
</I18nProvider>
).find('Lookup');
wrapper.instance().onSetPage(2, 10);
expect(wrapper.state('page')).toEqual(2);
expect(wrapper.state('page_size')).toEqual(10);
expect(spy).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,11 @@
import React from 'react';
import { mount } from 'enzyme';
import VerticalSeparator from '../../src/components/VerticalSeparator';
describe('VerticalSeparator', () => {
test('renders the expected content', () => {
const wrapper = mount(<VerticalSeparator />);
expect(wrapper).toHaveLength(1);
});
});

View File

@ -1 +1 @@
/* eslint-disable */module.exports={languageData:{"plurals":function(n,ord){var s=String(n).split("."),v0=!s[1],t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2);if(ord)return n10==1&&n100!=11?"one":n10==2&&n100!=12?"two":n10==3&&n100!=13?"few":"other";return n==1&&v0?"one":"other"}},messages:{"> add":"> add","> edit":"> edit","About":"About","AboutModal Logo":"AboutModal Logo","Access":"Access","Add":"Add","Administration":"Administration","Admins":"Admins","Ansible Version":"Ansible Version","Applications":"Applications","Authentication":"Authentication","Authentication Settings":"Authentication Settings","Brand Image":"Brand Image","Collapse":"Collapse","Copyright 2018 Red Hat, Inc.":"Copyright 2018 Red Hat, Inc.","Created":"Created","Credential Types":"Credential Types","Credentials":"Credentials","Dashboard":"Dashboard","Delete":"Delete","Edit":"Edit","Expand":"Expand","First":"First","Help":"Help","Instance Groups":"Instance Groups","Integrations":"Integrations","Invalid username or password. Please try again.":"Invalid username or password. Please try again.","Inventories":"Inventories","Inventory Scripts":"Inventory Scripts","Jobs":"Jobs","Jobs Settings":"Jobs Settings","Last":"Last","License":"License","Logout":"Logout","Management Jobs":"Management Jobs","Modified":"Modified","My View":"Foo","Name":"Name","Next":"Next","Notification Templates":"Notification Templates","Notifications":"Notifications","Organization Add":"Organization Add","Organization detail tabs":"Organization detail tabs","Organizations":"Organizations","Organizations List":"Organizations List","Page <0/> of {pageCount}":function(a){return["Page <0/> of ",a("pageCount")]},"Page Number":"Page Number","Password":"Password","Per Page":"Per Page","Portal Mode":"Portal Mode","Previous":"Previous","Primary Navigation":"Primary Navigation","Projects":"Projects","Resources":"Resources","Schedules":"Schedules","Search":"Search","Search text input":"Search text input","Select all":"Select all","Settings":"Settings","Sort":"Sort","System":"System","System Settings":"System Settings","Teams":"Teams","Templates":"Templates","Tower Brand Image":"Tower Brand Image","User Details":"User Details","User Interface":"User Interface","User Interface Settings":"User Interface Settings","Username":"Username","Users":"Users","Views":"Views","Welcome to Ansible Tower! Please Sign In.":"Welcome to Ansible Tower! Please Sign In.","add {currentTab}":function(a){return["add ",a("currentTab")]},"adding {currentTab}":function(a){return["adding ",a("currentTab")]},"confirm removal of {currentTab}/cancel and go back to {currentTab} view.":function(a){return["confirm removal of ",a("currentTab"),"/cancel and go back to ",a("currentTab")," view."]},"delete {currentTab}":function(a){return["delete ",a("currentTab")]},"deleting {currentTab} association with orgs":function(a){return["deleting ",a("currentTab")," association with orgs"]},"edit view":"edit view","save/cancel and go back to view":"save/cancel and go back to view","save/cancel and go back to {currentTab} view":function(a){return["save/cancel and go back to ",a("currentTab")," view"]},"select organization {itemId}":function(a){return["select organization ",a("itemId")]},"{0}":function(a){return[a("0")]},"{currentTab} detail view":function(a){return[a("currentTab")," detail view"]},"{itemMin} - {itemMax} of {count}":function(a){return[a("itemMin")," - ",a("itemMax")," of ",a("count")]}}};
/* eslint-disable */module.exports={languageData:{"plurals":function(n,ord){var s=String(n).split("."),v0=!s[1],t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2);if(ord)return n10==1&&n100!=11?"one":n10==2&&n100!=12?"two":n10==3&&n100!=13?"few":"other";return n==1&&v0?"one":"other"}},messages:{"> add":"> add","> edit":"> edit","About":"About","AboutModal Logo":"AboutModal Logo","Access":"Access","Add":"Add","Administration":"Administration","Admins":"Admins","Ansible Version":"Ansible Version","Applications":"Applications","Authentication":"Authentication","Authentication Settings":"Authentication Settings","Brand Image":"Brand Image","Collapse":"Collapse","Copyright 2018 Red Hat, Inc.":"Copyright 2018 Red Hat, Inc.","Created":"Created","Credential Types":"Credential Types","Credentials":"Credentials","Dashboard":"Dashboard","Delete":"Delete","Edit":"Edit","Expand":"Expand","First":"First","Help":"Help","Instance Groups":"Instance Groups","Integrations":"Integrations","Invalid username or password. Please try again.":"Invalid username or password. Please try again.","Inventories":"Inventories","Inventory Scripts":"Inventory Scripts","Jobs":"Jobs","Jobs Settings":"Jobs Settings","Last":"Last","License":"License","Logout":"Logout","Management Jobs":"Management Jobs","Modified":"Modified","My View":"My View","Name":"Name","Next":"Next","Notification Templates":"Notification Templates","Notifications":"Notifications","Organization Add":"Organization Add","Organization detail tabs":"Organization detail tabs","Organizations":"Organizations","Organizations List":"Organizations List","Page <0/> of {pageCount}":function(a){return["Page <0/> of ",a("pageCount")]},"Page Number":"Page Number","Password":"Password","Per Page":"Per Page","Portal Mode":"Portal Mode","Previous":"Previous","Primary Navigation":"Primary Navigation","Projects":"Projects","Resources":"Resources","Schedules":"Schedules","Search":"Search","Search text input":"Search text input","Select all":"Select all","Settings":"Settings","Sort":"Sort","System":"System","System Settings":"System Settings","Teams":"Teams","Templates":"Templates","Tower Brand Image":"Tower Brand Image","User Details":"User Details","User Interface":"User Interface","User Interface Settings":"User Interface Settings","Username":"Username","Users":"Users","Views":"Views","Welcome to Ansible Tower! Please Sign In.":"Welcome to Ansible Tower! Please Sign In.","add {currentTab}":function(a){return["add ",a("currentTab")]},"adding {currentTab}":function(a){return["adding ",a("currentTab")]},"confirm removal of {currentTab}/cancel and go back to {currentTab} view.":function(a){return["confirm removal of ",a("currentTab"),"/cancel and go back to ",a("currentTab")," view."]},"delete {currentTab}":function(a){return["delete ",a("currentTab")]},"deleting {currentTab} association with orgs":function(a){return["deleting ",a("currentTab")," association with orgs"]},"edit view":"edit view","save/cancel and go back to view":"save/cancel and go back to view","save/cancel and go back to {currentTab} view":function(a){return["save/cancel and go back to ",a("currentTab")," view"]},"select organization {itemId}":function(a){return["select organization ",a("itemId")]},"{0}":function(a){return[a("0")]},"{currentTab} detail view":function(a){return[a("currentTab")," detail view"]},"{itemMin} - {itemMax} of {count}":function(a){return[a("itemMin")," - ",a("itemMax")," of ",a("count")]}}};

View File

@ -158,10 +158,13 @@
.awx-c-modal.pf-c-modal-box {
margin: 0;
padding: 20px;
width: 550px;
width: 600px;
.pf-c-button:not(:last-child) {
.pf-c-modal-box__body {
overflow: visible;
}
.pf-c-modal-box__footer > .pf-c-button:not(:last-child) {
margin-right: 20px;
}
}
@ -233,7 +236,6 @@
}
.awx-c-list {
border-top: 1px solid #d7d7d7;
border-bottom: 1px solid #d7d7d7;
}

View File

@ -31,6 +31,11 @@ import {
} from 'react-router-dom';
import Tooltip from '../Tooltip';
import VerticalSeparator from '../VerticalSeparator';
const flexGrowStyling = {
flexGrow: '1'
};
class DataListToolbar extends React.Component {
constructor (props) {
@ -108,10 +113,10 @@ class DataListToolbar extends React.Component {
addUrl,
showExpandCollapse,
showDelete,
showSelectAll
showSelectAll,
isLookup
} = this.props;
const {
// isActionDropdownOpen,
isSearchDropdownOpen,
isSortDropdownOpen,
searchKey,
@ -150,8 +155,8 @@ class DataListToolbar extends React.Component {
{({ i18n }) => (
<div className="awx-toolbar">
<Level>
<LevelItem>
<Toolbar style={{ marginLeft: '20px' }}>
<LevelItem style={{ display: 'flex', flexBasis: '700px' }}>
<Toolbar style={{ marginLeft: isLookup ? '0px' : '20px', flexGrow: '1' }}>
{ showSelectAll && (
<ToolbarGroup>
<ToolbarItem>
@ -162,10 +167,11 @@ class DataListToolbar extends React.Component {
id="select-all"
/>
</ToolbarItem>
<VerticalSeparator />
</ToolbarGroup>
)}
<ToolbarGroup>
<ToolbarItem>
<ToolbarGroup style={flexGrowStyling}>
<ToolbarItem style={flexGrowStyling}>
<div className="pf-c-input-group">
<Dropdown
className="searchKeyDropdown"
@ -187,6 +193,7 @@ class DataListToolbar extends React.Component {
aria-label={i18n._(t`Search text input`)}
value={searchValue}
onChange={this.handleSearchInputChange}
style={{ height: '30px' }}
/>
<Button
variant="tertiary"
@ -197,26 +204,29 @@ class DataListToolbar extends React.Component {
</Button>
</div>
</ToolbarItem>
<VerticalSeparator />
</ToolbarGroup>
<ToolbarGroup
className="sortDropdownGroup"
>
<ToolbarItem>
<Dropdown
onToggle={this.onSortDropdownToggle}
onSelect={this.onSortDropdownSelect}
direction={up}
isOpen={isSortDropdownOpen}
toggle={(
<DropdownToggle
onToggle={this.onSortDropdownToggle}
>
{sortedColumnName}
</DropdownToggle>
)}
dropdownItems={sortDropdownItems}
/>
</ToolbarItem>
{ sortDropdownItems.length > 1 && (
<ToolbarItem>
<Dropdown
onToggle={this.onSortDropdownToggle}
onSelect={this.onSortDropdownSelect}
direction={up}
isOpen={isSortDropdownOpen}
toggle={(
<DropdownToggle
onToggle={this.onSortDropdownToggle}
>
{sortedColumnName}
</DropdownToggle>
)}
dropdownItems={sortDropdownItems}
/>
</ToolbarItem>
)}
<ToolbarItem>
<Button
onClick={this.onSort}
@ -226,6 +236,9 @@ class DataListToolbar extends React.Component {
<SortIcon />
</Button>
</ToolbarItem>
{ (showExpandCollapse || showDelete || addUrl) && (
<VerticalSeparator />
)}
</ToolbarGroup>
{showExpandCollapse && (
<ToolbarGroup>
@ -245,6 +258,9 @@ class DataListToolbar extends React.Component {
<EqualsIcon />
</Button>
</ToolbarItem>
{ (showDelete || addUrl) && (
<VerticalSeparator />
)}
</ToolbarGroup>
)}
</Toolbar>

View File

@ -28,16 +28,6 @@
--pf-l-toolbar__group--MarginLeft: 0px;
}
.awx-toolbar .pf-l-toolbar__group:after {
content: "";
background-color: #d7d7d7;
width: 1px;
height: 30px;
display: block;
margin-left: 20px;
margin-right: 20px;
}
.awx-toolbar button.pf-c-button {
height: 30px;
padding: 0px;
@ -47,12 +37,6 @@
min-height: 0px;
height: 30px;
input {
height: 30px;
padding: 0 10px;
width: 300px;
}
.pf-m-tertiary {
width: 34px;
padding: 0px;

View File

@ -13,6 +13,7 @@ import { I18n } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import CheckboxListItem from '../ListItem';
import DataListToolbar from '../DataListToolbar';
import SelectedList from '../SelectedList';
import Pagination from '../Pagination';
@ -34,7 +35,9 @@ class Lookup extends React.Component {
count: 0,
page: 1,
page_size: 5,
error: null
error: null,
sortOrder: 'ascending',
sortedColumnKey: props.sortedColumnKey
};
this.onSetPage = this.onSetPage.bind(this);
this.handleModalToggle = this.handleModalToggle.bind(this);
@ -42,6 +45,8 @@ class Lookup extends React.Component {
this.toggleSelected = this.toggleSelected.bind(this);
this.saveModal = this.saveModal.bind(this);
this.getData = this.getData.bind(this);
this.onSearch = this.onSearch.bind(this);
this.onSort = this.onSort.bind(this);
}
componentDidMount () {
@ -49,18 +54,35 @@ class Lookup extends React.Component {
this.getData({ page_size, page });
}
async getData (queryParams) {
onSearch () {
const { sortedColumnKey, sortOrder } = this.state;
this.onSort(sortedColumnKey, sortOrder);
}
onSort (sortedColumnKey, sortOrder) {
this.setState({ page: 1, sortedColumnKey, sortOrder }, this.getData);
}
async getData () {
const { getItems } = this.props;
const { page } = queryParams;
const { page, page_size, sortedColumnKey, sortOrder } = this.state;
this.setState({ error: false });
const queryParams = {
page,
page_size
};
if (sortedColumnKey) {
queryParams.order_by = sortOrder === 'descending' ? `-${sortedColumnKey}` : sortedColumnKey;
}
try {
const { data } = await getItems(queryParams);
const { results, count } = data;
const stateToUpdate = {
page,
results,
count
};
@ -74,7 +96,7 @@ class Lookup extends React.Component {
onSetPage = async (pageNumber, pageSize) => {
const page = parseInt(pageNumber, 10);
const page_size = parseInt(pageSize, 10);
this.getData({ page_size, page });
this.setState({ page, page_size }, this.getData);
};
toggleSelected (row) {
@ -124,8 +146,18 @@ class Lookup extends React.Component {
}
render () {
const { isModalOpen, lookupSelectedItems, error, results, count, page, page_size } = this.state;
const { lookupHeader, value } = this.props;
const {
isModalOpen,
lookupSelectedItems,
error,
results,
count,
page,
page_size,
sortedColumnKey,
sortOrder
} = this.state;
const { lookupHeader = 'items', value, columns } = this.props;
return (
<I18n>
@ -157,6 +189,14 @@ class Lookup extends React.Component {
</EmptyState>
) : (
<Fragment>
<DataListToolbar
sortedColumnKey={sortedColumnKey}
sortOrder={sortOrder}
columns={columns}
onSearch={this.onSearch}
onSort={this.onSort}
isLookup
/>
<ul className="pf-c-data-list awx-c-list">
{results.map(i => (
<CheckboxListItem

View File

@ -4,6 +4,8 @@ import {
Chip
} from '@patternfly/react-core';
import VerticalSeparator from '../VerticalSeparator';
const selectedRowStyling = {
paddingTop: '15px',
paddingBottom: '5px',
@ -41,6 +43,7 @@ class SelectedList extends Component {
<div className="pf-l-split__item" style={selectedLabelStyling}>
{label}
</div>
<VerticalSeparator />
<div className="pf-l-split__item">
<div className="pf-c-chip-group">
{selected

View File

@ -15,15 +15,6 @@
white-space: nowrap;
height: 30px;
}
.pf-l-split__item:not(:last-child):after {
content: "";
background-color: var(--awx-selectedList--BorderColor);
width: 1px;
height: 30px;
display: block;
margin-left: 20px;
margin-right: 20px;
}
.pf-c-chip {
margin-right: 10px;
margin-bottom: 10px;

View File

@ -0,0 +1,16 @@
import React from 'react';
const VerticalSeparator = () => (
<span style={{
content: '',
backgroundColor: '#d7d7d7',
width: '1px',
height: '30px',
display: 'block',
marginLeft: '20px',
marginRight: '20px'
}}
/>
);
export default VerticalSeparator;

View File

@ -0,0 +1,3 @@
import VerticalSeparator from './VerticalSeparator';
export default VerticalSeparator;

View File

@ -1,6 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { I18n, i18nMark } from '@lingui/react';
import { t } from '@lingui/macro';
import {
PageSection,
Form,
@ -98,60 +100,71 @@ class OrganizationAdd extends React.Component {
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 (
<PageSection>
<Card>
<CardBody>
<Form autoComplete="off">
<Gallery gutter="md">
<FormGroup
label="Name"
isRequired
fieldId="add-org-form-name"
>
<TextInput
isRequired
id="add-org-form-name"
name="name"
value={name}
onChange={this.onFieldChange}
/>
</FormGroup>
<FormGroup label="Description" fieldId="add-org-form-description">
<TextInput
id="add-org-form-description"
name="description"
value={description}
onChange={this.onFieldChange}
/>
</FormGroup>
<FormGroup label="Instance Groups" fieldId="add-org-form-instance-groups">
<Lookup
lookupHeader="Instance Groups"
name="instanceGroups"
value={instanceGroups}
onLookupSave={this.onLookupSave}
getItems={this.getInstanceGroups}
/>
</FormGroup>
<ConfigContext.Consumer>
{({ custom_virtualenvs }) => (
custom_virtualenvs && custom_virtualenvs.length > 1 && (
<FormGroup label="Ansible Environment" fieldId="add-org-custom-virtualenv">
<AnsibleSelect
label="Ansible Environment"
name="custom_virtualenv"
value={custom_virtualenv}
onChange={this.onFieldChange}
data={custom_virtualenvs}
defaultSelected={defaultEnv}
/>
</FormGroup>
)
)}
</ConfigContext.Consumer>
</Gallery>
<I18n>
{({ i18n }) => (
<Gallery gutter="md">
<FormGroup
label={i18n._(t`Name`)}
isRequired
fieldId="add-org-form-name"
>
<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={i18n._(t`Instance Groups`)} 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={i18n._(t`Ansible Environment`)} 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>
)}
</I18n>
<FormActionGroup
onSubmit={this.onSubmit}
submitDisabled={!enabled}