diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 6c2d0b7a0e00..179207d4466e 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -36,6 +36,7 @@ import hudson.init.InitMilestone; import hudson.model.AbstractProject; import hudson.model.Action; +import hudson.model.Actionable; import hudson.model.Computer; import hudson.model.Describable; import hudson.model.Descriptor; @@ -167,6 +168,9 @@ import jenkins.model.ModelObjectWithChildren; import jenkins.model.ModelObjectWithContextMenu; import jenkins.model.SimplePageDecorator; +import jenkins.model.details.Detail; +import jenkins.model.details.DetailFactory; +import jenkins.model.details.DetailGroup; import jenkins.util.SystemProperties; import org.apache.commons.jelly.JellyContext; import org.apache.commons.jelly.JellyTagException; @@ -2586,6 +2590,33 @@ public static String generateItemId() { return String.valueOf(Math.floor(Math.random() * 3000)); } + /** + * Returns a grouped list of Detail objects for the given Actionable object + */ + @Restricted(NoExternalUse.class) + public static Map> getDetailsFor(Actionable object) { + ExtensionList groupsExtensionList = ExtensionList.lookup(DetailGroup.class); + List> components = groupsExtensionList.getComponents(); + Map detailGroupOrdinal = components.stream() + .collect(Collectors.toMap( + (k) -> k.getInstance().getClass().getName(), + ExtensionComponent::ordinal + )); + + Map> result = new TreeMap<>(Comparator.comparingDouble(d -> detailGroupOrdinal.get(d.getClass().getName()))); + for (DetailFactory taf : DetailFactory.factoriesFor(object.getClass())) { + List details = taf.createFor(object); + details.forEach(e -> result.computeIfAbsent(e.getGroup(), k -> new ArrayList<>()).add(e)); + } + + for (Map.Entry> entry : result.entrySet()) { + List detailList = entry.getValue(); + detailList.sort(Comparator.comparingInt(Detail::getOrder)); + } + + return result; + } + @Restricted(NoExternalUse.class) public static ExtensionList getSearchFactories() { return SearchFactory.all(); diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index ec7eaabf32c6..24109b979ac4 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -40,6 +40,7 @@ import hudson.AbortException; import hudson.BulkChange; import hudson.EnvVars; +import hudson.Extension; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.FeedAdapter; @@ -120,6 +121,10 @@ import jenkins.model.JenkinsLocationConfiguration; import jenkins.model.RunAction2; import jenkins.model.StandardArtifactManager; +import jenkins.model.details.Detail; +import jenkins.model.details.DetailFactory; +import jenkins.model.details.DurationDetail; +import jenkins.model.details.TimestampDetail; import jenkins.model.lazy.BuildReference; import jenkins.model.lazy.LazyBuildMixIn; import jenkins.security.MasterToSlaveCallable; @@ -2669,4 +2674,17 @@ public void doDynamic(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExcept out.flush(); } } + + @Extension + public static final class BasicRunDetailFactory extends DetailFactory { + + @Override + public Class type() { + return Run.class; + } + + @NonNull @Override public List createFor(@NonNull Run target) { + return List.of(new TimestampDetail(target), new DurationDetail(target)); + } + } } diff --git a/core/src/main/java/jenkins/model/details/Detail.java b/core/src/main/java/jenkins/model/details/Detail.java new file mode 100644 index 000000000000..a8e31e41f13e --- /dev/null +++ b/core/src/main/java/jenkins/model/details/Detail.java @@ -0,0 +1,68 @@ +package jenkins.model.details; + +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.model.Actionable; +import hudson.model.ModelObject; +import hudson.model.Run; +import org.jenkins.ui.icon.IconSpec; + +/** + * {@link Detail} represents a piece of information about a {@link Run}. + * Such information could include: + *
    + *
  • the date and time the run started
  • + *
  • the amount of time the run took to complete
  • + *
  • SCM information for the build
  • + *
  • who kicked the build off
  • + *
+ * @since TODO + */ +public abstract class Detail implements ModelObject, IconSpec { + + private final Actionable object; + + public Detail(Actionable object) { + this.object = object; + } + + public Actionable getObject() { + return object; + } + + /** + * {@inheritDoc} + */ + public @Nullable String getIconClassName() { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nullable String getDisplayName() { + return null; + } + + /** + * Optional URL for the {@link Detail}. + * If provided the detail element will be a link instead of plain text. + */ + public @Nullable String getLink() { + return null; + } + + /** + * @return the grouping of the detail + */ + public DetailGroup getGroup() { + return GeneralDetailGroup.get(); + } + + /** + * @return order in the group, zero is first, MAX_VALUE is any order + */ + public int getOrder() { + return Integer.MAX_VALUE; + } +} diff --git a/core/src/main/java/jenkins/model/details/DetailFactory.java b/core/src/main/java/jenkins/model/details/DetailFactory.java new file mode 100644 index 000000000000..2c1dd7ad57bc --- /dev/null +++ b/core/src/main/java/jenkins/model/details/DetailFactory.java @@ -0,0 +1,57 @@ +/* + * The MIT License + * + * Copyright 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. + */ + +package jenkins.model.details; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.model.Actionable; +import java.util.ArrayList; +import java.util.List; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Allows you to add multiple details to an Actionable object at once. + * @param the type of object to add to; typically an {@link Actionable} subtype + * @since TODO + */ +public abstract class DetailFactory implements ExtensionPoint { + + public abstract Class type(); + + public abstract @NonNull List createFor(@NonNull T target); + + @Restricted(NoExternalUse.class) + public static List> factoriesFor(Class type) { + List> result = new ArrayList<>(); + for (DetailFactory wf : ExtensionList.lookup(DetailFactory.class)) { + if (wf.type().isAssignableFrom(type)) { + result.add(wf); + } + } + return result; + } +} diff --git a/core/src/main/java/jenkins/model/details/DetailGroup.java b/core/src/main/java/jenkins/model/details/DetailGroup.java new file mode 100644 index 000000000000..2840e66ae81b --- /dev/null +++ b/core/src/main/java/jenkins/model/details/DetailGroup.java @@ -0,0 +1,6 @@ +package jenkins.model.details; + +/** + * Represents a group for categorizing {@link Detail} + */ +public abstract class DetailGroup {} diff --git a/core/src/main/java/jenkins/model/details/DurationDetail.java b/core/src/main/java/jenkins/model/details/DurationDetail.java new file mode 100644 index 000000000000..76d466aa26a1 --- /dev/null +++ b/core/src/main/java/jenkins/model/details/DurationDetail.java @@ -0,0 +1,13 @@ +package jenkins.model.details; + +import hudson.model.Run; + +/** + * Displays the duration of the given run, or, if the run has completed, shows the total time it took to execute + */ +public class DurationDetail extends Detail { + + public DurationDetail(Run run) { + super(run); + } +} diff --git a/core/src/main/java/jenkins/model/details/GeneralDetailGroup.java b/core/src/main/java/jenkins/model/details/GeneralDetailGroup.java new file mode 100644 index 000000000000..c59e2fe7e743 --- /dev/null +++ b/core/src/main/java/jenkins/model/details/GeneralDetailGroup.java @@ -0,0 +1,12 @@ +package jenkins.model.details; + +import hudson.Extension; +import hudson.ExtensionList; + +@Extension +public class GeneralDetailGroup extends DetailGroup { + + public static GeneralDetailGroup get() { + return ExtensionList.lookupSingleton(GeneralDetailGroup.class); + } +} diff --git a/core/src/main/java/jenkins/model/details/TimestampDetail.java b/core/src/main/java/jenkins/model/details/TimestampDetail.java new file mode 100644 index 000000000000..fb000089ad92 --- /dev/null +++ b/core/src/main/java/jenkins/model/details/TimestampDetail.java @@ -0,0 +1,13 @@ +package jenkins.model.details; + +import hudson.model.Run; + +/** + * Displays the start time of the given run + */ +public class TimestampDetail extends Detail { + + public TimestampDetail(Run run) { + super(run); + } +} diff --git a/core/src/main/resources/hudson/model/Run/new-build-page.jelly b/core/src/main/resources/hudson/model/Run/new-build-page.jelly index cf1c270163e5..625424723f41 100644 --- a/core/src/main/resources/hudson/model/Run/new-build-page.jelly +++ b/core/src/main/resources/hudson/model/Run/new-build-page.jelly @@ -24,12 +24,14 @@ THE SOFTWARE. --> - + +