diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index 12d3078927..47fc2c6ad3 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -2478,6 +2478,33 @@ public class Queue extends ResourceController implements Saveable { } } + /** + * Mock queue Item used for unit testing. + *

+ * No other clean way to effectively mock Item. + */ + @SuppressWarnings("unchecked") + public static final class MockItem extends Item { + + public MockItem(long id) { + super(null, Collections.EMPTY_LIST, id, null); + } + + @Override + public CauseOfBlockage getCauseOfBlockage() { + return null; + } + + @Override + void enter(Queue q) { + } + + @Override + boolean leave(Queue q) { + return true; + } + } + private static final Logger LOGGER = Logger.getLogger(Queue.class.getName()); /** diff --git a/core/src/main/java/hudson/widgets/BuildHistoryWidget.java b/core/src/main/java/hudson/widgets/BuildHistoryWidget.java index ec80e138a8..871e888a6f 100644 --- a/core/src/main/java/hudson/widgets/BuildHistoryWidget.java +++ b/core/src/main/java/hudson/widgets/BuildHistoryWidget.java @@ -23,10 +23,13 @@ */ package hudson.widgets; +import hudson.model.ModelObject; import jenkins.model.Jenkins; import hudson.model.Queue.Item; import hudson.model.Queue.Task; +import org.apache.commons.collections.IteratorUtils; +import java.util.Collection; import java.util.LinkedList; import java.util.List; @@ -67,4 +70,18 @@ public class BuildHistoryWidget extends HistoryWidget { } return list; } + + @Override + public HistoryPageFilter getPage() { + final HistoryPageFilter historyPageFilter = newPageFilter(); + + List items = new LinkedList(); + + items.addAll((Collection) getQueuedItems()); + items.addAll(IteratorUtils.toList(baseList.iterator())); + historyPageFilter.add(items); + historyPageFilter.widget = this; + + return historyPageFilter; + } } diff --git a/core/src/main/java/hudson/widgets/HistoryPageEntry.java b/core/src/main/java/hudson/widgets/HistoryPageEntry.java new file mode 100644 index 0000000000..d819650301 --- /dev/null +++ b/core/src/main/java/hudson/widgets/HistoryPageEntry.java @@ -0,0 +1,67 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets; + +import hudson.model.Queue; +import hudson.model.Run; + +import javax.annotation.Nonnull; + +/** + * @author tom.fennelly@gmail.com + */ +public class HistoryPageEntry { + + private final T entry; + + public HistoryPageEntry(T entry) { + this.entry = entry; + } + + public T getEntry() { + return entry; + } + + public long getEntryId() { + return getEntryId(entry); + } + + protected static long getEntryId(@Nonnull Object entry) { + if (entry instanceof Queue.Item) { + return ((Queue.Item) entry).getId(); + } else if (entry instanceof Run) { + Run run = (Run) entry; + long queueId = run.getQueueId(); + if (queueId == Run.QUEUE_ID_UNKNOWN) { + // Backward compatibility. This is a run from before the Queue.Item IDs + // were mapped onto their resulting Run instance. + return (Integer.MIN_VALUE + run.getNumber()); + } else { + return queueId; + } + } else { + return Run.QUEUE_ID_UNKNOWN; + } + } +} diff --git a/core/src/main/java/hudson/widgets/HistoryPageFilter.java b/core/src/main/java/hudson/widgets/HistoryPageFilter.java new file mode 100644 index 0000000000..b5ac6cfaf9 --- /dev/null +++ b/core/src/main/java/hudson/widgets/HistoryPageFilter.java @@ -0,0 +1,280 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets; + +import hudson.model.Job; +import hudson.model.Queue; +import hudson.model.Run; +import hudson.widgets.buildsearch.BuildSearchParamProcessor; +import hudson.widgets.buildsearch.BuildSearchParamProcessorList; +import hudson.widgets.buildsearch.BuildSearchParams; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * History page filter. + * + * @author tom.fennelly@gmail.com + */ +public class HistoryPageFilter { + + private final int maxEntries; + private Long newerThan; + private Long olderThan; + private BuildSearchParamProcessorList searchParamProcessorList; + + // Need to use different Lists for Queue.Items and Runs because + // we need access to them separately in the jelly files for rendering. + public final List> queueItems = new ArrayList>(); + public final List> runs = new ArrayList>(); + + public boolean hasUpPage = false; // there are newer builds than on this page + public boolean hasDownPage = false; // there are older builds than on this page + public long nextBuildNumber; + public HistoryWidget widget; + + public long newestOnPage = Long.MIN_VALUE; // see updateNewestOldest() + public long oldestOnPage = Long.MAX_VALUE; // see updateNewestOldest() + + /** + * Create a history page filter instance. + * + * @param maxEntries The max number of entries allowed for the page. + */ + public HistoryPageFilter(int maxEntries) { + this.maxEntries = maxEntries; + } + + /** + * Set the 'newerThan' queue ID. + * @param newerThan Queue IDs newer/greater than this queue ID take precedence on this page. + */ + public void setNewerThan(Long newerThan) { + if (olderThan != null) { + throw new UnsupportedOperationException("Cannot set 'newerThan'. 'olderThan' already set."); + } + this.newerThan = newerThan; + } + + /** + * Set the 'olderThan' queue ID. + * @param olderThan Queue IDs older/less than this queue ID take precedence on this page. + */ + public void setOlderThan(Long olderThan) { + if (newerThan != null) { + throw new UnsupportedOperationException("Cannot set 'olderThan'. 'newerThan' already set."); + } + this.olderThan = olderThan; + } + + /** + * Set the search string used to narrow the filtered set of builds. + * @param searchString The search string. + */ + public void setSearchString(@Nonnull String searchString) { + BuildSearchParams searchParams = new BuildSearchParams(searchString); + this.searchParamProcessorList = new BuildSearchParamProcessorList(searchParams); + } + + /** + * Add build items to the History page. + * + * @param items The items to be added. Assumes the list of items are in descending queue ID order i.e. newest first. + */ + public void add(@Nonnull List items) { + if (items.isEmpty()) { + return; + } + + sort(items); + + nextBuildNumber = getNextBuildNumber(items.get(0)); + + if (newerThan == null && olderThan == null) { + // Just return the first page of entries (newest) + for (T item : items) { + add(item); + if (isFull()) { + break; + } + } + hasDownPage = (items.size() > maxEntries); + } else if (newerThan != null) { + int toFillCount = getFillCount(); + if (toFillCount > 0) { + // Locate the point in the items list where the 'newerThan' build item is. Once located, + // add a max of 'getFillCount' build items before that build item. + long newestInList = HistoryPageEntry.getEntryId(items.get(0)); + long oldestInList = HistoryPageEntry.getEntryId(items.get(items.size() - 1)); + int newerThanIdx = -1; + + if (newerThan > newestInList) { + // Nothing newer + } else if (newerThan >= oldestInList) { + // newerThan is within the range of items in the item list. + // go through the list and locate the cut-off point. + for (int i = 0; i < items.size(); i++) { + T item = items.get(i); + if (HistoryPageEntry.getEntryId(item) <= newerThan) { + newerThanIdx = i; + break; + } + } + } else if (newerThan < oldestInList) { + newerThanIdx = items.size(); + } + + if (newerThanIdx != -1) { + if (newerThanIdx <= maxEntries) { + // If there's less than a full page of items newer than "newerThan", then it's ok to + // fill the page with items older than "newerThan". + int itemCountToAdd = Math.min(toFillCount, items.size()); + for (int i = 0; i < itemCountToAdd; i++) { + add(items.get(i)); + } + } else { + // There's more than a full page of items newer than "newerThan". + for (int i = (newerThanIdx - toFillCount); i < newerThanIdx; i++) { + add(items.get(i)); + } + hasUpPage = true; + } + // if there are items after the "newerThan" item, then we + // can page down. + hasDownPage = (items.size() > newerThanIdx + 1); + } else { + // All builds are older than newerThan ? + hasDownPage = true; + } + } + } else if (olderThan != null) { + for (int i = 0; i < items.size(); i++) { + T item = items.get(i); + if (HistoryPageEntry.getEntryId(item) >= olderThan) { + hasUpPage = true; + } else { + add(item); + if (isFull()) { + // This page is full but there may be more builds older + // than the oldest on this page. + hasDownPage = (i + 1 < items.size()); + break; + } + } + } + } + } + + public int size() { + return queueItems.size() + runs.size(); + } + + private void sort(List items) { + // Queue items can start building out of order with how they got added to the queue. Sorting them + // before adding to the page. They'll still get displayed before the building items coz they end + // up in a different list in HistoryPageFilter. + Collections.sort(items, new Comparator() { + @Override + public int compare(T o1, T o2) { + long o1QID = HistoryPageEntry.getEntryId(o1); + long o2QID = HistoryPageEntry.getEntryId(o2); + + if (o1QID < o2QID) { + return 1; + } else if (o1QID == o2QID) { + return 0; + } else { + return -1; + } + } + }); + } + + private long getNextBuildNumber(@Nonnull T entry) { + if (entry instanceof Queue.Item) { + Queue.Task task = ((Queue.Item) entry).task; + if (task instanceof Job) { + return ((Job) task).getNextBuildNumber(); + } + } else if (entry instanceof Run) { + return ((Run) entry).getParent().getNextBuildNumber(); + } + + // TODO maybe this should be an error? + return HistoryPageEntry.getEntryId(entry) + 1; + } + + private void addQueueItem(Queue.Item item) { + updateNewestOldest(item.getId()); + queueItems.add(new HistoryPageEntry(item)); + } + + private void addRun(Run run) { + updateNewestOldest(run.getQueueId()); + runs.add(new HistoryPageEntry(run)); + } + + private void updateNewestOldest(long queueId) { + newestOnPage = Math.max(newestOnPage, queueId); + oldestOnPage = Math.min(oldestOnPage, queueId); + } + + private boolean add(T entry) { + // Purposely not calling isFull(). May need to add a greater number of entries + // to the page initially, newerThan then cutting it back down to size using cutLeading() + if (entry instanceof Queue.Item) { + Queue.Item item = (Queue.Item) entry; + if (searchParamProcessorList != null && !searchParamProcessorList.fitsSearchParams(item)) { + return false; + } + addQueueItem(item); + return true; + } else if (entry instanceof Run) { + Run run = (Run) entry; + if (searchParamProcessorList != null && !searchParamProcessorList.fitsSearchParams(run)) { + return false; + } + addRun(run); + return true; + } + return false; + } + + private boolean isFull() { + return (size() >= maxEntries); + } + + /** + * Get the number of items required to fill the page. + * + * @return The number of items required to fill the page. + */ + private int getFillCount() { + return Math.max(0, (maxEntries - size())); + } +} diff --git a/core/src/main/java/hudson/widgets/HistoryWidget.java b/core/src/main/java/hudson/widgets/HistoryWidget.java index a2ec051839..90f86d3fcf 100644 --- a/core/src/main/java/hudson/widgets/HistoryWidget.java +++ b/core/src/main/java/hudson/widgets/HistoryWidget.java @@ -28,6 +28,7 @@ import hudson.model.ModelObject; import hudson.model.Run; import hudson.util.Iterators; +import org.apache.commons.collections.IteratorUtils; import org.kohsuke.stapler.Header; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.Stapler; @@ -37,6 +38,7 @@ import org.kohsuke.stapler.StaplerResponse; import javax.servlet.ServletException; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -73,6 +75,10 @@ public class HistoryWidget extends Widget { public final Adapter adapter; + final Long newerThan; + final Long olderThan; + final String searchString; + /** * First transient build record. Everything >= this will be discarded when AJAX call is made. */ @@ -83,10 +89,14 @@ public class HistoryWidget extends Widget { * The parent model object that owns this widget. */ public HistoryWidget(O owner, Iterable baseList, Adapter adapter) { + StaplerRequest currentRequest = Stapler.getCurrentRequest(); this.adapter = adapter; this.baseList = baseList; - this.baseUrl = Functions.getNearestAncestorUrl(Stapler.getCurrentRequest(),owner); + this.baseUrl = Functions.getNearestAncestorUrl(currentRequest,owner); this.owner = owner; + this.newerThan = getPagingParam(currentRequest, "newer-than"); + this.olderThan = getPagingParam(currentRequest, "older-than"); + this.searchString = currentRequest.getParameter("search");; } /** @@ -105,11 +115,13 @@ public class HistoryWidget extends Widget { return firstTransientBuildKey; } - private Iterable updateFirstTransientBuildKey(Iterable source) { + private Iterable> updateFirstTransientBuildKey(Iterable> source) { String key=null; - for (T t : source) - if(adapter.isBuilding(t)) - key = adapter.getKey(t); + for (HistoryPageEntry t : source) { + if(adapter.isBuilding(t.getEntry())) { + key = adapter.getKey(t.getEntry()); + } + } firstTransientBuildKey = key; return source; } @@ -117,29 +129,63 @@ public class HistoryWidget extends Widget { /** * The records to be rendered this time. */ - public Iterable getRenderList() { + public Iterable> getRenderList() { if(trimmed) { - List lst; - if (baseList instanceof List) { - lst = (List) baseList; - if(lst.size()>THRESHOLD) - return updateFirstTransientBuildKey(lst.subList(0,THRESHOLD)); - trimmed=false; - return updateFirstTransientBuildKey(lst); + List> pageEntries = toPageEntries(baseList); + if(pageEntries.size() > THRESHOLD) { + return updateFirstTransientBuildKey(pageEntries.subList(0,THRESHOLD)); } else { - lst = new ArrayList(THRESHOLD); - Iterator itr = baseList.iterator(); - while(lst.size()<=THRESHOLD && itr.hasNext()) - lst.add(itr.next()); - trimmed = itr.hasNext(); // if we don't have enough items in the base list, setting this to false will optimize the next getRenderList() invocation. - return updateFirstTransientBuildKey(lst); + trimmed=false; + return updateFirstTransientBuildKey(pageEntries); } } else { - // to prevent baseList's concrete type from getting picked up by in view - return updateFirstTransientBuildKey(Iterators.wrap(baseList)); + // to prevent baseList's concrete type from getting picked up by in view + return updateFirstTransientBuildKey(toPageEntries(baseList)); } } + private List> toPageEntries(Iterable historyItemList) { + Iterator iterator = historyItemList.iterator(); + + if (!iterator.hasNext()) { + return Collections.EMPTY_LIST; + } + + List> pageEntries = new ArrayList>(); + while (iterator.hasNext()) { + pageEntries.add(new HistoryPageEntry(iterator.next())); + } + + return pageEntries; + } + + /** + * Render a "page" of records. + */ + public HistoryPageFilter getPage() { + HistoryPageFilter historyPageFilter = newPageFilter(); + + historyPageFilter.add(IteratorUtils.toList(baseList.iterator())); + historyPageFilter.widget = this; + return historyPageFilter; + } + + protected HistoryPageFilter newPageFilter() { + HistoryPageFilter historyPageFilter = new HistoryPageFilter(THRESHOLD); + + if (newerThan != null) { + historyPageFilter.setNewerThan(newerThan); + } else if (olderThan != null) { + historyPageFilter.setOlderThan(olderThan); + } + + if (searchString != null) { + historyPageFilter.setSearchString(searchString); + } + + return historyPageFilter; + } + public boolean isTrimmed() { return trimmed; } @@ -156,46 +202,48 @@ public class HistoryWidget extends Widget { * uses non-numbers as the build key. */ public void doAjax( StaplerRequest req, StaplerResponse rsp, - @Header("n") String n ) throws IOException, ServletException { - - if (n==null) throw HttpResponses.error(SC_BAD_REQUEST,new Exception("Missing the 'n' HTTP header")); + @Header("n") String n ) throws IOException, ServletException { rsp.setContentType("text/html;charset=UTF-8"); // pick up builds to send back List items = new ArrayList(); - String nn=null; // we'll compute next n here + if (n != null) { + String nn=null; // we'll compute next n here - // list up all builds >=n. - for (T t : baseList) { - if(adapter.compare(t,n)>=0) { - items.add(t); - if(adapter.isBuilding(t)) + // list up all builds >=n. + for (T t : baseList) { + if(adapter.compare(t,n)>=0) { + items.add(t); + if(adapter.isBuilding(t)) nn = adapter.getKey(t); // the next fetch should start from youngest build in progress - } else - break; - } - - if (nn==null) { - if (items.isEmpty()) { - // nothing to report back. next fetch should retry the same 'n' - nn=n; - } else { - // every record fetched this time is frozen. next fetch should start from the next build - nn=adapter.getNextKey(adapter.getKey(items.get(0))); + } else + break; } + + if (nn==null) { + if (items.isEmpty()) { + // nothing to report back. next fetch should retry the same 'n' + nn=n; + } else { + // every record fetched this time is frozen. next fetch should start from the next build + nn=adapter.getNextKey(adapter.getKey(items.get(0))); + } + } + + baseList = items; + + rsp.setHeader("n",nn); + firstTransientBuildKey = nn; // all builds >= nn should be marked transient } - baseList = items; - - rsp.setHeader("n",nn); - firstTransientBuildKey = nn; // all builds >= nn should be marked transient - - req.getView(this,"ajaxBuildHistory.jelly").forward(req,rsp); + HistoryPageFilter page = getPage(); + updateFirstTransientBuildKey(page.runs); + req.getView(page,"ajaxBuildHistory.jelly").forward(req,rsp); } - private static final int THRESHOLD = Integer.getInteger(HistoryWidget.class.getName()+".threshold",30); + static final int THRESHOLD = Integer.getInteger(HistoryWidget.class.getName()+".threshold",30); public String getNextBuildNumberToFetch() { return nextBuildNumberToFetch; @@ -214,4 +262,16 @@ public class HistoryWidget extends Widget { boolean isBuilding(T record); String getNextKey(String key); } + + private Long getPagingParam(StaplerRequest currentRequest, String name) { + String paramVal = currentRequest.getParameter(name); + if (paramVal == null) { + return null; + } + try { + return new Long(paramVal); + } catch (NumberFormatException nfe) { + return null; + } + } } diff --git a/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessor.java b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessor.java new file mode 100644 index 0000000000..14a2063ea8 --- /dev/null +++ b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessor.java @@ -0,0 +1,91 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets.buildsearch; + +import hudson.model.Queue; +import hudson.model.Run; + +import java.util.ArrayList; +import java.util.List; + +/** + * Search param/term processor. + * + *

+ * Implementations created through a {@link BuildSearchParamProcessorFactory} implementation. + * + * @author tom.fennelly@gmail.com + */ +public abstract class BuildSearchParamProcessor { + + /** + * Does the supplied {@link Queue.Item} fit the {@link BuildSearchParams} used to create this + * + *

+ * Implementations should call {@link #fitsSearchParams(Object)} after extracting the appropriate data from + * the {@link Queue.Item}. + * + * {@link BuildSearchParamProcessor} instance. + * @param item The {@link Queue.Item} to test. + * @return {@code true} if the {@link Queue.Item} fits, otherwise {@code false}. + */ + public abstract boolean fitsSearchParams(Queue.Item item); + + /** + * Does the supplied {@link Run} fit the {@link BuildSearchParams} used to create this + * + *

+ * Implementations should call {@link #fitsSearchParams(Object)} after extracting the appropriate data from + * the {@link Run}. + * + * {@link BuildSearchParamProcessor} instance. + * @param run The {@link Run} to test. + * @return {@code true} if the {@link Run} fits, otherwise {@code false}. + */ + public abstract boolean fitsSearchParams(Run run); + + /** + * Does the supplied "data" (extracted from a {@link Queue.Item} or {@link Run}) fit the {@link BuildSearchParams} + * used to create this {@link BuildSearchParamProcessor} instance. + * + *

+ * This method implementation should do all of the work in terms of testing the extracted {@link Queue.Item} + * or {@link Run} data against the search criteria used to create the instance. The + * {@link #fitsSearchParams(hudson.model.Queue.Item)} and {@link #fitsSearchParams(hudson.model.Run)} + * implementations should call this function after extracting the appropriate data from the {@link Queue.Item} + * or {@link Run}. + * + *

+ * This method makes unit testing of the {@link BuildSearchParamProcessor} implementation a little easier in + * that it allows the implementation to be hidden in the parent {@link BuildSearchParamProcessorFactory factory} + * class, plus allows testing without the need to create {@link Queue.Item} or {@link Run} instances, which can't + * be created without spinning a {@link jenkins.model.Jenkins} instance via a JenkinsRule (which in turn requires + * test classes to be in the test harness etc). + * + * + * @param data The data to test. + * @return {@code true} if the {@link Run} fits, otherwise {@code false}. + */ + public abstract boolean fitsSearchParams(T data); +} diff --git a/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorFactory.java b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorFactory.java new file mode 100644 index 0000000000..6d8e2d8cd1 --- /dev/null +++ b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorFactory.java @@ -0,0 +1,109 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets.buildsearch; + +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.widgets.buildsearch.processors.DateProcessorFactory; +import hudson.widgets.buildsearch.processors.DescriptionProcessorFactory; +import hudson.widgets.buildsearch.processors.NameProcessorFactory; +import hudson.widgets.buildsearch.processors.ResultProcessorFactory; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Implementations of this class create {@link BuildSearchParamProcessor} instances from a set + * of {@link BuildSearchParams}. The set of {@link BuildSearchParamProcessor} instances are then used + * by the {@link hudson.widgets.HistoryPageFilter} class to filter the build history (via an instance of + * {@link BuildSearchParamProcessorList}). + * + *

+ * Each {@link BuildSearchParamProcessor} implementation processes one or more search terms e.g. the + * {@link BuildSearchParamProcessor} instance created by the {@link NameProcessorFactory} checks the + * build {@link hudson.model.Queue.Item} or {@link hudson.model.Run} name using the "name:" search token(s) + * provided in the search, while the {@link DateProcessorFactory} checks date using one or both of the + * "date-from:" and "date-to:" tokens. + * + * @author tom.fennelly@gmail.com + */ +public abstract class BuildSearchParamProcessorFactory implements ExtensionPoint { + + private static List coreParamProcessorFactories = new ArrayList(); + + static { + // Statically adding the core BuildSearchParamProcessorFactory impls Vs adding via @Extension. + // Makes testing easier coz no need to create a JenkinsRule instance + put in test harness etc. + // Plugins etc can still contribute via @Extension. + coreParamProcessorFactories.add(new NameProcessorFactory()); + coreParamProcessorFactories.add(new DescriptionProcessorFactory()); + coreParamProcessorFactories.add(new ResultProcessorFactory()); + coreParamProcessorFactories.add(new DateProcessorFactory()); + } + + /** + * Get the list of search terms that this processor is interested in. + * + * @return List of search terms. + */ + public abstract String[] getSearchTerms(); + + /** + * Create the {@link BuildSearchParamProcessor} instance using the supplied set of search parameters. + * + * @param searchParams The parsed search parameters. + * @return The {@link BuildSearchParamProcessor} instance to be used for searching, or {@code null} if the search + * parameters are such that {@link BuildSearchParamProcessor} instance of this type is not needed. + */ + public abstract @CheckForNull BuildSearchParamProcessor createProcessor(@Nonnull BuildSearchParams searchParams); + + public static List all() { + + // TODO: Not convinced we want to bother with @Extension + + ExtensionList extensions = ExtensionList.lookup(BuildSearchParamProcessorFactory.class); + if (extensions.isEmpty()) { + // No BuildSearchParamProcessorFactory defined by @Extension + return coreParamProcessorFactories; + } else { + // One or more BuildSearchParamProcessorFactory defined by @Extension. + List combined = new ArrayList(coreParamProcessorFactories); + combined.addAll(extensions); + return combined; + } + } + + public static Set getAllSearchTerms() { + Set allSearchTerms = new LinkedHashSet(); + for (BuildSearchParamProcessorFactory paramProcessorFactory : all()) { + allSearchTerms.addAll(Arrays.asList(paramProcessorFactory.getSearchTerms())); + } + return allSearchTerms; + } +} diff --git a/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorList.java b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorList.java new file mode 100644 index 0000000000..2c0dee50cf --- /dev/null +++ b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorList.java @@ -0,0 +1,91 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets.buildsearch; + +import hudson.model.Queue; +import hudson.model.Run; + +import javax.annotation.Nonnull; +import java.util.ArrayList; + +/** + * Wrapper class for the list of {@link hudson.widgets.buildsearch.BuildSearchParamProcessor} needed to process/apply + * a set of {@link hudson.widgets.buildsearch.BuildSearchParams}. + * + * @author tom.fennelly@gmail.com + */ +public class BuildSearchParamProcessorList { + + private final ArrayList processors; + + /** + * Create a {@link BuildSearchParamProcessorList} instance from a set of {@link hudson.widgets.buildsearch.BuildSearchParams}. + * @param searchParams The search parameters to use for creating the {@link BuildSearchParamProcessorList}. + */ + public BuildSearchParamProcessorList(@Nonnull BuildSearchParams searchParams) { + processors = new ArrayList(); + for (BuildSearchParamProcessorFactory factory : BuildSearchParamProcessorFactory.all()) { + BuildSearchParamProcessor processor = factory.createProcessor(searchParams); + if (processor != null) { + processors.add(processor); + } + } + } + + public ArrayList getProcessors() { + return processors; + } + + /** + * Does the supplied {@link hudson.model.Queue.Item} fit the {@link BuildSearchParams} used to create this + * {@link BuildSearchParamProcessorList} instance. + * @param item The {@link hudson.model.Queue.Item} to test. + * @return {@code true} if the {@link hudson.model.Queue.Item} fits all of the {@link BuildSearchParamProcessor}s + * on this {@link BuildSearchParamProcessorList}, otherwise {@code false}. + */ + public boolean fitsSearchParams(@Nonnull Queue.Item item) { + for (BuildSearchParamProcessor processor : processors) { + if (!processor.fitsSearchParams(item)) { + return false; + } + } + return true; + } + + /** + * Does the supplied {@link hudson.model.Run} fit the {@link BuildSearchParams} used to create this + * {@link BuildSearchParamProcessorList} instance. + * @param run The {@link hudson.model.Run} to test. + * @return {@code true} if the {@link hudson.model.Run} fits all of the {@link BuildSearchParamProcessor}s + * on this {@link BuildSearchParamProcessorList}, otherwise {@code false}. + */ + public boolean fitsSearchParams(@Nonnull Run run) { + for (BuildSearchParamProcessor processor : processors) { + if (!processor.fitsSearchParams(run)) { + return false; + } + } + return true; + } +} diff --git a/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParams.java b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParams.java new file mode 100644 index 0000000000..277bd43b29 --- /dev/null +++ b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParams.java @@ -0,0 +1,174 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets.buildsearch; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * @author tom.fennelly@gmail.com + */ +public class BuildSearchParams { + + private final String searchString; + private final Map> searchParamsSets = newParamMap(); + private boolean parsingComplete = false; + + private static Map> newParamMap() { + Map> map = new LinkedHashMap>(); + for (String searchTerm: BuildSearchParamProcessorFactory.getAllSearchTerms()) { + map.put(searchTerm, new ArrayList()); + } + return Collections.unmodifiableMap(map); + } + + public class BuildSearchParam { + + public String searchTerm; + public String searchTermToken; + + private String param; + // The start index (in this.searchString) of the search parameter (including the searchTermToken itself). + int startIndex; + // The end index (in this.searchString) of the search parameter, -1 being the end of the string. + int endIndex = -1; + + BuildSearchParam(String searchTerm, String searchTermToken, int startIndex) { + this.searchTerm = searchTerm; + this.searchTermToken = searchTermToken; + this.startIndex = startIndex; + } + + public String get() { + if (this.param != null) { + return this.param; + } + + String param = searchString.substring(getEndOfTokenIndex(), getParamEndIndex()); + param = param.trim(); + + if (parsingComplete) { + // We can cache the value if parsing is complete. + this.param = param; + } + + return param; + } + + private boolean isWhitespace() { + int paramStartIndex = getEndOfTokenIndex(); + int paramEndIndex = getParamEndIndex(); + for (int i = paramStartIndex; i < paramEndIndex; i++) { + if (!Character.isWhitespace(searchString.charAt(i))) { + return false; + } + } + return true; + } + + private int getEndOfTokenIndex() { + return (startIndex + searchTermToken.length()); + } + + private int getParamEndIndex() { + return (endIndex != -1 ? endIndex : searchString.length()); + } + + @Override + public String toString() { + return searchTerm + ":'" + get() + "'"; + } + } + + public BuildSearchParams(@Nonnull String searchString) { + this.searchString = searchString; + if (searchString.trim().length() > 0) { + parseSearchString(); + } + } + + public List getParams(@Nonnull String searchTerm) { + List params = searchParamsSets.get(searchTerm); + if (params == null) { + throw new IllegalArgumentException(String.format("Unknown search term '%s'.", searchTerm)); + } + return params; + } + + @Override + public String toString() { + return searchParamsSets.toString(); + } + + private void parseSearchString() { + // Step 1: find all search params in the searchString, recording their startIndex + List allSearchParams = new ArrayList(); + String searchStringLower = searchString.toLowerCase(); + for (String searchTerm : BuildSearchParamProcessorFactory.getAllSearchTerms()) { + String searchTermToken = searchTerm + ":"; + + int startIndex = 0; + while((startIndex = searchStringLower.indexOf(searchTermToken, startIndex)) != -1) { + allSearchParams.add(new BuildSearchParam(searchTerm, searchTermToken, startIndex)); + startIndex += searchTerm.length(); + } + } + + // Step 2: Remove all empty/whitespace params i.e. token defined but no param + + // Step 2: sort all search params by their startIndex. This will then make it easy to find the endIndexes (step 3) + Collections.sort(allSearchParams, new Comparator() { + @Override + public int compare(BuildSearchParam param1, BuildSearchParam param2) { + return param1.startIndex - param2.startIndex; + } + }); + + // Step 3: find the endIndex for each search param. + // That's easy: it's the startIndex of the next search param, or the end of the searchString for the last. + int numParams = allSearchParams.size(); + for (int i = 0; i < numParams; i++) { + BuildSearchParam searchParam = allSearchParams.get(i); + + if (i == numParams - 1) { + // last one, so leave endIndex as -1 + } else { + BuildSearchParam nextSearchParam = allSearchParams.get(i + 1); + searchParam.endIndex = nextSearchParam.startIndex; + } + + // And add it to the top level searchParamsSets Map, but only if it's not whitespace. + if (!searchParam.isWhitespace()) { + searchParamsSets.get(searchParam.searchTerm).add(searchParam); + } + } + + parsingComplete = true; + } +} diff --git a/core/src/main/java/hudson/widgets/buildsearch/README.md b/core/src/main/java/hudson/widgets/buildsearch/README.md new file mode 100644 index 0000000000..f539a9fda3 --- /dev/null +++ b/core/src/main/java/hudson/widgets/buildsearch/README.md @@ -0,0 +1,34 @@ +__TODO__: Move to jenkins-ci web/wiki if changes accepted. + +## Search Terms + +* [__name:__] : Is contained in the name of the build. +* [__desc:__] : Is contained in the description of the build.d. +* [__result:__] : Is the "result" of the build (SUCCESS, UNSTABLE, FAILURE, NOT_BUILT, ABORTED). +* [__date-from:__] and/or [__date-to:__] : Build date is before, after or between. Date format is *yyyy-MM-yy*. Date is start of day i.e. 00:00. Also supports some shorthands e.g. "today", "yesterday", "1 day", "2 days", "2 weeks", "2 months". + +Different search terms are __AND__ together to result in a matching Build, while multiples of the same search term are __OR__'d together e.g. + +* result: FAILURE desc: staging desc: production + +Is interpreted as: + +* (result: FAILURE) AND (desc: staging OR desc: production) + +## Examples + +Builds that contain the words "staging" or "production" in the description: + +* desc: staging desc: production + +Failed builds in last week (7 days): + +* result: FAILURE date-from: 1 week + +Failed builds since a specific date: + +* result: FAILURE date-from: 2015-03-31 + +Unstable builds in March: + +* result: UNSTABLE date-from: 2015-03-01 date-to: 2015-04-01 diff --git a/core/src/main/java/hudson/widgets/buildsearch/processors/DateProcessorFactory.java b/core/src/main/java/hudson/widgets/buildsearch/processors/DateProcessorFactory.java new file mode 100644 index 0000000000..95a5679298 --- /dev/null +++ b/core/src/main/java/hudson/widgets/buildsearch/processors/DateProcessorFactory.java @@ -0,0 +1,212 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets.buildsearch.processors; + +import hudson.model.Queue; +import hudson.model.Run; +import hudson.widgets.buildsearch.BuildSearchParamProcessor; +import hudson.widgets.buildsearch.BuildSearchParamProcessorFactory; +import hudson.widgets.buildsearch.BuildSearchParams; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +/** + * Search build history by {@link hudson.model.Queue.Item#getInQueueSince()} + * or {@link hudson.model.Run#getTimeInMillis()} using one or both of the + * "date-from:" and "date-to:" tokens. Also supports some shorthands such as "today" + * + * @author tom.fennelly@gmail.com + */ +public class DateProcessorFactory extends BuildSearchParamProcessorFactory { + + public static String LONG_DATE_FORMAT = "yyyy-MM-dd"; + public static String MEDIUM_DATE_FORMAT = "yy-MM-dd"; + public static String SHORT_DATE_FORMAT = "MM-dd"; + + public static final long DAY = 1000 * 60 * 60 * 24; + public static final long WEEK = DAY * 7; + public static final long MONTH = WEEK * 4; + + private static final String[] SEARCH_TERMS = new String[] {"date-from", "date-to"}; + + /** + * {@inheritDoc} + */ + @Override + public String[] getSearchTerms() { + return SEARCH_TERMS; + } + + /** + * {@inheritDoc} + */ + @Override + public BuildSearchParamProcessor createProcessor(BuildSearchParams searchParams) { + final DateFromTo dateFromTo = new DateFromTo(searchParams); + + if (dateFromTo.dateFrom == null && dateFromTo.dateTo == null) { + return null; + } + + return new BuildSearchParamProcessor() { + + @Override + public boolean fitsSearchParams(Long timeInMillis) { + if (dateFromTo.dateFrom != null && timeInMillis < dateFromTo.dateFrom) { + return false; + } + if (dateFromTo.dateTo != null && timeInMillis > dateFromTo.dateTo) { + return false; + } + return true; + } + + @Override + public boolean fitsSearchParams(Queue.Item item) { + return fitsSearchParams(item.getInQueueSince()); + } + @Override + public boolean fitsSearchParams(Run run) { + return fitsSearchParams(run.getTimeInMillis()); + } + }; + } + + private class DateFromTo { + + private Long dateFrom; + private Long dateTo; + + private DateFromTo(BuildSearchParams searchParams) { + final List dateFromParams = searchParams.getParams(SEARCH_TERMS[0]); + final List dateToParams = searchParams.getParams(SEARCH_TERMS[1]); + + if (dateFromParams.isEmpty() && dateToParams.isEmpty()) { + // none of the date search terms are specified + return; + } + + // Only supports spec of a single pair of "date-from:" and "date-to:" search params. + if (!dateFromParams.isEmpty()) { + dateFrom = toTimeInMillis(dateFromParams.get(0).get()); + } else { + dateFrom = null; + } + + if (!dateToParams.isEmpty()) { + dateTo = toTimeInMillis(dateToParams.get(0).get()); + } else { + dateTo = null; + } + + if (dateFrom != null && dateTo != null && dateFrom > dateTo) { + dateTo = null; + } + } + } + + // make this available to test code too + public static Long toTimeInMillis(String date) { + date = date.toLowerCase(); + + if (date.equals("today")) { + return getToday(); + } else if (date.equals("yesterday")) { + return getYesterday(); + } else if (date.endsWith("day") || date.endsWith("days")) { + return getToday() - (extractMultiple(date) * DAY); + } else if (date.endsWith("week") || date.endsWith("weeks")) { + return getToday() - (extractMultiple(date) * WEEK); + } else if (date.endsWith("month") || date.endsWith("months")) { + return getToday() - (extractMultiple(date) * MONTH); + } + + try { + // SimpleDateFormat is not thread safe. What a PITA !!! + date = normalize(date); + if (date.length() == LONG_DATE_FORMAT.length()) { + return new SimpleDateFormat(LONG_DATE_FORMAT).parse(date).getTime(); + } else if (date.length() == MEDIUM_DATE_FORMAT.length()) { + return new SimpleDateFormat(MEDIUM_DATE_FORMAT).parse(date).getTime(); + } else { + return null; + } + } catch (ParseException e) { + return null; + } + } + + private static long extractMultiple(String date) { + if (Character.isDigit(date.charAt(0))) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < date.length(); i++) { + char c = date.charAt(i); + if (!Character.isDigit(c)) { + break; + } + builder.append(c); + } + return new Long(builder.toString()); + } + return 1; + } + + // make this available to test code too + public static String normalize(String date) { + // In case the user forgot the format and used '/' instead of '-' as the delimiter. + date = date.replace('/', '-'); + // In case there are spaces + date = date.replace(" ", ""); + date = date.toLowerCase(); + + if (date.length() == SHORT_DATE_FORMAT.length()) { + date = getCurrentYearString() + "-" + date; + } + + return date; + } + + public static String getCurrentYearString() { + return new SimpleDateFormat("yyyy").format(new Date()); + } + + public static long getToday() { + long now = System.currentTimeMillis(); + long partOfToday = now % DAY; + long startOfToday = now - partOfToday; + + return startOfToday; + } + + public static long getTomorrow() { + return getToday() + DAY; + } + + public static long getYesterday() { + return getToday() - DAY; + } +} diff --git a/core/src/main/java/hudson/widgets/buildsearch/processors/DescriptionProcessorFactory.java b/core/src/main/java/hudson/widgets/buildsearch/processors/DescriptionProcessorFactory.java new file mode 100644 index 0000000000..60b917df16 --- /dev/null +++ b/core/src/main/java/hudson/widgets/buildsearch/processors/DescriptionProcessorFactory.java @@ -0,0 +1,87 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets.buildsearch.processors; + +import hudson.model.Queue; +import hudson.model.Run; +import hudson.widgets.buildsearch.BuildSearchParamProcessor; +import hudson.widgets.buildsearch.BuildSearchParamProcessorFactory; +import hudson.widgets.buildsearch.BuildSearchParams; + +import java.util.List; + +/** + * Search build history by {@link hudson.model.Run} description. + * + * @author tom.fennelly@gmail.com + */ +public class DescriptionProcessorFactory extends BuildSearchParamProcessorFactory { + + private static final String[] SEARCH_TERMS = new String[] {"desc"}; // Build description + + /** + * {@inheritDoc} + */ + @Override + public String[] getSearchTerms() { + return SEARCH_TERMS; + } + + /** + * {@inheritDoc} + */ + @Override + public BuildSearchParamProcessor createProcessor(BuildSearchParams searchParams) { + final List descParams = searchParams.getParams(SEARCH_TERMS[0]); + + if (descParams.isEmpty()) { + // "desc" search term not specified in search + return null; + } + + return new BuildSearchParamProcessor() { + + @Override + public boolean fitsSearchParams(String description) { + // It fits if it contains any of the specified "desc" search terms. + for (BuildSearchParams.BuildSearchParam nameParam : descParams) { + if (description.contains(nameParam.get())) { + return true; + } + } + return false; + } + + @Override + public boolean fitsSearchParams(Queue.Item item) { + // Queue items don't have a description. + return false; + } + @Override + public boolean fitsSearchParams(Run run) { + return fitsSearchParams(run.getDescription()); + } + }; + } +} diff --git a/core/src/main/java/hudson/widgets/buildsearch/processors/NameProcessorFactory.java b/core/src/main/java/hudson/widgets/buildsearch/processors/NameProcessorFactory.java new file mode 100644 index 0000000000..fff9f24747 --- /dev/null +++ b/core/src/main/java/hudson/widgets/buildsearch/processors/NameProcessorFactory.java @@ -0,0 +1,86 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets.buildsearch.processors; + +import hudson.model.Queue; +import hudson.model.Run; +import hudson.widgets.buildsearch.BuildSearchParamProcessor; +import hudson.widgets.buildsearch.BuildSearchParamProcessorFactory; +import hudson.widgets.buildsearch.BuildSearchParams; + +import java.util.List; + +/** + * Search build history by {@link hudson.model.Queue.Item} or {@link hudson.model.Run} name. + * + * @author tom.fennelly@gmail.com + */ +public class NameProcessorFactory extends BuildSearchParamProcessorFactory { + + private static final String[] SEARCH_TERMS = new String[] {"name"}; // Build name + + /** + * {@inheritDoc} + */ + @Override + public String[] getSearchTerms() { + return SEARCH_TERMS; + } + + /** + * {@inheritDoc} + */ + @Override + public BuildSearchParamProcessor createProcessor(BuildSearchParams searchParams) { + final List nameParams = searchParams.getParams(SEARCH_TERMS[0]); + + if (nameParams.isEmpty()) { + // "name" search term not specified in search + return null; + } + + return new BuildSearchParamProcessor() { + + @Override + public boolean fitsSearchParams(String name) { + // It fits if it contains any of the specified "name" search terms. + for (BuildSearchParams.BuildSearchParam nameParam : nameParams) { + if (name.contains(nameParam.get())) { + return true; + } + } + return false; + } + + @Override + public boolean fitsSearchParams(Queue.Item item) { + return fitsSearchParams(item.getDisplayName()); + } + @Override + public boolean fitsSearchParams(Run run) { + return fitsSearchParams(run.getDisplayName()); + } + }; + } +} diff --git a/core/src/main/java/hudson/widgets/buildsearch/processors/ResultProcessorFactory.java b/core/src/main/java/hudson/widgets/buildsearch/processors/ResultProcessorFactory.java new file mode 100644 index 0000000000..e7e69a912b --- /dev/null +++ b/core/src/main/java/hudson/widgets/buildsearch/processors/ResultProcessorFactory.java @@ -0,0 +1,93 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets.buildsearch.processors; + +import hudson.model.Queue; +import hudson.model.Result; +import hudson.model.Run; +import hudson.widgets.buildsearch.BuildSearchParamProcessor; +import hudson.widgets.buildsearch.BuildSearchParamProcessorFactory; +import hudson.widgets.buildsearch.BuildSearchParams; + +import java.util.ArrayList; +import java.util.List; + +/** + * Search build history by {@link hudson.model.Run} result. + * + * @author tom.fennelly@gmail.com + */ +public class ResultProcessorFactory extends BuildSearchParamProcessorFactory { + + private static final String[] SEARCH_TERMS = new String[] {"result"}; // Build result. TODO: Could do "status" too or maybe that's different + + /** + * {@inheritDoc} + */ + @Override + public String[] getSearchTerms() { + return SEARCH_TERMS; + } + + /** + * {@inheritDoc} + */ + @Override + public BuildSearchParamProcessor createProcessor(BuildSearchParams searchParams) { + List resultParams = searchParams.getParams(SEARCH_TERMS[0]); + + if (resultParams.isEmpty()) { + // "result" search term not specified in search + return null; + } + + final List interestingResults = new ArrayList(); + for (BuildSearchParams.BuildSearchParam resultParam : resultParams) { + Result result = Result.fromString(resultParam.get()); + // Result.fromString returns Result.FAILURE if there's no match. We don't want that. + if (result.toString().equalsIgnoreCase(resultParam.get())) { + interestingResults.add(result); + } + } + + return new BuildSearchParamProcessor() { + + @Override + public boolean fitsSearchParams(Result result) { + // It fits if it's any of the specified "result" search terms. + return interestingResults.contains(result); + } + + @Override + public boolean fitsSearchParams(Queue.Item item) { + // Queue items don't have a Result. + return false; + } + @Override + public boolean fitsSearchParams(Run run) { + return fitsSearchParams(run.getResult()); + } + }; + } +} diff --git a/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly b/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly index 639002d537..54309319f0 100644 --- a/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly +++ b/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly @@ -27,52 +27,6 @@ THE SOFTWARE. Render build histories. --> - - - - - - - - -

-
- -
- -
- #${queuedItems.size()==1 ? it.owner.nextBuildNumber - : it.owner.nextBuildNumber+queuedItems.size()-i-1} -
-
-
- - - - (${%pending}—) - - - (${%pending}) - - - -
- -
-
-
-
-
- - - -
-
-
- - - - - - + + diff --git a/core/src/main/resources/hudson/widgets/HistoryPageFilter/ajaxBuildHistory.jelly b/core/src/main/resources/hudson/widgets/HistoryPageFilter/ajaxBuildHistory.jelly new file mode 100644 index 0000000000..6fdf095c51 --- /dev/null +++ b/core/src/main/resources/hudson/widgets/HistoryPageFilter/ajaxBuildHistory.jelly @@ -0,0 +1,41 @@ + + + + + + + + + + +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/widgets/HistoryPageFilter/entries.jelly b/core/src/main/resources/hudson/widgets/HistoryPageFilter/entries.jelly new file mode 100644 index 0000000000..622a838738 --- /dev/null +++ b/core/src/main/resources/hudson/widgets/HistoryPageFilter/entries.jelly @@ -0,0 +1,29 @@ + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/widgets/HistoryPageFilter/queue-items.jelly b/core/src/main/resources/hudson/widgets/HistoryPageFilter/queue-items.jelly new file mode 100644 index 0000000000..be46285a13 --- /dev/null +++ b/core/src/main/resources/hudson/widgets/HistoryPageFilter/queue-items.jelly @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + +
+
+ +
+ +
+ #${queuedItems.size()==1 ? it.widget.owner.nextBuildNumber + : it.widget.owner.nextBuildNumber+queuedItems.size()-i-1} +
+
+
+ + + + (${%pending}—) + + + (${%pending}) + + + +
+ +
+
+
+
+
+ + + +
+
+
+ + +
+
+
diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/ajaxBuildHistory.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/ajaxBuildHistory.jelly index 1011d9510e..6684873987 100644 --- a/core/src/main/resources/hudson/widgets/HistoryWidget/ajaxBuildHistory.jelly +++ b/core/src/main/resources/hudson/widgets/HistoryWidget/ajaxBuildHistory.jelly @@ -25,10 +25,6 @@ THE SOFTWARE. - - - - -
-
+ + \ No newline at end of file diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/entries.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/entries.jelly index 2a49d5a698..8c07368d68 100644 --- a/core/src/main/resources/hudson/widgets/HistoryWidget/entries.jelly +++ b/core/src/main/resources/hudson/widgets/HistoryWidget/entries.jelly @@ -27,9 +27,9 @@ THE SOFTWARE. --> - + - - + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly index 44e3f664e3..215cb4d54a 100644 --- a/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly +++ b/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly @@ -26,10 +26,12 @@ THE SOFTWARE. Render a single build history entry indicated by ${build} --> - - - - + + + + + +
@@ -42,7 +44,7 @@ THE SOFTWARE. - +
@@ -59,7 +61,7 @@ THE SOFTWARE.
- +
diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly index f43ba94068..d42b56abb2 100644 --- a/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly +++ b/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly @@ -51,10 +51,33 @@ THE SOFTWARE. - + +
+
+ + + +
+ - - + + + + + + + +
+ x + + + +
+ + + + + - ${%More ...} -
+
+
diff --git a/core/src/test/java/hudson/widgets/HistoryPageFilterTest.java b/core/src/test/java/hudson/widgets/HistoryPageFilterTest.java new file mode 100644 index 0000000000..ab9eeb51c2 --- /dev/null +++ b/core/src/test/java/hudson/widgets/HistoryPageFilterTest.java @@ -0,0 +1,320 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets; + +import hudson.model.Job; +import hudson.model.ModelObject; +import hudson.model.Queue; +import hudson.model.Result; +import hudson.model.Run; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author tom.fennelly@gmail.com + */ +public class HistoryPageFilterTest { + + /** + * No items. + */ + @Test + public void test_latest_empty_page() { + HistoryPageFilter historyPageFilter = newPage(5, null, null); + List itemList = new ArrayList(); + + historyPageFilter.add(itemList); + Assert.assertEquals(false, historyPageFilter.hasUpPage); + Assert.assertEquals(false, historyPageFilter.hasDownPage); + Assert.assertEquals(true, historyPageFilter.queueItems.isEmpty()); + Assert.assertEquals(true, historyPageFilter.runs.isEmpty()); + } + + /** + * Latest/top page where total number of items < the max page size. + */ + @Test + public void test_latest_partial_page() throws IOException { + HistoryPageFilter historyPageFilter = newPage(5, null, null); + List itemList = new ArrayList(); + + itemList.addAll(newRuns(1, 2)); + itemList.addAll(newQueueItems(3, 4)); + + // want to make sure the list items are ordered by id in descending order + Assert.assertEquals(1, HistoryPageEntry.getEntryId(itemList.get(0))); + historyPageFilter.add(itemList); + Assert.assertEquals(4, HistoryPageEntry.getEntryId(itemList.get(0))); + + Assert.assertEquals(false, historyPageFilter.hasUpPage); + Assert.assertEquals(false, historyPageFilter.hasDownPage); + Assert.assertEquals(2, historyPageFilter.queueItems.size()); + Assert.assertEquals(2, historyPageFilter.runs.size()); + + Assert.assertEquals(4, historyPageFilter.queueItems.get(0).getEntryId()); + Assert.assertEquals(4, historyPageFilter.newestOnPage); + Assert.assertEquals(3, historyPageFilter.queueItems.get(1).getEntryId()); + Assert.assertEquals(2, historyPageFilter.runs.get(0).getEntryId()); + Assert.assertEquals(1, historyPageFilter.runs.get(1).getEntryId()); + Assert.assertEquals(1, historyPageFilter.oldestOnPage); + } + + /** + * Latest/top page where total number of items > the max page size. + */ + @Test + public void test_latest_longer_list() throws IOException { + HistoryPageFilter historyPageFilter = newPage(5, null, null); + List itemList = new ArrayList(); + + itemList.addAll(newRuns(1, 10)); + itemList.addAll(newQueueItems(11, 12)); + + historyPageFilter.add(itemList); + + Assert.assertEquals(false, historyPageFilter.hasUpPage); + Assert.assertEquals(true, historyPageFilter.hasDownPage); + Assert.assertEquals(2, historyPageFilter.queueItems.size()); + Assert.assertEquals(3, historyPageFilter.runs.size()); + + Assert.assertEquals(12, historyPageFilter.queueItems.get(0).getEntryId()); + Assert.assertEquals(12, historyPageFilter.newestOnPage); + Assert.assertEquals(10, historyPageFilter.runs.get(0).getEntryId()); + } + + /** + * Test olderThan (page down) when set to id greater than newest (should never happen). Should be same as not + * specifying newerThan/olderThan. + */ + @Test + public void test_olderThan_gt_newest() throws IOException { + HistoryPageFilter historyPageFilter = newPage(5, null, 11L); + List itemList = new ArrayList(); + + itemList.addAll(newRuns(1, 10)); + historyPageFilter.add(itemList); + + Assert.assertEquals(false, historyPageFilter.hasUpPage); + Assert.assertEquals(true, historyPageFilter.hasDownPage); + Assert.assertEquals(5, historyPageFilter.runs.size()); + + Assert.assertEquals(10, historyPageFilter.newestOnPage); + Assert.assertEquals(6, historyPageFilter.oldestOnPage); + } + + /** + * Test olderThan (page down) when set to id less than the oldest (should never happen). Should just give an + * empty list of builds. + */ + @Test + public void test_olderThan_lt_oldest() throws IOException { + HistoryPageFilter historyPageFilter = newPage(5, null, 0L); + List itemList = new ArrayList(); + + itemList.addAll(newRuns(1, 10)); + historyPageFilter.add(itemList); + + Assert.assertEquals(true, historyPageFilter.hasUpPage); + Assert.assertEquals(false, historyPageFilter.hasDownPage); + Assert.assertEquals(0, historyPageFilter.runs.size()); + } + + /** + * Test olderThan (page down) when set to an id close to the oldest in the list (where + * there's less than a full page older than the supplied olderThan arg). + */ + @Test + public void test_olderThan_leaving_part_page() throws IOException { + HistoryPageFilter historyPageFilter = newPage(5, null, 4L); + List itemList = new ArrayList(); + + itemList.addAll(newRuns(1, 10)); + historyPageFilter.add(itemList); + + Assert.assertEquals(true, historyPageFilter.hasUpPage); + Assert.assertEquals(false, historyPageFilter.hasDownPage); + + // Should only be 3 runs on the page (oldest 3) + Assert.assertEquals(3, historyPageFilter.runs.size()); + + Assert.assertEquals(3, historyPageFilter.newestOnPage); + Assert.assertEquals(1, historyPageFilter.oldestOnPage); + } + + /** + * Test olderThan (page down) when set to an id in the middle. Should be a page up and a page down. + */ + @Test + public void test_olderThan_mid_page() throws IOException { + HistoryPageFilter historyPageFilter = newPage(5, null, 8L); + List itemList = new ArrayList(); + + itemList.addAll(newRuns(1, 10)); + historyPageFilter.add(itemList); + + Assert.assertEquals(true, historyPageFilter.hasUpPage); + Assert.assertEquals(true, historyPageFilter.hasDownPage); + Assert.assertEquals(5, historyPageFilter.runs.size()); + + Assert.assertEquals(7, historyPageFilter.newestOnPage); + Assert.assertEquals(3, historyPageFilter.oldestOnPage); + } + + /** + * Test newerThan (page up) when set to id greater than newest (should never happen). Should be an empty list. + */ + @Test + public void test_newerThan_gt_newest() throws IOException { + HistoryPageFilter historyPageFilter = newPage(5, 11L, null); + List itemList = new ArrayList(); + + itemList.addAll(newRuns(1, 10)); + historyPageFilter.add(itemList); + + Assert.assertEquals(false, historyPageFilter.hasUpPage); + Assert.assertEquals(true, historyPageFilter.hasDownPage); + Assert.assertEquals(0, historyPageFilter.runs.size()); + } + + /** + * Test newerThan (page up) when set to id less than the oldest (should never happen). Should give the oldest + * set of builds. + */ + @Test + public void test_newerThan_lt_oldest() throws IOException { + HistoryPageFilter historyPageFilter = newPage(5, 0L, null); + List itemList = new ArrayList(); + + itemList.addAll(newRuns(1, 10)); + historyPageFilter.add(itemList); + + Assert.assertEquals(true, historyPageFilter.hasUpPage); + Assert.assertEquals(false, historyPageFilter.hasDownPage); + Assert.assertEquals(5, historyPageFilter.runs.size()); + + Assert.assertEquals(5, historyPageFilter.newestOnPage); + Assert.assertEquals(1, historyPageFilter.oldestOnPage); + } + + /** + * Test newerThan (page up) mid range nearer the oldest build in the list. + */ + @Test + public void test_newerThan_near_oldest() throws IOException { + HistoryPageFilter historyPageFilter = newPage(5, 3L, null); + List itemList = new ArrayList(); + + itemList.addAll(newRuns(1, 10)); + historyPageFilter.add(itemList); + + Assert.assertEquals(true, historyPageFilter.hasUpPage); + Assert.assertEquals(true, historyPageFilter.hasDownPage); + Assert.assertEquals(5, historyPageFilter.runs.size()); + + Assert.assertEquals(8, historyPageFilter.newestOnPage); + Assert.assertEquals(4, historyPageFilter.oldestOnPage); + } + + /** + * Test newerThan (page up) mid range nearer the newest build in the list. This works a little different + * in that it will put the 2 builds newer than newerThan on the page and then fill the remaining slots on the + * page with builds equal to and older i.e. it return the newest/latest builds. + */ + @Test + public void test_newerThan_near_newest() throws IOException { + HistoryPageFilter historyPageFilter = newPage(5, 8L, null); + List itemList = new ArrayList(); + + itemList.addAll(newRuns(1, 10)); + historyPageFilter.add(itemList); + + Assert.assertEquals(false, historyPageFilter.hasUpPage); + Assert.assertEquals(true, historyPageFilter.hasDownPage); + Assert.assertEquals(5, historyPageFilter.runs.size()); + + Assert.assertEquals(10, historyPageFilter.newestOnPage); + Assert.assertEquals(6, historyPageFilter.oldestOnPage); + } + + private List newQueueItems(long startId, long endId) { + List items = new ArrayList(); + for (long queueId = startId; queueId <= endId; queueId++) { + items.add(new Queue.MockItem(queueId)); + } + return items; + } + + private List newRuns(long startId, long endId) throws IOException { + List runs = new ArrayList(); + for (long queueId = startId; queueId <= endId; queueId++) { + runs.add(new MockRun(queueId)); + } + return runs; + } + + private HistoryPageFilter newPage(int maxEntries, Long newerThan, Long olderThan) { + HistoryPageFilter pageFilter = new HistoryPageFilter(maxEntries); + if (newerThan != null) { + pageFilter.setNewerThan(newerThan); + } else if (olderThan != null) { + pageFilter.setOlderThan(olderThan); + } + return pageFilter; + } + + @SuppressWarnings("unchecked") + private static class MockRun extends Run { + private final long queueId; + + public MockRun(long queueId) throws IOException { + super(Mockito.mock(Job.class)); + this.queueId = queueId; + } + + @Override + public int compareTo(Run o) { + return 0; + } + + @Override + public Result getResult() { + return result; + } + + @Override + public boolean isBuilding() { + return false; + } + + @Override + public long getQueueId() { + return queueId; + } + } +} diff --git a/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamProcessorTest.java b/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamProcessorTest.java new file mode 100644 index 0000000000..12baebc656 --- /dev/null +++ b/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamProcessorTest.java @@ -0,0 +1,178 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets.buildsearch; + +import hudson.model.Result; +import hudson.widgets.buildsearch.processors.DateProcessorFactory; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +/** + * @author tom.fennelly@gmail.com + */ +public class BuildSearchParamProcessorTest { + + @Test + public void test_name() { + BuildSearchParams searchParams = new BuildSearchParams(" name: Build1 name: Build2 "); + List processors = new BuildSearchParamProcessorList(searchParams).getProcessors(); + + Assert.assertEquals(1, processors.size()); + + BuildSearchParamProcessor processor = processors.get(0); + Assert.assertTrue(processor.fitsSearchParams("Build1 was good")); + Assert.assertTrue(processor.fitsSearchParams("The Build2 not so good")); + Assert.assertFalse(processor.fitsSearchParams("Build3")); + } + + @Test + public void test_description() { + BuildSearchParams searchParams = new BuildSearchParams(" desc: Build1 desc: Build2 "); + List processors = new BuildSearchParamProcessorList(searchParams).getProcessors(); + + Assert.assertEquals(1, processors.size()); + + BuildSearchParamProcessor processor = processors.get(0); + Assert.assertTrue(processor.fitsSearchParams("Build1 was good")); + Assert.assertTrue(processor.fitsSearchParams("The Build2 not so good")); + Assert.assertFalse(processor.fitsSearchParams("Build3")); + } + + @Test + public void test_result() { + BuildSearchParams searchParams = new BuildSearchParams(" result: UNSTABLE result: aborted result: BLAH "); + List processors = new BuildSearchParamProcessorList(searchParams).getProcessors(); + + Assert.assertEquals(1, processors.size()); + + BuildSearchParamProcessor processor = processors.get(0); + Assert.assertTrue(processor.fitsSearchParams(Result.UNSTABLE)); + Assert.assertTrue(processor.fitsSearchParams(Result.ABORTED)); + Assert.assertFalse(processor.fitsSearchParams(Result.SUCCESS)); + Assert.assertFalse(processor.fitsSearchParams(Result.FAILURE)); // need to make sure "BLAH" is not interpreted as FAILURE. See Result.fromString. + } + + @Test + public void test_date_from_and_to_ok() { + BuildSearchParams searchParams = new BuildSearchParams("date-from: 2015-02-20 date-to: 2015-03-20"); + + List processors = new BuildSearchParamProcessorList(searchParams).getProcessors(); + + Assert.assertEquals(1, processors.size()); + + BuildSearchParamProcessor processor = processors.get(0); + + Assert.assertFalse(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-19"))); + Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-20"))); + Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-21"))); + Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-03-19"))); + Assert.assertFalse(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-03-21"))); + } + + /** + * date-to: set to a date before date-from: should result in the date-to: being ignored. + */ + @Test + public void test_date_from_and_to_with_to_before_from() { + BuildSearchParams searchParams = new BuildSearchParams("date-from:2015-02-20 date-to:2015-02-10"); + + List processors = new BuildSearchParamProcessorList(searchParams).getProcessors(); + + Assert.assertEquals(1, processors.size()); + + BuildSearchParamProcessor processor = processors.get(0); + + Assert.assertFalse(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-19"))); + Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-20"))); + Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-21"))); + Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-03-21"))); + } + + @Test + public void test_date_short() { + List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: 02-20")).getProcessors(); + BuildSearchParamProcessor processor = processors.get(0); + + Assert.assertFalse(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis(DateProcessorFactory.getCurrentYearString() + "-02-19"))); + Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis(DateProcessorFactory.getCurrentYearString() + "-02-20"))); + } + + @Test + public void test_date_today() { + List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: today")).getProcessors(); + BuildSearchParamProcessor processor = processors.get(0); + + long now = System.currentTimeMillis(); + Assert.assertFalse(processor.fitsSearchParams(now - DateProcessorFactory.DAY)); + Assert.assertTrue(processor.fitsSearchParams(now)); + Assert.assertTrue(processor.fitsSearchParams(now + DateProcessorFactory.DAY)); + } + + @Test + public void test_date_last_week_to_today() { + List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: week date-to: today")).getProcessors(); + BuildSearchParamProcessor processor = processors.get(0); + + long now = System.currentTimeMillis(); + Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 8))); + Assert.assertTrue(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 7))); + Assert.assertTrue(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 1))); + Assert.assertFalse(processor.fitsSearchParams(now)); + } + + @Test + public void test_date_days() { + List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: 5 days date-to: 1 day")).getProcessors(); + BuildSearchParamProcessor processor = processors.get(0); + + long now = System.currentTimeMillis(); + Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 6))); + Assert.assertTrue(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 5))); + Assert.assertFalse(processor.fitsSearchParams(now)); + } + + @Test + public void test_date_weeks() { + List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: 5 weeks date-to: 1 week")).getProcessors(); + BuildSearchParamProcessor processor = processors.get(0); + + long now = System.currentTimeMillis(); + Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.WEEK * 6))); + Assert.assertTrue(processor.fitsSearchParams(now - (DateProcessorFactory.WEEK * 5))); + Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 6))); + } + + @Test + public void test_date_months() { + List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: 5 months date-to: 1 month")).getProcessors(); + BuildSearchParamProcessor processor = processors.get(0); + + long now = System.currentTimeMillis(); + Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.MONTH * 6))); + Assert.assertTrue(processor.fitsSearchParams(now - (DateProcessorFactory.MONTH * 5))); + Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.MONTH - DateProcessorFactory.DAY))); + } +} diff --git a/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamsTest.java b/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamsTest.java new file mode 100644 index 0000000000..a71f827ed9 --- /dev/null +++ b/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamsTest.java @@ -0,0 +1,73 @@ +/* + * The MIT License + * + * Copyright (c) 2013-2014, 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. + */ +package hudson.widgets.buildsearch; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @author tom.fennelly@gmail.com + */ +public class BuildSearchParamsTest { + + @Test + public void test_no_params() { + Assert.assertEquals("{name=[], desc=[], result=[], date-from=[], date-to=[]}", new BuildSearchParams("").toString()); + Assert.assertEquals("{name=[], desc=[], result=[], date-from=[], date-to=[]}", new BuildSearchParams("blah").toString()); + Assert.assertEquals("{name=[], desc=[], result=[], date-from=[], date-to=[]}", new BuildSearchParams("name: desc: ").toString()); // whitespace only params should be ignored + } + + @Test + public void test_unknown_search_term() { + try { + new BuildSearchParams("").getParams("blah"); + Assert.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + Assert.assertEquals("Unknown search term 'blah'.", e.getMessage()); + } + } + + @Test + public void test_with_params_one_of_each() { + Assert.assertEquals( + "{name=[name:'Build1'], desc=[desc:'something big'], result=[result:'FAILED'], date-from=[], date-to=[]}", + new BuildSearchParams(" result: FAILED Name: Build1 desc: something big ").toString()); + } + + @Test + public void test_with_params_multiples_of_each() { + BuildSearchParams searchParams = + new BuildSearchParams(" result: FAILED name: Build1 desc: something big result:SUCCESS desc: something middle "); + + Assert.assertEquals("[name:'Build1']", + searchParams.getParams("name").toString()); + Assert.assertEquals("[desc:'something big', desc:'something middle']", + searchParams.getParams("desc").toString()); + Assert.assertEquals("[result:'FAILED', result:'SUCCESS']", + searchParams.getParams("result").toString()); + Assert.assertEquals( + "{name=[name:'Build1'], desc=[desc:'something big', desc:'something middle'], result=[result:'FAILED', result:'SUCCESS'], date-from=[], date-to=[]}", + searchParams.toString()); + } +} diff --git a/war/src/main/webapp/css/style.css b/war/src/main/webapp/css/style.css index 31480a0fdc..a1867ea6c4 100644 --- a/war/src/main/webapp/css/style.css +++ b/war/src/main/webapp/css/style.css @@ -967,6 +967,89 @@ table.parameters > tbody:hover { float: right; } +#buildHistoryPage { + position: relative; +} + +#buildHistoryPage .build-search-row td { + position: relative; +} +#buildHistoryPage .build-search-row .clear { + position: absolute; + padding: 0px 4px; + cursor: pointer; + top: 7px; + right: 6px; + opacity: 0.5; +} +#buildHistoryPage .build-search-row .clear:hover { + background-color: #d3d3d3; + border: 1px solid #bbb; + top: 6px; + padding-right: 3px; + border-top-right-radius: 2px; +} +#buildHistoryPage .build-search-row input { + width: 100%; + padding-left: 5px; + padding-right: 20px; +} +#buildHistoryPage .build-search-row table { + width: 100%; +} +#buildHistoryPage .build-search-row .build-search-help { + width: 20px; +} +#buildHistoryPage .build-search-row .icon-help { + position: absolute; + cursor: pointer; + top: 6px; +} + +#buildHistoryPageNav { + position: absolute; + right: -21px; + top: 41px; + border: 1px solid; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + visibility: hidden; +} + +#buildHistoryPageNav .nav { + width: 16px; + height: 16px; + background: no-repeat; + background-position: center; + background-size: 16px 16px; + margin: 4px 2px; + cursor: pointer; +} + +#buildHistoryPageNav.mouseOverSidePanel { + visibility: visible; + border-color: #f0f0f0 !important; +} +#buildHistoryPageNav.mouseOverSidePanelBuildHistory { + border-color: #bbb !important; +} +#buildHistoryPageNav:hover { + border-color: #bbb !important; +} + +#buildHistoryPageNav .pageOne { + background-image: url(../images/16x16/bullseye.png); +} +#buildHistoryPageNav .pageUp { + background-image: url(../images/16x16/go_up.png); +} +#buildHistoryPageNav .pageDown { + background-image: url(../images/16x16/go_down.png); +} +#buildHistoryPageNav .find { + background-image: url(../images/16x16/find.png); +} + .build-row { padding: 3px 4px 3px 4px; } diff --git a/war/src/main/webapp/images/16x16/bullseye.png b/war/src/main/webapp/images/16x16/bullseye.png new file mode 100644 index 0000000000..442ccf26dd Binary files /dev/null and b/war/src/main/webapp/images/16x16/bullseye.png differ diff --git a/war/src/main/webapp/images/16x16/find.png b/war/src/main/webapp/images/16x16/find.png new file mode 100644 index 0000000000..20d07f7050 Binary files /dev/null and b/war/src/main/webapp/images/16x16/find.png differ diff --git a/war/src/main/webapp/images/16x16/go_down.png b/war/src/main/webapp/images/16x16/go_down.png new file mode 100644 index 0000000000..54de8f7ec2 Binary files /dev/null and b/war/src/main/webapp/images/16x16/go_down.png differ diff --git a/war/src/main/webapp/images/16x16/go_up.png b/war/src/main/webapp/images/16x16/go_up.png new file mode 100644 index 0000000000..242309a631 Binary files /dev/null and b/war/src/main/webapp/images/16x16/go_up.png differ diff --git a/war/src/main/webapp/scripts/hudson-behavior.js b/war/src/main/webapp/scripts/hudson-behavior.js index 270ab19cbc..270195fd53 100644 --- a/war/src/main/webapp/scripts/hudson-behavior.js +++ b/war/src/main/webapp/scripts/hudson-behavior.js @@ -1583,11 +1583,12 @@ function fireBuildHistoryChanged() { function updateBuildHistory(ajaxUrl,nBuild) { if(isRunAsTest) return; var bh = $('buildHistory'); + var buildHistoryPage = $('buildHistoryPage'); bh.headers = ["n",nBuild]; function getDataTable(buildHistoryDiv) { - return $(buildHistoryDiv).getElementsBySelector('table.pane')[0]; + return $(buildHistoryDiv).getElementsBySelector('table.pane')[0]; } var leftRightPadding = 4; @@ -1884,7 +1885,6 @@ function updateBuildHistory(ajaxUrl,nBuild) { } } - var updateBuildsRefreshInterval = 5000; function updateBuilds() { if(isPageVisible()){ if (bh.headers == null) { @@ -1898,8 +1898,12 @@ function updateBuildHistory(ajaxUrl,nBuild) { var rows = dataTable.rows; //delete rows with transitive data - while (rows.length > 0 && Element.hasClassName(rows[0], "transitive")) { - Element.remove(rows[0]); + var firstBuildRow = 0; + if (Element.hasClassName(rows[firstBuildRow], "build-search-row")) { + firstBuildRow++; + } + while (rows.length > 0 && Element.hasClassName(rows[firstBuildRow], "transitive")) { + Element.remove(rows[firstBuildRow]); } // insert new rows @@ -1907,8 +1911,9 @@ function updateBuildHistory(ajaxUrl,nBuild) { div.innerHTML = rsp.responseText; Behaviour.applySubtree(div); - var pivot = rows[0]; - var newRows = getDataTable(div).rows; + var pivot = rows[firstBuildRow]; + var newDataTable = getDataTable(div); + var newRows = newDataTable.rows; while (newRows.length > 0) { if (pivot !== undefined) { // The data table has rows. Insert before a "pivot" row (first row). @@ -1917,26 +1922,201 @@ function updateBuildHistory(ajaxUrl,nBuild) { // The data table has no rows. In this case, we just add all new rows directly to the // table, one after the other i.e. we don't insert before a "pivot" row (first row). dataTable.appendChild(newRows[0]); - } + } + } + + if (Element.hasClassName(newDataTable, 'hasPageData')) { + buildHistoryPage.setAttribute('page-entry-newest', newDataTable.getAttribute('page-entry-newest')); } // next update bh.headers = ["n",rsp.getResponseHeader("n")]; - window.setTimeout(updateBuilds, updateBuildsRefreshInterval); - checkAllRowCellOverflows(); + createRefreshTimeout(); } - }); - } else { + }); + } else { // Reschedule again - window.setTimeout(updateBuilds, updateBuildsRefreshInterval); + createRefreshTimeout(); } } - window.setTimeout(updateBuilds, updateBuildsRefreshInterval); + + var updateBuildsRefreshInterval = 5000; + var buildRefreshTimeout; + function createRefreshTimeout() { + cancelRefreshTimeout(); + buildRefreshTimeout = window.setTimeout(updateBuilds, updateBuildsRefreshInterval); + } + function cancelRefreshTimeout() { + if (buildRefreshTimeout) { + window.clearTimeout(buildRefreshTimeout); + buildRefreshTimeout = undefined; + } + } + + createRefreshTimeout(); onBuildHistoryChange(function() { checkAllRowCellOverflows(); }); + + function setupHistoryNav() { + var sidePanel = $('side-panel'); + var buildHistoryPageNav = $('buildHistoryPageNav'); + + // Show/hide the nav as the mouse moves into the sidepanel and build history. + sidePanel.observe('mouseover', function() { + Element.addClassName($(buildHistoryPageNav), "mouseOverSidePanel"); + }); + sidePanel.observe('mouseout', function() { + Element.removeClassName($(buildHistoryPageNav), "mouseOverSidePanel"); + }); + bh.observe('mouseover', function() { + Element.addClassName($(buildHistoryPageNav), "mouseOverSidePanelBuildHistory"); + }); + bh.observe('mouseout', function() { + Element.removeClassName($(buildHistoryPageNav), "mouseOverSidePanelBuildHistory"); + }); + + var pageSearchInput = Element.getElementsBySelector(bh, '.build-search-row input')[0]; + var pageSearchClear = Element.getElementsBySelector(bh, '.build-search-row .clear')[0]; + var pageOne = Element.getElementsBySelector(buildHistoryPageNav, '.pageOne')[0]; + var pageUp = Element.getElementsBySelector(buildHistoryPageNav, '.pageUp')[0]; + var pageDown = Element.getElementsBySelector(buildHistoryPageNav, '.pageDown')[0]; + + function hasPageUp() { + return buildHistoryPage.getAttribute('page-has-up') === 'true'; + } + function hasPageDown() { + return buildHistoryPage.getAttribute('page-has-down') === 'true'; + } + function getNewestEntryId() { + return parseInt(buildHistoryPage.getAttribute('page-entry-newest')); + } + function getOldestEntryId() { + return parseInt(buildHistoryPage.getAttribute('page-entry-oldest')); + } + function updatePageParams(dataTable) { + buildHistoryPage.setAttribute('page-has-up', dataTable.getAttribute('page-has-up')); + buildHistoryPage.setAttribute('page-has-down', dataTable.getAttribute('page-has-down')); + buildHistoryPage.setAttribute('page-entry-newest', dataTable.getAttribute('page-entry-newest')); + buildHistoryPage.setAttribute('page-entry-oldest', dataTable.getAttribute('page-entry-oldest')); + } + function togglePageUpDown() { + Element.removeClassName($(buildHistoryPageNav), "hasUpPage"); + Element.removeClassName($(buildHistoryPageNav), "hasDownPage"); + if (hasPageUp()) { + Element.addClassName($(buildHistoryPageNav), "hasUpPage"); + } + if (hasPageDown()) { + Element.addClassName($(buildHistoryPageNav), "hasDownPage"); + } + } + function logPageParams() { + console.log('-----'); + console.log('Has up: ' + hasPageUp()); + console.log('Has down: ' + hasPageDown()); + console.log('Newest: ' + getNewestEntryId()); + console.log('Oldest: ' + getOldestEntryId()); + console.log('-----'); + } + + function loadPage(params, focusOnSearch) { + var searchString = pageSearchInput.value; + + if (searchString !== '') { + if (params === undefined) { + params = {}; + } + params.search = searchString; + } + + new Ajax.Request(ajaxUrl + toQueryString(params), { + onSuccess: function(rsp) { + var dataTable = getDataTable(bh); + var rows = dataTable.rows; + + // delete all rows + var searchRow; + if (Element.hasClassName(rows[0], "build-search-row")) { + searchRow = rows[0]; + } + while (rows.length > 0) { + Element.remove(rows[0]); + } + if (searchRow) { + dataTable.appendChild(searchRow); + } + + // insert new rows + var div = document.createElement('div'); + div.innerHTML = rsp.responseText; + Behaviour.applySubtree(div); + + var newDataTable = getDataTable(div); + var newRows = newDataTable.rows; + while (newRows.length > 0) { + dataTable.appendChild(newRows[0]); + } + + checkAllRowCellOverflows(); + updatePageParams(newDataTable); + togglePageUpDown(); + if (!hasPageUp()) { + createRefreshTimeout(); + } + + if (focusOnSearch) { + pageSearchInput.focus(); + } + //logPageParams(); + } + }); + } + + pageSearchInput.observe('keypress', function(e) { + var key = e.which || e.keyCode; + // On enter + if (key === 13) { + loadPage({}, true); + } + }); + pageSearchClear.observe('click', function() { + pageSearchInput.value = ''; + loadPage({}, true); + }); + pageOne.observe('click', function() { + loadPage(); + }); + pageUp.observe('click', function() { + loadPage({'newer-than': getNewestEntryId()}); + }); + pageDown.observe('click', function() { + cancelRefreshTimeout(); + loadPage({'older-than': getOldestEntryId()}); + }); + + togglePageUpDown(); + //logPageParams(); + } + setupHistoryNav(); +} + +function toQueryString(params) { + var query = ''; + if (params) { + for (var paramName in params) { + if (params.hasOwnProperty(paramName)) { + if (query === '') { + query = '?'; + } else { + query += '&'; + } + query += paramName + '=' + encodeURIComponent(params[paramName]); + } + } + } + return query; } function getElementOverflowParams(element) {