utilize new DataToolbar experimental patternfly components

This commit is contained in:
John Mitchell 2019-12-10 13:12:19 -05:00
parent c69d497093
commit a31661ce08
6 changed files with 245 additions and 370 deletions

View File

@ -4,73 +4,16 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Checkbox,
Toolbar as PFToolbar,
ToolbarGroup as PFToolbarGroup,
ToolbarItem,
} from '@patternfly/react-core';
import styled from 'styled-components';
import { SearchIcon } from '@patternfly/react-icons';
import { DataToolbarGroup, DataToolbarToggleGroup, DataToolbarItem } from '@patternfly/react-core/dist/esm/experimental';
import ExpandCollapse from '../ExpandCollapse';
import Search from '../Search';
import Sort from '../Sort';
import VerticalSeparator from '../VerticalSeparator';
import { SearchColumns, SortColumns, QSConfig } from '@types';
const AWXToolbar = styled.div`
--awx-toolbar--BackgroundColor: var(--pf-global--BackgroundColor--light-100);
--awx-toolbar--BorderColor: #ebebeb;
--awx-toolbar--BorderWidth: var(--pf-global--BorderWidth--sm);
--pf-global--target-size--MinHeight: 0;
--pf-global--target-size--MinWidth: 0;
--pf-global--FontSize--md: 14px;
border-bottom: var(--awx-toolbar--BorderWidth) solid
var(--awx-toolbar--BorderColor);
background-color: var(--awx-toolbar--BackgroundColor);
display: flex;
min-height: 70px;
flex-grow: 1;
`;
const Toolbar = styled(PFToolbar)`
flex-grow: 1;
margin-left: 20px;
margin-right: 20px;
`;
const ToolbarGroup = styled(PFToolbarGroup)`
&&& {
margin: 0;
}
`;
const ColumnLeft = styled.div`
display: flex;
flex-basis: ${props => (props.fillWidth ? 'auto' : '100%')};
flex-grow: ${props => (props.fillWidth ? '1' : '0')};
justify-content: flex-start;
align-items: center;
padding: 10px 0 8px 0;
@media screen and (min-width: 980px) {
flex-basis: ${props => (props.fillWidth ? 'auto' : '50%')};
}
`;
const ColumnRight = styled.div`
display: flex;
flex-basis: ${props => (props.fillWidth ? 'auto' : '100%')};
flex-grow: 0;
justify-content: flex-start;
align-items: center;
padding: 8px 0 10px 0;
@media screen and (min-width: 980px) {
flex-basis: ${props => (props.fillWidth ? 'auto' : '50%')};
}
`;
const AdditionalControlsWrapper = styled.div`
display: flex;
flex-grow: 1;
@ -82,6 +25,18 @@ const AdditionalControlsWrapper = styled.div`
}
`;
const AdditionalControlsDataToolbarGroup = styled(DataToolbarGroup)`
margin-left: auto;
margin-right: 0 !important;
`;
const DataToolbarSeparator = styled(DataToolbarItem)`
width: 1px !important;
height: 30px !important;
margin-left: 3px !important;
margin-right: 10px !important;
`;
class DataListToolbar extends React.Component {
render() {
const {
@ -90,9 +45,9 @@ class DataListToolbar extends React.Component {
showSelectAll,
isAllSelected,
isCompact,
fillWidth,
onSort,
onSearch,
onRemove,
onCompact,
onExpand,
onSelectAll,
@ -103,58 +58,58 @@ class DataListToolbar extends React.Component {
const showExpandCollapse = onCompact && onExpand;
return (
<AWXToolbar>
<Toolbar css={fillWidth ? 'margin-right: 0; margin-left: 0' : ''}>
<ColumnLeft fillWidth={fillWidth}>
{showSelectAll && (
<Fragment>
<ToolbarItem>
<Checkbox
isChecked={isAllSelected}
onChange={onSelectAll}
aria-label={i18n._(t`Select all`)}
id="select-all"
/>
</ToolbarItem>
<VerticalSeparator />
</Fragment>
)}
<ToolbarItem css="flex-grow: 1;">
<Search
qsConfig={qsConfig}
columns={searchColumns}
onSearch={onSearch}
<Fragment>
{showSelectAll && (
<DataToolbarGroup>
<DataToolbarItem>
<Checkbox
isChecked={isAllSelected}
onChange={onSelectAll}
aria-label={i18n._(t`Select all`)}
id="select-all"
/>
</ToolbarItem>
<VerticalSeparator />
</ColumnLeft>
<ColumnRight fillWidth={fillWidth}>
<ToolbarItem>
<Sort
qsConfig={qsConfig}
columns={sortColumns}
onSort={onSort}
/>
</ToolbarItem>
{showExpandCollapse && (
<Fragment>
<VerticalSeparator />
<ToolbarGroup>
<ExpandCollapse
isCompact={isCompact}
onCompact={onCompact}
onExpand={onExpand}
/>
</ToolbarGroup>
{additionalControls && <VerticalSeparator />}
</Fragment>
)}
</DataToolbarItem>
<DataToolbarSeparator variant="separator" />
</DataToolbarGroup>
)}
<DataToolbarToggleGroup toggleIcon={<SearchIcon />} breakpoint="xl">
<DataToolbarItem>
<Search
qsConfig={qsConfig}
columns={searchColumns}
onSearch={onSearch}
onRemove={onRemove}
/>
</DataToolbarItem>
<DataToolbarItem>
<Sort
qsConfig={qsConfig}
columns={sortColumns}
onSort={onSort}
/>
</DataToolbarItem>
</DataToolbarToggleGroup>
<DataToolbarGroup>
{showExpandCollapse && (
<Fragment>
<DataToolbarItem>
<ExpandCollapse
isCompact={isCompact}
onCompact={onCompact}
onExpand={onExpand}
/>
</DataToolbarItem>
</Fragment>
)}
</DataToolbarGroup>
<AdditionalControlsDataToolbarGroup>
<DataToolbarItem>
<AdditionalControlsWrapper>
{additionalControls}
</AdditionalControlsWrapper>
</ColumnRight>
</Toolbar>
</AWXToolbar>
</DataToolbarItem>
</AdditionalControlsDataToolbarGroup>
</Fragment>
);
}
}
@ -166,7 +121,6 @@ DataListToolbar.propTypes = {
showSelectAll: PropTypes.bool,
isAllSelected: PropTypes.bool,
isCompact: PropTypes.bool,
fillWidth: PropTypes.bool,
onCompact: PropTypes.func,
onExpand: PropTypes.func,
onSearch: PropTypes.func,
@ -179,7 +133,6 @@ DataListToolbar.defaultProps = {
showSelectAll: false,
isAllSelected: false,
isCompact: false,
fillWidth: false,
onCompact: null,
onExpand: null,
onSearch: null,

View File

@ -1,21 +1,13 @@
import React from 'react';
import React, { Fragment } from 'react';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import { Button } from '@patternfly/react-core';
import { DataToolbarGroup, DataToolbarItem } from '@patternfly/react-core/dist/esm/experimental';
import { parseQueryString } from '@util/qs';
import { ChipGroup as _ChipGroup, Chip } from '@components/Chip';
import { Button, Chip, ChipGroup, ChipGroupToolbarItem } from '@patternfly/react-core';
import VerticalSeparator from '@components/VerticalSeparator';
const FilterTagsRow = styled.div`
display: flex;
padding: 15px 20px;
border-top: 1px solid #d2d2d2;
font-size: 14px;
align-items: center;
`;
const ResultCount = styled.span`
font-weight: bold;
`;
@ -24,12 +16,6 @@ const FilterLabel = styled.span`
padding-right: 20px;
`;
const ChipGroup = styled(_ChipGroup)`
li.pf-m-overflow {
display: none;
}
`;
// remove non-default query params so they don't show up as filter tags
const filterDefaultParams = (paramsArr, config) => {
const defaultParamsKeys = Object.keys(config.defaultParams);
@ -45,7 +31,7 @@ const FilterTags = ({
onRemoveAll,
}) => {
const queryParams = parseQueryString(qsConfig, location.search);
const queryParamsArr = [];
const queryParamsByKey = {};
const nonDefaultParams = filterDefaultParams(
Object.keys(queryParams),
qsConfig
@ -56,45 +42,45 @@ const FilterTags = ({
.split('_')
.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
.join(' ');
queryParamsByKey[key] = { label, tags: [] };
if (Array.isArray(queryParams[key])) {
queryParams[key].forEach(val =>
queryParamsArr.push({ key, value: val, label })
queryParamsByKey[key].tags.push(val)
);
} else {
queryParamsArr.push({ key, value: queryParams[key], label });
queryParamsByKey[key].tags.push(queryParams[key]);
}
});
return (
queryParamsArr.length > 0 && (
<FilterTagsRow>
<ResultCount>{i18n._(t`${itemCount} results`)}</ResultCount>
<VerticalSeparator />
<FilterLabel>{i18n._(t`Active Filters:`)}</FilterLabel>
<ChipGroup defaultIsOpen>
{queryParamsArr.map(({ key, label, value }) => (
<Chip
className="searchTagChip"
key={`${key}__${value}`}
isReadOnly={false}
onClick={() => onRemove(key, value)}
>
<b>{label}:</b>&nbsp;{value}
</Chip>
))}
<div className="pf-c-chip pf-m-overflow">
<Button
variant="plain"
type="button"
aria-label={i18n._(t`Clear all search filters`)}
onClick={onRemoveAll}
>
<span className="pf-c-chip__text">{i18n._(t`Clear all`)}</span>
Object.keys(queryParamsByKey).length > 0 && (
<Fragment>
<DataToolbarGroup>
<ResultCount>{i18n._(t`${itemCount} results`)}</ResultCount>
</DataToolbarGroup>
<DataToolbarGroup>
<FilterLabel>{i18n._(t`Active Filters:`)}</FilterLabel>
<DataToolbarItem variant="chip-group">
{Object.keys(queryParamsByKey).map(key => (
<ChipGroup withToolbar key={`${key}-group`}>
<ChipGroupToolbarItem key={key} categoryName={queryParamsByKey[key].label}>
{queryParamsByKey[key].tags.map(chip => (
<Chip key={chip} onClick={() => onRemove(key, chip)}>
{chip}
</Chip>
))}
</ChipGroupToolbarItem>
</ChipGroup>
))}
</DataToolbarItem>
<DataToolbarItem>
<Button variant="link" onClick={onRemoveAll} isInline>
{i18n._(t`Clear all search filters`)}
</Button>
</div>
</ChipGroup>
</FilterTagsRow>
</DataToolbarItem>
</DataToolbarGroup>
</Fragment>
)
);
};

View File

@ -2,8 +2,8 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import styled from 'styled-components';
import { DataToolbar, DataToolbarContent } from '@patternfly/react-core/dist/esm/experimental';
import DataListToolbar from '@components/DataListToolbar';
import FilterTags from '@components/FilterTags';
import {
encodeNonDefaultQueryString,
@ -84,33 +84,32 @@ class ListHeader extends React.Component {
return (
<Fragment>
{isEmpty ? (
<Fragment>
<EmptyStateControlsWrapper>
{emptyStateControls}
</EmptyStateControlsWrapper>
<FilterTags
itemCount={itemCount}
qsConfig={qsConfig}
onRemove={this.handleRemove}
onRemoveAll={this.handleRemoveAll}
/>
</Fragment>
<DataToolbar id={`${qsConfig.namespace}-list-toolbar`}
clearAllFilters={this.handleRemoveAll}
collapseListedFiltersBreakpoint="md"
>
<DataToolbarContent>
<EmptyStateControlsWrapper>
{emptyStateControls}
</EmptyStateControlsWrapper>
</DataToolbarContent>
</DataToolbar>
) : (
<Fragment>
{renderToolbar({
searchColumns,
sortColumns,
onSearch: this.handleSearch,
onSort: this.handleSort,
qsConfig,
})}
<FilterTags
itemCount={itemCount}
qsConfig={qsConfig}
onRemove={this.handleRemove}
onRemoveAll={this.handleRemoveAll}
/>
</Fragment>
<DataToolbar id={`${qsConfig.namespace}-list-toolbar`}
clearAllFilters={this.handleRemoveAll}
collapseListedFiltersBreakpoint="xl"
>
<DataToolbarContent>
{renderToolbar({
searchColumns,
sortColumns,
onSearch: this.handleSearch,
onSort: this.handleSort,
onRemove: this.handleRemove,
qsConfig,
})}
</DataToolbarContent>
</DataToolbar>
)}
</Fragment>
);

View File

@ -2,80 +2,33 @@ import React from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { withRouter } from 'react-router-dom';
import {
Button as PFButton,
Dropdown as PFDropdown,
Button,
ButtonVariant,
Dropdown,
DropdownPosition,
DropdownToggle,
DropdownItem,
Form,
FormGroup,
TextInput as PFTextInput,
InputGroup,
TextInput,
} from '@patternfly/react-core';
import {
DataToolbarGroup,
DataToolbarItem,
DataToolbarFilter
} from '@patternfly/react-core/dist/esm/experimental';
import { SearchIcon } from '@patternfly/react-icons';
import { parseQueryString } from '@util/qs';
import { QSConfig, SearchColumns } from '@types';
import styled from 'styled-components';
const TextInput = styled(PFTextInput)`
min-height: 0px;
height: 30px;
--pf-c-form-control--BorderTopColor: var(--pf-global--BorderColor--200);
--pf-c-form-control--BorderLeftColor: var(--pf-global--BorderColor--200);
`;
const Button = styled(PFButton)`
width: 34px;
padding: 0px;
::after {
border: var(--pf-c-button--BorderWidth) solid
var(--pf-global--BorderColor--200);
}
`;
const Dropdown = styled(PFDropdown)`
&&& {
/* Higher specificity required because we are selecting unclassed elements */
> button {
min-height: 30px;
min-width: 70px;
height: 30px;
padding: 0 10px;
margin: 0px;
::before {
border-color: var(--pf-global--BorderColor--200);
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
> span {
/* text element */
width: auto;
}
> svg {
/* caret icon */
margin: 0px;
padding-top: 3px;
padding-left: 3px;
}
}
}
`;
const NoOptionDropdown = styled.div`
align-self: stretch;
border: 1px solid var(--pf-global--BorderColor--200);
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
padding: 3px 7px;
border: 1px solid var(--pf-global--BorderColor--300);
padding: 5px 15px;
white-space: nowrap;
`;
const InputFormGroup = styled(FormGroup)`
flex: 1;
border-bottom-color: var(--pf-global--BorderColor--200);
`;
class Search extends React.Component {
@ -94,6 +47,7 @@ class Search extends React.Component {
this.handleDropdownToggle = this.handleDropdownToggle.bind(this);
this.handleDropdownSelect = this.handleDropdownSelect.bind(this);
this.handleSearch = this.handleSearch.bind(this);
this.handleTextKeyDown = this.handleTextKeyDown.bind(this);
}
handleDropdownToggle(isSearchDropdownOpen) {
@ -134,9 +88,15 @@ class Search extends React.Component {
this.setState({ searchValue });
}
handleTextKeyDown(e) {
if (e.key && e.key === 'Enter') {
this.handleSearch(e);
}
}
render() {
const { up } = DropdownPosition;
const { columns, i18n } = this.props;
const { columns, i18n, onRemove, qsConfig, location } = this.props;
const { isSearchDropdownOpen, searchKey, searchValue } = this.state;
const { name: searchColumnName } = columns.find(
({ key }) => key === searchKey
@ -150,65 +110,95 @@ class Search extends React.Component {
</DropdownItem>
));
const filterDefaultParams = (paramsArr, config) => {
const defaultParamsKeys = Object.keys(config.defaultParams);
return paramsArr.filter(key => defaultParamsKeys.indexOf(key) === -1);
};
const getChipsByKey = () => {
const queryParams = parseQueryString(qsConfig, location.search);
const queryParamsByKey = {};
columns.forEach(({name, key}) => {
queryParamsByKey[key] = {key, label: name, chips: []};
});
const nonDefaultParams = filterDefaultParams(
Object.keys(queryParams),
qsConfig
);
nonDefaultParams.forEach(key => {
const columnKey = key
.replace('__icontains', '');
const label = key
.replace('__icontains', '')
.split('_')
.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
.join(' ');
queryParamsByKey[columnKey] = { key, label, chips: [] };
if (Array.isArray(queryParams[key])) {
queryParams[key].forEach(val =>
queryParamsByKey[columnKey].chips.push(val)
);
} else {
queryParamsByKey[columnKey].chips.push(queryParams[key]);
}
});
return queryParamsByKey;
}
const chipsByKey = getChipsByKey();
return (
<Form autoComplete="off">
<div className="pf-c-input-group">
<DataToolbarGroup variant="filter-group">
<DataToolbarItem>
{searchDropdownItems.length > 0 ? (
<FormGroup
fieldId="searchKeyDropdown"
label={
<span className="pf-screen-reader">
{i18n._(t`Search key dropdown`)}
</span>
<Dropdown
onToggle={this.handleDropdownToggle}
onSelect={this.handleDropdownSelect}
direction={up}
toggle={
<DropdownToggle
id="awx-search"
onToggle={this.handleDropdownToggle}
style={{ width: '100%' }}
>
{searchColumnName}
</DropdownToggle>
}
>
<Dropdown
onToggle={this.handleDropdownToggle}
onSelect={this.handleDropdownSelect}
direction={up}
isOpen={isSearchDropdownOpen}
toggle={
<DropdownToggle
id="awx-search"
onToggle={this.handleDropdownToggle}
>
{searchColumnName}
</DropdownToggle>
}
dropdownItems={searchDropdownItems}
/>
</FormGroup>
) : (
<NoOptionDropdown>{searchColumnName}</NoOptionDropdown>
)}
<InputFormGroup
fieldId="searchValueTextInput"
label={
<span className="pf-screen-reader">
{i18n._(t`Search value text input`)}
</span>
}
style={{ width: '100%' }}
suppressClassNameWarning
>
isOpen={isSearchDropdownOpen}
dropdownItems={searchDropdownItems}
style={{ width: '100%' }}
/>) : (<NoOptionDropdown>{searchColumnName}</NoOptionDropdown>)}
</DataToolbarItem>
{columns.map(({key}) => (<DataToolbarFilter
chips={chipsByKey[key] ? chipsByKey[key].chips : []}
deleteChip={(unusedKey, val) => { onRemove(chipsByKey[key].key, val) }}
categoryName={chipsByKey[key] ? chipsByKey[key].label : key}
key={key}
showToolbarItem={searchKey === key}
>
<InputGroup>
<TextInput
type="search"
aria-label={i18n._(t`Search text input`)}
value={searchValue}
onChange={this.handleSearchInputChange}
style={{ height: '30px' }}
onKeyDown={this.handleTextKeyDown}
/>
</InputFormGroup>
<Button
variant="tertiary"
type="submit"
aria-label={i18n._(t`Search submit button`)}
onClick={this.handleSearch}
>
<SearchIcon />
</Button>
</div>
</Form>
<Button
variant={ButtonVariant.control}
aria-label={i18n._(t`Search submit button`)}
onClick={this.handleSearch}
>
<SearchIcon />
</Button>
</InputGroup>
</DataToolbarFilter>))}
</DataToolbarGroup>
);
}
}
@ -216,11 +206,13 @@ class Search extends React.Component {
Search.propTypes = {
qsConfig: QSConfig.isRequired,
columns: SearchColumns.isRequired,
onSearch: PropTypes.func
onSearch: PropTypes.func,
onRemove: PropTypes.func
};
Search.defaultProps = {
onSearch: null,
onRemove: null
};
export default withI18n()(Search);
export default withI18n()(withRouter(Search));

View File

@ -1,15 +1,16 @@
import React from 'react';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { withRouter } from 'react-router-dom';
import { t } from '@lingui/macro';
import {
Button,
Dropdown as PFDropdown,
ButtonVariant,
Dropdown,
DropdownPosition,
DropdownToggle,
DropdownItem,
Tooltip,
InputGroup,
} from '@patternfly/react-core';
import {
SortAlphaDownIcon,
@ -18,58 +19,11 @@ import {
SortNumericUpIcon,
} from '@patternfly/react-icons';
import styled from 'styled-components';
import {
parseQueryString
} from '@util/qs';
import { SortColumns, QSConfig } from '@types';
const Dropdown = styled(PFDropdown)`
&&& {
> button {
min-height: 30px;
min-width: 70px;
height: 30px;
padding: 0 10px;
margin: 0px;
> span {
/* text element within dropdown */
width: auto;
}
> svg {
/* caret icon */
margin: 0px;
padding-top: 3px;
padding-left: 3px;
}
}
}
`;
const IconWrapper = styled.span`
> svg {
font-size: 18px;
}
`;
const SortButton = styled(Button)`
padding: 5px 8px;
margin-top: 3px;
&:hover {
background-color: #0166cc;
color: white;
}
`;
const SortBy = styled.span`
margin-right: 15px;
font-size: var(--pf-global--FontSize--md);
`;
class Sort extends React.Component {
constructor(props) {
super(props);
@ -165,12 +119,10 @@ class Sort extends React.Component {
}
return (
<React.Fragment>
<Fragment>
{sortDropdownItems.length > 0 && (
<React.Fragment>
<SortBy>{i18n._(t`Sort By`)}</SortBy>
<InputGroup>
<Dropdown
style={{ marginRight: '10px' }}
onToggle={this.handleDropdownToggle}
onSelect={this.handleDropdownSelect}
direction={up}
@ -185,23 +137,16 @@ class Sort extends React.Component {
}
dropdownItems={sortDropdownItems}
/>
</React.Fragment>
<Button
variant={ButtonVariant.control}
aria-label={i18n._(t`Reverse Sort Order`)}
onClick={this.handleSort}
>
<SortIcon />
</Button>
</InputGroup>
)}
<Tooltip
content={<div>{i18n._(t`Reverse Sort Order`)}</div>}
position="top"
>
<SortButton
onClick={this.handleSort}
variant="plain"
aria-label={i18n._(t`Sort`)}
>
<IconWrapper>
<SortIcon style={{ verticalAlign: '-0.225em' }} />
</IconWrapper>
</SortButton>
</Tooltip>
</React.Fragment>
</Fragment>
);
}
}

View File

@ -5,7 +5,7 @@ const Separator = styled.span`
display: inline-block;
width: 1px;
height: 30px;
margin-right: 20px;
margin-right: 27px;
margin-left: 20px;
background-color: #d7d7d7;
vertical-align: middle;