web: Use a select filter for pipelines and queues on status page

On the PipelineOverview and PipelineDetails page, we know the set of
valid pipeline and queue names. With this change we suggest those to the
user when filtering in the form of the type-ahead select filter we
already use in other places. This should make it clearer to users that
they can filter for multiple values by selecting more options from the
filter suggestions.

To retain the "wildcard filtering" feature, this change removes the
separate "fuzzy-search" filter type and instead adds a new flag for
this, so we can enable this for other filter types, too.

Change-Id: I395d2b51441c02ffcf8a07770e42791cbaf62f0e
This commit is contained in:
Benjamin Schanzel 2025-01-30 12:43:53 +01:00
parent a3790ea280
commit 98ab7899ce
No known key found for this signature in database
7 changed files with 101 additions and 83 deletions

View File

@ -12,7 +12,7 @@
// License for the specific language governing permissions and limitations
// under the License.
import React, { useState } from 'react'
import React, { useState, useRef } from 'react'
import { useDispatch } from 'react-redux'
import PropTypes from 'prop-types'
import {
@ -90,6 +90,8 @@ function FilterToolbar(props) {
)
const [inputValue, setInputValue] = useState('')
const fuzzySearchTooltipRef = useRef()
function handleCategoryToggle(isOpen) {
setIsCategoryDropdownOpen(isOpen)
}
@ -195,7 +197,6 @@ function FilterToolbar(props) {
isOpen={isCategoryDropdownOpen}
dropdownItems={filterCategories.filter(
(category) => (category.type === 'search' ||
category.type === 'fuzzy-search' ||
category.type === 'select' ||
category.type === 'ternary' ||
category.type === 'checkbox')
@ -222,6 +223,7 @@ function FilterToolbar(props) {
value={inputValue}
placeholder={category.placeholder}
onKeyDown={(event) => handleInputSend(event, category)}
ref={category.fuzzy ? fuzzySearchTooltipRef : null}
/>
<Button
variant={ButtonVariant.control}
@ -232,39 +234,18 @@ function FilterToolbar(props) {
</Button>
</InputGroup>
)
} else if (category.type === 'fuzzy-search') {
return (
<InputGroup>
<Tooltip
content="Wildcard search with * placeholders">
<TextInput
name={`${category.key}-input`}
id={`${category.key}-input`}
type="search"
aria-label={`${category.key} filter`}
onChange={handleInputChange}
value={inputValue}
placeholder={category.placeholder}
onKeyDown={(event) => handleInputSend(event, category)}
/>
</Tooltip>
<Button
variant={ButtonVariant.control}
aria-label="search button for search input"
onClick={(event) => handleInputSend(event, category)}
>
<SearchIcon />
</Button>
</InputGroup>
)
} else if (category.type === 'select') {
return (
<InputGroup>
<FilterSelect
onFilterChange={onFilterChange}
filters={filters}
category={category}
/>
{/* enclosing the FilterSelect with a div because setting the
tooltip ref on the FilterSelect does not work */}
<div ref={category.fuzzy ? fuzzySearchTooltipRef : null}>
<FilterSelect
onFilterChange={onFilterChange}
filters={filters}
category={category}
/>
</div>
</InputGroup>
)
} else if (category.type === 'ternary') {
@ -304,6 +285,10 @@ function FilterToolbar(props) {
categoryName={category.title}
showToolbarItem={currentCategory === category.title}
>
<Tooltip
reference={fuzzySearchTooltipRef}
content="Wildcard search with * placeholders"
/>
{renderFilterInput(category, filters)}
</ToolbarFilter>
))}

View File

@ -142,7 +142,7 @@ function filterPipelines(pipelines, filters, filterCategories, truncateEmpty) {
// by going over the valid FILTER_CATEGORIES
for (const category of filterCategories) {
const key = category['key']
const fuzzy = category['type'] === 'fuzzy-search'
const fuzzy = category['fuzzy']
const filter = filters[key]
if (filter.length === 0) {
continue

View File

@ -43,31 +43,36 @@ class BuildsPage extends React.Component {
key: 'job_name',
title: 'Job',
placeholder: 'Filter by Job...',
type: 'fuzzy-search',
type: 'search',
fuzzy: true,
},
{
key: 'project',
title: 'Project',
placeholder: 'Filter by Project...',
type: 'fuzzy-search',
type: 'search',
fuzzy: true,
},
{
key: 'branch',
title: 'Branch',
placeholder: 'Filter by Branch...',
type: 'fuzzy-search',
type: 'search',
fuzzy: true,
},
{
key: 'pipeline',
title: 'Pipeline',
placeholder: 'Filter by Pipeline...',
type: 'fuzzy-search',
type: 'search',
fuzzy: true,
},
{
key: 'change',
title: 'Change',
placeholder: 'Filter by Change...',
type: 'search',
fuzzy: false,
},
{
key: 'result',
@ -96,12 +101,14 @@ class BuildsPage extends React.Component {
'LOST',
'EXCEPTION',
'NO_HANDLE'],
fuzzy: false,
},
{
key: 'uuid',
title: 'Build',
placeholder: 'Filter by Build UUID...',
type: 'search',
fuzzy: false,
},
{
key: 'held',
@ -112,7 +119,8 @@ class BuildsPage extends React.Component {
'All',
'Held Builds Only',
'Non Held Builds Only',
]
],
fuzzy: false,
},
{
key: 'voting',
@ -123,7 +131,8 @@ class BuildsPage extends React.Component {
'All',
'Voting Only',
'Non-Voting Only',
]
],
fuzzy: false,
},
]

View File

@ -42,25 +42,29 @@ class BuildsetsPage extends React.Component {
key: 'project',
title: 'Project',
placeholder: 'Filter by Project...',
type: 'fuzzy-search',
type: 'search',
fuzzy: true,
},
{
key: 'branch',
title: 'Branch',
placeholder: 'Filter by Branch...',
type: 'fuzzy-search',
type: 'search',
fuzzy: true,
},
{
key: 'pipeline',
title: 'Pipeline',
placeholder: 'Filter by Pipeline...',
type: 'fuzzy-search',
type: 'search',
fuzzy: true,
},
{
key: 'change',
title: 'Change',
placeholder: 'Filter by Change...',
type: 'search',
fuzzy: false,
},
{
key: 'result',
@ -75,13 +79,15 @@ class BuildsetsPage extends React.Component {
'DEQUEUED',
'CONFIG_ERROR',
'NO_JOBS',
]
],
fuzzy: false,
},
{
key: 'uuid',
title: 'Buildset',
placeholder: 'Filter by Buildset UUID...',
type: 'search',
fuzzy: false,
},
]

View File

@ -54,24 +54,28 @@ class ConfigErrorsPage extends React.Component {
title: 'Project',
placeholder: 'Filter by project...',
type: 'search',
fuzzy: false,
},
{
key: 'branch',
title: 'Branch',
placeholder: 'Filter by branch...',
type: 'search',
fuzzy: false,
},
{
key: 'severity',
title: 'Severity',
placeholder: 'Filter by severity...',
type: 'search',
fuzzy: false,
},
{
key: 'name',
title: 'Name',
placeholder: 'Filter by name...',
type: 'search',
fuzzy: false,
},
]

