There should be a way to associate bugs with analysis tasks
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 22 Nov 2014 02:06:00 +0000 (02:06 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 22 Nov 2014 02:06:00 +0000 (02:06 +0000)
https://bugs.webkit.org/show_bug.cgi?id=138977

Reviewed by Benjamin Poulain.

Updated associate-bug.php to match the new database schema.

* public/include/json-header.php:
(require_format): Removed the call to camel_case_words_separated_by_underscore since the name is
already camel-cased in require_existence_of. This makes the function usable elsewhere.

* public/privileged-api/associate-bug.php:
(main): Changed the API to take run, bugTracker, and number to match the new database schema.
Also verify that those values are integers using require_format.

* public/v2/analysis.js:
(App.AnalysisTask.label): Added. Concatenates the task's name with the bug numbers.
(App.Bug.label): Added.
(App.BugAdapter): Added.
(App.BugAdapter.createRecord): Use PrivilegedAPI instead of the builtin ajax call.
(App.BuildRequest): Inherit from newly added App.Model, which is set to DS.Model right now.

* public/v2/app.css: Renamed .test-groups to .analysis-group. Also added new rules for the table
containing the bug information.

* public/v2/app.js:
(App.InteractiveChartComponent._rangesChanged): Added label to range bar objects.
(App.AnalysisTaskRoute):
(App.AnalysisTaskController): Replaced the functionality of App.AnalysisTaskViewModel.
(App.AnalysisTaskController._fetchedManifest): Added.
(App.AnalysisTaskController.actions.associateBug): Added.

* public/v2/chart-pane.css: Renamed .bugs-pane to .analysis-pane.

* public/v2/data.js:
(Measurement.prototype.associateBug): Deleted.

* public/v2/index.html: Renamed .bugs-pane to .analysis-pane and .test-groups to .analysis-group.
Added a table show the bug information. Also hide the chart until chartData is available.

* public/v2/manifest.js:
(App.Model): Added.

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

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/include/json-header.php
Websites/perf.webkit.org/public/privileged-api/associate-bug.php
Websites/perf.webkit.org/public/v2/analysis.js
Websites/perf.webkit.org/public/v2/app.css
Websites/perf.webkit.org/public/v2/app.js
Websites/perf.webkit.org/public/v2/chart-pane.css
Websites/perf.webkit.org/public/v2/data.js
Websites/perf.webkit.org/public/v2/index.html
Websites/perf.webkit.org/public/v2/manifest.js

index 3c988e8..ef47817 100644 (file)
@@ -1,3 +1,48 @@
+2014-11-21  Ryosuke Niwa  <rniwa@webkit.org>
+
+        There should be a way to associate bugs with analysis tasks
+        https://bugs.webkit.org/show_bug.cgi?id=138977
+
+        Reviewed by Benjamin Poulain.
+
+        Updated associate-bug.php to match the new database schema.
+
+        * public/include/json-header.php:
+        (require_format): Removed the call to camel_case_words_separated_by_underscore since the name is
+        already camel-cased in require_existence_of. This makes the function usable elsewhere.
+
+        * public/privileged-api/associate-bug.php:
+        (main): Changed the API to take run, bugTracker, and number to match the new database schema.
+        Also verify that those values are integers using require_format.
+
+        * public/v2/analysis.js:
+        (App.AnalysisTask.label): Added. Concatenates the task's name with the bug numbers.
+        (App.Bug.label): Added.
+        (App.BugAdapter): Added.
+        (App.BugAdapter.createRecord): Use PrivilegedAPI instead of the builtin ajax call.
+        (App.BuildRequest): Inherit from newly added App.Model, which is set to DS.Model right now.
+
+        * public/v2/app.css: Renamed .test-groups to .analysis-group. Also added new rules for the table
+        containing the bug information.
+
+        * public/v2/app.js:
+        (App.InteractiveChartComponent._rangesChanged): Added label to range bar objects.
+        (App.AnalysisTaskRoute):
+        (App.AnalysisTaskController): Replaced the functionality of App.AnalysisTaskViewModel.
+        (App.AnalysisTaskController._fetchedManifest): Added.
+        (App.AnalysisTaskController.actions.associateBug): Added.
+
+        * public/v2/chart-pane.css: Renamed .bugs-pane to .analysis-pane.
+
+        * public/v2/data.js:
+        (Measurement.prototype.associateBug): Deleted.
+
+        * public/v2/index.html: Renamed .bugs-pane to .analysis-pane and .test-groups to .analysis-group.
+        Added a table show the bug information. Also hide the chart until chartData is available.
+
+        * public/v2/manifest.js:
+        (App.Model): Added.
+
 2014-11-20  Ryosuke Niwa  <rniwa@webkit.org>
 
         Fix misc bugs and typos in app.js
index 8da4a76..0fef7a1 100644 (file)
@@ -53,9 +53,9 @@ function camel_case_words_separated_by_underscore($name) {
     return implode('', array_map('ucfirst', explode('_', $name)));
 }
 
-function require_format($key, $value, $pattern) {
+function require_format($name, $value, $pattern) {
     if (!preg_match($pattern, $value))
-        exit_with_error('Invalid' . camel_case_words_separated_by_underscore($key), array('value' => $value));
+        exit_with_error('Invalid' . $name, array('value' => $value));
 }
 
 function require_existence_of($array, $list_of_arguments, $prefix = '') {
index 4eda7f7..d4bd260 100644 (file)
@@ -5,29 +5,28 @@ 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');
+    $analysis_task_id = array_get($data, 'task');
+    $bug_tracker_id = array_get($data, 'bugTracker');
+    $bug_number = array_get($data, 'number');
 
-    if (!$run_id)
-        exit_with_error('InvalidRunId', array('run' => $run_id));
-    if (!$bug_tracker_id)
-        exit_with_error('InvalidBugTrackerId', array('tracker' => $bug_tracker_id));
+    require_format('AnalysisTask', $analysis_task_id, '/^\d+$/');
+    require_format('BugTracker', $bug_tracker_id, '/^\d+$/');
+    require_format('BugNumber', $bug_number, '/^\d*$/');
 
     $db = connect();
     $db->begin_transaction();
 
     $bug_id = NULL;
     if (!$bug_number) {
-        $count = $db->query_and_get_affected_rows("DELETE FROM bugs WHERE bug_run = $1 AND bug_tracker = $2",
-            array($run_id, $bug_tracker_id));
+        $count = $db->query_and_get_affected_rows("DELETE FROM bugs WHERE bug_task = $1 AND bug_tracker = $2",
+            array($analysis_task_id, $bug_tracker_id));
         if ($count > 1) {
             $db->rollback_transaction();
             exit_with_error('UnexpectedNumberOfAffectedRows', array('affectedRows' => $count));
         }
     } else {
-        $bug_id = $db->update_or_insert_row('bugs', 'bug', array('run' => $run_id, 'tracker' => $bug_tracker_id),
-            array('run' => $run_id, 'tracker' => $bug_tracker_id, 'number' => $bug_number));
+        $bug_id = $db->update_or_insert_row('bugs', 'bug', array('task' => $analysis_task_id, 'tracker' => $bug_tracker_id),
+            array('task' => $analysis_task_id, 'tracker' => $bug_tracker_id, 'number' => $bug_number));
     }
     $db->commit_transaction();
 
index 08190b5..eb90ea0 100644 (file)
@@ -9,13 +9,21 @@ App.AnalysisTask = App.NameLabelModel.extend({
     testGroups: function () {
         return this.store.find('testGroup', {task: this.get('id')});
     }.property(),
+    label: function () {
+        var label = this.get('name');
+        var bugs = this.get('bugs').map(function (bug) { return bug.get('label'); }).join(' / ');
+        return bugs ? label + ' (' + bugs + ')' : label;
+    }.property('name', 'bugs'),
 });
 
-App.Bug = App.NameLabelModel.extend({
+App.Bug = App.Model.extend({
     task: DS.belongsTo('AnalysisTask'),
     bugTracker: DS.belongsTo('BugTracker'),
     createdAt: DS.attr('date'),
     number: DS.attr('number'),
+    label: function () {
+        return this.get('bugTracker').get('label') + ': ' + this.get('number');
+    }.property('name', 'bugTracker'),
 });
 
 // FIXME: Use DS.RESTAdapter instead.
@@ -35,6 +43,21 @@ App.AnalysisTaskAdapter = DS.RESTAdapter.extend({
     },
 });
 
+App.BugAdapter = DS.RESTAdapter.extend({
+    createRecord: function (store, type, record)
+    {
+        var param = {
+            task: record.get('task').get('id'),
+            bugTracker: record.get('bugTracker').get('id'),
+            number: record.get('number'),
+        };
+        return PrivilegedAPI.sendRequest('associate-bug', param).then(function (data) {
+            param['id'] = data['bugId'];
+            return {'bug': param};
+        });
+    }
+});
+
 App.TestGroup = App.NameLabelModel.extend({
     analysisTask: DS.belongsTo('analysisTask'),
     author: DS.attr('string'),
@@ -57,7 +80,7 @@ App.AnalysisTaskSerializer = App.TestGroupSerializer = DS.RESTSerializer.extend(
     }
 });
 
-App.BuildRequest = DS.Model.extend({
+App.BuildRequest = App.Model.extend({
     group: DS.belongsTo('testGroup'),
     order: DS.attr('number'),
     rootSet: DS.attr('number'),
index 7a4b780..45fa890 100755 (executable)
@@ -413,32 +413,32 @@ table.dashboard tbody td .progress {
 }
 
 #analysis-tasks,
-.test-groups > table {
+.analysis-group > table {
     border: solid 0px #999;
     border-collapse: collapse;
 }
 
 #analysis-tasks thead,
-.test-groups > table thead {
+.analysis-group > table thead {
     color: #c93;
 }
 
 #analysis-tasks th,
-.test-groups > table th {
+.analysis-group > table th {
     font-weight: normal;
 }
 
 #analysis-tasks td,
 #analysis-tasks th,
-.test-groups > table td,
-.test-groups > table th {
+.analysis-group > table td,
+.analysis-group > table th {
     padding: 0.2rem 0.5rem;
 }
 
 #analysis-tasks tbody td,
 #analysis-tasks tbody th,
-.test-groups > table tbody td,
-.test-groups > table tbody th {
+.analysis-group > table tbody td,
+.analysis-group > table tbody th {
     border-top: solid 1px #ddd;
 }
 
@@ -457,7 +457,7 @@ table.dashboard tbody td .progress {
     color: #333;
 }
 
-.test-groups {
+.analysis-group {
     border: 1px solid #bbb;
     border-radius: 0.5rem;
     box-shadow: rgba(0, 0, 0, 0.03) 1px 1px 0px 0px;
@@ -466,8 +466,13 @@ table.dashboard tbody td .progress {
     margin-bottom: 1.5rem;
 }
 
-.test-groups caption {
+.analysis-group caption {
     font-size: 1.1rem;
     text-align: left;
     margin-bottom: 0.5rem;
 }
+
+.analysis-bugs th {
+    font-weight: normal;
+    text-align: right;
+}
index bec50a1..afb5869 100755 (executable)
@@ -1470,6 +1470,7 @@ App.InteractiveChartComponent = Ember.Component.extend({
                 bottom: null,
                 linkRoute: linkRoute,
                 linkId: range.get('id'),
+                label: range.get('label'),
             });
         }));
 
@@ -1653,22 +1654,46 @@ App.AnalysisRoute = Ember.Route.extend({
 });
 
 App.AnalysisTaskRoute = Ember.Route.extend({
-    model: function (param) {
-        return this.store.find('analysisTask', param.taskId).then(function (task) {
-            return App.AnalysisTaskViewModel.create({content: task, store: store});
-        });
+    model: function (param)
+    {
+        return this.store.find('analysisTask', param.taskId);
     },
 });
 
-App.AnalysisTaskViewModel = Ember.ObjectProxy.extend({
+App.AnalysisTaskController = Ember.Controller.extend({
+    label: Ember.computed.alias('model.name'),
+    platform: Ember.computed.alias('model.platform'),
+    metric: Ember.computed.alias('model.metric'),
     testSets: [],
     roots: [],
+    bugTrackers: [],
     _taskUpdated: function ()
     {
-        var platformId = this.get('platform').get('id');
-        var metricId = this.get('metric').get('id');
-        App.Manifest.fetchRunsWithPlatformAndMetric(this.get('store'), platformId, metricId).then(this._fetchedRuns.bind(this));
-    }.observes('platform', 'metric').on('init'),
+        var model = this.get('model');
+        if (!model)
+            return;
+
+        var platformId = model.get('platform').get('id');
+        var metricId = model.get('metric').get('id');
+        App.Manifest.fetch(this.store).then(this._fetchedManifest.bind(this));
+        App.Manifest.fetchRunsWithPlatformAndMetric(this.store, platformId, metricId).then(this._fetchedRuns.bind(this));
+    }.observes('model').on('init'),
+    _fetchedManifest: function ()
+    {
+        var trackerIdToBugNumber = {};
+        this.get('model').get('bugs').forEach(function (bug) {
+            trackerIdToBugNumber[bug.get('bugTracker').get('id')] = bug.get('number');
+        });
+
+        this.set('bugTrackers', App.Manifest.get('bugTrackers').map(function (bugTracker) {
+            var bugNumber = trackerIdToBugNumber[bugTracker.get('id')];
+            return Ember.ObjectProxy.create({
+                content: bugTracker,
+                bugNumber: bugNumber,
+                editedBugNumber: bugNumber,
+            });
+        }));
+    },
     _fetchedRuns: function (data) {
         var runs = data.runs;
 
@@ -1676,8 +1701,8 @@ App.AnalysisTaskViewModel = Ember.ObjectProxy.extend({
         if (!currentTimeSeries)
             return; // FIXME: Report an error.
 
-        var start = currentTimeSeries.findPointByMeasurementId(this.get('startRun'));
-        var end = currentTimeSeries.findPointByMeasurementId(this.get('endRun'));
+        var start = currentTimeSeries.findPointByMeasurementId(this.get('model').get('startRun'));
+        var end = currentTimeSeries.findPointByMeasurementId(this.get('model').get('endRun'));
         if (!start || !end)
             return; // FIXME: Report an error.
 
@@ -1768,4 +1793,16 @@ App.AnalysisTaskViewModel = Ember.ObjectProxy.extend({
         }
         return roots;
     }.property('analysisPoints'),
+    actions: {
+        associateBug: function (bugTracker, bugNumber)
+        {
+            var model = this.get('model');
+            this.store.createRecord('bug',
+                {task: this.get('model'), bugTracker: bugTracker.get('content'), number: bugNumber}).save().then(function () {
+                    // FIXME: Should we notify the user?
+                }, function (error) {
+                    alert('Failed to associate the bug: ' + error);
+                });
+        }
+    },
 });
index f5dc9ed..1a902ad 100755 (executable)
@@ -64,7 +64,7 @@
     top: 0.55rem;
 }
 
-.search-pane, .bugs-pane {
+.search-pane, .analysis-pane {
     position: absolute;
     top: 1.7rem;
     border: 1px solid #bbb;
     background: white;
 }
 
-.bugs-pane {
+.analysis-pane {
     right: 1.3rem;
 }
 
-.bugs-pane table {
+.analysis-pane table {
     margin: 0.2rem;
     font-size: 0.8rem;
 }
 
-.bugs-pane th {
+.analysis-pane th {
     font-weight: normal;
 }
 
@@ -91,7 +91,7 @@
     right: 0rem;
 }
 
-.bugs-pane.hidden,
+.analysis-pane.hidden,
 .search-pane.hidden {
     display: none;
 }
index 059de42..f337c62 100755 (executable)
@@ -278,23 +278,6 @@ Measurement.prototype.hasBugs = function ()
     return bugs && 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];
-    });
-}
-
 function RunsData(rawData)
 {
     this._measurements = rawData.map(function (run) { return new Measurement(run); });
index 039c76f..a52b635 100755 (executable)
                     {{input action="searchCommit" placeholder="Name or email" value=commitSearchKeyword}}
                 </form>
 
-                <div {{bind-attr class=":bugs-pane showingAnalysisPane::hidden"}}>
+                <div {{bind-attr class=":analysis-pane showingAnalysisPane::hidden"}}>
                     <table>
                         <tbody>
                             <tr>
         {{/if}}
         <div class="rangeBarsContainerInlineStyle">
             {{#each rangeBars}}
-                {{#link-to linkRoute linkId}}
+                {{#link-to linkRoute linkId title=label}}
                     <span class="rangeBar" {{bind-attr style=inlineStyle}}></span>
                 {{/link-to}}
             {{/each}}
             {{partial "navbar"}}
         </header>
 
-        <h2 id="analysis-task-title">{{name}}</h2>
+        <h2 id="analysis-task-title">{{label}}</h2>
         {{#if platform.label}}
             <h3 id="analysis-task-testname">{{metric.fullName}} - {{platform.label}}</h3>
+        {{/if}}
 
+        {{#if chartData}}
             <section class="analysis-chart-pane chart-pane">
                 <div class="svg-container">
                     {{interactive-chart
                         markedPoints=markedPoints}}
                 </div>
                 <div class="details">
+                    <table class="analysis-bugs">
+                        <tbody>
+                            {{#each bugTrackers}}
+                                <tr>
+                                    <th>{{label}}</th>
+                                    <td>
+                                        <form {{action "associateBug" this editedBugNumber on="submit"}}>
+                                            {{input type=text value=editedBugNumber}}
+                                        </form>
+                                    </td>
+                                </tr>
+                            {{/each}}
+                        </tbody>
+                    </table>
                     <table>
                         <tbody>
                             {{#each analysisPoints}}
             </section>
 
             {{#each testGroups}}
-                <section class="test-groups">
+                <section class="analysis-group">
                     <table>
                         <caption>{{name}}</caption>
                         <thead>
                 </section>
             {{/each}}
 
-            <form class="test-groups">
+            <form class="analysis-group">
                 <table>
                     <caption><input name="name" placeholder="Test group name" required></caption>
                     <thead>
index 7cca009..0bae01b 100755 (executable)
@@ -1,3 +1,5 @@
+App.Model = DS.Model;
+
 App.NameLabelModel = DS.Model.extend({
     name: DS.attr('string'),
     label: function ()