mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 17:37:37 -02:30
utilize new DataToolbar experimental patternfly components
This commit is contained in:
@@ -4,73 +4,16 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Toolbar as PFToolbar,
|
|
||||||
ToolbarGroup as PFToolbarGroup,
|
|
||||||
ToolbarItem,
|
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
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 ExpandCollapse from '../ExpandCollapse';
|
||||||
import Search from '../Search';
|
import Search from '../Search';
|
||||||
import Sort from '../Sort';
|
import Sort from '../Sort';
|
||||||
import VerticalSeparator from '../VerticalSeparator';
|
|
||||||
|
|
||||||
import { SearchColumns, SortColumns, QSConfig } from '@types';
|
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`
|
const AdditionalControlsWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
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 {
|
class DataListToolbar extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
@@ -90,9 +45,9 @@ class DataListToolbar extends React.Component {
|
|||||||
showSelectAll,
|
showSelectAll,
|
||||||
isAllSelected,
|
isAllSelected,
|
||||||
isCompact,
|
isCompact,
|
||||||
fillWidth,
|
|
||||||
onSort,
|
onSort,
|
||||||
onSearch,
|
onSearch,
|
||||||
|
onRemove,
|
||||||
onCompact,
|
onCompact,
|
||||||
onExpand,
|
onExpand,
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
@@ -103,58 +58,58 @@ class DataListToolbar extends React.Component {
|
|||||||
|
|
||||||
const showExpandCollapse = onCompact && onExpand;
|
const showExpandCollapse = onCompact && onExpand;
|
||||||
return (
|
return (
|
||||||
<AWXToolbar>
|
<Fragment>
|
||||||
<Toolbar css={fillWidth ? 'margin-right: 0; margin-left: 0' : ''}>
|
{showSelectAll && (
|
||||||
<ColumnLeft fillWidth={fillWidth}>
|
<DataToolbarGroup>
|
||||||
{showSelectAll && (
|
<DataToolbarItem>
|
||||||
<Fragment>
|
<Checkbox
|
||||||
<ToolbarItem>
|
isChecked={isAllSelected}
|
||||||
<Checkbox
|
onChange={onSelectAll}
|
||||||
isChecked={isAllSelected}
|
aria-label={i18n._(t`Select all`)}
|
||||||
onChange={onSelectAll}
|
id="select-all"
|
||||||
aria-label={i18n._(t`Select all`)}
|
|
||||||
id="select-all"
|
|
||||||
/>
|
|
||||||
</ToolbarItem>
|
|
||||||
<VerticalSeparator />
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
<ToolbarItem css="flex-grow: 1;">
|
|
||||||
<Search
|
|
||||||
qsConfig={qsConfig}
|
|
||||||
columns={searchColumns}
|
|
||||||
onSearch={onSearch}
|
|
||||||
/>
|
/>
|
||||||
</ToolbarItem>
|
</DataToolbarItem>
|
||||||
<VerticalSeparator />
|
<DataToolbarSeparator variant="separator" />
|
||||||
</ColumnLeft>
|
</DataToolbarGroup>
|
||||||
<ColumnRight fillWidth={fillWidth}>
|
)}
|
||||||
<ToolbarItem>
|
<DataToolbarToggleGroup toggleIcon={<SearchIcon />} breakpoint="xl">
|
||||||
<Sort
|
<DataToolbarItem>
|
||||||
qsConfig={qsConfig}
|
<Search
|
||||||
columns={sortColumns}
|
qsConfig={qsConfig}
|
||||||
onSort={onSort}
|
columns={searchColumns}
|
||||||
/>
|
onSearch={onSearch}
|
||||||
</ToolbarItem>
|
onRemove={onRemove}
|
||||||
{showExpandCollapse && (
|
/>
|
||||||
<Fragment>
|
</DataToolbarItem>
|
||||||
<VerticalSeparator />
|
<DataToolbarItem>
|
||||||
<ToolbarGroup>
|
<Sort
|
||||||
<ExpandCollapse
|
qsConfig={qsConfig}
|
||||||
isCompact={isCompact}
|
columns={sortColumns}
|
||||||
onCompact={onCompact}
|
onSort={onSort}
|
||||||
onExpand={onExpand}
|
/>
|
||||||
/>
|
</DataToolbarItem>
|
||||||
</ToolbarGroup>
|
</DataToolbarToggleGroup>
|
||||||
{additionalControls && <VerticalSeparator />}
|
<DataToolbarGroup>
|
||||||
</Fragment>
|
{showExpandCollapse && (
|
||||||
)}
|
<Fragment>
|
||||||
|
<DataToolbarItem>
|
||||||
|
<ExpandCollapse
|
||||||
|
isCompact={isCompact}
|
||||||
|
onCompact={onCompact}
|
||||||
|
onExpand={onExpand}
|
||||||
|
/>
|
||||||
|
</DataToolbarItem>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</DataToolbarGroup>
|
||||||
|
<AdditionalControlsDataToolbarGroup>
|
||||||
|
<DataToolbarItem>
|
||||||
<AdditionalControlsWrapper>
|
<AdditionalControlsWrapper>
|
||||||
{additionalControls}
|
{additionalControls}
|
||||||
</AdditionalControlsWrapper>
|
</AdditionalControlsWrapper>
|
||||||
</ColumnRight>
|
</DataToolbarItem>
|
||||||
</Toolbar>
|
</AdditionalControlsDataToolbarGroup>
|
||||||
</AWXToolbar>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,7 +121,6 @@ DataListToolbar.propTypes = {
|
|||||||
showSelectAll: PropTypes.bool,
|
showSelectAll: PropTypes.bool,
|
||||||
isAllSelected: PropTypes.bool,
|
isAllSelected: PropTypes.bool,
|
||||||
isCompact: PropTypes.bool,
|
isCompact: PropTypes.bool,
|
||||||
fillWidth: PropTypes.bool,
|
|
||||||
onCompact: PropTypes.func,
|
onCompact: PropTypes.func,
|
||||||
onExpand: PropTypes.func,
|
onExpand: PropTypes.func,
|
||||||
onSearch: PropTypes.func,
|
onSearch: PropTypes.func,
|
||||||
@@ -179,7 +133,6 @@ DataListToolbar.defaultProps = {
|
|||||||
showSelectAll: false,
|
showSelectAll: false,
|
||||||
isAllSelected: false,
|
isAllSelected: false,
|
||||||
isCompact: false,
|
isCompact: false,
|
||||||
fillWidth: false,
|
|
||||||
onCompact: null,
|
onCompact: null,
|
||||||
onExpand: null,
|
onExpand: null,
|
||||||
onSearch: null,
|
onSearch: null,
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
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 { 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';
|
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`
|
const ResultCount = styled.span`
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
`;
|
`;
|
||||||
@@ -24,12 +16,6 @@ const FilterLabel = styled.span`
|
|||||||
padding-right: 20px;
|
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
|
// remove non-default query params so they don't show up as filter tags
|
||||||
const filterDefaultParams = (paramsArr, config) => {
|
const filterDefaultParams = (paramsArr, config) => {
|
||||||
const defaultParamsKeys = Object.keys(config.defaultParams);
|
const defaultParamsKeys = Object.keys(config.defaultParams);
|
||||||
@@ -45,7 +31,7 @@ const FilterTags = ({
|
|||||||
onRemoveAll,
|
onRemoveAll,
|
||||||
}) => {
|
}) => {
|
||||||
const queryParams = parseQueryString(qsConfig, location.search);
|
const queryParams = parseQueryString(qsConfig, location.search);
|
||||||
const queryParamsArr = [];
|
const queryParamsByKey = {};
|
||||||
const nonDefaultParams = filterDefaultParams(
|
const nonDefaultParams = filterDefaultParams(
|
||||||
Object.keys(queryParams),
|
Object.keys(queryParams),
|
||||||
qsConfig
|
qsConfig
|
||||||
@@ -56,45 +42,45 @@ const FilterTags = ({
|
|||||||
.split('_')
|
.split('_')
|
||||||
.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
queryParamsByKey[key] = { label, tags: [] };
|
||||||
|
|
||||||
if (Array.isArray(queryParams[key])) {
|
if (Array.isArray(queryParams[key])) {
|
||||||
queryParams[key].forEach(val =>
|
queryParams[key].forEach(val =>
|
||||||
queryParamsArr.push({ key, value: val, label })
|
queryParamsByKey[key].tags.push(val)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
queryParamsArr.push({ key, value: queryParams[key], label });
|
queryParamsByKey[key].tags.push(queryParams[key]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
queryParamsArr.length > 0 && (
|
Object.keys(queryParamsByKey).length > 0 && (
|
||||||
<FilterTagsRow>
|
<Fragment>
|
||||||
<ResultCount>{i18n._(t`${itemCount} results`)}</ResultCount>
|
<DataToolbarGroup>
|
||||||
<VerticalSeparator />
|
<ResultCount>{i18n._(t`${itemCount} results`)}</ResultCount>
|
||||||
<FilterLabel>{i18n._(t`Active Filters:`)}</FilterLabel>
|
</DataToolbarGroup>
|
||||||
<ChipGroup defaultIsOpen>
|
<DataToolbarGroup>
|
||||||
{queryParamsArr.map(({ key, label, value }) => (
|
<FilterLabel>{i18n._(t`Active Filters:`)}</FilterLabel>
|
||||||
<Chip
|
<DataToolbarItem variant="chip-group">
|
||||||
className="searchTagChip"
|
{Object.keys(queryParamsByKey).map(key => (
|
||||||
key={`${key}__${value}`}
|
<ChipGroup withToolbar key={`${key}-group`}>
|
||||||
isReadOnly={false}
|
<ChipGroupToolbarItem key={key} categoryName={queryParamsByKey[key].label}>
|
||||||
onClick={() => onRemove(key, value)}
|
{queryParamsByKey[key].tags.map(chip => (
|
||||||
>
|
<Chip key={chip} onClick={() => onRemove(key, chip)}>
|
||||||
<b>{label}:</b> {value}
|
{chip}
|
||||||
</Chip>
|
</Chip>
|
||||||
))}
|
))}
|
||||||
<div className="pf-c-chip pf-m-overflow">
|
</ChipGroupToolbarItem>
|
||||||
<Button
|
</ChipGroup>
|
||||||
variant="plain"
|
))}
|
||||||
type="button"
|
</DataToolbarItem>
|
||||||
aria-label={i18n._(t`Clear all search filters`)}
|
<DataToolbarItem>
|
||||||
onClick={onRemoveAll}
|
<Button variant="link" onClick={onRemoveAll} isInline>
|
||||||
>
|
{i18n._(t`Clear all search filters`)}
|
||||||
<span className="pf-c-chip__text">{i18n._(t`Clear all`)}</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</DataToolbarItem>
|
||||||
</ChipGroup>
|
</DataToolbarGroup>
|
||||||
</FilterTagsRow>
|
</Fragment>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React, { Fragment } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { DataToolbar, DataToolbarContent } from '@patternfly/react-core/dist/esm/experimental';
|
||||||
import DataListToolbar from '@components/DataListToolbar';
|
import DataListToolbar from '@components/DataListToolbar';
|
||||||
import FilterTags from '@components/FilterTags';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
encodeNonDefaultQueryString,
|
encodeNonDefaultQueryString,
|
||||||
@@ -84,33 +84,32 @@ class ListHeader extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{isEmpty ? (
|
{isEmpty ? (
|
||||||
<Fragment>
|
<DataToolbar id={`${qsConfig.namespace}-list-toolbar`}
|
||||||
<EmptyStateControlsWrapper>
|
clearAllFilters={this.handleRemoveAll}
|
||||||
{emptyStateControls}
|
collapseListedFiltersBreakpoint="md"
|
||||||
</EmptyStateControlsWrapper>
|
>
|
||||||
<FilterTags
|
<DataToolbarContent>
|
||||||
itemCount={itemCount}
|
<EmptyStateControlsWrapper>
|
||||||
qsConfig={qsConfig}
|
{emptyStateControls}
|
||||||
onRemove={this.handleRemove}
|
</EmptyStateControlsWrapper>
|
||||||
onRemoveAll={this.handleRemoveAll}
|
</DataToolbarContent>
|
||||||
/>
|
</DataToolbar>
|
||||||
</Fragment>
|
|
||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<DataToolbar id={`${qsConfig.namespace}-list-toolbar`}
|
||||||
{renderToolbar({
|
clearAllFilters={this.handleRemoveAll}
|
||||||
searchColumns,
|
collapseListedFiltersBreakpoint="xl"
|
||||||
sortColumns,
|
>
|
||||||
onSearch: this.handleSearch,
|
<DataToolbarContent>
|
||||||
onSort: this.handleSort,
|
{renderToolbar({
|
||||||
qsConfig,
|
searchColumns,
|
||||||
})}
|
sortColumns,
|
||||||
<FilterTags
|
onSearch: this.handleSearch,
|
||||||
itemCount={itemCount}
|
onSort: this.handleSort,
|
||||||
qsConfig={qsConfig}
|
onRemove: this.handleRemove,
|
||||||
onRemove={this.handleRemove}
|
qsConfig,
|
||||||
onRemoveAll={this.handleRemoveAll}
|
})}
|
||||||
/>
|
</DataToolbarContent>
|
||||||
</Fragment>
|
</DataToolbar>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,80 +2,33 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Button as PFButton,
|
Button,
|
||||||
Dropdown as PFDropdown,
|
ButtonVariant,
|
||||||
|
Dropdown,
|
||||||
DropdownPosition,
|
DropdownPosition,
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
Form,
|
InputGroup,
|
||||||
FormGroup,
|
TextInput,
|
||||||
TextInput as PFTextInput,
|
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
import {
|
||||||
|
DataToolbarGroup,
|
||||||
|
DataToolbarItem,
|
||||||
|
DataToolbarFilter
|
||||||
|
} from '@patternfly/react-core/dist/esm/experimental';
|
||||||
import { SearchIcon } from '@patternfly/react-icons';
|
import { SearchIcon } from '@patternfly/react-icons';
|
||||||
|
import { parseQueryString } from '@util/qs';
|
||||||
import { QSConfig, SearchColumns } from '@types';
|
import { QSConfig, SearchColumns } from '@types';
|
||||||
|
|
||||||
import styled from 'styled-components';
|
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`
|
const NoOptionDropdown = styled.div`
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
border: 1px solid var(--pf-global--BorderColor--200);
|
border: 1px solid var(--pf-global--BorderColor--300);
|
||||||
border-top-left-radius: 3px;
|
padding: 5px 15px;
|
||||||
border-bottom-left-radius: 3px;
|
|
||||||
padding: 3px 7px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
border-bottom-color: var(--pf-global--BorderColor--200);
|
||||||
|
|
||||||
const InputFormGroup = styled(FormGroup)`
|
|
||||||
flex: 1;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
class Search extends React.Component {
|
class Search extends React.Component {
|
||||||
@@ -94,6 +47,7 @@ class Search extends React.Component {
|
|||||||
this.handleDropdownToggle = this.handleDropdownToggle.bind(this);
|
this.handleDropdownToggle = this.handleDropdownToggle.bind(this);
|
||||||
this.handleDropdownSelect = this.handleDropdownSelect.bind(this);
|
this.handleDropdownSelect = this.handleDropdownSelect.bind(this);
|
||||||
this.handleSearch = this.handleSearch.bind(this);
|
this.handleSearch = this.handleSearch.bind(this);
|
||||||
|
this.handleTextKeyDown = this.handleTextKeyDown.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDropdownToggle(isSearchDropdownOpen) {
|
handleDropdownToggle(isSearchDropdownOpen) {
|
||||||
@@ -134,9 +88,15 @@ class Search extends React.Component {
|
|||||||
this.setState({ searchValue });
|
this.setState({ searchValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTextKeyDown(e) {
|
||||||
|
if (e.key && e.key === 'Enter') {
|
||||||
|
this.handleSearch(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { up } = DropdownPosition;
|
const { up } = DropdownPosition;
|
||||||
const { columns, i18n } = this.props;
|
const { columns, i18n, onRemove, qsConfig, location } = this.props;
|
||||||
const { isSearchDropdownOpen, searchKey, searchValue } = this.state;
|
const { isSearchDropdownOpen, searchKey, searchValue } = this.state;
|
||||||
const { name: searchColumnName } = columns.find(
|
const { name: searchColumnName } = columns.find(
|
||||||
({ key }) => key === searchKey
|
({ key }) => key === searchKey
|
||||||
@@ -150,65 +110,95 @@ class Search extends React.Component {
|
|||||||
</DropdownItem>
|
</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 (
|
return (
|
||||||
<Form autoComplete="off">
|
<DataToolbarGroup variant="filter-group">
|
||||||
<div className="pf-c-input-group">
|
<DataToolbarItem>
|
||||||
{searchDropdownItems.length > 0 ? (
|
{searchDropdownItems.length > 0 ? (
|
||||||
<FormGroup
|
<Dropdown
|
||||||
fieldId="searchKeyDropdown"
|
onToggle={this.handleDropdownToggle}
|
||||||
label={
|
onSelect={this.handleDropdownSelect}
|
||||||
<span className="pf-screen-reader">
|
direction={up}
|
||||||
{i18n._(t`Search key dropdown`)}
|
toggle={
|
||||||
</span>
|
<DropdownToggle
|
||||||
|
id="awx-search"
|
||||||
|
onToggle={this.handleDropdownToggle}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{searchColumnName}
|
||||||
|
</DropdownToggle>
|
||||||
}
|
}
|
||||||
>
|
isOpen={isSearchDropdownOpen}
|
||||||
<Dropdown
|
dropdownItems={searchDropdownItems}
|
||||||
onToggle={this.handleDropdownToggle}
|
style={{ width: '100%' }}
|
||||||
onSelect={this.handleDropdownSelect}
|
/>) : (<NoOptionDropdown>{searchColumnName}</NoOptionDropdown>)}
|
||||||
direction={up}
|
</DataToolbarItem>
|
||||||
isOpen={isSearchDropdownOpen}
|
{columns.map(({key}) => (<DataToolbarFilter
|
||||||
toggle={
|
chips={chipsByKey[key] ? chipsByKey[key].chips : []}
|
||||||
<DropdownToggle
|
deleteChip={(unusedKey, val) => { onRemove(chipsByKey[key].key, val) }}
|
||||||
id="awx-search"
|
categoryName={chipsByKey[key] ? chipsByKey[key].label : key}
|
||||||
onToggle={this.handleDropdownToggle}
|
key={key}
|
||||||
>
|
showToolbarItem={searchKey === key}
|
||||||
{searchColumnName}
|
>
|
||||||
</DropdownToggle>
|
<InputGroup>
|
||||||
}
|
|
||||||
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
|
|
||||||
>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
type="search"
|
type="search"
|
||||||
aria-label={i18n._(t`Search text input`)}
|
aria-label={i18n._(t`Search text input`)}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={this.handleSearchInputChange}
|
onChange={this.handleSearchInputChange}
|
||||||
style={{ height: '30px' }}
|
onKeyDown={this.handleTextKeyDown}
|
||||||
/>
|
/>
|
||||||
</InputFormGroup>
|
<Button
|
||||||
<Button
|
variant={ButtonVariant.control}
|
||||||
variant="tertiary"
|
aria-label={i18n._(t`Search submit button`)}
|
||||||
type="submit"
|
onClick={this.handleSearch}
|
||||||
aria-label={i18n._(t`Search submit button`)}
|
>
|
||||||
onClick={this.handleSearch}
|
<SearchIcon />
|
||||||
>
|
</Button>
|
||||||
<SearchIcon />
|
</InputGroup>
|
||||||
</Button>
|
</DataToolbarFilter>))}
|
||||||
</div>
|
</DataToolbarGroup>
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,11 +206,13 @@ class Search extends React.Component {
|
|||||||
Search.propTypes = {
|
Search.propTypes = {
|
||||||
qsConfig: QSConfig.isRequired,
|
qsConfig: QSConfig.isRequired,
|
||||||
columns: SearchColumns.isRequired,
|
columns: SearchColumns.isRequired,
|
||||||
onSearch: PropTypes.func
|
onSearch: PropTypes.func,
|
||||||
|
onRemove: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
Search.defaultProps = {
|
Search.defaultProps = {
|
||||||
onSearch: null,
|
onSearch: null,
|
||||||
|
onRemove: null
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(Search);
|
export default withI18n()(withRouter(Search));
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dropdown as PFDropdown,
|
ButtonVariant,
|
||||||
|
Dropdown,
|
||||||
DropdownPosition,
|
DropdownPosition,
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
Tooltip,
|
InputGroup,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import {
|
import {
|
||||||
SortAlphaDownIcon,
|
SortAlphaDownIcon,
|
||||||
@@ -18,58 +19,11 @@ import {
|
|||||||
SortNumericUpIcon,
|
SortNumericUpIcon,
|
||||||
} from '@patternfly/react-icons';
|
} from '@patternfly/react-icons';
|
||||||
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
parseQueryString
|
parseQueryString
|
||||||
} from '@util/qs';
|
} from '@util/qs';
|
||||||
import { SortColumns, QSConfig } from '@types';
|
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 {
|
class Sort extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -165,12 +119,10 @@ class Sort extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<Fragment>
|
||||||
{sortDropdownItems.length > 0 && (
|
{sortDropdownItems.length > 0 && (
|
||||||
<React.Fragment>
|
<InputGroup>
|
||||||
<SortBy>{i18n._(t`Sort By`)}</SortBy>
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
style={{ marginRight: '10px' }}
|
|
||||||
onToggle={this.handleDropdownToggle}
|
onToggle={this.handleDropdownToggle}
|
||||||
onSelect={this.handleDropdownSelect}
|
onSelect={this.handleDropdownSelect}
|
||||||
direction={up}
|
direction={up}
|
||||||
@@ -185,23 +137,16 @@ class Sort extends React.Component {
|
|||||||
}
|
}
|
||||||
dropdownItems={sortDropdownItems}
|
dropdownItems={sortDropdownItems}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
<Button
|
||||||
|
variant={ButtonVariant.control}
|
||||||
|
aria-label={i18n._(t`Reverse Sort Order`)}
|
||||||
|
onClick={this.handleSort}
|
||||||
|
>
|
||||||
|
<SortIcon />
|
||||||
|
</Button>
|
||||||
|
</InputGroup>
|
||||||
)}
|
)}
|
||||||
<Tooltip
|
</Fragment>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const Separator = styled.span`
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
margin-right: 20px;
|
margin-right: 27px;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
background-color: #d7d7d7;
|
background-color: #d7d7d7;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|||||||
Reference in New Issue
Block a user