Skip to content

Commit

Permalink
Merge pull request #125 from stephenc/jenkins-32007
Browse files Browse the repository at this point in the history
[FIXED JENKINS-32007][FIXED JENKINS-34242] Use a custom control to display spinner and errors
  • Loading branch information
stephenc committed Mar 3, 2017
2 parents ef3ca2f + 2f2a2e1 commit f7546e3
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 17 deletions.
7 changes: 7 additions & 0 deletions pom.xml
Expand Up @@ -137,6 +137,13 @@
<plugin>
<groupId>org.jenkins-ci.tools</groupId>
<artifactId>maven-hpi-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>generate-taglib-interface</goal>
</goals>
</execution>
</executions>
<configuration>
<compatibleSinceVersion>2.0.0</compatibleSinceVersion>
</configuration>
Expand Down
@@ -0,0 +1,36 @@
package org.jenkinsci.plugins.github_branch_source;

import hudson.Util;
import java.io.IOException;
import java.util.Locale;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

// TODO replace with corresponding core functionality once Jenkins core has JENKINS-42443
class FillErrorResponse extends IOException implements HttpResponse {

private final String message;
private final boolean clearList;

public FillErrorResponse(String message, boolean clearList) {
this.message = message;
this.clearList = clearList;
}

@Override
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node)
throws IOException, ServletException {
rsp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
rsp.setContentType("text/html;charset=UTF-8");
rsp.setHeader("X-Jenkins-Select-Error", clearList ? "clear" : "retain");
rsp.getWriter().print(
"<div class=\'error\'><img src=\'" + req.getContextPath()
+ Jenkins.RESOURCE_PATH + "/images/none.gif\' height=16 width=1>" + Util.escape(message) +
"</div>");

}
}
Expand Up @@ -72,6 +72,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletResponse;
import jenkins.plugins.git.AbstractGitSCMSource;
Expand Down Expand Up @@ -1297,12 +1298,11 @@ public ListBoxModel doFillScanCredentialsIdItems(@CheckForNull @AncestorInPath I
}

