diff --git a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx index 7248dc0c51..31a1184cc9 100644 --- a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx +++ b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx @@ -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 ( - - - - {showSelectAll && ( - - - - - - - )} - - + {showSelectAll && ( + + + - - - - - - - - {showExpandCollapse && ( - - - - - - {additionalControls && } - - )} + + + + )} + } breakpoint="xl"> + + + + + + + + + {showExpandCollapse && ( + + + + + + )} + + + {additionalControls} - - - + + + ); } } @@ -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, diff --git a/awx/ui_next/src/components/FilterTags/FilterTags.jsx b/awx/ui_next/src/components/FilterTags/FilterTags.jsx index 749c326256..b088d10b8d 100644 --- a/awx/ui_next/src/components/FilterTags/FilterTags.jsx +++ b/awx/ui_next/src/components/FilterTags/FilterTags.jsx @@ -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 && ( - - {i18n._(t`${itemCount} results`)} - - {i18n._(t`Active Filters:`)} - - {queryParamsArr.map(({ key, label, value }) => ( - onRemove(key, value)} - > - {label}: {value} - - ))} -
- -
-
-
+ + + ) ); }; diff --git a/awx/ui_next/src/components/ListHeader/ListHeader.jsx b/awx/ui_next/src/components/ListHeader/ListHeader.jsx index 93d52df4c9..e88505cb5f 100644 --- a/awx/ui_next/src/components/ListHeader/ListHeader.jsx +++ b/awx/ui_next/src/components/ListHeader/ListHeader.jsx @@ -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 ( {isEmpty ? ( - - - {emptyStateControls} - - - + + + + {emptyStateControls} + + + ) : ( - - {renderToolbar({ - searchColumns, - sortColumns, - onSearch: this.handleSearch, - onSort: this.handleSort, - qsConfig, - })} - - + + + {renderToolbar({ + searchColumns, + sortColumns, + onSearch: this.handleSearch, + onSort: this.handleSort, + onRemove: this.handleRemove, + qsConfig, + })} + + )} ); diff --git a/awx/ui_next/src/components/Search/Search.jsx b/awx/ui_next/src/components/Search/Search.jsx index 6dda72a866..528b428cc2 100644 --- a/awx/ui_next/src/components/Search/Search.jsx +++ b/awx/ui_next/src/components/Search/Search.jsx @@ -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 { )); + 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 ( -
-
+ + {searchDropdownItems.length > 0 ? ( - - {i18n._(t`Search key dropdown`)} - + + {searchColumnName} + } - > - - {searchColumnName} - - } - dropdownItems={searchDropdownItems} - /> - - ) : ( - {searchColumnName} - )} - - {i18n._(t`Search value text input`)} - - } - style={{ width: '100%' }} - suppressClassNameWarning - > + isOpen={isSearchDropdownOpen} + dropdownItems={searchDropdownItems} + style={{ width: '100%' }} + />) : ({searchColumnName})} + + {columns.map(({key}) => ( { onRemove(chipsByKey[key].key, val) }} + categoryName={chipsByKey[key] ? chipsByKey[key].label : key} + key={key} + showToolbarItem={searchKey === key} + > + - - -
-
+ + + ))} + ); } } @@ -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)); diff --git a/awx/ui_next/src/components/Sort/Sort.jsx b/awx/ui_next/src/components/Sort/Sort.jsx index 5b9ba6f523..fd12908917 100644 --- a/awx/ui_next/src/components/Sort/Sort.jsx +++ b/awx/ui_next/src/components/Sort/Sort.jsx @@ -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 ( - + {sortDropdownItems.length > 0 && ( - - {i18n._(t`Sort By`)} + - + + )} - {i18n._(t`Reverse Sort Order`)}} - position="top" - > - - - - - - - + ); } } diff --git a/awx/ui_next/src/components/VerticalSeparator/VerticalSeparator.jsx b/awx/ui_next/src/components/VerticalSeparator/VerticalSeparator.jsx index ebe53e5ada..94679dadf7 100644 --- a/awx/ui_next/src/components/VerticalSeparator/VerticalSeparator.jsx +++ b/awx/ui_next/src/components/VerticalSeparator/VerticalSeparator.jsx @@ -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;