v3 UI should show status and associated bugs on analysis task pages
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 13 Feb 2016 21:30:10 +0000 (21:30 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 13 Feb 2016 21:30:10 +0000 (21:30 +0000)
https://bugs.webkit.org/show_bug.cgi?id=154212

Reviewed by Chris Dumez.

Added the capability to see and modify the status and the list of associated of bugs on analysis task pages.

Also added the list of related tasks, which are analysis tasks associated with the same bug or have
overlapping time ranges with the same test metric but on a potentially different platform.

In addition, categorize analysis tasks with the status of "no change" or "inconclusive" as "closed" as no
further action can be taken (users can bring them back to non-closed state without any restrictions).

* public/api/analysis-tasks.php:
(format_task): Categorize 'unchanged' and 'inconclusive' analysis tasks as closed.

* public/privileged-api/associate-bug.php:
(main): Added shouldDelete as a new mechanism to disassociate a bug since v3 UI shares a single Bug object
between multiple analysis tasks (as it should have been in the first place).

* public/v3/components/chart-pane-base.js:
(ChartPaneBase):
(ChartPaneBase.prototype._fetchAnalysisTasks): Since each analysis task's change type (status/result) could
change, we need to create annotation objects during each render() call.
(ChartPaneBase.prototype.render):
(ChartPaneBase.prototype._renderAnnotations): Extracted from ChartPaneBase.prototype._fetchAnalysisTasks to
do that. I was afraid of the perf impact of this but it doesn't seem to be an issue in my testing.
(ChartPaneBase.cssTemplate): Removed superfluous margins (moved to ChartPane.cssTemplate) around the charts
since they are only useful in the charts page.

* public/v3/models/analysis-task.js:
(AnalysisTask):
(AnalysisTask.prototype.updateSingleton): Added a comment as to why object.result cannot be renamed to
object.changeType in the JSON API.
(AnalysisTask.prototype.updateName): Added.
(AnalysisTask.prototype.updateChangeType): Added.
(AnalysisTask.prototype._updateRemoteState): Added.
(AnalysisTask.prototype.associateBug): Added.
(AnalysisTask.prototype.disassociateBug): Added.
(AnalysisTask.fetchRelatedTasks): Added. See above for the criteria of related-ness.

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskPage):
(AnalysisTaskPage.prototype.updateFromSerializedState):
(AnalysisTaskPage.prototype._fetchRelatedInfoForTaskId): Extracted from updateFromSerializedState.
(AnalysisTaskPage.prototype._didFetchRelatedAnalysisTasks): Added.
(AnalysisTaskPage.prototype.render): Render the list of associated bugs, the list of bug trackers (so that
users can use it to associate with a new bug), and the list of related analysis tasks.
(AnalysisTaskPage.prototype._renderTestGroupList): Extracted from render since it was getting too long.
(AnalysisTaskPage.prototype._renderTestGroupDetails): Ditto.
(AnalysisTaskPage.prototype._updateChangeType): Added.
(AnalysisTaskPage.prototype._associateBug): Added.
(AnalysisTaskPage.prototype._disassociateBug): Added.
(AnalysisTaskPage.htmlTemplate): Added various elements to show and modify the status, associate bugs,
and a list of related analysis tasks.
(AnalysisTaskPage.cssTemplate): Added various styles for those form controls.

* public/v3/pages/chart-pane.js:
(ChartPane.cssTemplate): Moved the margins from ChartPaneBase.cssTemplate.

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

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/api/analysis-tasks.php
Websites/perf.webkit.org/public/privileged-api/associate-bug.php
Websites/perf.webkit.org/public/v3/components/chart-pane-base.js
Websites/perf.webkit.org/public/v3/models/analysis-task.js
Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js
Websites/perf.webkit.org/public/v3/pages/chart-pane.js

