<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[175006] trunk/Websites/perf.webkit.org</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/175006">175006</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2014-10-21 17:53:39 -0700 (Tue, 21 Oct 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>Perf dashboard should provide a way to associate bugs with a test run
https://bugs.webkit.org/show_bug.cgi?id=137857

Reviewed by Andreas Kling.

Added a &quot;privileged&quot; API, /privileged-api/associate-bug, to associate a bug with a test run.
/privileged-api/ is to be protected by an authentication mechanism such as DigestAuth over https by
the Apache configuration.

The Cross Site Request (CSRF) Forgery prevention for privileged APIs work as follows. When a user is
about to make a privileged API access, the front end code obtains a CSRF token generated by POST'ing
to privileged-api/generate-csrf-token; the page sets a randomly generated salt and an expiration time
via the cookie and returns a token computed from those two values as well as the remote username.

The font end code then POST's the request along with the returned token. The server side code verifies
that the specified token can be generated from the salt and the expiration time set in the cookie, and
the token hasn't expired.

* init-database.sql: Added bug_url to bug_trackers table, and added bugs table. Each bug tracker will
have zero or exactly one bug associated with a test run.

* public/admin/bug-trackers.php: Added the support for editing bug_url.
* public/api/runs.php:
(fetch_runs_for_config): Modified the query to fetch bugs associated with test_runs.
(parse_bugs_array): Added. Parses the aggregated bugs and creates a dictionary that maps a tracker id to
an associated bug if there is one.
(format_run): Calls parse_bugs_array.

* public/include/json-header.php: Added helper functions to deal for CSRF prevention.
(ensure_privileged_api_data): Added. Dies immediately if the request's method is not POST or doesn't
have a valid JSON payload.
(ensure_privileged_api_data_and_token): Ditto. Also checks that the CSRF prevention token is valid.
(compute_token): Computes a CSRF token using the REMOTE_USER (e.g. set via BasicAuth), the salt, and
the expiration time stored in the cookie.
(verify_token): Returns true iff the specified token matches what compute_token returns from the cookie.

* public/include/manifest.php:
(ManifestGenerator::bug_trackers): Include bug_url as bugUrl in the manifest. Also use tracker_id instead
of tracker_name as the key in the manifest. This requires changes to both v1 and v2 front end.

* public/index.html:
(Chart..showTooltipWithResults): Updated for the manifest format changed mentioned above.

* public/privileged-api/associate-bug.php: Added.
(main): Added. Associates or dissociates a bug with a test run inside a transaction. It prevent a CSRF
attack via ensure_privileged_api_data_and_token, which calls verify_token.

* public/privileged-api/generate-csrf-token.php: Added. Generates a CSRF token valid for one hour.

* public/v2/app.css:
(.disabled .icon-button:hover g): Used by the &quot;bugs&quot; icon when a range of points or no points are
selected in a chart.

* public/v2/app.js:
(App.PaneController.actions.toggleBugsPane): Added. Toggles the visibility of the bugs pane when exactly
one point is selected in the chart. Also hides the search pane when making the bugs pane visible since
they would overlap on each other if both of them are shown.
(App.PaneController.actions.associateBug): Makes a privileged API request to associate the specified bug
with the currently selected point (test run). Updates the bug information in &quot;details&quot; and colors of dots
in the charts to reflect new states. Because chart data objects aren't real Ember objects for performance
reasons, we have to use a dirty hack of modifying a dummy counter bugsChangeCount.
(App.PaneController.actions.toggleSearchPane): Renamed from toggleSearch. Also hides the bugs pane when
showing the search pane.
(App.PaneController.actions.rangeChanged): Takes all selected points as the second argument instead of
taking start and end points as the second and the third arguments so that _showDetails can enumerate all
bugs in the selected range.

(App.PaneController._detailsChanged): Added. Hide the bugs pane whenever a new point is selected.
Also update singlySelectedPoint, which is used by toggleBugsPane and associateBug.
(App.PaneController._currentItemChanged): Updated for the _showDetails change.
(App.PaneController._showDetails): Takes an array of selected points in place of old arguments.
Simplified the code to compute the revision information. Calls _updateBugs to format the associated bugs.
(App.PaneController._updateBugs): Sets details.bugTrackers to a dictionary that maps a bug tracker id to
a bug tracker proxy with an array of (bugNumber, bugUrl) pairs and also editedBugNumber, which is used by
the bugs pane to associate or dissociate a bug number, if exactly one point is selected.

(App.InteractiveChartComponent._updateDotsWithBugs): Added. Sets hasBugs class on dots as needed.
(App.InteractiveChartComponent._setCurrentSelection): Finds and passes all points in the selected range
to selectionChanged action instead of just finding the first and the last points.

* public/v2/chart-pane.css: Updated the style.

