Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table view for Virtual Machines screen #1600

Merged
merged 6 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/actions/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export function saveGlobalOptions ({
fullScreenSpice,
ctrlAltEndSpice,
smartcardSpice,
viewForVirtualMachines,
} = {},
}: Object, { transactionId }: Object): SaveGlobalOptionsActionType {
return {
Expand All @@ -107,6 +108,7 @@ export function saveGlobalOptions ({
fullScreenSpice,
ctrlAltEndSpice,
smartcardSpice,
viewForVirtualMachines,
},
meta: {
transactionId,
Expand All @@ -124,6 +126,17 @@ export function saveSSHKey ({ key, userId, sshId }: Object): Object {
},
}
}

export function saveRemoteOptionSilently ({ name, value }: Object): Object {
return {
type: C.SAVE_USER_OPTION_SILENTLY,
payload: {
name,
value,
},
}
}

export function deleteUserOption ({ optionId, userId }: Object): Object {
return {
type: C.DELETE_USER_OPTION,
Expand Down
3 changes: 2 additions & 1 deletion src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export type SaveGlobalOptionsActionType = {
ctrlAltEndVnc?: boolean,
fullScreenSpice?: boolean,
ctrlAltEndSpice?: boolean,
smartcardSpice?: boolean
smartcardSpice?: boolean,
viewForVirtualMachines?: string
|},
meta: {|
transactionId: string
Expand Down
78 changes: 78 additions & 0 deletions src/components/Toolbar/DatePickerFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { withMsg } from '_/intl'
import {
DatePicker,
InputGroup,
ToolbarFilter,
} from '@patternfly/react-core'

import moment from 'moment'

const DatePickerFilter = ({
filterId,
selectedFilters,
showToolbarItem,
title,
onFilterUpdate,
msg,
}) => {
const dateFormat = moment.localeData().longDateFormat('L')
const formatDate = date => moment(date).format(dateFormat)
const parseDate = str => moment(str, dateFormat).toDate()
const isValidDate = date => moment(date, dateFormat).isValid()
const toISO = str => moment(str, dateFormat).format('YYYY-MM-DD')
const fromISO = str => moment(str, 'YYYY-MM-DD').format(dateFormat)

const [date, setDate] = useState(toISO(formatDate(Date.now())))

const clearSingleDate = (option) => {
console.warn('clearSingle ', option)
const fixed = toISO(option)
onFilterUpdate([...selectedFilters?.filter(d => d !== fixed)])
}

const onDateChange = (inputDate, newDate) => {
if (isValidDate(inputDate)) {
const fixed = toISO(inputDate)
setDate(fixed)
onFilterUpdate([...selectedFilters?.filter(d => d !== fixed), fixed])
}
}

return (
<ToolbarFilter
key={filterId}
chips={selectedFilters?.map(fromISO)}
deleteChip={(category, option) => clearSingleDate(option)}
deleteChipGroup={() => onFilterUpdate([])}
categoryName={title}
showToolbarItem={showToolbarItem}
>
<InputGroup>
<DatePicker
value={fromISO(date)}
dateFormat={formatDate }
dateParse={parseDate}
onChange={onDateChange}
aria-label={msg.date()}
buttonAriaLabel={msg.toggleDatePicker()}
placeholder={dateFormat}
invalidFormatText={msg.invalidDateFormat({ format: dateFormat })}
/>
</InputGroup>
</ToolbarFilter>
)
}

DatePickerFilter.propTypes = {
filterId: PropTypes.string.isRequired,
selectedFilters: PropTypes.array.isRequired,
showToolbarItem: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
onFilterUpdate: PropTypes.func.isRequired,

msg: PropTypes.object.isRequired,
}

export default withMsg(DatePickerFilter)
142 changes: 142 additions & 0 deletions src/components/Toolbar/Filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { withMsg } from '_/intl'
import {
Button,
ButtonVariant,
Dropdown,
DropdownItem,
DropdownPosition,
DropdownToggle,
InputGroup,
TextInput,
ToolbarGroup,
ToolbarFilter,
ToolbarItem,
ToolbarToggleGroup,
Tooltip,
} from '@patternfly/react-core'

import { FilterIcon, SearchIcon } from '@patternfly/react-icons/dist/esm/icons'

import DatePickerFilter from './DatePickerFilter'
import SelectFilter from './SelectFilter'

const Filters = ({ msg, locale, selectedFilters, onFilterUpdate, filterTypes, textBasedFilterId }) => {
const [currentFilterType, setCurrentFilterType] = useState(filterTypes[0])
const [expanded, setExpanded] = useState(false)
const [inputValue, setInputValue] = useState('')

const nameFilter = filterTypes.find(({ id }) => id === textBasedFilterId)
const labelToFilter = (label) => filterTypes.find(({ title }) => title === label) ?? currentFilterType

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?.[textBasedFilterId]?.includes(inputValue)) {
return
}
onFilterUpdate({ ...selectedFilters, [textBasedFilterId]: [...(selectedFilters?.[textBasedFilterId] ?? []), inputValue] })
setInputValue('')
}

return (
<ToolbarToggleGroup toggleIcon={<Tooltip content={msg.filter()}><FilterIcon /></Tooltip>} breakpoint="xl">
<ToolbarGroup variant="filter-group">
<ToolbarItem>
<Dropdown
onSelect={onFilterTypeSelect}
position={DropdownPosition.left}
toggle={(
<DropdownToggle onToggle={onFilterTypeToggle} style={{ width: '100%' }}>
<FilterIcon /> {currentFilterType.title}
</DropdownToggle>
)}
isOpen={expanded}
style={{ width: '100%' }}
dropdownItems={
filterTypes.map(({ id, title }) =>
<DropdownItem key={id}>{title}</DropdownItem>)
}
/>
</ToolbarItem>
<ToolbarFilter
key={textBasedFilterId}
chips={selectedFilters?.[textBasedFilterId] ?? [] }
deleteChip={(category, option) => onFilterUpdate({
...selectedFilters,
[textBasedFilterId]: selectedFilters?.[textBasedFilterId]?.filter?.(value => value !== option) ?? [],
})}
deleteChipGroup={() => onFilterUpdate({ ...selectedFilters, [textBasedFilterId]: [] })}
categoryName={nameFilter.title}
showToolbarItem={currentFilterType.id === textBasedFilterId}
>
<InputGroup>
<TextInput
id={textBasedFilterId}
type="search"
onChange={setInputValue}
value={inputValue}
placeholder={nameFilter.placeholder}
onKeyDown={onNameInput}
/>
<Button
variant={ButtonVariant.control}
aria-label={msg.vmFilterTypePlaceholderName()}
onClick={onNameInput}
>
<SearchIcon />
</Button>
</InputGroup>
</ToolbarFilter>
{
filterTypes.filter(({ datePicker }) => datePicker).map(({ id: filterId, title }) => (
<DatePickerFilter
title={title}
key={filterId}
selectedFilters={selectedFilters?.[filterId] ?? []}
filterId={filterId}
showToolbarItem={currentFilterType.id === filterId}
onFilterUpdate={(filtersToSave) => {
console.warn('filtersToSave', filtersToSave)
onFilterUpdate({ ...selectedFilters, [filterId]: filtersToSave })
}
}
/>
))
}
{filterTypes.filter(({ filterValues }) => !!filterValues?.length)
?.map(({ id, filterValues, placeholder, title }) => (
<SelectFilter
title={title}
key={id}
filterColumnId={id}
showToolbarItem={currentFilterType.id === id}
filterIds={selectedFilters?.[id] ?? []}
allSupportedFilters={filterValues}
setFilters={(filtersToSave) => onFilterUpdate({ ...selectedFilters, [id]: filtersToSave })}
title={title}
placeholderText={placeholder}
/>
)
)}
</ToolbarGroup>
</ToolbarToggleGroup>
)
}

Filters.propTypes = {
selectedFilters: PropTypes.object.isRequired,
filterTypes: PropTypes.array.isRequired,
textBasedFilterId: PropTypes.string.isRequired,
onFilterUpdate: PropTypes.func.isRequired,
msg: PropTypes.object.isRequired,
locale: PropTypes.string.isRequired,
}

export default withMsg(Filters)
84 changes: 84 additions & 0 deletions src/components/Toolbar/SelectFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import {
Select,
SelectOption,
SelectVariant,
ToolbarFilter,
} from '@patternfly/react-core'

const SelectFilter = ({ filterIds = [], setFilters, allSupportedFilters = [], title, placeholderText, filterColumnId, showToolbarItem }) => {
const [isExpanded, setExpanded] = useState(false)

// 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 }) =>
<SelectOption key={ title} value={title}/>

// 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
}
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]))
}

