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',