提交 e1ea7ddb 编写于 作者: A Alex Lehmann

Merge branch 'master' of github.com:alexlehm/jenkins

......@@ -57,10 +57,16 @@ Upcoming changes</a>
<ul class=image>
<li class=bug>
Fixed NPE in Groovy script execution via CLI (<a href="https://issues.jenkins-ci.org/browse/JENKINS-12302">issue 12302</a>)
<li class=rfe>
Supported programmatic retrieval/update of slave <tt>config.xml</tt>
<li class=rfe>
Breadcrumb now supports drop-down menu for faster navigation
(<a href="https://groups.google.com/forum/#!topic/jenkinsci-dev/j9uCKnQB-Xw/discussion">discussion</a>)
<li class=rfe>
Configuration pages show a navigation drop-down menu in the breadcrumb bar to jump to sections
<li class=rfe>
Hyperlinks to model objects also supports drop-down menu for faster navigation.
(<a href="https://groups.google.com/forum/#!topic/jenkinsci-dev/j9uCKnQB-Xw/discussion">discussion</a>)
</ul>
</div><!--=TRUNK-END=-->
......
......@@ -262,6 +262,14 @@ public abstract class ExtensionFinder implements ExtensionPoint {
sezpozIndex = loadSezpozIndices(Jenkins.getInstance().getPluginManager().uberClassLoader);
List<Module> modules = new ArrayList<Module>();
modules.add(new AbstractModule() {
@Override
protected void configure() {
Jenkins j = Jenkins.getInstance();
bind(Jenkins.class).toInstance(j);
bind(PluginManager.class).toInstance(j.getPluginManager());
}
});
modules.add(new SezpozModule(sezpozIndex));
for (ExtensionComponent<Module> ec : moduleFinder.find(Module.class, Hudson.getInstance())) {
......
......@@ -1173,6 +1173,24 @@ public class Functions {
return base;
}
/**
* Combine path components via '/' while handling leading/trailing '/' to avoid duplicates.
*/
public static String joinPath(String... components) {
StringBuilder buf = new StringBuilder();
for (String s : components) {
if (s.length()==0) continue;
if (buf.length()>0) {
if (buf.charAt(buf.length()-1)!='/')
buf.append('/');
if (s.charAt(0)=='/') s=s.substring(1);
}
buf.append(s);
}
return buf.toString();
}
/**
* Computes the hyperlink to actions, to handle the situation when the {@link Action#getUrlName()}
* returns absolute URL.
......@@ -1183,10 +1201,10 @@ public class Functions {
if(SCHEME.matcher(urlName).find())
return urlName; // absolute URL
if(urlName.startsWith("/"))
return Stapler.getCurrentRequest().getContextPath()+urlName;
return joinPath(Stapler.getCurrentRequest().getContextPath(),urlName);
else
// relative URL name
return Stapler.getCurrentRequest().getContextPath()+'/'+itUrl+urlName;
return joinPath(Stapler.getCurrentRequest().getContextPath(),itUrl,urlName);
}
/**
......
......@@ -54,7 +54,7 @@ import java.net.MalformedURLException;
* @author Kohsuke Kawaguchi
*/
@Extension
public class GroovyCommand extends CLICommand implements Serializable {
public class GroovyCommand extends CLICommand {
@Override
public String getShortDescription() {
return Messages.GroovyCommand_ShortDescription();
......
......@@ -44,6 +44,7 @@ import org.kohsuke.args4j.ClassParser;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.CmdLineException;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
......@@ -66,6 +67,9 @@ import java.util.logging.Logger;
*/
@Extension
public class CLIRegisterer extends ExtensionFinder {
@Inject
Jenkins jenkins;
@Override
public ExtensionComponentSet refresh() throws ExtensionRefreshException {
// TODO: this is not complex. just bit tedious.
......@@ -74,7 +78,7 @@ public class CLIRegisterer extends ExtensionFinder {
public <T> Collection<ExtensionComponent<T>> find(Class<T> type, Hudson jenkins) {
if (type==CLICommand.class)
return (List)discover(jenkins);
return (List)discover();
else
return Collections.emptyList();
}
......@@ -83,7 +87,7 @@ public class CLIRegisterer extends ExtensionFinder {
* Finds a resolved method annotated with {@link CLIResolver}.
*/
private Method findResolver(Class type) throws IOException {
List<Method> resolvers = Util.filter(Index.list(CLIResolver.class, Jenkins.getInstance().getPluginManager().uberClassLoader), Method.class);
List<Method> resolvers = Util.filter(Index.list(CLIResolver.class, jenkins.getPluginManager().uberClassLoader), Method.class);
for ( ; type!=null; type=type.getSuperclass())
for (Method m : resolvers)
if (m.getReturnType()==type)
......@@ -91,12 +95,12 @@ public class CLIRegisterer extends ExtensionFinder {
return null;
}
private List<ExtensionComponent<CLICommand>> discover(final Jenkins hudson) {
private List<ExtensionComponent<CLICommand>> discover() {
LOGGER.fine("Listing up @CLIMethod");
List<ExtensionComponent<CLICommand>> r = new ArrayList<ExtensionComponent<CLICommand>>();
try {
for ( final Method m : Util.filter(Index.list(CLIMethod.class, hudson.getPluginManager().uberClassLoader),Method.class)) {
for ( final Method m : Util.filter(Index.list(CLIMethod.class, jenkins.getPluginManager().uberClassLoader),Method.class)) {
try {
// command name
final String name = m.getAnnotation(CLIMethod.class).name();
......@@ -150,7 +154,7 @@ public class CLIRegisterer extends ExtensionFinder {
binders.add(new MethodBinder(chains.pop(),this,parser));
// authentication
CliAuthenticator authenticator = Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this);
CliAuthenticator authenticator = jenkins.getSecurityRealm().createCliAuthenticator(this);
new ClassParser().parse(authenticator,parser);
// fill up all the binders
......@@ -160,7 +164,7 @@ public class CLIRegisterer extends ExtensionFinder {
if (auth== Jenkins.ANONYMOUS)
auth = loadStoredAuthentication();
sc.setAuthentication(auth); // run the CLI with the right credential
hudson.checkPermission(Jenkins.READ);
jenkins.checkPermission(Jenkins.READ);
// resolve them
Object instance = null;
......
......@@ -64,6 +64,7 @@ import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.args4j.Option;
......@@ -87,6 +88,8 @@ import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.Inet4Address;
import static javax.servlet.http.HttpServletResponse.*;
/**
* Represents the running state of a remote computer that holds {@link Executor}s.
*
......@@ -1069,27 +1072,57 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
String name = Util.fixEmptyAndTrim(req.getSubmittedForm().getString("name"));
Jenkins.checkGoodName(name);
final Jenkins app = Jenkins.getInstance();
Node result = getNode().reconfigure(req, req.getSubmittedForm());
replaceBy(result);
// take the user back to the slave top page.
rsp.sendRedirect2("../"+result.getNodeName()+'/');
}
/**
* Accepts <tt>config.xml</tt> submission, as well as serve it.
*/
@WebMethod(name = "config.xml")
public void doConfigDotXml(StaplerRequest req, StaplerResponse rsp)
throws IOException, ServletException {
checkPermission(Jenkins.ADMINISTER);
if (req.getMethod().equals("GET")) {
// read
rsp.setContentType("application/xml");
Jenkins.XSTREAM2.toXML(getNode(), rsp.getOutputStream());
return;
}
if (req.getMethod().equals("POST")) {
// submission
Node result = (Node)Jenkins.XSTREAM2.fromXML(req.getReader());
replaceBy(result);
return;
}
// huh?
rsp.sendError(SC_BAD_REQUEST);
}
/**
* Replaces the current {@link Node} by another one.
*/
private void replaceBy(Node newNode) throws ServletException, IOException {
final Jenkins app = Jenkins.getInstance();
// replace the old Node object by the new one
synchronized (app) {
List<Node> nodes = new ArrayList<Node>(app.getNodes());
int i = nodes.indexOf(getNode());
if(i<0) {
sendError("This slave appears to be removed while you were editing the configuration",req,rsp);
return;
throw new IOException("This slave appears to be removed while you were editing the configuration");
}
nodes.set(i,result);
nodes.set(i, newNode);
app.setNodes(nodes);
}
// take the user back to the slave top page.
rsp.sendRedirect2("../"+result.getNodeName()+'/');
}
/**
* Really deletes the slave.
*/
......
......@@ -255,7 +255,7 @@ public class Queue extends ResourceController implements Saveable {
public void setLoadBalancer(LoadBalancer loadBalancer) {
if(loadBalancer==null) throw new IllegalArgumentException();
this.loadBalancer = loadBalancer;
this.loadBalancer = loadBalancer.sanitize();
}
public QueueSorter getSorter() {
......
......@@ -35,7 +35,7 @@ THE SOFTWARE.
<img src="${imagesURL}/24x24/grey.png" tooltip="${%Not run}" alt="${%Not run}" height="24" width="24"/>
</j:when>
<j:otherwise>
<a href="${p.shortUrl}/">
<a href="${p.shortUrl}/" class="model-link">
<img src="${imagesURL}/24x24/${b.buildStatusUrl}" tooltip="${p.tooltip} ${it.number!=b.number?(it.isBuilding()?'- pending' : '- skipped'):''}" alt="${p.tooltip}" style="${it.number!=b.number?'opacity:0.5':''}" height="24" width="24"/>
<j:if test="${empty(o.x) and empty(o.y)}">
${p.combination.toString(o.z)}
......
......@@ -34,7 +34,7 @@ THE SOFTWARE.
<img src="${imagesURL}/24x24/grey.png" tooltip="${%Not configured}" alt="${%Not configured}" height="24" width="24"/>
</j:when>
<j:otherwise>
<a href="${p.shortUrl}">
<a href="${p.shortUrl}" class="model-link">
<img src="${imagesURL}/24x24/${p.buildStatusUrl}" tooltip="${p.iconColor.description}" alt="${p.iconColor.description}" height="24" width="24"/>
<j:if test="${empty(o.x) and empty(o.y)}">
${p.combination.toString(o.z)}
......
......@@ -63,7 +63,7 @@ THE SOFTWARE.
<j:set var="atr" value="${it.lastCompletedBuild.aggregatedTestResultAction}"/>
<j:if test="${atr!=null}">
<t:summary icon="clipboard.png">
<a href="lastCompletedBuild/testReport/">${%Latest Test Result}</a>
<a href="lastCompletedBuild/testReport/" class="model-link">${%Latest Test Result}</a>
<st:nbsp/>
<t:test-result it="${atr}" />
</t:summary>
......
......@@ -76,11 +76,11 @@ THE SOFTWARE.
<ol>
<j:forEach var="dep" items="${depChanges.values()}">
<li>
<a href="${rootURL}/${dep.project.url}">${dep.project.displayName}</a>
<a href="${rootURL}/${dep.project.url}" class="model-link">${dep.project.displayName}</a>
<st:nbsp/>
<j:choose>
<j:when test="${dep.from!=null}">
<a href="${rootURL}/${dep.from.url}">
<a href="${rootURL}/${dep.from.url}" class="model-link">
<img src="${imagesURL}/16x16/${dep.from.buildStatusUrl}"
alt="${dep.from.iconColor.description}" height="16" width="16" />${dep.from.displayName}</a>
</j:when>
......@@ -91,7 +91,7 @@ THE SOFTWARE.
&#x2192; <!-- right arrow -->
<a href="${rootURL}/${dep.to.url}">
<a href="${rootURL}/${dep.to.url}" class="model-link">
<img src="${imagesURL}/16x16/${dep.to.buildStatusUrl}"
alt="${dep.to.iconColor.description}" height="16" width="16" />${dep.to.displayName}</a>
......@@ -125,7 +125,7 @@ THE SOFTWARE.
<ul style="list-style-type: none;">
<j:forEach var="item" items="${upstream}">
<li>
<a href="${rootURL}/${item.key.url}">${item.key.displayName}</a>
<a href="${rootURL}/${item.key.url}" class="model-link">${item.key.displayName}</a>
<t:buildLink job="${item.key}" number="${item.value}" />
</li>
</j:forEach>
......@@ -137,7 +137,7 @@ THE SOFTWARE.
<ul style="list-style-type: none;">
<j:forEach var="item" items="${downstream}">
<li>
<a href="${rootURL}/${item.key.url}">${item.key.displayName}</a>
<a href="${rootURL}/${item.key.url}" class="model-link">${item.key.displayName}</a>
<j:choose>
<j:when test="${item.value.isEmpty()}">
(${%none})
......
......@@ -50,7 +50,7 @@ THE SOFTWARE.
<j:if test="${tr!=null}">
<j:if test="${tr.class.name != 'hudson.tasks.test.AggregatedTestResultAction'}">
<t:summary icon="clipboard.png">
<a href="lastCompletedBuild/testReport/">${%Latest Test Result}</a>
<a href="lastCompletedBuild/testReport/" class="model-link">${%Latest Test Result}</a>
<st:nbsp/>
<t:test-result it="${tr}" />
</t:summary>
......@@ -60,7 +60,7 @@ THE SOFTWARE.
<j:set var="atr" value="${it.lastCompletedBuild.aggregatedTestResultAction}"/>
<j:if test="${atr!=null}">
<t:summary icon="clipboard.png">
<a href="lastCompletedBuild/aggregatedTestReport/">${%Latest Aggregated Test Result}</a>
<a href="lastCompletedBuild/aggregatedTestReport/" class="model-link">${%Latest Aggregated Test Result}</a>
<st:nbsp/>
<t:test-result it="${atr}" />
</t:summary>
......
......@@ -28,4 +28,12 @@ THE SOFTWARE.
<p>
Load statistics of this computer has <a href="../loadStatistics/api/">its own separate API</a>.
</p>
<h2>Fetch/Update config.xml</h2>
<p>
To programmatically obtain <tt>config.xml</tt>, hit <a href="../config.xml">this URL</a>.
You can also POST an updated <tt>config.xml</tt> to the same URL to programmatically
update the configuration of a node.
</p>
</j:jelly>
......@@ -75,7 +75,7 @@ THE SOFTWARE.
<j:forEach var="entry" items="${it.node.labelCloud}">
<!-- Skip the label for this node -->
<j:if test="${entry.item!=it.node.selfLabel}">
<a class="${entry.className}" href="${rootURL}/label/${entry.item.name}">${entry.item.name}</a>
<a class="${entry.className} model-link" href="${rootURL}/label/${entry.item.name}">${entry.item.name}</a>
<st:nbsp/>
</j:if>
</j:forEach>
......
......@@ -56,7 +56,7 @@ THE SOFTWARE.
<td width="32" data="${c.icon}">
<img src="${imagesURL}/32x32/${c.icon}" width="32" height="32" alt="${c.iconAltText}"/>
</td>
<td><a href="${rootURL}/${c.url}">${c.displayName}</a></td>
<td><a href="${rootURL}/${c.url}" class="model-link">${c.displayName}</a></td>
<j:forEach var="m" items="${monitors}">
<j:if test="${m.columnCaption!=null}">
<j:set var="data" value="${m.data(c)}"/>
......
......@@ -73,7 +73,7 @@ THE SOFTWARE.
<td class="fingerprint-summary-header">
<j:choose>
<j:when test="${job!=null}">
<a href="${rootURL}/${job.url}">${j}</a>
<a href="${rootURL}/${job.url}" class="model-link">${j}</a>
</j:when>
<j:otherwise>
${j}
......
......@@ -57,7 +57,7 @@ THE SOFTWARE.
<img width="16" height="16" src="${imagesURL}/16x16/${r.buildStatusUrl}" alt="${r.iconColor.description}" />
</td>
<td data="${r.number}">
<a href="${r.number}">
<a href="${r.number}/" class="model-link">
${r.displayName}
</a>
</td>
......
......@@ -36,7 +36,7 @@ THE SOFTWARE.
${%Nodes:}
<j:forEach var="n" items="${it.nodes}">
<j:set var="c" value="${app.getComputer(n.nodeName)}"/>
<a href="${rootURL}/computer/${n.nodeName}">
<a href="${rootURL}/computer/${n.nodeName}" class="model-link">
<img src="${imagesURL}/16x16/${c.icon}" width="16" height="16" alt=""/>
${c.displayName}
</a>
......
......@@ -32,7 +32,7 @@ THE SOFTWARE.
<j:set var="b" value="${it.resolve(job)}"/>
<j:if test="${b!=null}">
<li>
<a href="${it.id}/">${%format(it.displayName,b.displayName,b.timestampString)}</a>
<a class="model-link tl-tr" href="${it.id}/">${%format(it.displayName,b.displayName,b.timestampString)}</a>
</li>
</j:if>
</j:jelly>
......@@ -27,6 +27,6 @@ THE SOFTWARE.
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}">
<a href="${rootURL}/${it.run.url}">${it.run}</a>
<a href="${rootURL}/${it.run.url}" class="model-link">${it.run}</a>
</f:entry>
</j:jelly>
......@@ -45,12 +45,12 @@ THE SOFTWARE.
</tr>
<j:forEach var="p" items="${it.users}">
<tr>
<td><a href="${rootURL}/${p.user.url}/"><img src="${h.getUserAvatar(p.user,iconSize)}"
<td><a href="${rootURL}/${p.user.url}/" class="model-link"><img src="${h.getUserAvatar(p.user,iconSize)}"
alt="" class="icon${iconSize}"/></a></td>
<td><a href="${rootURL}/${p.user.url}/">${p.user.id}</a></td>
<td><a href="${rootURL}/${p.user.url}/">${p.user}</a></td>
<td data="${p.timeSortKey}">${p.lastChangeTimeString}</td>
<td><a href="${rootURL}/${p.project.url}">${p.project.fullDisplayName}</a></td>
<td><a href="${rootURL}/${p.project.url}" class="model-link">${p.project.fullDisplayName}</a></td>
</tr>
</j:forEach>
</table>
......
......@@ -26,7 +26,7 @@ THE SOFTWARE.
<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">
<st:structuredMessageFormat key="description">
<st:structuredMessageArgument>
<a href="${rootURL}/label/${it.label.name}">${it.label.name}</a>
<a href="${rootURL}/label/${it.label.name}" class="model-link">${it.label.name}</a>
</st:structuredMessageArgument>
</st:structuredMessageFormat>
</j:jelly>
\ No newline at end of file
......@@ -53,7 +53,7 @@ THE SOFTWARE.
&#8212;
<a href="${rootURL}/${c.author.url}/">${c.author}</a> /
<a href="${rootURL}/${c.author.url}/" class="model-link">${c.author}</a> /
<j:set var="cslink" value="${browser.getChangeSetLink(c)}"/>
<j:choose>
......
......@@ -39,7 +39,7 @@ THE SOFTWARE.
</tr>
<j:forEach var="user" items="${it.allUsers}">
<tr>
<td><a href="${user.url}/"><img src="${h.getUserAvatar(user,'32x32')}" alt="" height="32" width="32"/></a></td>
<td><a href="${user.url}/" class="model-link"><img src="${h.getUserAvatar(user,'32x32')}" alt="" height="32" width="32"/></a></td>
<td><a href="${user.url}/">${user.id}</a></td>
<td><a href="${user.url}/">${user}</a></td>
<td>
......
......@@ -41,7 +41,7 @@ THE SOFTWARE.
<j:if test="${test != null}">
<tr>
<td class="pane">
<a href="${app.rootUrl}${b.url}testReport${p.url}">${b.fullDisplayName}</a>
<a href="${app.rootUrl}${b.url}testReport${p.url}" class="model-link">${b.fullDisplayName}</a>
<st:nbsp/>
<j:forEach var="badge" items="${test.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
......
......@@ -36,7 +36,7 @@ THE SOFTWARE.
<j:forEach var="p" items="${it.children}" varStatus="status">
<tr>
<td class="pane">
<a href="${p.safeName}"><span style="${p.previousResult==null?'font-weight:bold':''}"><st:out value="${p.name}" /></span></a>
<a href="${p.safeName}" class="model-link"><span style="${p.previousResult==null?'font-weight:bold':''}"><st:out value="${p.name}" /></span></a>
<st:nbsp/>
<j:forEach var="badge" items="${p.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
......
......@@ -40,7 +40,7 @@ THE SOFTWARE.
<j:if test="${p != null}">
<tr>
<td class="pane">
<a href="${app.rootUrl}${b.url}testReport${p.url}">${b.fullDisplayName}</a>
<a href="${app.rootUrl}${b.url}testReport${p.url}" class="model-link">${b.fullDisplayName}</a>
<st:nbsp/>
<j:forEach var="badge" items="${p.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
......
......@@ -61,7 +61,7 @@ THE SOFTWARE.
<j:forEach var="i" items="${it.didntRun}">
<tr>
<td class="pane">
<a href="${rootURL}/${i.url}">${i.fullDisplayName}</a>
<a href="${rootURL}/${i.url}" class="model-link">${i.fullDisplayName}</a>
(${%test result not available})
</td>
<td class="pane">
......@@ -75,7 +75,7 @@ THE SOFTWARE.
<j:forEach var="i" items="${it.noFingerprints}">
<tr>
<td class="pane">
<a href="${rootURL}/${i.url}">${i.fullDisplayName}</a>
<a href="${rootURL}/${i.url}" class="model-link">${i.fullDisplayName}</a>
(${%last successful job is not fingerprinted})
</td>
<td class="pane">
......
......@@ -42,7 +42,7 @@ THE SOFTWARE.
<j:forEach var="report" items="${it.childReports}">
<tr>
<td class="pane">
<a href="../${report.child.project.shortUrl}testReport">${report.child.project.name}</a>
<a href="../${report.child.project.shortUrl}testReport" class="model-link">${report.child.project.name}</a>
</td>
<td data="${report.result.duration}" class="pane" style="text-align:right">
${report.result.durationString}
......@@ -74,7 +74,7 @@ THE SOFTWARE.
<h3>
<a name="${report.child.project.name}"/>
<a href="../${report.child.project.shortUrl}testReport">${report.child.project.name}</a>
<a href="../${report.child.project.shortUrl}testReport" class="model-link">${report.child.project.name}</a>
</h3>
<table class="pane sortable">
......@@ -86,7 +86,7 @@ THE SOFTWARE.
<j:forEach var="f" items="${report.result.failedTests}" varStatus="i">
<tr>
<td class="pane">
<a href="../${report.child.project.shortUrl}testReport/${f.getRelativePathFrom(report.result)}">
<a href="../${report.child.project.shortUrl}testReport/${f.getRelativePathFrom(report.result)}" class="model-link">
<st:out value="${f.fullName}"/>
<st:nbsp/>
<j:forEach var="badge" items="${f.testActions}">
......
......@@ -63,7 +63,7 @@ THE SOFTWARE.
<a id="test-${f.fullName}-hidelink" style="display:none"
href="javascript:hideStackTrace('test-${h.jsStringEscape(f.fullName)}')">&lt;&lt;&lt;</a>
<st:nbsp/>
<a href="${f.getRelativePathFrom(it)}"><st:out value="${f.fullName}"/></a>
<a href="${f.getRelativePathFrom(it)}" class="model-link"><st:out value="${f.fullName}"/></a>
<st:nbsp/>
<j:forEach var="badge" items="${f.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
......@@ -77,7 +77,7 @@ THE SOFTWARE.
${f.durationString}
</td>
<td class="pane" style="text-align:right;">
<a href="${rootURL}/${f.failedSinceRun.url}">${f.age}</a>
<a href="${rootURL}/${f.failedSinceRun.url}" class="model-link">${f.age}</a>
</td>
</tr>
</j:forEach>
......@@ -102,7 +102,7 @@ THE SOFTWARE.
<j:set var="prev" value="${p.previousResult}" />
<tr>
<td class="pane">
<a href="${p.safeName}/"><span style="${prev==null?'font-weight:bold':''}"><st:out value="${p.name}" /></span></a>
<a href="${p.safeName}/" class="model-link"><span style="${prev==null?'font-weight:bold':''}"><st:out value="${p.name}" /></span></a>
<st:nbsp/>
<j:forEach var="badge" items="${p.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
......
......@@ -40,7 +40,7 @@ THE SOFTWARE.
<j:if test="${p != null}">
<tr>
<td class="pane">
<a href="${app.rootUrl}${b.url}testReport${p.url}">${b.fullDisplayName}</a>
<a href="${app.rootUrl}${b.url}testReport${p.url}" class="model-link">${b.fullDisplayName}</a>
<st:nbsp/>
<j:forEach var="badge" items="${p.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
......
......@@ -25,6 +25,6 @@ THE SOFTWARE.
<?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" xmlns:i="jelly:fmt">
<td style="${indenter.getCss(job)}">
<a href="${jobBaseUrl}${job.shortUrl}" tooltip="${job.name}"> ${job.displayName}</a>
<a href="${jobBaseUrl}${job.shortUrl}" class='model-link'> ${job.displayName}</a>
</td>
</j:jelly>
\ No newline at end of file
......@@ -29,7 +29,7 @@ THE SOFTWARE.
<j:choose>
<j:when test="${lfBuild!=null}">
${lfBuild.timestampString}
(<a href="${jobBaseUrl}${job.shortUrl}lastFailedBuild/">${lfBuild.displayName}</a>)
(<a href="${jobBaseUrl}${job.shortUrl}lastFailedBuild/" class="model-link">${lfBuild.displayName}</a>)
</j:when>
<j:otherwise>
${%N/A}
......
......@@ -29,7 +29,7 @@ THE SOFTWARE.
<j:choose>
<j:when test="${lstBuild!=null}">
${lstBuild.timestampString}
(<a href="${jobBaseUrl}${job.shortUrl}lastStableBuild/">${lstBuild.displayName}</a>)
(<a href="${jobBaseUrl}${job.shortUrl}lastStableBuild/" class="model-link">${lstBuild.displayName}</a>)
</j:when>
<j:otherwise>
${%N/A}
......
......@@ -29,7 +29,7 @@ THE SOFTWARE.
<j:choose>
<j:when test="${lsBuild!=null}">
${lsBuild.timestampString}
(<a href="${jobBaseUrl}${job.shortUrl}lastSuccessfulBuild/">${lsBuild.displayName}</a>)
(<a href="${jobBaseUrl}${job.shortUrl}lastSuccessfulBuild/" class="model-link">${lsBuild.displayName}</a>)
</j:when>
<j:otherwise>
${%N/A}
......
......@@ -35,7 +35,7 @@ THE SOFTWARE.
${build.displayName}
</td>
<td style="padding-right:0">
<a class="tip" href="${link}">
<a class="tip model-link tl-tr" href="${link}">
<i:formatDate value="${build.timestamp.time}" type="both" dateStyle="medium" timeStyle="medium"/>
</a>
</td>
......
......@@ -29,5 +29,5 @@ THE SOFTWARE.
Displays a link when given an AbstractItem. It is assumed that that Abstract
Item is passed in ${it}
</st:documentation>
<a href="${it.absoluteUrl}" tooltip="${it.name}">${it.displayName}</a>
<a href="${it.absoluteUrl}" class="model-link">${it.displayName}</a>
</j:jelly>
\ No newline at end of file
......@@ -42,11 +42,11 @@ THE SOFTWARE.
<j:set var="r" value="${job.getBuildByNumber(number)}" />
<j:choose>
<j:when test="${r==null}">
<a href="${rootURL}/${job.url}">${jobName}</a>
<a href="${rootURL}/${job.url}" class="model-link">${jobName}</a>
#<!-- -->${number}
</j:when>
<j:otherwise>
<a href="${attrs.href ?: rootURL+'/'+r.url}">
<a href="${attrs.href ?: rootURL+'/'+r.url}" class="model-link">
<img src="${imagesURL}/16x16/${r.buildStatusUrl}"
alt="${r.iconColor.description}" height="16" width="16"/>${jobName_}#<!-- -->${number}</a>
</j:otherwise>
......
......@@ -47,15 +47,15 @@ THE SOFTWARE.
<j:forEach var="b" items="${h.subList(attrs.builds,50)}">
<tr>
<td data="${b.iconColor.ordinal()}">
<a href="${jobBaseUrl}${b.url}">
<a href="${jobBaseUrl}${b.url}" class="model-link">
<img src="${imagesURL}/${iconSize}/${b.buildStatusUrl}"
alt="${b.iconColor.description}" class="icon${iconSize}"/>
</a>
</td>
<td>
<a href="${jobBaseUrl}${b.parent.url}">${b.parent.fullDisplayName}</a>
<a href="${jobBaseUrl}${b.parent.url}" class="model-link">${b.parent.fullDisplayName}</a>
<st:nbsp/>
<a href="${jobBaseUrl}${b.url}">${b.displayName}</a>
<a href="${jobBaseUrl}${b.url}" class="model-link">${b.displayName}</a>
</td>
<td data="${b.timestampString2}" tooltip="${%Click to center timeline on event}" onclick="javascript:tl.getBand(0).scrollToCenter(Timeline.DateTime.parseGregorianDateTime('${b.timestampString2}'))">
${b.timestampString}
......
......@@ -33,7 +33,7 @@ THE SOFTWARE.
</st:documentation>
<d:taglib uri="local">
<d:tag name="computerCaption">
<a href="${rootURL}/${c.url}">${title}</a>
<a href="${rootURL}/${c.url}" class="model-link">${title}</a>
<j:if test="${c.offline}"> <st:nbsp/> (${%offline})</j:if>
<j:if test="${!c.acceptingTasks}"> <st:nbsp/> (${%suspended})</j:if>
</d:tag>
......@@ -57,7 +57,7 @@ THE SOFTWARE.
<td class="pane">
<j:choose>
<j:when test="${c.offline}">
<a href="${rootURL}/${c.url}">${%Offline}</a>
<a href="${rootURL}/${c.url}" class="model-link">${%Offline}</a>
</j:when>
<j:otherwise>
${%Idle}
......@@ -79,7 +79,7 @@ THE SOFTWARE.
</j:invokeStatic>
<j:choose>
<j:when test="${h.hasPermission(exeparent,exeparent.READ)}">
<a href="${rootURL}/${exeparent.url}">${exeparent.fullDisplayName}</a>&#160;<a href="${rootURL}/${exe.url}">#${exe.number}</a>
<a href="${rootURL}/${exeparent.url}" class="model-link">${exeparent.fullDisplayName}</a>&#160;<a href="${rootURL}/${exe.url}" class="model-link">#${exe.number}</a>
<t:buildProgressBar build="${exe}" executor="${executor}"/>
</j:when>
<j:otherwise>
......
......@@ -32,5 +32,5 @@ THE SOFTWARE.
</st:documentation>
<img src="${imagesURL}/16x16/${job.buildStatusUrl}" alt="${job.iconColor.description}" height="16" width="16"/>
<a href="${h.getRelativeLinkTo(job)}" tooltip="${job.name}">${job.displayName}</a>
<a href="${h.getRelativeLinkTo(job)}" class="model-link">${job.displayName}</a>
</j:jelly>
......@@ -30,7 +30,7 @@ THE SOFTWARE.
</st:documentation>
<j:choose>
<j:when test="${value!=null and value!=app}">
<a href="${rootURL}/computer/${value.nodeName}">${value.nodeName}</a>
<a href="${rootURL}/computer/${value.nodeName}" class="model-link">${value.nodeName}</a>
</j:when>
<j:otherwise>
${%master}
......
......@@ -36,7 +36,7 @@ THE SOFTWARE.
<j:set var="b" value="${it[property]}"/>
<j:if test="${b!=null}">
<li>
<a href="${property}/">${%format(title,b.number,b.timestampString)}</a>
<a href="${property}/" class="model-link">${%format(title,b.number,b.timestampString)}</a>
</li>
</j:if>
</j:jelly>
\ No newline at end of file
......@@ -48,7 +48,7 @@ THE SOFTWARE.
</div>
</td>
<td colspan="5" style="${indenter.getCss(job)}">
<a href="${rootURL}/${v.url}">
<a href="${rootURL}/${v.url}" class="model-link">
${v.viewName}
</a>
</td>
......
......@@ -62,7 +62,7 @@ THE SOFTWARE.
<j:set var="stuck" value="${item.isStuck()}"/>
<j:choose>
<j:when test="${h.hasPermission(item.task,item.task.READ)}">
<a href="${rootURL}/${item.task.url}">
<a href="${rootURL}/${item.task.url}" class="model-link">
${item.task.fullDisplayName}
</a>
<j:if test="${stuck}">
......
var breadcrumbs = (function() {
var Dom = YAHOO.util.Dom;
/**
* This component actually renders the menu.
*
......@@ -11,6 +13,26 @@ var breadcrumbs = (function() {
*/
var xhr;
/**
* When mouse hovers over the anchor that has context menu, we capture its region here.
* This is used to to avoid showing menu when the mouse slides over to other elements after a delay.
*
* @type {YAHOO.util.Region}
*/
var hitTest;
/**
* Current mouse cursor position in the page coordinate.
*
* @type {YAHOO.util.Point}
*/
var mouse;
/**
* Timer ID for lazy menu display.
*/
var menuDelay;
function makeMenuHtml(icon,displayName) {
return (icon!=null ? "<img src='"+icon+"' width=24 height=24 style='margin: 2px;' alt=''> " : "")+displayName;
}
......@@ -19,39 +41,79 @@ var breadcrumbs = (function() {
menu = new YAHOO.widget.Menu("breadcrumb-menu", {position:"dynamic", hidedelay:1000});
});
Event.observe(window,"mousemove",function (ev){
mouse = new YAHOO.util.Point(ev.pageX,ev.pageY);
});
function cancelMenu() {
if (menuDelay) {
window.clearTimeout(menuDelay);
menuDelay = null;
}
}
function combinePath(a,b) {
if (a.endsWith('/')) return a+b;
return a+'/'+b;
}
/**
* @param {HTMLElement} e
* anchor tag
* @param {Number} delay
* Number of milliseconds to wait before the menu is displayed.
* The mouse needs to be on the same anchor tag after this delay.
*/
function handleHover(e,delay) {
function showMenu(items) {
cancelMenu();
hitTest = Dom.getRegion(e);
menuDelay = window.setTimeout(function() {
if (hitTest.contains(mouse)) {
menu.hide();
var pos = [e, "tl", "bl"];
if ($(e).hasClassName("tl-tr")) pos = [e,"tl","tr"]
menu.cfg.setProperty("context", pos);
menu.clearContent();
menu.addItems(items);
menu.render("breadcrumb-menu-target");
menu.show();
}
menuDelay = null;
},delay);
}
if (xhr)
xhr.options.onComplete = function () {
}; // ignore the currently pending call
if (e.items) {// use what's already loaded
showMenu(e.items());
} else {// fetch menu on demand
xhr = new Ajax.Request(combinePath(e.getAttribute("href"),"contextMenu"), {
onComplete:function (x) {
var a = x.responseText.evalJSON().items;
a.each(function (e) {
e.text = makeMenuHtml(e.icon, e.displayName);
});
e.items = function() { return a };
showMenu(a);
}
});
}
return false;
}
jenkinsRules["#breadcrumbs LI"] = function (e) {
// when the mouse hovers over LI, activate the menu
$(e).observe("mouseover", function () {
function showMenu(items) {
menu.hide();
menu.cfg.setProperty("context", [e, "tl", "bl"]);
menu.clearContent();
menu.addItems(items);
menu.render("breadcrumb-menu-target");
menu.show();
}
if (xhr)
xhr.options.onComplete = function () {
}; // ignore the currently pending call
if (e.items) {// use what's already loaded
showMenu(e.items());
} else {// fetch menu on demand
xhr = new Ajax.Request(e.firstChild.getAttribute("href") + "contextMenu", {
onComplete:function (x) {
var a = x.responseText.evalJSON().items;
a.each(function (e) {
e.text = makeMenuHtml(e.icon, e.displayName);
});
e.items = function() { return a };
showMenu(a);
}
});
}
return false;
});
$(e).observe("mouseover", function () { handleHover(e.firstChild,0) });
};
jenkinsRules["A.model-link"] = function (a) {
// ditto for model-link, but give it a larger delay to avoid unintended menus to be displayed
$(a).observe("mouseover", function () { handleHover(a,500); });
};
/**
......@@ -86,7 +148,7 @@ var breadcrumbs = (function() {
* populating the content.
*/
"attachMenu" : function (li,menu) {
$(li).items = (typeof menu=="function") ? menu : function() { return menu.items };
$(li).firstChild.items = (typeof menu=="function") ? menu : function() { return menu.items };
},
"ContextMenu" : ContextMenu
......
......@@ -182,7 +182,7 @@ THE SOFTWARE.
<j:choose>
<j:when test="${!h.isAnonymous()}">
<span style="white-space:nowrap">
<a href="${rootURL}/user/${app.authentication.name}"><b>${app.authentication.name}</b></a>
<a href="${rootURL}/user/${app.authentication.name}" class="model-link"><b>${app.authentication.name}</b></a>
<j:if test="${app.securityRealm.canLogOut()}">
|
<a href="${rootURL}/logout"><b>${%logout}</b></a>
......
......@@ -4,7 +4,7 @@
<parent>
<groupId>org.jenkins-ci</groupId>
<artifactId>jenkins</artifactId>
<version>1.25</version>
<version>1.26</version>
</parent>
<groupId>org.jenkins-ci.plugins</groupId>
......@@ -256,8 +256,8 @@
<repositories>
<repository>
<id>m.g.o-public</id>
<url>http://maven.glassfish.org/content/groups/public/</url>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
<releases>
<enabled>true</enabled>
</releases>
......@@ -269,8 +269,8 @@
<pluginRepositories>
<pluginRepository>
<id>m.g.o-public</id>
<url>http://maven.glassfish.org/content/groups/public/</url>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
<releases>
<enabled>true</enabled>
</releases>
......
......@@ -97,8 +97,8 @@ THE SOFTWARE.
<repositories>
<repository>
<id>m.g.o-public</id>
<url>http://maven.glassfish.org/content/groups/public/</url>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
<releases>
<enabled>true</enabled>
</releases>
......@@ -119,8 +119,8 @@ THE SOFTWARE.
</repositories>
<pluginRepositories>
<pluginRepository>
<id>m.g.o-public</id>
<url>http://maven.glassfish.org/content/groups/public/</url>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
<releases>
<enabled>true</enabled>
</releases>
......
......@@ -74,7 +74,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>subversion</artifactId>
<version>1.25</version>
<version>1.26</version>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
......
......@@ -25,6 +25,8 @@ package hudson.model
import hudson.slaves.DumbSlave
import org.jvnet.hudson.test.GroovyHudsonTestCase
import org.apache.commons.io.IOUtils
import hudson.slaves.JNLPLauncher
/**
*
......@@ -40,4 +42,40 @@ public class SlaveTest extends GroovyHudsonTestCase {
assertNotNull(hudson.getDescriptor(DumbSlave.class).getCheckUrl("remoteFS"))
}
}
/**
* Programmatica config.xml submission.
*/
void testSlaveConfigDotXml() {
DumbSlave s = createSlave();
def wc = createWebClient()
def p = wc.goTo("/computer/${s.name}/config.xml","application/xml");
def xml = p.webResponse.contentAsString
new XmlSlurper().parseText(xml) // verify that it is XML
// make sure it survives the roundtrip
post("/computer/${s.name}/config.xml",xml);
assertNotNull(jenkins.getNode(s.name))
xml = IOUtils.toString(getClass().getResource("SlaveTest/slave.xml").openStream());
xml = xml.replace("NAME",s.name)
post("/computer/${s.name}/config.xml",xml);
s = jenkins.getNode(s.name)
assertNotNull(s)
assertEquals("some text",s.nodeDescription)
assertEquals(JNLPLauncher.class,s.launcher.class)
}
def post(url,String xml) {
HttpURLConnection con = new URL(this.getURL(),url).openConnection();
con.requestMethod = "POST"
con.setRequestProperty("Content-Type","application/xml;charset=UTF-8")
con.setRequestProperty(".crumb","test")
con.doOutput = true;
con.outputStream.write(xml.getBytes("UTF-8"))
con.outputStream.close();
IOUtils.copy(con.inputStream,System.out)
}
}
<slave>
<name>NAME</name>
<description>some text</description>
<remoteFS>/tmp</remoteFS>
<numExecutors>1</numExecutors>
<mode>NORMAL</mode>
<retentionStrategy class="hudson.slaves.RetentionStrategy$Always"/>
<launcher class="hudson.slaves.JNLPLauncher"/>
<label></label>
<nodeProperties/>
</slave>
\ No newline at end of file
package jenkins.plugins.ui_samples;
import hudson.Extension;
import java.util.Arrays;
import java.util.List;
/**
* @author Kohsuke Kawaguchi
*/
@Extension
public class InpageNavigationWithBreadcrumb extends UISample {
@Override
public String getDescription() {
return "Adds in-page navigation with extra breadcrumb";
}
public List<SourceFile> getSourceFiles() {
// TODO: generate this from index
return Arrays.asList(
new SourceFile("index.groovy"),
new SourceFile("header.groovy"));
}
@Extension
public static final class DescriptorImpl extends UISampleDescriptor {
}
}
package jenkins.plugins.ui_samples;
import hudson.Extension;
import jenkins.model.ModelObjectWithContextMenu;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.util.List;
/**
* @author Kohsuke Kawaguchi
*/
@Extension
public class NavigationContextMenu extends UISample implements ModelObjectWithContextMenu {
@Override
public String getDescription() {
return "Integrate with navigational context menu to provider quick access around object graph";
}
/**
* This method is called via AJAX to obtain the context menu for this model object.
*/
public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception {
if (false) {
// this implementation is suffice for most ModelObjects. It uses sidepanel.jelly/.groovy to
// generate the context menu
return new ContextMenu().from(this,request,response);
} else {
// otherwise you can also programatically create them.
// see the javadoc for various convenience methods to add items
return new ContextMenu()
.add("http://jenkins-ci.org/","Jenkins project")
.add("http://www.cloudbees.com/","CloudBees")
.add(request.getContextPath(),"/images/24x24/gear.png","top-page");
}
}
@Extension
public static final class DescriptorImpl extends UISampleDescriptor {
}
}
......@@ -2,6 +2,9 @@ package jenkins.plugins.ui_samples;
import hudson.Extension;
import hudson.model.RootAction;
import jenkins.model.ModelObjectWithContextMenu;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.util.List;
......@@ -11,7 +14,7 @@ import java.util.List;
* @author Kohsuke Kawaguchi
*/
@Extension
public class Root implements RootAction {
public class Root implements RootAction, ModelObjectWithContextMenu {
public String getIconFileName() {
return "gear.png";
}
......@@ -34,4 +37,8 @@ public class Root implements RootAction {
public List<UISample> getAll() {
return UISample.all();
}
public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception {
return new ContextMenu().addAll(getAll());
}
}
......@@ -10,6 +10,7 @@ import org.kohsuke.stapler.StaplerResponse;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
......@@ -38,8 +39,15 @@ public abstract class UISample implements ExtensionPoint, Action, Describable<UI
* Source files associated with this sample.
*/
public List<SourceFile> getSourceFiles() {
return Arrays.asList(new SourceFile(getClass().getSimpleName()+".java"),
new SourceFile("index.jelly"));
List<SourceFile> r = new ArrayList<SourceFile>();
r.add(new SourceFile(getClass().getSimpleName()+".java"));
for (String name : new String[]{"index.jelly","index.groovy"}) {
SourceFile s = new SourceFile(name);
if (s.resolve()!=null)
r.add(s);
}
return r;
}
/**
......
package jenkins.plugins.ui_samples.InpageNavigationWithBreadcrumb
def l=namespace(lib.LayoutTagLib.class)
// put them under your l.layout
l.breadcrumb(title:"Click me! Click me!",id:"id-of-breadcrumb-item")
package jenkins.plugins.ui_samples.InpageNavigationWithBreadcrumb;
import lib.JenkinsTagLib
def st=namespace("jelly:stapler")
t=namespace(JenkinsTagLib.class)
namespace("/lib/samples").sample(title:_("In-page navigation via breadcrumb")) {
raw(_("blurb"))
script """
Event.observe(window,"load",function(){
var menu = new breadcrumbs.ContextMenu();
menu.add('#section1',rootURL+"/images/24x24/gear.png","Section 1")
menu.add('#section2',rootURL+"/images/24x24/gear.png","Section 2")
breadcrumbs.attachMenu('id-of-breadcrumb-item',menu);
});
"""
}
blurb=You can add arbitrary additional items to the breadcrumb bar (see above), and associate menus with them. \
This mechanism is convenient for adding anchors to a large page and provide quick access to the key parts. \
A very typical place where you want to do this is in the configuration page. There's a &lt;f:breadcrumb-config-outline> \
tag specifically for this use case, which parses &lt;f:section>s and use that to build the context menu. \
See the freestyle job configuration page for an example of using this tag.
\ No newline at end of file
package jenkins.plugins.ui_samples.NavigationContextMenu;
import lib.JenkinsTagLib
def st=namespace("jelly:stapler")
t=namespace(JenkinsTagLib.class)
def example(html) {
tr {
td {
text(html)
}
td {
raw(html)
}
}
}
namespace("/lib/samples").sample(title:_("Navigational context menu integration")) {
raw(_("blurb"))
h2(_("Defining context menu"))
raw(_("blurb.define"))
h2(_("Breadcrumb integration"))
raw(_("blurb.breadcrumb"))
h2(_("Model hyperlink"))
raw(_("blurb.modelLink"))
table(border:1) {
example "<a href='.' class='model-link'>self</a>"
example "<a href='..' class='model-link'>up</a>"
}
raw(_("blurb.tltr"))
table(border:1) {
example "<a href='.' class='model-link tl-tr'>self</a>"
example "<a href='..' class='model-link tl-tr'>up</a>"
}
}
blurb=<p>Jenkins consists of a large and complex graph of domain objects (<tt>ModelObject</tt>), where each node in the graph is a web page and edges are hyperlinks. \
To help users navigate quickly on this graph, Jenkins provides a mechanism to attach context menus to model objects, \
which can be used to hop multiple edges without going through individual hyperlinks.
blurb.define=<p>To define a context menu on <tt>ModelObject</tt>, implement <a href="http://javadoc.jenkins-ci.org/byShortName/ModelObjectWithContextMenu"><tt>ModelObjectWithContextMenu</tt></a>. \
See <a href="sourceFile/NavigationContextMenu.java">the example</a> for how to implement this method.
blurb.breadcrumb=<p>Implementing <tt>ModelObjectWithContextMenu</tt> is sufficient for the core to show the context menu \
for your model object in the breadcrumb. Hover your mouse over the breadcrumb of this page to see context menu \
associated with this sample.
blurb.modelLink=<p>By adding CSS class "model-link" to the &lt;a> tags pointing to model objects with context menu, \
you can enable the context menu support to that hyperlink. For example:
blurb.tltr=<p>By default, context menu appears below the link ,but this is inconvenient when model links line up in a vertical list. \
Add additional "tl-tr" CSS class (read it as 'top-left of the context menu to top-right of the target anchor) to \
make context menu appear on the right.
\ No newline at end of file
......@@ -9,7 +9,7 @@ t=namespace(JenkinsTagLib.class)
namespace("/lib/samples").sample(title:_("Notification Bar")) {
raw(_("blurb"))
raw("To show a notification bar, call <tt>notificationBar.show('message')</tt><")
raw("To show a notification bar, call <tt>notificationBar.show('message')</tt>")
button(onclick:"notificationBar.show('This is a notification');", "Show a notification bar")
raw(_("blurb.hide"))
......
......@@ -32,6 +32,7 @@ THE SOFTWARE.
<j:set var="instance" value="${it}" />
<j:set var="descriptor" value="${it.descriptor}" />
<l:layout title="${title}">
<st:include page="header" optional="true" />
<l:main-panel>
<h1>${title}</h1>
<p>
......
......@@ -1003,7 +1003,7 @@ table.progress-bar.red td.progress-bar-done {
text-align:center;
left:0px;
font-size: 2em;
z-index:999;
z-index:1000;
border-bottom: 1px solid black;
}
......
......@@ -240,7 +240,7 @@ function findFollowingTR(input, className) {
// then next TR that matches the CSS
do {
tr = tr.nextSibling;
tr = $(tr).next();
} while (tr != null && (tr.tagName != "TR" || !Element.hasClassName(tr,className)));
return tr;
......@@ -518,20 +518,21 @@ var jenkinsRules = {
btn = btns[btns.length-1]; // In case nested content also uses hetero-list
YAHOO.util.Dom.insertAfter(menu,btn);
var prototypes = e.lastChild;
while(!Element.hasClassName(prototypes,"prototypes"))
prototypes = prototypes.previousSibling;
var insertionPoint = prototypes.previousSibling; // this is where the new item is inserted.
var prototypes = $(e.lastChild);
while(!prototypes.hasClassName("prototypes"))
prototypes = prototypes.previous();
var insertionPoint = prototypes.previous(); // this is where the new item is inserted.
// extract templates
var templates = []; var i=0;
for(var n=prototypes.firstChild;n!=null;n=n.nextSibling,i++) {
$(prototypes).childElements().each(function (n) {
var name = n.getAttribute("name");
var tooltip = n.getAttribute("tooltip");
var descriptorId = n.getAttribute("descriptorId");
menu.options[i] = new Option(n.getAttribute("title"),""+i);
templates.push({html:n.innerHTML, name:name, tooltip:tooltip,descriptorId:descriptorId});
}
i++;
});
Element.remove(prototypes);
var withDragDrop = initContainerDD(e);
......@@ -568,9 +569,9 @@ var jenkinsRules = {
if(isInsideRemovable(e)) return;
// compute the insertion point
var ip = e.lastChild;
while (!Element.hasClassName(ip, "repeatable-insertion-point"))
ip = ip.previousSibling;
var ip = $(e.lastChild);
while (!ip.hasClassName("repeatable-insertion-point"))
ip = ip.previous();
// set up the logic
object(repeatableSupport).init(e, e.firstChild, ip);
},
......@@ -589,7 +590,8 @@ var jenkinsRules = {
},
"INPUT.applyButton":function (e) {
var id = "iframe"+(iota++);
var id;
var containerId = "container"+(iota++);
var responseDialog = new YAHOO.widget.Panel("wait"+(iota++), {
fixedcenter:true,
......@@ -601,9 +603,9 @@ var jenkinsRules = {
});
responseDialog.setHeader("Error");
responseDialog.setBody("<iframe id='"+id+"' name='"+id+"' style='height:100%; width:100%'></iframe>");
responseDialog.setBody("<div id='"+containerId+"'></iframe>");
responseDialog.render(document.body);
var target = $(id); // iframe
var target; // iframe
function attachIframeOnload(target, f) {
if (target.attachEvent) {
......@@ -613,26 +615,33 @@ var jenkinsRules = {
}
}
var attached = false;
makeButton(e,function (e) {
var f = findAncestor(e.target, "FORM");
if (!attached) {
attached = true;
attachIframeOnload(target, function () {
if (target.contentWindow && target.contentWindow.applyCompletionHandler) {
// apply-aware server is expected to set this handler
target.contentWindow.applyCompletionHandler(window);
} else {
// otherwise this is possibly an error from the server, so we need to render the whole content.
var r = YAHOO.util.Dom.getClientRegion();
responseDialog.cfg.setProperty("width",r.width*3/4+"px");
responseDialog.cfg.setProperty("height",r.height*3/4+"px");
responseDialog.center();
responseDialog.show();
}
});
}
// create a throw-away IFRAME to avoid back button from loading the POST result back
id = "iframe"+(iota++);
target = document.createElement("iframe");
target.setAttribute("id",id);
target.setAttribute("name",id);
target.setAttribute("style","height:100%; width:100%");
$(containerId).appendChild(target);
attachIframeOnload(target, function () {
if (target.contentWindow && target.contentWindow.applyCompletionHandler) {
// apply-aware server is expected to set this handler
target.contentWindow.applyCompletionHandler(window);
} else {
// otherwise this is possibly an error from the server, so we need to render the whole content.
var r = YAHOO.util.Dom.getClientRegion();
responseDialog.cfg.setProperty("width",r.width*3/4+"px");
responseDialog.cfg.setProperty("height",r.height*3/4+"px");
responseDialog.center();
responseDialog.show();
}
window.setTimeout(function() {// otherwise Firefox will fail to leave the "connecting" state
$(id).remove();
},0)
});
f.target = target.id;
f.elements['core:apply'].value = "true";
......@@ -653,7 +662,7 @@ var jenkinsRules = {
link = link.parentNode;
link.style.display = "none"; // hide the button
var container = link.nextSibling.firstChild; // TABLE -> TBODY
var container = $(link).next().down(); // TABLE -> TBODY
var tr = link;
while (tr.tagName != "TR")
......@@ -665,7 +674,7 @@ var jenkinsRules = {
var row = container.lastChild;
if(nameRef!=null && row.getAttribute("nameref")==null)
row.setAttribute("nameref",nameRef); // to handle inner rowSets, don't override existing values
tr.parentNode.insertBefore(row, tr.nextSibling);
tr.parentNode.insertBefore(row, $(tr).next());
}
});
e = null; // avoid memory leak
......@@ -677,7 +686,7 @@ var jenkinsRules = {
while(!Element.hasClassName(link,"advancedLink"))
link = link.parentNode;
link.style.display = "none";
link.nextSibling.style.display="block";
$(link).next().style.display="block";
});
e = null; // avoid memory leak
},
......@@ -704,13 +713,13 @@ var jenkinsRules = {
// <label> that doesn't use ID, so that it can be copied in <repeatable>
"LABEL.attach-previous" : function(e) {
e.onclick = function() {
var e = this.previousSibling;
var e = $(this).previous();
while (e!=null) {
if (e.tagName=="INPUT") {
e.click();
break;
}
e = e.previousSibling;
e = e.previous();
}
}
e = null;
......@@ -734,7 +743,7 @@ var jenkinsRules = {
"INPUT.auto-complete": function(e) {// form field with auto-completion support
// insert the auto-completion container
var div = document.createElement("DIV");
e.parentNode.insertBefore(div,e.nextSibling);
e.parentNode.insertBefore(div,$(e).next());
e.style.position = "relative"; // or else by default it's absolutely positioned, making "width:100%" break
var ds = new YAHOO.util.XHRDataSource(e.getAttribute("autoCompleteUrl"));
......@@ -765,7 +774,7 @@ var jenkinsRules = {
"A.help-button" : function(e) {
e.onclick = function() {
var tr = findFollowingTR(this, "help-area");
var div = tr.firstChild.nextSibling.firstChild;
var div = $(tr).down().next().down();
if (div.style.display != "block") {
div.style.display = "block";
......@@ -1032,7 +1041,7 @@ var jenkinsRules = {
var e = s;
var cnt=1;
while(cnt>0) {
e = e.nextSibling;
e = $(e).next();
if (Element.hasClassName(e,"radio-block-start"))
cnt++;
if (Element.hasClassName(e,"radio-block-end"))
......@@ -1058,7 +1067,7 @@ var jenkinsRules = {
"TR.rowvg-start" : function(e) {
// figure out the corresponding end marker
function findEnd(e) {
for( var depth=0; ; e=e.nextSibling) {
for( var depth=0; ; e=$(e).next()) {
if(Element.hasClassName(e,"rowvg-start")) depth++;
if(Element.hasClassName(e,"rowvg-end")) depth--;
if(depth==0) return e;
......@@ -1102,8 +1111,7 @@ var jenkinsRules = {
*/
updateVisibility : function() {
var display = (this.outerVisible && this.innerVisible) ? "" : "none";
for (var e=this.start; e!=this.end; e=e.nextSibling) {
if (e.nodeType!=1) continue;
for (var e=this.start; e!=this.end; e=$(e).next()) {
if (e.rowVisibilityGroup && e!=this.start) {
e.rowVisibilityGroup.makeOuterVisisble(this.innerVisible);
e = e.rowVisibilityGroup.end; // the above call updates visibility up to e.rowVisibilityGroup.end inclusive
......@@ -1133,11 +1141,12 @@ var jenkinsRules = {
"TR.row-set-end": function(e) { // see rowSet.jelly and optionalBlock.jelly
// figure out the corresponding start block
e = $(e);
var end = e;
for( var depth=0; ; e=e.previousSibling) {
if(Element.hasClassName(e,"row-set-end")) depth++;
if(Element.hasClassName(e,"row-set-start")) depth--;
for( var depth=0; ; e=e.previous()) {
if(e.hasClassName("row-set-end")) depth++;
if(e.hasClassName("row-set-start")) depth--;
if(depth==0) break;
}
var start = e;
......@@ -1212,13 +1221,12 @@ var jenkinsRules = {
// editableComboBox.jelly
"INPUT.combobox" : function(c) {
// Next element after <input class="combobox"/> should be <div class="combobox-values">
var vdiv = c.nextSibling;
if (Element.hasClassName(vdiv, "combobox-values")) {
var vdiv = $(c).next();
if (vdiv.hasClassName("combobox-values")) {
createComboBox(c, function() {
var values = [];
for (var value = vdiv.firstChild; value; value = value.nextSibling)
values.push(value.getAttribute('value'));
return values;
return vdiv.childElements().collect(function(value) {
return value.getAttribute('value');
});
});
}
},
......@@ -1228,7 +1236,7 @@ var jenkinsRules = {
if(isInsideRemovable(e)) return;
var subForms = [];
var start = findFollowingTR(e, 'dropdownList-container').firstChild.nextSibling, end;
var start = $(findFollowingTR(e, 'dropdownList-container')).down().next(), end;
do { start = start.firstChild; } while (start && start.tagName != 'TR');
if (start && !Element.hasClassName(start,'dropdownList-start'))
......@@ -1242,9 +1250,9 @@ var jenkinsRules = {
function updateDropDownList() {
for (var i = 0; i < subForms.length; i++) {
var show = e.selectedIndex == i;
var f = subForms[i];
var f = $(subForms[i]);
if (show) renderOnDemand(f.nextSibling);
if (show) renderOnDemand(f.next());
f.rowVisibilityGroup.makeInnerVisisble(show);
// TODO: this is actually incorrect in the general case if nested vg uses field-disabled
......@@ -1315,7 +1323,7 @@ var jenkinsRules = {
"A.showDetails" : function(e) {
e.onclick = function() {
this.style.display = 'none';
this.nextSibling.style.display = 'block';
$(this).next().style.display = 'block';
return false;
};
e = null; // avoid memory leak
......@@ -1326,7 +1334,7 @@ var jenkinsRules = {
},
".button-with-dropdown" : function (e) {
new YAHOO.widget.Button(e, { type: "menu", menu: e.nextSibling });
new YAHOO.widget.Button(e, { type: "menu", menu: $(e).next() });
},
"DIV.textarea-preview-container" : function (e) {
......@@ -1505,7 +1513,7 @@ function xor(a,b) {
// used by editableDescription.jelly to replace the description field with a form
function replaceDescription() {
var d = document.getElementById("description");
d.firstChild.nextSibling.innerHTML = "<div class='spinner-right'>loading...</div>";
$(d).down().next().innerHTML = "<div class='spinner-right'>loading...</div>";
new Ajax.Request(
"./descriptionForm",
{
......@@ -1529,9 +1537,9 @@ function replaceDescription() {
function applyNameRef(s,e,id) {
$(id).groupingNode = true;
// s contains the node itself
for(var x=s.nextSibling; x!=e; x=x.nextSibling) {
for(var x=$(s).next(); x!=e; x=x.next()) {
// to handle nested <f:rowSet> correctly, don't overwrite the existing value
if(x.nodeType==1 && x.getAttribute("nameRef")==null)
if(x.getAttribute("nameRef")==null)
x.setAttribute("nameRef",id);
}
}
......@@ -1541,14 +1549,14 @@ function applyNameRef(s,e,id) {
// @param c checkbox element
function updateOptionalBlock(c,scroll) {
// find the start TR
var s = c;
while(!Element.hasClassName(s, "optional-block-start"))
s = s.parentNode;
var s = $(c);
while(!s.hasClassName("optional-block-start"))
s = s.up();
// find the beginning of the rowvg
var vg = s;
while (!Element.hasClassName(vg,"rowvg-start"))
vg = vg.nextSibling;
var vg =s;
while (!vg.hasClassName("rowvg-start"))
vg = vg.next();
var checked = xor(c.checked,Element.hasClassName(c,"negative"));
......@@ -1683,8 +1691,8 @@ function refreshPart(id,url) {
new Ajax.Request(url, {
onSuccess: function(rsp) {
var hist = $(id);
var p = hist.parentNode;
var next = hist.nextSibling;
var p = hist.up();
var next = hist.next();
p.removeChild(hist);
var div = document.createElement('div');
......@@ -1816,10 +1824,9 @@ var repeatableSupport = {
// update CSS classes associated with repeated items.
update : function() {
var children = [];
for( var n=this.container.firstChild; n!=null; n=n.nextSibling )
if(Element.hasClassName(n,"repeated-chunk"))
children.push(n);
var children = $(this.container).childElements().findAll(function (n) {
return n.hasClassName("repeated-chunk");
});
if(children.length==0) {
// noop
......@@ -1873,13 +1880,14 @@ var radioBlockSupport = {
// update one block based on the status of the given radio button
updateSingleButton : function(radio, blockStart, blockEnd) {
var show = radio.checked;
blockStart = $(blockStart);
if (blockStart.getAttribute('hasHelp') == 'true') {
n = blockStart.nextSibling;
n = blockStart.next();
} else {
n = blockStart;
}
while((n = n.nextSibling) != blockEnd) {
while((n = n.next()) != blockEnd) {
n.style.display = show ? "" : "none";
}
}
......@@ -2296,17 +2304,17 @@ var hoverNotification = (function() {
function initContainerDD(e) {
if (!Element.hasClassName(e,"with-drag-drop")) return false;
for (e=e.firstChild; e!=null; e=e.nextSibling) {
if (Element.hasClassName(e,"repeated-chunk"))
$(e).childElements().each(function (e) {
if (e.hasClassName("repeated-chunk"))
prepareDD(e);
}
});
return true;
}
function prepareDD(e) {
var h = e;
var h = $(e);
// locate a handle
while (h!=null && !Element.hasClassName(h,"dd-handle"))
h = h.firstChild ? h.firstChild : h.nextSibling;
while (h!=null && !h.hasClassName("dd-handle"))
h = h.down() ? h.down() : h.next();
if (h!=null) {
var dd = new DragDrop(e);
dd.setHandleElId(h);
......@@ -2533,8 +2541,8 @@ function validateButton(checkUrl,paramList,button) {
}
});
var spinner = Element.up(button,"DIV").nextSibling;
var target = spinner.nextSibling;
var spinner = $(button).up("DIV").next();
var target = spinner.next();
spinner.style.display="block";
new Ajax.Request(checkUrl, {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册