diff --git a/__tests__/components/DataListToolbar.test.jsx b/__tests__/components/DataListToolbar.test.jsx index 98920bc02c..8f966ec739 100644 --- a/__tests__/components/DataListToolbar.test.jsx +++ b/__tests__/components/DataListToolbar.test.jsx @@ -162,6 +162,7 @@ describe('', () => { onSearch={onSearch} onSort={onSort} onSelectAll={onSelectAll} + showDelete /> ); diff --git a/__tests__/components/Lookup.test.jsx b/__tests__/components/Lookup.test.jsx index a331d4a7d1..9402dcbbcc 100644 --- a/__tests__/components/Lookup.test.jsx +++ b/__tests__/components/Lookup.test.jsx @@ -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('', () => { test('initially renders succesfully', () => { mount( @@ -14,6 +17,8 @@ describe('', () => { value={mockData} onLookupSave={() => { }} getItems={() => { }} + columns={mockColumns} + sortedColumnKey="name" /> ); @@ -27,6 +32,8 @@ describe('', () => { value={mockData} onLookupSave={() => { }} getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })} + columns={mockColumns} + sortedColumnKey="name" /> ).find('Lookup'); @@ -47,6 +54,8 @@ describe('', () => { value={mockSelected} onLookupSave={() => { }} getItems={() => { }} + columns={mockColumns} + sortedColumnKey="name" /> ).find('Lookup'); @@ -72,6 +81,8 @@ describe('', () => { value={mockSelected} onLookupSave={() => { }} getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })} + columns={mockColumns} + sortedColumnKey="name" /> ); @@ -94,6 +105,8 @@ describe('', () => { value={mockData} onLookupSave={() => { }} getItems={() => { }} + columns={mockColumns} + sortedColumnKey="name" /> ); @@ -112,6 +125,8 @@ describe('', () => { value={mockData} selected={[]} getItems={() => { }} + columns={mockColumns} + sortedColumnKey="name" /> ); @@ -129,6 +144,8 @@ describe('', () => { value={mockData} selected={[]} getItems={() => { }} + columns={mockColumns} + sortedColumnKey="name" /> ).find('Lookup'); @@ -174,4 +191,42 @@ describe('', () => { name: 'foo' }], 'fooBar'); }); + test('onSort sets state and calls getData ', () => { + const spy = jest.spyOn(Lookup.prototype, 'getData'); + const wrapper = mount( + + { }} + data={mockData} + selected={[]} + columns={mockColumns} + sortedColumnKey="name" + /> + + ).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( + + { }} + data={mockData} + selected={[]} + columns={mockColumns} + sortedColumnKey="name" + /> + + ).find('Lookup'); + wrapper.instance().onSetPage(2, 10); + expect(wrapper.state('page')).toEqual(2); + expect(wrapper.state('page_size')).toEqual(10); + expect(spy).toHaveBeenCalled(); + }); }); diff --git a/__tests__/components/VerticalSeparator.test.jsx b/__tests__/components/VerticalSeparator.test.jsx new file mode 100644 index 0000000000..158ad9a09d --- /dev/null +++ b/__tests__/components/VerticalSeparator.test.jsx @@ -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(); + expect(wrapper).toHaveLength(1); + }); +}); diff --git a/build/locales/en/messages.js b/build/locales/en/messages.js index 7b6fb177b6..47ec354bb5 100644 --- a/build/locales/en/messages.js +++ b/build/locales/en/messages.js @@ -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")]}}}; \ No newline at end of file +/* 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")]}}}; \ No newline at end of file diff --git a/src/app.scss b/src/app.scss index 09ce2c5863..0a1e430ded 100644 --- a/src/app.scss +++ b/src/app.scss @@ -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; } diff --git a/src/components/DataListToolbar/DataListToolbar.jsx b/src/components/DataListToolbar/DataListToolbar.jsx index b6a468c550..a84f086a00 100644 --- a/src/components/DataListToolbar/DataListToolbar.jsx +++ b/src/components/DataListToolbar/DataListToolbar.jsx @@ -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 }) => (
- - + + { showSelectAll && ( @@ -162,10 +167,11 @@ class DataListToolbar extends React.Component { id="select-all" /> + )} - - + +
+
- - - {sortedColumnName} - - )} - dropdownItems={sortDropdownItems} - /> - + { sortDropdownItems.length > 1 && ( + + + {sortedColumnName} + + )} + dropdownItems={sortDropdownItems} + /> + + )} + { (showExpandCollapse || showDelete || addUrl) && ( + + )} {showExpandCollapse && ( @@ -245,6 +258,9 @@ class DataListToolbar extends React.Component {
+ { (showDelete || addUrl) && ( + + )}
)}
diff --git a/src/components/DataListToolbar/styles.scss b/src/components/DataListToolbar/styles.scss index 23ec65dba8..e8d989da3c 100644 --- a/src/components/DataListToolbar/styles.scss +++ b/src/components/DataListToolbar/styles.scss @@ -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; diff --git a/src/components/Lookup/Lookup.jsx b/src/components/Lookup/Lookup.jsx index 0dbfc59813..54fb3dbebf 100644 --- a/src/components/Lookup/Lookup.jsx +++ b/src/components/Lookup/Lookup.jsx @@ -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 ( @@ -157,6 +189,14 @@ class Lookup extends React.Component { ) : ( +
    {results.map(i => ( {label}
+
{selected diff --git a/src/components/SelectedList/styles.scss b/src/components/SelectedList/styles.scss index 4bf49ecfa7..7db18e2c3f 100644 --- a/src/components/SelectedList/styles.scss +++ b/src/components/SelectedList/styles.scss @@ -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; diff --git a/src/components/VerticalSeparator/VerticalSeparator.jsx b/src/components/VerticalSeparator/VerticalSeparator.jsx new file mode 100644 index 0000000000..b86a94537b --- /dev/null +++ b/src/components/VerticalSeparator/VerticalSeparator.jsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const VerticalSeparator = () => ( + +); + +export default VerticalSeparator; diff --git a/src/components/VerticalSeparator/index.js b/src/components/VerticalSeparator/index.js new file mode 100644 index 0000000000..737c49a099 --- /dev/null +++ b/src/components/VerticalSeparator/index.js @@ -0,0 +1,3 @@ +import VerticalSeparator from './VerticalSeparator'; + +export default VerticalSeparator; diff --git a/src/pages/Organizations/screens/OrganizationAdd.jsx b/src/pages/Organizations/screens/OrganizationAdd.jsx index a42a381857..6dbd224781 100644 --- a/src/pages/Organizations/screens/OrganizationAdd.jsx +++ b/src/pages/Organizations/screens/OrganizationAdd.jsx @@ -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 (
- - - - - - - - - - - - {({ custom_virtualenvs }) => ( - custom_virtualenvs && custom_virtualenvs.length > 1 && ( - - - - ) - )} - - + + {({ i18n }) => ( + + + + + + + + + + + + {({ custom_virtualenvs }) => ( + custom_virtualenvs && custom_virtualenvs.length > 1 && ( + + + + ) + )} + + + )} +