diff --git a/web/src/containers/FilterToolbar.jsx b/web/src/containers/FilterToolbar.jsx
index dcb7c4c437..23a652cb31 100644
--- a/web/src/containers/FilterToolbar.jsx
+++ b/web/src/containers/FilterToolbar.jsx
@@ -32,6 +32,11 @@ import {
} from '@patternfly/react-core'
import { FilterIcon, SearchIcon } from '@patternfly/react-icons'
+import { FilterSelect } from './filters/Select'
+import { FilterTernarySelect } from './filters/TernarySelect'
+import { FilterCheckbox } from './filters/Checkbox'
+
+
function FilterToolbar(props) {
const [isCategoryDropdownOpen, setIsCategoryDropdownOpen] = useState(false)
const [currentCategory, setCurrentCategory] = useState(
@@ -89,9 +94,16 @@ function FilterToolbar(props) {
// filter to be updated/removed.
let newFilters = {}
if (type) {
- newFilters = {
- ...filters,
- [category.key]: filters[category.key].filter((s) => s !== id),
+ if (category.type === 'ternary') {
+ newFilters = {
+ ...filters,
+ [category.key]: [],
+ }
+ } else {
+ newFilters = {
+ ...filters,
+ [category.key]: filters[category.key].filter((s) => s !== id),
+ }
}
} else {
// Delete the values for each filter category
@@ -122,7 +134,12 @@ function FilterToolbar(props) {
}
isOpen={isCategoryDropdownOpen}
- dropdownItems={filterCategories.map((category) => (
+ dropdownItems={filterCategories.filter(
+ (category) => (category.type === 'search' ||
+ category.type === 'select' ||
+ category.type === 'ternary' ||
+ category.type === 'checkbox')
+ ).map((category) => (
{category.title}
))}
style={{ width: '100%' }}
@@ -131,7 +148,8 @@ function FilterToolbar(props) {
)
}
- function renderFilterInput(category) {
+ function renderFilterInput(category, filters) {
+ const { onFilterChange } = props
if (category.type === 'search') {
return (
@@ -154,6 +172,37 @@ function FilterToolbar(props) {
)
+ } else if (category.type === 'select') {
+ return (
+
+
+
+ )
+ } else if (category.type === 'ternary') {
+ return (
+
+
+
+ )
+ } else if (category.type === 'checkbox') {
+ return (
+
+
+
+
+ )
}
}
@@ -165,12 +214,12 @@ function FilterToolbar(props) {
{filterCategories.map((category) => (
handleDelete(type, id, category)}
categoryName={category.title}
showToolbarItem={currentCategory === category.title}
>
- {renderFilterInput(category)}
+ {renderFilterInput(category, filters)}
))}
>
@@ -203,14 +252,45 @@ FilterToolbar.propTypes = {
filterCategories: PropTypes.array.isRequired,
}
+function getChipsFromFilters(filters, category) {
+ if (category.type === 'ternary') {
+ switch ([...filters[category.key]].pop()) {
+ case 1:
+ case '1':
+ return ['True',]
+ case 0:
+ case '0':
+ return ['False',]
+ default:
+ return []
+ }
+ } else {
+ return filters[category.key]
+ }
+}
+
function getFiltersFromUrl(location, filterCategories) {
const urlParams = new URLSearchParams(location.search)
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
}, {})
diff --git a/web/src/containers/build/Build.jsx b/web/src/containers/build/Build.jsx
index 03b2d27044..c5ebbe08c6 100644
--- a/web/src/containers/build/Build.jsx
+++ b/web/src/containers/build/Build.jsx
@@ -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)'}
+ {build.held &&
+ }
{/* 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
diff --git a/web/src/containers/filters/Checkbox.jsx b/web/src/containers/filters/Checkbox.jsx
new file mode 100644
index 0000000000..6fdd7af269
--- /dev/null
+++ b/web/src/containers/filters/Checkbox.jsx
@@ -0,0 +1,50 @@
+// 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, onFilterChange } = props
+
+ function onChange(checked) {
+ const newFilters = {
+ ...filters,
+ [category.key]: checked ? [1,] : [],
+ }
+ onFilterChange(newFilters)
+ }
+
+ return (
+
+ )
+}
+
+FilterCheckbox.propTypes = {
+ onFilterChange: PropTypes.func.isRequired,
+ filters: PropTypes.object.isRequired,
+ category: PropTypes.object.isRequired,
+}
+
+export { FilterCheckbox }
\ No newline at end of file
diff --git a/web/src/containers/filters/Select.jsx b/web/src/containers/filters/Select.jsx
new file mode 100644
index 0000000000..3e99c8c262
--- /dev/null
+++ b/web/src/containers/filters/Select.jsx
@@ -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 (
+
+ {chipped.map((currentChip, index) => (
+ onSelect(e, currentChip)}
+ >
+ {currentChip}
+
+ ))}
+
+ )
+ }
+
+ function onCreateOption(newValue) {
+ const newOptions = [...options, newValue]
+ setOptions(newOptions)
+ }
+
+ return (
+
+ )
+}
+
+FilterSelect.propTypes = {
+ onFilterChange: PropTypes.func.isRequired,
+ filters: PropTypes.object.isRequired,
+ category: PropTypes.object.isRequired,
+}
+
+export { FilterSelect }
diff --git a/web/src/containers/filters/TernarySelect.jsx b/web/src/containers/filters/TernarySelect.jsx
new file mode 100644
index 0000000000..9905db6dfe
--- /dev/null
+++ b/web/src/containers/filters/TernarySelect.jsx
@@ -0,0 +1,113 @@
+// 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 {
+ Select,
+ SelectOption,
+ SelectVariant,
+} from '@patternfly/react-core'
+
+
+function FilterTernarySelect(props) {
+ /* The ternary select expects the options to be in order: All/True/False */
+ const { filters, category } = props
+
+ const [isOpen, setIsOpen] = useState(false)
+ const options = category.options
+ const _getSelected = (value) => {
+ switch (value) {
+ case 1:
+ case '1':
+ return category.options[1]
+ case 0:
+ case '0':
+ return category.options[2]
+ default:
+ return null
+ }
+ }
+ let _selected = _getSelected([...filters[category.key]].pop())
+ const [selected, setSelected] = useState(_selected)
+
+ function onToggle(isOpen) {
+ setIsOpen(isOpen)
+ }
+
+ function onSelect(event, selection) {
+ const { onFilterChange, filters, category } = props
+
+ let _selection = (selection === selected) ? null : selection
+
+ setSelected(_selection)
+ const setNewFilter = (value) => {
+ switch (value) {
+ case category.options[1]:
+ return [1,]
+ case category.options[2]:
+ return [0,]
+ default:
+ return []
+ }
+ }
+ const newFilters = {
+ ...filters,
+ [category.key]: setNewFilter(_selection),
+ }
+ onFilterChange(newFilters)
+ setIsOpen(false)
+ }
+
+ function onClear() {
+ const { onFilterChange, filters, category } = props
+ setSelected(null)
+ setIsOpen(false)
+ const newFilters = {
+ ...filters,
+ [category.key]: [],
+ }
+ onFilterChange(newFilters)
+ }
+
+ return (
+
+ )
+}
+
+FilterTernarySelect.propTypes = {
+ onFilterChange: PropTypes.func.isRequired,
+ filters: PropTypes.object.isRequired,
+ category: PropTypes.object.isRequired,
+}
+
+export { FilterTernarySelect }
\ No newline at end of file
diff --git a/web/src/pages/Builds.jsx b/web/src/pages/Builds.jsx
index bb4e4cf474..da78241de4 100644
--- a/web/src/pages/Builds.jsx
+++ b/web/src/pages/Builds.jsx
@@ -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,28 @@ class BuildsPage extends React.Component {
placeholder: 'Filter by Build UUID...',
type: 'search',
},
+ {
+ key: 'held',
+ title: 'Held',
+ placeholder: 'Choose Hold Status...',
+ type: 'ternary',
+ options: [
+ 'All',
+ 'Held Builds Only',
+ 'Non Held Builds Only',
+ ]
+ },
+ {
+ key: 'voting',
+ title: 'Voting',
+ placeholder: 'Choose Voting Status...',
+ type: 'ternary',
+ options: [
+ 'All',
+ 'Voting Only',
+ 'Non-Voting Only',
+ ]
+ },
]
this.state = {
diff --git a/web/src/pages/Buildsets.jsx b/web/src/pages/Buildsets.jsx
index a9372e8b01..62978308ec 100644
--- a/web/src/pages/Buildsets.jsx
+++ b/web/src/pages/Buildsets.jsx
@@ -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',