View File

@ -63,24 +63,30 @@ import {
clearFilters,
} from '../containers/status/Filters'
const filterCategories = [
const filterCategories = (pipeline) => [
{
key: 'project',
title: 'Project',
placeholder: 'Filter by Project...',
type: 'fuzzy-search',
type: 'search',
fuzzy: true,
},
{
key: 'change',
title: 'Change',
placeholder: 'Filter by Change...',
type: 'fuzzy-search',
type: 'search',
fuzzy: true,
},
{
key: 'queue',
title: 'Queue',
placeholder: 'Filter by Queue...',
type: 'fuzzy-search',
type: 'select',
// the last filter part makes sure we only provide options for queues
// which have a non-empty name
options: pipeline ? pipeline.change_queues.flat().map(q => q.name).filter(n => n): [],
fuzzy: true,
}
]
@ -141,7 +147,7 @@ function PipelineDetailsPage({
const location = useLocation()
const history = useHistory()
const filters = getFiltersFromUrl(location, filterCategories)
const filters = getFiltersFromUrl(location, filterCategories(pipeline))
const dispatch = useDispatch()
const updateData = useCallback((tenant) => {
@ -199,7 +205,7 @@ function PipelineDetailsPage({
<Level>
<LevelItem>
<FilterToolbar
filterCategories={filterCategories}
filterCategories={filterCategories(pipeline)}
onFilterChange={(newFilters) => { handleFilterChange(newFilters, filterCategories, location, history) }}
filters={filters}
filterInputValidation={filterInputValidation}
@ -280,7 +286,7 @@ function PipelineDetailsPage({
title="No items found"
icon={StreamIcon}
action="Clear all filters"
onAction={() => clearFilters(location, history, filterCategories)}
onAction={() => clearFilters(location, history, filterCategories(pipeline))}
>
No items match this filter criteria. Remove some filters or
clear all to show results.
@ -305,13 +311,13 @@ PipelineDetailsPage.propTypes = {
function mapStateToProps(state, ownProps) {
let pipeline = null
if (state.status.status) {
const filters = getFiltersFromUrl(ownProps.location, filterCategories)
const filters = getFiltersFromUrl(ownProps.location, filterCategories(null))
// we need to work on a copy of the state..pipelines, because when mutating
// the original, we couldn't reset or change the filters without reloading
// from the backend first.
const pipelines = global.structuredClone(state.status.status.pipelines)
// Filter the state for this specific pipeline
pipeline = filterPipelines(pipelines, filters, filterCategories, false)
pipeline = filterPipelines(pipelines, filters, filterCategories(null), false)
.find((p) => p.name === ownProps.match.params.pipelineName) || null
pipeline = countPipelineItems(pipeline)
}

View File

@ -57,34 +57,6 @@ import { EmptyBox } from '../containers/Errors'
import { countPipelineItems } from '../containers/status/Misc'
import { useDocumentVisibility, useInterval } from '../Hooks'
const filterCategories = [
{
key: 'change',
title: 'Change',
placeholder: 'Filter by Change...',
type: 'fuzzy-search',
},
{
key: 'project',
title: 'Project',
placeholder: 'Filter by Project...',
type: 'fuzzy-search',
},
{
key: 'queue',
title: 'Queue',
placeholder: 'Filter by Queue...',
type: 'fuzzy-search',
},
{
key: 'pipeline',
title: 'Pipeline',
placeholder: 'Filter by Pipeline...',
type: 'fuzzy-search',
},
]
function PipelineGallery({ pipelines, tenant, showAllPipelines, expandAll, isLoading, filters, onClearFilters, sortKey }) {
// Filter out empty pipelines if necessary
if (!showAllPipelines) {
@ -142,7 +114,7 @@ PipelineGallery.propTypes = {
sortKey: PropTypes.string,
}
function getPipelines(status, location) {
function getPipelines(status, location, filterCategories) {
let pipelines = []
let stats = {}
if (status) {
@ -169,6 +141,43 @@ function getPipelines(status, location) {
}
function PipelineOverviewPage() {
const status = useSelector((state) => state.status.status)
const filterCategories = [
{
key: 'change',
title: 'Change',
placeholder: 'Filter by Change...',
type: 'search',
fuzzy: true,
},
{
key: 'project',
title: 'Project',
placeholder: 'Filter by Project...',
type: 'search',
fuzzy: true,
},
{
key: 'queue',
title: 'Queue',
placeholder: 'Filter by Queue...',
type: 'select',
// the last filter part makes sure we only provide options for queues
// which have a non-empty name
options: status ? status.pipelines.map(p => p.change_queues).flat().map(q => q.name).filter(n => n) : [],
fuzzy: true,
},
{
key: 'pipeline',
title: 'Pipeline',
placeholder: 'Filter by Pipeline...',
type: 'select',
options: status ? status.pipelines.map(p => p.name) : [],
fuzzy: true,
},
]
const location = useLocation()
const history = useHistory()
const filters = getFiltersFromUrl(location, filterCategories)
@ -182,8 +191,7 @@ function PipelineOverviewPage() {
const isDocumentVisible = useDocumentVisibility()
const status = useSelector((state) => state.status.status)
const { pipelines, stats } = useMemo(() => getPipelines(status, location), [status, location])
const { pipelines, stats } = useMemo(() => getPipelines(status, location, filterCategories), [status, location, filterCategories])
const isFetching = useSelector((state) => state.status.isFetching)
const tenant = useSelector((state) => state.tenant)