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

This commit is contained in:
Zuul 2025-04-16 12:06:50 +00:00 committed by Gerrit Code Review
commit cbdcbdca0a
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)