Skip to content

Commit

Permalink
Add updates count badge to Updates sidebar item (jenkinsci#7084)
Browse files Browse the repository at this point in the history
* Init

* Update core/src/main/resources/lib/layout/task.jelly

Co-authored-by: Alexander Brandes <brandes.alexander@web.de>

* Remove duplicated class

* Update core/src/main/resources/lib/layout/task.jelly

Co-authored-by: Daniel Beck <1831569+daniel-beck@users.noreply.github.com>

* Add support for badges in menus + manage jenkins page

* Update index.jelly

* Update ModelObjectWithContextMenu.java

* Merge with jenkinsci#6995

* Add tooltip in task links

* Add tooltip support to badges

* Lint

* Escape badge tooltip/text in menus

* Use xmlEscape for escaping attributes

* Support nulls

* Update breadcrumbs.js

* Update breadcrumbs.js

---------

Co-authored-by: Alexander Brandes <brandes.alexander@web.de>
Co-authored-by: Daniel Beck <1831569+daniel-beck@users.noreply.github.com>
Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com>
Co-authored-by: Tim Jacomb <timjacomb1@gmail.com>
Co-authored-by: Alexander Brandes <mc.cache@web.de>
  • Loading branch information
6 people committed Apr 16, 2023
1 parent fb9031e commit 291f5ed
Show file tree
Hide file tree
Showing 11 changed files with 170 additions and 59 deletions.
7 changes: 4 additions & 3 deletions core/src/main/java/hudson/model/ManageJenkinsAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import hudson.Extension;
import hudson.Util;
import java.io.IOException;
import jenkins.management.Badge;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithContextMenu;
import org.apache.commons.jelly.JellyException;
Expand Down Expand Up @@ -78,13 +79,13 @@ public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse respons
* menu.
*/
@Restricted(NoExternalUse.class)
public void addContextMenuItem(ContextMenu menu, String url, String icon, String iconXml, String text, boolean post, boolean requiresConfirmation) {
public void addContextMenuItem(ContextMenu menu, String url, String icon, String iconXml, String text, boolean post, boolean requiresConfirmation, Badge badge) {
if (Stapler.getCurrentRequest().findAncestorObject(this.getClass()) != null || !Util.isSafeToRedirectTo(url)) {
// Default behavior if the URL is absolute or scheme-relative, or the current object is an ancestor (i.e. would resolve correctly)
menu.add(url, icon, iconXml, text, post, requiresConfirmation);
menu.add(url, icon, iconXml, text, post, requiresConfirmation, badge);
return;
}
// If neither is the case, rewrite the relative URL to point to inside the /manage/ URL space
menu.add("manage/" + url, icon, iconXml, text, post, requiresConfirmation);
menu.add("manage/" + url, icon, iconXml, text, post, requiresConfirmation, badge);
}
}
47 changes: 47 additions & 0 deletions core/src/main/java/hudson/model/UpdateCenter.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
import jenkins.MissingDependencyException;
import jenkins.RestartRequiredException;
import jenkins.install.InstallUtil;
import jenkins.management.Badge;
import jenkins.model.Jenkins;
import jenkins.security.stapler.StaplerDispatchable;
import jenkins.util.SystemProperties;
Expand Down Expand Up @@ -377,6 +378,52 @@ public InstallationJob getJob(Plugin plugin) {
return null;
}

@Restricted(NoExternalUse.class)
public Badge getBadge() {
if (!isSiteDataReady()) {
// Do not display message during this page load, but possibly later.
return null;
}
List<Plugin> plugins = getUpdates();
int size = plugins.size();
if (size > 0) {
StringBuilder tooltip = new StringBuilder();
Badge.Severity severity = Badge.Severity.WARNING;
int securityFixSize = (int) plugins.stream().filter(plugin -> plugin.fixesSecurityVulnerabilities()).count();
int incompatibleSize = (int) plugins.stream().filter(plugin -> !plugin.isCompatibleWithInstalledVersion()).count();
if (size > 1) {
tooltip.append(jenkins.management.Messages.PluginsLink_updatesAvailable(size));
} else {
tooltip.append(jenkins.management.Messages.PluginsLink_updateAvailable());
}
switch (incompatibleSize) {
case 0:
break;
case 1:
tooltip.append("\n").append(jenkins.management.Messages.PluginsLink_incompatibleUpdateAvailable());
break;
default:
tooltip.append("\n").append(jenkins.management.Messages.PluginsLink_incompatibleUpdatesAvailable(incompatibleSize));
break;
}
switch (securityFixSize) {
case 0:
break;
case 1:
tooltip.append("\n").append(jenkins.management.Messages.PluginsLink_securityUpdateAvailable());
severity = Badge.Severity.DANGER;
break;
default:
tooltip.append("\n").append(jenkins.management.Messages.PluginsLink_securityUpdatesAvailable(securityFixSize));
severity = Badge.Severity.DANGER;
break;
}
return new Badge(Integer.toString(size), tooltip.toString(), severity);
}
return null;

}

/**
* Get the current connection status.
* <p>
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/jenkins/management/Badge.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import hudson.model.AdministrativeMonitor;
import hudson.model.ManagementLink;
import java.util.Locale;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

/**
* Definition of a badge that can be returned by a {@link ManagementLink} implementation.
Expand All @@ -48,6 +50,7 @@
*
* @since 2.385
*/
@ExportedBean
public class Badge {

private final String text;
Expand Down Expand Up @@ -75,6 +78,7 @@ public Badge(@NonNull String text, @NonNull String tooltip, @NonNull Severity se
*
* @return badge text
*/
@Exported(visibility = 999)
public String getText() {
return text;
}
Expand All @@ -84,6 +88,7 @@ public String getText() {
*
* @return tooltip
*/
@Exported(visibility = 999)
public String getTooltip() {
return tooltip;
}
Expand Down
44 changes: 1 addition & 43 deletions core/src/main/java/jenkins/management/PluginsLink.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@
import hudson.Extension;
import hudson.model.ManagementLink;
import hudson.model.UpdateCenter;
import hudson.model.UpdateSite.Plugin;
import hudson.security.Permission;
import java.util.List;
import jenkins.model.Jenkins;
import org.jenkinsci.Symbol;

Expand Down Expand Up @@ -75,46 +73,6 @@ public Category getCategory() {
@Override
public Badge getBadge() {
final UpdateCenter updateCenter = Jenkins.get().getUpdateCenter();
if (!updateCenter.isSiteDataReady()) {
// Do not display message during this page load, but possibly later.
return null;
}
List<Plugin> plugins = updateCenter.getUpdates();
int size = plugins.size();
if (size > 0) {
StringBuilder tooltip = new StringBuilder();
Badge.Severity severity = Badge.Severity.WARNING;
int securityFixSize = (int) plugins.stream().filter(plugin -> plugin.fixesSecurityVulnerabilities()).count();
int incompatibleSize = (int) plugins.stream().filter(plugin -> !plugin.isCompatibleWithInstalledVersion()).count();
if (size > 1) {
tooltip.append(Messages.PluginsLink_updatesAvailable(size));
} else {
tooltip.append(Messages.PluginsLink_updateAvailable());
}
switch (incompatibleSize) {
case 0:
break;
case 1:
tooltip.append("\n").append(Messages.PluginsLink_incompatibleUpdateAvailable());
break;
default:
tooltip.append("\n").append(Messages.PluginsLink_incompatibleUpdatesAvailable(incompatibleSize));
break;
}
switch (securityFixSize) {
case 0:
break;
case 1:
tooltip.append("\n").append(Messages.PluginsLink_securityUpdateAvailable());
severity = Badge.Severity.DANGER;
break;
default:
tooltip.append("\n").append(Messages.PluginsLink_securityUpdatesAvailable(securityFixSize));
severity = Badge.Severity.DANGER;
break;
}
return new Badge(Integer.toString(size), tooltip.toString(), severity);
}
return null;
return updateCenter.getBadge();
}
}
25 changes: 25 additions & 0 deletions core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.Collection;
import java.util.List;
import javax.servlet.ServletException;
import jenkins.management.Badge;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyException;
import org.apache.commons.jelly.JellyTagException;
Expand Down Expand Up @@ -149,6 +150,19 @@ public ContextMenu add(String url, String icon, String iconXml, String text, boo
return this;
}

/** @since TODO */
public ContextMenu add(String url, String icon, String iconXml, String text, boolean post, boolean requiresConfirmation, Badge badge) {
if (text != null && icon != null && url != null) {
MenuItem item = new MenuItem(url, icon, text);
item.iconXml = iconXml;
item.post = post;
item.requiresConfirmation = requiresConfirmation;
item.badge = badge;
items.add(item);
}
return this;
}

/**
* Add a header row (no icon, no URL, rendered in header style).
*
Expand Down Expand Up @@ -317,6 +331,8 @@ class MenuItem {
public boolean requiresConfirmation;


private Badge badge;

/**
* The type of menu item
* @since 2.340
Expand All @@ -337,6 +353,15 @@ public String getIconXml() {
return iconXml;
}

/**
* The badge to display for the context menu item
* @since TODO
*/
@Exported
public Badge getBadge() {
return badge;
}

public MenuItem(String url, String icon, String displayName) {
withUrl(url).withIcon(icon).withDisplayName(displayName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:side-panel>
<l:tasks>
<l:task href="${rootURL}/manage/pluginManager/" icon="symbol-download" title="${%Updates}"/>
<l:task href="${rootURL}/manage/pluginManager/" icon="symbol-download" title="${%Updates}" badge="${app.updateCenter.badge}"/>
<l:task href="${rootURL}/manage/pluginManager/available" icon="symbol-shopping-bag" title="${%Available plugins}"/>
<l:task href="${rootURL}/manage/pluginManager/installed" icon="symbol-plugins" title="${%Installed plugins}"/>
<l:task href="${rootURL}/manage/pluginManager/advanced" icon="symbol-settings" title="${%Advanced settings}"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ THE SOFTWARE.
<j:set var="iconXml">
<l:icon src="${m.iconFileName}" />
</j:set>
${taskTags!=null and attrs.contextMenu!='false' ? it.addContextMenuItem(taskTags, m.urlName, iconSrc, iconXml, m.displayName, m.requiresPOST, m.requiresConfirmation) : null}
${taskTags!=null and attrs.contextMenu!='false' ? it.addContextMenuItem(taskTags, m.urlName, iconSrc, iconXml, m.displayName, m.requiresPOST, m.requiresConfirmation, m.badge) : null}
<j:choose>
<j:when test="${m.requiresConfirmation}">
<l:confirmationLink href="${m.urlName}" post="${m.requiresPOST}" message="${%are.you.sure(m.displayName)}">
Expand Down
60 changes: 53 additions & 7 deletions core/src/main/resources/lib/layout/breadcrumbs.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,51 @@ window.breadcrumbs = (function () {
var logger = function () {};
// logger = function() { console.log.apply(console,arguments) }; // uncomment this line to enable logging

function makeMenuHtml(icon, iconXml, displayName) {
// TODO - Use util/security.js xmlEscape in #7474
function xmlEscape(str) {
if (!str) {
return;
}

return str.replace(/[<>&'"]/g, (match) => {
switch (match) {
case "<":
return "&lt;";
case ">":
return "&gt;";
case "&":
return "&amp;";
case "'":
return "&apos;";
case '"':
return "&quot;";
}
});
}

function makeMenuHtml(icon, iconXml, displayName, badge) {
var displaynameSpan = "<span>" + displayName + "</span>";
let badgeText;
let badgeTooltip;
if (badge) {
badgeText = xmlEscape(badge.text);
badgeTooltip = xmlEscape(badge.tooltip);
}
const badgeSpan =
badge === null
? ""
: `<span class="yui-menu-badge" tooltip="${badgeTooltip}">${badgeText}</span>`;

if (iconXml != null) {
return iconXml + displaynameSpan;
return iconXml + displaynameSpan + badgeSpan;
}

if (icon === null) {
return "<span style='margin: 2px 4px 2px 2px;' />" + displaynameSpan;
return (
"<span style='margin: 2px 4px 2px 2px;' />" +
displaynameSpan +
badgeSpan
);
}

// TODO: move this to the API response in a clean way
Expand All @@ -41,11 +77,13 @@ window.breadcrumbs = (function () {
icon +
"' />" +
"</svg>" +
displaynameSpan
displaynameSpan +
badgeSpan
: "<img src='" +
icon +
"' width=24 height=24 style='margin: 2px 4px 2px 2px;' alt=''>" +
displaynameSpan;
displaynameSpan +
badgeSpan;
}

Event.observe(window, "load", function () {
Expand Down Expand Up @@ -156,6 +194,8 @@ window.breadcrumbs = (function () {
menu.addItems(items);
menu.render("breadcrumb-menu-target");
menu.show();

Behaviour.applySubtree(menu.body);
}

// ignore the currently pending call
Expand All @@ -180,14 +220,20 @@ window.breadcrumbs = (function () {
e.text = makeMenuHtml(
e.icon,
e.iconXml,
"<span class='header'>" + e.displayName + "</span>"
"<span class='header'>" + e.displayName + "</span>",
e.badge
);
e.disabled = true;
} else if (e.type === "SEPARATOR") {
e.text = "<span class='separator'>--</span>";
e.disabled = true;
} else {
e.text = makeMenuHtml(e.icon, e.iconXml, e.displayName);
e.text = makeMenuHtml(
e.icon,
e.iconXml,
e.displayName,
e.badge
);
}
if (e.subMenu != null) {
e.subMenu = {
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/resources/lib/layout/task.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ THE SOFTWARE.
Generally used with post="true".
(onclick supersedes this except in the context menu.)
</st:attribute>
<st:attribute name="badge" since="TODO">
If set, displays the value as a small badge on the right side of the sidepanel item.
</st:attribute>
<st:attribute name="confirmationMessage" since="1.512">
Message to use for confirmation, if requested; defaults to title.
</st:attribute>
Expand Down Expand Up @@ -133,6 +136,11 @@ THE SOFTWARE.
<l:icon src="${icon}" />
</span>
<span class="task-link-text">${title}</span>
<j:if test="${attrs.badge != null}">
<span class="task-icon-badge" tooltip="${attrs.badge.tooltip}">
${attrs.badge.text}
</span>
</j:if>
</span>
</span>
</div>
Expand Down Expand Up @@ -164,6 +172,11 @@ THE SOFTWARE.
<l:icon src="${icon}" />
</span>
<span>${title}</span>
<j:if test="${attrs.badge != null}">
<span class="task-icon-badge" tooltip="${attrs.badge.tooltip}">
${attrs.badge.text}
</span>
</j:if>
</l:confirmationLink>
</j:when>

Expand All @@ -173,6 +186,11 @@ THE SOFTWARE.
<l:icon src="${icon}" />
</span>
<span class="task-link-text">${title}</span>
<j:if test="${attrs.badge != null}">
<span class="task-icon-badge" tooltip="${attrs.badge.tooltip}">
${attrs.badge.text}
</span>
</j:if>
</a>
</j:otherwise>
</j:choose>
Expand Down

0 comments on commit 291f5ed

Please sign in to comment.