Skip to content

Commit

Permalink
Reimplement the UI for LabelAxis (#213)
Browse files Browse the repository at this point in the history
The `LabelAxis` configuration was using YUI TreeView. As YUI is very old and
deprecated we should remove this. This generates the complete configure page in
Jelly and just add some small JavasCript to show/hide the lists with labels.

Show the description as tooltip instead of adding it in brackets after the
label name. The description can contain HTML, so we need to safeguard it with
the markup formatter.
  • Loading branch information
mawinter69 authored Oct 7, 2024
1 parent 4d7b7bf commit ff91cd7
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 37 deletions.
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>ionicons-api</artifactId>
</dependency>

<dependency>
<groupId>org.jenkins-ci.main</groupId>
Expand Down
64 changes: 43 additions & 21 deletions src/main/java/hudson/matrix/LabelAxis.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@
package hudson.matrix;

import hudson.Extension;
import java.io.IOException;
import java.util.Set;
import jenkins.model.Jenkins;
import hudson.model.labels.LabelAtom;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;

import java.util.ArrayList;
import java.util.List;

import static hudson.Functions.htmlAttributeEscape;
import static hudson.Functions.jsStringEscape;

/**
* {@link Axis} that selects label expressions.
*
Expand All @@ -56,16 +56,9 @@ public String getValueString() {
return String.join("/", getValues());
}

public String getValueStringHtmlEscaped() {
final List<String> values = getValues();
StringBuilder str = new StringBuilder();
for (String value : values) {
if (str.length() > 0) {
str.append('/');
}
str.append(htmlAttributeEscape(value));
}
return str.toString();
@Restricted(NoExternalUse.class)
public boolean isChecked(String name) {
return getValues().contains(name);
}

@Extension
Expand All @@ -85,13 +78,42 @@ public boolean isInstantiable() {
return !j.getNodes().isEmpty() || !j.clouds.isEmpty();
}

public String buildLabelCheckBox(LabelAtom la) {
final String escapedName = jsStringEscape(htmlAttributeEscape(la.getName()));
final String escapedDescription = jsStringEscape(StringUtils.isEmpty(la.getDescription()) ? "" :
htmlAttributeEscape(la.getDescription()));
return "<input type='checkbox' name='values' json='" +
escapedName + "' " + "/><label class='attach-previous'>" + escapedName +
" (" + escapedDescription + ")</label>";
@Restricted(NoExternalUse.class)
public LabelLists getLabelLists() {
return new LabelLists();
}

@Restricted(NoExternalUse.class)
public String getSaveDescription(LabelAtom labelAtom) throws IOException {
// remove line breaks as html tooltip will replace linebreaks with </br>.
// This ensures that the description is displayed in the same way as on the label
return Jenkins.get().getMarkupFormatter().translate(labelAtom.getDescription()).
replaceAll("\r", "").replaceAll("\n", "");
}
}

@Restricted(NoExternalUse.class)
public static class LabelLists {
private List<LabelAtom> machines = new ArrayList<>();
private List<LabelAtom> labels = new ArrayList<>();

public LabelLists() {
Set<LabelAtom> labelsAtoms = Jenkins.get().getLabelAtoms();
labelsAtoms.forEach(atom -> {
if (atom.isSelfLabel()) {
machines.add(atom);
} else {
labels.add(atom);
}
});
}

public List<LabelAtom> getMachines() {
return machines;
}

public List<LabelAtom> getLabels() {
return labels;
}
}
}
50 changes: 39 additions & 11 deletions src/main/resources/hudson/matrix/LabelAxis/config.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,45 @@ THE SOFTWARE.
<f:textbox default="label" />
</f:entry>
<f:entry title="${%Node/Label}" field="labels">
<div class="yahooTree labelAxis-tree" style="border: 1px solid gray; height: 10em; overflow:auto;" values="${instance.valueStringHtmlEscaped}" />
<st:adjunct includes="hudson.matrix.LabelAxis.label-axis-resources"/>
<f:invisibleEntry>
<span class="label-axis-i18n" data-i18n-labels="${%Labels}" data-i18n-individual-nodes="${%Individual nodes}"/>
<div class="label-axis-data-container">
<j:forEach var="l" items="${app.labelAtoms}">
<span data-label-atom="${l}"
data-label-checkbox="${descriptor.buildLabelCheckBox(l)}"
data-label="${l.isSelfLabel()?'machines':'labels'}"/>
</j:forEach>
<div class="jenkins-input" >
<j:set var="labelLists" value="${descriptor.labelLists}"/>
<j:set var="machines" value="${labelLists.machines}"/>
<j:set var="labels" value="${labelLists.labels}"/>
<div class="mp-label-axis">
<j:if test="${labels.size() gt 0}">
<div class="mp-label-axis__container">
<div class="jenkins-form-label">${%Labels}
<button type="button" class="jenkins-button mp-label-axis__button" data-hidden="true">
<l:icon src="symbol-chevron-down-outline plugin-ionicons-api" class="icon-sm"/>
</button>
</div>
<div class="jenkins-!-margin-left-3 mp-label-axis__list jenkins-hidden">
<j:forEach var="l" items="${labels}">
<div data-html-tooltip="${descriptor.getSaveDescription(l)}" class="mp-label-axis__tooltip">
<f:checkbox name="values" title="${l.name}" checked="${instance.isChecked(l.name)}" json="${l.name}"/>
</div>
</j:forEach>
</div>
</div>
</j:if>
<j:if test="${machines.size() gt 0}">
<div class="mp-label-axis__container">
<div class="jenkins-form-label">${%Individual Agents}
<button type="button" class="jenkins-button mp-label-axis__button" data-hidden="true">
<l:icon src="symbol-chevron-down-outline plugin-ionicons-api" class="icon-sm"/>
</button>
</div>
<div class="jenkins-!-margin-left-3 mp-label-axis__list jenkins-hidden">
<j:forEach var="l" items="${machines}">
<div data-html-tooltip="${descriptor.getSaveDescription(l)}" class="mp-label-axis__tooltip">
<f:checkbox name="values" title="${l.name}" checked="${instance.isChecked(l.name)}" json="${l.name}"/>
</div>
</j:forEach>
</div>
</div>
</j:if>
</div>
</f:invisibleEntry>
</div>
</f:entry>
<st:adjunct includes="hudson.matrix.LabelAxis.label-axis"/>
</j:jelly>
27 changes: 27 additions & 0 deletions src/main/resources/hudson/matrix/LabelAxis/label-axis.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.mp-label-axis__button {
min-height: 20px;
height: 20px;
padding: 0.5rem 0.3rem;
}

.mp-label-axis__button[data-hidden=false] {
rotate: 180deg;
}

.mp-label-axis__container > .jenkins-form-label {
display:flex;
gap: 5px;
align-items: center;
}

.mp-label-axis {
display: flex;
gap: 10px;
flex-direction: column;
max-height: 300px;
overflow: auto;
}

.mp-label-axis__tooltip {
width: fit-content;
}
16 changes: 16 additions & 0 deletions src/main/resources/hudson/matrix/LabelAxis/label-axis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Behaviour.specify(".mp-label-axis__button", "mp-label-container", 0, function(btn) {
btn.addEventListener("click", function(evt) {
const container = btn.closest(".mp-label-axis__container");
if (container) {
const labelList = container.querySelector(".mp-label-axis__list");
if (labelList) {
labelList.classList.toggle("jenkins-hidden");
if (btn.dataset.hidden === "true") {
btn.dataset.hidden = "false";
} else {
btn.dataset.hidden = "true";
}
}
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,6 @@ THE SOFTWARE.
<p:config-trigger/>

<f:section title="${%Configuration Matrix}">
<!-- Needed for LabelAxis, but including these from LabelAxis/config.jelly is too late -->
<l:yui module="treeview" />
<link rel="stylesheet" type="text/css"
href="${rootURL}/scripts/yui/treeview/assets/skins/sam/treeview.css" />

<f:block>
<f:hetero-list name="axis" hasHeader="true"
descriptors="${descriptor.axisDescriptors}"
Expand Down

0 comments on commit ff91cd7

Please sign in to comment.