From 98ab7899cee705577f50672925328f88aceffc23 Mon Sep 17 00:00:00 2001 From: Benjamin Schanzel Date: Thu, 30 Jan 2025 12:43:53 +0100 Subject: [PATCH] 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 --- web/src/containers/FilterToolbar.jsx | 49 +++++++------------ web/src/containers/status/Filters.jsx | 2 +- web/src/pages/Builds.jsx | 21 +++++--- web/src/pages/Buildsets.jsx | 14 ++++-- web/src/pages/ConfigErrors.jsx | 4 ++ web/src/pages/PipelineDetails.jsx | 24 +++++---- web/src/pages/PipelineOverview.jsx | 70 +++++++++++++++------------ 7 files changed, 101 insertions(+), 83 deletions(-) diff --git a/web/src/containers/FilterToolbar.jsx b/web/src/containers/FilterToolbar.jsx index ccdfc3056f..912829b33e 100644 --- a/web/src/containers/FilterToolbar.jsx +++ b/web/src/containers/FilterToolbar.jsx @@ -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} /> - - ) } else if (category.type === 'select') { return ( - + {/* enclosing the FilterSelect with a div because setting the + tooltip ref on the FilterSelect does not work */} +
+ +
) } else if (category.type === 'ternary') { @@ -304,6 +285,10 @@ function FilterToolbar(props) { categoryName={category.title} showToolbarItem={currentCategory === category.title} > + {renderFilterInput(category, filters)} ))} diff --git a/web/src/containers/status/Filters.jsx b/web/src/containers/status/Filters.jsx index 791463f36b..d9e400be41 100644 --- a/web/src/containers/status/Filters.jsx +++ b/web/src/containers/status/Filters.jsx @@ -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 diff --git a/web/src/pages/Builds.jsx b/web/src/pages/Builds.jsx index 59c4d9f885..6f3fd7e2d2 100644 --- a/web/src/pages/Builds.jsx +++ b/web/src/pages/Builds.jsx @@ -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, }, ] diff --git a/web/src/pages/Buildsets.jsx b/web/src/pages/Buildsets.jsx index 19783aa664..0dbe5d77cd 100644 --- a/web/src/pages/Buildsets.jsx +++ b/web/src/pages/Buildsets.jsx @@ -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, }, ] diff --git a/web/src/pages/ConfigErrors.jsx b/web/src/pages/ConfigErrors.jsx index 308908cc0e..5e868f2664 100644 --- a/web/src/pages/ConfigErrors.jsx +++ b/web/src/pages/ConfigErrors.jsx @@ -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, }, ] diff --git a/web/src/pages/PipelineDetails.jsx b/web/src/pages/PipelineDetails.jsx index e2ccc42850..4ab5a86d64 100644 --- a/web/src/pages/PipelineDetails.jsx +++ b/web/src/pages/PipelineDetails.jsx @@ -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({ { 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) } diff --git a/web/src/pages/PipelineOverview.jsx b/web/src/pages/PipelineOverview.jsx index d5a55be07b..77b7216959 100644 --- a/web/src/pages/PipelineOverview.jsx +++ b/web/src/pages/PipelineOverview.jsx @@ -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)