diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java index 7d140656e363..643e3d186e85 100644 --- a/core/src/main/java/hudson/model/Job.java +++ b/core/src/main/java/hudson/model/Job.java @@ -519,6 +519,11 @@ public boolean supportsLogRotator() { return true; } + @Override + public String getSearchIcon() { + return "symbol-status-" + this.getIconColor().getIconName(); + } + @Override protected SearchIndexBuilder makeSearchIndex() { return super.makeSearchIndex().add(new SearchIndex() { diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java index 72d2719bc1ec..d40664ca2c7a 100644 --- a/core/src/main/java/hudson/model/User.java +++ b/core/src/main/java/hudson/model/User.java @@ -44,6 +44,7 @@ import hudson.security.AccessControlled; import hudson.security.SecurityRealm; import hudson.security.UserMayOrMayNotExistException2; +import hudson.tasks.UserAvatarResolver; import hudson.util.FormValidation; import hudson.util.RunList; import hudson.util.XStream2; @@ -278,6 +279,11 @@ public String getId() { return "/user/" + Util.rawEncode(idStrategy().keyFor(id)); } + @Override + public String getSearchIcon() { + return UserAvatarResolver.resolve(this, "48x48"); + } + /** * The URL of the user page. */ diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java index b17077cde44d..722254572c0c 100644 --- a/core/src/main/java/hudson/model/View.java +++ b/core/src/main/java/hudson/model/View.java @@ -560,6 +560,11 @@ public String getSearchUrl() { return getUrl(); } + @Override + public String getSearchIcon() { + return "symbol-jobs"; + } + /** * Returns the transient {@link Action}s associated with the top page. * diff --git a/core/src/main/java/hudson/search/Search.java b/core/src/main/java/hudson/search/Search.java index 9bd624e34742..97d15b65c2df 100644 --- a/core/src/main/java/hudson/search/Search.java +++ b/core/src/main/java/hudson/search/Search.java @@ -46,6 +46,8 @@ import jenkins.security.stapler.StaplerNotDispatchable; import jenkins.util.MemoryReductionUtil; import jenkins.util.SystemProperties; +import org.jenkins.ui.symbol.Symbol; +import org.jenkins.ui.symbol.SymbolRequest; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.Ancestor; @@ -56,6 +58,7 @@ import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.StaplerResponse2; import org.kohsuke.stapler.export.DataWriter; +import org.kohsuke.stapler.export.ExportConfig; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.export.Flavor; @@ -157,10 +160,23 @@ public void doSuggestOpenSearch(StaplerRequest2 req, StaplerResponse2 rsp, @Quer */ public void doSuggest(StaplerRequest2 req, StaplerResponse2 rsp, @QueryParameter String query) throws IOException, ServletException { Result r = new Result(); - for (SuggestedItem item : getSuggestions(req, query)) - r.suggestions.add(new Item(item.getPath(), item.getUrl())); + for (SuggestedItem curItem : getSuggestions(req, query)) { + String iconName = curItem.item.getSearchIcon(); - rsp.serveExposedBean(req, r, Flavor.JSON); + if (iconName == null || + (!iconName.startsWith("symbol-") && !iconName.startsWith("http")) + ) { + iconName = "symbol-search"; + } + + if (iconName.startsWith("symbol")) { + r.suggestions.add(new Item(curItem.getPath(), curItem.getUrl(), + Symbol.get(new SymbolRequest.Builder().withRaw(iconName).build()))); + } else { + r.suggestions.add(new Item(curItem.getPath(), curItem.getUrl(), iconName, "image")); + } + } + rsp.serveExposedBean(req, r, new ExportConfig()); } /** @@ -259,19 +275,42 @@ public static class Item { private final String url; + private final String type; + + private final String icon; + public Item(String name) { - this(name, null); + this(name, null, null); + } + + public Item(String name, String url, String icon) { + this.name = name; + this.url = url; + this.icon = icon; + this.type = "symbol"; } - public Item(String name, String url) { + public Item(String name, String url, String icon, String type) { this.name = name; this.url = url; + this.icon = icon; + this.type = type; } @Exported public String getUrl() { return url; } + + @Exported + public String getIcon() { + return icon; + } + + @Exported + public String getType() { + return type; + } } private enum Mode { diff --git a/core/src/main/java/hudson/search/SearchItem.java b/core/src/main/java/hudson/search/SearchItem.java index 02b2921b9741..e64efb0257ed 100644 --- a/core/src/main/java/hudson/search/SearchItem.java +++ b/core/src/main/java/hudson/search/SearchItem.java @@ -25,6 +25,7 @@ package hudson.search; import hudson.model.Build; +import org.jenkins.ui.icon.IconSpec; /** * Represents an item reachable from {@link SearchIndex}. @@ -54,6 +55,14 @@ public interface SearchItem { String getSearchUrl(); + default String getSearchIcon() { + if (this instanceof IconSpec) { + return ((IconSpec) this).getIconClassName(); + } + + return "symbol-search"; + } + /** * Returns the {@link SearchIndex} to further search sub items inside this item. * diff --git a/core/src/main/java/jenkins/model/IComputer.java b/core/src/main/java/jenkins/model/IComputer.java index 37fe0af98535..e00ee78bd30f 100644 --- a/core/src/main/java/jenkins/model/IComputer.java +++ b/core/src/main/java/jenkins/model/IComputer.java @@ -35,6 +35,7 @@ import jenkins.agents.IOfflineCause; import org.jenkins.ui.icon.Icon; import org.jenkins.ui.icon.IconSet; +import org.jenkins.ui.icon.IconSpec; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.Beta; @@ -44,7 +45,7 @@ * @since 2.480 */ @Restricted(Beta.class) -public interface IComputer extends AccessControlled { +public interface IComputer extends AccessControlled, IconSpec { /** * Returns {@link Node#getNodeName() the name of the node}. */ diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index e643e848dd9b..66adc45f82e8 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -147,6 +147,7 @@ import hudson.scm.RepositoryBrowser; import hudson.scm.SCM; import hudson.search.CollectionSearchIndex; +import hudson.search.SearchIndex; import hudson.search.SearchIndexBuilder; import hudson.search.SearchItem; import hudson.security.ACL; @@ -2345,9 +2346,29 @@ public String getSearchUrl() { @Override public SearchIndexBuilder makeSearchIndex() { SearchIndexBuilder builder = super.makeSearchIndex(); - if (hasPermission(ADMINISTER)) { - builder.add("manage", Messages.ManageJenkinsAction_DisplayName()); - } + + this.actions.stream().filter(e -> e.getIconFileName() != null).forEach(action -> builder.add(new SearchItem() { + @Override + public String getSearchName() { + return action.getDisplayName(); + } + + @Override + public String getSearchUrl() { + return action.getUrlName(); + } + + @Override + public String getSearchIcon() { + return action.getIconFileName(); + } + + @Override + public SearchIndex getSearchIndex() { + return SearchIndex.EMPTY; + } + })); + builder.add(new CollectionSearchIndex() { @Override protected SearchItem get(String key) { return getItemByFullName(key, TopLevelItem.class); } diff --git a/core/src/main/java/org/jenkins/ui/icon/IconSpec.java b/core/src/main/java/org/jenkins/ui/icon/IconSpec.java index c44e577e1ce0..f1bf535b6ca6 100644 --- a/core/src/main/java/org/jenkins/ui/icon/IconSpec.java +++ b/core/src/main/java/org/jenkins/ui/icon/IconSpec.java @@ -27,8 +27,7 @@ /** * Icon Specification. *
- * Plugin extension points that implement/extend Action/ManagementLink should - * also implement this interface. + * If your class provides an icon spec you should implement this interface. * * @author tom.fennelly@gmail.com * @since 2.0 diff --git a/src/main/js/components/command-palette/datasources.js b/src/main/js/components/command-palette/datasources.js index 8357fca3feaf..58dd5389d540 100644 --- a/src/main/js/components/command-palette/datasources.js +++ b/src/main/js/components/command-palette/datasources.js @@ -1,6 +1,5 @@ import { LinkResult } from "./models"; import Search from "@/api/search"; -import * as Symbols from "./symbols"; export const JenkinsSearchSource = { execute(query) { @@ -18,7 +17,8 @@ export const JenkinsSearchSource = { rsp.json().then((data) => { return data["suggestions"].slice().map((e) => LinkResult({ - icon: Symbols.SEARCH, + icon: e.icon, + type: e.type, label: e.name, url: correctAddress(e.url), }), diff --git a/src/main/js/components/command-palette/index.js b/src/main/js/components/command-palette/index.js index 7c8041b50007..ca2926d3422b 100644 --- a/src/main/js/components/command-palette/index.js +++ b/src/main/js/components/command-palette/index.js @@ -61,6 +61,7 @@ function init() { results = Promise.all([ LinkResult({ icon: Symbols.HELP, + type: "symbol", label: i18n.dataset.getHelp, url: headerCommandPaletteButton.dataset.searchHelpUrl, isExternal: true, diff --git a/src/main/js/components/command-palette/models.js b/src/main/js/components/command-palette/models.js index 8159ce68c9a2..0c6a733e0d0a 100644 --- a/src/main/js/components/command-palette/models.js +++ b/src/main/js/components/command-palette/models.js @@ -5,6 +5,7 @@ import { xmlEscape } from "@/util/security"; * @param {Object} params * @param {string} params.icon * @param {string} params.label + * @param {'symbol' | 'image'} params.type * @param {string} params.url * @param {boolean | undefined} params.isExternal */ @@ -16,9 +17,8 @@ export function LinkResult(params) { return ` -
${ - params.icon - }
+ ${params.type === "image" ? `${xmlEscape(params.label)}` : ""} + ${params.type !== "image" ? `
${params.icon}
` : ""} ${xmlEscape(params.label)} ${params.isExternal ? Symbols.EXTERNAL_LINK : ""}
`; diff --git a/src/main/js/components/command-palette/symbols.js b/src/main/js/components/command-palette/symbols.js index f1d7b63c4b2c..14fc815519da 100644 --- a/src/main/js/components/command-palette/symbols.js +++ b/src/main/js/components/command-palette/symbols.js @@ -1,3 +1,2 @@ export const EXTERNAL_LINK = ``; export const HELP = ``; -export const SEARCH = `Search`; diff --git a/src/main/scss/components/_command-palette.scss b/src/main/scss/components/_command-palette.scss index 8aadde54608d..ebd634cb9ce6 100644 --- a/src/main/scss/components/_command-palette.scss +++ b/src/main/scss/components/_command-palette.scss @@ -166,8 +166,8 @@ svg, img { - width: 1.125rem; - height: 1.125rem; + width: 1.25rem; + height: 1.25rem; } } diff --git a/test/src/test/java/hudson/model/AbstractProjectTest.java b/test/src/test/java/hudson/model/AbstractProjectTest.java index 04db7ea8f4fb..b8a9d8e1ea6a 100644 --- a/test/src/test/java/hudson/model/AbstractProjectTest.java +++ b/test/src/test/java/hudson/model/AbstractProjectTest.java @@ -664,6 +664,8 @@ private void testAutoCompleteResponse(JSONObject responseBody, String... project JSONObject o = new JSONObject(); o.put("name", p); o.put("url", JSONObject.fromObject(null)); + o.put("icon", JSONObject.fromObject(null)); + o.put("type", "symbol"); expected.add(o); } assertThat(suggestions.containsAll(expected), is(true)); diff --git a/war/src/main/resources/images/symbols/jobs.svg b/war/src/main/resources/images/symbols/jobs.svg new file mode 100644 index 000000000000..b17b227c6401 --- /dev/null +++ b/war/src/main/resources/images/symbols/jobs.svg @@ -0,0 +1,5 @@ + + + + +