* public/v2/data.js:
(PrivilegedAPI): Added. A wrapper for privileged APIs' CSRF tokens.
(PrivilegedAPI.sendRequest): Makes a privileged API call. Fetches a new CSRF token if needed.
(PrivilegedAPI._generateTokenInServerIfNeeded): Makes a request to privileged-api/generate-csrf-token if
we haven't already obtained a CSRF token or if the token has already been expired.
(PrivilegedAPI._post): Makes a single POST request to /privileged-api/* with a JSON payload.

(Measurement.prototype.bugs): Added.
(Measurement.prototype.hasBugs): Returns true iff bugs has more than one bug number.
(Measurement.prototype.associateBug): Associates a bug with a test run via privileged-api/associate-bug.

* public/v2/index.html: Added the bugs pane. Also added a list of bugs associated with the current run in
the details.

* public/v2/manifest.js:
(App.BugTracker.bugUrl):
(App.BugTracker.newBugUrl): Added.
(App.BugTracker.repositories): Added. This was a missing back reference to repositories.
(App.MetricSerializer.normalizePayload): Now parses/loads the list of bug trackers from the manifest.
(App.Manifest.repositoriesWithReportedCommits): Now initialized to an empty array instead of null.
(App.Manifest.bugTrackers): Added.
(App.Manifest._fetchedManifest): Sets App.Manifest.bugTrackers. Also sorts the list of repositories by
their respective ids to make the ordering stable.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorginitdatabasesql">trunk/Websites/perf.webkit.org/init-database.sql</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicadminbugtrackersphp">trunk/Websites/perf.webkit.org/public/admin/bug-trackers.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicapirunsphp">trunk/Websites/perf.webkit.org/public/api/runs.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludejsonheaderphp">trunk/Websites/perf.webkit.org/public/include/json-header.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludemanifestphp">trunk/Websites/perf.webkit.org/public/include/manifest.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicindexhtml">trunk/Websites/perf.webkit.org/public/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2appcss">trunk/Websites/perf.webkit.org/public/v2/app.css</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2appjs">trunk/Websites/perf.webkit.org/public/v2/app.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2chartpanecss">trunk/Websites/perf.webkit.org/public/v2/chart-pane.css</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2datajs">trunk/Websites/perf.webkit.org/public/v2/data.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2indexhtml">trunk/Websites/perf.webkit.org/public/v2/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2manifestjs">trunk/Websites/perf.webkit.org/public/v2/manifest.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li>trunk/Websites/perf.webkit.org/public/privileged-api/</li>
<li><a href="#trunkWebsitesperfwebkitorgpublicprivilegedapiassociatebugphp">trunk/Websites/perf.webkit.org/public/privileged-api/associate-bug.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicprivilegedapigeneratecsrftokenphp">trunk/Websites/perf.webkit.org/public/privileged-api/generate-csrf-token.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkWebsitesperfwebkitorgChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/ChangeLog (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -1,3 +1,113 @@
</span><ins>+2014-10-18  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Perf dashboard should provide a way to associate bugs with a test run
+        https://bugs.webkit.org/show_bug.cgi?id=137857
+
+        Reviewed by Andreas Kling.
+
+        Added a &quot;privileged&quot; API, /privileged-api/associate-bug, to associate a bug with a test run.
+        /privileged-api/ is to be protected by an authentication mechanism such as DigestAuth over https by
+        the Apache configuration.
+
+
+        The Cross Site Request (CSRF) Forgery prevention for privileged APIs work as follows. When a user is
+        about to make a privileged API access, the front end code obtains a CSRF token generated by POST'ing
+        to privileged-api/generate-csrf-token; the page sets a randomly generated salt and an expiration time
+        via the cookie and returns a token computed from those two values as well as the remote username.
+
+        The font end code then POST's the request along with the returned token. The server side code verifies
+        that the specified token can be generated from the salt and the expiration time set in the cookie, and
+        the token hasn't expired.
+
+
+        * init-database.sql: Added bug_url to bug_trackers table, and added bugs table. Each bug tracker will
+        have zero or exactly one bug associated with a test run.
+
+        * public/admin/bug-trackers.php: Added the support for editing bug_url.
+        * public/api/runs.php:
+        (fetch_runs_for_config): Modified the query to fetch bugs associated with test_runs.
+        (parse_bugs_array): Added. Parses the aggregated bugs and creates a dictionary that maps a tracker id to
+        an associated bug if there is one.
+        (format_run): Calls parse_bugs_array.
+
+        * public/include/json-header.php: Added helper functions to deal for CSRF prevention.
+        (ensure_privileged_api_data): Added. Dies immediately if the request's method is not POST or doesn't
+        have a valid JSON payload.
+        (ensure_privileged_api_data_and_token): Ditto. Also checks that the CSRF prevention token is valid.
+        (compute_token): Computes a CSRF token using the REMOTE_USER (e.g. set via BasicAuth), the salt, and
+        the expiration time stored in the cookie.
+        (verify_token): Returns true iff the specified token matches what compute_token returns from the cookie.
+
+        * public/include/manifest.php:
+        (ManifestGenerator::bug_trackers): Include bug_url as bugUrl in the manifest. Also use tracker_id instead
+        of tracker_name as the key in the manifest. This requires changes to both v1 and v2 front end.
+
+        * public/index.html:
+        (Chart..showTooltipWithResults): Updated for the manifest format changed mentioned above.
+
+        * public/privileged-api/associate-bug.php: Added.
+        (main): Added. Associates or dissociates a bug with a test run inside a transaction. It prevent a CSRF
+        attack via ensure_privileged_api_data_and_token, which calls verify_token.
+
+        * public/privileged-api/generate-csrf-token.php: Added. Generates a CSRF token valid for one hour.
+
+        * public/v2/app.css:
+        (.disabled .icon-button:hover g): Used by the &quot;bugs&quot; icon when a range of points or no points are
+        selected in a chart.
+
+        * public/v2/app.js:
+        (App.PaneController.actions.toggleBugsPane): Added. Toggles the visibility of the bugs pane when exactly
+        one point is selected in the chart. Also hides the search pane when making the bugs pane visible since
+        they would overlap on each other if both of them are shown.
+        (App.PaneController.actions.associateBug): Makes a privileged API request to associate the specified bug
+        with the currently selected point (test run). Updates the bug information in &quot;details&quot; and colors of dots
+        in the charts to reflect new states. Because chart data objects aren't real Ember objects for performance
+        reasons, we have to use a dirty hack of modifying a dummy counter bugsChangeCount.
+        (App.PaneController.actions.toggleSearchPane): Renamed from toggleSearch. Also hides the bugs pane when
+        showing the search pane.
+        (App.PaneController.actions.rangeChanged): Takes all selected points as the second argument instead of
+        taking start and end points as the second and the third arguments so that _showDetails can enumerate all
+        bugs in the selected range.
+
+        (App.PaneController._detailsChanged): Added. Hide the bugs pane whenever a new point is selected.
+        Also update singlySelectedPoint, which is used by toggleBugsPane and associateBug.
+        (App.PaneController._currentItemChanged): Updated for the _showDetails change.
+        (App.PaneController._showDetails): Takes an array of selected points in place of old arguments.
+        Simplified the code to compute the revision information. Calls _updateBugs to format the associated bugs.
+        (App.PaneController._updateBugs): Sets details.bugTrackers to a dictionary that maps a bug tracker id to
+        a bug tracker proxy with an array of (bugNumber, bugUrl) pairs and also editedBugNumber, which is used by
+        the bugs pane to associate or dissociate a bug number, if exactly one point is selected.
+
+        (App.InteractiveChartComponent._updateDotsWithBugs): Added. Sets hasBugs class on dots as needed.
+        (App.InteractiveChartComponent._setCurrentSelection): Finds and passes all points in the selected range
+        to selectionChanged action instead of just finding the first and the last points.
+
+        * public/v2/chart-pane.css: Updated the style.
+
+        * public/v2/data.js:
+        (PrivilegedAPI): Added. A wrapper for privileged APIs' CSRF tokens.
+        (PrivilegedAPI.sendRequest): Makes a privileged API call. Fetches a new CSRF token if needed.
+        (PrivilegedAPI._generateTokenInServerIfNeeded): Makes a request to privileged-api/generate-csrf-token if
+        we haven't already obtained a CSRF token or if the token has already been expired.
+        (PrivilegedAPI._post): Makes a single POST request to /privileged-api/* with a JSON payload.
+
+        (Measurement.prototype.bugs): Added.
+        (Measurement.prototype.hasBugs): Returns true iff bugs has more than one bug number.
+        (Measurement.prototype.associateBug): Associates a bug with a test run via privileged-api/associate-bug.
+
+        * public/v2/index.html: Added the bugs pane. Also added a list of bugs associated with the current run in
+        the details.
+
+        * public/v2/manifest.js:
+        (App.BugTracker.bugUrl):
+        (App.BugTracker.newBugUrl): Added.
+        (App.BugTracker.repositories): Added. This was a missing back reference to repositories.
+        (App.MetricSerializer.normalizePayload): Now parses/loads the list of bug trackers from the manifest.
+        (App.Manifest.repositoriesWithReportedCommits): Now initialized to an empty array instead of null.
+        (App.Manifest.bugTrackers): Added.
+        (App.Manifest._fetchedManifest): Sets App.Manifest.bugTrackers. Also sorts the list of repositories by
+        their respective ids to make the ordering stable.
+
</ins><span class="cx"> 2014-10-14  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Remove unused jobs table
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorginitdatabasesql"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/init-database.sql (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/init-database.sql        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/init-database.sql        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -29,6 +29,7 @@
</span><span class="cx"> CREATE TABLE bug_trackers (
</span><span class="cx">     tracker_id serial PRIMARY KEY,
</span><span class="cx">     tracker_name varchar(64) NOT NULL,
</span><ins>+    tracker_bug_url varchar(1024),
</ins><span class="cx">     tracker_new_bug_url varchar(1024));
</span><span class="cx"> 
</span><span class="cx"> CREATE TABLE tracker_repositories (
</span><span class="lines">@@ -128,3 +129,12 @@
</span><span class="cx">     report_content text,
</span><span class="cx">     report_failure varchar(64),
</span><span class="cx">     report_failure_details text);
</span><ins>+
+CREATE TABLE bugs (
+    bug_id serial PRIMARY KEY,
+    bug_run integer REFERENCES test_runs NOT NULL,
+    bug_tracker integer REFERENCES bug_trackers NOT NULL,
+    bug_number integer NOT NULL,
+    CONSTRAINT bug_tracker_and_run_must_be_unique UNIQUE(bug_tracker, bug_run));
+CREATE INDEX bugs_tracker_number_index ON bugs(bug_tracker, bug_number);
+CREATE INDEX bugs_run_index ON bugs(bug_run);
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicadminbugtrackersphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/admin/bug-trackers.php (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/admin/bug-trackers.php        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/admin/bug-trackers.php        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -11,7 +11,9 @@
</span><span class="cx">         } else
</span><span class="cx">             notice('Could not add the bug tracker.');
</span><span class="cx">     } else if ($action == 'update') {
</span><del>-        if (update_field('bug_trackers', 'tracker', 'name') || update_field('bug_trackers', 'tracker', 'new_bug_url'))
</del><ins>+        if (update_field('bug_trackers', 'tracker', 'name')
+            || update_field('bug_trackers', 'tracker', 'bug_url')
+            || update_field('bug_trackers', 'tracker', 'new_bug_url'))
</ins><span class="cx">             regenerate_manifest();
</span><span class="cx">         else
</span><span class="cx">             notice('Invalid parameters.');
</span><span class="lines">@@ -63,6 +65,7 @@
</span><span class="cx"> 
</span><span class="cx">     $page = new AdministrativePage($db, 'bug_trackers', 'tracker', array(
</span><span class="cx">         'name' =&gt; array('editing_mode' =&gt; 'string'),
</span><ins>+        'bug_url' =&gt; array('editing_mode' =&gt; 'url', 'label' =&gt; 'Bug URL ($number)'),
</ins><span class="cx">         'new_bug_url' =&gt; array('editing_mode' =&gt; 'text', 'label' =&gt; 'New Bug URL ($title, $description)'),
</span><span class="cx">         'Associated repositories' =&gt; array('custom' =&gt; function ($row) { return associated_repositories($row); }),
</span><span class="cx">     ));
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapirunsphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/api/runs.php (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/runs.php        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/api/runs.php        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -30,10 +30,14 @@
</span><span class="cx"> 
</span><span class="cx"> function fetch_runs_for_config($db, $config) {
</span><span class="cx">     $raw_runs = $db-&gt;query_and_fetch_all('
</span><del>-    SELECT test_runs.*, builds.*, array_agg((commit_repository, commit_revision, commit_time)) AS revisions
-        FROM builds LEFT OUTER JOIN build_commits ON commit_build = build_id LEFT OUTER JOIN commits ON build_commit = commit_id, test_runs
-        WHERE run_build = build_id AND run_config = $1
-        GROUP BY build_id, run_id', array($config['config_id']));
</del><ins>+        SELECT test_runs.*, builds.*, array_agg((commit_repository, commit_revision, commit_time)) AS revisions
+            FROM builds LEFT OUTER JOIN build_commits ON commit_build = build_id
+                LEFT OUTER JOIN commits ON build_commit = commit_id,
+                (SELECT test_runs.*, array_agg((bug_tracker, bug_number)) AS bugs
+                    FROM test_runs LEFT OUTER JOIN bugs ON bug_run = run_id WHERE run_config = $1 GROUP BY run_id) as test_runs
+                WHERE run_build = build_id
+                GROUP BY run_id, run_config, run_build, run_mean_cache, run_iteration_count_cache,
+                    run_sum_cache, run_square_sum_cache, bugs, build_id', array($config['config_id']));
</ins><span class="cx"> 
</span><span class="cx">     $formatted_runs = array();
</span><span class="cx">     if (!$raw_runs)
</span><span class="lines">@@ -62,6 +66,19 @@
</span><span class="cx">     return $revisions;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+function parse_bugs_array($postgres_array) {
+    // e.g. {&quot;(1 /* Bugzilla */, 12345)&quot;,&quot;(2 /* Radar */, 67890)&quot;}
+    $outer_array = json_decode('[' . trim($postgres_array, '{}') . ']');
+    $bugs = array();
+    foreach ($outer_array as $item) {
+        $raw_data = explode(',', trim($item, '()'));
+        if (!$raw_data[0])
+            continue;
+        $bugs[trim($raw_data[0], '&quot;')] = trim($raw_data[1], '&quot;');
+    }
+    return $bugs;
+}
+
</ins><span class="cx"> function format_run($run) {
</span><span class="cx">     return array(
</span><span class="cx">         'id' =&gt; intval($run['run_id']),
</span><span class="lines">@@ -70,6 +87,7 @@
</span><span class="cx">         'sum' =&gt; floatval($run['run_sum_cache']),
</span><span class="cx">         'squareSum' =&gt; floatval($run['run_square_sum_cache']),
</span><span class="cx">         'revisions' =&gt; parse_revisions_array($run['revisions']),
</span><ins>+        'bugs' =&gt; parse_bugs_array($run['bugs']),
</ins><span class="cx">         'buildTime' =&gt; strtotime($run['build_time']) * 1000,
</span><span class="cx">         'buildNumber' =&gt; intval($run['build_number']),
</span><span class="cx">         'builder' =&gt; $run['build_builder']);
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludejsonheaderphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/include/json-header.php (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/json-header.php        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/include/json-header.php        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -69,4 +69,42 @@
</span><span class="cx">     }
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+function ensure_privileged_api_data() {
+    global $HTTP_RAW_POST_DATA;
+
+    if ($_SERVER['REQUEST_METHOD'] != 'POST')
+        exit_with_error('InvalidRequestMethod');
+
+    if (!isset($HTTP_RAW_POST_DATA))
+        exit_with_error('InvalidRequestContent');
+
+    $data = json_decode($HTTP_RAW_POST_DATA, true);
+
+    if ($data === NULL)
+        exit_with_error('InvalidRequestContent');
+
+    return $data;
+}
+
+function ensure_privileged_api_data_and_token() {
+    $data = ensure_privileged_api_data();
+    if (!verify_token(array_get($data, 'token')))
+        exit_with_error('InvalidToken');
+    return $data;
+}
+
+function compute_token() {
+    if (!array_key_exists('CSRFSalt', $_COOKIE) || !array_key_exists('CSRFExpiration', $_COOKIE))
+        return NULL;
+    $user = array_get($_SERVER, 'REMOTE_USER');
+    $salt = $_COOKIE['CSRFSalt'];
+    $expiration = $_COOKIE['CSRFExpiration'];
+    return hash('sha256', &quot;$salt|$user|$expiration&quot;);
+}
+
+function verify_token($token) {
+    $expected_token = compute_token();
+    return $expected_token &amp;&amp; $token == $expected_token &amp;&amp; $_COOKIE['CSRFExpiration'] &gt; time();
+}
+
</ins><span class="cx"> ?&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludemanifestphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/include/manifest.php (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/manifest.php        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/include/manifest.php        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -139,7 +139,10 @@
</span><span class="cx">         $bug_trackers_table = $this-&gt;db-&gt;fetch_table('bug_trackers');
</span><span class="cx">         if ($bug_trackers_table) {
</span><span class="cx">             foreach ($bug_trackers_table as $row) {
</span><del>-                $bug_trackers[$row['tracker_name']] = array('newBugUrl' =&gt; $row['tracker_new_bug_url'],
</del><ins>+                $bug_trackers[$row['tracker_id']] = array(
+                    'name' =&gt; $row['tracker_name'],
+                    'bugUrl' =&gt; $row['tracker_bug_url'],
+                    'newBugUrl' =&gt; $row['tracker_new_bug_url'],
</ins><span class="cx">                     'repositories' =&gt; $tracker_id_to_repositories[$row['tracker_id']]);
</span><span class="cx">             }
</span><span class="cx">         }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicindexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/index.html (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/index.html        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/index.html        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -634,8 +634,9 @@
</span><span class="cx">                     + ' around ' + result.build().formattedTime();
</span><span class="cx">                 var revisions = result.build().formattedRevisions(resultToCompare.build());
</span><span class="cx"> 
</span><del>-                for (var trackerName in bugTrackers) {
-                    var repositories = bugTrackers[trackerName].repositories;
</del><ins>+                for (var trackerId in bugTrackers) {
+                    var tracker = bugTrackers[trackerId];
+                    var repositories = tracker.repositories;
</ins><span class="cx">                     var description = 'Platform: ' + result.build().platform().name + '\n\n';
</span><span class="cx">                     for (var i = 0; i &lt; repositories.length; ++i) {
</span><span class="cx">                         var repositoryName = repositories[i];
</span><span class="lines">@@ -648,13 +649,13 @@
</span><span class="cx">                             description += revision.label;
</span><span class="cx">                         description += '\n';
</span><span class="cx">                     }
</span><del>-                    var url = bugTrackers[trackerName].newBugUrl
</del><ins>+                    var url = tracker.newBugUrl
</ins><span class="cx">                         .replace(/\$title/g, encodeURIComponent(title))
</span><span class="cx">                         .replace(/\$description/g, encodeURIComponent(description))
</span><span class="cx">                         .replace(/\$link/g, encodeURIComponent(location.href));
</span><span class="cx">                     if (newBugUrls)
</span><span class="cx">                         newBugUrls += ',';
</span><del>-                    newBugUrls += ' &lt;a href=&quot;' + url + '&quot; target=&quot;_blank&quot;&gt;' + trackerName + '&lt;/a&gt;';
</del><ins>+                    newBugUrls += ' &lt;a href=&quot;' + url + '&quot; target=&quot;_blank&quot;&gt;' + tracker.name + '&lt;/a&gt;';
</ins><span class="cx">                 }
</span><span class="cx">                 newBugUrls = 'File:' + newBugUrls;
</span><span class="cx">             }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicprivilegedapiassociatebugphp"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/privileged-api/associate-bug.php (0 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/privileged-api/associate-bug.php                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/privileged-api/associate-bug.php        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -0,0 +1,39 @@
</span><ins>+&lt;?php
+
+require_once('../include/json-header.php');
+
+function main() {
+    $data = ensure_privileged_api_data_and_token();
+
+    $run_id = array_get($data, 'run');
+    $bug_tracker_id = array_get($data, 'tracker');
+    $bug_number = array_get($data, 'bugNumber');
+
+    if (!$run_id)
+        exit_with_error('InvalidRunId', array('run' =&gt; $run_id));
+    if (!$bug_tracker_id)
+        exit_with_error('InvalidBugTrackerId', array('tracker' =&gt; $bug_tracker_id));
+
+    $db = connect();
+    $db-&gt;begin_transaction();
+
+    $bug_id = NULL;
+    if (!$bug_number) {
+        $count = $db-&gt;query_and_get_affected_rows(&quot;DELETE FROM bugs WHERE bug_run = $1 AND bug_tracker = $2&quot;,
+            array($run_id, $bug_tracker_id));
+        if ($count &gt; 1) {
+            $db-&gt;rollback_transaction();
+            exit_with_error('UnexpectedNumberOfAffectedRows', array('affectedRows' =&gt; $count));
+        }
+    } else {
+        $bug_id = $db-&gt;update_or_insert_row('bugs', 'bug', array('run' =&gt; $run_id, 'tracker' =&gt; $bug_tracker_id),
+            array('run' =&gt; $run_id, 'tracker' =&gt; $bug_tracker_id, 'number' =&gt; $bug_number));
+    }
+    $db-&gt;commit_transaction();
+
+    exit_with_success(array('bug_id' =&gt; $bug_id));
+}
+
+main();
+
+?&gt;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicprivilegedapigeneratecsrftokenphp"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/privileged-api/generate-csrf-token.php (0 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/privileged-api/generate-csrf-token.php                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/privileged-api/generate-csrf-token.php        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -0,0 +1,18 @@
</span><ins>+&lt;?php
+
+require_once('../include/json-header.php');
+
+ensure_privileged_api_data();
+
+$user = array_get($_SERVER, 'REMOTE_USER');
+
+$expiritaion = time() + 3600; // Valid for one hour.
+$_COOKIE['CSRFSalt'] = rand();
+$_COOKIE['CSRFExpiration'] = $expiritaion;
+
+setcookie('CSRFSalt', $_COOKIE['CSRFSalt']);
+setcookie('CSRFExpiration', $expiritaion);
+
+exit_with_success(array('user' =&gt; $user, 'token' =&gt; compute_token(), 'expiration' =&gt; $expiritaion * 1000));
+
+?&gt;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2appcss"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/app.css (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/app.css        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/app.css        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -125,6 +125,9 @@
</span><span class="cx"> .icon-button:hover g {
</span><span class="cx">     stroke: #666;
</span><span class="cx"> }
</span><ins>+.disabled .icon-button:hover g {
+    stroke: #ccc;
+}
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> #header {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2appjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/app.js (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/app.js        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/app.js        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -664,6 +664,7 @@
</span><span class="cx">     sharedTime: Ember.computed.alias('parentController.sharedTime'),
</span><span class="cx">     sharedSelection: Ember.computed.alias('parentController.sharedSelection'),
</span><span class="cx">     selection: null,
</span><ins>+    bugsChangeCount: 0, // Dirty hack. Used to call InteractiveChartComponent's _updateDotsWithBugs.
</ins><span class="cx">     actions: {
</span><span class="cx">         toggleDetails: function()
</span><span class="cx">         {
</span><span class="lines">@@ -673,14 +674,33 @@
</span><span class="cx">         {
</span><span class="cx">             this.parentController.removePane(this.get('model'));
</span><span class="cx">         },
</span><del>-        toggleSearch: function ()
</del><ins>+        toggleBugsPane: function ()
</ins><span class="cx">         {
</span><ins>+            if (!App.Manifest.bugTrackers || !this.get('singlySelectedPoint'))
+                return;
+            if (this.toggleProperty('showingBugsPane'))
+                this.set('showingSearchPane', false);
+        },
+        associateBug: function (bugTracker, bugNumber)
+        {
+            var point = this.get('singlySelectedPoint');
+            if (!point)
+                return;
+            var self = this;
+            point.measurement.associateBug(bugTracker.get('id'), bugNumber).then(function () {
+                self._updateBugs();
+                self.set('bugsChangeCount', self.get('bugsChangeCount') + 1);
+            });
+        },
+        toggleSearchPane: function ()
+        {
</ins><span class="cx">             if (!App.Manifest.repositoriesWithReportedCommits)
</span><span class="cx">                 return;
</span><span class="cx">             var model = this.get('model');
</span><span class="cx">             if (!model.get('commitSearchRepository'))
</span><span class="cx">                 model.set('commitSearchRepository', App.Manifest.repositoriesWithReportedCommits[0]);
</span><del>-            this.toggleProperty('showingSearchPane');
</del><ins>+            if (this.toggleProperty('showingSearchPane'))
+                this.set('showingBugsPane', false);
</ins><span class="cx">         },
</span><span class="cx">         searchCommit: function () {
</span><span class="cx">             var model = this.get('model');
</span><span class="lines">@@ -697,19 +717,24 @@
</span><span class="cx">             this.set('intrinsicDomain', intrinsicDomain);
</span><span class="cx">             this.get('parentController').updateSharedDomain();
</span><span class="cx">         },
</span><del>-        rangeChanged: function (extent, startPoint, endPoint)
</del><ins>+        rangeChanged: function (extent, points)
</ins><span class="cx">         {
</span><del>-            if (!startPoint || !endPoint) {
</del><ins>+            if (!points) {
</ins><span class="cx">                 this._hasRange = false;
</span><span class="cx">                 this.set('details', null);
</span><span class="cx">                 this.set('timeRange', null);
</span><span class="cx">                 return;
</span><span class="cx">             }
</span><span class="cx">             this._hasRange = true;
</span><del>-            this._showDetails(startPoint.measurement, endPoint.measurement, false);
</del><ins>+            this._showDetails(points);
</ins><span class="cx">             this.set('timeRange', extent);
</span><span class="cx">         },
</span><span class="cx">     },
</span><ins>+    _detailsChanged: function ()
+    {
+        this.set('showingBugsPane', false);
+        this.set('singlySelectedPoint', !this._hasRange &amp;&amp; this._selectedPoints ? this._selectedPoints[0] : null);
+    }.observes('details'),
</ins><span class="cx">     _overviewSelectionChanged: function ()
</span><span class="cx">     {
</span><span class="cx">         var overviewSelection = this.get('overviewSelection');
</span><span class="lines">@@ -745,29 +770,25 @@
</span><span class="cx">         if (!point || !point.measurement)
</span><span class="cx">             this.set('details', null);
</span><span class="cx">         else
</span><del>-            this._showDetails(point.series.previousPoint(point).measurement, point.measurement, true);
</del><ins>+            this._showDetails([point]);
</ins><span class="cx">     }.observes('currentItem'),
</span><del>-    _showDetails: function (oldMeasurement, currentMeasurement, isShowingEndPoint)
</del><ins>+    _showDetails: function (points)
</ins><span class="cx">     {
</span><del>-        var revisions = [];
-
</del><ins>+        var isShowingEndPoint = !this._hasRange;
+        var currentMeasurement = points[0].measurement;
+        var oldMeasurement = points[points.length - 1].measurement;
</ins><span class="cx">         var formattedRevisions = currentMeasurement.formattedRevisions(oldMeasurement);
</span><del>-        var repositoryNames = [];
-        for (var repositoryName in formattedRevisions)
-            repositoryNames.push(repositoryName);
-        var revisions = [];
-        repositoryNames.sort().forEach(function (repositoryName) {
-            var revision = formattedRevisions[repositoryName];
-            var repository = App.Manifest.repository(repositoryName);
-            revision['url'] = false;
-            if (repository) {
-                revision['url'] = revision.previousRevision
-                    ? repository.urlForRevisionRange(revision.previousRevision, revision.currentRevision)
-                    : repository.urlForRevision(revision.currentRevision);
-            }
</del><ins>+        var revisions = App.Manifest.get('repositories')
+            .filter(function (repository) { return formattedRevisions[repository.get('id')]; })
+            .map(function (repository) {
+            var repositoryName = repository.get('id');
+            var revision = Ember.Object.create(formattedRevisions[repositoryName]);
+            revision['url'] = revision.previousRevision
+                ? repository.urlForRevisionRange(revision.previousRevision, revision.currentRevision)
+                : repository.urlForRevision(revision.currentRevision);
</ins><span class="cx">             revision['name'] = repositoryName;
</span><span class="cx">             revision['repository'] = repository;
</span><del>-            revisions.push(Ember.Object.create(revision));            
</del><ins>+            return revision; 
</ins><span class="cx">         });
</span><span class="cx"> 
</span><span class="cx">         var buildNumber = null;
</span><span class="lines">@@ -778,14 +799,48 @@
</span><span class="cx">             if (builder)
</span><span class="cx">                 buildURL = builder.urlFromBuildNumber(buildNumber);
</span><span class="cx">         }
</span><del>-        this.set('details', {
</del><ins>+
+        this._selectedPoints = points;
+        this.set('details', Ember.Object.create({
</ins><span class="cx">             currentValue: currentMeasurement.mean().toFixed(2),
</span><span class="cx">             oldValue: oldMeasurement &amp;&amp; !isShowingEndPoint ? oldMeasurement.mean().toFixed(2) : null,
</span><span class="cx">             buildNumber: buildNumber,
</span><span class="cx">             buildURL: buildURL,
</span><span class="cx">             buildTime: currentMeasurement.formattedBuildTime(),
</span><span class="cx">             revisions: revisions,
</span><ins>+        }));
+        this._updateBugs();
+    },
+    _updateBugs: function ()
+    {
+        if (!this._selectedPoints)
+            return;
+
+        var bugTrackers = App.Manifest.get('bugTrackers');
+        var trackerToBugNumbers = {};
+        bugTrackers.forEach(function (tracker) { trackerToBugNumbers[tracker.get('id')] = new Array(); });
+        this._selectedPoints.map(function (point) {
+            var bugs = point.measurement.bugs();
+            bugTrackers.forEach(function (tracker) {
+                var bugNumber = bugs[tracker.get('id')];
+                if (bugNumber)
+                    trackerToBugNumbers[tracker.get('id')].push(bugNumber);
+            });
</ins><span class="cx">         });
</span><ins>+
+        this.set('details.bugTrackers', App.Manifest.get('bugTrackers').map(function (tracker) {
+            var bugNumbers = trackerToBugNumbers[tracker.get('id')];
+            return Ember.ObjectProxy.create({
+                content: tracker,
+                bugs: bugNumbers.map(function (bugNumber) {
+                    return {
+                        bugNumber: bugNumber,
+                        bugUrl: bugNumber &amp;&amp; tracker.get('bugUrl') ? tracker.get('bugUrl').replace(/\$number/g, bugNumber) : null
+                    };
+                }),
+                editedBugNumber: this._hasRange ? null : bugNumbers[0],
+            }); // FIXME: Create urls for new bugs.
+        }));
</ins><span class="cx">     }
</span><span class="cx"> });
</span><span class="cx"> 
</span><span class="lines">@@ -1063,6 +1118,7 @@
</span><span class="cx">                 .attr(&quot;cx&quot;, function(measurement) { return xScale(measurement.time); })
</span><span class="cx">                 .attr(&quot;cy&quot;, function(measurement) { return yScale(measurement.value); });
</span><span class="cx">         });
</span><ins>+        this._updateDotsWithBugs();
</ins><span class="cx">         this._updateHighlightPositions();
</span><span class="cx"> 
</span><span class="cx">         if (this._brush) {
</span><span class="lines">@@ -1091,6 +1147,13 @@
</span><span class="cx">             .style(&quot;z-index&quot;, &quot;100&quot;)
</span><span class="cx">             .text(this._yAxisUnit);
</span><span class="cx">     },
</span><ins>+    _updateDotsWithBugs: function () {
+        if (!this.get('interactive'))
+            return;
+        this._dots.forEach(function (dot) {
+            dot.classed('hasBugs', function (point) { return !!point.measurement.hasBugs(); });
+        })
+    }.observes('bugsChangeCount'), // Never used for anything but to call this method :(
</ins><span class="cx">     _updateHighlightPositions: function () {
</span><span class="cx">         var xScale = this._x;
</span><span class="cx">         var yScale = this._y;
</span><span class="lines">@@ -1424,22 +1487,12 @@
</span><span class="cx">         if (this._brushExtent === newSelection)
</span><span class="cx">             return;
</span><span class="cx"> 
</span><ins>+        var points = null;
</ins><span class="cx">         if (newSelection) {
</span><del>-            var startPoint;
-            var endPoint;
-            for (var i = 0; i &lt; this._currentTimeSeriesData.length; i++) {
-                var point = this._currentTimeSeriesData[i];
-                if (!startPoint) {
-                    if (point.time &gt;= newSelection[0]) {
-                        if (point.time &gt; newSelection[1])
-                            break;
-                        startPoint = point;
-                    }
-                } else if (point.time &gt; newSelection[1])
-                    break;
-                if (point.time &gt;= newSelection[0] &amp;&amp; point.time &lt;= newSelection[1])
-                    endPoint = point;
-            }
</del><ins>+            points = this._currentTimeSeriesData
+                .filter(function (point) { return point.time &gt;= newSelection[0] &amp;&amp; point.time &lt;= newSelection[1]; });
+            if (!points.length)
+                points = null;
</ins><span class="cx">         }
</span><span class="cx"> 
</span><span class="cx">         this._brushExtent = newSelection;
</span><span class="lines">@@ -1447,7 +1500,7 @@
</span><span class="cx">         this._updateSelectionToolbar();
</span><span class="cx"> 
</span><span class="cx">         this.set('sharedSelection', newSelection);
</span><del>-        this.sendAction('selectionChanged', newSelection, startPoint, endPoint);
</del><ins>+        this.sendAction('selectionChanged', newSelection, points);
</ins><span class="cx">     },
</span><span class="cx">     _updateSelectionToolbar: function ()
</span><span class="cx">     {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2chartpanecss"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/chart-pane.css (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/chart-pane.css        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/chart-pane.css        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -50,6 +50,13 @@
</span><span class="cx">     top: 0.55rem;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+.chart-pane a.bugs-button {
+    display: inline-block;
+    position: absolute;
+    right: 1.85rem;
+    top: 0.55rem;
+}
+
</ins><span class="cx"> .chart-pane a.search-button {
</span><span class="cx">     display: inline-block;
</span><span class="cx">     position: absolute;
</span><span class="lines">@@ -57,9 +64,8 @@
</span><span class="cx">     top: 0.55rem;
</span><span class="cx"> }
</span><span class="cx"> 
</span><del>-.search-pane {
</del><ins>+.search-pane, .bugs-pane {
</ins><span class="cx">     position: absolute;
</span><del>-    right: 0rem;
</del><span class="cx">     top: 1.7rem;
</span><span class="cx">     border: 1px solid #bbb;
</span><span class="cx">     padding: 0;
</span><span class="lines">@@ -68,6 +74,24 @@
</span><span class="cx">     background: white;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+.bugs-pane {
+    right: 1.3rem;
+}
+
+.bugs-pane table {
+    margin: 0.2rem;
+    font-size: 0.8rem;
+}
+
+.bugs-pane th {
+    font-weight: normal;
+}
+
+.search-pane {
+    right: 0rem;
+}
+
+.bugs-pane.hidden,
</ins><span class="cx"> .search-pane.hidden {
</span><span class="cx">     display: none;
</span><span class="cx"> }
</span><span class="lines">@@ -186,12 +210,16 @@
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> .chart-pane .details-table th {
</span><del>-    width: 4rem;
</del><ins>+    width: 7rem;
</ins><span class="cx">     text-align: right;
</span><span class="cx">     font-weight: normal;
</span><span class="cx">     padding: 0;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+.chart-pane .details-table .bugs th {
+    font-weight: bold;
+}
+
</ins><span class="cx"> .chart-pane .details-table th:after {
</span><span class="cx">     content: &quot; : &quot;;
</span><span class="cx"> }
</span><span class="lines">@@ -230,6 +258,10 @@
</span><span class="cx">     stroke: none;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+.chart .hasBugs {
+    fill: #33f;
+}
+
</ins><span class="cx"> .chart path.area {
</span><span class="cx">     stroke: none;
</span><span class="cx">     fill: #ccc;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2datajs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/data.js (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/data.js        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/data.js        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -1,5 +1,55 @@
</span><span class="cx"> // We don't use DS.Model for these object types because we can't afford to process millions of them.
</span><span class="cx"> 
</span><ins>+var PrivilegedAPI = {
+    _token: null,
+    _expiration: null,
+    _maxNetworkLatency: 3 * 60 * 1000 /* 3 minutes */,
+};
+
+PrivilegedAPI.sendRequest = function (url, parameters)
+{
+    return this._generateTokenInServerIfNeeded().then(function (token) {
+        return PrivilegedAPI._post(url, $.extend({token: token}, parameters));
+    });
+}
+
+PrivilegedAPI._generateTokenInServerIfNeeded = function ()
+{
+    var self = this;
+    return new Ember.RSVP.Promise(function (resolve, reject) {
+        if (self._token &amp;&amp; self._expiration &gt; Date.now() + self._maxNetworkLatency)
+            resolve(self._token);
+
+        PrivilegedAPI._post('generate-csrf-token')
+            .then(function (result, reject) {
+                self._token = result['token'];
+                self._expiration = new Date(result['expiration']);
+                resolve(self._token);
+            }).catch(reject);
+    });
+}
+
+PrivilegedAPI._post = function (url, parameters)
+{
+    return new Ember.RSVP.Promise(function (resolve, reject) {
+        $.ajax({
+            url: '../privileged-api/' + url,
+            type: 'POST',
+            contentType: 'application/json',
+            data: parameters ? JSON.stringify(parameters) : '{}',
+            dataType: 'json',
+        }).done(function (data) {
+            if (data.status != 'OK')
+                reject(data.status);
+            else
+                resolve(data);
+        }).fail(function (xhr, status, error) {
+            console.log(xhr);
+            reject(xhr.status + (error ? ', ' + error : ''));
+        });
+    });
+}
+
</ins><span class="cx"> var CommitLogs = {
</span><span class="cx">     _cachedCommitsByRepository: {}
</span><span class="cx"> };
</span><span class="lines">@@ -220,6 +270,34 @@
</span><span class="cx">     return date.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+Measurement.prototype.bugs = function ()
+{
+    return this._raw['bugs'];
+}
+
+Measurement.prototype.hasBugs = function ()
+{
+    var bugs = this.bugs();
+    return bugs &amp;&amp; Object.keys(bugs).length;
+}
+
+Measurement.prototype.associateBug = function (trackerId, bugNumber)
+{
+    var bugs = this._raw['bugs'];
+    trackerId = parseInt(trackerId);
+    bugNumber = bugNumber ? parseInt(bugNumber) : null;
+    return PrivilegedAPI.sendRequest('associate-bug', {
+        run: this.id(),
+        tracker: trackerId,
+        bugNumber: bugNumber,
+    }).then(function () {
+        if (bugNumber)
+            bugs[trackerId] = bugNumber;
+        else
+            delete bugs[trackerId];
+    });
+}
+
</ins><span class="cx"> function RunsData(rawData)
</span><span class="cx"> {
</span><span class="cx">     this._measurements = rawData.map(function (run) { return new Measurement(run); });
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2indexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/index.html (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/index.html        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/index.html        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -135,9 +135,16 @@
</span><span class="cx">                     {{metric.label}}
</span><span class="cx">                     - {{ platform.name}}&lt;/h2&gt;
</span><span class="cx">                     &lt;a href=&quot;#&quot; title=&quot;Close&quot; class=&quot;close-button&quot; {{action &quot;close&quot;}}&gt;{{partial &quot;close-button&quot;}}&lt;/a&gt;
</span><del>-                    {{if App.Manifest.repositoriesWithReportedCommits}}
-                        &lt;a href=&quot;#&quot; title=&quot;Search&quot; class=&quot;search-button&quot; {{action &quot;toggleSearch&quot;}}&gt;{{partial &quot;search-button&quot;}}&lt;/a&gt;
</del><ins>+                    {{#if App.Manifest.bugTrackers}}
+                        &lt;a href=&quot;#&quot; title=&quot;Bugs&quot;
+                            {{bind-attr class=&quot;:bugs-button singlySelectedPoint::disabled&quot;}}
+                            {{action &quot;toggleBugsPane&quot;}}&gt;
+                            {{partial &quot;bugs-button&quot;}}
+                        &lt;/a&gt;
</ins><span class="cx">                     {{/if}}
</span><ins>+                    {{#if App.Manifest.repositoriesWithReportedCommits}}
+                        &lt;a href=&quot;#&quot; title=&quot;Search&quot; class=&quot;search-button&quot; {{action &quot;toggleSearchPane&quot;}}&gt;{{partial &quot;search-button&quot;}}&lt;/a&gt;
+                    {{/if}}
</ins><span class="cx">                 &lt;/header&gt;
</span><span class="cx"> 
</span><span class="cx">                 &lt;div class=&quot;body&quot;&gt;
</span><span class="lines">@@ -155,6 +162,7 @@
</span><span class="cx">                             sharedSelection=sharedSelection
</span><span class="cx">                             selectionChanged=&quot;rangeChanged&quot;
</span><span class="cx">                             selectionIsLocked=timeRangeIsLocked
</span><ins>+                            bugsChangeCount=bugsChangeCount
</ins><span class="cx">                             zoom=&quot;zoomed&quot;}}
</span><span class="cx">                     {{else}}
</span><span class="cx">                         {{#if failure}}
</span><span class="lines">@@ -191,6 +199,21 @@
</span><span class="cx">                     {{input action=&quot;searchCommit&quot; placeholder=&quot;Name or email&quot; value=commitSearchKeyword}}
</span><span class="cx">                 &lt;/form&gt;
</span><span class="cx"> 
</span><ins>+                &lt;div {{bind-attr class=&quot;:bugs-pane showingBugsPane::hidden&quot;}}&gt;
+                    &lt;table&gt;
+                        {{#each details.bugTrackers}}
+                            &lt;tr&gt;
+                                &lt;th&gt;{{label}}&lt;/th&gt;
+                                &lt;td&gt;
+                                    &lt;form {{action &quot;associateBug&quot; this editedBugNumber on=&quot;submit&quot;}}&gt;
+                                        {{input type=text value=editedBugNumber}}
+                                    &lt;/form&gt;
+                                &lt;/td&gt;
+                            &lt;/tr&gt;
+                        {{/each}}
+                    &lt;/table&gt;
+                &lt;/div&gt;
+
</ins><span class="cx">             &lt;/section&gt;
</span><span class="cx">         {{/each}}
</span><span class="cx">     &lt;/script&gt;
</span><span class="lines">@@ -214,6 +237,20 @@
</span><span class="cx">     &lt;script type=&quot;text/x-handlebars&quot; data-template-name=&quot;chart-details&quot;&gt;
</span><span class="cx">     &lt;div class=&quot;details-table-container&quot;&gt;
</span><span class="cx">         &lt;table class=&quot;details-table&quot;&gt;
</span><ins>+            &lt;tbody class=&quot;bugs&quot;&gt;
+            {{#each details.bugTrackers}}
+                {{#if bugs}}
+                    &lt;tr&gt;
+                        &lt;th&gt;{{label}}&lt;/th&gt;
+                        &lt;td&gt;
+                            {{#each bugs}}
+                                &lt;a {{bind-attr href=bugUrl}} target=&quot;_blank&quot;&gt;{{bugNumber}}&lt;/a&gt;
+                            {{/each}}
+                        &lt;/td&gt;
+                    &lt;/tr&gt;
+                {{/if}}
+            {{/each}}
+            &lt;/tbody&gt;
</ins><span class="cx">             &lt;tr&gt;&lt;th&gt;Current&lt;/th&gt;&lt;td&gt;{{details.currentValue}} {{chartData.unit}}
</span><span class="cx">             {{#if details.oldValue}}
</span><span class="cx">                 (from {{details.oldValue}})
</span><span class="lines">@@ -281,6 +318,16 @@
</span><span class="cx">         &lt;/svg&gt;
</span><span class="cx">     &lt;/script&gt;
</span><span class="cx"> 
</span><ins>+    &lt;script type=&quot;text/x-handlebars&quot; data-template-name=&quot;bugs-button&quot;&gt;
+        &lt;svg class=&quot;bugs-button icon-button&quot; viewBox=&quot;0 0 100 100&quot;&gt;
+            &lt;g stroke=&quot;black&quot; stroke-width=&quot;15&quot;&gt;
+                &lt;circle cx=&quot;50&quot; cy=&quot;50&quot; r=&quot;40&quot; fill=&quot;transparent&quot;/&gt;
+                &lt;line x1=&quot;50&quot; y1=&quot;25&quot; x2=&quot;50&quot; y2=&quot;55&quot;/&gt;
+                &lt;circle cx=&quot;50&quot; cy=&quot;67.5&quot; r=&quot;2.5&quot; fill=&quot;transparent&quot;/&gt;
+            &lt;/g&gt;
+        &lt;/svg&gt;
+    &lt;/script&gt;
+
</ins><span class="cx">     &lt;script type=&quot;text/x-handlebars&quot; data-template-name=&quot;search-button&quot;&gt;
</span><span class="cx">         &lt;svg class=&quot;search-button icon-button&quot; viewBox=&quot;0 0 100 100&quot;&gt;
</span><span class="cx">             &lt;g stroke=&quot;black&quot; stroke-width=&quot;15&quot;&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2manifestjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/manifest.js (175005 => 175006)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/manifest.js        2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/manifest.js        2014-10-22 00:53:39 UTC (rev 175006)
</span><span class="lines">@@ -42,7 +42,9 @@
</span><span class="cx"> });
</span><span class="cx"> 
</span><span class="cx"> App.BugTracker = App.NameLabelModel.extend({
</span><del>-    buildUrl: DS.attr('string'),
</del><ins>+    bugUrl: DS.attr('string'),
+    newBugUrl: DS.attr('string'),
+    repositories: DS.hasMany('repository'),
</ins><span class="cx"> });
</span><span class="cx"> 
</span><span class="cx"> App.Platform = App.NameLabelModel.extend({
</span><span class="lines">@@ -93,6 +95,7 @@
</span><span class="cx">             }),
</span><span class="cx">             metrics: this._normalizeIdMap(payload['metrics']),
</span><span class="cx">             repositories: this._normalizeIdMap(payload['repositories']),
</span><ins>+            bugTrackers: this._normalizeIdMap(payload['bugTrackers']),
</ins><span class="cx">         };
</span><span class="cx"> 
</span><span class="cx">         for (var testId in payload['tests']) {
</span><span class="lines">@@ -144,11 +147,12 @@
</span><span class="cx"> App.Manifest = Ember.Controller.extend({
</span><span class="cx">     platforms: null,
</span><span class="cx">     topLevelTests: null,
</span><ins>+    repositories: [],
+    repositoriesWithReportedCommits: [],
+    bugTrackers: [],
</ins><span class="cx">     _platformById: {},
</span><span class="cx">     _metricById: {},
</span><span class="cx">     _builderById: {},
</span><del>-    repositories: null,
-    repositoriesWithReportedCommits: null,
</del><span class="cx">     _repositoryById: {},
</span><span class="cx">     _fetchPromise: null,
</span><span class="cx">     fetch: function ()
</span><span class="lines">@@ -196,8 +200,10 @@
</span><span class="cx">         repositories.forEach(function (repository) {
</span><span class="cx">             self._repositoryById[repository.get('id')] = repository;
</span><span class="cx">         });
</span><del>-        this.set('repositories', repositories);
</del><ins>+        this.set('repositories', repositories.sortBy('id'));
</ins><span class="cx">         this.set('repositoriesWithReportedCommits',
</span><span class="cx">             repositories.filter(function (repository) { return repository.get('hasReportedCommits'); }));
</span><ins>+
+        this.set('bugTrackers', store.all('bugTracker').sortBy('name'));
</ins><span class="cx">     }
</span><span class="cx"> }).create();
</span></span></pre>
</div>
</div>

</body>
</html>