Please display information about pending runs in build.webkit.org/dashboard
authorap@apple.com <ap@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 29 Dec 2013 17:24:57 +0000 (17:24 +0000)
committerap@apple.com <ap@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 29 Dec 2013 17:24:57 +0000 (17:24 +0000)
https://bugs.webkit.org/show_bug.cgi?id=122180

Reviewed by Timothy Hatcher.

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/BuildbotBuilderQueueView.js:
(BuildbotBuilderQueueView.prototype.update.appendBuilderQueueStatus): Added a semicolon at the end of a line.

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/BuildbotQueueView.js:
(BuildbotQueueView.prototype._latestFinishedIteration): Factored out of _appendPendingRevisionCount.
(BuildbotQueueView.prototype._appendPendingRevisionCount): Install a popover tracker over the element.
(BuildbotQueueView.prototype.lineForCommit): Build an element for a particular commit ot be shown in popover.
(BuildbotQueueView.prototype.presentPopoverForElement): Build and show popover content when PopoverTracker
asks us to.

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Geometry.js: Added.
Taken from WebInspector with minimal changes:
- Changed root name from WebInspector to Dashboard.
- Removed some unused functionality.
- Added Rect.containsPoint.

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Popover.js: Added.
Popover has extensive changes compared to WebInspector version, only drawing code is the same:
- Fixed to work in scrollable pages - WebInspector version assumes that window
and document coordinates are the same, and also erroneously dismisses a scrollable
popover if scrolling cascades out of it after reaching a bound.
- Simplified API and implementation to Dashboard needs, it is no longer possible to
change content of an existing popover.
- Rewrote visibility tracking to be more complete, and not rely on external tracker
object so much.
- Added code to flash scroll bars when showing a scrollable popover.

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/PopoverTracker.js: Added.
Objects of this class show and hide popovers as appropriate for registered active elements.

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/StatusLineView.js:
(StatusLineView.prototype.get messageElement): Added an accessor, so that we could
install a popover on message element. It's the only visible element in pending commit
line, but the line has different bounds, so we can't install a popover on it (it
would be incorrectly positioned if we did).

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Trac.js:
(Trac.prototype._convertCommitInfoElementToObject):
- Some trac installations report author in a different element, updated to support that.
- Changed to parse title out of description, because trac titles are ugly. Also,
we get a nice HTML with links from the description.

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Utilities.js:
(Node.prototype.isAncestor): Copied from Web Inspector, only changing the form for
consistency with the rest of this file (add a property on prototype with assignment
instead of using Object.defineProperty).
(Node.prototype.isDescendant): Ditto.
(Node.prototype.isSelfOrAncestor): Ditto.
(Node.prototype.isSelfOrDescendant): Ditto.
(DOMTokenList.prototype.contains): Ditto.

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Styles/Popover.css: Added.
Like JS counterpart, mostly lifted from Web Inspector.

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Styles/QueueView.css:
Added style rules for pending commits popover.

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Styles/StatusLineView.css:
(.status-line.no-bubble .message): Changed to display:inline-block, so that it fits
to content, and we can show the popover in a correct place.

* BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/index.html: Added new files.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@161120 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/BuildbotBuilderQueueView.js
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/BuildbotQueueView.js
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Geometry.js [new file with mode: 0644]
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Popover.js [new file with mode: 0644]
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/PopoverTracker.js [new file with mode: 0644]
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/StatusLineView.js
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Trac.js
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Utilities.js
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Styles/Popover.css [new file with mode: 0644]
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Styles/QueueView.css
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Styles/StatusLineView.css
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/index.html
Tools/ChangeLog

