diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index 0c525dfabe84..383361ee9a85 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -1109,6 +1109,11 @@ public String getSearchUrl() { return getUrl(); } + @Override + public String getSearchIcon() { + return this.getIconClassName(); + } + /** * {@link RetentionStrategy} associated with this computer. * 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 685a80e540a9..85495f93ec0b 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; @@ -277,6 +278,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..a3c76a7fc9d5 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; @@ -157,9 +159,16 @@ 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 item : getSuggestions(req, query)) { + String symbolName = item.item.getSearchIcon(); + if (symbolName == null || !symbolName.startsWith("symbol-")) { + symbolName = "symbol-search"; + } + + r.suggestions.add(new Item(item.getPath(), item.getUrl(), "", + Symbol.get(new SymbolRequest.Builder().withRaw(symbolName).build()))); + } rsp.serveExposedBean(req, r, Flavor.JSON); } @@ -259,19 +268,35 @@ public static class Item { private final String url; + public final String icon; + + public final String iconXml; + public Item(String name) { - this(name, null); + this(name, null, null, null); } - public Item(String name, String url) { + public Item(String name, String url, String icon, String iconXml) { this.name = name; this.url = url; + this.icon = icon; + this.iconXml = iconXml; } @Exported public String getUrl() { return url; } + + @Exported + public String getIcon() { + return icon; + } + + @Exported + public String getIconXml() { + return iconXml; + } } 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/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index eee96b5bca05..516ed8a5565a 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/src/main/js/components/command-palette/datasources.js b/src/main/js/components/command-palette/datasources.js index 8357fca3feaf..20a380eb9157 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,7 @@ export const JenkinsSearchSource = { rsp.json().then((data) => { return data["suggestions"].slice().map((e) => LinkResult({ - icon: Symbols.SEARCH, + icon: e.iconXml, 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 dfb3c6eb7efe..5ece1106aed0 100644 --- a/src/main/js/components/command-palette/index.js +++ b/src/main/js/components/command-palette/index.js @@ -94,18 +94,23 @@ function init() { } searchResultsContainer.style.height = searchResults.offsetHeight + "px"; + debouncedSpinner.cancel(); commandPaletteSearchBarContainer.classList.remove( "jenkins-search--loading", ); }); } + const debouncedSpinner = debounce(() => { + commandPaletteSearchBarContainer.classList.add("jenkins-search--loading"); + }, 150); + const debouncedLoad = debounce(() => { renderResults(); }, 150); commandPaletteInput.addEventListener("input", () => { - commandPaletteSearchBarContainer.classList.add("jenkins-search--loading"); + debouncedSpinner(); debouncedLoad(); }); @@ -119,7 +124,16 @@ function init() { } function hideCommandPalette() { - commandPalette.close(); + commandPalette.setAttribute("closing", ""); + + commandPalette.addEventListener( + "animationend", + () => { + commandPalette.removeAttribute("closing"); + commandPalette.close(); + }, + { once: true }, + ); } function itemMouseEnter(item) { 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 6c3b16517e3a..ebd634cb9ce6 100644 --- a/src/main/scss/components/_command-palette.scss +++ b/src/main/scss/components/_command-palette.scss @@ -1,13 +1,5 @@ @use "../abstracts/mixins"; -.jenkins-command-palette__dialog { - &::backdrop { - background: var(--command-palette-backdrop-background); - backdrop-filter: contrast(0.7) brightness(0.9) saturate(1.25) blur(3px); - animation: jenkins-modal-backdrop-animate-in 0.3s; - } -} - .jenkins-command-palette__dialog { background: none; border: none; @@ -17,6 +9,41 @@ max-width: 100vw !important; margin: 0 !important; padding: 0 !important; + user-select: none; + + &::backdrop { + background: var(--command-palette-backdrop-background); + backdrop-filter: contrast(0.7) brightness(0.9) saturate(1.25) blur(3px); + animation: jenkins-dialog-backdrop-animate-in 0.1s linear; + } + + &[open] { + animation: command-palette-animate-in 0.1s cubic-bezier(0, 0.68, 0.5, 1.5); + } + + &[closing] { + animation: command-palette-animate-out 0.1s linear; + + &::backdrop { + animation: jenkins-dialog-backdrop-animate-out 0.1s linear; + } + } +} + +@keyframes command-palette-animate-in { + from { + translate: 0 4px; + scale: 98.5%; + opacity: 0; + transform: rotateX(30deg); + } +} + +@keyframes command-palette-animate-out { + to { + scale: 98.5%; + opacity: 0; + } } .jenkins-command-palette__wrapper { @@ -40,9 +67,7 @@ --search-bar-height: 3rem !important; background: transparent; - box-shadow: - 0 0 0 20px transparent, - var(--command-palette-inset-shadow); + box-shadow: var(--command-palette-inset-shadow); margin-bottom: 1.5rem; border-radius: 1rem; transition: var(--standard-transition); @@ -71,7 +96,8 @@ border-radius: 1rem; backdrop-filter: var(--command-palette-results-backdrop-filter); box-shadow: var(--command-palette-inset-shadow); - height: 0; + // If set to 0, Safari won't always show the backdrop-filter + height: 1px; transition: height var(--standard-transition); overflow: hidden; will-change: height; @@ -83,8 +109,8 @@ padding: 0.5rem; &__heading { - font-weight: 600; - font-size: 0.85rem; + font-weight: 500; + font-size: 0.875rem; margin: 0; padding: 0.75rem 0.75rem 0.625rem; color: var(--text-color-secondary); @@ -113,8 +139,7 @@ justify-content: flex-start; background: transparent; padding: 0.75rem; - border-radius: 0.66rem; - font-weight: 600; + border-radius: 0.5rem; cursor: pointer; color: var(--text-color) !important; transition: var(--standard-transition); @@ -132,17 +157,17 @@ display: flex; align-items: center; justify-content: center; - width: 1.4rem; - height: 1.4rem; - margin-right: 12.5px; + width: 1.375rem; + height: 1.375rem; + margin-right: 0.75rem; overflow: hidden; pointer-events: none; color: var(--text-color); svg, img { - width: 1.2rem; - height: 1.2rem; + width: 1.25rem; + height: 1.25rem; } } @@ -163,10 +188,10 @@ } &__info { - font-weight: 600; - font-size: 0.85rem; + font-size: 0.875rem; margin: 0; - padding: 12.5px 12.5px 10px; + padding: 0 14px; + line-height: 46px; color: var(--text-color); span { 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 @@ + + + + +