Skip to content

Commit f45d0bf

Browse files
authored
Add grouping to Command Palette (jenkinsci#10252)
2 parents 231d3ea + 6229954 commit f45d0bf

File tree

15 files changed

+225
-39
lines changed

15 files changed

+225
-39
lines changed

core/src/main/java/hudson/model/Computer.java

+6
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
import jenkins.model.IComputer;
113113
import jenkins.model.IDisplayExecutor;
114114
import jenkins.model.Jenkins;
115+
import jenkins.search.SearchGroup;
115116
import jenkins.security.ExtendedReadRedaction;
116117
import jenkins.security.ImpersonatingExecutorService;
117118
import jenkins.security.MasterToSlaveCallable;
@@ -1112,6 +1113,11 @@ public String getSearchUrl() {
11121113
return getUrl();
11131114
}
11141115

1116+
@Override
1117+
public SearchGroup getSearchGroup() {
1118+
return SearchGroup.get(SearchGroup.ComputerSearchGroup.class);
1119+
}
1120+
11151121
/**
11161122
* {@link RetentionStrategy} associated with this computer.
11171123
*

core/src/main/java/hudson/model/Item.java

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.io.IOException;
4040
import java.util.Collection;
4141
import jenkins.model.Jenkins;
42+
import jenkins.search.SearchGroup;
4243
import jenkins.util.SystemProperties;
4344
import jenkins.util.io.OnMaster;
4445
import org.kohsuke.stapler.StaplerRequest2;
@@ -249,6 +250,11 @@ default void onCreatedFromScratch() {
249250
*/
250251
void delete() throws IOException, InterruptedException;
251252

253+
@Override
254+
default SearchGroup getSearchGroup() {
255+
return SearchGroup.get(SearchGroup.ItemSearchGroup.class);
256+
}
257+
252258
PermissionGroup PERMISSIONS = new PermissionGroup(Item.class, Messages._Item_Permissions_Title());
253259
Permission CREATE =
254260
new Permission(

core/src/main/java/hudson/model/User.java

+6
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
import jenkins.model.Loadable;
7373
import jenkins.model.ModelObjectWithContextMenu;
7474
import jenkins.scm.RunWithSCM;
75+
import jenkins.search.SearchGroup;
7576
import jenkins.security.ImpersonatingUserDetailsService2;
7677
import jenkins.security.LastGrantedAuthoritiesProperty;
7778
import jenkins.security.UserDetailsCache;
@@ -284,6 +285,11 @@ public String getSearchIcon() {
284285
return UserAvatarResolver.resolve(this, "48x48");
285286
}
286287

288+
@Override
289+
public SearchGroup getSearchGroup() {
290+
return SearchGroup.get(SearchGroup.UserSearchGroup.class);
291+
}
292+
287293
/**
288294
* The URL of the user page.
289295
*/

core/src/main/java/hudson/model/View.java

+6
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
import jenkins.model.item_category.Categories;
9696
import jenkins.model.item_category.Category;
9797
import jenkins.model.item_category.ItemCategory;
98+
import jenkins.search.SearchGroup;
9899
import jenkins.security.ExtendedReadRedaction;
99100
import jenkins.security.stapler.StaplerNotDispatchable;
100101
import jenkins.util.xml.XMLUtils;
@@ -567,6 +568,11 @@ public String getSearchIcon() {
567568
return "symbol-jobs";
568569
}
569570

571+
@Override
572+
public SearchGroup getSearchGroup() {
573+
return SearchGroup.get(SearchGroup.ViewSearchGroup.class);
574+
}
575+
570576
/**
571577
* Returns the transient {@link Action}s associated with the top page.
572578
*

core/src/main/java/hudson/search/Search.java

+33-11
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
import edu.umd.cs.findbugs.annotations.CheckForNull;
3131
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
32+
import hudson.ExtensionComponent;
33+
import hudson.ExtensionList;
3234
import hudson.Util;
3335
import hudson.util.EditDistance;
3436
import io.jenkins.servlet.ServletExceptionWrapper;
@@ -37,12 +39,16 @@
3739
import java.util.AbstractList;
3840
import java.util.ArrayList;
3941
import java.util.Collections;
42+
import java.util.Comparator;
4043
import java.util.HashSet;
4144
import java.util.List;
45+
import java.util.Map;
4246
import java.util.Set;
4347
import java.util.logging.Level;
4448
import java.util.logging.Logger;
49+
import java.util.stream.Collectors;
4550
import jenkins.model.Jenkins;
51+
import jenkins.search.SearchGroup;
4652
import jenkins.security.stapler.StaplerNotDispatchable;
4753
import jenkins.util.MemoryReductionUtil;
4854
import jenkins.util.SystemProperties;
@@ -171,11 +177,26 @@ public void doSuggest(StaplerRequest2 req, StaplerResponse2 rsp, @QueryParameter
171177

172178
if (iconName.startsWith("symbol")) {
173179
r.suggestions.add(new Item(curItem.getPath(), curItem.getUrl(),
174-
Symbol.get(new SymbolRequest.Builder().withRaw(iconName).build())));
180+
Symbol.get(new SymbolRequest.Builder().withRaw(iconName).build()), "symbol", curItem.item.getSearchGroup().getDisplayName()));
175181
} else {
176-
r.suggestions.add(new Item(curItem.getPath(), curItem.getUrl(), iconName, "image"));
182+
r.suggestions.add(new Item(curItem.getPath(), curItem.getUrl(), iconName, "image", curItem.item.getSearchGroup().getDisplayName()));
177183
}
178184
}
185+
186+
// Sort results by group
187+
ExtensionList<SearchGroup> groupsExtensionList = ExtensionList.lookup(SearchGroup.class);
188+
List<ExtensionComponent<SearchGroup>> components = groupsExtensionList.getComponents();
189+
Map<String, Double> searchGroupOrdinal = components.stream()
190+
.collect(Collectors.toMap(
191+
(k) -> k.getInstance().getDisplayName(),
192+
ExtensionComponent::ordinal
193+
));
194+
r.suggestions.sort(
195+
Comparator.comparingDouble((Item item) -> searchGroupOrdinal.getOrDefault(item.getGroup(), Double.MAX_VALUE))
196+
.reversed()
197+
.thenComparing(item -> item.name)
198+
);
199+
179200
rsp.serveExposedBean(req, r, new ExportConfig());
180201
}
181202

@@ -270,7 +291,6 @@ public static class Result {
270291
public static class Item {
271292

272293
@Exported
273-
@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD", justification = "read by Stapler")
274294
public String name;
275295

276296
private final String url;
@@ -279,22 +299,19 @@ public static class Item {
279299

280300
private final String icon;
281301

302+
private final String group;
303+
282304
public Item(String name) {
283-
this(name, null, null);
305+
this(name, null, null, "symbol", null);
284306
}
285307

286-
public Item(String name, String url, String icon) {
308+
public Item(String name, String url, String icon, String type, String group) {
287309
this.name = name;
288310
this.url = url;
289311
this.icon = icon;
290-
this.type = "symbol";
291-
}
292-
293-
public Item(String name, String url, String icon, String type) {
294312
this.name = name;
295-
this.url = url;
296-
this.icon = icon;
297313
this.type = type;
314+
this.group = group;
298315
}
299316

300317
@Exported
@@ -311,6 +328,11 @@ public String getIcon() {
311328
public String getType() {
312329
return type;
313330
}
331+
332+
@Exported
333+
public String getGroup() {
334+
return group;
335+
}
314336
}
315337

316338
private enum Mode {

core/src/main/java/hudson/search/SearchItem.java

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package hudson.search;
2626

2727
import hudson.model.Build;
28+
import jenkins.search.SearchGroup;
2829
import org.jenkins.ui.icon.IconSpec;
2930

3031
/**
@@ -63,6 +64,10 @@ default String getSearchIcon() {
6364
return "symbol-search";
6465
}
6566

67+
default SearchGroup getSearchGroup() {
68+
return SearchGroup.get(SearchGroup.UnclassifiedSearchGroup.class);
69+
}
70+
6671
/**
6772
* Returns the {@link SearchIndex} to further search sub items inside this item.
6873
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package jenkins.search;
2+
3+
import static jenkins.search.Messages.SearchGroup_ComputerSearchGroup_DisplayName;
4+
import static jenkins.search.Messages.SearchGroup_ItemSearchGroup_DisplayName;
5+
import static jenkins.search.Messages.SearchGroup_UnclassifiedSearchGroup_DisplayName;
6+
import static jenkins.search.Messages.SearchGroup_UserSearchGroup_DisplayName;
7+
import static jenkins.search.Messages.SearchGroup_ViewSearchGroup_DisplayName;
8+
9+
import edu.umd.cs.findbugs.annotations.NonNull;
10+
import hudson.Extension;
11+
import hudson.ExtensionList;
12+
import hudson.ExtensionPoint;
13+
import hudson.model.ModelObject;
14+
15+
public interface SearchGroup extends ExtensionPoint, ModelObject {
16+
17+
static ExtensionList<SearchGroup> all() {
18+
return ExtensionList.lookup(SearchGroup.class);
19+
}
20+
21+
static @NonNull <T extends SearchGroup> T get(Class<T> type) {
22+
T category = all().get(type);
23+
if (category == null) {
24+
throw new AssertionError("Group not found. It seems the " + type + " is not annotated with @Extension and so not registered");
25+
}
26+
return category;
27+
}
28+
29+
@Extension(ordinal = -1)
30+
class UnclassifiedSearchGroup implements SearchGroup {
31+
32+
@Override
33+
public String getDisplayName() {
34+
return SearchGroup_UnclassifiedSearchGroup_DisplayName();
35+
}
36+
}
37+
38+
@Extension(ordinal = 999)
39+
class ItemSearchGroup implements SearchGroup {
40+
41+
@Override
42+
public String getDisplayName() {
43+
return SearchGroup_ItemSearchGroup_DisplayName();
44+
}
45+
}
46+
47+
@Extension
48+
class ComputerSearchGroup implements SearchGroup {
49+
50+
@Override
51+
public String getDisplayName() {
52+
return SearchGroup_ComputerSearchGroup_DisplayName();
53+
}
54+
}
55+
56+
@Extension
57+
class ViewSearchGroup implements SearchGroup {
58+
59+
@Override
60+
public String getDisplayName() {
61+
return SearchGroup_ViewSearchGroup_DisplayName();
62+
}
63+
}
64+
65+
@Extension
66+
class UserSearchGroup implements SearchGroup {
67+
68+
@Override
69+
public String getDisplayName() {
70+
return SearchGroup_UserSearchGroup_DisplayName();
71+
}
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# The MIT License
2+
#
3+
# Copyright (c) 2025 Jan Faracik
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
# THE SOFTWARE.
22+
23+
SearchGroup.UnclassifiedSearchGroup.DisplayName=Other
24+
SearchGroup.ItemSearchGroup.DisplayName=Items
25+
SearchGroup.ComputerSearchGroup.DisplayName=Nodes
26+
SearchGroup.ViewSearchGroup.DisplayName=Views
27+
SearchGroup.UserSearchGroup.DisplayName=Users

src/main/js/components/command-palette/datasources.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const JenkinsSearchSource = {
2121
type: e.type,
2222
label: e.name,
2323
url: correctAddress(e.url),
24+
group: e.group,
2425
}),
2526
);
2627
}),

src/main/js/components/command-palette/index.js

+18-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as Symbols from "./symbols";
55
import makeKeyboardNavigable from "@/util/keyboard";
66
import { xmlEscape } from "@/util/security";
77
import { createElementFromHtml } from "@/util/dom";
8+
import { groupResultsByCategory } from "@/components/command-palette/utils";
89

910
const datasources = [JenkinsSearchSource];
1011

@@ -68,6 +69,7 @@ function init() {
6869
label: i18n.dataset.getHelp,
6970
url: headerCommandPaletteButton.dataset.searchHelpUrl,
7071
isExternal: true,
72+
group: null,
7173
}),
7274
]);
7375
} else {
@@ -77,15 +79,26 @@ function init() {
7779
}
7880

7981
results.then((results) => {
82+
results = groupResultsByCategory(results);
83+
8084
// Clear current search results
8185
searchResults.innerHTML = "";
8286

8387
if (query.length === 0 || Object.keys(results).length > 0) {
84-
results.forEach(function (obj) {
85-
const link = createElementFromHtml(obj.render());
86-
link.addEventListener("mouseenter", (e) => itemMouseEnter(e));
87-
searchResults.append(link);
88-
});
88+
for (const [group, items] of Object.entries(results)) {
89+
if (group !== "null") {
90+
const heading = document.createElement("p");
91+
heading.className = "jenkins-command-palette__results__heading";
92+
heading.innerText = group;
93+
searchResults.append(heading);
94+
}
95+
96+
items.forEach(function (obj) {
97+
const link = createElementFromHtml(obj.render());
98+
link.addEventListener("mouseenter", (e) => itemMouseEnter(e));
99+
searchResults.append(link);
100+
});
101+
}
89102

90103
updateSelectedItem(0);
91104
} else {

src/main/js/components/command-palette/models.js

+2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import { xmlEscape } from "@/util/security";
77
* @param {string} params.label
88
* @param {'symbol' | 'image'} params.type
99
* @param {string} params.url
10+
* @param {string | null} params.group
1011
* @param {boolean | undefined} params.isExternal
1112
*/
1213
export function LinkResult(params) {
1314
return {
1415
label: params.label,
1516
url: params.url,
17+
group: params.group,
1618
render: () => {
1719
return `<a class="jenkins-command-palette__results__item" href="${xmlEscape(
1820
params.url,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Group results by 'group' field into a map
3+
*/
4+
export function groupResultsByCategory(array) {
5+
return array.reduce((hash, obj) => {
6+
if (obj.group === undefined) {
7+
return hash;
8+
}
9+
return Object.assign(hash, {
10+
[obj.group]: (hash[obj.group] || []).concat(obj),
11+
});
12+
}, {});
13+
}

0 commit comments

Comments
 (0)