diff --git a/src/components/CreateVmWizard/AddVmButton.js b/src/components/CreateVmWizard/AddVmButton.js index c9a454b97..e0323e564 100644 --- a/src/components/CreateVmWizard/AddVmButton.js +++ b/src/components/CreateVmWizard/AddVmButton.js @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { Button } from 'patternfly-react' +import { Button } from '@patternfly/react-core' import * as Actions from '_/actions' import { CREATE_PAGE_TYPE } from '_/constants' @@ -43,7 +43,6 @@ class AddVmButton extends React.Component { <> - - - - - , + + + + + + + + + + + + + + + + + + + + , container ) } diff --git a/src/components/Settings/style.css b/src/components/Settings/style.css index 6fc927d7e..c7da0bbed 100644 --- a/src/components/Settings/style.css +++ b/src/components/Settings/style.css @@ -58,8 +58,9 @@ margin-right: 10px; } -:global(#settings-toolbar) { - margin-left: -20px; +.settings-toolbar { + padding-top: 5px; + padding-bottom: 5px; } .section-list{ diff --git a/src/components/Toolbar/VmFilters.js b/src/components/Toolbar/VmFilters.js index b1d392056..383dd7d48 100644 --- a/src/components/Toolbar/VmFilters.js +++ b/src/components/Toolbar/VmFilters.js @@ -1,215 +1,262 @@ -import React from 'react' +import React, { useState, useMemo } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { Filter, FormControl } from 'patternfly-react' import { enumMsg, withMsg } from '_/intl' import { saveVmsFilters } from '_/actions' -import { localeCompare } from '_/helpers' +import { localeCompare, toJS } from '_/helpers' +import { + Button, + ButtonVariant, + Dropdown, + DropdownItem, + DropdownPosition, + DropdownToggle, + InputGroup, + Select, + SelectOption, + SelectVariant, + TextInput, + ToolbarGroup, + ToolbarFilter, + ToolbarItem, + ToolbarToggleGroup, +} from '@patternfly/react-core' -import style from './style.css' +import { FilterIcon, SearchIcon } from '@patternfly/react-icons/dist/esm/icons' -class VmFilters extends React.Component { - constructor (props) { - super(props) +const STATUS = 'status' +const OS = 'os' +const NAME = 'name' - this.composeFilterTypes = this.composeFilterTypes.bind(this) - this.filterAdded = this.filterAdded.bind(this) - this.selectFilterType = this.selectFilterType.bind(this) - this.filterValueSelected = this.filterValueSelected.bind(this) - this.updateCurrentValue = this.updateCurrentValue.bind(this) - this.onValueKeyPress = this.onValueKeyPress.bind(this) - this.filterExists = this.filterExists.bind(this) - this.getFilterValue = this.getFilterValue.bind(this) - this.renderInput = this.renderInput.bind(this) - - const filterTypes = this.composeFilterTypes() - this.state = { - currentFilterType: filterTypes[0], - activeFilters: {}, - currentValue: '', - } - } - - composeFilterTypes () { - const { msg, locale } = this.props - const statuses = [ - 'up', - 'powering_up', - 'down', - 'paused', - 'suspended', - 'powering_down', - 'not_responding', - 'unknown', - 'unassigned', - 'migrating', - 'wait_for_launch', - 'reboot_in_progress', - 'saving_state', - 'restoring_state', - 'image_locked', - ] - const filterTypes = [ - { - id: 'name', - title: msg.name(), - placeholder: msg.vmFilterTypePlaceholderName(), - filterType: 'text', - }, - { - id: 'status', - title: msg.status(), - placeholder: msg.vmFilterTypePlaceholderStatus(), - filterType: 'select', - filterValues: statuses - .map((status) => ({ title: enumMsg('VmStatus', status, msg), id: status })) - .sort((a, b) => localeCompare(a.title, b.title, locale)), - }, - { - id: 'os', - title: msg.operatingSystem(), - placeholder: msg.vmFilterTypePlaceholderOS(), - filterType: 'select', - filterValues: Array.from(this.props.operatingSystems - .toList() - .reduce((acc, item) => ( - acc.add(item.get('description')) - ), new Set())) - .map(item => ({ title: item, id: item })) - .sort((a, b) => localeCompare(a.title, b.title, locale)), - }, - ] - return filterTypes +const composeStatus = (msg, locale) => { + const statuses = [ + 'up', + 'powering_up', + 'down', + 'paused', + 'suspended', + 'powering_down', + 'not_responding', + 'unknown', + 'unassigned', + 'migrating', + 'wait_for_launch', + 'reboot_in_progress', + 'saving_state', + 'restoring_state', + 'image_locked', + ] + return { + id: STATUS, + title: msg.status(), + placeholder: msg.vmFilterTypePlaceholderStatus(), + filterValues: Object.entries(statuses + .map((status) => ({ title: enumMsg('VmStatus', status, msg), id: status })) + .reduce((acc, { title, id }) => { + acc[title] = { ...acc[title], [id]: id } + return acc + }, {})) + .map(([title, ids]) => ({ title, ids })) + .sort((a, b) => localeCompare(a.title, b.title, locale)), } +} - filterAdded (field, value) { - const activeFilters = { ...this.props.vms.get('filters').toJS() } - if ((field.filterType === 'select')) { - activeFilters[field.id] = value.title - } else { - if (!activeFilters[field.id]) { - activeFilters[field.id] = [] - } - activeFilters[field.id].push(value) - } - this.props.onFilterUpdate(activeFilters) - }; +const composeOs = (msg, locale, operatingSystems) => { + return ({ + id: OS, + title: msg.operatingSystem(), + placeholder: msg.vmFilterTypePlaceholderOS(), + filterValues: Object.entries(operatingSystems + .toList().toJS() + // { name: 'other_linux_ppc64', description: 'Linux'}, {description: 'Linux', name: 'other_linux'} + // {title: 'Linux', ids: {'other_linux_ppc64', 'other_linux'} + .reduce((acc, { name, description }) => { + acc[description] = { ...acc[description], [name]: name } + return acc + }, {})) + .map(([description, ids]) => ({ title: description, ids })) + .sort((a, b) => localeCompare(a.title, b.title, locale)), + }) +} - selectFilterType (filterType) { - const { currentFilterType } = this.state - if (currentFilterType !== filterType) { - let newCurrentValue = '' - if (filterType.filterType === 'select') { - if (this.filterExists(filterType.id)) { - const filterValue = this.getFilterValue(filterType.id) - newCurrentValue = filterValue - } - } - this.setState({ - currentFilterType: filterType, - currentValue: newCurrentValue, - }) - } - } +const Filter = ({ filterIds = [], setFilters, allSupportedFilters = [], title, filterColumnId, showToolbarItem }) => { + const [isExpanded, setExpanded] = useState(false) - filterValueSelected (filterValue) { - const { currentFilterType, currentValue } = this.state + // one label can map to many IDs so it's easier work with labels + // and reverse map label-to-IDs on save + const toChip = ({ title }) => title + const toOption = ({ title }) => title + const toOptionNode = ({ title }) => + - if (filterValue !== currentValue) { - this.setState({ currentValue: filterValue }) - if (filterValue) { - this.filterAdded(currentFilterType, filterValue) - } - } + // titles are guaranteed to be unique + // return first filter with matching title + const labelToIds = (title) => { + const [{ ids = {} } = {}] = allSupportedFilters.filter(filter => filter.title === title) || [] + return ids } - - updateCurrentValue (event) { - this.setState({ currentValue: event.target.value }) + const selectedFilters = allSupportedFilters.filter(({ ids }) => filterIds.find(id => ids[id])) + const deleteFilter = (title) => { + const ids = labelToIds(title) + // delete all filter IDs linked to provided title + setFilters(filterIds.filter(id => !ids[id])) } - onValueKeyPress (keyEvent) { - const { currentValue, currentFilterType } = this.state - - if (keyEvent.key === 'Enter' && currentValue && currentValue.length > 0) { - this.setState({ currentValue: '' }) - this.filterAdded(currentFilterType, currentValue) - keyEvent.stopPropagation() - keyEvent.preventDefault() - } + const addFilter = (title) => { + const ids = labelToIds(title) + // add all filter IDs linked + setFilters([...filterIds, ...Object.keys(ids)]) } + return ( + deleteFilter(option)} + deleteChipGroup={() => setFilters([])} + categoryName={filterColumnId} + showToolbarItem={showToolbarItem} + > + + + ) +} - filterExists (fieldId) { - return !!this.props.vms.getIn(['filters', fieldId]) - }; +Filter.propTypes = { + filterIds: PropTypes.array.isRequired, + allSupportedFilters: PropTypes.array.isRequired, + setFilters: PropTypes.func.isRequired, + title: PropTypes.string.isRequired, + filterColumnId: PropTypes.string.isRequired, + showToolbarItem: PropTypes.bool.isRequired, +} - getFilterValue (fieldId) { - return this.props.vms.getIn(['filters', fieldId]) - }; +const VmFilters = ({ msg, locale, operatingSystems, selectedFilters, onFilterUpdate }) => { + const filterTypes = useMemo(() => [ + { + id: NAME, + title: msg.name(), + placeholder: msg.vmFilterTypePlaceholderName(), + }, + composeStatus(msg, locale), + composeOs(msg, locale, operatingSystems), + ], [msg, locale, operatingSystems]) + const [currentFilterType, setCurrentFilterType] = useState(filterTypes[0]) + const [expanded, setExpanded] = useState(false) + const [inputValue, setInputValue] = useState('') - renderInput () { - const { currentFilterType, currentValue, filterCategory } = this.state - if (!currentFilterType) { - return null - } + const nameFilter = filterTypes.find(({ id }) => id === NAME) + const labelToFilter = (label) => filterTypes.find(({ title }) => title === label) ?? currentFilterType - if (currentFilterType.filterType === 'select') { - if (currentValue !== '' && !this.filterExists(currentFilterType.id)) { - this.setState({ - currentValue: '', - filterCategory, - }) - } - return ( - - ) + const onFilterTypeSelect = (event) => { + setCurrentFilterType(labelToFilter(event?.target?.innerText)) + setExpanded(!expanded) + } + const onFilterTypeToggle = () => setExpanded(!expanded) + const onNameInput = (event) => { + if ((event.key && event.key !== 'Enter') || + !inputValue || + selectedFilters?.[NAME]?.includes(inputValue)) { + return } - return ( - this.updateCurrentValue(e)} - onKeyPress={e => this.onValueKeyPress(e)} - /> - ) + onFilterUpdate({ ...selectedFilters, [NAME]: [...(selectedFilters?.[NAME] ?? []), inputValue] }) + setInputValue('') } - render () { - const { currentFilterType } = this.state - - const filterTypes = this.composeFilterTypes() - - return ( - - - {this.renderInput()} - - ) - } + return ( + } breakpoint="xl"> + + + + {currentFilterType.title} + + )} + isOpen={expanded} + style={{ width: '100%' }} + dropdownItems={ + filterTypes.map(({ id, title }) => + {title}) + } + /> + + onFilterUpdate({ + ...selectedFilters, + [NAME]: selectedFilters?.[NAME]?.filter?.(value => value !== option) ?? [], + })} + deleteChipGroup={() => onFilterUpdate({ ...selectedFilters, [NAME]: [] })} + categoryName={NAME} + showToolbarItem={currentFilterType.id === NAME} + > + + + + + + {filterTypes.filter(({ id }) => id !== NAME)?.map(({ id, filterValues, placeholder }) => ( + onFilterUpdate({ ...selectedFilters, [id]: filtersToSave })} + title={placeholder} + /> + ) + )} + + + ) } VmFilters.propTypes = { operatingSystems: PropTypes.object.isRequired, - vms: PropTypes.object.isRequired, + selectedFilters: PropTypes.object.isRequired, onFilterUpdate: PropTypes.func.isRequired, msg: PropTypes.object.isRequired, locale: PropTypes.string.isRequired, } export default connect( - (state) => ({ - operatingSystems: state.operatingSystems, - vms: state.vms, + ({ operatingSystems, vms }) => ({ + operatingSystems, + selectedFilters: toJS(vms.get('filters')), }), (dispatch) => ({ onFilterUpdate: (filters) => dispatch(saveVmsFilters({ filters })), diff --git a/src/components/Toolbar/VmSort.js b/src/components/Toolbar/VmSort.js index 7c7de7478..9fe14c2ac 100644 --- a/src/components/Toolbar/VmSort.js +++ b/src/components/Toolbar/VmSort.js @@ -1,63 +1,63 @@ -import React from 'react' +import React, { useState } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { Sort } from 'patternfly-react' import { setVmSort } from '_/actions' import { SortFields } from '_/utils' -import { Tooltip } from '_/components/tooltips' import { withMsg } from '_/intl' import { translate } from '_/helpers' +import { + OptionsMenu, + OptionsMenuItemGroup, + OptionsMenuSeparator, + OptionsMenuItem, + OptionsMenuToggle, +} from '@patternfly/react-core' +import { SortAmountDownIcon, SortAmountDownAltIcon } from '@patternfly/react-icons/dist/esm/icons' -class VmSort extends React.Component { - constructor (props) { - super(props) - this.updateCurrentSortType = this.updateCurrentSortType.bind(this) - this.toggleCurrentSortDirection = this.toggleCurrentSortDirection.bind(this) - this.getSortTooltipMessage = this.getSortTooltipMessage.bind(this) - } +const VmSort = ({ sort, msg, onSortChange }) => { + const { id: enabledSortId, isAsc } = sort + const [expanded, setExpanded] = useState(false) - updateCurrentSortType (sortType) { - this.props.onSortChange({ ...sortType, isAsc: this.props.sort.isAsc }) - } + const menuItems = [ + + {Object.values(SortFields) + .map(type => ({ ...type, title: translate({ ...type.messageDescriptor, msg }) })) + .map(({ title, id, messageDescriptor }) => ( + onSortChange({ ...sort, id, messageDescriptor })} + > + {title} + + )) + } + , + , + + onSortChange({ ...sort, isAsc: true })} isSelected={isAsc} id="ascending" key="ascending">{msg.ascending()} + onSortChange({ ...sort, isAsc: false })} isSelected={!isAsc} id="descending" key="descending">{msg.descending()} + , + ] - toggleCurrentSortDirection () { - const sort = this.props.sort - this.props.onSortChange({ ...sort, isAsc: !sort.isAsc }) - } - - getSortTooltipMessage () { - const { msg } = this.props - const { sort: { id, isAsc } } = this.props - return id === 'os' || id === 'name' - ? (isAsc ? msg.sortAToZ() : msg.sortZToA()) - : (isAsc ? msg.sortOffFirst() : msg.sortRunningFirst()) - } - - render () { - const { sort, msg } = this.props - - return ( - - ({ ...type, title: translate({ ...type.messageDescriptor, msg }) }))} - currentSortType={sort && { ...sort, title: translate({ ...sort.messageDescriptor, msg }) }} - onSortTypeSelected={this.updateCurrentSortType} + return [ + setExpanded(!expanded)} + toggleTemplate={msg.sortBy()} /> - - - - - ) - } + )} + isGrouped + />, + isAsc ? : , + ] } VmSort.propTypes = { diff --git a/src/components/Toolbar/VmsListToolbar.js b/src/components/Toolbar/VmsListToolbar.js index 8491eedff..2fab6fc84 100644 --- a/src/components/Toolbar/VmsListToolbar.js +++ b/src/components/Toolbar/VmsListToolbar.js @@ -1,123 +1,75 @@ -import React, { useContext } from 'react' +import React from 'react' import PropTypes from 'prop-types' -import { List } from 'immutable' import { connect } from 'react-redux' import { saveVmsFilters } from '_/actions' -import { MsgContext } from '_/intl' +import { withMsg } from '_/intl' import { RouterPropTypeShapes } from '_/propTypeShapes' -import { filterVms, mapFilterValues } from '_/utils' +import { filterVms } from '_/utils' -import { Toolbar, Filter } from 'patternfly-react' +import { + Toolbar, + ToolbarItem, + ToolbarContent, +} from '@patternfly/react-core' import { AddVmButton } from '_/components/CreateVmWizard' -import VmFilter from './VmFilters' +import VmFilters from './VmFilters' import VmSort from './VmSort' -import style from './style.css' -const VmsListToolbar = ({ match, vms, onRemoveFilter, onClearFilters }) => { - const { msg } = useContext(MsgContext) - const filters = vms.get('filters').toJS() +import { toJS } from '_/helpers' - const removeFilter = (filter) => { - let filters = vms.get('filters') - const filterValue = filters.get(filter.id) - if (filterValue) { - if (List.isList(filterValue)) { - filters = filters.update(filter.id, (v) => v.delete(v.findIndex(v2 => filter.value === v2))) - if (filters.get(filter.id).size === 0) { - filters = filters.delete(filter.id) - } - } else { - filters = filters.delete(filter.id) - } - onRemoveFilter(filters.toJS()) - } - } +const VmsListToolbar = ({ match, vms, pools, filters = {}, onClearFilters, msg }) => { + const { name, status, os } = filters + const hasFilters = name?.length || status?.length || os?.length - const mapLabels = (item, index) => { - const labels = [] - if (List.isList(item)) { - item.forEach((t, i) => { - labels.push( - - {msg[index]()}: {mapFilterValues[index](t)} - - ) - }) - } else { - labels.push( - - {msg[index]()}: {mapFilterValues[index](item)} - - ) - } - return labels - } - - const total = vms.get('vms').size + vms.get('pools').size - const available = vms.get('filters').size && - vms.get('vms').filter(vm => filterVms(vm, filters, msg)).size + - vms.get('pools').filter(vm => filterVms(vm, filters, msg)).size + const total = vms.size + pools.size + const available = vms.filter(vm => filterVms(vm, filters)).size + + pools.filter(vm => filterVms(vm, filters)).size return ( - - - - - - - -
- { - vms.get('filters').size + <> + + + + + + + +
+ { + hasFilters ? msg.resultsOf({ total, available }) : msg.results({ total }) } -
- { vms.get('filters').size > 0 && ( - <> - {msg.activeFilters()} - - {[].concat(...vms.get('filters').map(mapLabels).toList().toJS())} - - { - e.preventDefault() - onClearFilters() - }} - > - {msg.clearAllFilters()} - - - )} - -
+
+ + + + + +
+ ) } VmsListToolbar.propTypes = { vms: PropTypes.object.isRequired, + pools: PropTypes.object.isRequired, + filters: PropTypes.object.isRequired, match: RouterPropTypeShapes.match.isRequired, - onRemoveFilter: PropTypes.func.isRequired, onClearFilters: PropTypes.func.isRequired, + msg: PropTypes.object.isRequired, } export default connect( - (state) => ({ - vms: state.vms, + ({ vms }) => ({ + vms: vms.get('vms'), + pools: vms.get('pools'), + filters: toJS(vms.get('filters')), }), + (dispatch) => ({ - onRemoveFilter: (filters) => dispatch(saveVmsFilters({ filters })), onClearFilters: () => dispatch(saveVmsFilters({ filters: {} })), }) -)(VmsListToolbar) +)(withMsg(VmsListToolbar)) diff --git a/src/components/Toolbar/index.js b/src/components/Toolbar/index.js index 57c4b0edc..376eb08ff 100644 --- a/src/components/Toolbar/index.js +++ b/src/components/Toolbar/index.js @@ -2,14 +2,16 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { Toolbar } from 'patternfly-react' -import style from './style.css' +import { + Toolbar, + ToolbarContent, + ToolbarGroup, +} from '@patternfly/react-core' import { RouterPropTypeShapes } from '_/propTypeShapes' import VmActions from '../VmActions' import VmConsoleSelector from '../VmConsole/VmConsoleSelector' import VmConsoleInstructionsModal from '../VmConsole/VmConsoleInstructionsModal' import VmsListToolbar from './VmsListToolbar' - import { NATIVE_VNC, SPICE } from '_/constants' const VmDetailToolbar = ({ match, vms }) => { @@ -17,10 +19,12 @@ const VmDetailToolbar = ({ match, vms }) => { const poolId = vms.getIn(['vms', match.params.id, 'pool', 'id']) const pool = vms.getIn(['pools', poolId]) return ( - - - - + + + + + + ) } @@ -40,10 +44,14 @@ const VmDetailToolbarConnected = connect( )(VmDetailToolbar) const VmConsoleToolbar = ({ match: { params: { id, consoleType } } = {}, vms }) => { - if (vms.getIn(['vms', id])) { - return ( -
-
+ if (!vms.getIn(['vms', id])) { + return + } + + return ( + + + -
-
+ +
-
-
- ) - } - return
+ + + + ) } VmConsoleToolbar.propTypes = { diff --git a/src/components/Toolbar/style.css b/src/components/Toolbar/style.css index 9c47286f6..7cf1ddb5d 100644 --- a/src/components/Toolbar/style.css +++ b/src/components/Toolbar/style.css @@ -29,3 +29,8 @@ overflow: auto; max-height: 600px; } + +:global(.pf-c-toolbar.vm-list-toolbar) { + padding-top: 5px; + padding-bottom: 5px; +} diff --git a/src/components/VmActions/Action.js b/src/components/VmActions/Action.js index 5de706455..78b543af9 100644 --- a/src/components/VmActions/Action.js +++ b/src/components/VmActions/Action.js @@ -1,207 +1,28 @@ import React from 'react' import PropTypes from 'prop-types' -import { DropdownButton, MenuItem } from 'patternfly-react' - -import { hrefWithoutHistory } from '_/helpers' - -import style from './style.css' - -class Action extends React.Component { - constructor (props) { - super(props) - this.state = { showModal: false } - this.handleOpen = this.handleOpen.bind(this) - this.handleClose = this.handleClose.bind(this) - } - - handleOpen (e) { - if (e && e.preventDefault) e.preventDefault() - this.setState({ showModal: true }) - this.props.children.props.onClick && this.props.children.props.onClick(e) - } - - handleClose () { - this.setState({ showModal: false }) - } - - render () { - const { children, confirmation } = this.props - - const trigger = confirmation - ? React.cloneElement(children, { onClick: this.handleOpen }) - : children - - const confirmationDialog = confirmation - ? React.cloneElement(confirmation, { show: this.state.showModal, onClose: this.handleClose }) - : null - - return ( - <> - {trigger} - {confirmationDialog} - - ) - } -} -Action.propTypes = { - children: PropTypes.node.isRequired, - confirmation: PropTypes.node, -} - -const Button = ({ - className, - tooltip = '', - shortTitle, - onClick = () => {}, - actionDisabled = false, - id, -}) => { - const handleClick = hrefWithoutHistory(onClick) - - if (actionDisabled) { - return ( - - ) - } +import { + Button, + ButtonVariant, +} from '@patternfly/react-core' +const ActionButtonWraper = ({ id, actionDisabled, shortTitle, onClick, variant = ButtonVariant.control }) => { return ( - - - - {shortTitle} - - - + ) } -Button.propTypes = { - className: PropTypes.string.isRequired, - tooltip: PropTypes.string, +ActionButtonWraper.propTypes = { shortTitle: PropTypes.string.isRequired, onClick: PropTypes.func, actionDisabled: PropTypes.bool, id: PropTypes.string.isRequired, + variant: PropTypes.string, } -const MenuItemAction = ({ - confirmation, - onClick, - shortTitle, - icon, - actionDisabled = false, - id, - className, -}) => { - return ( - - { - onClick && onClick(...args) - document.dispatchEvent(new MouseEvent('click')) - }} - id={id} - className={className} - > - {shortTitle} {icon} - - - ) -} -MenuItemAction.propTypes = { - id: PropTypes.string.isRequired, - confirmation: PropTypes.node, - onClick: PropTypes.func, - shortTitle: PropTypes.string.isRequired, - icon: PropTypes.node, - className: PropTypes.string, - actionDisabled: PropTypes.bool, -} - -const ActionButtonWraper = ({ items, confirmation, actionDisabled, shortTitle, bsStyle, ...rest }) => { - if (items && items.filter(i => i !== null).length > 0) { - return ( - - { - items.filter(i => i !== null && !i.actionDisabled).map( - item => - ) - } - - ) - } - - return ( - -