Skip to content

Commit

Permalink
Feature/advanced view control (#1)
Browse files Browse the repository at this point in the history
* max number of columns extended from 8 to 12

* Extend 'ByFullName' with OrdinalSet to gain advanced control on job order

* Show Ordinal-Set config section only if 'ByFullName' is selected

* Show Ordinal-Set config section only if 'ByFullName' is selected

* revert: we do not need to extend to 12 columns, but we will introduce another formatting type which will use '.break' class

* first implementation of group lanes: refine

* revert some changes

* improve method name

* revert some changes

* Bugfix: make sure that multi-hierarchy jobs are picking the top level job as title

* Add screenshot to document additional features

* increment version and handle review findings

* revert version

* fix css

* clusterTitle with font-size: 100%

Co-authored-by: Carlo Giorgetta (tzhgica1) <[email protected]>
  • Loading branch information
cgiorgetta and Carlo Giorgetta (tzhgica1) authored Oct 1, 2020
1 parent 36f8e82 commit d9e6eb4
Show file tree
Hide file tree
Showing 16 changed files with 384 additions and 57 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Simple, right? :-) You can have as many Build Monitor Views as you want - the mo
![Three columns view](docs/2_Three_columns_view.png)
![Supports Claim and Build Failure Analyzer plugins](docs/3_Two_columns_view_with_claim_and_build_failure_analyzer_plugins.png)
![Colour-blind mode](docs/4_Colour_blind_mode.png)
![Job Clustering and Advanced_Ordering](docs/5_Job_clustering_and_advanced_ordering.png)

## Roadmap and work in progress

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.api.Respond;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.facade.StaticJenkinsAPIs;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.installation.BuildMonitorInstallation;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.order.ByFullName;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.order.ByFullName.OrdinalSet;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.JobView;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.JobViews;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.ClusterTitleJobView;
import hudson.Extension;
import hudson.model.Descriptor.FormException;
import hudson.model.Job;
Expand Down Expand Up @@ -86,11 +89,25 @@ public String currentOrder() {
return currentConfig().getOrder().getClass().getSimpleName();
}

@SuppressWarnings("unused") // used in the configure-entries.jelly form
public String currentOrdinalSet() {
if (currentConfig().getOrder() instanceof ByFullName) {
return ((ByFullName) currentConfig().getOrder()).getOrdinalSet().toParameter();
} else {
return "";
}
}

@SuppressWarnings("unused") // used in the configure-entries.jelly form
public String currentbuildFailureAnalyzerDisplayedField() {
return currentConfig().getBuildFailureAnalyzerDisplayedField().getValue();
}

@SuppressWarnings("unused") // used in the configure-entries.jelly form
public boolean isDisplayClusterTitle() {
return currentConfig().shouldDisplayClusterTitle();
}