index 1b0a4c9..e946646 100644 (file)
@@ -60,7 +60,7 @@ BuildbotBuilderQueueView.prototype = {
 
         function appendBuilderQueueStatus(queue)
         {
-            this._appendPendingRevisionCount(queue)
+            this._appendPendingRevisionCount(queue);
 
             var firstRecentFailedIteration = queue.firstRecentFailedIteration;
             if (firstRecentFailedIteration && firstRecentFailedIteration.loaded) {
index 8657cf2..a7dcce2 100644 (file)
@@ -60,42 +60,116 @@ BuildbotQueueView.prototype = {
     constructor: BuildbotQueueView,
     __proto__: QueueView.prototype,
 
-    _appendPendingRevisionCount: function(queue)
+    _latestFinishedIteration: function(queue)
     {
         for (var i = 0; i < queue.iterations.length; ++i) {
             var iteration = queue.iterations[i];
             if (!iteration.loaded || !iteration.finished)
                 continue;
+            return iteration;
+        }
+        return null;
+    },
+
+    _appendPendingRevisionCount: function(queue)
+    {
+        var latestFinishedIteration = this._latestFinishedIteration(queue);
+        if (!latestFinishedIteration)
+            return;
+
+        var latestRecordedOpenSourceRevisionNumber = webkitTrac.latestRecordedRevisionNumber;
+        if (!latestRecordedOpenSourceRevisionNumber)
+            return;
 
-            var latestRecordedOpenSourceRevisionNumber = webkitTrac.latestRecordedRevisionNumber;
-            if (!latestRecordedOpenSourceRevisionNumber)
+        var openSourceRevisionsBehind = latestRecordedOpenSourceRevisionNumber - latestFinishedIteration.openSourceRevision;
+        if (openSourceRevisionsBehind < 0)
+            openSourceRevisionsBehind = 0;
+
+        if (latestFinishedIteration.internalRevision) {
+            var latestRecordedInternalRevisionNumber = internalTrac.latestRecordedRevisionNumber;
+            if (!latestRecordedInternalRevisionNumber)
                 return;
 
-            var openSourceRevisionsBehind = latestRecordedOpenSourceRevisionNumber - iteration.openSourceRevision;
-            if (openSourceRevisionsBehind < 0)
-                openSourceRevisionsBehind = 0;
-
-            if (iteration.internalRevision) {
-                var latestRecordedInternalRevisionNumber = internalTrac.latestRecordedRevisionNumber;
-                if (!latestRecordedInternalRevisionNumber)
-                    return;
-
-                var internalRevisionsBehind = latestRecordedInternalRevisionNumber - iteration.internalRevision;
-                if (internalRevisionsBehind < 0)
-                    internalRevisionsBehind = 0;
-                if (openSourceRevisionsBehind || internalRevisionsBehind) {
-                    var message = openSourceRevisionsBehind + " \uff0b " + internalRevisionsBehind + " revisions behind";
-                    var status = new StatusLineView(message, StatusLineView.Status.NoBubble);
-                    this.element.appendChild(status.element);
-                }
-            } else if (openSourceRevisionsBehind) {
-                var message = openSourceRevisionsBehind + " " + (openSourceRevisionsBehind === 1 ? "revision behind" : "revisions behind");
+            var internalRevisionsBehind = latestRecordedInternalRevisionNumber - latestFinishedIteration.internalRevision;
+            if (internalRevisionsBehind < 0)
+                internalRevisionsBehind = 0;
+            if (openSourceRevisionsBehind || internalRevisionsBehind) {
+                var message = openSourceRevisionsBehind + " \uff0b " + internalRevisionsBehind + " revisions behind";
                 var status = new StatusLineView(message, StatusLineView.Status.NoBubble);
                 this.element.appendChild(status.element);
             }
+        } else if (openSourceRevisionsBehind) {
+            var message = openSourceRevisionsBehind + " " + (openSourceRevisionsBehind === 1 ? "revision behind" : "revisions behind");
+            var status = new StatusLineView(message, StatusLineView.Status.NoBubble);
+            this.element.appendChild(status.element);
+        }
 
-            return;
+        if (status)
+            new PopoverTracker(status.messageElement, this, queue);
+    },
+
+    presentPopoverForElement: function(element, popover, queue)
+    {
+        var latestFinishedIteration = this._latestFinishedIteration(queue);
+        if (!latestFinishedIteration)
+            return false;
+
+        function lineForCommit(trac, commit)
+        {
+            var result = document.createElement("div");
+            result.className = "pending-commit";
+
+            var linkElement = document.createElement("a");
+            linkElement.className = "revision";
+            linkElement.href = trac.revisionURL(commit.revisionNumber);
+            linkElement.target = "_blank";
+            linkElement.textContent = "r" + commit.revisionNumber;
+            result.appendChild(linkElement);
+
+            var authorElement = document.createElement("span");
+            authorElement.className = "author";
+            authorElement.textContent = commit.author;
+            result.appendChild(authorElement);
+
+            var titleElement = document.createElement("span");
+            titleElement.className = "title";
+            titleElement.innerHTML = commit.title.innerHTML;
+            result.appendChild(titleElement);
+
+            return result;
+        }
+
+        var content = document.createElement("div");
+        content.className = "pending-commits-popover";
+
+        for (var i = webkitTrac.recordedCommits.length - 1; i >= 0; --i) {
+            var commit = webkitTrac.recordedCommits[i];
+            if (commit.revisionNumber <= latestFinishedIteration.openSourceRevision)
+                break;
+
+            content.appendChild(lineForCommit(webkitTrac, commit));
         }
+
+        if (latestFinishedIteration.internalRevision && internalTrac.latestRecordedRevisionNumber) {
+            if (latestFinishedIteration.internalRevision < internalTrac.latestRecordedRevisionNumber && content.hasChildNodes()) {
+                var divider = document.createElement("div");
+                divider.className = "divider";
+                content.appendChild(divider);
+            }
+
+            for (var i = internalTrac.recordedCommits.length - 1; i >= 0; --i) {
+                var commit = internalTrac.recordedCommits[i];
+                if (commit.revisionNumber <= latestFinishedIteration.internalRevision)
+                    break;
+
+                content.appendChild(lineForCommit(internalTrac, commit));
+            }
+        }
+
+        var rect = Dashboard.Rect.rectFromClientRect(element.getBoundingClientRect());
+        popover.present(rect, content, [Dashboard.RectEdge.MIN_Y, Dashboard.RectEdge.MAX_Y, Dashboard.RectEdge.MAX_X, Dashboard.RectEdge.MIN_X]);
+
+        return true;
     },
 
     revisionLinksForIteration: function(iteration)
diff --git a/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Geometry.js b/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Geometry.js
new file mode 100644 (file)
index 0000000..ca4ba39
--- /dev/null
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2013 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+Dashboard.Point = function(x, y)
+{
+    this.x = x || 0;
+    this.y = y || 0;
+};
+
+Dashboard.Point.prototype = {
+    constructor: Dashboard.Point,
+
+    toString : function()
+    {
+        return "Dashboard.Point[" + this.x + "," + this.y + "]";
+    },
+
+    copy: function()
+    {
+        return new Dashboard.Point(this.x, this.y);
+    },
+
+    equals: function(anotherPoint)
+    {
+        return (this.x === anotherPoint.x && this.y === anotherPoint.y);
+    }
+};
+
+Dashboard.Size = function(width, height)
+{
+    this.width = width || 0;
+    this.height = height || 0;
+};
+
+Dashboard.Size.prototype = {
+    constructor: Dashboard.Size,
+
+    toString: function()
+    {
+        return "Dashboard.Size[" + this.width + "," + this.height + "]";
+    },
+
+    copy: function()
+    {
+        return new Dashboard.Size(this.width, this.height);
+    },
+
+    equals: function(anotherSize)
+    {
+        return (this.width === anotherSize.width && this.height === anotherSize.height);
+    }
+};
+
+Dashboard.Size.ZERO_SIZE = new Dashboard.Size(0, 0);
+
+
+Dashboard.Rect = function(x, y, width, height)
+{
+    this.origin = new Dashboard.Point(x || 0, y || 0);
+    this.size = new Dashboard.Size(width || 0, height || 0);
+};
+
+Dashboard.Rect.rectFromClientRect = function(clientRect)
+{
+    return new Dashboard.Rect(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
+};
+
+Dashboard.Rect.prototype = {
+    constructor: Dashboard.Rect,
+
+    toString: function()
+    {
+        return "Dashboard.Rect[" + [this.origin.x, this.origin.y, this.size.width, this.size.height].join(", ") + "]";
+    },
+
+    copy: function()
+    {
+        return new Dashboard.Rect(this.origin.x, this.origin.y, this.size.width, this.size.height);
+    },
+
+    equals: function(anotherRect)
+    {
+        return (this.origin.equals(anotherRect.origin) && this.size.equals(anotherRect.size));
+    },
+
+    inset: function(insets)
+    {
+        return new Dashboard.Rect(
+            this.origin.x + insets.left,
+            this.origin.y + insets.top,
+            this.size.width - insets.left - insets.right,
+            this.size.height - insets.top - insets.bottom
+        );
+    },
+
+    pad: function(padding)
+    {
+        return new Dashboard.Rect(
+            this.origin.x - padding,
+            this.origin.y - padding,
+            this.size.width + padding * 2,
+            this.size.height + padding * 2
+        );
+    },
+
+    minX: function()
+    {
+        return this.origin.x;
+    },
+
+    minY: function()
+    {
+        return this.origin.y;
+    },
+
+    midX: function()
+    {
+        return this.origin.x + (this.size.width / 2);
+    },
+
+    midY: function()
+    {
+        return this.origin.y + (this.size.height / 2);
+    },
+
+    maxX: function()
+    {
+        return this.origin.x + this.size.width;
+    },
+
+    maxY: function()
+    {
+        return this.origin.y + this.size.height;
+    },
+
+    intersectionWithRect: function(rect)
+    {
+        var x1 = Math.max(this.minX(), rect.minX());
+        var x2 = Math.min(this.maxX(), rect.maxX());
+        if (x1 > x2)
+            return Dashboard.Rect.ZERO_RECT;
+        var intersection = new Dashboard.Rect;
+        intersection.origin.x = x1;
+        intersection.size.width = x2 - x1;
+        var y1 = Math.max(this.minY(), rect.minY());
+        var y2 = Math.min(this.maxY(), rect.maxY());
+        if (y1 > y2)
+            return Dashboard.Rect.ZERO_RECT;
+        intersection.origin.y = y1;
+        intersection.size.height = y2 - y1;
+        return intersection;
+    },
+
+    containsPoint: function(point)
+    {
+        return point.x >= this.minX() && point.x <= this.maxX()
+            && point.y >= this.minY() && point.y <= this.maxY();
+    }
+};
+
+Dashboard.Rect.ZERO_RECT = new Dashboard.Rect(0, 0, 0, 0);
+
+
+Dashboard.EdgeInsets = function(top, right, bottom, left)
+{
+    console.assert(arguments.length === 1 || arguments.length === 4);
+
+    if (arguments.length === 1) {
+        this.top = top;
+        this.right = top;
+        this.bottom = top;
+        this.left = top;
+    } else if (arguments.length === 4) {
+        this.top = top;
+        this.right = right;
+        this.bottom = bottom;
+        this.left = left;
+    }
+};
+
+Dashboard.EdgeInsets.prototype = {
+    constructor: Dashboard.EdgeInsets,
+
+    equals: function(anotherInset)
+    {
+        return (this.top === anotherInset.top && this.right === anotherInset.right &&
+                this.bottom === anotherInset.bottom && this.left === anotherInset.left);
+    },
+
+    copy: function()
+    {
+        return new Dashboard.EdgeInsets(this.top, this.right, this.bottom, this.left);
+    }
+};
+
+Dashboard.RectEdge = {
+    MIN_X : 0,
+    MIN_Y : 1,
+    MAX_X : 2,
+    MAX_Y : 3
+};
diff --git a/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Popover.js b/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Popover.js
new file mode 100644 (file)
index 0000000..76f5c51
--- /dev/null
@@ -0,0 +1,506 @@
+/*
+ * Copyright (C) 2013 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+Dashboard.Popover = function(delegate)
+{
+    BaseObject.call(this);
+
+    this.delegate = delegate;
+    this._edge = null;
+    this._frame = new Dashboard.Rect;
+    this._content = null;
+    this._targetFrame = new Dashboard.Rect;
+    this._preferredEdges = null;
+
+    this._element = document.createElement("div");
+    this._element.className = Dashboard.Popover.StyleClassName;
+    this._canvasId = "popover-" + (Dashboard.Popover.canvasId++);
+    this._element.style.backgroundImage = "-webkit-canvas(" + this._canvasId + ")";
+    this._element.addEventListener("transitionend", this, true);
+    
+    this._container = this._element.appendChild(document.createElement("div"));
+    this._container.className = "container";
+};
+
+Dashboard.Popover.StyleClassName = "popover";
+Dashboard.Popover.VisibleClassName = "visible";
+Dashboard.Popover.StepInClassName = "step-in";
+Dashboard.Popover.FadeOutClassName = "fade-out";
+Dashboard.Popover.PreventDocumentScrollingClassName = "popover-prevent-document-scrolling";
+Dashboard.Popover.canvasId = 0;
+Dashboard.Popover.CornerRadius = 5;
+Dashboard.Popover.MinWidth = 40;
+Dashboard.Popover.MinHeight = 40;
+Dashboard.Popover.ShadowPadding = 5;
+Dashboard.Popover.ContentPadding = 5;
+Dashboard.Popover.AnchorSize = new Dashboard.Size(22, 11);
+Dashboard.Popover.ShadowEdgeInsets = new Dashboard.EdgeInsets(Dashboard.Popover.ShadowPadding);
+
+BaseObject.addConstructorFunctions(Dashboard.Popover);
+
+Dashboard.Popover.prototype = {
+    constructor: Dashboard.Popover,
+    __proto__: BaseObject.prototype,
+
+    // Public
+
+    get element()
+    {
+        return this._element;
+    },
+
+    get frame()
+    {
+        return this._frame;
+    },
+
+    set frame(frame)
+    {
+        this._element.style.left = window.scrollX + frame.origin.x + "px";
+        this._element.style.top = window.scrollY + frame.origin.y + "px";
+        this._element.style.width = frame.size.width + "px";
+        this._element.style.height = frame.size.height + "px";
+        this._element.style.backgroundSize = frame.size.width + "px " + frame.size.height + "px";
+        this._frame = frame;
+    },
+
+    get visible()
+    {
+        return this._element.parentNode === document.body
+            && !this._element.classList.contains(Dashboard.Popover.StepInClassName);
+    },
+
+    /**
+     * @param {Dashboard.Rect} targetFrame
+     * @param {Element} content
+     * @param {Dashboard.RectEdge}[] preferredEdges
+     */
+    present: function(targetFrame, content, preferredEdges)
+    {
+        console.assert(!this._content)
+
+        this._targetFrame = targetFrame;
+        this._content = content;
+        this._preferredEdges = preferredEdges;
+
+        window.addEventListener("mousedown", this, true);
+        window.addEventListener("scroll", this, true);
+
+        this._update();
+
+        this._element.classList.add(Dashboard.Popover.StepInClassName);
+
+        // Scrolling inside a popover should not cascade to document when reaching a bound, because that would make it disappear unexpectedly.
+        if (this._container.offsetHeight < this._container.scrollHeight) {
+            this._element.addEventListener("mouseenter", this, true);
+            this._element.addEventListener("mouseleave", this, true);
+        }
+    },
+
+    makeVisibleImmediately: function()
+    {
+        console.assert(this._content);
+
+        this._finalizePresentation();
+    },
+    
+    dismiss: function()
+    {
+        if (this._element.parentNode !== document.body)
+            return;
+
+        this._element.classList.add(Dashboard.Popover.FadeOutClassName);
+    },
+
+    dismissImmediately: function()
+    {
+        if (this._element.parentNode !== document.body)
+            return;
+
+        this._finalizeDismissal();
+    },
+
+    handleEvent: function(event)
+    {
+        switch (event.type) {
+        case "mousedown":
+        case "scroll":
+            if (!this._element.contains(event.target))
+                this.dismissImmediately();
+            break;
+        case "transitionend":
+            if (this._element.classList.contains(Dashboard.Popover.StepInClassName))
+                this._finalizePresentation();
+            else if (this._element.classList.contains(Dashboard.Popover.FadeOutClassName))
+                this._finalizeDismissal();
+            break;
+        case "mouseenter":
+            document.body.classList.add(Dashboard.Popover.PreventDocumentScrollingClassName);
+            break;
+        case "mouseleave":
+            if (!this._element.isSelfOrAncestor(event.toElement))
+                document.body.classList.remove(Dashboard.Popover.PreventDocumentScrollingClassName);
+            break;
+        }
+    },
+
+    // Private
+
+    _update: function()
+    {
+        var targetFrame = this._targetFrame;
+        var preferredEdges = this._preferredEdges;
+
+        // Ensure our element is on display so that its metrics can be resolved
+        // or interrupt any pending transition to remove it from display.
+        if (this._element.parentNode !== document.body)
+            document.body.appendChild(this._element);
+        else
+            this._element.classList.remove(Dashboard.Popover.FadeOutClassName);
+
+        // Reset CSS properties on element so that the element may be sized to fit its content.
+        this._element.style.removeProperty("left");
+        this._element.style.removeProperty("top");
+        this._element.style.removeProperty("width");
+        this._element.style.removeProperty("height");
+        if (this._edge !== null)
+            this._element.classList.remove(this._cssClassNameForEdge());
+
+        // Add the content in place of the wrapper to get the raw metrics.
+        this._element.replaceChild(this._content, this._container);
+
+        // Get the ideal size for the popover to fit its content.
+        var popoverBounds = this._element.getBoundingClientRect();
+        this._preferredSize = new Dashboard.Size(Math.ceil(popoverBounds.width), Math.ceil(popoverBounds.height));
+
+        // The frame of the window with a little inset to make sure we have room for shadows.
+        var containerFrame = new Dashboard.Rect(0, 0, window.innerWidth, window.innerHeight);
+        containerFrame = containerFrame.inset(Dashboard.Popover.ShadowEdgeInsets);
+
+        // Work out the metrics for all edges.
+        var metrics = new Array(preferredEdges.length);
+        for (var edgeName in Dashboard.RectEdge) {
+            var edge = Dashboard.RectEdge[edgeName];
+            var item = {
+                edge: edge,
+                metrics: this._bestMetricsForEdge(this._preferredSize, targetFrame, containerFrame, edge)
+            };
+            var preferredIndex = preferredEdges.indexOf(edge);
+            if (preferredIndex !== -1)
+                metrics[preferredIndex] = item;
+            else
+                metrics.push(item);
+        }
+
+        function area(size)
+        {
+            return size.width * size.height;
+        }
+
+        // Find if any of those fit better than the frame for the preferred edge.
+        var bestEdge = metrics[0].edge;
+        var bestMetrics = metrics[0].metrics;
+        for (var i = 1; i < metrics.length; i++) {
+            var itemMetrics = metrics[i].metrics;
+            if (area(itemMetrics.contentSize) > area(bestMetrics.contentSize)) {
+                bestEdge = metrics[i].edge;
+                bestMetrics = itemMetrics;
+            }
+        }
+
+        var anchorPoint;
+        var bestFrame = bestMetrics.frame;
+
+        this.frame = bestFrame;
+        this._edge = bestEdge;
+
+        if (this.frame === Dashboard.Rect.ZERO_RECT) {
+            // The target for the popover is offscreen.
+            this.dismiss();
+        } else {
+            switch (bestEdge) {
+            case Dashboard.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right.
+                anchorPoint = new Dashboard.Point(bestFrame.size.width - Dashboard.Popover.ShadowPadding, targetFrame.midY() - bestFrame.minY());
+                break;
+            case Dashboard.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left.
+                anchorPoint = new Dashboard.Point(Dashboard.Popover.ShadowPadding, targetFrame.midY() - bestFrame.minY());
+                break;
+            case Dashboard.RectEdge.MIN_Y: // Displayed above the target, arrow points down.
+                anchorPoint = new Dashboard.Point(targetFrame.midX() - bestFrame.minX(), bestFrame.size.height - Dashboard.Popover.ShadowPadding);
+                break;
+            case Dashboard.RectEdge.MAX_Y: // Displayed below the target, arrow points up.
+                anchorPoint = new Dashboard.Point(targetFrame.midX() - bestFrame.minX(), Dashboard.Popover.ShadowPadding);
+                break;
+            }
+
+            this._element.classList.add(this._cssClassNameForEdge());
+
+            this._drawBackground(bestEdge, anchorPoint);
+
+            // Make sure content is centered in case either of the dimension is smaller than the minimal bounds.
+            if (this._preferredSize.width < Dashboard.Popover.MinWidth || this._preferredSize.height < Dashboard.Popover.MinHeight)
+                this._container.classList.add("center");
+            else
+                this._container.classList.remove("center");
+        }
+
+        // Wrap the content in the container so that it's located correctly.
+        this._container.textContent = "";
+        this._element.replaceChild(this._container, this._content);
+        this._container.appendChild(this._content);
+    },
+
+    _cssClassNameForEdge: function()
+    {
+        switch (this._edge) {
+        case Dashboard.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right.
+            return "arrow-right";
+        case Dashboard.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left.
+            return "arrow-left";
+        case Dashboard.RectEdge.MIN_Y: // Displayed above the target, arrow points down.
+            return "arrow-down";
+        case Dashboard.RectEdge.MAX_Y: // Displayed below the target, arrow points up.
+            return "arrow-up";
+        }
+        console.error("Unknown edge.");
+        return "arrow-up";
+    },
+
+    _drawBackground: function(edge, anchorPoint)
+    {
+        var scaleFactor = window.devicePixelRatio;
+
+        var width = this._frame.size.width;
+        var height = this._frame.size.height;
+        var scaledWidth = width * scaleFactor;
+        var scaledHeight = height * scaleFactor;
+
+        // Create a scratch canvas so we can draw the popover that will later be drawn into
+        // the final context with a shadow.
+        var scratchCanvas = document.createElement("canvas");
+        scratchCanvas.width = scaledWidth;
+        scratchCanvas.height = scaledHeight;
+
+        var ctx = scratchCanvas.getContext("2d");
+        ctx.scale(scaleFactor, scaleFactor);
+
+        // Bounds of the path don't take into account the arrow, but really only the tight bounding box
+        // of the content contained within the frame.
+        var bounds;
+        var arrowHeight = Dashboard.Popover.AnchorSize.height;
+        switch (edge) {
+        case Dashboard.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right.
+            bounds = new Dashboard.Rect(0, 0, width - arrowHeight, height);
+            break;
+        case Dashboard.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left.
+            bounds = new Dashboard.Rect(arrowHeight, 0, width - arrowHeight, height);
+            break;
+        case Dashboard.RectEdge.MIN_Y: // Displayed above the target, arrow points down.
+            bounds = new Dashboard.Rect(0, 0, width, height - arrowHeight);
+            break;
+        case Dashboard.RectEdge.MAX_Y: // Displayed below the target, arrow points up.
+            bounds = new Dashboard.Rect(0, arrowHeight, width, height - arrowHeight);
+            break;
+        }
+
+        bounds = bounds.inset(Dashboard.Popover.ShadowEdgeInsets);
+
+        // Clip the frame.
+        ctx.fillStyle = "black";
+        this._drawFrame(ctx, bounds, edge, anchorPoint);
+        ctx.clip();
+
+        // Gradient fill, top-to-bottom.
+        var fillGradient = ctx.createLinearGradient(0, 0, 0, height);
+        fillGradient.addColorStop(0, "rgba(255, 255, 255, 0.95)");
+        fillGradient.addColorStop(1, "rgba(235, 235, 235, 0.95)");
+        ctx.fillStyle = fillGradient;
+        ctx.fillRect(0, 0, width, height);
+
+        // Stroke.
+        ctx.strokeStyle = "rgba(0, 0, 0, 0.25)";
+        ctx.lineWidth = 2;
+        this._drawFrame(ctx, bounds, edge, anchorPoint);
+        ctx.stroke();
+
+        // Draw the popover into the final context with a drop shadow.
+        var finalContext = document.getCSSCanvasContext("2d", this._canvasId, scaledWidth, scaledHeight);
+
+        finalContext.clearRect(0, 0, scaledWidth, scaledHeight);
+
+        finalContext.shadowOffsetX = 1;
+        finalContext.shadowOffsetY = 1;
+        finalContext.shadowBlur = 5;
+        finalContext.shadowColor = "rgba(0, 0, 0, 0.5)";
+
+        finalContext.drawImage(scratchCanvas, 0, 0, scaledWidth, scaledHeight);
+    },
+    
+    _bestMetricsForEdge: function(preferredSize, targetFrame, containerFrame, edge)
+    {
+        var x, y;
+        var width = preferredSize.width + (Dashboard.Popover.ShadowPadding * 2) + (Dashboard.Popover.ContentPadding * 2);
+        var height = preferredSize.height + (Dashboard.Popover.ShadowPadding * 2) + (Dashboard.Popover.ContentPadding * 2);
+        var arrowLength = Dashboard.Popover.AnchorSize.height;
+
+        switch (edge) {
+        case Dashboard.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right.
+            width += arrowLength;
+            x = targetFrame.origin.x - width + Dashboard.Popover.ShadowPadding;
+            y = targetFrame.origin.y - (height - targetFrame.size.height) / 2;
+            break;
+        case Dashboard.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left.
+            width += arrowLength;
+            x = targetFrame.origin.x + targetFrame.size.width - Dashboard.Popover.ShadowPadding;
+            y = targetFrame.origin.y - (height - targetFrame.size.height) / 2;
+            break;
+        case Dashboard.RectEdge.MIN_Y: // Displayed above the target, arrow points down.
+            height += arrowLength;
+            x = targetFrame.origin.x - (width - targetFrame.size.width) / 2;
+            y = targetFrame.origin.y - height + Dashboard.Popover.ShadowPadding;
+            break;
+        case Dashboard.RectEdge.MAX_Y: // Displayed below the target, arrow points up.
+            height += arrowLength;
+            x = targetFrame.origin.x - (width - targetFrame.size.width) / 2;
+            y = targetFrame.origin.y + targetFrame.size.height - Dashboard.Popover.ShadowPadding;
+            break;
+        }
+
+        if (edge === Dashboard.RectEdge.MIN_X || edge === Dashboard.RectEdge.MAX_X) {
+            if (y < containerFrame.minY())
+                y = containerFrame.minY();
+            if (y + height > containerFrame.maxY())
+                y = containerFrame.maxY() - height;
+        } else {
+            if (x < containerFrame.minX())
+                x = containerFrame.minX(); 
+            if (x + width > containerFrame.maxX())
+                x = containerFrame.maxX() - width;
+        }
+
+        var preferredFrame = new Dashboard.Rect(x, y, width, height);
+        var bestFrame = preferredFrame.intersectionWithRect(containerFrame);
+
+        width = bestFrame.size.width - (Dashboard.Popover.ShadowPadding * 2);
+        height = bestFrame.size.height - (Dashboard.Popover.ShadowPadding * 2);
+
+        switch (edge) {
+        case Dashboard.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right.
+        case Dashboard.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left.
+            width -= arrowLength;
+            break;
+        case Dashboard.RectEdge.MIN_Y: // Displayed above the target, arrow points down.
+        case Dashboard.RectEdge.MAX_Y: // Displayed below the target, arrow points up.
+            height -= arrowLength;
+            break;
+        }
+
+        return {
+            frame: bestFrame,
+            contentSize: new Dashboard.Size(width, height)
+        };
+    },
+
+    _drawFrame: function(ctx, bounds, anchorEdge, anchorPoint)
+    {
+        var r = Dashboard.Popover.CornerRadius;
+        var arrowHalfLength = Dashboard.Popover.AnchorSize.width / 2;
+
+        ctx.beginPath();
+        switch (anchorEdge) {
+        case Dashboard.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right.
+            ctx.moveTo(bounds.maxX(), bounds.minY() + r);
+            ctx.lineTo(bounds.maxX(), anchorPoint.y - arrowHalfLength);
+            ctx.lineTo(anchorPoint.x, anchorPoint.y);
+            ctx.lineTo(bounds.maxX(), anchorPoint.y + arrowHalfLength);
+            ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r);
+            ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r);
+            ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r);
+            ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r);
+            break;
+        case Dashboard.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left.
+            ctx.moveTo(bounds.minX(), bounds.maxY() - r);
+            ctx.lineTo(bounds.minX(), anchorPoint.y + arrowHalfLength);
+            ctx.lineTo(anchorPoint.x, anchorPoint.y);
+            ctx.lineTo(bounds.minX(), anchorPoint.y - arrowHalfLength);
+            ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r);
+            ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r);
+            ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r);
+            ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r);
+            break;
+        case Dashboard.RectEdge.MIN_Y: // Displayed above the target, arrow points down.
+            ctx.moveTo(bounds.maxX() - r, bounds.maxY());
+            ctx.lineTo(anchorPoint.x + arrowHalfLength, bounds.maxY());
+            ctx.lineTo(anchorPoint.x, anchorPoint.y);
+            ctx.lineTo(anchorPoint.x - arrowHalfLength, bounds.maxY());
+            ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r);
+            ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r);
+            ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r);
+            ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r);
+            break;
+        case Dashboard.RectEdge.MAX_Y: // Displayed below the target, arrow points up.
+            ctx.moveTo(bounds.minX() + r, bounds.minY());
+            ctx.lineTo(anchorPoint.x - arrowHalfLength, bounds.minY());
+            ctx.lineTo(anchorPoint.x, anchorPoint.y);
+            ctx.lineTo(anchorPoint.x + arrowHalfLength, bounds.minY());
+            ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r);
+            ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r);
+            ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r);
+            ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r);
+            break;
+        }
+        ctx.closePath();
+    },
+
+    _finalizePresentation: function()
+    {
+        var wasVisible = this.visible;
+        this._element.classList.remove(Dashboard.Popover.StepInClassName);
+        this._element.classList.remove(Dashboard.Popover.FadeOutClassName);
+        this._element.classList.add(Dashboard.Popover.VisibleClassName);
+
+        // Make scroll bar flash if present, so that the user sees that scrolling is possible.
+        // FIXME: Is there a better way?
+        if (!wasVisible) {
+            this._container.scrollTop = 1;
+            this._container.scrollTop = 0;
+        }
+    },
+
+    _finalizeDismissal: function()
+    {
+        window.removeEventListener("mousedown", this, true);
+        window.removeEventListener("scroll", this, true);
+
+        document.body.removeChild(this._element);
+        document.body.classList.remove(Dashboard.Popover.PreventDocumentScrollingClassName);
+        this._element.classList.remove(Dashboard.Popover.VisibleClassName);
+        this._element.classList.remove(Dashboard.Popover.StepInClassName);
+        this._element.classList.remove(Dashboard.Popover.FadeOutClassName);
+        this._container.textContent = "";
+        if (this.delegate && typeof this.delegate.didDismissPopover === "function")
+            this.delegate.didDismissPopover(this);
+    }
+};
diff --git a/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/PopoverTracker.js b/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/PopoverTracker.js
new file mode 100644 (file)
index 0000000..eae6196
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2013 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+PopoverTracker = function(element, delegate, context)
+{
+    BaseObject.call(this);
+
+    this._element = element;
+    this._delegate = delegate;
+    this._context = context;
+    this._active = false;
+
+    element.addEventListener("mouseenter", this._mouseEnteredPopoverOrElement.bind(this), true);
+    element.addEventListener("mouseleave", this._mouseExitedPopoverOrElement.bind(this), true);
+};
+
+PopoverTracker._popover = null; // Only one popover may be active at any time.
+
+BaseObject.addConstructorFunctions(PopoverTracker);
+
+PopoverTracker.prototype = {
+    constructor: PopoverTracker,
+    __proto__: BaseObject.prototype,
+
+    _mouseEnteredPopoverOrElement: function(event)
+    {
+        var popover = PopoverTracker._popover;
+        var popoverWasVisible = popover && popover.visible;
+        if (popover) {
+            // Abort fade-out when re-entering the same element or an existing popover.
+            if ((this._active && this._element.isSelfOrAncestor(event.toElement))
+                || popover.element.isSelfOrAncestor(event.toElement)) {
+                popover.makeVisibleImmediately();
+                return;
+            }
+
+            // We entered a different element, dismiss the old popover.
+            popover.dismissImmediately();
+        }
+        console.assert(!PopoverTracker._popover);
+
+        if (!this._delegate || !this._delegate.presentPopoverForElement || typeof this._delegate.presentPopoverForElement != "function")
+            return;
+
+        var popover = new Dashboard.Popover(this);
+        if (!this._delegate.presentPopoverForElement(event.target, popover, this._context))
+            return;
+
+        if (popoverWasVisible)
+            popover.makeVisibleImmediately();
+
+        this._active = true;
+        PopoverTracker._popover = popover;
+        popover.element.addEventListener("mouseenter", this._mouseEnteredPopoverOrElement.bind(this), true);
+        popover.element.addEventListener("mouseleave", this._mouseExitedPopoverOrElement.bind(this), true);
+    },
+
+    _mouseExitedPopoverOrElement: function(event)
+    {
+        var popover = PopoverTracker._popover;
+
+        if (!popover)
+            return;
+
+        if (this._element.isSelfOrAncestor(event.toElement) || popover.element.isSelfOrAncestor(event.toElement))
+            return;
+
+        if (popover.visible)
+            popover.dismiss();
+        else
+            popover.dismissImmediately();
+    },
+
+    didDismissPopover: function(popover)
+    {
+        console.assert(popover === PopoverTracker._popover);
+        this._active = false;
+        PopoverTracker._popover = null;
+    }
+};
index d6e0adc..5b47487 100644 (file)
@@ -160,5 +160,10 @@ StatusLineView.prototype = {
         } else {
             this._messageElement.textContent = x;
         }
