diff --git a/core/src/main/java/hudson/util/FormApply.java b/core/src/main/java/hudson/util/FormApply.java index 7542b81a5d..8a1db5bebe 100644 --- a/core/src/main/java/hudson/util/FormApply.java +++ b/core/src/main/java/hudson/util/FormApply.java @@ -50,7 +50,7 @@ public class FormApply { public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { if (isApply(req)) { // if the submission is via 'apply', show a response in the notification bar - applyResponse("notificationBar.show('" + Messages.HttpResponses_Saved() + "',notificationBar.OK)") + applyResponse("notificationBar.show('" + Messages.HttpResponses_Saved() + "',notificationBar.SUCCESS)") .generateResponse(req, rsp, node); } else { rsp.sendRedirect(destination); diff --git a/core/src/main/resources/lib/layout/layout.jelly b/core/src/main/resources/lib/layout/layout.jelly index efecd8a2ce..d35336e9c8 100644 --- a/core/src/main/resources/lib/layout/layout.jelly +++ b/core/src/main/resources/lib/layout/layout.jelly @@ -173,6 +173,7 @@ THE SOFTWARE. +
diff --git a/war/src/main/js/app.js b/war/src/main/js/app.js new file mode 100644 index 0000000000..65c4e0f33f --- /dev/null +++ b/war/src/main/js/app.js @@ -0,0 +1,3 @@ +import Notifications from "@/components/notifications"; + +Notifications.init(); diff --git a/war/src/main/js/components/notifications/index.js b/war/src/main/js/components/notifications/index.js new file mode 100644 index 0000000000..8c5f784006 --- /dev/null +++ b/war/src/main/js/components/notifications/index.js @@ -0,0 +1,78 @@ +import * as Symbols from "@/util/symbols"; +import { createElementFromHtml } from "@/util/dom"; + +function init() { + window.notificationBar = { + OPACITY: 1, + DELAY: 3000, // milliseconds to auto-close the notification + div: null, // the main 'notification-bar' DIV + token: null, // timer for cancelling auto-close + defaultIcon: Symbols.INFO, + defaultAlertClass: "jenkins-notification", + + SUCCESS: { + alertClass: "jenkins-notification jenkins-notification--success", + icon: Symbols.SUCCESS, + }, + WARNING: { + alertClass: "jenkins-notification jenkins-notification--warning", + icon: Symbols.WARNING, + }, + ERROR: { + alertClass: "jenkins-notification jenkins-notification--error", + icon: Symbols.ERROR, + sticky: true, + }, + + init: function () { + if (this.div == null) { + this.div = document.createElement("div"); + this.div.id = "notification-bar"; + document.body.insertBefore(this.div, document.body.firstElementChild); + const self = this; + this.div.onclick = function () { + self.hide(); + }; + } else { + this.div.innerHTML = ""; + } + }, + // cancel pending auto-hide timeout + clearTimeout: function () { + if (this.token) { + window.clearTimeout(this.token); + } + this.token = null; + }, + // hide the current notification bar, if it's displayed + hide: function () { + this.clearTimeout(); + this.div.classList.remove("jenkins-notification--visible"); + this.div.classList.add("jenkins-notification--hidden"); + }, + // show a notification bar + show: function (text, options) { + options = options || {}; + this.init(); + + this.div.appendChild( + createElementFromHtml(options.icon || this.defaultIcon) + ); + const message = this.div.appendChild(document.createElement("span")); + message.appendChild(document.createTextNode(text)); + + this.div.className = options.alertClass || this.defaultAlertClass; + this.div.classList.add("jenkins-notification--visible"); + + this.clearTimeout(); + const self = this; + if (!options.sticky) { + this.token = window.setTimeout(function () { + self.hide(); + }, this.DELAY); + } + }, + }; +} + +export default { init }; diff --git a/war/src/main/js/util/symbols.js b/war/src/main/js/util/symbols.js new file mode 100644 index 0000000000..f135b1c7b9 --- /dev/null +++ b/war/src/main/js/util/symbols.js @@ -0,0 +1,4 @@ +export const INFO = ``; +export const SUCCESS = ``; +export const WARNING = ``; +export const ERROR = ``; diff --git a/war/src/main/less/abstracts/theme.less b/war/src/main/less/abstracts/theme.less index 14cc22b43e..d63ce01ceb 100644 --- a/war/src/main/less/abstracts/theme.less +++ b/war/src/main/less/abstracts/theme.less @@ -41,7 +41,7 @@ // Status icon colors --weather-icon-color: var(--primary); - --unstable-build-icon-color: var(--notification-warning-icon-color); + --unstable-build-icon-color: var(--orange); // Background colors --background: var(--white); @@ -76,31 +76,6 @@ --breadcrumbs-text-color: #4d545d; --breadcrumbs-item-bg-color--hover: var(--light-grey); - // Alert banners - // Default - --alert-default-icon-color: #2196f3; - --alert-default-bg-color: #d1ecf1; - --alert-default-border-color: #bee5eb; - --alert-default-color: #0c5464; - - // Success - --notification-success-icon-color: #4caf50; - --notification-success-bg-color: #d4edda; - --notification-success-border-color: #c3e6cb; - --notification-success-color: var(--success); - - // Warning - --notification-warning-icon-color: #ff9800; - --notification-warning-bg-color: #fff3cd; - --notification-warning-border-color: #ffeeba; - --notification-warning-color: #856404; - - // Error - --notification-error-icon-color: #f44336; - --notification-error-bg-color: #f8d7da; - --notification-error-border-color: #f5c6cb; - --notification-error-color: #721c24; - // Alert call outs --alert-success-text-color: #155724; --alert-success-bg-color: #d4edda; diff --git a/war/src/main/less/base/style.less b/war/src/main/less/base/style.less index 132cf4e863..8764a91bdf 100644 --- a/war/src/main/less/base/style.less +++ b/war/src/main/less/base/style.less @@ -1170,96 +1170,7 @@ table.progress-bar.red td.progress-bar-done { background-color: #c00; } -/* ========================= notification bar ========================= */ -#notification-bar { - width: 100%; - position: fixed; - text-align: center; - left: 0; - font-size: 1.75rem; - z-index: 1000; - border-bottom: 1px solid var(--black); - line-height: 3.5rem; - height: 3.5rem; - display: block; - will-change: opacity; -} - -#notification-bar .svg-icon { - width: 35px; - height: 35px; - padding-bottom: 5px; -} - -#notification-bar.notif-alert-default { - background-color: var(--alert-default-bg-color); - border-color: var(--alert-default-border-color); - color: var(--alert-default-color); - - .svg-icon { - color: var(--alert-default-icon-color); - } -} - -#notification-bar.notif-alert-success { - background-color: var(--notification-success-bg-color); - border-color: var(--notification-success-border-color); - color: var(--notification-success-color); - - .svg-icon { - color: var(--notification-success-icon-color); - } -} - -#notification-bar.notif-alert-warn { - background-color: var(--notification-warning-bg-color); - border-color: var(--notification-warning-border-color); - color: var(--notification-warning-color); - - .svg-icon { - color: var(--notification-warning-icon-color); - } -} - -#notification-bar.notif-alert-err { - background-color: var(--notification-error-bg-color); - border-color: var(--notification-error-border-color); - color: var(--notification-error-color); - - .svg-icon { - color: var(--notification-error-icon-color); - } -} - -#notification-bar.notif-alert-show { - animation: fadein 350ms ease-out 1 normal forwards; -} - -#notification-bar.notif-alert-clear { - animation: fadeout 350ms ease-in 1 normal forwards; -} - -@keyframes fadein { - from { - opacity: 0; - } - - to { - opacity: 1; - visibility: visible; - } -} - -@keyframes fadeout { - from { - opacity: 1; - } - - to { - opacity: 0; - visibility: hidden; - } -} +/* Unknown */ @keyframes spin { from { diff --git a/war/src/main/less/modules/notifications.less b/war/src/main/less/modules/notifications.less new file mode 100644 index 0000000000..7a1cd5ef40 --- /dev/null +++ b/war/src/main/less/modules/notifications.less @@ -0,0 +1,124 @@ +.jenkins-notification { + position: fixed; + left: 1.2rem; + bottom: 1.2rem; + min-width: 321px; + max-width: ~"min(600px, calc(100vw - 2.4rem))"; + display: grid; + grid-template-columns: auto 1fr; + grid-gap: 1.5ch; + padding: 0.8rem; + border-radius: 10px; + font-weight: 500; + line-height: 1.66; + color: var(--text-color); + box-shadow: 0 0 1px 1px rgba(darken(#024cb6, 50%), 0.075), + 0 10px 30px rgba(darken(#024cb6, 50%), 0.25), 0 0 30px 5px var(--background); + will-change: opacity, transform; + z-index: 999; + cursor: pointer; + transition: filter var(--standard-transition); + backdrop-filter: brightness(2) blur(30px); + + svg { + width: 1.4rem; + height: 1.4rem; + } + + &::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + z-index: -1; + background: var(--background); + opacity: 0.3; + } + + @supports not (backdrop-filter: blur(15px)) { + &::after { + opacity: 0.9; + } + } + + &:hover { + filter: brightness(0.95); + } + + &:active { + filter: brightness(0.9); + } +} + +.jenkins-notification--success { + color: var(--background); + + &::after { + background-color: var(--success-color); + opacity: 1; + } +} + +.jenkins-notification--warning { + color: var(--background); + + &::after { + background-color: var(--warning-color); + opacity: 1; + } +} + +.jenkins-notification--error { + color: var(--background); + + &::after { + background-color: var(--error-color); + opacity: 1; + } +} + +.jenkins-notification--visible { + animation: show-notification var(--elastic-transition) 1 normal forwards; + + & > * { + animation: show-notification-icon var(--elastic-transition) 1 normal + forwards; + } +} + +.jenkins-notification--hidden { + animation: hide-notification 150ms ease-in 1 normal forwards; +} + +@keyframes show-notification { + from { + opacity: 0; + transform: translateY(1.2rem); + } + + to { + opacity: 1; + transform: translateY(0); + visibility: visible; + } +} + +@keyframes show-notification-icon { + from { + opacity: 0; + transform: translateY(0.3rem); + } +} + +@keyframes hide-notification { + from { + opacity: 1; + transform: scale(1); + } + + to { + opacity: 0; + transform: scale(0.9); + visibility: hidden; + } +} diff --git a/war/src/main/less/styles.less b/war/src/main/less/styles.less index 649dc831a8..44d1c801d5 100644 --- a/war/src/main/less/styles.less +++ b/war/src/main/less/styles.less @@ -23,6 +23,7 @@ @import url("./modules/buttons-deprecated"); @import url("./modules/content-blocks"); @import url("./modules/icons"); +@import url("./modules/notifications"); @import url("./modules/page-footer"); @import url("./modules/page-header"); @import url("./modules/panes-and-bigtable"); diff --git a/war/src/main/webapp/scripts/hudson-behavior.js b/war/src/main/webapp/scripts/hudson-behavior.js index eaaec5a5f1..7483b21796 100644 --- a/war/src/main/webapp/scripts/hudson-behavior.js +++ b/war/src/main/webapp/scripts/hudson-behavior.js @@ -2759,94 +2759,3 @@ var layoutUpdateCallback = { this.callbacks[i](); }, }; - -// Notification bar -// ============================== -// this control displays a single line message at the top of the page, like StackOverflow does -// see ui-samples for more details -var notificationBar = { - OPACITY: 1, - DELAY: 3000, // milliseconds to auto-close the notification - div: null, // the main 'notification-bar' DIV - token: null, // timer for cancelling auto-close - defaultIcon: "svg-sprite-action-symbol.svg#ic_info_24px", - defaultAlertClass: "notif-alert-default", - - OK: { - // standard option values for typical OK notification - icon: "svg-sprite-action-symbol.svg#ic_check_circle_24px", - alertClass: "notif-alert-success", - }, - WARNING: { - // likewise, for warning - icon: "svg-sprite-action-symbol.svg#ic_report_problem_24px", - alertClass: "notif-alert-warn", - }, - ERROR: { - // likewise, for error - icon: "svg-sprite-action-symbol.svg#ic_highlight_off_24px", - alertClass: "notif-alert-err", - sticky: true, - }, - - init: function () { - if (this.div == null) { - this.div = document.createElement("div"); - YAHOO.util.Dom.setStyle(this.div, "opacity", 0); - this.div.id = "notification-bar"; - document.body.insertBefore(this.div, document.body.firstElementChild); - var self = this; - this.div.onclick = function () { - self.hide(); - }; - } else { - this.div.innerHTML = ""; - } - }, - // cancel pending auto-hide timeout - clearTimeout: function () { - if (this.token) window.clearTimeout(this.token); - this.token = null; - }, - // hide the current notification bar, if it's displayed - hide: function () { - this.clearTimeout(); - this.div.classList.remove("notif-alert-show"); - this.div.classList.add("notif-alert-clear"); - }, - // show a notification bar - show: function (text, options) { - options = options || {}; - this.init(); - var icon = this.div.appendChild(document.createElement("div")); - icon.style.display = "inline-block"; - if (options.iconColor || this.defaultIconColor) { - icon.style.color = options.iconColor || this.defaultIconColor; - } - var svg = icon.appendChild( - document.createElementNS("http://www.w3.org/2000/svg", "svg") - ); - svg.setAttribute("viewBox", "0 0 24 24"); - svg.setAttribute("focusable", "false"); - svg.setAttribute("class", "svg-icon"); - var use = svg.appendChild( - document.createElementNS("http://www.w3.org/2000/svg", "use") - ); - use.setAttribute( - "href", - rootURL + "/images/material-icons/" + (options.icon || this.defaultIcon) - ); - var message = this.div.appendChild(document.createElement("span")); - message.appendChild(document.createTextNode(text)); - - this.div.className = options.alertClass || this.defaultAlertClass; - this.div.classList.add("notif-alert-show"); - - this.clearTimeout(); - var self = this; - if (!options.sticky) - this.token = window.setTimeout(function () { - self.hide(); - }, this.DELAY); - }, -}; diff --git a/war/webpack.config.js b/war/webpack.config.js index 26b04c36d1..6208629e8c 100644 --- a/war/webpack.config.js +++ b/war/webpack.config.js @@ -30,6 +30,7 @@ module.exports = (env, argv) => ({ path.join(__dirname, "src/main/js/config-tabbar.js"), path.join(__dirname, "src/main/js/config-tabbar.less"), ], + app: [path.join(__dirname, "src/main/js/app.js")], "keyboard-shortcuts": [ path.join(__dirname, "src/main/js/keyboard-shortcuts.js"), ], @@ -178,6 +179,7 @@ module.exports = (env, argv) => ({ }, resolve: { alias: { + "@": path.resolve(__dirname, "src/main/js"), // Needed to be able to register helpers at runtime handlebars: "handlebars/runtime", },