Skip to content

Commit 8c385f4

Browse files
janfaraciktimja
andauthored
Add experimental dashboard UI (#11208)
Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com>
1 parent 68b2695 commit 8c385f4

File tree

13 files changed

+413
-83
lines changed

13 files changed

+413
-83
lines changed

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
import javax.xml.transform.sax.SAXSource;
9090
import javax.xml.transform.stream.StreamResult;
9191
import javax.xml.transform.stream.StreamSource;
92+
import jenkins.model.Badgeable;
9293
import jenkins.model.Jenkins;
9394
import jenkins.model.ModelObjectWithChildren;
9495
import jenkins.model.ModelObjectWithContextMenu;
@@ -144,7 +145,7 @@
144145
* @see ViewGroup
145146
*/
146147
@ExportedBean
147-
public abstract class View extends AbstractModelObject implements AccessControlled, Describable<View>, ExtensionPoint, Saveable, ModelObjectWithChildren, DescriptorByNameOwner, HasWidgets {
148+
public abstract class View extends AbstractModelObject implements AccessControlled, Describable<View>, ExtensionPoint, Saveable, ModelObjectWithChildren, DescriptorByNameOwner, HasWidgets, Badgeable {
148149

149150
/**
150151
* Container of this view. Set right after the construction
@@ -368,6 +369,19 @@ public String getDisplayName() {
368369
return getViewName();
369370
}
370371

372+
/**
373+
* Returns the icon file name for this action.
374+
* <p>
375+
* Only displays if {@link jenkins.model.experimentalflags.NewDashboardPageUserExperimentalFlag} is enabled.
376+
* <p>
377+
* This behaves similarly to {@link Action#getIconFileName()}, except that
378+
* returning {@code null} here does not hide the associated view; the view
379+
* will still be displayed even when this method returns {@code null}.
380+
*/
381+
public String getIconFileName() {
382+
return null;
383+
}
384+
371385
public String getNewPronoun() {
372386
return AlternativeUiTextProvider.get(NEW_PRONOUN, this, Messages.AbstractItem_Pronoun());
373387
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 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.experimentalflags;
26+
27+
import edu.umd.cs.findbugs.annotations.Nullable;
28+
import hudson.Extension;
29+
import org.kohsuke.accmod.Restricted;
30+
import org.kohsuke.accmod.restrictions.NoExternalUse;
31+
32+
@Extension
33+
@Restricted(NoExternalUse.class)
34+
public class NewDashboardPageUserExperimentalFlag extends BooleanUserExperimentalFlag {
35+
public NewDashboardPageUserExperimentalFlag() {
36+
super("new-dashboard-page.flag");
37+
}
38+
39+
@Override
40+
public String getDisplayName() {
41+
return "New dashboard page";
42+
}
43+
44+
@Nullable
45+
@Override
46+
public String getShortDescription() {
47+
return "Enables a revamped dashboard page. This feature is still a work in progress, so some things might not work perfectly yet.";
48+
}
49+
}

core/src/main/resources/hudson/model/View/index.jelly

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,24 @@ THE SOFTWARE.
2424

2525
<?jelly escape-by-default='true'?>
2626
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
27-
<l:layout title="${(it.class.name=='hudson.model.AllView' and it.ownerItemGroup == app) ? '%Dashboard' : it.displayName}${not empty it.ownerItemGroup.fullDisplayName?' ['+it.ownerItemGroup.fullDisplayName+']':''}">
28-
<j:set var="view" value="${it}"/> <!-- expose view to the scripts we include from owner -->
27+
<l:userExperimentalFlag var="newDashboardPage" flagClassName="jenkins.model.experimentalflags.NewDashboardPageUserExperimentalFlag" />
28+
29+
<j:choose>
30+
<j:when test="${newDashboardPage}">
31+
<st:include page="new-view-page.jelly" />
32+
</j:when>
33+
<j:otherwise>
34+
<l:layout title="${(it.class.name=='hudson.model.AllView' and it.ownerItemGroup == app) ? '%Dashboard' : it.displayName}${not empty it.ownerItemGroup.fullDisplayName?' ['+it.ownerItemGroup.fullDisplayName+']':''}">
35+
<j:set var="view" value="${it}"/> <!-- expose view to the scripts we include from owner -->
2936
<st:include page="sidepanel.jelly" />
3037
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
3138
<l:main-panel>
3239
<st:include page="view-index-top.jelly" it="${it.owner}" optional="true">
3340
<!-- allow the owner to take over the top section, but we also need the default to be backward compatible -->
3441
<div id="view-message">
35-
<div id="systemmessage">
36-
<j:out value="${app.systemMessage!=null ? app.markupFormatter.translate(app.systemMessage) : ''}" />
37-
</div>
42+
<div id="systemmessage">
43+
<j:out value="${app.systemMessage!=null ? app.markupFormatter.translate(app.systemMessage) : ''}" />
44+
</div>
3845
<t:editableDescription permission="${it.CONFIGURE}"/>
3946
</div>
4047
</st:include>
@@ -47,5 +54,7 @@ THE SOFTWARE.
4754
<script id="screenResolution-script" data-use-secure-cookie="${request2.secure}"/>
4855
<st:adjunct includes="hudson.model.View.screen-resolution"/>
4956
</l:header>
50-
</l:layout>
57+
</l:layout>
58+
</j:otherwise>
59+
</j:choose>
5160
</j:jelly>

core/src/main/resources/hudson/model/View/main.groovy

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?jelly escape-by-default='true'?>
2+
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:t="/lib/hudson" xmlns:l="/lib/layout" xmlns:d="jelly:define" xmlns:local="local">
3+
<l:userExperimentalFlag var="newDashboardPage" flagClassName="jenkins.model.experimentalflags.NewDashboardPageUserExperimentalFlag" />
4+
5+
<d:taglib uri="local">
6+
<d:tag name="view">
7+
<j:choose>
8+
<j:when test="${items == null}">
9+
<p>${%broken}</p>
10+
</j:when>
11+
12+
<j:when test="${items.isEmpty()}">
13+
<j:if test="${!app.items.isEmpty()}">
14+
<j:set var="views" value="${it.owner.views}" />
15+
<j:set var="currentView" value="${it}" />
16+
17+
<j:if test="${!newDashboardPage}">
18+
<j:choose>
19+
<j:when test="${it.owner.class == hudson.model.MyViewsProperty.class}">
20+
<st:include it="${it.owner.viewsTabBar}" page="viewTabs" />
21+
</j:when>
22+
<j:otherwise>
23+
<st:include it="${it.owner.userViewsTabBar}" page="viewTabs" />
24+
</j:otherwise>
25+
</j:choose>
26+
</j:if>
27+
</j:if>
28+
<st:include it="${it}" page="noJob.jelly" />
29+
</j:when>
30+
31+
<j:otherwise>
32+
<t:projectView
33+
jobs="${items}"
34+
showViewTabs="true"
35+
columnExtensions="${it.columns}"
36+
indenter="${it.indenter}"
37+
itemGroup="${it.owner.itemGroup}">
38+
39+
<j:set var="views" value="${it.owner.views}" />
40+
<j:set var="currentView" value="${it}" />
41+
42+
<j:if test="${!newDashboardPage}">
43+
<j:choose>
44+
<j:when test="${it.owner.class == hudson.model.MyViewsProperty.class}">
45+
<st:include it="${it.owner.viewsTabBar}" page="viewTabs" />
46+
</j:when>
47+
<j:otherwise>
48+
<st:include it="${it.owner.userViewsTabBar}" page="viewTabs" />
49+
</j:otherwise>
50+
</j:choose>
51+
</j:if>
52+
</t:projectView>
53+
</j:otherwise>
54+
</j:choose>
55+
</d:tag>
56+
</d:taglib>
57+
58+
<j:choose>
59+
<j:when test="${newDashboardPage}">
60+
<div class="jenkins-inline-page">
61+
<div class="jenkins-inline-page__side-panel">
62+
<j:if test="${h.hasPermission(app.READ)}">
63+
<j:forEach var="w" items="${it.widgets}">
64+
<j:set var="view" value="${it}" />
65+
<st:include it="${w}" page="index.jelly" />
66+
</j:forEach>
67+
</j:if>
68+
</div>
69+
<div>
70+
<local:view />
71+
</div>
72+
</div>
73+
</j:when>
74+
<j:otherwise>
75+
<local:view />
76+
</j:otherwise>
77+
</j:choose>
78+
</j:jelly>
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<!--
2+
The MIT License
3+
4+
Copyright (c) 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+
<?jelly escape-by-default='true'?>
26+
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"
27+
xmlns:l="/lib/layout" xmlns:dd="/lib/layout/dropdowns" xmlns:t="/lib/hudson">
28+
<l:layout title="${(it.class.name=='hudson.model.AllView' and it.ownerItemGroup == app) ? '%Dashboard' : it.displayName}${not empty it.ownerItemGroup.fullDisplayName ? ' - ' + it.ownerItemGroup.fullDisplayName : ''}">
29+
<l:header>
30+
<!-- for screen resolution detection -->
31+
<script id="screenResolution-script" data-use-secure-cookie="${request2.secure}"/>
32+
<st:adjunct includes="hudson.model.View.screen-resolution"/>
33+
</l:header>
34+
35+
<j:set var="view" value="${it}"/> <!-- expose view to the scripts we include from owner -->
36+
<l:main-panel>
37+
<div class="app-build-bar">
38+
<div class="app-build-bar__content">
39+
<div class="app-build-bar__content__headline">
40+
<j:set var="iconClass" value="${it.owner.iconColor.getIconClassName()}"/>
41+
<j:if test="${iconClass != null}">
42+
<j:set var="iconClass" value="${iconClass} icon-lg"/>
43+
</j:if>
44+
<j:choose>
45+
<j:when test="${iconClass != null}">
46+
<l:icon class="${iconClass}" alt="${it.owner.iconColor.description}" tooltip="${it.owner.iconColor.description}" />
47+
</j:when>
48+
<j:when test="${it.owner.iconColor.getImageOf('32x32') != null}">
49+
<l:icon src="${it.owner.iconColor.getImageOf('32x32')}" alt="${it.owner.iconColor.description}" tooltip="${it.owner.iconColor.description}" />
50+
</j:when>
51+
</j:choose>
52+
53+
<h1>${it.ownerItemGroup == app ? '%Dashboard' : it.owner == app ? it.displayName : it.owner.displayName}</h1>
54+
</div>
55+
</div>
56+
57+
<j:set var="currentView" value="${it}" />
58+
<j:set var="views" value="${it.owner.views}" />
59+
60+
<div class="app-build-bar__tabs">
61+
<j:choose>
62+
<j:when test="${view.class.name eq 'hudson.model.MyViewsProperty'}">
63+
<st:include page="viewTabs" it="${view.owner.userViewsTabBar}" />
64+
</j:when>
65+
<j:otherwise>
66+
<st:include page="viewTabs" it="${view.owner.viewsTabBar}" />
67+
</j:otherwise>
68+
</j:choose>
69+
</div>
70+
71+
<div class="app-build-bar__controls">
72+
<j:if test="${it.hasPermission(it.CREATE)}">
73+
<a class="jenkins-button"
74+
href="${rootURL}/${it.viewUrl}newJob">
75+
<l:icon src="symbol-add" />
76+
New Item
77+
</a>
78+
</j:if>
79+
80+
<j:if test="${it.isEditable() and it.hasPermission(it.CONFIGURE)}">
81+
<!-- /configure URL on Jenkins object is overloaded with Jenkins's system config, so always use the explicit name. -->
82+
<a class="jenkins-button"
83+
href="${rootURL}/${it.viewUrl}configure">
84+
${%Edit View}
85+
</a>
86+
</j:if>
87+
88+
<l:dialog title="${%Legend}" hash="legend">
89+
<div>
90+
<st:include page="_legend.jelly" it="${app}" />
91+
</div>
92+
</l:dialog>
93+
94+
<l:overflowButton>
95+
<dd:custom>
96+
<div class="app-build-overflow">
97+
<j:set var="mode" value="side-panel" />
98+
<st:include page="sidepanel.jelly" it="${it.object}" />
99+
</div>
100+
</dd:custom>
101+
<dd:separator />
102+
<dd:submenu icon="symbol-rss" text="${%Atom feed}">
103+
<dd:item icon="symbol-rss" text="${%All}" href="rssAll" />
104+
<dd:item icon="symbol-rss" text="${%Failures}" href="rssFailed" />
105+
<dd:item icon="symbol-rss" text="${%LatestBuilds}" href="rssLatest" />
106+
</dd:submenu>
107+
<dd:custom>
108+
<button class="jenkins-dropdown__item"
109+
data-type="dialog-opener"
110+
data-dialog-id="${dialogId}">
111+
<div class="jenkins-dropdown__item__icon">
112+
<l:icon src="symbol-information-circle" />
113+
</div>
114+
${%Legend}
115+
</button>
116+
</dd:custom>
117+
<j:if test="${it.owner.canDelete(it) and it.hasPermission(it.DELETE)}">
118+
<dd:separator />
119+
<dd:custom>
120+
<l:confirmationLink class="jenkins-dropdown__item jenkins-!-destructive-color"
121+
href="doDelete"
122+
title="todo"
123+
message="${%delete.logrecorder(it.displayName)}"
124+
destructive="true"
125+
post="true">
126+
<div class="jenkins-dropdown__item__icon">
127+
<l:icon src="symbol-trash" />
128+
</div>
129+
${%Delete View}
130+
</l:confirmationLink>
131+
</dd:custom>
132+
</j:if>
133+
</l:overflowButton>
134+
</div>
135+
</div>
136+
137+
<div class="app-build-content" style="padding-top: var(--section-padding)">
138+
<t:editableDescription permission="${it.CONFIGURE}" hideButton="true" />
139+
140+
<j:set var="items" value="${it.items}"/>
141+
<st:include page="main.jelly" />
142+
</div>
143+
</l:main-panel>
144+
</l:layout>
145+
</j:jelly>

0 commit comments

Comments
 (0)