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:
parent
a3790ea280
commit
98ab7899ce
@ -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>
|
||||
))}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user