<!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>[196521] 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/196521">196521</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2016-02-12 15:33:18 -0800 (Fri, 12 Feb 2016)</dd>
</dl>

<h3>Log Message</h3>
<pre>Perf dashboard should allow renaming analysis tasks and test groups
https://bugs.webkit.org/show_bug.cgi?id=154200

Reviewed by Chris Dumez.

Allow editing names of analysis tasks and A/B testing groups in the v3 UI.

Added the support for updating the name to the privileged API at /privileged-api/update-analysis-task
and added a new prevailed API to update A/B testing groups at /privileged-api/update-test-group.

* public/privileged-api/update-analysis-task.php: Added the support for renaming the analysis task.
(main):

* public/privileged-api/update-test-group.php: Added. Supports updating the test group's name.
(main):

* public/v3/components/editable-text.js: Added.
(EditableText): Added. A new editable text label control. It looks like a text node with &quot;(Edit)&quot; link
at the end which allow users to go into the &quot;editing mode&quot;, which reveals an input element.
The user can exit the editing mode by either moving the focus away from the control or clicking on
&quot;(Save)&quot; at the end. It calls _updateCallback in the latter case.
(EditableText.prototype.editedText): Returns the current value of the input element user.
(EditableText.prototype.setText): Sets the label. This does not live-update the input element until
the user exists the current editing mode and re-enters it.
(EditableText.prototype.setStartedEditingCallback): Sets a callback which gets called when the user
requested to enter the editing mode. Since EditableText relies on AnalysisTaskPage to render, this
callback only exits to call EditableText.render() in AnalysisTask._didStartEditingTaskName.
(EditableText.prototype.setUpdateCallback): Sets a callback which gets called when the user exits
the editing mode by activating the &quot;(Save)&quot; link. This callback MUST return a promise upon resolution
of which the control gets out of the editing mode. While the promise is in flight, the input element
becomes readonly.
(EditableText.prototype.render): Updates various states of the elements. When _updatingPromise is not
falsy, we make the input element readonly and show '(...)' on the link. Don't show the action link
if the label is empty (e.g. analysis task or test group is still being fetched).
(EditableText.prototype._didClick): Called when the user clicked on the action link. Enter the editing
mode or save the edited label via _updateCallback.
(EditableText.prototype._didBlur): Exit the editing mode without saving if the input element is not
focused, there is no inflight promise returned by _updateCallback, and the action link &quot;(Save)&quot; does
not have the focus.
(EditableText.prototype._didUpdate): Called when exiting the editing mode.
(EditableText.htmlTemplate):
(EditableText.cssTemplate):

* public/v3/index.html: Include newly added editable-text.js.

* public/v3/models/analysis-task.js:
(AnalysisTask.prototype.updateSingleton): Added.
(AnalysisTask.prototype.updateName): Added. Uses PrivilegedAPI to update the name and re-fetches
the analysis task from the sever.
(AnalysisTask._constructAnalysisTasksFromRawData): Use ensureSingleton instead of manually calling
findById since we need to update the name of the singleton object we found (via updateSingleton).

* public/v3/models/bug.js:
(Bug.ensureSingleton): Moved the code to compute the synthetic id from AnalysisTask's
_constructAnalysisTasksFromRawData.
(Bug.prototype.updateSingleton): Added. Just assert that nothing changes.

* public/v3/models/build-request.js:
(BuildRequest.prototype.updateSingleton): Added. Assert that the intrinsic values of a build request
doesn't change and update status text, status url, and build id as they could change.

* public/v3/models/commit-log.js:
(CommitLog): Made the constructor argument conform to the convention of id, object pair so that we can
use DataModelObject.ensureSingleton.
(CommitLog.ensureSingleton): 
(CommitLog.prototype.updateSingleton): Extracted from CommitLog.ensureSingleton.

* public/v3/models/data-model.js:
(DataModelObject.ensureSingleton): Call newly added updateSingleton.
(DataModelObject.prototype.updateSingleton):
(LabeledObject): Removed the name map since it's never used (findByName is never called anywhere).
(LabeledObject.prototype.updateSingleton): Added. Updates _name.
(LabeledObject.findByName): Deleted.