index 6d9b00e..e5e7512 100644 (file)
@@ -1,3 +1,65 @@
+2016-02-13  Ryosuke Niwa  <rniwa@webkit.org>
+
+        v3 UI should show status and associated bugs on analysis task pages
+        https://bugs.webkit.org/show_bug.cgi?id=154212
+
+        Reviewed by Chris Dumez.
+
+        Added the capability to see and modify the status and the list of associated of bugs on analysis task pages.
+
+        Also added the list of related tasks, which are analysis tasks associated with the same bug or have
+        overlapping time ranges with the same test metric but on a potentially different platform.
+
+        In addition, categorize analysis tasks with the status of "no change" or "inconclusive" as "closed" as no
+        further action can be taken (users can bring them back to non-closed state without any restrictions).
+
+        * public/api/analysis-tasks.php:
+        (format_task): Categorize 'unchanged' and 'inconclusive' analysis tasks as closed.
+
+        * public/privileged-api/associate-bug.php:
+        (main): Added shouldDelete as a new mechanism to disassociate a bug since v3 UI shares a single Bug object
+        between multiple analysis tasks (as it should have been in the first place).
+
+        * public/v3/components/chart-pane-base.js:
+        (ChartPaneBase):
+        (ChartPaneBase.prototype._fetchAnalysisTasks): Since each analysis task's change type (status/result) could
+        change, we need to create annotation objects during each render() call.
+        (ChartPaneBase.prototype.render):
+        (ChartPaneBase.prototype._renderAnnotations): Extracted from ChartPaneBase.prototype._fetchAnalysisTasks to
+        do that. I was afraid of the perf impact of this but it doesn't seem to be an issue in my testing.
+        (ChartPaneBase.cssTemplate): Removed superfluous margins (moved to ChartPane.cssTemplate) around the charts
+        since they are only useful in the charts page.
+
+        * public/v3/models/analysis-task.js:
+        (AnalysisTask):
+        (AnalysisTask.prototype.updateSingleton): Added a comment as to why object.result cannot be renamed to
+        object.changeType in the JSON API.
+        (AnalysisTask.prototype.updateName): Added.
+        (AnalysisTask.prototype.updateChangeType): Added.
+        (AnalysisTask.prototype._updateRemoteState): Added.
+        (AnalysisTask.prototype.associateBug): Added.
+        (AnalysisTask.prototype.disassociateBug): Added.
+        (AnalysisTask.fetchRelatedTasks): Added. See above for the criteria of related-ness.
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskPage):
+        (AnalysisTaskPage.prototype.updateFromSerializedState):
+        (AnalysisTaskPage.prototype._fetchRelatedInfoForTaskId): Extracted from updateFromSerializedState.
+        (AnalysisTaskPage.prototype._didFetchRelatedAnalysisTasks): Added.
+        (AnalysisTaskPage.prototype.render): Render the list of associated bugs, the list of bug trackers (so that
+        users can use it to associate with a new bug), and the list of related analysis tasks.
+        (AnalysisTaskPage.prototype._renderTestGroupList): Extracted from render since it was getting too long.
+        (AnalysisTaskPage.prototype._renderTestGroupDetails): Ditto.
+        (AnalysisTaskPage.prototype._updateChangeType): Added.
+        (AnalysisTaskPage.prototype._associateBug): Added.
+        (AnalysisTaskPage.prototype._disassociateBug): Added.
+        (AnalysisTaskPage.htmlTemplate): Added various elements to show and modify the status, associate bugs,
+        and a list of related analysis tasks.
+        (AnalysisTaskPage.cssTemplate): Added various styles for those form controls.
+
+        * public/v3/pages/chart-pane.js:
+        (ChartPane.cssTemplate): Moved the margins from ChartPaneBase.cssTemplate.
+
 2016-02-12  Ryosuke Niwa  <rniwa@webkit.org>
 
         Perf dashboard should allow renaming analysis tasks and test groups