public ListBoxModel doFillRepositoryItems(@CheckForNull @AncestorInPath Item context, @QueryParameter String apiUri,
@QueryParameter String scanCredentialsId, @QueryParameter String repoOwner) {
Set<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
@QueryParameter String scanCredentialsId, @QueryParameter String repoOwner) throws IOException {

repoOwner = Util.fixEmptyAndTrim(repoOwner);
if (repoOwner == null) {
return nameAndValueModel(result);
return new ListBoxModel();
}
try {
StandardCredentials credentials = Connector.lookupScanCredentials(context, apiUri, scanCredentialsId);
Expand All @@ -1314,14 +1314,23 @@ public ListBoxModel doFillRepositoryItems(@CheckForNull @AncestorInPath Item con
try {
myself = github.getMyself();
} catch (IllegalStateException e) {
LOGGER.log(Level.WARNING, e.getMessage());
LOGGER.log(Level.WARNING, e.getMessage(), e);
throw new FillErrorResponse(e.getMessage(), false);
} catch (IOException e) {
LOGGER.log(Level.WARNING,
"Exception retrieving the repositories of the owner {0} on {1} with credentials {2}",

new Object[]{repoOwner, apiUri, CredentialsNameProvider.name(credentials)});
LogRecord lr = new LogRecord(Level.WARNING,
"Exception retrieving the repositories of the owner {0} on {1} with credentials {2}");
lr.setThrown(e);
lr.setParameters(new Object[]{
repoOwner, apiUri,
credentials == null
? "anonymous access"
: CredentialsNameProvider.name(credentials)
});
LOGGER.log(lr);
throw new FillErrorResponse(e.getMessage(), false);
}
if (myself != null && repoOwner.equalsIgnoreCase(myself.getLogin())) {
Set<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for (String name : myself.getAllRepositories().keySet()) {
result.add(name);
}
Expand All @@ -1335,9 +1344,20 @@ public ListBoxModel doFillRepositoryItems(@CheckForNull @AncestorInPath Item con
} catch (FileNotFoundException fnf) {
LOGGER.log(Level.FINE, "There is not any GH Organization named {0}", repoOwner);
} catch (IOException e) {
LOGGER.log(Level.WARNING, e.getMessage());
LogRecord lr = new LogRecord(Level.WARNING,
"Exception retrieving the repositories of the organization {0} on {1} with credentials {2}");
lr.setThrown(e);
lr.setParameters(new Object[]{
repoOwner, apiUri,
credentials == null
? "anonymous access"
: CredentialsNameProvider.name(credentials)
});
LOGGER.log(lr);
throw new FillErrorResponse(e.getMessage(), false);
}
if (org != null && repoOwner.equalsIgnoreCase(org.getLogin())) {
Set<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
LOGGER.log(Level.FINE, "as {0} looking for repositories in {1}",
new Object[]{scanCredentialsId, repoOwner});
for (GHRepository repo : org.listRepositories(100)) {
Expand All @@ -1356,9 +1376,20 @@ public ListBoxModel doFillRepositoryItems(@CheckForNull @AncestorInPath Item con
} catch (FileNotFoundException fnf) {
LOGGER.log(Level.FINE, "There is not any GH User named {0}", repoOwner);
} catch (IOException e) {
LOGGER.log(Level.WARNING, e.getMessage());
LogRecord lr = new LogRecord(Level.WARNING,
"Exception retrieving the repositories of the user {0} on {1} with credentials {2}");
lr.setThrown(e);
lr.setParameters(new Object[]{
repoOwner, apiUri,
credentials == null
? "anonymous access"
: CredentialsNameProvider.name(credentials)
});
LOGGER.log(lr);
throw new FillErrorResponse(e.getMessage(), false);
}
if (user != null && repoOwner.equalsIgnoreCase(user.getLogin())) {
Set<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for (GHRepository repo : user.listRepositories(100)) {
result.add(repo.getName());
}
Expand All @@ -1367,10 +1398,13 @@ public ListBoxModel doFillRepositoryItems(@CheckForNull @AncestorInPath Item con
} finally {
Connector.release(github);
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getMessage());
} catch (FillErrorResponse e) {
throw e;
} catch (Throwable e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
throw new FillErrorResponse(e.getMessage(), false);
}
return nameAndValueModel(result);
throw new FillErrorResponse(Messages.GitHubSCMSource_NoMatchingOwner(repoOwner), true);
}
/**
* Creates a list box model from a list of values.
Expand Down
@@ -1,5 +1,6 @@
<?jelly escape-by-default='true'?>
<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">
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:c="/lib/credentials" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:f2="/org/jenkinsci/plugins/github_branch_source/form">
<j:if test="${descriptor.apiUriSelectable}">
<f:entry title="${%API endpoint}" field="apiUri">
<f:select/>
Expand All @@ -9,10 +10,11 @@
<f:textbox/>
</f:entry>
<f:entry title="${%Scan credentials}" field="scanCredentialsId">
<f:select/>
<c:select/>
</f:entry>
<f:entry title="${%Repository}" field="repository">
<f:select/>
<!-- TODO switch back to f:select once Jenkins core has JENKINS-42443 -->
<f2:select/>
</f:entry>
<f:advanced>
<j:if test="${!descriptor.apiUriSelectable}">
Expand All @@ -21,7 +23,7 @@
</f:entry>
</j:if>
<f:entry title="${%Checkout credentials}" field="checkoutCredentialsId">
<f:select default="${descriptor.SAME}"/>
<c:select default="${descriptor.SAME}"/>
</f:entry>
<f:entry title="${%Include branches}" field="includes">
<f:textbox default="${descriptor.defaultIncludes}"/>
Expand Down
Expand Up @@ -29,5 +29,6 @@ GitHubSCMSource.DisplayName=GitHub
GitHubSCMSource.Pronoun=Repository
GitHubSCMSource.UncategorizedCategory=Branches
GitHubSCMSource.did_you_mean_to_use_to_match_all_branches=Did you mean to use * to match all branches?
GitHubSCMSource.NoMatchingOwner=Could not find owner: {0}

PullRequestSCMHead.Pronoun=Pull Request
@@ -0,0 +1,60 @@
<!--
The MIT License
Copyright (c) 2004-2017, Sun Microsystems, Inc., Kohsuke Kawaguchi, Bruce Chapman, Alan Harder, CloudBees, Inc.
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.
-->
<!-- TODO remove once Jenkins core has JENKINS-42443 -->
<!-- Tomcat doesn't like us using the attribute called 'class' -->
<?jelly escape-by-default='true'?>
<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">
<st:documentation>
Glorified &lt;select> control that supports the data binding and AJAX updates.
Your descriptor should have the 'doFillXyzItems' method, which returns a ListBoxModel
representation of the items in your drop-down list box, and your instance field should
hold the current value.

<st:attribute name="clazz">
Additional CSS classes that the control gets.
</st:attribute>
<st:attribute name="field">
Used for databinding.
</st:attribute>
<st:attribute name="default">
The default value of the text box, in case both @value is and 'instance[field]' is null.
</st:attribute>
</st:documentation>

<f:prepareDatabinding/>
<st:adjunct includes="org.jenkinsci.plugins.github_branch_source.form.select.select"/>

${descriptor.calcFillSettings(field,attrs)} <!-- this figures out the 'fillUrl' and 'fillDependsOn' attribute -->
<j:set var="value" value="${attrs.value ?: instance[attrs.field] ?: attrs.default}" />
<m:select xmlns:m="jelly:hudson.util.jelly.MorphTagLibrary"
class="setting-input ${attrs.checkUrl!=null?'validated':''} select2 ${attrs.clazz}"
name="${attrs.name ?: '_.'+attrs.field}"
value="${value}"
ATTRIBUTES="${attrs}" EXCEPT="field clazz">
<j:if test="${value!=null}">
<option value="${value}">${value}</option>
</j:if>
</m:select>
<!-- TODO consider customizedFields -->
</j:jelly>
@@ -0,0 +1,11 @@
/* TODO remove once Jenkins core has JENKINS-42443 */
select.select2-loading {
padding-left: 1.5em;
padding-top: 0.5em;
padding-bottom: 0.5em;
color: transparent;
background-image: url("spinner.gif");
background-repeat: no-repeat;
background-position: 2px;
}

@@ -0,0 +1,100 @@
// TODO remove once Jenkins core has JENKINS-42443
// send async request to the given URL (which will send back serialized ListBoxModel object),
// then use the result to fill the list box.
function updateListBox2(listBox,url,config) {
config = config || {};
config = object(config);
var originalOnSuccess = config.onSuccess;
var l = $(listBox);
var status = findFollowingTR(listBox, "validation-error-area").firstChild.nextSibling;
if (status.firstChild && status.firstChild.getAttribute('data-select2')) {
status.innerHTML = "";
}
config.onSuccess = function(rsp) {
l.removeClassName("select2-loading");
var currentSelection = l.value;

// clear the contents
while(l.length>0) l.options[0] = null;

var selectionSet = false; // is the selection forced by the server?
var possibleIndex = null; // if there's a new option that matches the current value, remember its index
var opts = eval('('+rsp.responseText+')').values;
for( var i=0; i<opts.length; i++ ) {
l.options[i] = new Option(opts[i].name,opts[i].value);
if(opts[i].selected) {
l.selectedIndex = i;
selectionSet = true;
}
if (opts[i].value==currentSelection)
possibleIndex = i;
}

// if no value is explicitly selected by the server, try to select the same value
if (!selectionSet && possibleIndex!=null)
l.selectedIndex = possibleIndex;

if (originalOnSuccess!=undefined)
originalOnSuccess(rsp);
};
config.onFailure = function(rsp) {
l.removeClassName("select2-loading");
status.innerHTML = rsp.responseText;
if (status.firstChild) {
status.firstChild.setAttribute('data-select2', 'true')
}
Behaviour.applySubtree(status);
// deleting values can result in the data loss, so let's not do that unless instructed
var header = rsp.getResponseHeader('X-Jenkins-Select-Error');
if (header && "clear".toUpperCase() === header.toUpperCase()) {
for (var i = l.options.length - 1; i >= 0; i--) {
l.remove(i);
}
}

};

l.addClassName("select2-loading");
new Ajax.Request(url, config);
}

Behaviour.specify("SELECT.select2", 'select2', 1000, function(e) {

function hasChanged(selectEl, originalValue) {
var firstValue = selectEl.options[0].value;
var selectedValue = selectEl.value;
if (originalValue == "" && selectedValue == firstValue) {
// There was no value pre-selected but after the call to updateListBox the first value is selected by
// default. This must not be considered a change.
return false;
} else {
return originalValue != selectedValue;
}
};

// controls that this SELECT box depends on
refillOnChange(e,function(params) {
var value = e.value;
updateListBox2(e,e.getAttribute("fillUrl"),{
parameters: params,
onSuccess: function() {
if (value=="") {
// reflect the initial value. if the control depends on several other SELECT.select,
// it may take several updates before we get the right items, which is why all these precautions.
var v = e.getAttribute("value");
if (v) {
e.value = v;
if (e.value==v) e.removeAttribute("value"); // we were able to apply our initial value
}
}

fireEvent(e,"filled"); // let other interested parties know that the items have changed

// if the update changed the current selection, others listening to this control needs to be notified.
if (hasChanged(e, value)) {
fireEvent(e,"change");
}
}
});
});
});
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.

0 comments on commit f7546e3

Please sign in to comment.