@SuppressWarnings("unused") // used in the configure-entries.jelly form
public boolean isDisplayCommitters() {
return currentConfig().shouldDisplayCommitters();
Expand Down Expand Up @@ -123,11 +140,17 @@ protected void submit(StaplerRequest req) throws ServletException, IOException,
String requestedOrdering = req.getParameter("order");
title = req.getParameter("title");

currentConfig().setDisplayClusterTitle(json.optBoolean("displayClusterTitle", true));
currentConfig().setDisplayCommitters(json.optBoolean("displayCommitters", true));
currentConfig().setBuildFailureAnalyzerDisplayedField(req.getParameter("buildFailureAnalyzerDisplayedField"));

try {
currentConfig().setOrder(orderIn(requestedOrdering));
Comparator<Job<?, ?>> jobComparator = orderIn(requestedOrdering);
if (jobComparator instanceof ByFullName) {
OrdinalSet ordinalSet = OrdinalSet.fromParameter(req.getParameter("ordinalSet"));
((ByFullName) jobComparator).setOrdinalSet(ordinalSet);
}
currentConfig().setOrder(jobComparator);
} catch (Exception e) {
throw new FormException("Can't order projects by " + requestedOrdering, "order");
}
Expand Down Expand Up @@ -161,13 +184,27 @@ private List<JobView> jobViews() {

Collections.sort(projects, currentConfig().getOrder());

String currentClusterTitle = null;
for (Job project : projects) {
jobs.add(views.viewOf(project));
JobView job = views.viewOf(project);
if (config.shouldDisplayClusterTitle() && !getClusterTitle(project).equals(currentClusterTitle)) {
jobs.add(ClusterTitleJobView.create(project.getParent()));
currentClusterTitle = getClusterTitle(project);
}
jobs.add(job);
}

return jobs;
}

/**
* @return job title which consists of the name of the top level folder.
*/
private static String getClusterTitle(Job job) {
String fullName = job.getFullName();
return fullName.substring(0, fullName.indexOf('/') + 1);
}

/**
* When Jenkins is started up, Jenkins::loadTasks is called.
* At that point config.xml file is unmarshaled into a Jenkins object containing a list of Views, including BuildMonitorView objects.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

public class Config {

private boolean displayClusterTitle;
private boolean displayCommitters;
private BuildFailureAnalyzerDisplayedField buildFailureAnalyzerDisplayedField;

Expand Down Expand Up @@ -40,7 +41,15 @@ public BuildFailureAnalyzerDisplayedField getBuildFailureAnalyzerDisplayedField(
public void setBuildFailureAnalyzerDisplayedField(String buildFailureAnalyzerDisplayedField) {
this.buildFailureAnalyzerDisplayedField = BuildFailureAnalyzerDisplayedField.valueOf(buildFailureAnalyzerDisplayedField);
}


public boolean shouldDisplayClusterTitle() {
return getOrElse(displayClusterTitle, false);
}

public void setDisplayClusterTitle(boolean flag) {
this.displayClusterTitle = flag;
}

public boolean shouldDisplayCommitters() {
return getOrElse(displayCommitters, true);
}
Expand Down Expand Up @@ -75,4 +84,4 @@ public enum BuildFailureAnalyzerDisplayedField {
}

private Comparator<Job<?, ?>> order;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,62 @@
import hudson.model.Job;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class ByFullName implements Comparator<Job<?, ?>>, Serializable {

private OrdinalSet ordinalSet;

public OrdinalSet getOrdinalSet() {
return ordinalSet;
}

public void setOrdinalSet(OrdinalSet ordinalSet) {
this.ordinalSet = ordinalSet;
}

@Override
public int compare(Job<?, ?> a, Job<?, ?> b) {
if (ordinalSet != null) {
return ordinalSet.expandWithOrdinalNumber(a.getFullName()).compareToIgnoreCase(
ordinalSet.expandWithOrdinalNumber(b.getFullName())
);
}
return a.getFullName().compareToIgnoreCase(b.getFullName());
}

public static class OrdinalSet {
private final List<String> entries;

public OrdinalSet(List<String> entries) {
this.entries = entries;
}

public List<String> getEntries() {
return entries;
}

String expandWithOrdinalNumber(String name) {
if (entries != null) {
for(int i = 0; i< entries.size(); i++) {
name = name.replaceAll(entries.get(i), "#" + i + "_" + entries.get(i));
}
}
return name;
}

public static OrdinalSet fromParameter(String ordinalSetParam) {
List<String> entries = Arrays.stream(ordinalSetParam.split("<"))
.map(String::trim)
.collect(Collectors.toList());
return new OrdinalSet(entries);
}

public String toParameter() {
return String.join(" < ", entries);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel;

import com.cloudbees.hudson.plugins.folder.Folder;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.BuildMonitorLogger;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.facade.RelativeLocation;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.installation.BuildMonitorBuildProperties;
import hudson.model.FreeStyleProject;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Job;

import java.util.Collections;
import java.util.Date;

public class ClusterTitleJobView extends JobView {

private static final BuildMonitorLogger logger = BuildMonitorLogger.forClass(BuildMonitorBuildProperties.class);

private String url;

public static ClusterTitleJobView create(ItemGroup parent) {
ItemGroup topLevelItem = getTopLevelItem(parent);
Job clusterTitle = new FreeStyleProject(topLevelItem, "");
String url = topLevelItem instanceof Folder ? ((Folder) topLevelItem).getShortUrl() : topLevelItem.getUrl();
return new ClusterTitleJobView(clusterTitle, url);
}

private static ItemGroup getTopLevelItem(ItemGroup job) {
ItemGroup ig = job;
while(ig instanceof Item) {
ItemGroup parent = ((Item) ig).getParent();
if (!(parent instanceof Item)) { // root found
return ig;
}
ig = parent;
}
logger.warning("constructor", "Build Monitor could not find top level item of {0}", job.getFullName());
return ig;
}

private ClusterTitleJobView(Job<?, ?> clusterTitle, String url) {
super(clusterTitle, Collections.emptyList(), false, RelativeLocation.of(clusterTitle), new Date(), false);
this.url = url;
}

@Override
public String url() {
return url;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import hudson.model.Result;
import hudson.model.Run;
import hudson.util.RunList;

import org.codehaus.jackson.map.annotate.JsonSerialize;

import java.util.Date;
Expand All @@ -29,19 +28,22 @@ public class JobView {
private final Job<?, ?> job;
private final boolean isPipelineJob;
private final RelativeLocation relative;
private final boolean showShortenedJobName;

private final List<Feature> features = newArrayList();

public static JobView of(Job<?, ?> job, List<Feature> features, boolean isPipelineJob) {
return new JobView(job, features, isPipelineJob, RelativeLocation.of(job), new Date());
public static JobView of(Job<?, ?> job, List<Feature> features, boolean isPipelineJob, boolean showShortenedJobName) {
return new JobView(job, features, isPipelineJob, RelativeLocation.of(job), new Date(), showShortenedJobName);
}

@SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "systemTime is non-critical and no security should be compromised by mutating")
public JobView(Job<?, ?> job, List<Feature> features, boolean isPipelineJob, RelativeLocation relative, Date systemTime) {
public JobView(Job<?, ?> job, List<Feature> features, boolean isPipelineJob, RelativeLocation relative, Date systemTime,
boolean showShortenedJobName) {
this.job = job;
this.isPipelineJob = isPipelineJob;
this.relative = relative;
this.systemTime = systemTime;
this.showShortenedJobName = showShortenedJobName;

for (Feature feature : features) {
this.features.add(feature.of(this));
Expand All @@ -63,7 +65,11 @@ public <F extends Feature> F which(Class<F> requestedFeature) {
}

public String name() {
return relative.name();
String name = relative.name();
if (showShortenedJobName) {
name = name.substring(name.indexOf('»') + 1);
}
return name;
}

public String url() {
Expand Down Expand Up @@ -162,4 +168,4 @@ private BuildViewModel buildViewOf(Run<?, ?> build) {

return BuildView.of(build, isPipelineJob, relative, systemTime);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class JobViewSerialiser extends JsonSerializer<JobView> {
public void serialize(JobView job, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStartObject();
jgen.writeObjectField("name", job.name());
jgen.writeObjectField("isClusterTitle", job instanceof ClusterTitleJobView);
jgen.writeObjectField("url", job.url());
jgen.writeObjectField("status", job.status());
jgen.writeObjectField("hashCode", job.hashCode());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public JobView viewOf(Job<?, ?> job) {

boolean isPipelineJob = jenkins.hasPlugin(Pipeline) && job instanceof WorkflowJob;

return JobView.of(job, viewFeatures, isPipelineJob);
return JobView.of(job, viewFeatures, isPipelineJob, config.shouldDisplayClusterTitle());
}

private boolean hasGroovyPostbuildActionClass() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
</f:entry>

<f:entry title="${%Ordered by}">
<select name="order" class="setting-input">
<select name="order" id="orderSelection" class="setting-input" onchange="adjustVisibilityOfOrdinalSet()">
<f:option value="ByName" selected='${it.currentOrder()=="ByName"}'>${%Name}</f:option>
<f:option value="ByDisplayName" selected="${it.currentOrder()=='ByDisplayName'}">${%Display name}</f:option>
<f:option value="ByFullName" selected="${it.currentOrder()=='ByFullName'}">${%Full name}</f:option>
Expand All @@ -68,6 +68,14 @@
</select>
</f:entry>

<f:entry title="${%Advanced order with ordinal set}" help="${descriptor.getHelpFile('ordinalSet')}">
<f:textbox name="ordinalSet" value="${it.currentOrdinalSet()}"/>
</f:entry>

<f:entry title="${%Display title bar to job clusters}" help="${descriptor.getHelpFile('displayClusterTitle')}">
<f:checkbox id="displayClusterTitle" field="displayClusterTitle" />
</f:entry>

</f:section>

<f:section title="${%Build Monitor - Widget Settings}">
Expand Down Expand Up @@ -97,6 +105,18 @@
}
});
}());

function adjustVisibilityOfOrdinalSet() {
var selectedOrder = $('orderSelection').value;
var ordinalSetRow = $$('input[name="ordinalSet"]').first().parentNode.parentNode;
if (selectedOrder === 'ByFullName') {
ordinalSetRow.show();
} else {
ordinalSetRow.hide();
}
}

adjustVisibilityOfOrdinalSet();
</script>

</j:jelly>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div>
<p>
If checked, then a title bar is displayed at the top of a job cluster, i.e. jobs sharing same root folder.
The title bar contains the name of the root folder, whereas the jobs would only display their job names,
without any hierarchy.
</p>
</div>
Loading

0 comments on commit d9e6eb4

Please sign in to comment.