mirror of
https://github.com/ansible/awx.git
synced 2026-03-03 17:51:06 -03:30
Merge pull request #5 from ansible/pf4-table
basic pf4 data list, toolbar, pagination
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import mockAxios from 'axios';
|
import mockAxios from 'axios';
|
||||||
import APIClient from '../src/api';
|
import APIClient from '../src/api';
|
||||||
import * as endpoints from '../src/endpoints';
|
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 React from 'react';
|
||||||
|
import { HashRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
import api from '../../src/api';
|
||||||
import { API_ORGANIZATIONS } from '../../src/endpoints';
|
import { API_ORGANIZATIONS } from '../../src/endpoints';
|
||||||
import Organizations from '../../src/pages/Organizations';
|
import Organizations from '../../src/pages/Organizations';
|
||||||
|
|
||||||
describe('<Organizations />', () => {
|
describe('<Organizations />', () => {
|
||||||
let pageWrapper;
|
let pageWrapper;
|
||||||
let pageSections;
|
|
||||||
let title;
|
|
||||||
let gallery;
|
|
||||||
let galleryItems;
|
|
||||||
let orgCards;
|
|
||||||
|
|
||||||
const orgs = [
|
const results = [
|
||||||
{ id: 1, name: 'org 1' },
|
{
|
||||||
{ id: 2, name: 'org 2' },
|
id: 1,
|
||||||
{ id: 3, name: 'org 3' }
|
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 count = results.length;
|
||||||
const findOrgCards = () => {
|
const response = { data: { count, results } };
|
||||||
galleryItems = pageWrapper.find('GalleryItem');
|
|
||||||
orgCards = pageWrapper.find('OrganizationCard');
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
pageWrapper = mount(<Organizations />);
|
api.get = jest.fn().mockImplementation(() => Promise.resolve(response));
|
||||||
pageSections = pageWrapper.find('PageSection');
|
pageWrapper = mount(<HashRouter><Organizations /></HashRouter>);
|
||||||
title = pageWrapper.find('Title');
|
|
||||||
gallery = pageWrapper.find('Gallery');
|
|
||||||
findOrgCards();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
pageWrapper.unmount();
|
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(pageWrapper.length).toBe(1);
|
||||||
expect(pageSections.length).toBe(2);
|
expect(pageSections.length).toBe(2);
|
||||||
expect(title.length).toBe(1);
|
expect(title.length).toBe(1);
|
||||||
@@ -42,19 +68,23 @@ describe('<Organizations />', () => {
|
|||||||
pageSections.forEach(section => {
|
pageSections.forEach(section => {
|
||||||
expect(section.props().variant).toBeDefined();
|
expect(section.props().variant).toBeDefined();
|
||||||
});
|
});
|
||||||
expect(gallery.length).toBe(1);
|
expect(pageWrapper.find('ul').length).toBe(1);
|
||||||
// by default, no galleryItems or orgCards
|
expect(pageWrapper.find('ul li').length).toBe(0);
|
||||||
expect(galleryItems.length).toBe(0);
|
// will render all list items on update
|
||||||
expect(orgCards.length).toBe(0);
|
pageWrapper.update();
|
||||||
// will render all orgCards if state is set
|
expect(pageWrapper.find('ul li').length).toBe(count);
|
||||||
pageWrapper.setState({ organizations: orgs });
|
|
||||||
findOrgCards();
|
|
||||||
expect(galleryItems.length).toBe(3);
|
|
||||||
expect(orgCards.length).toBe(3);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('API Organization endpoint is valid', () => {
|
test('API Organization endpoint is valid', () => {
|
||||||
expect(API_ORGANIZATIONS).toBeDefined();
|
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 });
|
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();
|
export default new APIClient();
|
||||||
|
|||||||
52
src/app.scss
52
src/app.scss
@@ -59,4 +59,54 @@
|
|||||||
.pf-l-page__main-section.pf-m-condensed {
|
.pf-l-page__main-section.pf-m-condensed {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
padding-bottom: 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 '@patternfly/patternfly-next/patternfly.css';
|
||||||
|
|
||||||
import './app.scss';
|
import './app.scss';
|
||||||
|
import './components/Pagination/styles.scss';
|
||||||
|
import './components/DataListToolbar/styles.scss';
|
||||||
|
|
||||||
const el = document.getElementById('app');
|
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 {
|
import {
|
||||||
Gallery,
|
|
||||||
GalleryItem,
|
|
||||||
PageSection,
|
PageSection,
|
||||||
PageSectionVariants,
|
PageSectionVariants,
|
||||||
Title,
|
Title,
|
||||||
} from '@patternfly/react-core';
|
} 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 from '../api';
|
||||||
import { API_ORGANIZATIONS } from '../endpoints';
|
import { API_ORGANIZATIONS } from '../endpoints';
|
||||||
|
|
||||||
|
import {
|
||||||
|
encodeQueryString,
|
||||||
|
parseQueryString,
|
||||||
|
} from '../qs';
|
||||||
|
|
||||||
class Organizations extends Component {
|
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) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
const { page, page_size } = this.getQueryParams();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
organizations: [],
|
page,
|
||||||
error: false,
|
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 {
|
try {
|
||||||
const { data } = await api.get(API_ORGANIZATIONS);
|
const { data } = await api.get(API_ORGANIZATIONS, queryParams);
|
||||||
this.setState({ organizations: data.results });
|
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) {
|
} catch (err) {
|
||||||
this.setState({ error: err });
|
this.setState({ error: true });
|
||||||
|
} finally {
|
||||||
|
this.setState({ loading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { light, medium } = PageSectionVariants;
|
const {
|
||||||
const { organizations, error } = this.state;
|
light,
|
||||||
|
medium,
|
||||||
|
} = PageSectionVariants;
|
||||||
|
const {
|
||||||
|
count,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
page,
|
||||||
|
pageCount,
|
||||||
|
page_size,
|
||||||
|
sortedColumnKey,
|
||||||
|
sortOrder,
|
||||||
|
results,
|
||||||
|
selected,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -40,18 +192,43 @@ class Organizations extends Component {
|
|||||||
<Title size="2xl">Organizations</Title>
|
<Title size="2xl">Organizations</Title>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
<PageSection variant={medium}>
|
<PageSection variant={medium}>
|
||||||
<Gallery gutter="md">
|
<DataListToolbar
|
||||||
{organizations.map(o => (
|
isAllSelected={selected.length === results.length}
|
||||||
<GalleryItem key={o.id}>
|
sortedColumnKey={sortedColumnKey}
|
||||||
<OrganizationCard key={o.id} organization={o} />
|
sortOrder={sortOrder}
|
||||||
</GalleryItem>
|
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> : '' }
|
</ul>
|
||||||
</Gallery>
|
<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>
|
</PageSection>
|
||||||
</Fragment>
|
</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 })));
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user