const addFilter = (title) => {
const ids = labelToIds(title)
// add all filter IDs linked
setFilters([...filterIds, ...Object.keys(ids)])
}

const hasFilter = (title) => {
const ids = labelToIds(title)
return filterIds.some(id => ids[id])
}
return (
<ToolbarFilter
key={filterColumnId}
chips={selectedFilters.map(toChip)}
deleteChip={(category, option) => deleteFilter(option)}
deleteChipGroup={() => setFilters([])}
categoryName={title}
showToolbarItem={showToolbarItem}
>
<Select
variant={SelectVariant.checkbox}
aria-label={placeholderText}
onSelect={(e, option, isPlaceholder) => {
if (isPlaceholder) {
return
}
hasFilter(option)
? deleteFilter(option)
: addFilter(option)
} }
selections={selectedFilters.map(toOption)}
placeholderText={placeholderText}
isOpen={isExpanded}
onToggle={setExpanded}
>
{allSupportedFilters.map(toOptionNode)}
</Select>
</ToolbarFilter>
)
}

SelectFilter.propTypes = {
filterIds: PropTypes.array.isRequired,
allSupportedFilters: PropTypes.array.isRequired,
setFilters: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
placeholderText: PropTypes.string.isRequired,
filterColumnId: PropTypes.string.isRequired,
showToolbarItem: PropTypes.bool.isRequired,
}

export default SelectFilter
Loading