* public/v3/models/test-group.js:
(TestGroup.prototype.updateName): Added. Uses PrivilegedAPI to update the name and re-fetches
the test group from the sever.
(TestGroup._createModelsFromFetchedTestGroups): Removed bogus code. A root set doesn't have a test
group associated with it since multiple test groups can share a single root set (this property doesn't
even exist).

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskPage): Removed useless _taskId and added this._testGroupLabelMap and this._taskNameLabel.
(AnalysisTaskPage.prototype.updateFromSerializedState): Cleanup.
(AnalysisTaskPage.prototype._didFetchTask): Assert that this function is called exactly once.
(AnalysisTaskPage.prototype.render): Use this._task.id() to show the v2 link. Use EditableText to show
the names of the analysis task and the associated test groups. Hide the overview chart and the list of
test groups (along with the retry/confirm button) when the analysis task failed to fetch. We always
update the names of the analysis task and the associated test groups since they could be updated by
the server.
(AnalysisTaskPage.prototype._didStartEditingTaskName): Added.
(AnalysisTaskPage.prototype._updateTaskName): Added.
(AnalysisTaskPage.prototype._updateTestGroupName): Added.
(AnalysisTaskPage.htmlTemplate): Updated the style.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicprivilegedapiupdateanalysistaskphp">trunk/Websites/perf.webkit.org/public/privileged-api/update-analysis-task.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3indexhtml">trunk/Websites/perf.webkit.org/public/v3/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsanalysistaskjs">trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsbugjs">trunk/Websites/perf.webkit.org/public/v3/models/bug.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsbuildrequestjs">trunk/Websites/perf.webkit.org/public/v3/models/build-request.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelscommitlogjs">trunk/Websites/perf.webkit.org/public/v3/models/commit-log.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsdatamodeljs">trunk/Websites/perf.webkit.org/public/v3/models/data-model.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelstestgroupjs">trunk/Websites/perf.webkit.org/public/v3/models/test-group.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs">trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicprivilegedapiupdatetestgroupphp">trunk/Websites/perf.webkit.org/public/privileged-api/update-test-group.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentseditabletextjs">trunk/Websites/perf.webkit.org/public/v3/components/editable-text.js</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 (196520 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2016-02-12 23:25:02 UTC (rev 196520)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -1,3 +1,100 @@
</span><ins>+2016-02-12  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Perf dashboard should allow renaming analysis tasks and test groups
+        https://bugs.webkit.org/show_bug.cgi?id=154200
+
+        Reviewed by Chris Dumez.
+
+        Allow editing names of analysis tasks and A/B testing groups in the v3 UI.
+
+        Added the support for updating the name to the privileged API at /privileged-api/update-analysis-task
+        and added a new prevailed API to update A/B testing groups at /privileged-api/update-test-group.
+
+        * public/privileged-api/update-analysis-task.php: Added the support for renaming the analysis task.
+        (main):
+
+        * public/privileged-api/update-test-group.php: Added. Supports updating the test group's name.
+        (main):
+
+        * public/v3/components/editable-text.js: Added.
+        (EditableText): Added. A new editable text label control. It looks like a text node with &quot;(Edit)&quot; link
+        at the end which allow users to go into the &quot;editing mode&quot;, which reveals an input element.
+        The user can exit the editing mode by either moving the focus away from the control or clicking on
+        &quot;(Save)&quot; at the end. It calls _updateCallback in the latter case.
+        (EditableText.prototype.editedText): Returns the current value of the input element user.
+        (EditableText.prototype.setText): Sets the label. This does not live-update the input element until
+        the user exists the current editing mode and re-enters it.
+        (EditableText.prototype.setStartedEditingCallback): Sets a callback which gets called when the user
+        requested to enter the editing mode. Since EditableText relies on AnalysisTaskPage to render, this
+        callback only exits to call EditableText.render() in AnalysisTask._didStartEditingTaskName.
+        (EditableText.prototype.setUpdateCallback): Sets a callback which gets called when the user exits
+        the editing mode by activating the &quot;(Save)&quot; link. This callback MUST return a promise upon resolution
+        of which the control gets out of the editing mode. While the promise is in flight, the input element
+        becomes readonly.
+        (EditableText.prototype.render): Updates various states of the elements. When _updatingPromise is not
+        falsy, we make the input element readonly and show '(...)' on the link. Don't show the action link
+        if the label is empty (e.g. analysis task or test group is still being fetched).
+        (EditableText.prototype._didClick): Called when the user clicked on the action link. Enter the editing
+        mode or save the edited label via _updateCallback.
+        (EditableText.prototype._didBlur): Exit the editing mode without saving if the input element is not
+        focused, there is no inflight promise returned by _updateCallback, and the action link &quot;(Save)&quot; does
+        not have the focus.
+        (EditableText.prototype._didUpdate): Called when exiting the editing mode.
+        (EditableText.htmlTemplate):
+        (EditableText.cssTemplate):
+
+        * public/v3/index.html: Include newly added editable-text.js.
+
+        * public/v3/models/analysis-task.js:
+        (AnalysisTask.prototype.updateSingleton): Added.
+        (AnalysisTask.prototype.updateName): Added. Uses PrivilegedAPI to update the name and re-fetches
+        the analysis task from the sever.
+        (AnalysisTask._constructAnalysisTasksFromRawData): Use ensureSingleton instead of manually calling
+        findById since we need to update the name of the singleton object we found (via updateSingleton).
+
+        * public/v3/models/bug.js:
+        (Bug.ensureSingleton): Moved the code to compute the synthetic id from AnalysisTask's
+        _constructAnalysisTasksFromRawData.
+        (Bug.prototype.updateSingleton): Added. Just assert that nothing changes.
+
+        * public/v3/models/build-request.js:
+        (BuildRequest.prototype.updateSingleton): Added. Assert that the intrinsic values of a build request
+        doesn't change and update status text, status url, and build id as they could change.
+
+        * public/v3/models/commit-log.js:
+        (CommitLog): Made the constructor argument conform to the convention of id, object pair so that we can
+        use DataModelObject.ensureSingleton.
+        (CommitLog.ensureSingleton): 
+        (CommitLog.prototype.updateSingleton): Extracted from CommitLog.ensureSingleton.
+
+        * public/v3/models/data-model.js:
+        (DataModelObject.ensureSingleton): Call newly added updateSingleton.
+        (DataModelObject.prototype.updateSingleton):
+        (LabeledObject): Removed the name map since it's never used (findByName is never called anywhere).
+        (LabeledObject.prototype.updateSingleton): Added. Updates _name.
+        (LabeledObject.findByName): Deleted.
+
+        * public/v3/models/test-group.js:
+        (TestGroup.prototype.updateName): Added. Uses PrivilegedAPI to update the name and re-fetches
+        the test group from the sever.
+        (TestGroup._createModelsFromFetchedTestGroups): Removed bogus code. A root set doesn't have a test
+        group associated with it since multiple test groups can share a single root set (this property doesn't
+        even exist).
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskPage): Removed useless _taskId and added this._testGroupLabelMap and this._taskNameLabel.
+        (AnalysisTaskPage.prototype.updateFromSerializedState): Cleanup.
+        (AnalysisTaskPage.prototype._didFetchTask): Assert that this function is called exactly once.
+        (AnalysisTaskPage.prototype.render): Use this._task.id() to show the v2 link. Use EditableText to show
+        the names of the analysis task and the associated test groups. Hide the overview chart and the list of
+        test groups (along with the retry/confirm button) when the analysis task failed to fetch. We always
+        update the names of the analysis task and the associated test groups since they could be updated by
+        the server.
+        (AnalysisTaskPage.prototype._didStartEditingTaskName): Added.
+        (AnalysisTaskPage.prototype._updateTaskName): Added.
+        (AnalysisTaskPage.prototype._updateTestGroupName): Added.
+        (AnalysisTaskPage.htmlTemplate): Updated the style.
+
</ins><span class="cx"> 2016-02-11  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Land the change that was supposed to be the part of r196463.
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicprivilegedapiupdateanalysistaskphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/privileged-api/update-analysis-task.php (196520 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/privileged-api/update-analysis-task.php        2016-02-12 23:25:02 UTC (rev 196520)
+++ trunk/Websites/perf.webkit.org/public/privileged-api/update-analysis-task.php        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -11,6 +11,9 @@
</span><span class="cx"> 
</span><span class="cx">     $values = array();
</span><span class="cx"> 
</span><ins>+    if (array_key_exists('name', $data))
+        $values['name'] = $data['name'];
+
</ins><span class="cx">     if (array_key_exists('result', $data)) {
</span><span class="cx">         require_match_one_of_values('Result', $data['result'], array(null, 'progression', 'regression', 'unchanged', 'inconclusive'));
</span><span class="cx">         $values['result'] = $data['result'];
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicprivilegedapiupdatetestgroupphp"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/privileged-api/update-test-group.php (0 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/privileged-api/update-test-group.php                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/privileged-api/update-test-group.php        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -0,0 +1,35 @@
</span><ins>+&lt;?php
+
+require_once('../include/json-header.php');
+
+function main() {
+    $data = ensure_privileged_api_data_and_token();
+
+    $test_group_id = array_get($data, 'group');
+    if (!$test_group_id)
+        exit_with_error('TestGroupNotSpecified');
+
+    $values = array();
+
+    if (array_key_exists('name', $data))
+        $values['name'] = $data['name'];
+
+    if (!$values)
+        exit_with_error('NothingToUpdate');
+
+    $db = connect();
+    $db-&gt;begin_transaction();
+
+    if (!$db-&gt;update_row('analysis_test_groups', 'testgroup', array('id' =&gt; $test_group_id), $values)) {
+        $db-&gt;rollback_transaction();
+        exit_with_error('FailedToUpdateTestGroup', array('id' =&gt; $test_group_id, 'values' =&gt; $values));
+    }
+
+    $db-&gt;commit_transaction();
+
+    exit_with_success();
+}
+
+main();
+
+?&gt;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentseditabletextjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v3/components/editable-text.js (0 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/editable-text.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/components/editable-text.js        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -0,0 +1,123 @@
</span><ins>+
+class EditableText extends ComponentBase {
+
+    constructor(text)
+    {
+        super('editable-text');
+        this._text = text;
+        this._inEditingMode = false;
+        this._startedEditingCallback = null;
+        this._updateCallback = null;
+        this._updatingPromise = null;
+        this._actionLink = this.content().querySelector('.editable-text-action a');
+        this._actionLink.onclick = this._didClick.bind(this);
+        this._actionLink.onmousedown = this._didClick.bind(this);
+        this._textField = this.content().querySelector('.editable-text-field');
+        this._textField.onblur = this._didBlur.bind(this);
+        this._label = this.content().querySelector('.editable-text-label');
+    }
+
+    editedText() { return this._textField.value; }
+    setText(text) { this._text = text; }
+
+    setStartedEditingCallback(callback) { this._startedEditingCallback = callback; }
+    setUpdateCallback(callback) { this._updateCallback = callback; }
+
+    render()
+    {
+        this._label.textContent = this._text;
+        this._actionLink.textContent = this._inEditingMode ? (this._updatingPromise ? '...' : 'Save') : 'Edit';
+        this._actionLink.parentNode.style.display = this._text ? null : 'none';
+
+        if (this._inEditingMode) {
+            this._textField.readOnly = !!this._updatingPromise;
+            this._textField.style.display = null;
+            this._label.style.display = 'none';
+            if (!this._updatingPromise)
+                this._textField.focus();
+        } else {
+            this._textField.style.display = 'none';
+            this._label.style.display = null;
+        }
+
+        super.render();
+    }
+
+    _didClick(event)
+    {
+        event.preventDefault();
+        event.stopPropagation();
+
+        if (!this._updateCallback || this._updatingPromise)
+            return;
+
+        if (this._inEditingMode)
+            this._updatingPromise = this._updateCallback().then(this._didUpdate.bind(this));
+        else {
+            this._inEditingMode = true;
+            this._textField.value = this._text;
+            this._textField.style.width = (this._text.length / 1.5) + 'rem';
+            if (this._startedEditingCallback)
+                this._startedEditingCallback();
+        }
+    }
+
+    _didBlur(event)
+    {
+        var self = this;
+        if (self._inEditingMode &amp;&amp; !self._updatingPromise &amp;&amp; !self.hasFocus())
+            self._didUpdate();
+    }
+
+    _didUpdate()
+    {
+        this._inEditingMode = false;
+        this._updatingPromise = null;
+        this.render();
+    }
+
+    static htmlTemplate()
+    {
+        return `
+            &lt;span class=&quot;editable-text-container&quot;&gt;
+                &lt;input type=&quot;text&quot; class=&quot;editable-text-field&quot;&gt;
+                &lt;span class=&quot;editable-text-label&quot;&gt;&lt;/span&gt;
+                &lt;span class=&quot;editable-text-action&quot;&gt;(&lt;a href=&quot;#&quot;&gt;Edit&lt;/a&gt;)&lt;/span&gt;
+            &lt;/span&gt;`;
+    }
+
+    static cssTemplate()
+    {
+        return `
+            .editable-text-container {
+                position: relative;
+                padding-right: 2.5rem;
+            }
+            .editable-text-field {
+                background: transparent;
+                margin: 0;
+                padding: 0;
+                color: inherit;
+                font-weight: inherit;
+                font-size: inherit;
+                width: 8rem;
+                border: none;
+            }
+            .editable-text-action {
+                position: absolute;
+                padding-left: 0.2rem;
+                color: #999;
+                font-size: 0.8rem;
+                top: 50%;
+                margin-top: -0.4rem;
+                vertical-align: middle;
+            }
+            .editable-text-action a {
+                color: inherit;
+                text-decoration: none;
+            }
+        `;
+    }
+}
+
+ComponentBase.defineElement('editable-text', EditableText);
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3indexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (196520 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/index.html        2016-02-12 23:25:02 UTC (rev 196520)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -66,6 +66,7 @@
</span><span class="cx">         &lt;script src=&quot;components/button-base.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/close-button.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/commit-log-viewer.js&quot;&gt;&lt;/script&gt;
</span><ins>+        &lt;script src=&quot;components/editable-text.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx">         &lt;script src=&quot;components/time-series-chart.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/interactive-time-series-chart.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/chart-status-view.js&quot;&gt;&lt;/script&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsanalysistaskjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js (196520 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js        2016-02-12 23:25:02 UTC (rev 196520)
+++ trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -29,6 +29,27 @@
</span><span class="cx">         return this.all().filter(function (task) { return task._platform.id() == platformId &amp;&amp; task._metric.id() == metricId; });
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    updateSingleton(object)
+    {
+        super.updateSingleton(object);
+
+        console.assert(this._author == object.author);
+        console.assert(+this._createdAt == +object.createdAt);
+        console.assert(this._platform == object.platform);
+        console.assert(this._metric == object.metric);
+        console.assert(this._startMeasurementId == object.startRun);
+        console.assert(this._startTime == object.startRunTime);
+        console.assert(this._endMeasurementId == object.endRun);
+        console.assert(this._endTime == object.endRunTime);
+
+        this._category = object.category;
+        this._changeType = object.result;
+        this._needed = object.needed;
+        this._bugs = object.bugs || [];
+        this._buildRequestCount = object.buildRequestCount;
+        this._finishedBuildRequestCount = object.finishedBuildRequestCount;
+    }
+
</ins><span class="cx">     hasResults() { return this._finishedBuildRequestCount; }
</span><span class="cx">     hasPendingRequests() { return this._finishedBuildRequestCount &lt; this._buildRequestCount; }
</span><span class="cx">     requestLabel() { return `${this._finishedBuildRequestCount} of ${this._buildRequestCount}`; }
</span><span class="lines">@@ -46,6 +67,19 @@
</span><span class="cx">     category() { return this._category; }
</span><span class="cx">     changeType() { return this._changeType; }
</span><span class="cx"> 
</span><ins>+    updateName(newName)
+    {
+        var self = this;
+        var id = this.id();
+        return PrivilegedAPI.sendRequest('update-analysis-task', {
+            task: id,
+            name: newName,
+        }).then(function (data) {
+            return AnalysisTask.cachedFetch('../api/analysis-tasks', {id: id}, true)
+                .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
+        });
+    }
+
</ins><span class="cx">     static categories()
</span><span class="cx">     {
</span><span class="cx">         return [
</span><span class="lines">@@ -94,12 +128,11 @@
</span><span class="cx">         // FIXME: The backend shouldn't create a separate bug row per task for the same bug number.
</span><span class="cx">         var taskToBug = {};
</span><span class="cx">         for (var rawData of data.bugs) {
</span><del>-            var id = rawData.bugTracker + '-' + rawData.number;
</del><span class="cx">             rawData.bugTracker = BugTracker.findById(rawData.bugTracker);
</span><span class="cx">             if (!rawData.bugTracker)
</span><span class="cx">                 continue;
</span><span class="cx"> 
</span><del>-            var bug = Bug.findById(id) || new Bug(id, rawData);
</del><ins>+            var bug = Bug.ensureSingleton(rawData);
</ins><span class="cx">             if (!taskToBug[rawData.task])
</span><span class="cx">                 taskToBug[rawData.task] = [];
</span><span class="cx">             taskToBug[rawData.task].push(bug);
</span><span class="lines">@@ -113,8 +146,7 @@
</span><span class="cx">                 continue;
</span><span class="cx"> 
</span><span class="cx">             rawData.bugs = taskToBug[rawData.id];
</span><del>-            var task = AnalysisTask.findById(rawData.id) || new AnalysisTask(rawData.id, rawData);
-            results.push(task);
</del><ins>+            results.push(AnalysisTask.ensureSingleton(rawData.id, rawData));
</ins><span class="cx">         }
</span><span class="cx"> 
</span><span class="cx">         Instrumentation.endMeasuringTime('AnalysisTask', 'construction');
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsbugjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/bug.js (196520 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/bug.js        2016-02-12 23:25:02 UTC (rev 196520)
+++ trunk/Websites/perf.webkit.org/public/v3/models/bug.js        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -9,6 +9,20 @@
</span><span class="cx">         this._bugNumber = object.number;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    static ensureSingleton(object)
+    {
+        console.assert(object.bugTracker instanceof BugTracker);
+        var id = object.bugTracker.id() + '-' + object.number;
+        return super.ensureSingleton(id, object);
+    }
+
+    updateSingleton(object)
+    {
+        super.updateSingleton(object);
+        console.assert(this._bugTracker == object.bugTracker);
+        console.assert(this._bugNumber == object.number);
+    }
+
</ins><span class="cx">     bugTracker() { return this._bugTracker; }
</span><span class="cx">     bugNumber() { return this._bugNumber; }
</span><span class="cx">     url() { return this._bugTracker.bugUrl(this._bugNumber); }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsbuildrequestjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/build-request.js (196520 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/build-request.js        2016-02-12 23:25:02 UTC (rev 196520)
+++ trunk/Websites/perf.webkit.org/public/v3/models/build-request.js        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -16,6 +16,16 @@
</span><span class="cx">         this._result = null;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    updateSingleton(object)
+    {
+        console.assert(this._testGroup == object.testGroup);
+        console.assert(this._order == object.order);
+        console.assert(this._rootSet == object.rootSet);
+        this._status = object.status;
+        this._statusUrl = object.url;
+        this._buildId = object.build;
+    }
+
</ins><span class="cx">     testGroup() { return this._testGroup; }
</span><span class="cx">     order() { return this._order; }
</span><span class="cx">     rootSet() { return this._rootSet; }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelscommitlogjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/commit-log.js (196520 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/commit-log.js        2016-02-12 23:25:02 UTC (rev 196520)
+++ trunk/Websites/perf.webkit.org/public/v3/models/commit-log.js        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -1,26 +1,32 @@
</span><span class="cx"> 
</span><span class="cx"> class CommitLog extends DataModelObject {
</span><del>-    constructor(id, repository, rawData)
</del><ins>+    constructor(id, rawData)
</ins><span class="cx">     {
</span><span class="cx">         super(id);
</span><del>-        this._repository = repository;
</del><ins>+        this._repository = rawData.repository;
</ins><span class="cx">         this._rawData = rawData;
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     static ensureSingleton(repository, rawData)
</span><span class="cx">     {
</span><span class="cx">         var id = repository.id() + '-' + rawData['revision'];
</span><del>-        var singleton = this.findById(id);
-        if (singleton) {
-            if (rawData.authorName)
-                singleton._rawData.authorName = rawData.authorName;
-            if (rawData.message)
-                singleton._rawData.message = rawData.message;
-            return singleton;
-        }
-        return new CommitLog(id, repository, rawData);
</del><ins>+        rawData.repository = repository;
+        return super.ensureSingleton(id, rawData);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    updateSingleton(rawData)
+    {
+        super.updateSingleton(rawData);
+
+        console.assert(+this._rawData['time'] == +rawData['time']);
+        console.assert(this._rawData['revision'] == rawData['revision']);
+
+        if (rawData.authorName)
+            this._rawData.authorName = rawData.authorName;
+        if (rawData.message)
+            this._rawData.message = rawData.message;
+    }
+
</ins><span class="cx">     repository() { return this._repository; }
</span><span class="cx">     time() { return new Date(this._rawData['time']); }
</span><span class="cx">     author() { return this._rawData['authorName']; }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsdatamodeljs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/data-model.js (196520 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/data-model.js        2016-02-12 23:25:02 UTC (rev 196520)
+++ trunk/Websites/perf.webkit.org/public/v3/models/data-model.js        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -10,11 +10,15 @@
</span><span class="cx">     static ensureSingleton(id, object)
</span><span class="cx">     {
</span><span class="cx">         var singleton = this.findById(id);
</span><del>-        if (singleton)
</del><ins>+        if (singleton) {
+            singleton.updateSingleton(object)
</ins><span class="cx">             return singleton;
</span><ins>+        }
</ins><span class="cx">         return new (this)(id, object);
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    updateSingleton(object) { }
+
</ins><span class="cx">     static namedStaticMap(name)
</span><span class="cx">     {
</span><span class="cx">         var staticMap = this[DataModelObject.StaticMapSymbol];
</span><span class="lines">@@ -80,14 +84,9 @@
</span><span class="cx">     {
</span><span class="cx">         super(id);
</span><span class="cx">         this._name = object.name;
</span><del>-        this.ensureNamedStaticMap('name')[this._name] = this;
</del><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static findByName(name)
-    {
-        var nameMap = this.namedStaticMap('id');
-        return nameMap ? nameMap[name] : null;
-    }
</del><ins>+    updateSingleton(object) { this._name = object.name; }
</ins><span class="cx"> 
</span><span class="cx">     static sortByName(list)
</span><span class="cx">     {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelstestgroupjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/test-group.js (196520 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/test-group.js        2016-02-12 23:25:02 UTC (rev 196520)
+++ trunk/Websites/perf.webkit.org/public/v3/models/test-group.js        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -152,6 +152,19 @@
</span><span class="cx">         return values;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    updateName(newName)
+    {
+        var self = this;
+        var id = this.id();
+        return PrivilegedAPI.sendRequest('update-test-group', {
+            group: id,
+            name: newName,
+        }).then(function (data) {
+            return TestGroup.cachedFetch(`../api/test-groups/${id}`, {}, true)
+                .then(TestGroup._createModelsFromFetchedTestGroups.bind(TestGroup));
+        });
+    }
+
</ins><span class="cx">     static createAndRefetchTestGroups(task, name, repetitionCount, rootSets)
</span><span class="cx">     {
</span><span class="cx">         var self = this;
</span><span class="lines">@@ -183,7 +196,6 @@
</span><span class="cx"> 
</span><span class="cx">         var rootSets = data['rootSets'].map(function (row) {
</span><span class="cx">             row.roots = row.roots.map(function (rootId) { return rootIdMap[rootId]; });
</span><del>-            row.testGroup = RootSet.findById(row.testGroup);
</del><span class="cx">             return RootSet.ensureSingleton(row.id, row);
</span><span class="cx">         });
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js (196520 => 196521)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2016-02-12 23:25:02 UTC (rev 196520)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2016-02-12 23:33:18 UTC (rev 196521)
</span><span class="lines">@@ -16,10 +16,10 @@
</span><span class="cx">     constructor()
</span><span class="cx">     {
</span><span class="cx">         super('Analysis Task');
</span><del>-        this._taskId = null;
</del><span class="cx">         this._task = null;
</span><span class="cx">         this._testGroups = null;
</span><span class="cx">         this._renderedTestGroups = null;
</span><ins>+        this._testGroupLabelMap = new Map;
</ins><span class="cx">         this._renderedCurrentTestGroup = undefined;
</span><span class="cx">         this._analysisResults = null;
</span><span class="cx">         this._measurementSet = null;
</span><span class="lines">@@ -32,6 +32,9 @@
</span><span class="cx">         this._analysisResultsViewer = this.content().querySelector('analysis-results-viewer').component();
</span><span class="cx">         this._analysisResultsViewer.setTestGroupCallback(this._showTestGroup.bind(this));
</span><span class="cx">         this._testGroupResultsTable = this.content().querySelector('test-group-results-table').component();
</span><ins>+        this._taskNameLabel = this.content().querySelector('.analysis-task-name editable-text').component();
+        this._taskNameLabel.setStartedEditingCallback(this._didStartEditingTaskName.bind(this));
+        this._taskNameLabel.setUpdateCallback(this._updateTaskName.bind(this));
</ins><span class="cx"> 
</span><span class="cx">         this.content().querySelector('.test-group-retry-form').onsubmit = this._retryCurrentTestGroup.bind(this);
</span><span class="cx">     }
</span><span class="lines">@@ -43,20 +46,18 @@
</span><span class="cx">     {
</span><span class="cx">         var self = this;
</span><span class="cx">         if (state.remainingRoute) {
</span><del>-            this._taskId = parseInt(state.remainingRoute);
-            AnalysisTask.fetchById(this._taskId).then(this._didFetchTask.bind(this), function (error) {
</del><ins>+            var taskId = parseInt(state.remainingRoute);
+            AnalysisTask.fetchById(taskId).then(this._didFetchTask.bind(this), function (error) {
</ins><span class="cx">                 self._errorMessage = `Failed to fetch the analysis task ${state.remainingRoute}: ${error}`;
</span><span class="cx">                 self.render();
</span><span class="cx">             });
</span><del>-            TestGroup.fetchByTask(this._taskId).then(this._didFetchTestGroups.bind(this));
-            AnalysisResults.fetch(this._taskId).then(this._didFetchAnalysisResults.bind(this));
</del><ins>+            TestGroup.fetchByTask(taskId).then(this._didFetchTestGroups.bind(this));
+            AnalysisResults.fetch(taskId).then(this._didFetchAnalysisResults.bind(this));
</ins><span class="cx">         } else if (state.buildRequest) {
</span><span class="cx">             var buildRequestId = parseInt(state.buildRequest);
</span><del>-            AnalysisTask.fetchByBuildRequestId(buildRequestId).then(this._didFetchTask.bind(this)).then(function () {
-                if (self._task) {
-                    TestGroup.fetchByTask(self._task.id()).then(self._didFetchTestGroups.bind(self));
-                    AnalysisResults.fetch(self._task.id()).then(this._didFetchAnalysisResults.bind(this));
-                }
</del><ins>+            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));
</ins><span class="cx">             }, function (error) {
</span><span class="cx">                 self._errorMessage = `Failed to fetch the analysis task for the build request ${buildRequestId}: ${error}`;
</span><span class="cx">                 self.render();
</span><span class="lines">@@ -66,6 +67,8 @@
</span><span class="cx"> 
</span><span class="cx">     _didFetchTask(task)
</span><span class="cx">     {
</span><ins>+        console.assert(!this._task);
+
</ins><span class="cx">         this._task = task;
</span><span class="cx">         var platform = task.platform();
</span><span class="cx">         var metric = task.metric();
</span><span class="lines">@@ -85,6 +88,8 @@
</span><span class="cx">         this._chartPane.setMainDomain(domain[0], domain[1]);
</span><span class="cx"> 
</span><span class="cx">         this.render();
</span><ins>+
+        return task;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _didFetchMeasurement()
</span><span class="lines">@@ -146,20 +151,25 @@
</span><span class="cx"> 
</span><span class="cx">         this.content().querySelector('.error-message').textContent = this._errorMessage || '';
</span><span class="cx"> 
</span><del>-        var v2URL = `/v2/#/analysis/task/${this._taskId}`;
-        this.content().querySelector('.error-message').innerHTML +=
-            `&lt;p&gt;To schedule a custom A/B testing, use &lt;a href=&quot;${v2URL}&quot;&gt;v2 UI&lt;/a&gt;.&lt;/p&gt;`;
</del><ins>+        if (this._task) {
+            var v2URL = `/v2/#/analysis/task/${this._task.id()}`;
+            this.content().querySelector('.error-message').innerHTML =
+                `&lt;p&gt;To schedule a custom A/B testing, use &lt;a href=&quot;${v2URL}&quot;&gt;v2 UI&lt;/a&gt;.&lt;/p&gt;`;
+        }
</ins><span class="cx"> 
</span><del>-         this._chartPane.render();
</del><ins>+        this._chartPane.render();
</ins><span class="cx"> 
</span><span class="cx">         if (this._task) {
</span><del>-            this.renderReplace(this.content().querySelector('.analysis-task-name'), this._task.name());
</del><ins>+            this._taskNameLabel.setText(this._task.name());
</ins><span class="cx">             var platform = this._task.platform();
</span><span class="cx">             var metric = this._task.metric();
</span><span class="cx">             var anchor = this.content().querySelector('.platform-metric-names a');
</span><span class="cx">             this.renderReplace(anchor, metric.fullName() + ' on ' + platform.label());
</span><span class="cx">             anchor.href = this.router().url('charts', ChartsPage.createStateForAnalysisTask(this._task));
</span><span class="cx">         }
</span><ins>+        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();
</ins><span class="cx"> 
</span><span class="cx">         this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
</span><span class="cx">         this._analysisResultsViewer.render();
</span><span class="lines">@@ -168,16 +178,34 @@
</span><span class="cx">         var link = ComponentBase.createLink;
</span><span class="cx">         if (this._testGroups != this._renderedTestGroups) {
</span><span class="cx">             this._renderedTestGroups = this._testGroups;
</span><ins>+            this._testGroupLabelMap.clear();
+
</ins><span class="cx">             var self = this;
</span><ins>+            var updateTestGroupName = this._updateTestGroupName.bind(this);
+            var showTestGroup = this._showTestGroup.bind(this);
+
</ins><span class="cx">             this.renderReplace(this.content().querySelector('.test-group-list'),
</span><span class="cx">                 this._testGroups.map(function (group) {
</span><del>-                    return element('li', {class: 'test-group-list-' + group.id()}, link(group.label(), function () {
-                        self._showTestGroup(group);
-                    }));
</del><ins>+                    var text = new EditableText(group.label());
+                    text.setStartedEditingCallback(function () { return text.render(); });
+                    text.setUpdateCallback(function () { return updateTestGroupName(group); });
+
+                    self._testGroupLabelMap.set(group, text);
+                    return element('li', {class: 'test-group-list-' + group.id()},
+                        link(text, group.label(), function () { showTestGroup(group); }));
</ins><span class="cx">                 }).reverse());
</span><ins>+
</ins><span class="cx">             this._renderedCurrentTestGroup = null;
</span><span class="cx">         }
</span><span class="cx"> 
</span><ins>+        if (this._testGroups) {
+            for (var testGroup of this._testGroups) {
+                var label = this._testGroupLabelMap.get(testGroup);
+                label.setText(testGroup.label());
+                label.render();
+            }
+        }
+
</ins><span class="cx">         if (this._renderedCurrentTestGroup !== this._currentTestGroup) {
</span><span class="cx">             if (this._renderedCurrentTestGroup) {
</span><span class="cx">                 var element = this.content().querySelector('.test-group-list-' + this._renderedCurrentTestGroup.id());
</span><span class="lines">@@ -222,6 +250,39 @@
</span><span class="cx">         this.render();
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    _didStartEditingTaskName()
+    {
+        this._taskNameLabel.render();
+    }
+
+    _updateTaskName()
+    {
+        console.assert(this._task);
+        this._taskNameLabel.render();
+
+        var self = this;
+        return self._task.updateName(self._taskNameLabel.editedText()).then(function () {
+            self.render();
+        }, function (error) {
+            self.render();
+            alert('Failed to update the name: ' + error);
+        });
+    }
+
+    _updateTestGroupName(testGroup)
+    {
+        var label = this._testGroupLabelMap.get(testGroup);
+        label.render();
+
+        var self = this;
+        return testGroup.updateName(label.editedText()).then(function () {
+            self.render();
+        }, function (error) {
+            self.render();
+            alert('Failed to update the name: ' + error);
+        });
+    }
+
</ins><span class="cx">     _retryCurrentTestGroup(event)
</span><span class="cx">     {
</span><span class="cx">         event.preventDefault();
</span><span class="lines">@@ -307,7 +368,7 @@
</span><span class="cx">         return `
</span><span class="cx">         &lt;div class=&quot;analysis-tasl-page-container&quot;&gt;
</span><span class="cx">             &lt;div class=&quot;analysis-tasl-page&quot;&gt;
</span><del>-                &lt;h2 class=&quot;analysis-task-name&quot;&gt;&lt;/h2&gt;
</del><ins>+                &lt;h2 class=&quot;analysis-task-name&quot;&gt;&lt;editable-text&gt;&lt;/editable-text&gt;&lt;/h2&gt;
</ins><span class="cx">                 &lt;h3 class=&quot;platform-metric-names&quot;&gt;&lt;a href=&quot;&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
</span><span class="cx">                 &lt;p class=&quot;error-message&quot;&gt;&lt;/p&gt;
</span><span class="cx">                 &lt;div class=&quot;overview-chart&quot;&gt;&lt;analysis-task-chart-pane&gt;&lt;/analysis-task-chart-pane&gt;&lt;/div&gt;
</span><span class="lines">@@ -425,11 +486,11 @@
</span><span class="cx">                 border-right: none;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .test-group-list li {
</del><ins>+            .test-group-list &gt; li {
</ins><span class="cx">                 display: block;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .test-group-list a {
</del><ins>+            .test-group-list &gt; li &gt; a {
</ins><span class="cx">                 display: block;
</span><span class="cx">                 color: inherit;
</span><span class="cx">                 text-decoration: none;
</span><span class="lines">@@ -438,11 +499,11 @@
</span><span class="cx">                 padding: 0.2rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .test-group-list li.selected a {
</del><ins>+            .test-group-list &gt; li.selected &gt; a {
</ins><span class="cx">                 background: rgba(204, 153, 51, 0.1);
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .test-group-list li:not(.selected) a:hover {
</del><ins>+            .test-group-list &gt; li:not(.selected) &gt; a:hover {
</ins><span class="cx">                 background: #eee;
</span><span class="cx">             }
</span><span class="cx"> 
</span></span></pre>
</div>
</div>

</body>
</html>