提交 588f9edf 编写于 作者: K kohsuke

- added a widget abstraction to allow plugins to contribute boxes to be displayed on the left.

- moved out the build history to a widget, so that this can be reused elsewhere.


git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@5193 71c3de6d-444a-0410-be80-ed276b4c234a
上级 e903a3bf
......@@ -5,6 +5,8 @@ import hudson.FilePath;
import hudson.Launcher;
import hudson.AbortException;
import hudson.StructuredForm;
import hudson.widgets.HistoryWidget;
import hudson.widgets.BuildHistoryWidget;
import hudson.maven.MavenModule;
import hudson.model.Descriptor.FormException;
import hudson.model.Fingerprint.RangeSet;
......@@ -710,6 +712,11 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
return sib;
}
@Override
protected HistoryWidget createHistoryWidget() {
return new BuildHistoryWidget<R>(this,getBuilds(),HISTORY_ADAPTER);
}
//
//
// actions
......
......@@ -2,12 +2,12 @@ package hudson.model;
import hudson.ExtensionPoint;
import hudson.Util;
import hudson.model.Descriptor.FormException;
import hudson.search.QuickSilver;
import hudson.search.SearchIndexBuilder;
import hudson.search.SearchIndex;
import hudson.search.SearchIndexBuilder;
import hudson.search.SearchItem;
import hudson.search.SearchItems;
import hudson.model.Descriptor.FormException;
import hudson.tasks.BuildTrigger;
import hudson.tasks.LogRotator;
import hudson.util.ChartUtil;
......@@ -19,6 +19,9 @@ import hudson.util.RunList;
import hudson.util.ShiftedCategoryAxis;
import hudson.util.StackedAreaRenderer2;
import hudson.util.TextFile;
import hudson.widgets.HistoryWidget;
import hudson.widgets.Widget;
import hudson.widgets.HistoryWidget.Adapter;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.types.FileSet;
import org.jfree.chart.ChartFactory;
......@@ -31,15 +34,13 @@ import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.StackedAreaRenderer;
import org.jfree.data.category.CategoryDataset;
import org.jfree.ui.RectangleInsets;
import org.kohsuke.stapler.Header;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.awt.Color;
import java.awt.Paint;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
......@@ -256,6 +257,34 @@ public abstract class Job<JobT extends Job<JobT,RunT>, RunT extends Run<JobT,Run
return null;
}
public List<Widget> getWidgets() {
ArrayList<Widget> r = new ArrayList<Widget>();
r.add(createHistoryWidget());
return r;
}
protected HistoryWidget createHistoryWidget() {
return new HistoryWidget<Job,RunT>(this,getBuilds(),HISTORY_ADAPTER);
}
protected static final HistoryWidget.Adapter<Run> HISTORY_ADAPTER = new Adapter<Run>() {
public int compare(Run record, String key) {
return record.getNumber()-Integer.parseInt(key);
}
public String getKey(Run record) {
return String.valueOf(record.getNumber());
}
public boolean isBuilding(Run record) {
return record.isBuilding();
}
public String getNextKey(String key) {
return String.valueOf(Integer.parseInt(key)+1);
}
};
/**
* Renames a job.
*
......@@ -427,6 +456,12 @@ public abstract class Job<JobT extends Job<JobT,RunT>, RunT extends Run<JobT,Run
// try to interpret the token as build number
return _getRuns().get(Integer.valueOf(token));
} catch (NumberFormatException e) {
// try to map that to widgets
for (Widget w : getWidgets()) {
if(w.getUrlName().equals(token))
return w;
}
return super.getDynamic(token,req,rsp);
}
}
......@@ -869,33 +904,6 @@ public abstract class Job<JobT extends Job<JobT,RunT>, RunT extends Run<JobT,Run
rsp.sendRedirect2(req.getContextPath()+'/'+getParent().getUrl()+getShortUrl());
}
/**
* Handles AJAX requests from browsers to update build history.
*
* @param n
* The build number to fetch
*/
public void doAjaxBuildHistoryUpdate( StaplerRequest req, StaplerResponse rsp,
@Header("n") int n ) throws IOException, ServletException {
rsp.setContentType("text/html;charset=UTF-8");
// pick up builds to send back
Collection<? extends RunT> builds = _getRuns().headMap(n-1).values();
req.setAttribute("builds",builds);
int next = getNextBuildNumber();
if(!builds.isEmpty()) {
RunT b = builds.iterator().next();
next = b.getNumber();
if(!b.isBuilding()) next++;
}
rsp.setHeader("n",String.valueOf(next));
req.getView(this,"ajaxBuildHistory.jelly").forward(req,rsp);
}
public void doRssAll( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
rss(req, rsp, " all builds", new RunList(this));
}
......
......@@ -250,7 +250,7 @@ public class Queue extends ResourceController {
*
* @return null if the project is not in the queue.
*/
public synchronized Item getItem(AbstractProject p) {
public synchronized Item getItem(Task p) {
if(blockedProjects.contains(p))
return new Item(new GregorianCalendar(),p,true,false);
if(buildables.contains(p))
......@@ -262,6 +262,15 @@ public class Queue extends ResourceController {
return null;
}
/**
* Left for backward compatibility.
*
* @see #getItem(Task)
*/
public synchronized Item getItem(AbstractProject p) {
return getItem((Task)p);
}
/**
* Returns true if this queue contains the said project.
*/
......
package hudson.util;
import java.util.Collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* Varios {@link Iterator} implementations.
*
* @author Kohsuke Kawaguchi
*/
public class Iterators {
/**
* Returns the empty iterator.
*/
public static <T> Iterator<T> empty() {
return Collections.<T>emptyList().iterator();
}
/**
* Produces {A,B,C,D,E,F} from {{A,B},{C},{},{D,E,F}}.
*/
public static abstract class FlattenIterator<U,T> implements Iterator<U> {
private final Iterator<? extends T> core;
private Iterator<U> cur;
protected FlattenIterator(Iterator<? extends T> core) {
this.core = core;
cur = Collections.<U>emptyList().iterator();
}
protected abstract Iterator<U> expand(T t);
public boolean hasNext() {
while(!cur.hasNext()) {
if(!core.hasNext())
return false;
cur = expand(core.next());
}
return true;
}
public U next() {
if(!hasNext()) throw new NoSuchElementException();
return cur.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
}
}
package hudson.widgets;
import hudson.model.Hudson;
import hudson.model.Queue.Item;
import hudson.model.Queue.Task;
import java.util.List;
/**
* Displays the build history on the side panel.
*
* <p>
* This widget enhances {@link HistoryWidget} by groking the notion
* that {@link #owner} can be in the queue toward the next build.
*
* @author Kohsuke Kawaguchi
*/
public class BuildHistoryWidget<T> extends HistoryWidget<Task,T> {
/**
* @param owner
* The parent model object that owns this widget.
*/
public BuildHistoryWidget(Task owner, Iterable<T> baseList,Adapter<? super T> adapter) {
super(owner,baseList, adapter);
}
/**
* Returns the queue item if the owner is scheduled for execution in the queue.
*/
public Item getQueuedItem() {
return Hudson.getInstance().getQueue().getItem(owner);
}
}
package hudson.widgets;
import hudson.Functions;
import hudson.model.ModelObject;
import hudson.model.Run;
import org.kohsuke.stapler.Header;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Displays the history of records (normally {@link Run}s) on the side panel.
*
* @author Kohsuke Kawaguchi
*/
public class HistoryWidget<O extends ModelObject,T> extends Widget {
/**
* The given data model of records.
*/
public Iterable<T> baseList;
/**
* Indicates the next build number that client ajax should fetch.
*/
private String nextBuildNumberToFetch;
/**
* URL of the {@link #owner}.
*/
public final String baseUrl;
public final O owner;
private boolean trimmed;
public final Adapter<? super T> adapter;
/**
* @param owner
* The parent model object that owns this widget.
*/
public HistoryWidget(O owner, Iterable<T> baseList, Adapter<? super T> adapter) {
this.adapter = adapter;
this.baseList = baseList;
this.baseUrl = Functions.getNearestAncestorUrl(Stapler.getCurrentRequest(),owner);
this.owner = owner;
}
public String getUrlName() {
return "buildHistory";
}
/**
* The records to be rendered this time.
*/
public Iterable<T> getRenderList() {
if(trimmed) {
List<T> lst;
if (baseList instanceof List) {
lst = (List<T>) baseList;
if(lst.size()>THRESHOLD)
return lst.subList(0,THRESHOLD);
trimmed=false;
return lst;
} else {
lst = new ArrayList<T>(THRESHOLD);
Iterator<T> itr = baseList.iterator();
while(lst.size()<=THRESHOLD && itr.hasNext())
lst.add(itr.next());
trimmed = itr.hasNext();
return lst;
}
} else
return baseList;
}
public boolean isTrimmed() {
return trimmed;
}
public void setTrimmed(boolean trimmed) {
this.trimmed = trimmed;
}
/**
* Handles AJAX requests from browsers to update build history.
*
* @param n
* The build 'number' to fetch. This is string because various variants
* uses non-numbers as the build key.
*/
public void doAjax( StaplerRequest req, StaplerResponse rsp,
@Header("n") String n ) throws IOException, ServletException {
rsp.setContentType("text/html;charset=UTF-8");
// pick up builds to send back
List<T> items = new ArrayList<T>();
for (T t : baseList) {
if(adapter.compare(t,n)>=0)
items.add(t);
else
break;
}
baseList = items;
if(!items.isEmpty()) {
T b = items.get(0);
n = adapter.getKey(b);
if(!adapter.isBuilding(b))
n = adapter.getNextKey(n);
}
rsp.setHeader("n",n);
req.getView(this,"ajaxBuildHistory.jelly").forward(req,rsp);
}
private static final int THRESHOLD = 10; // while debugging
public String getNextBuildNumberToFetch() {
return nextBuildNumberToFetch;
}
public void setNextBuildNumberToFetch(String nextBuildNumberToFetch) {
this.nextBuildNumberToFetch = nextBuildNumberToFetch;
}
public interface Adapter<T> {
/**
* If record is newer than the key, return a positive number.
*/
int compare(T record, String key);
String getKey(T record);
boolean isBuilding(T record);
String getNextKey(String key);
}
}
package hudson.widgets;
import hudson.ExtensionPoint;
/**
* Box to be rendered in the side panel.
*
*
* <h2>Views</h2>
* <ul>
* <li><b>index.jelly</b> should display the widget. It should have:
* &lt;l:pane width="2" title="..."> ...body... &lt;/l:pane> structure.
*
* @author Kohsuke Kawaguchi
* @since 1.146
*/
public abstract class Widget implements ExtensionPoint {
/**
* Gets the URL path name.
*
* <p>
* For example, if this method returns "xyz", and if the parent object
* (that this widget is associated with) is bound to /foo/bar/zot,
* then this widget object will be exposed to /foo/bar/zot/xyz.
*
* <p>
* This method is useful when the widget needs to expose additional URLs,
* for example for serving AJAX requests.
*
* <p>
* This method should return a string that's unique among other {@link Widget}s.
* The default implementation returns the unqualified class name.
*/
public String getUrlName() {
return getClass().getSimpleName();
}
}
......@@ -26,6 +26,8 @@
<st:include page="actions.jelly" />
</l:tasks>
<st:include page="buildHistory.jelly" />
<j:forEach var="w" items="${it.widgets}">
<st:include it="${w}" page="index.jelly" />
</j:forEach>
</l:side-panel>
</j:jelly>
\ No newline at end of file
<!--
Used by buildHistory.jelly to render one build record.
@builds : builds to be displayed
upon return, export "nBuild" to the parent scope indicating the build number
that needs to be checked for new status in the next run.
-->
<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">
<j:set var="url" value="${h.getNearestAncestorUrl(request,it)}"/>
<j:set var="nBuild" value="${it.nextBuildNumber}"/>
<!-- pending build -->
<j:set var="pending" value="${it.queueItem}" />
<j:if test="${pending!=null}">
<j:set var="id" value="${h.generateId()}"/>
<tr class="build-row transitive" id="${id}">
<td nowrap="nowrap">
<img width="16" height="16" src="${imagesURL}/16x16/grey.gif" alt="pending" /><st:nbsp/>
#${it.nextBuildNumber}
</td>
<td nowrap="nowrap" tooltip="${pending.why}">
(pending)
</td>
</tr>
</j:if>
<!-- build history -->
<j:forEach var="build" items="${builds}">
<j:set var="link" value="${url}/${build.number}/" />
<tr class="build-row ${h.ifThenElse(build.building,'transitive',null)}">
<td nowrap="nowrap">
<img width="16" height="16" src="${imagesURL}/16x16/${build.buildStatusUrl}" alt="${build.iconColor.description}" /><st:nbsp/>
#${build.number}
</td>
<td nowrap="nowrap">
<a class="tip" href="${link}">
<i:formatDate value="${build.timestamp.time}" type="both" dateStyle="medium" timeStyle="medium"/>
</a>
<j:if test="${build.keepLog}">
<st:nbsp/>
<img width="16" height="16"
title="${build.whyKeepLog}"
src="${imagesURL}/16x16/lock.gif"/>
</j:if>
</td>
</tr>
<j:if test="${build.building}">
<j:set var="nBuild" value="${build.number}"/>
<tr class="transitive"><td></td><td style="padding:0">
<table class="middle-align">
<tr><td>
<t:buildProgressBar build="${build}"/>
</td><td style="padding:0">
<l:isAdmin>
<a href="${link}stop"><img src="${imagesURL}/16x16/stop.gif" alt="[cancel]"/></a>
</l:isAdmin>
</td></tr>
</table>
</td></tr>
</j:if>
<j:if test="${!empty build.description}">
<tr>
<td></td>
<td style="padding:0;white-space:normal">
${build.truncatedDescription}
</td>
</tr>
</j:if>
</j:forEach>
<j:set var="parentScope" value="${context.parent.parent.variables}"/>
<j:if test="${parentScope!=null}">
<j:set target="${parentScope}" property="nBuild" value="${nBuild}" /><!-- export -->
</j:if>
</j:jelly>
\ No newline at end of file
<!--
Render build histories.
-->
<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">
<!-- pending build -->
<j:set var="pending" value="${it.queuedItem}" />
<j:if test="${pending!=null}">
<j:set var="id" value="${h.generateId()}"/>
<tr class="build-row transitive" id="${id}">
<td nowrap="nowrap">
<img width="16" height="16" src="${imagesURL}/16x16/grey.gif" alt="pending" /><st:nbsp/>
#${it.owner.nextBuildNumber}
</td>
<td nowrap="nowrap" tooltip="${pending.why}">
(pending)
</td>
</tr>
</j:if>
<st:include page="/hudson/widgets/HistoryWidget/entries.jelly" />
</j:jelly>
\ No newline at end of file
......@@ -3,7 +3,7 @@
<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">
<l:ajax>
<table>
<st:include page="buildHistoryEntries.jelly" />
<st:include page="entries.jelly" />
</table>
</l:ajax>
</j:jelly>
\ No newline at end of file
......@@ -4,6 +4,6 @@
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout">
<l:ajax>
<j:set var="all" value="${true}"/>
<st:include page="buildHistory.jelly" />
<st:include page="index.jelly" />
</l:ajax>
</j:jelly>
\ No newline at end of file
<!--
Render build histories.
-->
<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">
<j:set target="${it}" property="nextBuildNumberToFetch" value="${it.owner.nextBuildNumber}"/>
<!-- build history -->
<j:forEach var="build" items="${it.renderList}">
<st:include page="entry.jelly" />
</j:forEach>
</j:jelly>
\ No newline at end of file
<!--
Render a single build history entry indicated by ${build}
-->
<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">
<j:set var="link" value="${it.baseUrl}/${build.number}/" />
<tr class="build-row ${h.ifThenElse(build.building,'transitive',null)}">
<td nowrap="nowrap">
<img width="16" height="16" src="${imagesURL}/16x16/${build.buildStatusUrl}" alt="${build.iconColor.description}" /><st:nbsp/>
#${build.number}
</td>
<td nowrap="nowrap">
<a class="tip" href="${link}">
<i:formatDate value="${build.timestamp.time}" type="both" dateStyle="medium" timeStyle="medium"/>
</a>
<j:if test="${build.keepLog}">
<st:nbsp/>
<img width="16" height="16"
title="${build.whyKeepLog}"
src="${imagesURL}/16x16/lock.gif"/>
</j:if>
</td>
</tr>
<j:if test="${build.building}">
<j:set target="it" property="nextBuildNumberToFetch" value="${it.owner.nextBuildNumber}"/>
<tr class="transitive"><td></td><td style="padding:0">
<table class="middle-align">
<tr><td>
<t:buildProgressBar build="${build}"/>
</td><td style="padding:0">
<l:isAdmin>
<a href="${link}stop"><img src="${imagesURL}/16x16/stop.gif" alt="[cancel]"/></a>
</l:isAdmin>
</td></tr>
</table>
</td></tr>
</j:if>
<j:if test="${!empty build.description}">
<tr>
<td></td>
<td style="padding:0;white-space:normal">
${build.truncatedDescription}
</td>
</tr>
</j:if>
</j:jelly>
\ No newline at end of file
<!--
History of runs.
variables:
all="true" : display all builds. Otherwise limit it to most recent N builds.
-->
<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">
<j:set var="url" value="${h.getNearestAncestorUrl(request,it)}"/>
<j:set target="${it}" property="trimmed" value="${all==null}"/>
<l:pane width="2" title="
&lt;div style='float:right'>(&lt;a href='${url}/buildTimeTrend'>trend&lt;/a>)&lt;/div>Build History
&lt;div style='float:right'>(&lt;a href='${it.baseUrl}/buildTimeTrend'>trend&lt;/a>)&lt;/div>Build History
" id="buildHistory">
<!-- decide if we want to display all or just top 50 -->
<j:set var="builds" value="${it.builds}"/>
<j:if test="${!all &amp;&amp; builds.size() &gt; 50}">
<j:set var="builds" value="${builds.subList(0,50)}"/>
<j:set var="trimmed" value="${true}" />
</j:if>
<!-- build history -->
<st:include page="buildHistoryEntries.jelly" />
<st:include page="entries.jelly" />
<!--
If we are trimming the build history because it's too long,
show the link to fetch all the records by using AJAX.
-->
<j:if test="${trimmed}">
<j:if test="${it.trimmed}">
<tr class="build-row">
<td colspan="2" align="right">
<script>
......@@ -36,11 +21,11 @@
box.innerHTML = '<img src="${imagesURL}/spinner.gif" />';
// then actually fetch the HTML
new Ajax.Request("allBuildHistory",{
new Ajax.Request("${it.baseUrl}/buildHistory/all",{
method: "get",
onComplete: function(rsp,_) {
<!-- neither outerHTML nor responseXML works in Firefox 2.0 -->
var hist = $('buildHistory');
var hist = $$("buildHistory");
var p = hist.parentNode;
var next = hist.nextSibling;
p.removeChild(hist);
......@@ -65,13 +50,13 @@
-->
<tr class="build-row">
<td colspan="2" align="right">
<a href="${rootURL}/${it.url}rssAll"><img src="${imagesURL}/atom.gif" border="0"/> for all</a>
<a href="${it.baseUrl}/rssAll"><img src="${imagesURL}/atom.gif" border="0"/> for all</a>
<st:nbsp/>
<a href="${rootURL}/${it.url}rssFailed"><img src="${imagesURL}/atom.gif" border="0"/> for failures</a>
<a href="${it.baseUrl}/rssFailed"><img src="${imagesURL}/atom.gif" border="0"/> for failures</a>
</td>
</tr>
</l:pane>
<script defer="true">
updateBuildHistory(${nBuild});
updateBuildHistory("${it.baseUrl}/buildHistory/ajax",${it.nextBuildNumberToFetch});
</script>
</j:jelly>
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册