index 47d2159..43673a5 100644 (file)
@@ -85,6 +85,13 @@ function fetch_and_push_bugs_to_tasks($db, &$tasks) {
 }
 
 function format_task($task_row) {
+    $category = 'unconfirmed';
+    $result = $task_row['task_result'];
+    if ($result == 'unchanged' || $result == 'inconclusive')
+        $category = 'closed';
+    else if ($result)
+        $category = 'bisecting';
+
     return array(
         'id' => $task_row['task_id'],
         'name' => $task_row['task_name'],
@@ -98,8 +105,8 @@ function format_task($task_row) {
         'startRunTime' => Database::to_js_time($task_row['task_start_run_time']),
         'endRun' => $task_row['task_end_run'],
         'endRunTime' => Database::to_js_time($task_row['task_end_run_time']),
-        'category' => $task_row['task_result'] ? 'bisecting' : 'unconfirmed',
-        'result' => $task_row['task_result'],
+        'category' => $category,
+        'result' => $result,
         'needed' => $task_row['task_needed'] ? Database::is_true($task_row['task_needed']) : null,
         'bugs' => array(),
     );
index 4318d4a..f16ec5b 100644 (file)
@@ -9,11 +9,12 @@ function main() {
     $bug_tracker_id = array_get($data, 'bugTracker');
     $bug_number = array_get($data, 'number');
     $bug_id = array_get($data, 'bugToDelete');
+    $should_delete = array_get($data, 'shouldDelete');
 
     $db = connect();
     $db->begin_transaction();
 
-    if ($bug_id) {
+    if ($bug_id) { // V2 compatibility
         require_format('BugToDelete', $bug_id, '/^\d+$/');
         $count = $db->query_and_get_affected_rows("DELETE FROM bugs WHERE bug_id = $1", array($bug_id));
         if ($count != 1) {
@@ -24,7 +25,16 @@ function main() {
         require_format('AnalysisTask', $analysis_task_id, '/^\d+$/');
         require_format('BugTracker', $bug_tracker_id, '/^\d+$/');
         require_format('BugNumber', $bug_number, '/^\d+$/');
-        $bug_id = $db->insert_row('bugs', 'bug', array('task' => $analysis_task_id, 'tracker' => $bug_tracker_id, 'number' => $bug_number));
+        if ($should_delete) { // V3
+            $count = $db->query_and_get_affected_rows("DELETE FROM bugs WHERE bug_task = $1 AND bug_tracker = $2 AND bug_number = $3",
+                array($analysis_task_id, $bug_tracker_id, $bug_number));
+            if ($count < 1) { // FIXME: We should forbid duplicate bugs (same bug number on same tracker for same task)
+                $db->rollback_transaction();
+                exit_with_error('UnexpectedNumberOfAffectedRows', array('affectedRows' => $count));
+            }
+        } else {
+            $bug_id = $db->insert_row('bugs', 'bug', array('task' => $analysis_task_id, 'tracker' => $bug_tracker_id, 'number' => $bug_number));
+        }
     }
     $db->commit_transaction();
 
index 906db77..37e5f6d 100644 (file)
@@ -15,6 +15,7 @@ class ChartPaneBase extends ComponentBase {
         this._mainChart = null;
         this._mainChartStatus = null;
         this._commitLogViewer = null;
+        this._tasksForAnnotations = null;
     }
 
     configure(platformId, metricId)
@@ -63,32 +64,11 @@ class ChartPaneBase extends ComponentBase {
 
     _fetchAnalysisTasks(platformId, metricId)
     {
+        // FIXME: we need to update the annotation bars when the change type of tasks change.
         var self = this;
         AnalysisTask.fetchByPlatformAndMetric(platformId, metricId).then(function (tasks) {
-            self._mainChart.setAnnotations(tasks.map(function (task) {
-                var fillStyle = '#fc6';
-                switch (task.changeType()) {
-                case 'inconclusive':
-                    fillStyle = '#fcc';
-                case 'progression':
-                    fillStyle = '#39f';
-                    break;
-                case 'regression':
-                    fillStyle = '#c60';
-                    break;
-                case 'unchanged':
-                    fillStyle = '#ccc';
-                    break;
-                }
-
-                return {
-                    task: task,
-                    startTime: task.startTime(),
-                    endTime: task.endTime(),
-                    label: task.label(),
-                    fillStyle: fillStyle,
-                };
-            }));
+            self._tasksForAnnotations = tasks;
+            self.render();
         });
     }
 
@@ -193,6 +173,8 @@ class ChartPaneBase extends ComponentBase {
             return;
         }
 
+        this._renderAnnotations();
+
         if (this._mainChartStatus)
             this._mainChartStatus.render();
 
@@ -206,6 +188,38 @@ class ChartPaneBase extends ComponentBase {
         Instrumentation.endMeasuringTime('ChartPane', 'render');
     }
 
+    _renderAnnotations()
+    {
+        if (!this._tasksForAnnotations)
+            return;
+
+        var annotations = this._tasksForAnnotations.map(function (task) {
+            var fillStyle = '#fc6';
+            switch (task.changeType()) {
+            case 'inconclusive':
+                fillStyle = '#fcc';
+            case 'progression':
+                fillStyle = '#39f';
+                break;
+            case 'regression':
+                fillStyle = '#c60';
+                break;
+            case 'unchanged':
+                fillStyle = '#ccc';
+                break;
+            }
+
+            return {
+                task: task,
+                startTime: task.startTime(),
+                endTime: task.endTime(),
+                label: task.label(),
+                fillStyle: fillStyle,
+            };
+        });
+        this._mainChart.setAnnotations(annotations);
+    }
+
     static htmlTemplate()
     {
         return `
@@ -231,8 +245,6 @@ class ChartPaneBase extends ComponentBase {
     {
         return Toolbar.cssTemplate() + `
             .chart-pane {
-                margin: 1rem;
-                margin-bottom: 2rem;
                 padding: 0rem;
                 height: 18rem;
                 outline: none;
index dc9d6a3..22408bc 100644 (file)
@@ -17,7 +17,7 @@ class AnalysisTask extends LabeledObject {
         this._endMeasurementId = object.endRun;
         this._endTime = object.endRunTime;
         this._category = object.category;
-        this._changeType = object.result;
+        this._changeType = object.result; // Can't change due to v2 compatibility.
         this._needed = object.needed;
         this._bugs = object.bugs || [];
         this._buildRequestCount = object.buildRequestCount;
@@ -43,7 +43,7 @@ class AnalysisTask extends LabeledObject {
         console.assert(this._endTime == object.endRunTime);
 
         this._category = object.category;
-        this._changeType = object.result;
+        this._changeType = object.result; // Can't change due to v2 compatibility.
         this._needed = object.needed;
         this._bugs = object.bugs || [];
         this._buildRequestCount = object.buildRequestCount;
@@ -67,13 +67,43 @@ class AnalysisTask extends LabeledObject {
     category() { return this._category; }
     changeType() { return this._changeType; }
 
-    updateName(newName)
+    updateName(newName) { return this._updateRemoteState({name: newName}); }
+    updateChangeType(changeType) { return this._updateRemoteState({result: changeType}); }
+
+    _updateRemoteState(param)
+    {
+        param.task = this.id();
+        return PrivilegedAPI.sendRequest('update-analysis-task', param).then(function (data) {
+            return AnalysisTask.cachedFetch('../api/analysis-tasks', {id: param.task}, true)
+                .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
+        });
+    }
+
+    associateBug(tracker, bugNumber)
     {
-        var self = this;
+        console.assert(tracker instanceof BugTracker);
+        console.assert(typeof(bugNumber) == 'number');
         var id = this.id();
-        return PrivilegedAPI.sendRequest('update-analysis-task', {
+        return PrivilegedAPI.sendRequest('associate-bug', {
             task: id,
-            name: newName,
+            bugTracker: tracker.id(),
+            number: bugNumber,
+        }).then(function (data) {
+            return AnalysisTask.cachedFetch('../api/analysis-tasks', {id: id}, true)
+                .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
+        });
+    }
+
+    disassociateBug(bug)
+    {
+        console.assert(bug instanceof Bug);
+        console.assert(this.bugs().includes(bug));
+        var id = this.id();
+        return PrivilegedAPI.sendRequest('associate-bug', {
+            task: id,
+            bugTracker: bug.bugTracker().id(),
+            number: bug.bugNumber(),
+            shouldDelete: true,
         }).then(function (data) {
             return AnalysisTask.cachedFetch('../api/analysis-tasks', {id: id}, true)
                 .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
@@ -107,6 +137,31 @@ class AnalysisTask extends LabeledObject {
         });
     }
 
+    static fetchRelatedTasks(taskId)
+    {
+        // FIXME: We should add new sever-side API to just fetch the related tasks.
+        return this.fetchAll().then(function () {
+            var task = AnalysisTask.findById(taskId);
+            if (!task)
+                return undefined;
+            var relatedTasks = new Set;
+            for (var bug of task.bugs()) {
+                for (var otherTask of AnalysisTask.all()) {
+                    if (otherTask.bugs().includes(bug))
+                        relatedTasks.add(otherTask);
+                }
+            }
+            for (var otherTask of AnalysisTask.all()) {
+                if (task.endTime() < otherTask.startTime()
+                    || otherTask.endTime() < task.startTime()
+                    || task.metric() != otherTask.metric())
+                    continue;
+                relatedTasks.add(otherTask);
+            }
+            return Array.from(relatedTasks);
+        });
+    }
+
     static _fetchSubset(params)
     {
         if (this._fetchAllPromise)
index c5d14a3..b80e9b1 100644 (file)
@@ -17,6 +17,7 @@ class AnalysisTaskPage extends PageWithHeading {
     {
         super('Analysis Task');
         this._task = null;
+        this._relatedTasks = null;
         this._testGroups = null;
         this._renderedTestGroups = null;
         this._testGroupLabelMap = new Map;
@@ -27,6 +28,7 @@ class AnalysisTaskPage extends PageWithHeading {
         this._endPoint = null;
         this._errorMessage = null;
         this._currentTestGroup = null;
+
         this._chartPane = this.content().querySelector('analysis-task-chart-pane').component();
         this._chartPane.setPage(this);
         this._analysisResultsViewer = this.content().querySelector('analysis-results-viewer').component();
@@ -36,6 +38,13 @@ class AnalysisTaskPage extends PageWithHeading {
         this._taskNameLabel.setStartedEditingCallback(this._didStartEditingTaskName.bind(this));
         this._taskNameLabel.setUpdateCallback(this._updateTaskName.bind(this));
 
+        this.content().querySelector('.change-type-form').onsubmit = this._updateChangeType.bind(this);
+        this._taskStatusControl = this.content().querySelector('.change-type-form select');
+
+        this.content().querySelector('.associate-bug-form').onsubmit = this._associateBug.bind(this);
+        this._bugTrackerControl = this.content().querySelector('.bug-tracker-control');
+        this._bugNumberControl = this.content().querySelector('.bug-number-control');
+
         this.content().querySelector('.test-group-retry-form').onsubmit = this._retryCurrentTestGroup.bind(this);
     }
 
@@ -51,13 +60,11 @@ class AnalysisTaskPage extends PageWithHeading {
                 self._errorMessage = `Failed to fetch the analysis task ${state.remainingRoute}: ${error}`;
                 self.render();
             });
-            TestGroup.fetchByTask(taskId).then(this._didFetchTestGroups.bind(this));
-            AnalysisResults.fetch(taskId).then(this._didFetchAnalysisResults.bind(this));
+            this._fetchRelatedInfoForTaskId(taskId);
         } else if (state.buildRequest) {
             var buildRequestId = parseInt(state.buildRequest);
             AnalysisTask.fetchByBuildRequestId(buildRequestId).then(this._didFetchTask.bind(this)).then(function (task) {
-                TestGroup.fetchByTask(task.id()).then(self._didFetchTestGroups.bind(self));
-                AnalysisResults.fetch(task.id()).then(self._didFetchAnalysisResults.bind(self));
+                self._fetchRelatedInfoForTaskId(task.id());
             }, function (error) {
                 self._errorMessage = `Failed to fetch the analysis task for the build request ${buildRequestId}: ${error}`;
                 self.render();
@@ -65,6 +72,13 @@ class AnalysisTaskPage extends PageWithHeading {
         }
     }
 
+    _fetchRelatedInfoForTaskId(taskId)
+    {
+        TestGroup.fetchByTask(taskId).then(this._didFetchTestGroups.bind(this));
+        AnalysisResults.fetch(taskId).then(this._didFetchAnalysisResults.bind(this));
+        AnalysisTask.fetchRelatedTasks(taskId).then(this._didFetchRelatedAnalysisTasks.bind(this));
+    }
+
     _didFetchTask(task)
     {
         console.assert(!this._task);
@@ -92,6 +106,12 @@ class AnalysisTaskPage extends PageWithHeading {
         return task;
     }
 
+    _didFetchRelatedAnalysisTasks(relatedTasks)
+    {
+        this._relatedTasks = relatedTasks;
+        this.render();
+    }
+
     _didFetchMeasurement()
     {
         console.assert(this._task);
@@ -159,6 +179,8 @@ class AnalysisTaskPage extends PageWithHeading {
 
         this._chartPane.render();
 
+        var element = ComponentBase.createElement;
+        var link = ComponentBase.createLink;
         if (this._task) {
             this._taskNameLabel.setText(this._task.name());
             var platform = this._task.platform();
@@ -166,14 +188,59 @@ class AnalysisTaskPage extends PageWithHeading {
             var anchor = this.content().querySelector('.platform-metric-names a');
             this.renderReplace(anchor, metric.fullName() + ' on ' + platform.label());
             anchor.href = this.router().url('charts', ChartsPage.createStateForAnalysisTask(this._task));
+
+            var bugs = [];
+            for (var bug of this._task.bugs()) {
+                bugs.push(element('li', [
+                    bug.bugTracker().label() + ' ',
+                    link(bug.label(), bug.title(), bug.url()),
+                    ' ',
+                    link(new CloseButton, 'Disassociate this bug', this._disassociateBug.bind(this, bug))]));
+            }
+            this.renderReplace(this.content().querySelector('.associated-bugs'), bugs);
+
+            this._taskStatusControl.value = this._task.changeType() || 'unconfirmed';
         }
+
+        var element = ComponentBase.createElement;
+        this.renderReplace(this._bugTrackerControl,
+            BugTracker.all().map(function (tracker) {
+                return element('option', {value: tracker.id()}, tracker.label());
+            }));
+
+        this.content().querySelector('.analysis-task-status').style.display = this._task ? null : 'none';
         this.content().querySelector('.overview-chart').style.display = this._task ? null : 'none';
         this.content().querySelector('.test-group-view').style.display = this._task ? null : 'none';
         this._taskNameLabel.render();
 
+        if (this._relatedTasks && this._task) {
+            var router = this.router();
+            var link = ComponentBase.createLink;
+            var thisTask = this._task;
+            this.renderReplace(this.content().querySelector('.related-tasks-list'),
+                this._relatedTasks.map(function (otherTask) {
+                    console.assert(otherTask.metric() == thisTask.metric());
+                    var suffix = '';
+                    var taskLabel = otherTask.label();
+                    if (otherTask.platform() != thisTask.platform() && taskLabel.indexOf(otherTask.platform().label()) < 0)
+                        suffix = ` on ${otherTask.platform().label()}`;
+                    return element('li', [link(taskLabel, router.url(`analysis/task/${otherTask.id()}`)), suffix]);
+                }));
+        }
+
         this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
         this._analysisResultsViewer.render();
 
+        this._renderTestGroupList();
+        this._renderTestGroupDetails();
+
+        this._testGroupResultsTable.render();
+
+        Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
+    }
+
+    _renderTestGroupList()
+    {
         var element = ComponentBase.createElement;
         var link = ComponentBase.createLink;
         if (this._testGroups != this._renderedTestGroups) {
@@ -205,7 +272,10 @@ class AnalysisTaskPage extends PageWithHeading {
                 label.render();
             }
         }
+    }
 
+    _renderTestGroupDetails()
+    {
         if (this._renderedCurrentTestGroup !== this._currentTestGroup) {
             if (this._renderedCurrentTestGroup) {
                 var element = this.content().querySelector('.test-group-list-' + this._renderedCurrentTestGroup.id());
@@ -237,10 +307,6 @@ class AnalysisTaskPage extends PageWithHeading {
         }
 
         this.content().querySelector('.test-group-retry-button').disabled = !(this._currentTestGroup || this._startPoint);
-
-        this._testGroupResultsTable.render();
-
-        Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
     }
 
     _showTestGroup(testGroup)
@@ -283,6 +349,47 @@ class AnalysisTaskPage extends PageWithHeading {
         });
     }
 
+    _updateChangeType(event)
+    {
+        event.preventDefault();
+        console.assert(this._task);
+
+        var newChangeType = this._taskStatusControl.value;
+        if (newChangeType == 'unconfirmed')
+            newChangeType = null;
+
+        var render = this.render.bind(this);
+        return this._task.updateChangeType(newChangeType).then(render, function (error) {
+            render();
+            alert('Failed to update the status: ' + error);
+        });
+    }
+
+    _associateBug(event)
+    {
+        event.preventDefault();
+        console.assert(this._task);
+
+        var tracker = BugTracker.findById(this._bugTrackerControl.value);
+        console.assert(tracker);
+        var bugNumber = parseInt(this._bugNumberControl.value);
+
+        var render = this.render.bind(this);
+        return this._task.associateBug(tracker, bugNumber).then(render, function (error) {
+            render();
+            alert('Failed to associate the bug: ' + error);
+        });
+    }
+
+    _disassociateBug(bug)
+    {
+        var render = this.render.bind(this);
+        return this._task.disassociateBug(bug).then(render, function (error) {
+            render();
+            alert('Failed to disassociate the bug: ' + error);
+        });
+    }
+
     _retryCurrentTestGroup(event)
     {
         event.preventDefault();
@@ -366,12 +473,41 @@ class AnalysisTaskPage extends PageWithHeading {
     static htmlTemplate()
     {
         return `
-        <div class="analysis-tasl-page-container">
-            <div class="analysis-tasl-page">
+            <div class="analysis-task-page">
                 <h2 class="analysis-task-name"><editable-text></editable-text></h2>
                 <h3 class="platform-metric-names"><a href=""></a></h3>
                 <p class="error-message"></p>
-                <div class="overview-chart"><analysis-task-chart-pane></analysis-task-chart-pane></div>
+                <div class="analysis-task-status">
+                    <section>
+                        <h3>Status</h3>
+                        <form class="change-type-form">
+                            <select>
+                                <option value="unconfirmed">Unconfirmed</option>
+                                <option value="regression">Definite regression</option>
+                                <option value="progression">Definite progression</option>
+                                <option value="inconclusive">Inconclusive (Closed)</option>
+                                <option value="unchanged">No change (Closed)</option>
+                            </select>
+                            <button type="submit">Save</button>
+                        </form>
+                    </section>
+                    <section>
+                        <h3>Associated Bugs</h3>
+                        <ul class="associated-bugs"></ul>
+                        <form class="associate-bug-form">
+                            <select class="bug-tracker-control"></select>
+                            <input type="number" class="bug-number-control">
+                            <button type="submit">Add</button>
+                        </form>
+                    </section>
+                    <section class="related-tasks">
+                        <h3>Related Tasks</h3>
+                        <ul class="related-tasks-list"></ul>
+                    </section>
+                </div>
+                <section class="overview-chart">
+                    <analysis-task-chart-pane></analysis-task-chart-pane>
+                </section>
                 <section class="analysis-results-view">
                     <analysis-results-viewer></analysis-results-viewer>
                 </section>
@@ -399,16 +535,13 @@ class AnalysisTaskPage extends PageWithHeading {
                     </div>
                 </section>
             </div>
-        </div>
 `;
     }
 
     static cssTemplate()
     {
         return `
-            .analysis-tasl-page-container {
-            }
-            .analysis-tasl-page {
+            .analysis-task-page {
             }
 
             .analysis-task-name {
@@ -441,6 +574,48 @@ class AnalysisTaskPage extends PageWithHeading {
                 padding: 0;
             }
 
+            .overview-chart {
+                margin: 0 1rem;
+            }
+
+            .analysis-task-status {
+                margin: 0;
+                display: flex;
+                margin-bottom: 1.5rem;
+            }
+
+            .analysis-task-status > section {
+                flex-grow: 1;
+                border-left: solid 1px #eee;
+                padding-left: 1rem;
+            }
+
+            .analysis-task-status > section:first-child {
+                border-left: none;
+            }
+
+            .associated-bugs:not(:empty) {
+                margin-bottom: 1rem;
+            }
+
+            .analysis-task-status h3 {
+                font-size: 1rem;
+                font-weight: inherit;
+                color: #c93;
+            }
+
+            .analysis-task-status ul,
+            .analysis-task-status li {
+                list-style: none;
+                padding: 0;
+                margin: 0;
+            }
+
+            .related-tasks-list {
+                max-height: 10rem;
+                overflow-y: scroll;
+            }
+
             .analysis-results-view {
                 margin: 1rem;
             }
@@ -506,13 +681,6 @@ class AnalysisTaskPage extends PageWithHeading {
             .test-group-list > li:not(.selected) > a:hover {
                 background: #eee;
             }
-
-            .x-overview-chart {
-                width: auto;
-                height: 10rem;
-                margin: 1rem;
-                border: solid 0px red;
-            }
 `;
     }
 }
index 2e0705d..ecf3073 100644 (file)
@@ -239,6 +239,8 @@ class ChartPane extends ChartPaneBase {
             .chart-pane {
                 border: solid 1px #ccc;
                 border-radius: 0.5rem;
+                margin: 1rem;
+                margin-bottom: 2rem;
             }
 
             .chart-pane-body {