web: Add an option to sort pipelines
This adds a dropdown menu giving users an option to sort pipelines on the PipelineOverview page, either by the initial implicit sorting, alphabetically by name, or by the number of items in the pipelines. This is to give users more control over how pipelines are displayed, especially in large tenants with many pipelines. Change-Id: I5475a1de77851351b2e776e78cffef5815d0c2f2
This commit is contained in:
@@ -390,9 +390,18 @@ function getFiltersFromUrl(location, filterCategories) {
|
||||
return filters
|
||||
}
|
||||
|
||||
function writeFiltersToUrl(filters, location, history) {
|
||||
function writeFiltersToUrl(filters, filterCategories, location, history) {
|
||||
// Build new URL parameters from the filters in state
|
||||
const searchParams = new URLSearchParams('')
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
|
||||
// first clear existing searchParams contained in the current valid
|
||||
// filterCategories or "skip"/"limit". This is to make sure we don't remove
|
||||
// other unrelated searchParams
|
||||
const keys = filterCategories.map(c => c.key).concat(['skip', 'limit'])
|
||||
for (const key of keys) {
|
||||
searchParams.delete(key)
|
||||
}
|
||||
|
||||
Object.keys(filters).map((key) => {
|
||||
filters[key].forEach((value) => {
|
||||
searchParams.append(key, value)
|
||||
|
||||
75
web/src/containers/SortDropdown.jsx
Normal file
75
web/src/containers/SortDropdown.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright 2025 BMW Group
|
||||
//
|
||||
// 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 { Dropdown, DropdownItem, DropdownPosition, DropdownToggle, Tooltip } from '@patternfly/react-core'
|
||||
import { SortAmountDownIcon } from '@patternfly/react-icons'
|
||||
|
||||
function getSearchKeyFromUrl(location, sortKeys) {
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
const searchKey = searchParams.get('sort')
|
||||
if (!searchKey) {
|
||||
return null
|
||||
}
|
||||
return sortKeys.find(k => k.key === searchKey)
|
||||
}
|
||||
|
||||
function writeSearchKeyToUrl(sortKey, location, history) {
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
searchParams.set('sort', sortKey.key)
|
||||
history.push({
|
||||
pathname: location.pathname,
|
||||
search: searchParams.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
function SortDropdown({sortKeys, selectedSortKey, onSortKeyChange}) {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
||||
|
||||
function onDropdownSelect(event) {
|
||||
const sortKey = sortKeys.find(k => k.title === event.target.innerText)
|
||||
onSortKeyChange(sortKey)
|
||||
setIsDropdownOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content="Sort pipelines by...">
|
||||
<Dropdown
|
||||
position={DropdownPosition.left}
|
||||
onSelect={onDropdownSelect}
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
onToggle={setIsDropdownOpen}>
|
||||
<SortAmountDownIcon/>
|
||||
{selectedSortKey.title}
|
||||
</DropdownToggle>
|
||||
}
|
||||
isOpen={isDropdownOpen}
|
||||
dropdownItems={sortKeys.map((k) =>
|
||||
<DropdownItem key={k.key}>{k.title}</DropdownItem>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
SortDropdown.propTypes = {
|
||||
sortKeys: PropTypes.array.isRequired,
|
||||
selectedSortKey: PropTypes.object.isRequired,
|
||||
onSortKeyChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export { SortDropdown, getSearchKeyFromUrl, writeSearchKeyToUrl }
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
import { applyFilter, isFilterActive, writeFiltersToUrl } from '../FilterToolbar'
|
||||
import { isPipelineEmpty } from './Misc'
|
||||
|
||||
function handleFilterChange(newFilters, location, history) {
|
||||
writeFiltersToUrl(newFilters, location, history)
|
||||
function handleFilterChange(newFilters, filterCategories, location, history) {
|
||||
writeFiltersToUrl(newFilters, filterCategories, location, history)
|
||||
}
|
||||
|
||||
function clearFilters(location, history, filterCategories) {
|
||||
@@ -25,7 +25,7 @@ function clearFilters(location, history, filterCategories) {
|
||||
filterDict[category.key] = []
|
||||
return filterDict
|
||||
}, {})
|
||||
handleFilterChange(filters, location, history)
|
||||
handleFilterChange(filters, filterCategories, location, history)
|
||||
}
|
||||
|
||||
function filterInputValidation(_, filterValue) {
|
||||
|
||||
@@ -1034,3 +1034,7 @@ details.foldable[open] summary::before {
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.sort-toolbar {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ class BuildsPage extends React.Component {
|
||||
// We must update the URL parameters before the state. Otherwise, the URL
|
||||
// will always be one filter selection behind the state. But as the URL
|
||||
// reflects our state this should be ok.
|
||||
writeFiltersToUrl(finalFilters, location, history)
|
||||
writeFiltersToUrl(finalFilters, this.filterCategories, location, history)
|
||||
let newState = {
|
||||
filters: finalFilters,
|
||||
// if filters haven't changed besides skip or limit, keep our itemCount and currentPage
|
||||
|
||||
@@ -187,7 +187,7 @@ class BuildsetsPage extends React.Component {
|
||||
// will always be one filter selection behind the state. But as the URL
|
||||
// reflects our state this should be ok.
|
||||
|
||||
writeFiltersToUrl(newFilters, location, history)
|
||||
writeFiltersToUrl(newFilters, this.filterCategories, location, history)
|
||||
let newState = {
|
||||
filters: finalFilters,
|
||||
// if filters haven't changed besides skip or limit, keep our itemCount and currentPage
|
||||
|
||||
@@ -204,7 +204,7 @@ class ConfigErrorsPage extends React.Component {
|
||||
// We must update the URL parameters before the state. Otherwise, the URL
|
||||
// will always be one filter selection behind the state. But as the URL
|
||||
// reflects our state this should be ok.
|
||||
writeFiltersToUrl(finalFilters, location, history)
|
||||
writeFiltersToUrl(finalFilters, this.filterCategories, location, history)
|
||||
const newState = {
|
||||
filters: finalFilters,
|
||||
// if filters haven't changed besides skip or limit, keep our itemCount and currentPage
|
||||
|
||||
@@ -200,7 +200,7 @@ function PipelineDetailsPage({
|
||||
<LevelItem>
|
||||
<FilterToolbar
|
||||
filterCategories={filterCategories}
|
||||
onFilterChange={(newFilters) => { handleFilterChange(newFilters, location, history) }}
|
||||
onFilterChange={(newFilters) => { handleFilterChange(newFilters, filterCategories, location, history) }}
|
||||
filters={filters}
|
||||
filterInputValidation={filterInputValidation}
|
||||
>
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
ToolbarStatsGroup,
|
||||
ToolbarStatsItem,
|
||||
} from '../containers/FilterToolbar'
|
||||
import { getSearchKeyFromUrl, writeSearchKeyToUrl, SortDropdown } from '../containers/SortDropdown'
|
||||
import {
|
||||
clearFilters,
|
||||
filterInputValidation,
|
||||
@@ -84,12 +85,24 @@ const filterCategories = [
|
||||
]
|
||||
|
||||
|
||||
function PipelineGallery({ pipelines, tenant, showAllPipelines, expandAll, isLoading, filters, onClearFilters }) {
|
||||
function PipelineGallery({ pipelines, tenant, showAllPipelines, expandAll, isLoading, filters, onClearFilters, sortKey }) {
|
||||
// Filter out empty pipelines if necessary
|
||||
if (!showAllPipelines) {
|
||||
pipelines = pipelines.filter(ppl => ppl._count > 0)
|
||||
}
|
||||
|
||||
switch(sortKey) {
|
||||
case 'name':
|
||||
pipelines = [...pipelines].sort((p1, p2) => p1.name.localeCompare(p2.name))
|
||||
break
|
||||
case 'length':
|
||||
pipelines = [...pipelines].sort((p1, p2) => p2._count - p1._count)
|
||||
break
|
||||
default:
|
||||
pipelines = [...pipelines]
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Gallery
|
||||
@@ -126,6 +139,7 @@ PipelineGallery.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
filters: PropTypes.object,
|
||||
onClearFilters: PropTypes.func,
|
||||
sortKey: PropTypes.string,
|
||||
}
|
||||
|
||||
function getPipelines(status, location) {
|
||||
@@ -178,6 +192,18 @@ function PipelineOverviewPage() {
|
||||
const timezone = useSelector((state) => state.timezone)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const sortKeys = [
|
||||
{key: 'none', title: 'Preset'},
|
||||
{key: 'name', title: 'Name'},
|
||||
{key: 'length', title: 'Length'},
|
||||
]
|
||||
const [currentSortKey, setCurrentSortKey] = useState(getSearchKeyFromUrl(location, sortKeys) || sortKeys[0])
|
||||
|
||||
const onSortKeyChanged = (sortKey) => {
|
||||
setCurrentSortKey(sortKey)
|
||||
writeSearchKeyToUrl(sortKey, location, history)
|
||||
}
|
||||
|
||||
const onShowAllPipelinesToggle = (isChecked) => {
|
||||
setShowAllPipelines(isChecked)
|
||||
localStorage.setItem('zuul_show_all_pipelines', isChecked.toString())
|
||||
@@ -190,7 +216,7 @@ function PipelineOverviewPage() {
|
||||
}
|
||||
|
||||
const onFilterChanged = (newFilters) => {
|
||||
handleFilterChange(newFilters, location, history)
|
||||
handleFilterChange(newFilters, filterCategories, location, history)
|
||||
// show all pipelines when filtering, hide when not
|
||||
setShowAllPipelines(
|
||||
isFilterActive(newFilters) || localStorage.getItem('zuul_show_all_pipelines') === 'true')
|
||||
@@ -262,6 +288,9 @@ function PipelineOverviewPage() {
|
||||
filters={filters}
|
||||
filterInputValidation={filterInputValidation}
|
||||
>
|
||||
<ToolbarItem>
|
||||
<SortDropdown sortKeys={sortKeys} selectedSortKey={currentSortKey} onSortKeyChange={onSortKeyChanged}/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
{filterActive ?
|
||||
<Tooltip content="Disabled when filtering">{allPipelinesSwitch}</Tooltip> :
|
||||
@@ -327,6 +356,7 @@ function PipelineOverviewPage() {
|
||||
isLoading={isFetching}
|
||||
filters={filters}
|
||||
onClearFilters={onClearFilters}
|
||||
sortKey={currentSortKey.key}
|
||||
/>
|
||||
</PageSection>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user