mirror of https://github.com/jenkinsci/jenkins.git
				
				
				
			Add experimental 'Parameters' detail for runs (#11116)
This commit is contained in:
		
						commit
						1b0b8bfff7
					
				|  | @ -48,6 +48,7 @@ import java.util.TreeSet; | |||
| import java.util.logging.Level; | ||||
| import java.util.logging.Logger; | ||||
| import jenkins.model.RunAction2; | ||||
| import jenkins.model.experimentalflags.NewBuildPageUserExperimentalFlag; | ||||
| import jenkins.util.SystemProperties; | ||||
| import org.kohsuke.accmod.Restricted; | ||||
| import org.kohsuke.accmod.restrictions.NoExternalUse; | ||||
|  | @ -208,6 +209,12 @@ public class ParametersAction implements RunAction2, Iterable<ParameterValue>, Q | |||
| 
 | ||||
|     @Override | ||||
|     public String getIconFileName() { | ||||
|         Boolean newBuildPageEnabled = new NewBuildPageUserExperimentalFlag().getFlagValue(); | ||||
| 
 | ||||
|         if (newBuildPageEnabled) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         return "symbol-parameters"; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -29,6 +29,8 @@ import static jakarta.servlet.http.HttpServletResponse.SC_CONFLICT; | |||
| import static jakarta.servlet.http.HttpServletResponse.SC_CREATED; | ||||
| 
 | ||||
| import edu.umd.cs.findbugs.annotations.CheckForNull; | ||||
| import edu.umd.cs.findbugs.annotations.NonNull; | ||||
| import hudson.Extension; | ||||
| import hudson.Util; | ||||
| import hudson.cli.declarative.CLIMethod; | ||||
| import hudson.cli.declarative.CLIResolver; | ||||
|  | @ -60,6 +62,9 @@ import java.util.Arrays; | |||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| import jenkins.model.details.Detail; | ||||
| import jenkins.model.details.DetailFactory; | ||||
| import jenkins.model.details.ParameterizedDetail; | ||||
| import jenkins.model.lazy.LazyBuildMixIn; | ||||
| import jenkins.security.stapler.StaplerNotDispatchable; | ||||
| import jenkins.triggers.SCMTriggerItem; | ||||
|  | @ -561,6 +566,25 @@ public abstract class ParameterizedJobMixIn<JobT extends Job<JobT, RunT> & Param | |||
|             return !isDisabled() && !((Job) this).isHoldOffBuildUntilSave(); | ||||
|         } | ||||
| 
 | ||||
|         @Extension | ||||
|         final class ParameterizedDetailFactory extends DetailFactory<Run> { | ||||
| 
 | ||||
|             @Override | ||||
|             public Class<Run> type() { | ||||
|                 return Run.class; | ||||
|             } | ||||
| 
 | ||||
|             @NonNull | ||||
|             @Override public List<? extends Detail> createFor(@NonNull Run target) { | ||||
|                 var action = target.getAction(ParametersAction.class); | ||||
| 
 | ||||
|                 if (action == null || action.getParameters().isEmpty()) { | ||||
|                     return List.of(); | ||||
|                 } | ||||
| 
 | ||||
|                 return List.of(new ParameterizedDetail(target)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,27 @@ | |||
| package jenkins.model.details; | ||||
| 
 | ||||
| import edu.umd.cs.findbugs.annotations.Nullable; | ||||
| import hudson.model.ParametersAction; | ||||
| import hudson.model.Run; | ||||
| 
 | ||||
| /** | ||||
|  * Displays if a run has parameters | ||||
|  */ | ||||
| public class ParameterizedDetail extends Detail { | ||||
| 
 | ||||
|     public final ParametersAction action; | ||||
| 
 | ||||
|     public ParameterizedDetail(Run<?, ?> run) { | ||||
|         super(run); | ||||
|         this.action = getObject().getAction(ParametersAction.class); | ||||
|     } | ||||
| 
 | ||||
|     public @Nullable String getIconClassName() { | ||||
|         return "symbol-parameters"; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public @Nullable String getDisplayName() { | ||||
|         return action.getDisplayName(); | ||||
|     } | ||||
| } | ||||
|  | @ -27,8 +27,8 @@ THE SOFTWARE. | |||
| 	xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" | ||||
| 	xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> | ||||
|     <j:set var="escapeEntryTitleAndDescription" value="false"/> | ||||
|     <f:entry description="${it.formattedDescription}"> | ||||
|     <j:set var="readOnlyMode" value="true"/> | ||||
|       <f:checkbox title="${h.escape(it.name)}" name="value" checked="${it.value}"/> | ||||
|     </f:entry> | ||||
|     <div> | ||||
|       <f:checkbox title="${h.escape(it.name)}" name="value" checked="${it.value}" description="${it.formattedDescription}" /> | ||||
|     </div> | ||||
| </j:jelly> | ||||
|  |  | |||
|  | @ -28,7 +28,9 @@ THE SOFTWARE. | |||
|          xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> | ||||
|   <j:set var="escapeEntryTitleAndDescription" value="false"/> | ||||
|   <f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}"> | ||||
|     <j:set var="readOnlyMode" value="true"/> | ||||
|     <f:textbox name="value" value="${it.value}"/> | ||||
|     <div class="jenkins-quote jenkins-quote--full-width jenkins-quote--monospace" id="example"> | ||||
|       ${it.value} | ||||
|       <l:copyButton text="${it.value}" iconOnly="true" /> | ||||
|     </div> | ||||
|   </f:entry> | ||||
| </j:jelly> | ||||
|  | @ -0,0 +1,52 @@ | |||
| <!-- | ||||
| The MIT License | ||||
| 
 | ||||
| Copyright (c) 2025 Jan Faracik | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
| 
 | ||||
| The above copyright notice and this permission notice shall be included in | ||||
| all copies or substantial portions of the Software. | ||||
| 
 | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||
| THE SOFTWARE. | ||||
| --> | ||||
| 
 | ||||
| <?jelly escape-by-default='true'?> | ||||
| <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout"> | ||||
|   <l:dialog title="${it.displayName}" hash="parameters"> | ||||
|     <div class="app-parameters-dialog"> | ||||
|       <j:invokeStatic var="currentThread" className="java.lang.Thread" method="currentThread" /> | ||||
|       <j:invoke var="buildClass" on="${currentThread.contextClassLoader}" method="loadClass"> | ||||
|         <j:arg value="hudson.model.Run" /> | ||||
|       </j:invoke> | ||||
|       <j:set var="build" value="${request2.findAncestorObject(buildClass)}" /> | ||||
|       <j:set var="escapeEntryTitleAndDescription" value="true" /> <!-- SECURITY-353 defense unless overridden --> | ||||
|       <j:set var="readOnlyMode" value="true"/> | ||||
|       <j:forEach var="parameterValue" items="${it.action.parameters}"> | ||||
|         <st:include it="${parameterValue}" page="value.jelly" /> | ||||
|       </j:forEach> | ||||
|     </div> | ||||
|   </l:dialog> | ||||
| 
 | ||||
|   <button class="jenkins-details__item" | ||||
|           data-type="dialog-opener" | ||||
|           data-dialog-id="${dialogId}"> | ||||
|     <div class="jenkins-details__item__icon"> | ||||
|       <l:icon src="${it.iconClassName}" /> | ||||
|     </div> | ||||
|     <span> | ||||
|       ${it.displayName} | ||||
|     </span> | ||||
|   </button> | ||||
| </j:jelly> | ||||
|  | @ -0,0 +1,48 @@ | |||
| <!-- | ||||
| The MIT License | ||||
| 
 | ||||
| Copyright (c) 2025 Jan Faracik | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
| 
 | ||||
| The above copyright notice and this permission notice shall be included in | ||||
| all copies or substantial portions of the Software. | ||||
| 
 | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||
| THE SOFTWARE. | ||||
| --> | ||||
| <?jelly escape-by-default='true'?> | ||||
| <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"> | ||||
|   <st:documentation> | ||||
|     <st:attribute name="title" use="required"> | ||||
|       The title for the dialog. | ||||
|     </st:attribute> | ||||
|     <st:attribute name="hash"> | ||||
|       An optional hash for the dialog. When provided, the dialog can be opened and navigated | ||||
|       directly using its unique URL hash. | ||||
|     </st:attribute> | ||||
| 
 | ||||
|     A lazy-loaded dialog. Set 'data-type="dialog-opener"' and 'data-dialog-id="${dialogId}"' | ||||
|     on your button to open the dialog. | ||||
|   </st:documentation> | ||||
| 
 | ||||
|   <j:set var="dialogId" value="${h.generateId()}" scope="parent" /> | ||||
| 
 | ||||
|   <l:renderOnDemand clazz="dialog-${dialogId}-template dialog-${attrs.hash}-hash" tag="template" capture="it,dialogId"> | ||||
|     <l:ajax> | ||||
|       <template id="dialog-${dialogId}-template" data-title="${attrs.title}" data-dialog-hash="${attrs.hash}"> | ||||
|         <d:invokeBody /> | ||||
|       </template> | ||||
|     </l:ajax> | ||||
|   </l:renderOnDemand> | ||||
| </j:jelly> | ||||
|  | @ -189,6 +189,13 @@ Dialog.prototype.show = function () { | |||
|       (e) => { | ||||
|         e.preventDefault(); | ||||
| 
 | ||||
|         // Clear any hash
 | ||||
|         history.pushState( | ||||
|           "", | ||||
|           document.title, | ||||
|           window.location.pathname + window.location.search, | ||||
|         ); | ||||
| 
 | ||||
|         this.dialog.setAttribute("closing", ""); | ||||
| 
 | ||||
|         this.dialog.addEventListener( | ||||
|  | @ -233,6 +240,34 @@ Dialog.prototype.show = function () { | |||
|   }); | ||||
| }; | ||||
| 
 | ||||
| function renderOnDemandDialog(dialogId) { | ||||
|   const templateId = "dialog-" + dialogId + "-template"; | ||||
| 
 | ||||
|   function render() { | ||||
|     const template = document.querySelector("#" + templateId); | ||||
|     const title = template.dataset.title; | ||||
|     const hash = template.dataset.dialogHash; | ||||
|     const content = template.content.firstElementChild.cloneNode(true); | ||||
| 
 | ||||
|     if (hash) { | ||||
|       window.location.hash = hash; | ||||
|     } | ||||
| 
 | ||||
|     behaviorShim.applySubtree(content, false); | ||||
|     dialog.modal(content, { | ||||
|       maxWidth: "550px", | ||||
|       title: title, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   if (document.querySelector("#" + templateId)) { | ||||
|     render(); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   renderOnDemand(document.querySelector("." + templateId), render); | ||||
| } | ||||
| 
 | ||||
| function init() { | ||||
|   window.dialog = { | ||||
|     modal: function (content, options) { | ||||
|  | @ -292,6 +327,29 @@ function init() { | |||
|       return dialog.show(); | ||||
|     }, | ||||
|   }; | ||||
| 
 | ||||
|   behaviorShim.specify( | ||||
|     "[data-type='dialog-opener']", | ||||
|     "-dialog-", | ||||
|     1000, | ||||
|     (element) => { | ||||
|       element.addEventListener("click", () => { | ||||
|         renderOnDemandDialog(element.dataset.dialogId); | ||||
|       }); | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   // Open the relevant dialog if the hash is set
 | ||||
|   if (window.location.hash) { | ||||
|     const element = document.querySelector( | ||||
|       ".dialog-" + window.location.hash.substring(1) + "-hash", | ||||
|     ); | ||||
|     if (element) { | ||||
|       renderOnDemandDialog( | ||||
|         element.className.match(/dialog-(id\d+)-template/)[1], | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default { init }; | ||||
|  |  | |||
|  | @ -1,16 +1,33 @@ | |||
| @use "../abstracts/mixins"; | ||||
| 
 | ||||
| $icon-size: 1.125rem; | ||||
| 
 | ||||
| .jenkins-details { | ||||
|   display: flex; | ||||
|   gap: 0.75rem 1.25rem; | ||||
|   align-items: center; | ||||
|   gap: 0.625rem 1.25rem; | ||||
|   flex-wrap: wrap; | ||||
| 
 | ||||
|   button.jenkins-details__item, | ||||
|   a.jenkins-details__item { | ||||
|     @include mixins.item($border: false); | ||||
| 
 | ||||
|     &::before, | ||||
|     &::after { | ||||
|       inset: 0 -0.5rem; | ||||
|       pointer-events: all; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &__item { | ||||
|     display: grid; | ||||
|     grid-template-columns: auto 1fr; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     gap: 0.5rem; | ||||
|     font-weight: normal; | ||||
|     color: var(--text-color-secondary); | ||||
|     color: var(--text-color-secondary) !important; | ||||
|     padding: 0; | ||||
|     min-height: 2rem; | ||||
| 
 | ||||
|     &__icon { | ||||
|       display: flex; | ||||
|  | @ -18,7 +35,7 @@ $icon-size: 1.125rem; | |||
|       justify-content: center; | ||||
|       align-self: start; | ||||
|       width: $icon-size; | ||||
|       height: 1lh; | ||||
|       height: 2rem; | ||||
| 
 | ||||
|       svg { | ||||
|         width: $icon-size; | ||||
|  | @ -34,5 +51,6 @@ $icon-size: 1.125rem; | |||
|   &__separator { | ||||
|     color: var(--text-color-secondary); | ||||
|     opacity: 0.5; | ||||
|     margin-inline: -0.5rem; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| $jenkins-dialog-padding: 1.5rem; | ||||
| $jenkins-dialog-font-size: 0.9375rem; | ||||
| $jenkins-dialog-padding: 1.25rem; | ||||
| 
 | ||||
| .jenkins-dialog { | ||||
|   border-radius: 1rem; | ||||
|  | @ -25,12 +24,13 @@ $jenkins-dialog-font-size: 0.9375rem; | |||
|   } | ||||
| 
 | ||||
|   &__title { | ||||
|     font-size: 1.125rem; | ||||
|     font-size: 1rem; | ||||
|     font-weight: var(--font-bold-weight); | ||||
|     padding: 0 $jenkins-dialog-padding; | ||||
|     color: var(--text-color); | ||||
|     overflow-wrap: anywhere; | ||||
|     text-box: cap alphabetic; | ||||
|     margin-top: 0.5625rem; | ||||
|   } | ||||
| 
 | ||||
|   &__contents { | ||||
|  | @ -38,7 +38,6 @@ $jenkins-dialog-font-size: 0.9375rem; | |||
|     overflow-wrap: break-word; | ||||
|     padding: 0 $jenkins-dialog-padding; | ||||
|     max-height: 75vh; | ||||
|     font-size: $jenkins-dialog-font-size; | ||||
|     color: var(--text-color-secondary); | ||||
| 
 | ||||
|     &--modal { | ||||
|  | @ -72,7 +71,7 @@ $jenkins-dialog-font-size: 0.9375rem; | |||
|   } | ||||
| 
 | ||||
|   &__subtitle { | ||||
|     font-size: 1rem; | ||||
|     font-size: var(--font-size-sm); | ||||
|     font-weight: var(--font-bold-weight); | ||||
|     color: var(--text-color-secondary); | ||||
|     padding: 0; | ||||
|  | @ -81,10 +80,11 @@ $jenkins-dialog-font-size: 0.9375rem; | |||
| 
 | ||||
|   &__close-button { | ||||
|     position: absolute; | ||||
|     top: $jenkins-dialog-padding - 0.5rem; | ||||
|     right: $jenkins-dialog-padding - 0.5rem; | ||||
|     aspect-ratio: 1; | ||||
|     top: $jenkins-dialog-padding; | ||||
|     right: $jenkins-dialog-padding; | ||||
|     padding: 0; | ||||
|     width: 2rem; | ||||
|     min-height: 2rem; | ||||
|     border-radius: 100%; | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -87,8 +87,19 @@ | |||
|     border-radius: calc(var(--form-input-border-radius) - 0.25rem); | ||||
|   } | ||||
| 
 | ||||
|   &--full-width { | ||||
|     display: grid; | ||||
|     grid-template-columns: 1fr auto; | ||||
|   } | ||||
| 
 | ||||
|   &--monospace { | ||||
|     font-family: var(--font-family-mono); | ||||
| 
 | ||||
|     // Copy buttons' tooltips inherit font-family from the parent element, | ||||
|     // so override it | ||||
|     span { | ||||
|       font-family: var(--font-family-sans); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| @use "../base/breakpoints"; | ||||
| @use "../components/dialogs"; | ||||
| 
 | ||||
| .build-caption-progress-container { | ||||
|   display: flex; | ||||
|  | @ -107,3 +108,13 @@ | |||
|     font-size: var(--font-size-sm); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .app-parameters-dialog { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: dialogs.$jenkins-dialog-padding; | ||||
| 
 | ||||
|   & > * { | ||||
|     margin-bottom: 0; | ||||
|   } | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue