Web UI: add checkboxes and selects to filter toolbar
Support two new filter types: checkbox and select. Checkboxes add support for "boolean-type" filter criteria when using a filter toolbar. Note that while checking the box will set the filter argument to True (1), unchecking it will remove it from the query rather than setting it to False (0). Such a value can however be set by hand in the UI URL. Use this feature to allow filtering builds by held status (i.e. show builds that triggered a autohold only) or by voting status (i.e. show only voting jobs). Selects let a user choose among predefined values. Use a select to help a user choose a result value when searching builds and buildsets. Build page: add a thumbtack icon next to a build's status badge if that build triggered an autohold. Change-Id: I40b06c83ed069e0756c7f8b00430d36a36230bfa
This commit is contained in:
parent
a2b8b975d0
commit
3795b2c67a
|
@ -32,6 +32,9 @@ import {
|
|||
} from '@patternfly/react-core'
|
||||
import { FilterIcon, SearchIcon } from '@patternfly/react-icons'
|
||||
|
||||
import { FilterSelect } from './filters/Select'
|
||||
import { FilterCheckbox } from './filters/Checkbox'
|
||||
|
||||
function FilterToolbar(props) {
|
||||
const [isCategoryDropdownOpen, setIsCategoryDropdownOpen] = useState(false)
|
||||
const [currentCategory, setCurrentCategory] = useState(
|
||||
|
@ -122,7 +125,11 @@ function FilterToolbar(props) {
|
|||
</DropdownToggle>
|
||||
}
|
||||
isOpen={isCategoryDropdownOpen}
|
||||
dropdownItems={filterCategories.map((category) => (
|
||||
dropdownItems={filterCategories.filter(
|
||||
(category) => (category.type === 'search' ||
|
||||
category.type === 'select' ||
|
||||
category.type === 'checkbox')
|
||||
).map((category) => (
|
||||
<DropdownItem key={category.key}>{category.title}</DropdownItem>
|
||||
))}
|
||||
style={{ width: '100%' }}
|
||||
|
@ -131,7 +138,8 @@ function FilterToolbar(props) {
|
|||
)
|
||||
}
|
||||
|
||||
function renderFilterInput(category) {
|
||||
function renderFilterInput(category, filters) {
|
||||
const { onFilterChange } = props
|
||||
if (category.type === 'search') {
|
||||
return (
|
||||
<InputGroup>
|
||||
|
@ -154,6 +162,27 @@ function FilterToolbar(props) {
|
|||
</Button>
|
||||
</InputGroup>
|
||||
)
|
||||
} else if (category.type === 'select') {
|
||||
return (
|
||||
<InputGroup>
|
||||
<FilterSelect
|
||||
onFilterChange={onFilterChange}
|
||||
filters={filters}
|
||||
category={category}
|
||||
/>
|
||||
</InputGroup>
|
||||
)
|
||||
} else if (category.type === 'checkbox') {
|
||||
return (
|
||||
<InputGroup>
|
||||
<br />
|
||||
<FilterCheckbox
|
||||
onFilterChange={onFilterChange}
|
||||
filters={filters}
|
||||
category={category}
|
||||
/>
|
||||
</InputGroup>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,7 +199,7 @@ function FilterToolbar(props) {
|
|||
categoryName={category.title}
|
||||
showToolbarItem={currentCategory === category.title}
|
||||
>
|
||||
{renderFilterInput(category)}
|
||||
{renderFilterInput(category, filters)}
|
||||
</ToolbarFilter>
|
||||
))}
|
||||
</>
|
||||
|
@ -208,9 +237,23 @@ function getFiltersFromUrl(location, filterCategories) {
|
|||
const filters = filterCategories.reduce((filterDict, item) => {
|
||||
// Initialize each filter category with an empty list
|
||||
filterDict[item.key] = []
|
||||
|
||||
// And update the list with each matching element from the URL query
|
||||
urlParams.getAll(item.key).forEach((param) => {
|
||||
filterDict[item.key].push(param)
|
||||
if (item.type === 'checkbox') {
|
||||
switch (param) {
|
||||
case '1':
|
||||
filterDict[item.key].push(1)
|
||||
break
|
||||
case '0':
|
||||
filterDict[item.key].push(0)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
filterDict[item.key].push(param)
|
||||
}
|
||||
})
|
||||
return filterDict
|
||||
}, {})
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
OutlinedCalendarAltIcon,
|
||||
OutlinedClockIcon,
|
||||
StreamIcon,
|
||||
ThumbtackIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
import * as moment from 'moment'
|
||||
import 'moment-duration-format'
|
||||
|
@ -57,6 +58,12 @@ function Build({ build, tenant, timezone }) {
|
|||
{build.job_name} {!build.voting && ' (non-voting)'}
|
||||
</BuildResultWithIcon>
|
||||
<BuildResultBadge result={build.result} />
|
||||
{build.held &&
|
||||
<ThumbtackIcon title="This build triggered an autohold"
|
||||
style={{
|
||||
marginLeft: 'var(--pf-global--spacer--sm)',
|
||||
}}
|
||||
/>}
|
||||
</Title>
|
||||
{/* We handle the spacing for the body and the flex items by ourselves
|
||||
so they go hand in hand. By default, the flex items' spacing only
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2021 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
} from '@patternfly/react-core'
|
||||
|
||||
|
||||
function FilterCheckbox(props) {
|
||||
const { filters, category } = props
|
||||
|
||||
function onChange(checked) {
|
||||
const { onFilterChange } = props
|
||||
const newFilters = {
|
||||
...filters,
|
||||
[category.key]: checked ? [1,] : [],
|
||||
}
|
||||
onFilterChange(newFilters)
|
||||
}
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
label={category.placeholder}
|
||||
isChecked={[...filters[category.key]].pop() ? true : false}
|
||||
onChange={onChange}
|
||||
aria-label={category.placeholder}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
FilterCheckbox.propTypes = {
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
filters: PropTypes.object.isRequired,
|
||||
category: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export { FilterCheckbox }
|
|
@ -0,0 +1,122 @@
|
|||
// Copyright 2021 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {
|
||||
Chip,
|
||||
ChipGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core'
|
||||
|
||||
|
||||
function FilterSelect(props) {
|
||||
const { filters, category } = props
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [selected, setSelected] = useState(filters[category.key])
|
||||
const [options, setOptions] = useState(category.options)
|
||||
|
||||
function onToggle(isOpen) {
|
||||
setSelected(filters[category.key])
|
||||
setIsOpen(isOpen)
|
||||
}
|
||||
|
||||
function onSelect(event, selection) {
|
||||
const { onFilterChange, filters, category } = props
|
||||
|
||||
const newSelected = selected.includes(selection)
|
||||
? selected.filter(item => item !== selection)
|
||||
: [...selected, selection]
|
||||
|
||||
setSelected(newSelected)
|
||||
const newFilters = {
|
||||
...filters,
|
||||
[category.key]: newSelected,
|
||||
}
|
||||
onFilterChange(newFilters)
|
||||
}
|
||||
|
||||
function onClear() {
|
||||
const { onFilterChange, filters, category } = props
|
||||
setSelected([])
|
||||
setIsOpen(false)
|
||||
const newFilters = {
|
||||
...filters,
|
||||
[category.key]: [],
|
||||
}
|
||||
onFilterChange(newFilters)
|
||||
}
|
||||
|
||||
function chipGroupComponent() {
|
||||
const { filters, category } = props
|
||||
const chipped = filters[category.key]
|
||||
return (
|
||||
<ChipGroup>
|
||||
{chipped.map((currentChip, index) => (
|
||||
<Chip
|
||||
isReadOnly={index === 0 ? true : false}
|
||||
key={currentChip}
|
||||
onClick={(e) => onSelect(e, currentChip)}
|
||||
>
|
||||
{currentChip}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
)
|
||||
}
|
||||
|
||||
function onCreateOption(newValue) {
|
||||
const newOptions = [ ...options, newValue ]
|
||||
setOptions(newOptions)
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
chipGroupProps={{ numChips: 1, expandedText: 'Hide' }}
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
typeAheadAriaLabel={category.title}
|
||||
onToggle={onToggle}
|
||||
onClear={onClear}
|
||||
onSelect={onSelect}
|
||||
selections={filters[category.key]}
|
||||
isOpen={isOpen}
|
||||
isCreatable="true"
|
||||
onCreateOption={onCreateOption}
|
||||
placeholderText={category.placeholder}
|
||||
chipGroupComponent={chipGroupComponent}
|
||||
maxHeight={300}
|
||||
>
|
||||
{
|
||||
options.map((option, index) => (
|
||||
<SelectOption
|
||||
key={index}
|
||||
value={option}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Select >
|
||||
)
|
||||
}
|
||||
|
||||
FilterSelect.propTypes = {
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
filters: PropTypes.object.isRequired,
|
||||
category: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export { FilterSelect }
|
|
@ -68,12 +68,32 @@ class BuildsPage extends React.Component {
|
|||
placeholder: 'Filter by Change...',
|
||||
type: 'search',
|
||||
},
|
||||
// TODO (felix): We could change the result filter to a dropdown later on
|
||||
{
|
||||
key: 'result',
|
||||
title: 'Result',
|
||||
placeholder: 'Filter by Result...',
|
||||
type: 'search',
|
||||
placeholder: 'Any result',
|
||||
type: 'select',
|
||||
// TODO there should be a single source of truth for this
|
||||
options: [
|
||||
'SUCCESS',
|
||||
'FAILURE',
|
||||
'RETRY_LIMIT',
|
||||
'POST_FAILURE',
|
||||
'SKIPPED',
|
||||
'NODE_FAILURE',
|
||||
'MERGER_FAILURE',
|
||||
'CONFIG_ERROR',
|
||||
'TIMED_OUT',
|
||||
'CANCELED',
|
||||
'ERROR',
|
||||
'RETRY',
|
||||
'DISK_FULL',
|
||||
'NO_JOBS',
|
||||
'DISCONNECT',
|
||||
'ABORTED',
|
||||
'LOST',
|
||||
'EXCEPTION',
|
||||
'NO_HANDLE'],
|
||||
},
|
||||
{
|
||||
key: 'uuid',
|
||||
|
@ -81,6 +101,18 @@ class BuildsPage extends React.Component {
|
|||
placeholder: 'Filter by Build UUID...',
|
||||
type: 'search',
|
||||
},
|
||||
{
|
||||
key: 'held',
|
||||
title: 'Hold Status',
|
||||
placeholder: 'Only show builds that triggered an autohold',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
key: 'voting',
|
||||
title: 'Voting',
|
||||
placeholder: 'Only show builds for voting jobs',
|
||||
type: 'checkbox',
|
||||
},
|
||||
]
|
||||
|
||||
this.state = {
|
||||
|
|
|
@ -60,12 +60,17 @@ class BuildsetsPage extends React.Component {
|
|||
placeholder: 'Filter by Change...',
|
||||
type: 'search',
|
||||
},
|
||||
// TODO (felix): We could change the result filter to a dropdown later on
|
||||
{
|
||||
key: 'result',
|
||||
title: 'Result',
|
||||
placeholder: 'Filter by Result...',
|
||||
type: 'search',
|
||||
type: 'select',
|
||||
// are there more?
|
||||
options: [
|
||||
'SUCCESS',
|
||||
'FAILURE',
|
||||
'MERGER_FAILURE',
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'uuid',
|
||||
|
|
Loading…
Reference in New Issue