Skip to content

Commit

Permalink
Add experimental 'Details' widget for builds (#10147)
Browse files Browse the repository at this point in the history
  • Loading branch information
krisstern authored Feb 17, 2025
2 parents 0d6718a + da4b90a commit eab7102
Show file tree
Hide file tree
Showing 17 changed files with 576 additions and 30 deletions.
31 changes: 31 additions & 0 deletions core/src/main/java/hudson/Functions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<DetailGroup, List<Detail>> getDetailsFor(Actionable object) {
ExtensionList<DetailGroup> groupsExtensionList = ExtensionList.lookup(DetailGroup.class);
List<ExtensionComponent<DetailGroup>> components = groupsExtensionList.getComponents();
Map<String, Double> detailGroupOrdinal = components.stream()
.collect(Collectors.toMap(
(k) -> k.getInstance().getClass().getName(),
ExtensionComponent::ordinal
));

Map<DetailGroup, List<Detail>> result = new TreeMap<>(Comparator.comparingDouble(d -> detailGroupOrdinal.get(d.getClass().getName())));
for (DetailFactory taf : DetailFactory.factoriesFor(object.getClass())) {
List<Detail> details = taf.createFor(object);
details.forEach(e -> result.computeIfAbsent(e.getGroup(), k -> new ArrayList<>()).add(e));
}

for (Map.Entry<DetailGroup, List<Detail>> entry : result.entrySet()) {
List<Detail> detailList = entry.getValue();
detailList.sort(Comparator.comparingInt(Detail::getOrder));
}

return result;
}

@Restricted(NoExternalUse.class)
public static ExtensionList<SearchFactory> getSearchFactories() {
return SearchFactory.all();
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/java/hudson/model/Run.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2669,4 +2674,17 @@ public void doDynamic(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExcept
out.flush();
}
}

@Extension
public static final class BasicRunDetailFactory extends DetailFactory<Run> {

@Override
public Class<Run> type() {
return Run.class;
}

@NonNull @Override public List<? extends Detail> createFor(@NonNull Run target) {
return List.of(new TimestampDetail(target), new DurationDetail(target));
}
}
}
68 changes: 68 additions & 0 deletions core/src/main/java/jenkins/model/details/Detail.java
Original file line number Diff line number Diff line change
@@ -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:
* <ul>
* <li>the date and time the run started</li>
* <li>the amount of time the run took to complete</li>
* <li>SCM information for the build</li>
* <li>who kicked the build off</li>
* </ul>
* @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;
}
}
57 changes: 57 additions & 0 deletions core/src/main/java/jenkins/model/details/DetailFactory.java
Original file line number Diff line number Diff line change
@@ -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 <T> the type of object to add to; typically an {@link Actionable} subtype
* @since TODO
*/
public abstract class DetailFactory<T extends Actionable> implements ExtensionPoint {

public abstract Class<T> type();

public abstract @NonNull List<? extends Detail> createFor(@NonNull T target);

@Restricted(NoExternalUse.class)
public static <T extends Actionable> List<DetailFactory<T>> factoriesFor(Class<T> type) {
List<DetailFactory<T>> result = new ArrayList<>();
for (DetailFactory<T> wf : ExtensionList.lookup(DetailFactory.class)) {
if (wf.type().isAssignableFrom(type)) {
result.add(wf);
}
}
return result;
}
}
6 changes: 6 additions & 0 deletions core/src/main/java/jenkins/model/details/DetailGroup.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package jenkins.model.details;

/**
* Represents a group for categorizing {@link Detail}
*/
public abstract class DetailGroup {}
13 changes: 13 additions & 0 deletions core/src/main/java/jenkins/model/details/DurationDetail.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
12 changes: 12 additions & 0 deletions core/src/main/java/jenkins/model/details/GeneralDetailGroup.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
13 changes: 13 additions & 0 deletions core/src/main/java/jenkins/model/details/TimestampDetail.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
69 changes: 41 additions & 28 deletions core/src/main/resources/hudson/model/Run/new-build-page.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -24,55 +24,68 @@ THE SOFTWARE.
-->

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:i="jelly:fmt">
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:x="jelly:xml">
<l:layout title="${it.fullDisplayName}">
<st:include page="sidepanel.jelly" />

<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
<l:main-panel>
<script src="${resURL}/jsbundles/pages/job.js" type="text/javascript" defer="true" />

<j:set var="controls">
<t:editDescriptionButton permission="${it.UPDATE}"/>
<l:hasPermission permission="${it.UPDATE}">
<st:include page="logKeep.jelly" />
</l:hasPermission>
</j:set>

<t:buildCaption controls="${controls}">${it.displayName} (<i:formatDate value="${it.timestamp.time}" type="both" dateStyle="medium" timeStyle="medium"/>)</t:buildCaption>
<t:buildCaption controls="${controls}">${it.displayName}</t:buildCaption>

<div>
<t:editableDescription permission="${it.UPDATE}" hideButton="true"/>
</div>

<st:include page="console.jelly" from="${h.getConsoleProviderFor(it)}" optional="true" />

<div style="float:right; z-index: 1; position:relative; margin-left: 1em">
<div style="margin-top:1em">
${%startedAgo(it.timestampString)}
</div>
<div>
<j:if test="${it.building}">
${%beingExecuted(it.executor.timestampString)}
</j:if>
<j:if test="${!it.building}">
${%Took} <a href="${rootURL}/${it.parent.url}buildTimeTrend">${it.durationString}</a>
</j:if>
<st:include page="details.jelly" optional="true" />
</div>
</div>
<div class="app-build__grid">
<st:include page="console.jelly" from="${h.getConsoleProviderFor(it)}" optional="true" />
<l:card title="${%Details}">
<div class="jenkins-card__details">
<j:forEach var="group" items="${h.getDetailsFor(it)}" indexVar="index">
<j:if test="${index gt 0}">
<hr />
</j:if>
<j:forEach var="detail" items="${group.value}">
<st:include page="detail.jelly" it="${detail}" optional="true">
<x:element name="${detail.link != null ? 'a' : 'div'}">
<x:attribute name="class">jenkins-card__details__item</x:attribute>
<x:attribute name="href">${detail.link}</x:attribute>
<div class="jenkins-card__details__item__icon">
<l:icon src="${detail.iconClassName}" />
</div>
${detail.displayName}
</x:element>
</st:include>
</j:forEach>
</j:forEach>
</div>
</l:card>

<table>
<t:artifactList build="${it}" caption="${%Build Artifacts}"
permission="${it.ARTIFACTS}" />
<l:card title="Summary">
<div>
<table>
<t:artifactList build="${it}" caption="${%Build Artifacts}" permission="${it.ARTIFACTS}" />

<!-- give actions a chance to contribute summary item -->
<j:forEach var="a" items="${it.allActions}">
<st:include page="summary.jelly" from="${a}" optional="true" it="${a}" />
</j:forEach>
<!-- give actions a chance to contribute summary item -->
<j:forEach var="a" items="${it.allActions}">
<st:include page="summary.jelly" from="${a}" optional="true" it="${a}" />
</j:forEach>

<st:include page="summary.jelly" optional="true" />
</table>
<st:include page="summary.jelly" optional="true" />
</table>

<st:include page="main.jelly" optional="true" />
<st:include page="main.jelly" optional="true" />
</div>
</l:card>
</div>
</l:main-panel>
</l:layout>
</j:jelly>
Loading

0 comments on commit eab7102

Please sign in to comment.