+    },
+
+    get messageElement()
+    {
+        return this._messageElement;
     }
 };
index 7fbbec0..ba73443 100644 (file)
@@ -70,13 +70,32 @@ Trac.prototype = {
         var link = doc.evaluate("./link", commitElement, null, XPathResult.STRING_TYPE).stringValue;
         var revisionNumber = parseInt(/\d+$/.exec(link))
 
-        var title = doc.evaluate("./title", commitElement, null, XPathResult.STRING_TYPE).stringValue;
-        title = title.replace(/^Changeset \[\d+\]: /, "");
-        var author = doc.evaluate("./author", commitElement, null, XPathResult.STRING_TYPE).stringValue;
+        function tracNSResolver(prefix)
+        {
+            if (prefix == "dc")
+                return "http://purl.org/dc/elements/1.1/";
+            return null;
+        }
+
+        var author = doc.evaluate("./author|dc:creator", commitElement, tracNSResolver, XPathResult.STRING_TYPE).stringValue;
         var date = doc.evaluate("./pubDate", commitElement, null, XPathResult.STRING_TYPE).stringValue;
         date = new Date(Date.parse(date));
         var description = doc.evaluate("./description", commitElement, null, XPathResult.STRING_TYPE).stringValue;
 
+        // The feed contains a <title>, but it's not parsed as well as what we are getting from description.
+        var parsedDescription = document.createElement("div");
+        parsedDescription.innerHTML = description;
+        var title = document.createElement("div");
+        var node = parsedDescription.firstChild.firstChild;
+        while (node && node.tagName != "BR") {
+            title.appendChild(node.cloneNode(true));
+            node = node.nextSibling;
+        }
+
+        // For some reason, trac titles start with a newline. Delete it.
+        if (title.firstChild && title.firstChild.nodeType == Node.TEXT_NODE && title.firstChild.textContent.length > 0 && title.firstChild.textContent[0] == "\n")
+            title.firstChild.textContent = title.firstChild.textContent.substring(1);
+
         return {
             revisionNumber: revisionNumber,
             link: link,
index 05495cb..c66a913 100644 (file)
@@ -70,6 +70,36 @@ function loadXML(url, callback) {
     request.send();
 };
 
+Node.prototype.isAncestor = function(node)
+{
+    if (!node)
+        return false;
+
+    var currentNode = node.parentNode;
+    while (currentNode) {
+        if (this === currentNode)
+            return true;
+        currentNode = currentNode.parentNode;
+    }
+
+    return false;
+}
+
+Node.prototype.isDescendant = function(descendant)
+{
+    return !!descendant && descendant.isAncestor(this);
+}
+
+Node.prototype.isSelfOrAncestor = function(node)
+{
+    return !!node && (node === this || this.isAncestor(node));
+}
+
+Node.prototype.isSelfOrDescendant = function(node)
+{
+    return !!node && (node === this || this.isDescendant(node));
+}
+
 Element.prototype.removeChildren = function()
 {
     // This has been tested to be the fastest removal method.
@@ -77,6 +107,15 @@ Element.prototype.removeChildren = function()
         this.textContent = "";
 };
 
+DOMTokenList.prototype.contains = function(string)
+{
+    for (var i = 0, end = this.length; i < end; ++i) {
+        if (this.item(i) === string)
+            return true;
+    }
+    return false;
+}
+
 Array.prototype.contains = function(value)
 {
     return this.indexOf(value) >= 0;
diff --git a/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Styles/Popover.css b/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Styles/Popover.css
new file mode 100644 (file)
index 0000000..fa61a8c
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.popover {
+    position: absolute;
+    min-width: 20px;
+    min-height: 20px;
+    box-sizing: border-box;
+    pointer-events: none;
+    z-index: 1000;
+    opacity: 0;
+}
+
+.popover.arrow-up {
+    padding: 16px 5px 5px 5px;
+}
+
+.popover.arrow-right {
+    padding: 5px 16px 5px 5px;
+}
+
+.popover.arrow-down {
+    padding: 5px 5px 16px 5px;
+}
+
+.popover.arrow-left {
+    padding: 5px 5px 5px 16px;
+}
+
+.popover.visible {
+    opacity: 1;
+}
+
+.popover.step-in {
+    transition: opacity 0ms;
+    transition-delay: 1s;
+    opacity: 1;
+}
+
+.popover.fade-out {
+    transition: opacity 350ms;
+    transition-delay: 1s;
+    opacity: 0;
+}
+
+.popover > .container {
+    position: absolute;
+    left: 5px;
+    top: 5px;
+    right: 5px;
+    bottom: 5px;
+
+    padding: 5px;
+
+    overflow-y: auto;
+    overflow-x: hidden;
+
+    pointer-events: auto;
+}
+
+.popover > .container.center {
+    display: -webkit-flex;
+    -webkit-justify-content: center;
+    -webkit-align-items: center;
+}
+
+.popover.arrow-up > .container {
+    top: 16px;
+}
+
+.popover.arrow-right > .container {
+    right: 16px;
+}
+
+.popover.arrow-down > .container {
+    bottom: 16px;
+}
+
+.popover.arrow-left > .container {
+    left: 16px;
+}
+
+body.popover-prevent-document-scrolling {
+    overflow: hidden;
+}
index fb136e4..0492993 100644 (file)
 .queue-view .queueLabel:not(:first-child) {
     margin-top: 10px;
 }
+
+.pending-commits-popover > .pending-commit {
+    font-family: "HelveticaNeue-Light", "Helvetica Neue", sans-serif;
+    color: rgb(145, 135, 95);
+    font-size: 12px;
+    text-align: left;
+    padding: 1px 6px 1px 6px;
+    user-select: auto;
+}
+
+.pending-commits-popover > .pending-commit > .author {
+    padding-left: 5px;
+    padding-right: 5px;
+}
+
+.pending-commits-popover > .pending-commit > .title {
+    color: black;
+}
+
+.pending-commits-popover > .divider {
+    height: 7px;
+}
index a9709a1..9ba06b9 100644 (file)
     line-height: 13px;
 }
 
+.status-line.no-bubble .message {
+    display: inline-block; /* Fit block to content, so that popovers are positioned correctly. */
+}
+
 .status-line.neutral .label,
 .status-line.neutral .message,
 .status-line.no-bubble .label,
index db555d4..8594595 100644 (file)
@@ -28,6 +28,7 @@ THE POSSIBILITY OF SUCH DAMAGE.
     <title>WebKit Bot Watcher's Dashboard</title>
 
     <link rel="stylesheet" href="Styles/Main.css"></link>
+    <link rel="stylesheet" href="Styles/Popover.css"></link>
     <link rel="stylesheet" href="Styles/QueueView.css"></link>
     <link rel="stylesheet" href="Styles/StatusLineView.css"></link>
 
@@ -41,6 +42,9 @@ THE POSSIBILITY OF SUCH DAMAGE.
     <script src="Scripts/EWSQueue.js"></script>
     <script src="Scripts/BuildbotIteration.js"></script>
     <script src="Scripts/BuildbotTestResults.js"></script>
+    <script src="Scripts/Geometry.js"></script>
+    <script src="Scripts/Popover.js"></script>
+    <script src="Scripts/PopoverTracker.js"></script>
     <script src="Scripts/QueueView.js"></script>
     <script src="Scripts/BuildbotQueueView.js"></script>
     <script src="Scripts/BuildbotBuilderQueueView.js"></script>
index fbaf8a7..6a84cc9 100644 (file)
@@ -1,3 +1,73 @@
+2013-12-29  Alexey Proskuryakov  <ap@apple.com>
+
+        Please display information about pending runs in build.webkit.org/dashboard
+        https://bugs.webkit.org/show_bug.cgi?id=122180
+
+        Reviewed by Timothy Hatcher.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/BuildbotBuilderQueueView.js:
+        (BuildbotBuilderQueueView.prototype.update.appendBuilderQueueStatus): Added a semicolon at the end of a line.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/BuildbotQueueView.js:
+        (BuildbotQueueView.prototype._latestFinishedIteration): Factored out of _appendPendingRevisionCount.
+        (BuildbotQueueView.prototype._appendPendingRevisionCount): Install a popover tracker over the element.
+        (BuildbotQueueView.prototype.lineForCommit): Build an element for a particular commit ot be shown in popover.
+        (BuildbotQueueView.prototype.presentPopoverForElement): Build and show popover content when PopoverTracker
+        asks us to.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Geometry.js: Added.
+        Taken from WebInspector with minimal changes:
+        - Changed root name from WebInspector to Dashboard.
+        - Removed some unused functionality.
+        - Added Rect.containsPoint.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Popover.js: Added.
+        Popover has extensive changes compared to WebInspector version, only drawing code is the same:
+        - Fixed to work in scrollable pages - WebInspector version assumes that window
+        and document coordinates are the same, and also erroneously dismisses a scrollable
+        popover if scrolling cascades out of it after reaching a bound.
+        - Simplified API and implementation to Dashboard needs, it is no longer possible to
+        change content of an existing popover.
+        - Rewrote visibility tracking to be more complete, and not rely on external tracker
+        object so much.
+        - Added code to flash scroll bars when showing a scrollable popover.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/PopoverTracker.js: Added.
+        Objects of this class show and hide popovers as appropriate for registered active elements.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/StatusLineView.js:
+        (StatusLineView.prototype.get messageElement): Added an accessor, so that we could
+        install a popover on message element. It's the only visible element in pending commit
+        line, but the line has different bounds, so we can't install a popover on it (it
+        would be incorrectly positioned if we did).
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Trac.js:
+        (Trac.prototype._convertCommitInfoElementToObject):
+        - Some trac installations report author in a different element, updated to support that.
+        - Changed to parse title out of description, because trac titles are ugly. Also,
+        we get a nice HTML with links from the description.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Scripts/Utilities.js:
+        (Node.prototype.isAncestor): Copied from Web Inspector, only changing the form for
+        consistency with the rest of this file (add a property on prototype with assignment
+        instead of using Object.defineProperty).
+        (Node.prototype.isDescendant): Ditto.
+        (Node.prototype.isSelfOrAncestor): Ditto.
+        (Node.prototype.isSelfOrDescendant): Ditto.
+        (DOMTokenList.prototype.contains): Ditto.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Styles/Popover.css: Added.
+        Like JS counterpart, mostly lifted from Web Inspector.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Styles/QueueView.css:
+        Added style rules for pending commits popover.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/Styles/StatusLineView.css:
+        (.status-line.no-bubble .message): Changed to display:inline-block, so that it fits
+        to content, and we can show the popover in a correct place.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/dashboard/index.html: Added new files.
+
 2013-12-27  Gavin Barraclough  <barraclough@apple.com>
 
         Merge PageVisibilityState & ViewState::IsVisible in WebKit2