Skip to content

Commit eab7102

Browse files
authored
Add experimental 'Details' widget for builds (#10147)
2 parents 0d6718a + da4b90a commit eab7102

File tree

17 files changed

+576
-30
lines changed

17 files changed

+576
-30
lines changed

core/src/main/java/hudson/Functions.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import hudson.init.InitMilestone;
3737
import hudson.model.AbstractProject;
3838
import hudson.model.Action;
39+
import hudson.model.Actionable;
3940
import hudson.model.Computer;
4041
import hudson.model.Describable;
4142
import hudson.model.Descriptor;
@@ -167,6 +168,9 @@
167168
import jenkins.model.ModelObjectWithChildren;
168169
import jenkins.model.ModelObjectWithContextMenu;
169170
import jenkins.model.SimplePageDecorator;
171+
import jenkins.model.details.Detail;
172+
import jenkins.model.details.DetailFactory;
173+
import jenkins.model.details.DetailGroup;
170174
import jenkins.util.SystemProperties;
171175
import org.apache.commons.jelly.JellyContext;
172176
import org.apache.commons.jelly.JellyTagException;
@@ -2586,6 +2590,33 @@ public static String generateItemId() {
25862590
return String.valueOf(Math.floor(Math.random() * 3000));
25872591
}
25882592

2593+
/**
2594+
* Returns a grouped list of Detail objects for the given Actionable object
2595+
*/
2596+
@Restricted(NoExternalUse.class)
2597+
public static Map<DetailGroup, List<Detail>> getDetailsFor(Actionable object) {
2598+
ExtensionList<DetailGroup> groupsExtensionList = ExtensionList.lookup(DetailGroup.class);
2599+
List<ExtensionComponent<DetailGroup>> components = groupsExtensionList.getComponents();
2600+
Map<String, Double> detailGroupOrdinal = components.stream()
2601+
.collect(Collectors.toMap(
2602+
(k) -> k.getInstance().getClass().getName(),
2603+
ExtensionComponent::ordinal
2604+
));
2605+
2606+
Map<DetailGroup, List<Detail>> result = new TreeMap<>(Comparator.comparingDouble(d -> detailGroupOrdinal.get(d.getClass().getName())));
2607+
for (DetailFactory taf : DetailFactory.factoriesFor(object.getClass())) {
2608+
List<Detail> details = taf.createFor(object);
2609+
details.forEach(e -> result.computeIfAbsent(e.getGroup(), k -> new ArrayList<>()).add(e));
2610+
}
2611+
2612+
for (Map.Entry<DetailGroup, List<Detail>> entry : result.entrySet()) {
2613+
List<Detail> detailList = entry.getValue();
2614+
detailList.sort(Comparator.comparingInt(Detail::getOrder));
2615+
}
2616+
2617+
return result;
2618+
}
2619+
25892620
@Restricted(NoExternalUse.class)
25902621
public static ExtensionList<SearchFactory> getSearchFactories() {
25912622
return SearchFactory.all();

core/src/main/java/hudson/model/Run.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import hudson.AbortException;
4141
import hudson.BulkChange;
4242
import hudson.EnvVars;
43+
import hudson.Extension;
4344
import hudson.ExtensionList;
4445
import hudson.ExtensionPoint;
4546
import hudson.FeedAdapter;
@@ -120,6 +121,10 @@
120121
import jenkins.model.JenkinsLocationConfiguration;
121122
import jenkins.model.RunAction2;
122123
import jenkins.model.StandardArtifactManager;
124+
import jenkins.model.details.Detail;
125+
import jenkins.model.details.DetailFactory;
126+
import jenkins.model.details.DurationDetail;
127+
import jenkins.model.details.TimestampDetail;
123128
import jenkins.model.lazy.BuildReference;
124129
import jenkins.model.lazy.LazyBuildMixIn;
125130
import jenkins.security.MasterToSlaveCallable;
@@ -2669,4 +2674,17 @@ public void doDynamic(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExcept
26692674
out.flush();
26702675
}
26712676
}
2677+
2678+
@Extension
2679+
public static final class BasicRunDetailFactory extends DetailFactory<Run> {
2680+
2681+
@Override
2682+
public Class<Run> type() {
2683+
return Run.class;
2684+
}
2685+
2686+
@NonNull @Override public List<? extends Detail> createFor(@NonNull Run target) {
2687+
return List.of(new TimestampDetail(target), new DurationDetail(target));
2688+
}
2689+
}
26722690
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package jenkins.model.details;
2+
3+
import edu.umd.cs.findbugs.annotations.Nullable;
4+
import hudson.model.Actionable;
5+
import hudson.model.ModelObject;
6+
import hudson.model.Run;
7+
import org.jenkins.ui.icon.IconSpec;
8+
9+
/**
10+
* {@link Detail} represents a piece of information about a {@link Run}.
11+
* Such information could include:
12+
* <ul>
13+
* <li>the date and time the run started</li>
14+
* <li>the amount of time the run took to complete</li>
15+
* <li>SCM information for the build</li>
16+
* <li>who kicked the build off</li>
17+
* </ul>
18+
* @since TODO
19+
*/
20+
public abstract class Detail implements ModelObject, IconSpec {
21+
22+
private final Actionable object;
23+
24+
public Detail(Actionable object) {
25+
this.object = object;
26+
}
27+
28+
public Actionable getObject() {
29+
return object;
30+
}
31+
32+
/**
33+
* {@inheritDoc}
34+
*/
35+
public @Nullable String getIconClassName() {
36+
return null;
37+
}
38+
39+
/**
40+
* {@inheritDoc}
41+
*/
42+
@Override
43+
public @Nullable String getDisplayName() {
44+
return null;
45+
}
46+
47+
/**
48+
* Optional URL for the {@link Detail}.
49+
* If provided the detail element will be a link instead of plain text.
50+
*/
51+
public @Nullable String getLink() {
52+
return null;
53+
}
54+
55+
/**
56+
* @return the grouping of the detail
57+
*/
58+
public DetailGroup getGroup() {
59+
return GeneralDetailGroup.get();
60+
}
61+
62+
/**
63+
* @return order in the group, zero is first, MAX_VALUE is any order
64+
*/
65+
public int getOrder() {
66+
return Integer.MAX_VALUE;
67+
}
68+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright 2025 Jan Faracik
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package jenkins.model.details;
26+
27+
import edu.umd.cs.findbugs.annotations.NonNull;
28+
import hudson.ExtensionList;
29+
import hudson.ExtensionPoint;
30+
import hudson.model.Actionable;
31+
import java.util.ArrayList;
32+
import java.util.List;
33+
import org.kohsuke.accmod.Restricted;
34+
import org.kohsuke.accmod.restrictions.NoExternalUse;
35+
36+
/**
37+
* Allows you to add multiple details to an Actionable object at once.
38+
* @param <T> the type of object to add to; typically an {@link Actionable} subtype
39+
* @since TODO
40+
*/
41+
public abstract class DetailFactory<T extends Actionable> implements ExtensionPoint {
42+
43+
public abstract Class<T> type();
44+
45+
public abstract @NonNull List<? extends Detail> createFor(@NonNull T target);
46+
47+
@Restricted(NoExternalUse.class)
48+
public static <T extends Actionable> List<DetailFactory<T>> factoriesFor(Class<T> type) {
49+
List<DetailFactory<T>> result = new ArrayList<>();
50+
for (DetailFactory<T> wf : ExtensionList.lookup(DetailFactory.class)) {
51+
if (wf.type().isAssignableFrom(type)) {
52+
result.add(wf);
53+
}
54+
}
55+
return result;
56+
}
57+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package jenkins.model.details;
2+
3+
/**
4+
* Represents a group for categorizing {@link Detail}
5+
*/
6+
public abstract class DetailGroup {}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package jenkins.model.details;
2+
3+
import hudson.model.Run;
4+
5+
/**
6+
* Displays the duration of the given run, or, if the run has completed, shows the total time it took to execute
7+
*/
8+
public class DurationDetail extends Detail {
9+
10+
public DurationDetail(Run<?, ?> run) {
11+
super(run);
12+
}
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package jenkins.model.details;
2+
3+
import hudson.Extension;
4+
import hudson.ExtensionList;
5+
6+
@Extension
7+
public class GeneralDetailGroup extends DetailGroup {
8+
9+
public static GeneralDetailGroup get() {
10+
return ExtensionList.lookupSingleton(GeneralDetailGroup.class);
11+
}
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package jenkins.model.details;
2+
3+
import hudson.model.Run;
4+
5+
/**
6+
* Displays the start time of the given run
7+
*/
8+
public class TimestampDetail extends Detail {
9+
10+
public TimestampDetail(Run<?, ?> run) {
11+
super(run);
12+
}
13+
}

core/src/main/resources/hudson/model/Run/new-build-page.jelly

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,55 +24,68 @@ THE SOFTWARE.
2424
-->
2525

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

3131
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
3232
<l:main-panel>
33+
<script src="${resURL}/jsbundles/pages/job.js" type="text/javascript" defer="true" />
34+
3335
<j:set var="controls">
3436
<t:editDescriptionButton permission="${it.UPDATE}"/>
3537
<l:hasPermission permission="${it.UPDATE}">
3638
<st:include page="logKeep.jelly" />
3739
</l:hasPermission>
3840
</j:set>
3941

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

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

46-
<st:include page="console.jelly" from="${h.getConsoleProviderFor(it)}" optional="true" />
47-
48-
<div style="float:right; z-index: 1; position:relative; margin-left: 1em">
49-
<div style="margin-top:1em">
50-
${%startedAgo(it.timestampString)}
51-
</div>
52-
<div>
53-
<j:if test="${it.building}">
54-
${%beingExecuted(it.executor.timestampString)}
55-
</j:if>
56-
<j:if test="${!it.building}">
57-
${%Took} <a href="${rootURL}/${it.parent.url}buildTimeTrend">${it.durationString}</a>
58-
</j:if>
59-
<st:include page="details.jelly" optional="true" />
60-
</div>
61-
</div>
48+
<div class="app-build__grid">
49+
<st:include page="console.jelly" from="${h.getConsoleProviderFor(it)}" optional="true" />
50+
<l:card title="${%Details}">
51+
<div class="jenkins-card__details">
52+
<j:forEach var="group" items="${h.getDetailsFor(it)}" indexVar="index">
53+
<j:if test="${index gt 0}">
54+
<hr />
55+
</j:if>
56+
<j:forEach var="detail" items="${group.value}">
57+
<st:include page="detail.jelly" it="${detail}" optional="true">
58+
<x:element name="${detail.link != null ? 'a' : 'div'}">
59+
<x:attribute name="class">jenkins-card__details__item</x:attribute>
60+
<x:attribute name="href">${detail.link}</x:attribute>
61+
<div class="jenkins-card__details__item__icon">
62+
<l:icon src="${detail.iconClassName}" />
63+
</div>
64+
${detail.displayName}
65+
</x:element>
66+
</st:include>
67+
</j:forEach>
68+
</j:forEach>
69+
</div>
70+
</l:card>
6271

63-
<table>
64-
<t:artifactList build="${it}" caption="${%Build Artifacts}"
65-
permission="${it.ARTIFACTS}" />
72+
<l:card title="Summary">
73+
<div>
74+
<table>
75+
<t:artifactList build="${it}" caption="${%Build Artifacts}" permission="${it.ARTIFACTS}" />
6676

67-
<!-- give actions a chance to contribute summary item -->
68-
<j:forEach var="a" items="${it.allActions}">
69-
<st:include page="summary.jelly" from="${a}" optional="true" it="${a}" />
70-
</j:forEach>
77+
<!-- give actions a chance to contribute summary item -->
78+
<j:forEach var="a" items="${it.allActions}">
79+
<st:include page="summary.jelly" from="${a}" optional="true" it="${a}" />
80+
</j:forEach>
7181

72-
<st:include page="summary.jelly" optional="true" />
73-
</table>
82+
<st:include page="summary.jelly" optional="true" />
83+
</table>
7484

75-
<st:include page="main.jelly" optional="true" />
85+
<st:include page="main.jelly" optional="true" />
86+
</div>
87+
</l:card>
88+
</div>
7689
</l:main-panel>
7790
</l:layout>
7891
</j:jelly>

0 commit comments

Comments
 (0)