mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 11:00:03 -03:30
Merge pull request #5 from ansible/pf4-table
basic pf4 data list, toolbar, pagination
This commit is contained in:
commit
7fdf27eece
@ -1,4 +1,3 @@
|
||||
|
||||
import mockAxios from 'axios';
|
||||
import APIClient from '../src/api';
|
||||
import * as endpoints from '../src/endpoints';
|
||||
|
||||
57
__tests__/components/DataListToolbar.test.jsx
Normal file
57
__tests__/components/DataListToolbar.test.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import DataListToolbar from '../../src/components/DataListToolbar';
|
||||
|
||||
describe('<DataListToolbar />', () => {
|
||||
const columns = [{ name: 'Name', key: 'name', isSortable: true }];
|
||||
const noop = () => {};
|
||||
|
||||
let toolbar;
|
||||
|
||||
afterEach(() => {
|
||||
if (toolbar) {
|
||||
toolbar.unmount();
|
||||
toolbar = null;
|
||||
}
|
||||
});
|
||||
|
||||
test('it triggers the expected callbacks', () => {
|
||||
const search = 'button[aria-label="Search"]';
|
||||
const searchTextInput = 'input[aria-label="search text input"]';
|
||||
const selectAll = 'input[aria-label="Select all"]';
|
||||
const sort = 'button[aria-label="Sort"]';
|
||||
|
||||
const onSearch = jest.fn();
|
||||
const onSort = jest.fn();
|
||||
const onSelectAll = jest.fn();
|
||||
|
||||
toolbar = mount(
|
||||
<DataListToolbar
|
||||
isAllSelected={false}
|
||||
sortedColumnKey="name"
|
||||
sortOrder="ascending"
|
||||
columns={columns}
|
||||
onSearch={onSearch}
|
||||
onSort={onSort}
|
||||
onSelectAll={onSelectAll}
|
||||
/>
|
||||
);
|
||||
|
||||
toolbar.find(sort).simulate('click');
|
||||
toolbar.find(selectAll).simulate('change', { target: { checked: false } });
|
||||
|
||||
expect(onSelectAll).toHaveBeenCalledTimes(1);
|
||||
expect(onSort).toHaveBeenCalledTimes(1);
|
||||
expect(onSort).toBeCalledWith('name', 'descending');
|
||||
|
||||
expect(onSelectAll).toHaveBeenCalledTimes(1);
|
||||
expect(onSelectAll.mock.calls[0][0]).toBe(false);
|
||||
|
||||
toolbar.find(searchTextInput).instance().value = 'test-321';
|
||||
toolbar.find(searchTextInput).simulate('change');
|
||||
toolbar.find(search).simulate('click');
|
||||
|
||||
expect(onSearch).toHaveBeenCalledTimes(1);
|
||||
expect(onSearch).toBeCalledWith('test-321');
|
||||
});
|
||||
});
|
||||
72
__tests__/components/Pagination.test.jsx
Normal file
72
__tests__/components/Pagination.test.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import Pagination from '../../src/components/Pagination';
|
||||
|
||||
describe('<Pagination />', () => {
|
||||
const noop = () => {};
|
||||
|
||||
let pagination;
|
||||
|
||||
afterEach(() => {
|
||||
if (toolbar) {
|
||||
pagination.unmount();
|
||||
pagination = null;
|
||||
}
|
||||
});
|
||||
|
||||
test('it triggers the expected callbacks on next and last', () => {
|
||||
const next = 'button[aria-label="next"]';
|
||||
const last = 'button[aria-label="last"]';
|
||||
|
||||
const onSetPage = jest.fn();
|
||||
|
||||
pagination = mount(
|
||||
<Pagination
|
||||
count={21}
|
||||
page={1}
|
||||
pageCount={5}
|
||||
page_size={5}
|
||||
pageSizeOptions={[5, 10, 25, 50]}
|
||||
onSetPage={onSetPage}
|
||||
/>
|
||||
);
|
||||
|
||||
pagination.find(next).simulate('click');
|
||||
|
||||
expect(onSetPage).toHaveBeenCalledTimes(1);
|
||||
expect(onSetPage).toBeCalledWith(2, 5);
|
||||
|
||||
pagination.find(last).simulate('click');
|
||||
|
||||
expect(onSetPage).toHaveBeenCalledTimes(2);
|
||||
expect(onSetPage).toBeCalledWith(5, 5);
|
||||
});
|
||||
|
||||
test('it triggers the expected callback on previous and first', () => {
|
||||
const previous = 'button[aria-label="previous"]';
|
||||
const first = 'button[aria-label="first"]';
|
||||
|
||||
const onSetPage = jest.fn();
|
||||
|
||||
pagination = mount(
|
||||
<Pagination
|
||||
count={21}
|
||||
page={5}
|
||||
pageCount={5}
|
||||
page_size={5}
|
||||
pageSizeOptions={[5, 10, 25, 50]}
|
||||
onSetPage={onSetPage}
|
||||
/>
|
||||
);
|
||||
|
||||
pagination.find(previous).simulate('click');
|
||||
|
||||
expect(onSetPage).toHaveBeenCalledTimes(1);
|
||||
expect(onSetPage).toBeCalledWith(4, 5);
|
||||
|
||||
pagination.find(first).simulate('click');
|
||||
|
||||
expect(onSetPage).toHaveBeenCalledTimes(2);
|
||||
expect(onSetPage).toBeCalledWith(1, 5);
|
||||
});
|
||||
});
|
||||
@ -1,40 +1,66 @@
|
||||
import React from 'react';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import api from '../../src/api';
|
||||
import { API_ORGANIZATIONS } from '../../src/endpoints';
|
||||
import Organizations from '../../src/pages/Organizations';
|
||||
|
||||
describe('<Organizations />', () => {
|
||||
let pageWrapper;
|
||||
let pageSections;
|
||||
let title;
|
||||
let gallery;
|
||||
let galleryItems;
|
||||
let orgCards;
|
||||
|
||||
const orgs = [
|
||||
{ id: 1, name: 'org 1' },
|
||||
{ id: 2, name: 'org 2' },
|
||||
{ id: 3, name: 'org 3' }
|
||||
const results = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'org 1',
|
||||
summary_fields: {
|
||||
related_field_counts: {
|
||||
users: 1,
|
||||
teams: 1,
|
||||
admins: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'org 2',
|
||||
summary_fields: {
|
||||
related_field_counts: {
|
||||
users: 1,
|
||||
teams: 1,
|
||||
admins: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'org 3',
|
||||
summary_fields: {
|
||||
related_field_counts: {
|
||||
users: 1,
|
||||
teams: 1,
|
||||
admins: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const findOrgCards = () => {
|
||||
galleryItems = pageWrapper.find('GalleryItem');
|
||||
orgCards = pageWrapper.find('OrganizationCard');
|
||||
};
|
||||
const count = results.length;
|
||||
const response = { data: { count, results } };
|
||||
|
||||
beforeEach(() => {
|
||||
pageWrapper = mount(<Organizations />);
|
||||
pageSections = pageWrapper.find('PageSection');
|
||||
title = pageWrapper.find('Title');
|
||||
gallery = pageWrapper.find('Gallery');
|
||||
findOrgCards();
|
||||
api.get = jest.fn().mockImplementation(() => Promise.resolve(response));
|
||||
pageWrapper = mount(<HashRouter><Organizations /></HashRouter>);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
pageWrapper.unmount();
|
||||
});
|
||||
|
||||
test('initially renders without crashing', () => {
|
||||
test('it renders expected content', () => {
|
||||
const pageSections = pageWrapper.find('PageSection');
|
||||
const title = pageWrapper.find('Title');
|
||||
|
||||
expect(pageWrapper.length).toBe(1);
|
||||
expect(pageSections.length).toBe(2);
|
||||
expect(title.length).toBe(1);
|
||||
@ -42,19 +68,23 @@ describe('<Organizations />', () => {
|
||||
pageSections.forEach(section => {
|
||||
expect(section.props().variant).toBeDefined();
|
||||
});
|
||||
expect(gallery.length).toBe(1);
|
||||
// by default, no galleryItems or orgCards
|
||||
expect(galleryItems.length).toBe(0);
|
||||
expect(orgCards.length).toBe(0);
|
||||
// will render all orgCards if state is set
|
||||
pageWrapper.setState({ organizations: orgs });
|
||||
findOrgCards();
|
||||
expect(galleryItems.length).toBe(3);
|
||||
expect(orgCards.length).toBe(3);
|
||||
expect(pageWrapper.find('ul').length).toBe(1);
|
||||
expect(pageWrapper.find('ul li').length).toBe(0);
|
||||
// will render all list items on update
|
||||
pageWrapper.update();
|
||||
expect(pageWrapper.find('ul li').length).toBe(count);
|
||||
});
|
||||
|
||||
test('API Organization endpoint is valid', () => {
|
||||
expect(API_ORGANIZATIONS).toBeDefined();
|
||||
});
|
||||
|
||||
test('it displays a tooltip on delete hover', () => {
|
||||
const tooltip = '.pf-c-tooltip__content';
|
||||
const deleteButton = 'button[aria-label="Delete"]';
|
||||
|
||||
expect(pageWrapper.find(tooltip).length).toBe(0);
|
||||
pageWrapper.find(deleteButton).simulate('mouseover');
|
||||
expect(pageWrapper.find(tooltip).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
29
__tests__/qs.test.js
Normal file
29
__tests__/qs.test.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { encodeQueryString, parseQueryString } from '../src/qs';
|
||||
|
||||
describe('qs (qs.js)', () => {
|
||||
test('encodeQueryString returns the expected queryString', () => {
|
||||
[
|
||||
[null, ''],
|
||||
[{}, ''],
|
||||
[{ order_by: 'name', page: 1, page_size: 5 }, 'order_by=name&page=1&page_size=5'],
|
||||
[{ '-order_by': 'name', page: '1', page_size: 5 }, '-order_by=name&page=1&page_size=5'],
|
||||
]
|
||||
.forEach(([params, expectedQueryString]) => {
|
||||
const actualQueryString = encodeQueryString(params);
|
||||
|
||||
expect(actualQueryString).toEqual(expectedQueryString);
|
||||
});
|
||||
});
|
||||
|
||||
test('parseQueryString returns the expected queryParams', () => {
|
||||
[
|
||||
['order_by=name&page=1&page_size=5', ['page', 'page_size'], { order_by: 'name', page: 1, page_size: 5 }],
|
||||
['order_by=name&page=1&page_size=5', ['page_size'], { order_by: 'name', page: '1', page_size: 5 }],
|
||||
]
|
||||
.forEach(([queryString, integerFields, expectedQueryParams]) => {
|
||||
const actualQueryParams = parseQueryString(queryString, integerFields);
|
||||
|
||||
expect(actualQueryParams).toEqual(expectedQueryParams)
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -44,7 +44,7 @@ class APIClient {
|
||||
await this.http.post(endpoints.API_LOGIN, data, { headers });
|
||||
}
|
||||
|
||||
get = (endpoint) => this.http.get(endpoint);
|
||||
get = (endpoint, params = {}) => this.http.get(endpoint, { params });
|
||||
}
|
||||
|
||||
export default new APIClient();
|
||||
|
||||
52
src/app.scss
52
src/app.scss
@ -59,4 +59,54 @@
|
||||
.pf-l-page__main-section.pf-m-condensed {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// toolbar overrides
|
||||
//
|
||||
|
||||
.pf-l-toolbar {
|
||||
align-self: center;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// data list overrides
|
||||
//
|
||||
|
||||
.pf-c-data-list {
|
||||
--pf-global--target-size--MinHeight: 32px;
|
||||
--pf-global--target-size--MinWidth: 32px;
|
||||
--pf-global--FontSize--md: 14px;
|
||||
|
||||
--pf-c-data-list__item--PaddingTop: 16px;
|
||||
--pf-c-data-list__item--PaddingBottom: 16px;
|
||||
}
|
||||
|
||||
.pf-c-data-list__item {
|
||||
--pf-c-data-list__item--PaddingLeft: 20px;
|
||||
--pf-c-data-list__item--PaddingRight: 0px;
|
||||
}
|
||||
|
||||
.pf-c-data-list__check {
|
||||
--pf-c-data-list__check--MarginRight: 0;
|
||||
}
|
||||
|
||||
.pf-c-data-list__check:after {
|
||||
content: "";
|
||||
background-color: #d7d7d7;
|
||||
width: 1px;
|
||||
height: 25px;
|
||||
display: block;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.pf-c-data-list__cell a {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pf-c-data-list__cell span {
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
226
src/components/DataListToolbar/DataListToolbar.jsx
Normal file
226
src/components/DataListToolbar/DataListToolbar.jsx
Normal file
@ -0,0 +1,226 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
DropdownPosition,
|
||||
DropdownToggle,
|
||||
DropdownItem,
|
||||
FormGroup,
|
||||
KebabToggle,
|
||||
Level,
|
||||
LevelItem,
|
||||
TextInput,
|
||||
Toolbar,
|
||||
ToolbarGroup,
|
||||
ToolbarItem,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
BarsIcon,
|
||||
EqualsIcon,
|
||||
SortAlphaDownIcon,
|
||||
SortAlphaUpIcon,
|
||||
SortNumericDownIcon,
|
||||
SortNumericUpIcon,
|
||||
TrashAltIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
import Tooltip from '../Tooltip';
|
||||
|
||||
class DataListToolbar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { sortedColumnKey } = this.props;
|
||||
|
||||
this.state = {
|
||||
isSearchDropdownOpen: false,
|
||||
isSortDropdownOpen: false,
|
||||
searchKey: sortedColumnKey,
|
||||
searchValue: '',
|
||||
};
|
||||
}
|
||||
|
||||
handleSearchInputChange = searchValue => {
|
||||
this.setState({ searchValue });
|
||||
};
|
||||
|
||||
onSortDropdownToggle = isSortDropdownOpen => {
|
||||
this.setState({ isSortDropdownOpen });
|
||||
};
|
||||
|
||||
onSortDropdownSelect = ({ target }) => {
|
||||
const { columns, onSort, sortOrder } = this.props;
|
||||
|
||||
const [{ key }] = columns.filter(({ name }) => name === target.innerText);
|
||||
|
||||
this.setState({ isSortDropdownOpen: false });
|
||||
|
||||
onSort(key, sortOrder);
|
||||
};
|
||||
|
||||
onSearchDropdownToggle = isSearchDropdownOpen => {
|
||||
this.setState({ isSearchDropdownOpen });
|
||||
};
|
||||
|
||||
onSearchDropdownSelect = ({ target }) => {
|
||||
const { columns } = this.props;
|
||||
|
||||
const targetName = target.innerText;
|
||||
const [{ key }] = columns.filter(({ name }) => name === targetName);
|
||||
|
||||
this.setState({ isSearchDropdownOpen: false, searchKey: key });
|
||||
};
|
||||
|
||||
onActionToggle = isActionDropdownOpen => {
|
||||
this.setState({ isActionDropdownOpen });
|
||||
};
|
||||
|
||||
onActionSelect = ({ target }) => {
|
||||
this.setState({ isActionDropdownOpen: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { up } = DropdownPosition;
|
||||
const {
|
||||
columns,
|
||||
isAllSelected,
|
||||
onSearch,
|
||||
onSelectAll,
|
||||
onSort,
|
||||
sortedColumnKey,
|
||||
sortOrder,
|
||||
} = this.props;
|
||||
const {
|
||||
isActionDropdownOpen,
|
||||
isSearchDropdownOpen,
|
||||
isSortDropdownOpen,
|
||||
searchKey,
|
||||
searchValue,
|
||||
} = this.state;
|
||||
|
||||
const [searchColumn] = columns
|
||||
.filter(({ key }) => key === searchKey);
|
||||
const searchColumnName = searchColumn.name;
|
||||
|
||||
const [sortedColumn] = columns
|
||||
.filter(({ key }) => key === sortedColumnKey);
|
||||
const sortedColumnName = sortedColumn.name;
|
||||
const isSortNumeric = sortedColumn.isNumeric;
|
||||
|
||||
return (
|
||||
<div className="awx-toolbar">
|
||||
<Level>
|
||||
<LevelItem>
|
||||
<Toolbar style={{ marginLeft: "20px" }}>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onChange={onSelectAll}
|
||||
aria-label="Select all"
|
||||
id="select-all"/>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>
|
||||
<div className="pf-c-input-group">
|
||||
<Dropdown
|
||||
onToggle={this.onSearchDropdownToggle}
|
||||
onSelect={this.onSearchDropdownSelect}
|
||||
direction={up}
|
||||
isOpen={isSearchDropdownOpen}
|
||||
toggle={(
|
||||
<DropdownToggle
|
||||
onToggle={this.onSearchDropdownToggle}>
|
||||
{ searchColumnName }
|
||||
</DropdownToggle>
|
||||
)}>
|
||||
{columns.filter(({ key }) => key !== searchKey).map(({ key, name }) => (
|
||||
<DropdownItem key={key} component="button">
|
||||
{ name }
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
<TextInput
|
||||
type="search"
|
||||
aria-label="search text input"
|
||||
value={searchValue}
|
||||
onChange={this.handleSearchInputChange}/>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
aria-label="Search"
|
||||
onClick={() => onSearch(searchValue)}>
|
||||
<i className="fas fa-search" aria-hidden="true"></i>
|
||||
</Button>
|
||||
</div>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>
|
||||
<Dropdown
|
||||
onToggle={this.onSortDropdownToggle}
|
||||
onSelect={this.onSortDropdownSelect}
|
||||
direction={up}
|
||||
isOpen={isSortDropdownOpen}
|
||||
toggle={(
|
||||
<DropdownToggle
|
||||
onToggle={this.onSortDropdownToggle}>
|
||||
{ sortedColumnName }
|
||||
</DropdownToggle>
|
||||
)}>
|
||||
{columns
|
||||
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
|
||||
.map(({ key, name }) => (
|
||||
<DropdownItem key={key} component="button">
|
||||
{ name }
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
onClick={() => onSort(sortedColumnKey, sortOrder === "ascending" ? "descending" : "ascending")}
|
||||
variant="plain"
|
||||
aria-label="Sort">
|
||||
{
|
||||
isSortNumeric ? (
|
||||
sortOrder === "ascending" ? <SortNumericUpIcon /> : <SortNumericDownIcon />
|
||||
) : (
|
||||
sortOrder === "ascending" ? <SortAlphaUpIcon /> : <SortAlphaDownIcon />
|
||||
)
|
||||
}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>
|
||||
<Button variant="plain" aria-label="Expand">
|
||||
<BarsIcon />
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button variant="plain" aria-label="Collapse">
|
||||
<EqualsIcon />
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
</Toolbar>
|
||||
</LevelItem>
|
||||
<LevelItem>
|
||||
<Tooltip message="Delete" position="top">
|
||||
<Button variant="plain" aria-label="Delete">
|
||||
<TrashAltIcon/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button variant="primary" aria-label="Add">
|
||||
Add
|
||||
</Button>
|
||||
</LevelItem>
|
||||
</Level>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DataListToolbar;
|
||||
3
src/components/DataListToolbar/index.js
Normal file
3
src/components/DataListToolbar/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import DataListToolbar from './DataListToolbar';
|
||||
|
||||
export default DataListToolbar;
|
||||
89
src/components/DataListToolbar/styles.scss
Normal file
89
src/components/DataListToolbar/styles.scss
Normal file
@ -0,0 +1,89 @@
|
||||
.awx-toolbar {
|
||||
--awx-toolbar--BackgroundColor: var(--pf-global--BackgroundColor--light-100);
|
||||
--awx-toolbar--BorderColor: var(--pf-global--Color--light-200);
|
||||
--awx-toolbar--BorderWidth: var(--pf-global--BorderWidth--sm);
|
||||
|
||||
border: var(--awx-toolbar--BorderWidth) solid var(--awx-toolbar--BorderColor);
|
||||
background-color: var(--awx-toolbar--BackgroundColor);
|
||||
height: 70px;
|
||||
padding-top: 5px;
|
||||
|
||||
--pf-global--target-size--MinHeight: 0px;
|
||||
--pf-global--target-size--MinWidth: 0px;
|
||||
--pf-global--FontSize--md: 14px;
|
||||
}
|
||||
|
||||
.awx-toolbar .pf-c-button.pf-m-plain {
|
||||
--pf-c-button--m-plain--PaddingLeft: 0px;
|
||||
--pf-c-button--m-plain--PaddingRight: 0px;
|
||||
}
|
||||
|
||||
.awx-toolbar .pf-l-toolbar__group {
|
||||
--pf-l-toolbar__group--MarginRight: 0px;
|
||||
--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 {
|
||||
height: 30px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.awx-toolbar .pf-c-input-group {
|
||||
min-height: 0px;
|
||||
height: 30px;
|
||||
|
||||
input {
|
||||
padding: 0px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.pf-m-tertiary {
|
||||
width: 34px;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.awx-toolbar .pf-c-dropdown__toggle {
|
||||
min-height: 30px;
|
||||
min-width: 70px;
|
||||
height: 30px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
|
||||
.pf-c-dropdown__toggle-icon {
|
||||
margin: 0px;
|
||||
padding-top: 3px;
|
||||
padding-left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.awx-toolbar .pf-l-toolbar__item + .pf-l-toolbar__item button {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.awx-toolbar .pf-c-button.pf-m-primary {
|
||||
background-color: #5cb85c;
|
||||
min-width: 0px;
|
||||
width: 58px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.awx-toolbar .pf-l-toolbar__item .pf-c-button.pf-m-plain {
|
||||
font-size: 18px;
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
class OrganizationCard extends Component {
|
||||
static title = 'Organization Card';
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
const { name } = props.organization;
|
||||
|
||||
this.state = { name };
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name } = this.state;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>{name}</CardHeader>
|
||||
<CardBody>Card Body</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OrganizationCard;
|
||||
44
src/components/OrganizationListItem.jsx
Normal file
44
src/components/OrganizationListItem.jsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Checkbox,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
export default ({ itemId, name, userCount, teamCount, adminCount, isSelected, onSelect }) => (
|
||||
<li key={itemId} className="pf-c-data-list__item" aria-labelledby="check-action-item1">
|
||||
<div className="pf-c-data-list__check">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
aria-label={`select organization ${itemId}`}
|
||||
id={`select-organization-${itemId}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="pf-c-data-list__cell">
|
||||
<span id="check-action-item1">
|
||||
<a href={`#/organizations/${itemId}`}>{ name }</a>
|
||||
</span>
|
||||
</div>
|
||||
<div className="pf-c-data-list__cell">
|
||||
<a href="#/dashboard"> Users </a>
|
||||
<Badge isRead>
|
||||
{' '}
|
||||
{userCount}
|
||||
{' '}
|
||||
</Badge>
|
||||
<a href="#/dashboard"> Teams </a>
|
||||
<Badge isRead>
|
||||
{' '}
|
||||
{teamCount}
|
||||
{' '}
|
||||
</Badge>
|
||||
<a href="#/dashboard"> Admins </a>
|
||||
<Badge isRead>
|
||||
{' '}
|
||||
{adminCount}
|
||||
{' '}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="pf-c-data-list__cell" />
|
||||
</li>
|
||||
);
|
||||
220
src/components/Pagination/Pagination.jsx
Normal file
220
src/components/Pagination/Pagination.jsx
Normal file
@ -0,0 +1,220 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownDirection,
|
||||
DropdownItem,
|
||||
DropdownToggle,
|
||||
Form,
|
||||
FormGroup,
|
||||
Level,
|
||||
LevelItem,
|
||||
TextInput,
|
||||
Toolbar,
|
||||
ToolbarGroup,
|
||||
ToolbarItem,
|
||||
Split,
|
||||
SplitItem,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
class Pagination extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
const { page } = this.props;
|
||||
|
||||
this.state = { value: page, isOpen: false };
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { page } = this.props;
|
||||
|
||||
if (prevProps.page !== page) {
|
||||
this.setState({ value: page });
|
||||
}
|
||||
}
|
||||
|
||||
onPageChange = value => {
|
||||
this.setState({ value });
|
||||
};
|
||||
|
||||
onSubmit = event => {
|
||||
const { onSetPage, page, pageCount, page_size } = this.props;
|
||||
const { value } = this.state;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const isPositiveInteger = value >>> 0 === parseFloat(value) && parseInt(value, 10) > 0;
|
||||
const isValid = isPositiveInteger && parseInt(value, 10) <= pageCount;
|
||||
|
||||
if (isValid) {
|
||||
onSetPage(value, page_size);
|
||||
} else{
|
||||
this.setState({ value: page });
|
||||
}
|
||||
};
|
||||
|
||||
onFirst = () => {
|
||||
const { onSetPage, page_size} = this.props;
|
||||
|
||||
onSetPage(1, page_size);
|
||||
};
|
||||
|
||||
onPrevious = () => {
|
||||
const { onSetPage, page, page_size } = this.props;
|
||||
const previousPage = page - 1;
|
||||
|
||||
if (previousPage >= 1) {
|
||||
onSetPage(previousPage, page_size)
|
||||
}
|
||||
};
|
||||
|
||||
onNext = () => {
|
||||
const { onSetPage, page, pageCount, page_size } = this.props;
|
||||
const nextPage = page + 1;
|
||||
|
||||
if (nextPage <= pageCount) {
|
||||
onSetPage(nextPage, page_size)
|
||||
}
|
||||
};
|
||||
|
||||
onLast = () => {
|
||||
const { onSetPage, pageCount, page_size } = this.props;
|
||||
|
||||
onSetPage(pageCount, page_size)
|
||||
};
|
||||
|
||||
onTogglePageSize = isOpen => {
|
||||
this.setState({ isOpen });
|
||||
};
|
||||
|
||||
onSelectPageSize = ({ target }) => {
|
||||
const { onSetPage } = this.props;
|
||||
|
||||
const page = 1;
|
||||
const page_size = parseInt(target.innerText, 10);
|
||||
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
onSetPage(page, page_size);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { up } = DropdownDirection;
|
||||
const {
|
||||
count,
|
||||
page,
|
||||
pageCount,
|
||||
page_size,
|
||||
pageSizeOptions,
|
||||
} = this.props;
|
||||
const { value, isOpen } = this.state;
|
||||
|
||||
const opts = pageSizeOptions.slice().reverse().filter(o => o !== page_size);
|
||||
const isOnFirst = page === 1;
|
||||
const isOnLast = page === pageCount;
|
||||
|
||||
const itemCount = isOnLast ? count % page_size : page_size;
|
||||
const itemMin = ((page - 1) * page_size) + 1;
|
||||
const itemMax = itemMin + itemCount - 1;
|
||||
|
||||
const disabledStyle = {
|
||||
backgroundColor: '#EDEDED',
|
||||
border: '1px solid #C2C2CA',
|
||||
borderRadius: '0px',
|
||||
color: '#C2C2CA',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="awx-pagination">
|
||||
<Level>
|
||||
<LevelItem>
|
||||
<Dropdown
|
||||
onToggle={this.onTogglePageSize}
|
||||
onSelect={this.onSelectPageSize}
|
||||
direction={up}
|
||||
isOpen={isOpen}
|
||||
toggle={(
|
||||
<DropdownToggle
|
||||
onToggle={this.onTogglePageSize}>
|
||||
{ page_size }
|
||||
</DropdownToggle>
|
||||
)}>
|
||||
{opts.map(option => (
|
||||
<DropdownItem key={option} component="button">
|
||||
{ option }
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown> Per Page
|
||||
</LevelItem>
|
||||
<LevelItem>
|
||||
<Split gutter="md" className="pf-u-display-flex pf-u-align-items-center">
|
||||
<SplitItem>{itemMin} - {itemMax } of { count }</SplitItem>
|
||||
<SplitItem>
|
||||
<div className="pf-c-input-group">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
aria-label="first"
|
||||
style={isOnFirst ? disabledStyle : {}}
|
||||
isDisabled={isOnFirst}
|
||||
onClick={this.onFirst}>
|
||||
<i className="fas fa-angle-double-left"></i>
|
||||
</Button>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
aria-label="previous"
|
||||
style={isOnFirst ? disabledStyle : {}}
|
||||
isDisabled={isOnFirst}
|
||||
onClick={this.onPrevious}>
|
||||
<i className="fas fa-angle-left"></i>
|
||||
</Button>
|
||||
</div>
|
||||
</SplitItem>
|
||||
<SplitItem isMain>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
Page <TextInput
|
||||
isDisabled={pageCount === 1}
|
||||
aria-label="Page Number"
|
||||
style={{
|
||||
height: '30px',
|
||||
width: '30px',
|
||||
textAlign: 'center',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
...(pageCount === 1 ? disabledStyle : {})
|
||||
}}
|
||||
value={value}
|
||||
type="text"
|
||||
onChange={this.onPageChange}
|
||||
/> of { pageCount }
|
||||
</form>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<div className="pf-c-input-group">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
aria-label="next"
|
||||
style={isOnLast ? disabledStyle : {}}
|
||||
isDisabled={isOnLast}
|
||||
onClick={this.onNext}>
|
||||
<i className="fas fa-angle-right"></i>
|
||||
</Button>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
aria-label="last"
|
||||
style={isOnLast ? disabledStyle : {}}
|
||||
isDisabled={isOnLast}
|
||||
onClick={this.onLast}>
|
||||
<i className="fas fa-angle-double-right"></i>
|
||||
</Button>
|
||||
</div>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</LevelItem>
|
||||
</Level>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Pagination;
|
||||
3
src/components/Pagination/index.js
Normal file
3
src/components/Pagination/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import Pagination from './Pagination';
|
||||
|
||||
export default Pagination;
|
||||
39
src/components/Pagination/styles.scss
Normal file
39
src/components/Pagination/styles.scss
Normal file
@ -0,0 +1,39 @@
|
||||
.awx-pagination {
|
||||
--awx-pagination--BackgroundColor: var(--pf-global--BackgroundColor--light-100);
|
||||
--awx-pagination--BorderColor: var(--pf-global--Color--light-200);
|
||||
--awx-pagination--BorderWidth: var(--pf-global--BorderWidth--sm);
|
||||
|
||||
border: var(--awx-pagination--BorderWidth) solid var(--awx-pagination--BorderColor);
|
||||
background-color: var(--awx-pagination--BackgroundColor);
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 20px;
|
||||
height: 70px;
|
||||
|
||||
--pf-global--target-size--MinHeight: 30px;
|
||||
--pf-global--target-size--MinWidth: 30px;
|
||||
--pf-global--FontSize--md: 14px;
|
||||
|
||||
.pf-c-input-group button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.pf-c-dropdown button {
|
||||
width: 55px;
|
||||
height: 30px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
margin: 0px;
|
||||
margin-right: 10px;
|
||||
text-align: left;
|
||||
|
||||
.pf-c-dropdown__toggle-icon {
|
||||
margin: 0px;
|
||||
margin-top: 2px;
|
||||
padding: 0px;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/components/Tooltip/Tooltip.jsx
Normal file
68
src/components/Tooltip/Tooltip.jsx
Normal file
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
|
||||
class Tooltip extends React.Component {
|
||||
transforms = {
|
||||
top: {
|
||||
bottom: "100%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -25%)"
|
||||
},
|
||||
bottom: {
|
||||
top: "100%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, 25%)"
|
||||
},
|
||||
left: {
|
||||
top: "50%",
|
||||
right: "100%",
|
||||
transform: "translate(-25%, -50%)"
|
||||
},
|
||||
right: {
|
||||
bottom: "100%",
|
||||
left: "50%",
|
||||
transform: "translate(25%, 50%)"
|
||||
},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isDisplayed: false
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
message,
|
||||
position,
|
||||
} = this.props;
|
||||
const {
|
||||
isDisplayed
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{ position: "relative"}}
|
||||
onMouseLeave={() => this.setState({ isDisplayed: false })}>
|
||||
{ isDisplayed &&
|
||||
<div
|
||||
style={{ position: "absolute", zIndex: "10", ...this.transforms[position] }}
|
||||
className={`pf-c-tooltip pf-m-${position}`}>
|
||||
<div className="pf-c-tooltip__arrow"></div>
|
||||
<div className="pf-c-tooltip__content">
|
||||
{ message }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<span
|
||||
onMouseOver={() => this.setState({ isDisplayed: true })}>
|
||||
{ children }
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Tooltip;
|
||||
3
src/components/Tooltip/index.js
Normal file
3
src/components/Tooltip/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
export default Tooltip;
|
||||
@ -9,6 +9,8 @@ import '@patternfly/react-core/dist/styles/base.css';
|
||||
import '@patternfly/patternfly-next/patternfly.css';
|
||||
|
||||
import './app.scss';
|
||||
import './components/Pagination/styles.scss';
|
||||
import './components/DataListToolbar/styles.scss';
|
||||
|
||||
const el = document.getElementById('app');
|
||||
|
||||
|
||||
@ -1,38 +1,190 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, {
|
||||
Component,
|
||||
Fragment
|
||||
} from 'react';
|
||||
import {
|
||||
withRouter
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
Gallery,
|
||||
GalleryItem,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import OrganizationCard from '../components/OrganizationCard';
|
||||
import DataListToolbar from '../components/DataListToolbar';
|
||||
import OrganizationListItem from '../components/OrganizationListItem';
|
||||
import Pagination from '../components/Pagination';
|
||||
|
||||
import api from '../api';
|
||||
import { API_ORGANIZATIONS } from '../endpoints';
|
||||
|
||||
import {
|
||||
encodeQueryString,
|
||||
parseQueryString,
|
||||
} from '../qs';
|
||||
|
||||
class Organizations extends Component {
|
||||
columns = [
|
||||
{ name: 'Name', key: 'name', isSortable: true },
|
||||
{ name: 'Modified', key: 'modified', isSortable: true, isNumeric: true },
|
||||
{ name: 'Created', key: 'created', isSortable: true, isNumeric: true },
|
||||
];
|
||||
|
||||
defaultParams = {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
};
|
||||
|
||||
pageSizeOptions = [5, 10, 25, 50];
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
const { page, page_size } = this.getQueryParams();
|
||||
|
||||
this.state = {
|
||||
organizations: [],
|
||||
error: false,
|
||||
page,
|
||||
page_size,
|
||||
sortedColumnKey: 'name',
|
||||
sortOrder: 'ascending',
|
||||
count: null,
|
||||
error: null,
|
||||
loading: true,
|
||||
results: [],
|
||||
selected: [],
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount () {
|
||||
componentDidMount () {
|
||||
const queryParams = this.getQueryParams();
|
||||
|
||||
this.fetchOrganizations(queryParams);
|
||||
}
|
||||
|
||||
onSearch () {
|
||||
const { sortedColumnKey, sortOrder } = this.state;
|
||||
|
||||
this.onSort(sortedColumnKey, sortOrder);
|
||||
}
|
||||
|
||||
getQueryParams (overrides = {}) {
|
||||
const { location } = this.props;
|
||||
const { search } = location;
|
||||
|
||||
const searchParams = parseQueryString(search.substring(1));
|
||||
|
||||
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
||||
}
|
||||
|
||||
onSort = (sortedColumnKey, sortOrder) => {
|
||||
const { page_size } = this.state;
|
||||
|
||||
let order_by = sortedColumnKey;
|
||||
|
||||
if (sortOrder === 'descending') {
|
||||
order_by = `-${order_by}`;
|
||||
}
|
||||
|
||||
const queryParams = this.getQueryParams({ order_by, page_size });
|
||||
|
||||
this.fetchOrganizations(queryParams);
|
||||
};
|
||||
|
||||
onSetPage = (pageNumber, pageSize) => {
|
||||
const page = parseInt(pageNumber, 10);
|
||||
const page_size = parseInt(pageSize, 10);
|
||||
|
||||
const queryParams = this.getQueryParams({ page, page_size });
|
||||
|
||||
this.fetchOrganizations(queryParams);
|
||||
};
|
||||
|
||||
onSelectAll = isSelected => {
|
||||
const { results } = this.state;
|
||||
|
||||
const selected = isSelected ? results.map(o => o.id) : [];
|
||||
|
||||
this.setState({ selected });
|
||||
};
|
||||
|
||||
onSelect = id => {
|
||||
const { selected } = this.state;
|
||||
|
||||
const isSelected = selected.includes(id);
|
||||
|
||||
if (isSelected) {
|
||||
this.setState({ selected: selected.filter(s => s !== id) });
|
||||
} else {
|
||||
this.setState({ selected: selected.concat(id) });
|
||||
}
|
||||
};
|
||||
|
||||
updateUrl (queryParams) {
|
||||
const { history, location } = this.props;
|
||||
|
||||
const pathname = '/organizations';
|
||||
const search = `?${encodeQueryString(queryParams)}`;
|
||||
|
||||
if (search !== location.search) {
|
||||
history.replace({ pathname, search });
|
||||
}
|
||||
}
|
||||
|
||||
async fetchOrganizations (queryParams) {
|
||||
const { page, page_size, order_by } = queryParams;
|
||||
|
||||
let sortOrder = 'ascending';
|
||||
let sortedColumnKey = order_by;
|
||||
|
||||
if (order_by.startsWith('-')) {
|
||||
sortOrder = 'descending';
|
||||
sortedColumnKey = order_by.substring(1);
|
||||
}
|
||||
|
||||
this.setState({ error: false, loading: true });
|
||||
|
||||
try {
|
||||
const { data } = await api.get(API_ORGANIZATIONS);
|
||||
this.setState({ organizations: data.results });
|
||||
const { data } = await api.get(API_ORGANIZATIONS, queryParams);
|
||||
const { count, results } = data;
|
||||
|
||||
const pageCount = Math.ceil(count / page_size);
|
||||
|
||||
this.setState({
|
||||
count,
|
||||
page,
|
||||
pageCount,
|
||||
page_size,
|
||||
sortOrder,
|
||||
sortedColumnKey,
|
||||
results,
|
||||
selected: [],
|
||||
});
|
||||
this.updateUrl(queryParams);
|
||||
} catch (err) {
|
||||
this.setState({ error: err });
|
||||
this.setState({ error: true });
|
||||
} finally {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { light, medium } = PageSectionVariants;
|
||||
const { organizations, error } = this.state;
|
||||
const {
|
||||
light,
|
||||
medium,
|
||||
} = PageSectionVariants;
|
||||
const {
|
||||
count,
|
||||
error,
|
||||
loading,
|
||||
page,
|
||||
pageCount,
|
||||
page_size,
|
||||
sortedColumnKey,
|
||||
sortOrder,
|
||||
results,
|
||||
selected,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@ -40,18 +192,43 @@ class Organizations extends Component {
|
||||
<Title size="2xl">Organizations</Title>
|
||||
</PageSection>
|
||||
<PageSection variant={medium}>
|
||||
<Gallery gutter="md">
|
||||
{organizations.map(o => (
|
||||
<GalleryItem key={o.id}>
|
||||
<OrganizationCard key={o.id} organization={o} />
|
||||
</GalleryItem>
|
||||
<DataListToolbar
|
||||
isAllSelected={selected.length === results.length}
|
||||
sortedColumnKey={sortedColumnKey}
|
||||
sortOrder={sortOrder}
|
||||
columns={this.columns}
|
||||
onSearch={this.onSearch}
|
||||
onSort={this.onSort}
|
||||
onSelectAll={this.onSelectAll}
|
||||
/>
|
||||
<ul className="pf-c-data-list" aria-label="Organizations List">
|
||||
{ results.map(o => (
|
||||
<OrganizationListItem
|
||||
key={o.id}
|
||||
itemId={o.id}
|
||||
name={o.name}
|
||||
userCount={o.summary_fields.related_field_counts.users}
|
||||
teamCount={o.summary_fields.related_field_counts.teams}
|
||||
adminCount={o.summary_fields.related_field_counts.admins}
|
||||
isSelected={selected.includes(o.id)}
|
||||
onSelect={() => this.onSelect(o.id)}
|
||||
/>
|
||||
))}
|
||||
{ error ? <div>error</div> : '' }
|
||||
</Gallery>
|
||||
</ul>
|
||||
<Pagination
|
||||
count={count}
|
||||
page={page}
|
||||
pageCount={pageCount}
|
||||
page_size={page_size}
|
||||
pageSizeOptions={this.pageSizeOptions}
|
||||
onSetPage={this.onSetPage}
|
||||
/>
|
||||
{ loading ? <div>loading...</div> : '' }
|
||||
{ error ? <div>error</div> : '' }
|
||||
</PageSection>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Organizations;
|
||||
export default withRouter(Organizations);
|
||||
|
||||
41
src/qs.js
Normal file
41
src/qs.js
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Convert query param object to url query string
|
||||
*
|
||||
* @param {object} query param object
|
||||
* @return {string} url query string
|
||||
*/
|
||||
export const encodeQueryString = (params) => {
|
||||
if (!params) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Object.keys(params)
|
||||
.sort()
|
||||
.map(key => ([key, params[key]]))
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join('&');
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert url query string to query param object
|
||||
*
|
||||
* @param {string} url query string
|
||||
* @param {object} default params
|
||||
* @param {array} array of keys to parse as integers
|
||||
* @return {object} query param object
|
||||
*/
|
||||
export const parseQueryString = (queryString, integerFields = ['page', 'page_size']) => {
|
||||
if (!queryString) return {};
|
||||
|
||||
const keyValuePairs = queryString.split('&')
|
||||
.map(s => s.split('='))
|
||||
.map(([key, value]) => {
|
||||
if (integerFields.includes(key)) {
|
||||
return [key, parseInt(value, 10)];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
});
|
||||
|
||||
return Object.assign(...keyValuePairs.map(([k, v]) => ({ [k]: v })));
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user