Modernize AnalysisTaskPage
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 28 Mar 2017 23:00:36 +0000 (23:00 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 28 Mar 2017 23:00:36 +0000 (23:00 +0000)
https://bugs.webkit.org/show_bug.cgi?id=170165

Reviewed by Antti Koivisto.

Modernized AnalysisTaskPage and related components. The main refactoring happened in AnalysisTaskPage
from which AnalysisTaskResultsPane and AnalysisTaskTestGroupPane have been extracted.

Decoupled BuildRequest from its results. AnalysisResultsViewer and TestGroupResultsTable now stores
a reference to AnalysisResultsView and Metric to find the results for each build request.
This refactoring is necessary in order to view results of an arbitrary metric in the future.

Also refactored ResultsTable and its subclasses extensively. Instead of making its render() to invoke
subclass' methods such as buildRowGroups, heading, and additionalHeading, rely on each subclass call
to invoke renderTable(), renamed from render(), with callbacks to add extra headers and columns.

This patch also fixes a number of usability issues found by the user such as changing the test name
resets the customized revisions by the virtue of the modern code being naturally more correct.

* public/v3/components/analysis-results-viewer.js:
(AnalysisResultsViewer):
(AnalysisResultsViewer.prototype.setTestGroupCallback): Deleted. Replaced by "testGroupClick" action.
(AnalysisResultsViewer.prototype.setRangeSelectorLabels): Moved here from ResultsTable since it's
never used in ResultsTable or TestGroupResultsTable.
(AnalysisResultsViewer.prototype.selectedRange): Ditto.
(AnalysisResultsViewer.prototype.setPoints): Now takes metric as the third argument.
(AnalysisResultsViewer.prototype.setTestGroups): Now takes the current test group.
(AnalysisResultsViewer.prototype.didUpdateResults): Deleted.
(AnalysisResultsViewer.prototype.setAnalysisResultsView): Added.
(AnalysisResultsViewer.prototype.render): Invoke _renderTestGroups lazily. Also simplified the logic
to find the selected list item. Since we always use a shadow DOM now, we can simply look for an element
with ".seleted" instead of crafting a unique class name.
(AnalysisResultsViewer.prototype.renderTestGroups): Renamed from buildRowGroups. Specify callbacks to
insert headers for A/B radio buttons, which has been moved from ResultsTable.prototype.render, and the
stacked blocks of testing results.
(AnalysisResultsViewer.prototype._classForTestGroup): Deleted.
(AnalysisResultsViewer.prototype._openStackingBlock): Deleted.
(AnalysisResultsViewer.prototype._expandBetween): Create a new set for expandedPoints to make
_renderTestGroupsLazily.evaluate do the work.
(AnalysisResultsViewer._layoutBlocks): Moved from TestGroupStackingGrid.layout.
(AnalysisResultsViewer._sortBlocksByRow): Moved from AnalysisResultsViewer.TestGroupStackingGrid.
(AnalysisResultsViewer._insertAfterBlockWithSameRange): Ditto.
(AnalysisResultsViewer._insertBlockInFirstAvailableColumn): Ditto.
(AnalysisResultsViewer._createCellsForRow): Ditto.

(AnalysisResultsViewer.TestGroupStackingBlock):
(AnalysisResultsViewer.TestGroupStackingBlock.prototype.addRowIndex):
(AnalysisResultsViewer.TestGroupStackingBlock.prototype.createStackingCell): No longer creates a unique
class name here. See the inline comment for AnalysisResultsViewer.prototype.render.
(AnalysisResultsViewer.TestGroupStackingBlock.prototype.isThin): Deleted. We used to collapse "failed"
test groups as a thin vertical line, and we wanted to show them next to each other in _layoutBlock but
we don't do that anymore.
(AnalysisResultsViewer.TestGroupStackingBlock.prototype._valuesForCommitSet): Added. Uses
this._analysisResultsView to extract the results for the current metrics.
(AnalysisResultsViewer.TestGroupStackingBlock.prototype._computeTestGroupStatus):

* public/v3/components/analysis-task-bug-list.js: Added.
(AnalysisTaskBugList): Added. Extracted from AnalysisTaskChartPane.
(AnalysisTaskBugList.prototype.setTask): Added.
(AnalysisTaskBugList.prototype.didConstructShadowTree): Added.
(AnalysisTaskBugList.prototype.render): Added.
(AnalysisTaskBugList.prototype._associateBug): Added.
(AnalysisTaskBugList.prototype._dissociateBug): Added.
(AnalysisTaskBugList.htmlTemplate): Added.

* public/v3/components/chart-pane-base.js:
(ChartPaneBase.htmlTemplate): Added a hook to insert more content at the end in AnalysisTaskChartPane.
(ChartPaneBase.paneFooterTemplate): Added.

* public/v3/components/customizable-test-group-form.js:
(CustomizableTestGroupForm):
(CustomizableTestGroupForm.prototype.setCommitSetMap):
(CustomizableTestGroupForm.prototype.startTesting): Renamed from _submitted. Now dispatches an action
by the name of "startTesting" instead of calling this._startCallback.
(CustomizableTestGroupForm.prototype.didConstructShadowTree): Added. Moved the logic to attach event
handlers here to avoid eagerly creating the shadow tree in the constructor.
(CustomizableTestGroupForm.prototype._computeCommitSetMap): Use the newly added this._revisionEditorMap
to find the relevant input element instead of running a querySelector.
(CustomizableTestGroupForm.prototype.render): Lazily invoke _renderCustomRevisionTable. This avoids
overriding the customized revisions when the user finally types in the test group name.
(CustomizableTestGroupForm.prototype._renderCustomRevisionTable): Extracted from render.
(CustomizableTestGroupForm.prototype._constructRevisionRadioButtons): Made this a non-static method
since it needs to update this._revisionEditorMap now. Merged _constructRevisionRadioButtons.
(CustomizableTestGroupForm.prototype._createRadioButton): Deleted. See above.
(CustomizableTestGroupForm.cssTemplate):
(CustomizableTestGroupForm.formContent): Use IDs instead of classes to make this.content(ID) work.

* public/v3/components/mutable-list-view.js:
(MutableListView.prototype.setList):
(MutableListView.prototype.setKindList):
(MutableListView.prototype.setAddCallback): Deleted. Replaced by "addItem" action.
(MutableListView.prototype.render):
(MutableListItem.prototype.content):

* public/v3/components/results-table.js:
(ResultsTable): Removed this._rangeSelectorLabels, this._rangeSelectorCallback, and this._selectedRange
as they are only used by AnalysisResultsViewer. Also replaced this._valueFormatter by
this._analysisResultsView which knows a metric.
(ResultsTable.prototype.setValueFormatter): Deleted.
(ResultsTable.prototype.setRangeSelectorLabels): Deleted.
(ResultsTable.prototype.setRangeSelectorCallback): Deleted.
(ResultsTable.prototype.selectedRange): Deleted.
(ResultsTable.prototype._rangeSelectorClicked): Deleted.
(ResultsTable.prototype.setAnalysisResultsView): Added.
(ResultsTable.prototype.renderTable): Added. Removed the logic to add _rangeSelectorLabels since it has
been moved to AnalysisResultsViewer.prototype.render inside buildColumns, which also inserts additional
columns which used to be stored on each ResultsTableRow. Use the same technique to insert additional
headers. Also take the name (thead tr th) of row header (tbody tr td) as an argument and automatically
create a table cell of an appropriate colspan.
(ResultsTable.prototype._createRevisionListCells):
(ResultsTable.prototype.heading): Deleted. Superseded by buildHeaders callback.
(ResultsTable.prototype.additionalHeading): Ditto.
(ResultsTable.prototype.buildRowGroups): Deleted. It is now the responsibility of each subclass to call
ResultsTable's renderTable() in the subclass' render() function.
(ResultsTable.prototype._computeRepositoryList): No longer takes extraRepositories as an argument.
Instead, this function now returns a pair of the repository list and the list of constant commits.
(ResultsTable.htmlTemplate):
(ResultsTable.cssTemplate):

* public/v3/components/test-group-form.js:
(TestGroupForm): Avoid eagerly creating the shadow tree. Also removed the removed the dead code.
(TestGroupForm.prototype.setRepetitionCount): Simply override the value of the select element.
(TestGroupForm.prototype.didConstructShadowTree): Added. Attach event handlers here to avoid eagerly
creating the shadow tree in the constructor.
(TestGroupForm.prototype.startTesting): Renamed from _submitted. Dispatch "startTesting" action instead
of invoking _startCallback which has been removed.
(TestGroupForm.htmlTemplate):
(TestGroupForm.formContent):

* public/v3/components/test-group-results-table.js:
(TestGroupResultsTable):
(TestGroupResultsTable.prototype.didUpdateResults): Deleted. No longer neeed per setAnalysisResultsView
in ResultsTable.
(TestGroupResultsTable.prototype.setTestGroup):
(TestGroupResultsTable.prototype.heading): Deleted.
(TestGroupResultsTable.prototype.render):
(TestGroupResultsTable.prototype._renderTestGroup): Extracted from render.
(TestGroupResultsTable.prototype._buildRowGroups): Renamed from buildRowGroups.
(TestGroupResultsTable.prototype._buildRowGroupForCommitSet): Extracted from buildRowGroups.
(TestGroupResultsTable.prototype._buildComparisonRow): Extracted from buildRowGroups.buildRowGroups

* public/v3/index.html: Include analysis-task-bug-list.js.

* public/v3/models/analysis-results.js:
(AnalysisResults): Inverted the map so that we can easily create a view based on metric.
(AnalysisResults.prototype.find): Ditto.
(AnalysisResults.prototype.add): Ditto.
(AnalysisResults.prototype.viewForMetric): Added.
(AnalysisResults.fetch):
(AnalysisResultsView): Added.
(AnalysisResultsView.prototype.metric): Added.
(AnalysisResultsView.prototype.resultForBuildId): Added.

* public/v3/models/build-request.js:
(BuildRequest.result): Deleted.
(BuildRequest.setResult): Deleted.

* public/v3/models/test-group.js:
(TestGroup): Removed this._allCommitSets since it was never used.
(TestGroup.prototype.didSetResult): Deleted since it was never used.
(TestGroup.prototype.compareTestResults): Now takes an array of measurement set values.
(TestGroup.prototype._valuesForCommitSet): Deleted.

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskChartPane): This class now includes the form to cutomize the revisions.
(AnalysisTaskChartPane.prototype.setShowForm): Added.
(AnalysisTaskChartPane.prototype._mainSelectionDidChange):
(AnalysisTaskChartPane.prototype.didConstructShadowTree): Added. Dispatches "newTestGroup" action when
the user presses the button to start a new A/B testing from the chart.
(AnalysisTaskChartPane.prototype.render): Added.
(AnalysisTaskChartPane.prototype.paneFooterTemplate): Added.
(AnalysisTaskChartPane.cssTemplate):

(AnalysisTaskResultsPane): Added. Encapsulates AnalysisResultsViewer and CustomizableTestGroupForm.
(AnalysisTaskResultsPane.prototype.setPoints): Added.
(AnalysisTaskResultsPane.prototype.setTestGroups): Added.
(AnalysisTaskResultsPane.prototype.setAnalysisResultsView): Added.
(AnalysisTaskResultsPane.prototype.setShowForm): Added.
(AnalysisTaskResultsPane.prototype.didConstructShadowTree): Added. Dispatches "newTestGroup" action
when the user presses the button to start a new A/B testing from the chart.
(AnalysisTaskResultsPane.prototype.render): Added.
(AnalysisTaskResultsPane.htmlTemplate): Added.
(AnalysisTaskResultsPane.cssTemplate): Added.

(AnalysisTaskTestGroupPane): Added. Encapsulates TestGroupResultsTable and CustomizableTestGroupForm.
(AnalysisTaskTestGroupPane.prototype.didConstructShadowTree): Added.
(AnalysisTaskTestGroupPane.prototype.setTestGroups): Added.
(AnalysisTaskTestGroupPane.prototype.setAnalysisResultsView): Added.
(AnalysisTaskTestGroupPane.prototype.render): Added.
(AnalysisTaskTestGroupPane.prototype._renderTestGroups): Added. Updates the list of test groups. Hide
the hidden groups unless showHiddenGroups is set. Updates this._testGroupMap so that the visibility of
groups and their names can be updated without having to re-render the entire list.
(AnalysisTaskTestGroupPane.prototype._renderTestGroupVisibility): Added.
(AnalysisTaskTestGroupPane.prototype._renderTestGroupNames): Added.
(AnalysisTaskTestGroupPane.prototype._renderCurrentTestGroup): Added. Update TestGroupResultsTable with
the selected test group. Also highlight the list view, and update the hide-unhide toggle button's label
as needed.
(AnalysisTaskTestGroupPane.htmlTemplate): Added.
(AnalysisTaskTestGroupPane.cssTemplate): Added.

(AnalysisTaskPage): Deleted a massive number of instance variables. They are now manged by newly added
AnalysisTaskChartPane, AnalysisTaskResultsPane, and AnalysisTaskTestGroupPane
(AnalysisTaskPage.prototype.didConstructShadowTree): Added. Attach various event handlers here to avoid
eagerly creating the shadow tree in the constructor.
(AnalysisTaskPage.prototype._fetchRelatedInfoForTaskId):
(AnalysisTaskPage.prototype._didFetchTask): No longer sets the value formatter to the results viewer
and the results table as they now recieve AnalysisResultsView later in _assignTestResultsIfPossible.
(AnalysisTaskPage.prototype._didFetchMeasurement): Set the metric to the results viewer.
(AnalysisTaskPage.prototype._didUpdateTestGroupHiddenState):
(AnalysisTaskPage.prototype._assignTestResultsIfPossible): Create AnalysisResultsView from the newly
retrieved AnalysisResults and pass it to AnalysisTaskResultsPane and AnalysisTaskTestGroupPane.
(AnalysisTaskPage.prototype.render): Dramatically simplified.
(AnalysisTaskPage.prototype._renderTaskNameAndStatus): Extracted from render.
(AnalysisTaskPage.prototype._renderRelatedTasks): Ditto.
(AnalysisTaskPage.prototype._renderCauseAndFixes): Ditto.
(AnalysisTaskPage.prototype._showTestGroup):
(AnalysisTaskPage.prototype._updateTaskName): Now takes the new name as an argument.
(AnalysisTaskPage.prototype._updateTestGroupName): Now takes the new name as the second argument.
(AnalysisTaskPage.prototype._hideCurrentTestGroup): Now takes the test group to hide.
(AnalysisTaskPage.prototype._associateCommit): Moved to AnalysisTaskBugList.
(AnalysisTaskPage.prototype._dissociateCommit): Ditto.
(AnalysisTaskPage.prototype._retryCurrentTestGroup): Now takes the test group as the first argument.
(AnalysisTaskPage.prototype._chartSelectionDidChange): Deleted.
(AnalysisTaskPage.prototype._createNewTestGroupFromChart): Deleted.
(AnalysisTaskPage.prototype._selectedRowInAnalysisResultsViewer): Deleted.
(AnalysisTaskPage.prototype._createNewTestGroupFromViewer): Deleted.
(AnalysisTaskPage.htmlTemplate):
(AnalysisTaskPage.cssTemplate):

* unit-tests/test-groups-tests.js: Updated a test case which was expecting BuildReqeust's result, which
has been removed, to exist.

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

15 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/v3/components/analysis-results-viewer.js
Websites/perf.webkit.org/public/v3/components/analysis-task-bug-list.js [new file with mode: 0644]
Websites/perf.webkit.org/public/v3/components/chart-pane-base.js
Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js
Websites/perf.webkit.org/public/v3/components/mutable-list-view.js
Websites/perf.webkit.org/public/v3/components/results-table.js
Websites/perf.webkit.org/public/v3/components/test-group-form.js
Websites/perf.webkit.org/public/v3/components/test-group-results-table.js
Websites/perf.webkit.org/public/v3/index.html
Websites/perf.webkit.org/public/v3/models/analysis-results.js
Websites/perf.webkit.org/public/v3/models/build-request.js
Websites/perf.webkit.org/public/v3/models/test-group.js
Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js
Websites/perf.webkit.org/unit-tests/test-groups-tests.js

index 76ec2dc..848b007 100644 (file)
@@ -1,3 +1,237 @@
+2017-03-28  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Modernize AnalysisTaskPage
+        https://bugs.webkit.org/show_bug.cgi?id=170165
+
+        Reviewed by Antti Koivisto.
+
+        Modernized AnalysisTaskPage and related components. The main refactoring happened in AnalysisTaskPage
+        from which AnalysisTaskResultsPane and AnalysisTaskTestGroupPane have been extracted.
+
+        Decoupled BuildRequest from its results. AnalysisResultsViewer and TestGroupResultsTable now stores
+        a reference to AnalysisResultsView and Metric to find the results for each build request.
+        This refactoring is necessary in order to view results of an arbitrary metric in the future.
+
+        Also refactored ResultsTable and its subclasses extensively. Instead of making its render() to invoke
+        subclass' methods such as buildRowGroups, heading, and additionalHeading, rely on each subclass call
+        to invoke renderTable(), renamed from render(), with callbacks to add extra headers and columns.
+
+        This patch also fixes a number of usability issues found by the user such as changing the test name
+        resets the customized revisions by the virtue of the modern code being naturally more correct.
+
+        * public/v3/components/analysis-results-viewer.js:
+        (AnalysisResultsViewer):
+        (AnalysisResultsViewer.prototype.setTestGroupCallback): Deleted. Replaced by "testGroupClick" action.
+        (AnalysisResultsViewer.prototype.setRangeSelectorLabels): Moved here from ResultsTable since it's
+        never used in ResultsTable or TestGroupResultsTable.
+        (AnalysisResultsViewer.prototype.selectedRange): Ditto.
+        (AnalysisResultsViewer.prototype.setPoints): Now takes metric as the third argument.
+        (AnalysisResultsViewer.prototype.setTestGroups): Now takes the current test group.
+        (AnalysisResultsViewer.prototype.didUpdateResults): Deleted.
+        (AnalysisResultsViewer.prototype.setAnalysisResultsView): Added.
+        (AnalysisResultsViewer.prototype.render): Invoke _renderTestGroups lazily. Also simplified the logic
+        to find the selected list item. Since we always use a shadow DOM now, we can simply look for an element
+        with ".seleted" instead of crafting a unique class name.
+        (AnalysisResultsViewer.prototype.renderTestGroups): Renamed from buildRowGroups. Specify callbacks to
+        insert headers for A/B radio buttons, which has been moved from ResultsTable.prototype.render, and the
+        stacked blocks of testing results.
+        (AnalysisResultsViewer.prototype._classForTestGroup): Deleted.
+        (AnalysisResultsViewer.prototype._openStackingBlock): Deleted.
+        (AnalysisResultsViewer.prototype._expandBetween): Create a new set for expandedPoints to make
+        _renderTestGroupsLazily.evaluate do the work.
+        (AnalysisResultsViewer._layoutBlocks): Moved from TestGroupStackingGrid.layout.
+        (AnalysisResultsViewer._sortBlocksByRow): Moved from AnalysisResultsViewer.TestGroupStackingGrid.
+        (AnalysisResultsViewer._insertAfterBlockWithSameRange): Ditto.
+        (AnalysisResultsViewer._insertBlockInFirstAvailableColumn): Ditto.
+        (AnalysisResultsViewer._createCellsForRow): Ditto.
+
+        (AnalysisResultsViewer.TestGroupStackingBlock):
+        (AnalysisResultsViewer.TestGroupStackingBlock.prototype.addRowIndex):
+        (AnalysisResultsViewer.TestGroupStackingBlock.prototype.createStackingCell): No longer creates a unique
+        class name here. See the inline comment for AnalysisResultsViewer.prototype.render.
+        (AnalysisResultsViewer.TestGroupStackingBlock.prototype.isThin): Deleted. We used to collapse "failed"
+        test groups as a thin vertical line, and we wanted to show them next to each other in _layoutBlock but
+        we don't do that anymore.
+        (AnalysisResultsViewer.TestGroupStackingBlock.prototype._valuesForCommitSet): Added. Uses
+        this._analysisResultsView to extract the results for the current metrics.
+        (AnalysisResultsViewer.TestGroupStackingBlock.prototype._computeTestGroupStatus):
+
+        * public/v3/components/analysis-task-bug-list.js: Added.
+        (AnalysisTaskBugList): Added. Extracted from AnalysisTaskChartPane.
+        (AnalysisTaskBugList.prototype.setTask): Added.
+        (AnalysisTaskBugList.prototype.didConstructShadowTree): Added.
+        (AnalysisTaskBugList.prototype.render): Added.
+        (AnalysisTaskBugList.prototype._associateBug): Added.
+        (AnalysisTaskBugList.prototype._dissociateBug): Added.
+        (AnalysisTaskBugList.htmlTemplate): Added.
+
+        * public/v3/components/chart-pane-base.js:
+        (ChartPaneBase.htmlTemplate): Added a hook to insert more content at the end in AnalysisTaskChartPane.
+        (ChartPaneBase.paneFooterTemplate): Added.
+
+        * public/v3/components/customizable-test-group-form.js:
+        (CustomizableTestGroupForm):
+        (CustomizableTestGroupForm.prototype.setCommitSetMap):
+        (CustomizableTestGroupForm.prototype.startTesting): Renamed from _submitted. Now dispatches an action
+        by the name of "startTesting" instead of calling this._startCallback.
+        (CustomizableTestGroupForm.prototype.didConstructShadowTree): Added. Moved the logic to attach event
+        handlers here to avoid eagerly creating the shadow tree in the constructor.
+        (CustomizableTestGroupForm.prototype._computeCommitSetMap): Use the newly added this._revisionEditorMap
+        to find the relevant input element instead of running a querySelector.
+        (CustomizableTestGroupForm.prototype.render): Lazily invoke _renderCustomRevisionTable. This avoids
+        overriding the customized revisions when the user finally types in the test group name.
+        (CustomizableTestGroupForm.prototype._renderCustomRevisionTable): Extracted from render.
+        (CustomizableTestGroupForm.prototype._constructRevisionRadioButtons): Made this a non-static method
+        since it needs to update this._revisionEditorMap now. Merged _constructRevisionRadioButtons.
+        (CustomizableTestGroupForm.prototype._createRadioButton): Deleted. See above.
+        (CustomizableTestGroupForm.cssTemplate):
+        (CustomizableTestGroupForm.formContent): Use IDs instead of classes to make this.content(ID) work.
+
+        * public/v3/components/mutable-list-view.js:
+        (MutableListView.prototype.setList):
+        (MutableListView.prototype.setKindList):
+        (MutableListView.prototype.setAddCallback): Deleted. Replaced by "addItem" action.
+        (MutableListView.prototype.render):
+        (MutableListItem.prototype.content):
+
+        * public/v3/components/results-table.js:
+        (ResultsTable): Removed this._rangeSelectorLabels, this._rangeSelectorCallback, and this._selectedRange
+        as they are only used by AnalysisResultsViewer. Also replaced this._valueFormatter by
+        this._analysisResultsView which knows a metric.
+        (ResultsTable.prototype.setValueFormatter): Deleted.
+        (ResultsTable.prototype.setRangeSelectorLabels): Deleted.
+        (ResultsTable.prototype.setRangeSelectorCallback): Deleted.
+        (ResultsTable.prototype.selectedRange): Deleted.
+        (ResultsTable.prototype._rangeSelectorClicked): Deleted.
+        (ResultsTable.prototype.setAnalysisResultsView): Added.
+        (ResultsTable.prototype.renderTable): Added. Removed the logic to add _rangeSelectorLabels since it has
+        been moved to AnalysisResultsViewer.prototype.render inside buildColumns, which also inserts additional
+        columns which used to be stored on each ResultsTableRow. Use the same technique to insert additional
+        headers. Also take the name (thead tr th) of row header (tbody tr td) as an argument and automatically
+        create a table cell of an appropriate colspan.
+        (ResultsTable.prototype._createRevisionListCells):
+        (ResultsTable.prototype.heading): Deleted. Superseded by buildHeaders callback.
+        (ResultsTable.prototype.additionalHeading): Ditto.
+        (ResultsTable.prototype.buildRowGroups): Deleted. It is now the responsibility of each subclass to call
+        ResultsTable's renderTable() in the subclass' render() function.
+        (ResultsTable.prototype._computeRepositoryList): No longer takes extraRepositories as an argument.
+        Instead, this function now returns a pair of the repository list and the list of constant commits.
+        (ResultsTable.htmlTemplate):
+        (ResultsTable.cssTemplate):
+
+        * public/v3/components/test-group-form.js:
+        (TestGroupForm): Avoid eagerly creating the shadow tree. Also removed the removed the dead code.
+        (TestGroupForm.prototype.setRepetitionCount): Simply override the value of the select element.
+        (TestGroupForm.prototype.didConstructShadowTree): Added. Attach event handlers here to avoid eagerly
+        creating the shadow tree in the constructor.
+        (TestGroupForm.prototype.startTesting): Renamed from _submitted. Dispatch "startTesting" action instead
+        of invoking _startCallback which has been removed.
+        (TestGroupForm.htmlTemplate):
+        (TestGroupForm.formContent):
+
+        * public/v3/components/test-group-results-table.js:
+        (TestGroupResultsTable):
+        (TestGroupResultsTable.prototype.didUpdateResults): Deleted. No longer neeed per setAnalysisResultsView
+        in ResultsTable.
+        (TestGroupResultsTable.prototype.setTestGroup):
+        (TestGroupResultsTable.prototype.heading): Deleted.
+        (TestGroupResultsTable.prototype.render):
+        (TestGroupResultsTable.prototype._renderTestGroup): Extracted from render.
+        (TestGroupResultsTable.prototype._buildRowGroups): Renamed from buildRowGroups.
+        (TestGroupResultsTable.prototype._buildRowGroupForCommitSet): Extracted from buildRowGroups.
+        (TestGroupResultsTable.prototype._buildComparisonRow): Extracted from buildRowGroups.buildRowGroups
+
+        * public/v3/index.html: Include analysis-task-bug-list.js.
+
+        * public/v3/models/analysis-results.js:
+        (AnalysisResults): Inverted the map so that we can easily create a view based on metric.
+        (AnalysisResults.prototype.find): Ditto.
+        (AnalysisResults.prototype.add): Ditto.
+        (AnalysisResults.prototype.viewForMetric): Added.
+        (AnalysisResults.fetch):
+        (AnalysisResultsView): Added.
+        (AnalysisResultsView.prototype.metric): Added.
+        (AnalysisResultsView.prototype.resultForBuildId): Added.
+
+        * public/v3/models/build-request.js:
+        (BuildRequest.result): Deleted.
+        (BuildRequest.setResult): Deleted.
+
+        * public/v3/models/test-group.js:
+        (TestGroup): Removed this._allCommitSets since it was never used.
+        (TestGroup.prototype.didSetResult): Deleted since it was never used.
+        (TestGroup.prototype.compareTestResults): Now takes an array of measurement set values.
+        (TestGroup.prototype._valuesForCommitSet): Deleted.
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskChartPane): This class now includes the form to cutomize the revisions.
+        (AnalysisTaskChartPane.prototype.setShowForm): Added.
+        (AnalysisTaskChartPane.prototype._mainSelectionDidChange):
+        (AnalysisTaskChartPane.prototype.didConstructShadowTree): Added. Dispatches "newTestGroup" action when
+        the user presses the button to start a new A/B testing from the chart.
+        (AnalysisTaskChartPane.prototype.render): Added.
+        (AnalysisTaskChartPane.prototype.paneFooterTemplate): Added.
+        (AnalysisTaskChartPane.cssTemplate):
+
+        (AnalysisTaskResultsPane): Added. Encapsulates AnalysisResultsViewer and CustomizableTestGroupForm.
+        (AnalysisTaskResultsPane.prototype.setPoints): Added.
+        (AnalysisTaskResultsPane.prototype.setTestGroups): Added.
+        (AnalysisTaskResultsPane.prototype.setAnalysisResultsView): Added.
+        (AnalysisTaskResultsPane.prototype.setShowForm): Added.
+        (AnalysisTaskResultsPane.prototype.didConstructShadowTree): Added. Dispatches "newTestGroup" action
+        when the user presses the button to start a new A/B testing from the chart.
+        (AnalysisTaskResultsPane.prototype.render): Added.
+        (AnalysisTaskResultsPane.htmlTemplate): Added.
+        (AnalysisTaskResultsPane.cssTemplate): Added.
+
+        (AnalysisTaskTestGroupPane): Added. Encapsulates TestGroupResultsTable and CustomizableTestGroupForm.
+        (AnalysisTaskTestGroupPane.prototype.didConstructShadowTree): Added.
+        (AnalysisTaskTestGroupPane.prototype.setTestGroups): Added.
+        (AnalysisTaskTestGroupPane.prototype.setAnalysisResultsView): Added.
+        (AnalysisTaskTestGroupPane.prototype.render): Added.
+        (AnalysisTaskTestGroupPane.prototype._renderTestGroups): Added. Updates the list of test groups. Hide
+        the hidden groups unless showHiddenGroups is set. Updates this._testGroupMap so that the visibility of
+        groups and their names can be updated without having to re-render the entire list.
+        (AnalysisTaskTestGroupPane.prototype._renderTestGroupVisibility): Added.
+        (AnalysisTaskTestGroupPane.prototype._renderTestGroupNames): Added.
+        (AnalysisTaskTestGroupPane.prototype._renderCurrentTestGroup): Added. Update TestGroupResultsTable with
+        the selected test group. Also highlight the list view, and update the hide-unhide toggle button's label
+        as needed.
+        (AnalysisTaskTestGroupPane.htmlTemplate): Added.
+        (AnalysisTaskTestGroupPane.cssTemplate): Added.
+
+        (AnalysisTaskPage): Deleted a massive number of instance variables. They are now manged by newly added
+        AnalysisTaskChartPane, AnalysisTaskResultsPane, and AnalysisTaskTestGroupPane
+        (AnalysisTaskPage.prototype.didConstructShadowTree): Added. Attach various event handlers here to avoid
+        eagerly creating the shadow tree in the constructor.
+        (AnalysisTaskPage.prototype._fetchRelatedInfoForTaskId):
+        (AnalysisTaskPage.prototype._didFetchTask): No longer sets the value formatter to the results viewer
+        and the results table as they now recieve AnalysisResultsView later in _assignTestResultsIfPossible.
+        (AnalysisTaskPage.prototype._didFetchMeasurement): Set the metric to the results viewer.
+        (AnalysisTaskPage.prototype._didUpdateTestGroupHiddenState):
+        (AnalysisTaskPage.prototype._assignTestResultsIfPossible): Create AnalysisResultsView from the newly
+        retrieved AnalysisResults and pass it to AnalysisTaskResultsPane and AnalysisTaskTestGroupPane.
+        (AnalysisTaskPage.prototype.render): Dramatically simplified.
+        (AnalysisTaskPage.prototype._renderTaskNameAndStatus): Extracted from render.
+        (AnalysisTaskPage.prototype._renderRelatedTasks): Ditto.
+        (AnalysisTaskPage.prototype._renderCauseAndFixes): Ditto.
+        (AnalysisTaskPage.prototype._showTestGroup):
+        (AnalysisTaskPage.prototype._updateTaskName): Now takes the new name as an argument.
+        (AnalysisTaskPage.prototype._updateTestGroupName): Now takes the new name as the second argument.
+        (AnalysisTaskPage.prototype._hideCurrentTestGroup): Now takes the test group to hide.
+        (AnalysisTaskPage.prototype._associateCommit): Moved to AnalysisTaskBugList.
+        (AnalysisTaskPage.prototype._dissociateCommit): Ditto.
+        (AnalysisTaskPage.prototype._retryCurrentTestGroup): Now takes the test group as the first argument.
+        (AnalysisTaskPage.prototype._chartSelectionDidChange): Deleted.
+        (AnalysisTaskPage.prototype._createNewTestGroupFromChart): Deleted.
+        (AnalysisTaskPage.prototype._selectedRowInAnalysisResultsViewer): Deleted.
+        (AnalysisTaskPage.prototype._createNewTestGroupFromViewer): Deleted.
+        (AnalysisTaskPage.htmlTemplate):
+        (AnalysisTaskPage.cssTemplate):
+
+        * unit-tests/test-groups-tests.js: Updated a test case which was expecting BuildReqeust's result, which
+        has been removed, to exist.
+
 2017-03-23  Ryosuke Niwa  <rniwa@webkit.org>
 
         Share more code between ManifestGenerator and /api/triggerables
index 1504e98..f210162 100644 (file)
@@ -5,129 +5,125 @@ class AnalysisResultsViewer extends ResultsTable {
         super('analysis-results-viewer');
         this._startPoint = null;
         this._endPoint = null;
+        this._metric = null;
         this._testGroups = null;
         this._currentTestGroup = null;
-        this._renderedCurrentTestGroup = null;
-        this._shouldRenderTable = true;
-        this._additionalHeading = null;
-        this._testGroupCallback = null;
+        this._rangeSelectorLabels = [];
+        this._selectedRange = {};
         this._expandedPoints = new Set;
-    }
-
-    setTestGroupCallback(callback) { this._testGroupCallback = callback; }
+        this._groupToCellMap = new Map;
 
-    setCurrentTestGroup(testGroup)
-    {
-        this._currentTestGroup = testGroup;
+        this._renderTestGroupsLazily = new LazilyEvaluatedFunction(this.renderTestGroups.bind(this));
     }
 
-    setPoints(startPoint, endPoint)
+    setRangeSelectorLabels(labels) { this._rangeSelectorLabels = labels; }
+    selectedRange() { return this._selectedRange; }
+
+    setPoints(startPoint, endPoint, metric)
     {
+        this._metric = metric;
         this._startPoint = startPoint;
         this._endPoint = endPoint;
-        this._shouldRenderTable = true;
-        this._expandedPoints.clear();
+        this._expandedPoints = new Set;
         this._expandedPoints.add(startPoint);
         this._expandedPoints.add(endPoint);
+        this.enqueueToRender();
     }
 
-    setTestGroups(testGroups)
+    setTestGroups(testGroups, currentTestGroup)
     {
         this._testGroups = testGroups;
-        this._shouldRenderTable = true;
+        this._currentTestGroup = currentTestGroup;
+        this.enqueueToRender();
     }
 
-    didUpdateResults()
+    setAnalysisResultsView(analysisResultsView)
     {
-        this._shouldRenderTable = true;
+        console.assert(analysisResultsView instanceof AnalysisResultsView);
+        this._analysisResultsView = analysisResultsView;
+        this.enqueueToRender();
     }
 
     render()
     {
-        if (!this._valueFormatter || !this._startPoint)
-            return;
-
+        super.render();
         Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'render');
 
-        if (this._shouldRenderTable) {
-            this._shouldRenderTable = false;
-            this._renderedCurrentTestGroup = null;
-
-            Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'renderTable');
-            super.render();
-            Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'renderTable');
-        }
+        this._renderTestGroupsLazily.evaluate(this._testGroups,
+            this._startPoint, this._endPoint, this._metric, this._analysisResultsView, this._expandedPoints);
 
-        if (this._currentTestGroup != this._renderedCurrentTestGroup) {
-            if (this._renderedCurrentTestGroup) {
-                var className = this._classForTestGroup(this._renderedCurrentTestGroup);
-                var element = this.content().querySelector('.' + className);
-                if (element)
-                    element.classList.remove('selected');
-            }
-            if (this._currentTestGroup) {
-                var className = this._classForTestGroup(this._currentTestGroup);
-                var element = this.content().querySelector('.' + className);
-                if (element)
-                    element.classList.add('selected');
-            }
-            this._renderedCurrentTestGroup = this._currentTestGroup;
-        }
+        const selectedCell = this.content().querySelector('td.selected');
+        if (selectedCell)
+            selectedCell.classList.remove('selected');
+        if (this._groupToCellMap && this._currentTestGroup)
+            this._groupToCellMap.get(this._currentTestGroup).classList.add('selected');
 
         Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'render');
     }
 
-    heading() { return [ComponentBase.createElement('th', 'Point')]; }
-    additionalHeading() { return this._additionalHeading; }
-
-    buildRowGroups()
+    renderTestGroups(testGroups, startPoint, endPoint, metric, analysisResults, expandedPoints)
     {
-        Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'buildRowGroups');
+        if (!testGroups || !startPoint || !endPoint || !metric || !analysisResults)
+            return false;
 
-        var testGroups = this._testGroups || [];
-        var commitSetsInTestGroups = this._collectCommitSetsInTestGroups(testGroups);
+        Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'renderTestGroups');
 
-        var rowToMatchingCommitSets = new Map;
-        var rowList = this._buildRowsForPointsAndTestGroups(commitSetsInTestGroups, rowToMatchingCommitSets);
+        const commitSetsInTestGroups = this._collectCommitSetsInTestGroups(testGroups);
+        const rowToMatchingCommitSets = new Map;
+        const rows = this._buildRowsForPointsAndTestGroups(commitSetsInTestGroups, rowToMatchingCommitSets);
 
-        var testGroupLayoutMap = new Map;
-        var self = this;
-        rowList.forEach(function (row, rowIndex) {
-            var matchingCommitSets = rowToMatchingCommitSets.get(row);
+        const testGroupLayoutMap = new Map;
+        rows.forEach((row, rowIndex) => {
+            const matchingCommitSets = rowToMatchingCommitSets.get(row);
             if (!matchingCommitSets) {
                 console.assert(row instanceof AnalysisResultsViewer.ExpandableRow);
                 return;
             }
 
-            for (var entry of matchingCommitSets) {
-                var testGroup = entry.testGroup();
+            for (let entry of matchingCommitSets) {
+                const testGroup = entry.testGroup();
 
-                var block = testGroupLayoutMap.get(testGroup);
+                let block = testGroupLayoutMap.get(testGroup);
                 if (!block) {
-                    block = new AnalysisResultsViewer.TestGroupStackingBlock(
-                        testGroup, self._classForTestGroup(testGroup), self._openStackingBlock.bind(self, testGroup));
+                    block = new AnalysisResultsViewer.TestGroupStackingBlock(testGroup, this._analysisResultsView,
+                        this._groupToCellMap, () => this.dispatchAction('testGroupClick', testGroup));
                     testGroupLayoutMap.set(testGroup, block);
                 }
                 block.addRowIndex(entry, rowIndex);
             }
         });
 
-        var grid = new AnalysisResultsViewer.TestGroupStackingGrid(rowList.length);
-        for (var testGroup of testGroups) {
-            var block = testGroupLayoutMap.get(testGroup);
-            if (block)
-                grid.insertBlockToColumn(block);
+        const [additionalColumnsByRow, columnCount] = AnalysisResultsViewer._layoutBlocks(rows.length, testGroups.map((group) => testGroupLayoutMap.get(group)));
+
+        const element = ComponentBase.createElement;
+        const buildHeaders = (headers) => {
+            return [
+                this._rangeSelectorLabels.map((label) => element('th', label)),
+                headers,
+                columnCount ? element('td', {colspan: columnCount + 1, class: 'stacking-block'}) : [],
+            ]
+        };
+        const buildColumns = (columns, row, rowIndex) => {
+            return [
+                this._rangeSelectorLabels.map((label) => {
+                    if (!row.commitSet())
+                        return element('td', '');
+                    const checked = this._selectedRange[label] == row.commitSet();
+                    const onchange = () => {
+                        this._selectedRange[label] = row.commitSet();
+                        this.dispatchAction('rangeSelectorClick', label, row);
+                    };
+                    return element('td', element('input', {type: 'radio', name: label, checked, onchange}));
+                }),
+                columns,
+                additionalColumnsByRow[rowIndex],
+            ];
         }
+        this.renderTable(metric.makeFormatter(4), [{rows}], 'Point', buildHeaders, buildColumns);
 
-        grid.layout();
-        for (var rowIndex = 0; rowIndex < rowList.length; rowIndex++)
-            rowList[rowIndex].setAdditionalColumns(grid.createCellsForRow(rowIndex));
-
-        this._additionalHeading = grid._columns ? ComponentBase.createElement('td', {colspan: grid._columns.length + 1, class: 'stacking-block'}) : [];
+        Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'renderTestGroups');
 
-        Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'buildRowGroups');
-
-        return [{rows: rowList}];
+        return true;
     }
 
     _collectCommitSetsInTestGroups(testGroups)
@@ -235,17 +231,6 @@ class AnalysisResultsViewer extends ResultsTable {
         return rowList;
     }
 
-    _classForTestGroup(testGroup)
-    {
-        return 'stacked-test-group-' + testGroup.id();
-    }
-
-    _openStackingBlock(testGroup)
-    {
-        if (this._testGroupCallback)
-            this._testGroupCallback(testGroup);
-    }
-    
     _expandBetween(pointBeforeExpansion, pointAfterExpansion)
     {
         console.assert(pointBeforeExpansion.series == pointAfterExpansion.series);
@@ -257,12 +242,99 @@ class AnalysisResultsViewer extends ResultsTable {
         var increment = Math.ceil((indexAfterEnd - indexBeforeStart) / 5);
         if (increment < 3)
             increment = 1;
+
+        const expandedPoints = new Set([...this._expandedPoints]);
         for (var i = indexBeforeStart + 1; i < indexAfterEnd; i += increment)
-            this._expandedPoints.add(series.findPointByIndex(i));
-        this._shouldRenderTable = true;
+            expandedPoints.add(series.findPointByIndex(i));
+        this._expandedPoints = expandedPoints;
+
         this.enqueueToRender();
     }
 
+    static _layoutBlocks(rowCount, blocks)
+    {
+        const sortedBlocks = this._sortBlocksByRow(blocks);
+
+        const columns = [];
+        for (const block of sortedBlocks)
+            this._insertBlockInFirstAvailableColumn(columns, block);
+
+        const rows = new Array(rowCount);
+        for (let i = 0; i < rowCount; i++)
+            rows[i] = this._createCellsForRow(columns, i);
+
+        return [rows, columns.length];
+    }
+
+    static _sortBlocksByRow(blocks)
+    {
+        for (let i = 0; i < blocks.length; i++)
+            blocks[i].index = i;
+
+        return blocks.slice(0).sort((block1, block2) => {
+            const startRowDiff = block1.startRowIndex() - block2.startRowIndex();
+            if (startRowDiff)
+                return startRowDiff;
+
+            // Order backwards for end rows in order to place test groups with a larger range at the beginning.
+            const endRowDiff = block2.endRowIndex() - block1.endRowIndex();
+            if (endRowDiff)
+                return endRowDiff;
+
+            return block1.index - block2.index;
+        });
+    }
+
+    static _insertBlockInFirstAvailableColumn(columns, newBlock)
+    {
+        for (const existingColumn of columns) {
+            for (let i = 0; i < existingColumn.length; i++) {
+                const currentBlock = existingColumn[i];
+                if ((!i || existingColumn[i - 1].endRowIndex() < newBlock.startRowIndex())
+                    && newBlock.endRowIndex() < currentBlock.startRowIndex()) {
+                    existingColumn.splice(i, 0, newBlock);
+                    return;
+                }
+            }
+            const lastBlock = existingColumn[existingColumn.length - 1];
+            console.assert(lastBlock);
+            if (lastBlock.endRowIndex() < newBlock.startRowIndex()) {
+                existingColumn.push(newBlock);
+                return;
+            }
+        }
+        columns.push([newBlock]);
+    }
+
+    static _createCellsForRow(columns, rowIndex)
+    {
+        const element = ComponentBase.createElement;
+        const link = ComponentBase.createLink;
+
+        const crateEmptyCell = (rowspan) => element('td', {rowspan: rowspan, class: 'stacking-block'}, '');
+
+        const cells = [element('td', {class: 'stacking-block'}, '')];
+        for (const blocksInColumn of columns) {
+            if (!rowIndex && blocksInColumn[0].startRowIndex()) {
+                cells.push(crateEmptyCell(blocksInColumn[0].startRowIndex()));
+                continue;
+            }
+            for (let i = 0; i < blocksInColumn.length; i++) {
+                const block = blocksInColumn[i];
+                if (block.startRowIndex() == rowIndex) {
+                    cells.push(block.createStackingCell());
+                    break;
+                }
+                const rowCount = i + 1 < blocksInColumn.length ? blocksInColumn[i + 1].startRowIndex() : this._rowCount;
+                const remainingRows = rowCount - block.endRowIndex() - 1;
+                if (rowIndex == block.endRowIndex() + 1 && rowIndex < rowCount)
+                    cells.push(crateEmptyCell(remainingRows));
+            }
+        }
+
+        return cells;
+    }
+
     static htmlTemplate()
     {
         return `<section class="analysis-view">${ResultsTable.htmlTemplate()}</section>`;
@@ -371,148 +443,59 @@ AnalysisResultsViewer.CommitSetInTestGroup = class {
 }
 
 AnalysisResultsViewer.TestGroupStackingBlock = class {
-    constructor(testGroup, className, callback)
+    constructor(testGroup, analysisResultsView, groupToCellMap, callback)
     {
         this._testGroup = testGroup;
+        this._analysisResultsView = analysisResultsView;
         this._commitSetIndexRowIndexMap = [];
-        this._className = className;
-        this._label = null;
-        this._title = null;
-        this._status = null;
+        this._groupToCellMap = groupToCellMap;
         this._callback = callback;
     }
 
     addRowIndex(commitSetInTestGroup, rowIndex)
     {
         console.assert(commitSetInTestGroup instanceof AnalysisResultsViewer.CommitSetInTestGroup);
-        this._commitSetIndexRowIndexMap.push({commitSet: commitSetInTestGroup.commitSet(), rowIndex: rowIndex});
+        this._commitSetIndexRowIndexMap.push({commitSet: commitSetInTestGroup.commitSet(), rowIndex});
     }
 
     testGroup() { return this._testGroup; }
 
     createStackingCell()
     {
-        this._computeTestGroupStatus();
+        const {label, title, status} = this._computeTestGroupStatus();
 
-        return ComponentBase.createElement('td', {
+        const cell = ComponentBase.createElement('td', {
             rowspan: this.endRowIndex() - this.startRowIndex() + 1,
-            title: this._title,
-            class: 'stacking-block ' + this._className + ' ' + this._status,
+            title,
+            class: 'stacking-block ' + status,
             onclick: this._callback,
-        }, ComponentBase.createLink(this._label, this._title, this._callback));
+        }, ComponentBase.createLink(label, title, this._callback));
+
+        this._groupToCellMap.set(this._testGroup, cell);
+
+        return cell;
     }
 
     isComplete() { return this._commitSetIndexRowIndexMap.length >= 2; }
 
     startRowIndex() { return this._commitSetIndexRowIndexMap[0].rowIndex; }
     endRowIndex() { return this._commitSetIndexRowIndexMap[this._commitSetIndexRowIndexMap.length - 1].rowIndex; }
-    isThin()
+
+    _valuesForCommitSet(testGroup, commitSet)
     {
-        this._computeTestGroupStatus();
-        return this._status == 'failed';
+        return testGroup.requestsForCommitSet(commitSet).map((request) => {
+            return this._analysisResultsView.resultForBuildId(request.buildId());
+        }).filter((result) => !!result).map((result) => result.value);
     }
 
     _computeTestGroupStatus()
     {
-        if (this._status || !this.isComplete())
-            return;
-
+        if (!this.isComplete())
+            return {label: null, title: null, status: null};
         console.assert(this._commitSetIndexRowIndexMap.length <= 2); // FIXME: Support having more root sets.
-
-        var result = this._testGroup.compareTestResults(
-            this._commitSetIndexRowIndexMap[0].commitSet, this._commitSetIndexRowIndexMap[1].commitSet);
-
-        this._label = result.label;
-        this._title = result.fullLabel;
-        this._status = result.status;
-    }
-}
-
-AnalysisResultsViewer.TestGroupStackingGrid = class {
-    constructor(rowCount)
-    {
-        this._blocks = [];
-        this._columns = null;
-        this._rowCount = rowCount;
-    }
-
-    insertBlockToColumn(newBlock)
-    {
-        console.assert(newBlock instanceof AnalysisResultsViewer.TestGroupStackingBlock);
-        for (var i = this._blocks.length - 1; i >= 0; i--) {
-            var currentBlock = this._blocks[i];
-            if (currentBlock.startRowIndex() == newBlock.startRowIndex()
-                && currentBlock.endRowIndex() == newBlock.endRowIndex()) {
-                this._blocks.splice(i + 1, 0, newBlock);
-                return;
-            }
-        }
-        this._blocks.push(newBlock);
-    }
-
-    layout()
-    {
-        this._columns = [];
-        for (var block of this._blocks)
-            this._layoutBlock(block);
-    }
-
-    _layoutBlock(newBlock)
-    {
-        for (var columnIndex = 0; columnIndex < this._columns.length; columnIndex++) {
-            var existingColumn = this._columns[columnIndex];
-            if (newBlock.isThin() != existingColumn[0].isThin())
-                continue;
-
-            for (var i = 0; i < existingColumn.length; i++) {
-                var currentBlock = existingColumn[i];
-                if ((!i || existingColumn[i - 1].endRowIndex() < newBlock.startRowIndex())
-                    && newBlock.endRowIndex() < currentBlock.startRowIndex()) {
-                    existingColumn.splice(i, 0, newBlock);
-                    return;
-                }
-            }
-
-            var lastBlock = existingColumn[existingColumn.length - 1];
-            if (lastBlock.endRowIndex() < newBlock.startRowIndex()) {
-                existingColumn.push(newBlock);
-                return;
-            }
-        }
-        this._columns.push([newBlock]);
+        const startValues = this._valuesForCommitSet(this._testGroup, this._commitSetIndexRowIndexMap[0].commitSet);
+        const endValues = this._valuesForCommitSet(this._testGroup, this._commitSetIndexRowIndexMap[1].commitSet);
+        const result = this._testGroup.compareTestResults(startValues, endValues);
+        return {label: result.label, title: result.fullLabel, status: result.status};
     }
-
-    createCellsForRow(rowIndex)
-    {
-        var element = ComponentBase.createElement;
-        var link = ComponentBase.createLink;
-
-        var cells = [element('td', {class: 'stacking-block'}, '')];
-        for (var columnIndex = 0; columnIndex < this._columns.length; columnIndex++) {
-            var blocksInColumn = this._columns[columnIndex];
-            if (!rowIndex && blocksInColumn[0].startRowIndex()) {
-                cells.push(this._createEmptyStackingCell(blocksInColumn[0].startRowIndex()));
-                continue;
-            }
-            for (var i = 0; i < blocksInColumn.length; i++) {
-                var block = blocksInColumn[i];
-                if (block.startRowIndex() == rowIndex) {
-                    cells.push(block.createStackingCell());
-                    break;
-                }
-                var rowCount = i + 1 < blocksInColumn.length ? blocksInColumn[i + 1].startRowIndex() : this._rowCount;
-                var remainingRows = rowCount - block.endRowIndex() - 1;
-                if (rowIndex == block.endRowIndex() + 1 && rowIndex < rowCount)
-                    cells.push(this._createEmptyStackingCell(remainingRows));
-            }
-        }
-
-        return cells;
-    }
-
-    _createEmptyStackingCell(rowspan, content)
-    {
-        return ComponentBase.createElement('td', {rowspan: rowspan, class: 'stacking-block'}, '');
-    }
-
 }
diff --git a/Websites/perf.webkit.org/public/v3/components/analysis-task-bug-list.js b/Websites/perf.webkit.org/public/v3/components/analysis-task-bug-list.js
new file mode 100644 (file)
index 0000000..1bf043c
--- /dev/null
@@ -0,0 +1,55 @@
+
+class AnalysisTaskBugList extends ComponentBase {
+
+    constructor()
+    {
+        super('analysis-task-bug-list');
+        this._task = null;
+    }
+
+    setTask(task)
+    {
+        console.assert(task == null || task instanceof AnalysisTask);
+        this._task = task;
+        this.enqueueToRender();
+    }
+
+    didConstructShadowTree()
+    {
+        this.part('bug-list').setKindList(BugTracker.all());
+        this.part('bug-list').listenToAction('addItem', (tracker, bugNumber) => this._associateBug(tracker, bugNumber));
+    }
+
+    render()
+    {
+        const bugList = this._task ? this._task.bugs().map((bug) => {
+            return new MutableListItem(bug.bugTracker(), bug.label(), bug.title(), bug.url(),
+                'Dissociate this bug', () => this._dissociateBug(bug));
+        }) : [];
+        this.part('bug-list').setList(bugList);
+    }
+
+    _associateBug(tracker, bugNumber)
+    {
+        console.assert(tracker instanceof BugTracker);
+        bugNumber = parseInt(bugNumber);
+
+        return this._task.associateBug(tracker, bugNumber).then(() => this.enqueueToRender(), (error) => {
+            this.enqueueToRender();
+            alert('Failed to associate the bug: ' + error);
+        });
+    }
+
+    _dissociateBug(bug)
+    {
+        return this._task.dissociateBug(bug).then(() => this.enqueueToRender(), (error) => {
+            this.enqueueToRender();
+            alert('Failed to dissociate the bug: ' + error);
+        });
+    }
+
+    static htmlTemplate() { return `<mutable-list-view id="bug-list"></mutable-list-view>`; }
+
+}
+
+ComponentBase.defineElement('analysis-task-bug-list', AnalysisTaskBugList);
index ad37968..95662c4 100644 (file)
@@ -314,10 +314,12 @@ class ChartPaneBase extends ComponentBase {
                     </div>
                 </div>
             </section>
+            ${this.paneFooterTemplate()}
         `;
     }
 
     static paneHeaderTemplate() { return ''; }
+    static paneFooterTemplate() { return ''; }
 
     static cssTemplate()
     {
index 1df1551..4693801 100644 (file)
@@ -5,48 +5,55 @@ class CustomizableTestGroupForm extends TestGroupForm {
     {
         super('customizable-test-group-form');
         this._commitSetMap = null;
-        this._renderedRepositorylist = null;
-        this._customized = false;
-        this._nameControl = this.content().querySelector('.name');
-        this._nameControl.oninput = () => { this.enqueueToRender(); }
-        this.content().querySelector('a').onclick = this._customize.bind(this);
+        this._name = null;
+        this._isCustomized = false;
+        this._revisionEditorMap = {};
+
+        this._renderCustomRevisionTableLazily = new LazilyEvaluatedFunction(this._renderCustomRevisionTable.bind(this));
     }
 
     setCommitSetMap(map)
     {
         this._commitSetMap = map;
-        this._customized = false;
+        this._isCustomized = false;
+        this.enqueueToRender();
     }
 
-    _submitted()
+    startTesting()
     {
-        if (this._startCallback)
-            this._startCallback(this._nameControl.value, this._repetitionCount, this._computeCommitSetMap());
+        this.dispatchAction('startTesting', this._repetitionCount, this._name, this._computeCommitSetMap());
     }
 
-    _customize(event)
+    didConstructShadowTree()
     {
-        event.preventDefault();
-        this._customized = true;
-        this.enqueueToRender();
+        super.didConstructShadowTree();
+
+        const nameControl = this.content('name');
+        nameControl.oninput = () => {
+            this._name = nameControl.value;
+            this.enqueueToRender();
+        }
+
+        this.content('customize-link').onclick = this.createEventHandler(() => {
+            this._isCustomized = true;
+            this.enqueueToRender();
+        });
     }
 
     _computeCommitSetMap()
     {
         console.assert(this._commitSetMap);
-        if (!this._customized)
+        if (!this._isCustomized)
             return this._commitSetMap;
 
-        console.assert(this._renderedRepositorylist);
-        var map = {};
-        for (var label in this._commitSetMap) {
-            var customCommitSet = new CustomCommitSet;
-            for (var repository of this._renderedRepositorylist) {
-                var className = CustomizableTestGroupForm._classForLabelAndRepository(label, repository);
-                var revision = this.content().querySelector('.' + className).value;
-                console.assert(revision);
-                if (revision)
-                    customCommitSet.setRevisionForRepository(repository, revision);
+        const map = {};
+        for (const label in this._commitSetMap) {
+            const originalCommitSet = this._commitSetMap;
+            const customCommitSet = new CustomCommitSet;
+            for (let repository of this._commitSetMap[label].repositories()) {
+                const revisionEditor = this._revisionEditorMap[label].get(repository);
+                console.assert(revisionEditor);
+                customCommitSet.setRevisionForRepository(repository, revisionEditor.value);
             }
             map[label] = customCommitSet;
         }
@@ -56,58 +63,61 @@ class CustomizableTestGroupForm extends TestGroupForm {
     render()
     {
         super.render();
-        var map = this._commitSetMap;
 
-        this.content().querySelector('button').disabled = !(map && this._nameControl.value);
-        this.content().querySelector('.customize-link').style.display = !map ? 'none' : null;
+        this.content('start-button').disabled = !(this._commitSetMap && this._name);
+        this.content('customize-link-container').style.display = !this._commitSetMap ? 'none' : null;
 
-        if (!this._customized) {
-            this.renderReplace(this.content().querySelector('.custom-table-container'), []);
-            return;
+        this._renderCustomRevisionTableLazily.evaluate(this._commitSetMap, this._isCustomized);
+    }
+
+    _renderCustomRevisionTable(commitSetMap, isCustomized)
+    {
+        if (!commitSetMap || !isCustomized) {
+            this.renderReplace(this.content('custom-table'), []);
+            return null;
         }
-        console.assert(map);
 
-        var repositorySet = new Set;
-        var commitSetLabels = [];
-        for (var label in map) {
-            for (var repository of map[label].repositories())
+        const repositorySet = new Set;
+        const commitSetLabels = [];
+        this._revisionEditorMap = {};
+        for (const label in commitSetMap) {
+            for (const repository of commitSetMap[label].repositories())
                 repositorySet.add(repository);
             commitSetLabels.push(label);
+            this._revisionEditorMap[label] = new Map;
         }
 
-        this._renderedRepositorylist = Repository.sortByNamePreferringOnesWithURL(Array.from(repositorySet.values()));
-
-        var element = ComponentBase.createElement;
-        this.renderReplace(this.content().querySelector('.custom-table-container'),
-            element('table', {class: 'custom-table'}, [
-                element('thead',
-                    element('tr',
-                        [element('td', 'Repository'), commitSetLabels.map(function (label) {
-                            return element('td', {colspan: commitSetLabels.length + 1}, label);
-                        })])),
-                element('tbody',
-                    this._renderedRepositorylist.map(function (repository) {
-                        var cells = [element('th', repository.label())];
-                        for (var label in map)
-                            cells.push(CustomizableTestGroupForm._constructRevisionRadioButtons(map, repository, label));
-                        return element('tr', cells);
-                    }))]));
+        const repositoryList = Repository.sortByNamePreferringOnesWithURL(Array.from(repositorySet.values()));
+        const element = ComponentBase.createElement;
+        this.renderReplace(this.content('custom-table'), [
+            element('thead',
+                element('tr',
+                    [element('td', 'Repository'), commitSetLabels.map((label) => element('td', {colspan: commitSetLabels.length + 1}, label))])),
+            element('tbody',
+                repositoryList.map((repository) => {
+                    const cells = [element('th', repository.label())];
+                    for (const label in commitSetMap)
+                        cells.push(this._constructRevisionRadioButtons(commitSetMap, repository, label));
+                    return element('tr', cells);
+                }))]);
+
+        return repositoryList;
     }
 
-    static _classForLabelAndRepository(label, repository) { return label + '-' + repository.id(); }
-
-    static _constructRevisionRadioButtons(commitSetMap, repository, rowLabel)
+    _constructRevisionRadioButtons(commitSetMap, repository, rowLabel)
     {
-        var className = this._classForLabelAndRepository(rowLabel, repository);
-        var groupName = className + '-group';
-        var element = ComponentBase.createElement;
-        var revisionEditor = element('input', {class: className});
+        const element = ComponentBase.createElement;
+        const revisionEditor = element('input');
+
+        this._revisionEditorMap[rowLabel].set(repository, revisionEditor);
 
         const nodes = [];
         for (let labelToChoose in commitSetMap) {
             const commit = commitSetMap[labelToChoose].commitForRepository(repository);
             const checked = labelToChoose == rowLabel;
-            const radioButton = this._createRadioButton(groupName, revisionEditor, commit, checked);
+            const radioButton = element('input', {type: 'radio', name: `${rowLabel}-${repository.id()}-radio`, checked,
+                onchange: () => { revisionEditor.value = commit ? commit.revision() : ''; }});
+
             if (checked)
                 revisionEditor.value = commit ? commit.revision() : '';
             nodes.push(element('td', element('label', [radioButton, labelToChoose])));
@@ -117,36 +127,21 @@ class CustomizableTestGroupForm extends TestGroupForm {
         return nodes;
     }
 
-    static _createRadioButton(groupName, revisionEditor, commit, checked)
-    {
-        var button = ComponentBase.createElement('input', {
-            type: 'radio',
-            name: groupName + '-radio',
-            onchange: function () { revisionEditor.value = commit ? commit.revision() : ''; },
-        });
-        if (checked) // FIXME: createElement should be able to set boolean attribute properly.
-            button.checked = true;
-        return button;
-    }
-
     static cssTemplate()
     {
         return `
-            .customize-link {
+            #customize-link-container,
+            #customize-link {
                 color: #333;
             }
 
-            .customize-link a {
-                color: inherit;
-            }
-
-            .custom-table {
+            #custom-table:not(:empty) {
                 margin: 1rem 0;
             }
 
-            .custom-table,
-            .custom-table td,
-            .custom-table th {
+            #custom-table,
+            #custom-table td,
+            #custom-table th {
                 font-weight: inherit;
                 border-collapse: collapse;
                 border-top: solid 1px #ddd;
@@ -155,8 +150,8 @@ class CustomizableTestGroupForm extends TestGroupForm {
                 font-size: 0.9rem;
             }
 
-            .custom-table thead td,
-            .custom-table th {
+            #custom-table thead td,
+            #custom-table th {
                 text-align: center;
             }
             `;
@@ -165,10 +160,10 @@ class CustomizableTestGroupForm extends TestGroupForm {
     static formContent()
     {
         return `
-            <input class="name" type="text" placeholder="Test group name">
+            <input id="name" type="text" placeholder="Test group name">
             ${super.formContent()}
-            <span class="customize-link">(<a href="">Customize</a>)</span>
-            <div class="custom-table-container"></div>
+            <span id="customize-link-container">(<a id="customize-link" href="#">Customize</a>)</span>
+            <table id="custom-table"></table>
         `;
     }
 }
index 572d0b1..9d6ceab 100644 (file)
@@ -1,5 +1,4 @@
 
-
 class MutableListView extends ComponentBase {
 
     constructor()
@@ -7,14 +6,21 @@ class MutableListView extends ComponentBase {
         super('mutable-list-view');
         this._list = [];
         this._kindList = [];
-        this._addCallback = null;
         this._kindMap = new Map;
         this.content().querySelector('form').onsubmit = this._submitted.bind(this);
     }
 
-    setList(list) { this._list = list; }
-    setKindList(list) { this._kindList = list; }
-    setAddCallback(callback) { this._addCallback = callback; }
+    setList(list)
+    {
+        this._list = list;
+        this.enqueueToRender();
+    }
+
+    setKindList(list)
+    {
+        this._kindList = list;
+        this.enqueueToRender();
+    }
 
     render()
     {
@@ -37,8 +43,9 @@ class MutableListView extends ComponentBase {
     _submitted(event)
     {
         event.preventDefault();
-        if (this._addCallback)
-            this._addCallback(this._kindMap.get(this.content().querySelector('.kind').value), this.content().querySelector('.value').value);
+        const kind = this._kindMap.get(this.content().querySelector('.kind').value);
+        const item = this.content().querySelector('.value').value;
+        this.dispatchAction('addItem', kind, item);
     }
 
     static cssTemplate()
@@ -91,13 +98,15 @@ class MutableListItem {
 
     content()
     {
-        var link = ComponentBase.createLink;
+        const link = ComponentBase.createLink;
+        const closeButton = new CloseButton;
+        closeButton.listenToAction('activate', this._removalLink);
         return ComponentBase.createElement('li', [
             this._kind.label(),
             ' ',
             link(this._value, this._valueTitle, this._valueLink),
             ' ',
-            link(new CloseButton, this._removalTitle, this._removalLink)]);
+            link(closeButton, this._removalTitle, this._removalLink)]);
     }
 }
 
index f6decfe..1d079a3 100644 (file)
@@ -3,60 +3,32 @@ class ResultsTable extends ComponentBase {
     {
         super(name);
         this._repositoryList = [];
-        this._valueFormatter = null;
-        this._rangeSelectorLabels = null;
-        this._rangeSelectorCallback = null;
-        this._selectedRange = {};
+        this._analysisResultsView = null;
     }
 
-    setValueFormatter(valueFormatter) { this._valueFormatter = valueFormatter; }
-    setRangeSelectorLabels(labels) { this._rangeSelectorLabels = labels; }
-    setRangeSelectorCallback(callback) { this._rangeSelectorCallback = callback; }
-    selectedRange() { return this._selectedRange; }
-
-    _rangeSelectorClicked(label, row)
+    setAnalysisResultsView(analysisResultsView)
     {
-        this._selectedRange[label] = row;
-        if (this._rangeSelectorCallback)
-            this._rangeSelectorCallback();
+        console.assert(analysisResultsView instanceof AnalysisResultsView);
+        this._analysisResultsView = analysisResultsView;
+        this.enqueueToRender();
     }
 
-    render()
+    renderTable(valueFormatter, rowGroups, headingLabel, buildHeaders = (headers) => headers, buildColumns = (columns, row, rowIndex) => columns)
     {
-        if (!this._valueFormatter)
-            return;
-
-        Instrumentation.startMeasuringTime('ResultsTable', 'render');
-
-        var rowGroups = this.buildRowGroups();
-
-        var extraRepositories = [];
-        var repositoryList = this._computeRepositoryList(rowGroups, extraRepositories);
-
-        this._selectedRange = {};
-
-        var barGraphGroup = new BarGraphGroup(this._valueFormatter);
-        var element = ComponentBase.createElement;
-        var self = this;
-        var hasGroupHeading = false;
-        var tableBodies = rowGroups.map(function (group) {
-            var groupHeading = group.heading;
-            var revisionSupressionCount = {};
-            hasGroupHeading = !!groupHeading;
-
-            return element('tbody', group.rows.map(function (row, rowIndex) {
-                var cells = [];
-
-                if (self._rangeSelectorLabels) {
-                    for (var label of self._rangeSelectorLabels) {
-                        var content = '';
-                        if (row.commitSet()) {
-                            content = element('input',
-                                {type: 'radio', name: label, onchange: self._rangeSelectorClicked.bind(self, label, row)});
-                        }
-                        cells.push(element('td', content));
-                    }
-                }
+        Instrumentation.startMeasuringTime('ResultsTable', 'renderTable');
+
+        const [repositoryList, constantCommits] = this._computeRepositoryList(rowGroups);
+
+        const barGraphGroup = new BarGraphGroup(valueFormatter);
+        const element = ComponentBase.createElement;
+        let hasGroupHeading = false;
+        const tableBodies = rowGroups.map((group) => {
+            const groupHeading = group.heading;
+            const revisionSupressionCount = {};
+            hasGroupHeading = hasGroupHeading || groupHeading;
+
+            return element('tbody', group.rows.map((row, rowIndex) => {
+                const cells = [];
 
                 if (groupHeading !== undefined && !rowIndex)
                     cells.push(element('th', {rowspan: group.rows.length}, groupHeading));
@@ -66,39 +38,38 @@ class ResultsTable extends ComponentBase {
                     cells.push(element('td', {class: 'whole-row-label', colspan: repositoryList.length + 1}, row.labelForWholeRow()));
                 else {
                     cells.push(element('td', row.resultContent(barGraphGroup)));
-                    cells.push(self._createRevisionListCells(repositoryList, revisionSupressionCount, group, row.commitSet(), rowIndex));
+                    cells.push(this._createRevisionListCells(repositoryList, revisionSupressionCount, group, row.commitSet(), rowIndex));
                 }
 
-                return element('tr', [cells, row.additionalColumns()]);
+                return element('tr', buildColumns(cells, row, rowIndex));
             }));
         });
 
         this.renderReplace(this.content().querySelector('table'), [
             element('thead', [
-                this._rangeSelectorLabels ? this._rangeSelectorLabels.map(function (label) { return element('th', label) }) : [],
-                this.heading(),
-                element('th', 'Result'),
-                repositoryList.map(function (repository) { return element('th', repository.label()); }),
-                this.additionalHeading(),
+                buildHeaders([
+                    ComponentBase.createElement('th', {colspan: hasGroupHeading ? 2 : 1}, headingLabel),
+                    element('th', 'Result'),
+                    repositoryList.map((repository) => element('th', repository.label())),
+                ]),
             ]),
             tableBodies,
         ]);
 
-        this.renderReplace(this.content().querySelector('.results-table-extra-repositories'),
-            extraRepositories.map(function (commit) { return element('li', commit.title()); }));
+        this.renderReplace(this.content('constant-commits'), constantCommits.map((commit) => element('li', commit.title())));
 
         barGraphGroup.updateGroupRendering();
 
-        Instrumentation.endMeasuringTime('ResultsTable', 'render');
+        Instrumentation.endMeasuringTime('ResultsTable', 'renderTable');
     }
 
     _createRevisionListCells(repositoryList, revisionSupressionCount, testGroup, commitSet, rowIndex)
     {
-        var element = ComponentBase.createElement;
-        var link = ComponentBase.createLink;
-        var cells = [];
-        for (var repository of repositoryList) {
-            var commit = commitSet ? commitSet.commitForRepository(repository) : null;
+        const element = ComponentBase.createElement;
+        const link = ComponentBase.createLink;
+        const cells = [];
+        for (const repository of repositoryList) {
+            const commit = commitSet ? commitSet.commitForRepository(repository) : null;
 
             if (revisionSupressionCount[repository.id()]) {
                 revisionSupressionCount[repository.id()]--;
@@ -112,8 +83,8 @@ class ResultsTable extends ComponentBase {
                     break;
                 succeedingRowIndex++;
             }
-            var rowSpan = succeedingRowIndex - rowIndex;
-            var attributes = {class: 'revision'};
+            const rowSpan = succeedingRowIndex - rowIndex;
+            const attributes = {class: 'revision'};
             if (rowSpan > 1) {
                 revisionSupressionCount[repository.id()] = rowSpan - 1;
                 attributes['rowspan'] = rowSpan;                       
@@ -121,9 +92,9 @@ class ResultsTable extends ComponentBase {
             if (rowIndex + rowSpan >= testGroup.rows.length)
                 attributes['class'] += ' lastRevision';
 
-            var content = 'Missing';
+            let content = 'Missing';
             if (commit) {
-                var url = commit.url();
+                const url = commit.url();
                 content = url ? link(commit.label(), url) : commit.label();
             }
 
@@ -132,22 +103,9 @@ class ResultsTable extends ComponentBase {
         return cells;
     }
 
-    heading() { throw 'NotImplemented'; }
-    additionalHeading() { return []; }
-    buildRowGroups() { throw 'NotImplemented'; }
-
-    _computeRepositoryList(rowGroups, extraRepositories)
+    _computeRepositoryList(rowGroups)
     {
-        var allRepositories = Repository.all().sort(function (a, b) {
-            if (a.hasUrlForRevision() == b.hasUrlForRevision()) {
-                if (a.name() > b.name())
-                    return 1;
-                if (a.name() < b.name())
-                    return -1;
-                return 0;
-            }
-            return a.hasUrlForRevision() ? -1 /* a < b */ : 1; // a > b
-        });
+        const allRepositories = Repository.sortByNamePreferringOnesWithURL(Repository.all());
         const commitSets = [];
         for (let group of rowGroups) {
             for (let row of group.rows) {
@@ -159,20 +117,21 @@ class ResultsTable extends ComponentBase {
         if (!commitSets.length)
             return [];
 
-        const repositoryPresenceMap = {};
+        const changedRepositorySet = new Set;
+        const constantCommits = new Set;
         for (let repository of allRepositories) {
             const someCommit = commitSets[0].commitForRepository(repository);
             if (CommitSet.containsMultipleCommitsForRepository(commitSets, repository))
-                repositoryPresenceMap[repository.id()] = true;
+                changedRepositorySet.add(repository);
             else if (someCommit)
-                extraRepositories.push(someCommit);
+                constantCommits.add(someCommit);
         }
-        return allRepositories.filter(function (repository) { return repositoryPresenceMap[repository.id()]; });
+        return [allRepositories.filter((repository) => changedRepositorySet.has(repository)), [...constantCommits]];
     }
 
     static htmlTemplate()
     {
-        return `<table class="results-table"></table><ul class="results-table-extra-repositories"></ul>`;
+        return `<table class="results-table"></table><ul id="constant-commits"></ul>`;
     }
 
     static cssTemplate()
@@ -248,22 +207,22 @@ class ResultsTable extends ComponentBase {
                 height: 1.2rem;
             }
 
-            .results-table-extra-repositories {
+            #constant-commits {
                 list-style: none;
                 margin: 0;
                 padding: 0.5rem 0 0 0.5rem;
                 font-size: 0.8rem;
             }
 
-            .results-table-extra-repositories:empty {
+            #constant-commits:empty {
                 padding: 0;
             }
 
-            .results-table-extra-repositories li {
+            #constant-commits li {
                 display: inline;
             }
 
-            .results-table-extra-repositories li:not(:last-child):after {
+            #constant-commits li:not(:last-child):after {
                 content: ', ';
             }
         `;
@@ -278,7 +237,6 @@ class ResultsTableRow {
         this._link = null;
         this._label = '-';
         this._commitSet = commitSet;
-        this._additionalColumns = [];
         this._labelForWholeRow = null;
     }
 
@@ -294,9 +252,6 @@ class ResultsTableRow {
     setLabelForWholeRow(label) { this._labelForWholeRow = label; }
     labelForWholeRow() { return this._labelForWholeRow; }
 
-    additionalColumns() { return this._additionalColumns; }
-    setAdditionalColumns(additionalColumns) { this._additionalColumns = additionalColumns; }
-
     resultContent(barGraphGroup)
     {
         var resultContent = this._result ? barGraphGroup.addBar(this._result.value, this._result.interval) : this._label;
index 205f7c1..ac818f7 100644 (file)
@@ -4,52 +4,38 @@ class TestGroupForm extends ComponentBase {
     constructor(name)
     {
         super(name || 'test-group-form');
-        this._startCallback = null;
-        this._label = undefined;
         this._repetitionCount = 4;
-
-        this._nameControl = this.content().querySelector('.name');
-        this._repetitionCountControl = this.content().querySelector('.repetition-count');
-        var self = this;
-        this._repetitionCountControl.onchange = function () {
-            self._repetitionCount = self._repetitionCountControl.value;
-        }
-
-        var boundSubmitted = this._submitted.bind(this);
-        this.content().querySelector('form').onsubmit = function (event) {
-            event.preventDefault();
-            boundSubmitted();
-        }
     }
 
-    setStartCallback(callback) { this._startCallback = callback; }
-    setLabel(label) { this._label = label; }
-    setRepetitionCount(count) { this._repetitionCount = count; }
+    setRepetitionCount(count)
+    {
+        this.content('repetition-count').value = count;
+    }
 
-    render()
+    didConstructShadowTree()
     {
-        var button = this.content().querySelector('button');
-        if (this._label)
-            button.textContent = this._label;
-        this._repetitionCountControl.value = this._repetitionCount;
+        const repetitionCountSelect = this.content('repetition-count');
+        repetitionCountSelect.onchange = () => {
+            this._repetitionCount = repetitionCountSelect.value;
+        }
+        this.content('form').onsubmit = this.createEventHandler(() => this.startTesting());
     }
 
-    _submitted()
+    startTesting()
     {
-        if (this._startCallback)
-            this._startCallback(this._repetitionCount);
+        this.dispatchAction('startTesting', this._repetitionCount);
     }
 
     static htmlTemplate()
     {
-        return `<form><button type="submit">Start A/B testing</button>${this.formContent()}</form>`;
+        return `<form id="form"><button id="start-button" type="submit"><slot>Start A/B testing</slot></button>${this.formContent()}</form>`;
     }
 
     static formContent()
     {
         return `
             with
-            <select class="repetition-count">
+            <select id="repetition-count">
                 <option>1</option>
                 <option>2</option>
                 <option>3</option>
index 74a46c0..9d3d28b 100644 (file)
@@ -4,73 +4,56 @@ class TestGroupResultsTable extends ResultsTable {
     {
         super('test-group-results-table');
         this._testGroup = null;
-        this._renderedTestGroup = null;
+        this._renderTestGroupLazily = new LazilyEvaluatedFunction(this._renderTestGroup.bind(this));
     }
 
-    didUpdateResults() { this._renderedTestGroup = null; }
     setTestGroup(testGroup)
     {
         this._testGroup = testGroup;
-        this._renderedTestGroup = null;
+        this.enqueueToRender();
     }
 
-    heading()
+    render()
     {
-        return ComponentBase.createElement('th', {colspan: 2}, 'Configuration');
+        super.render();
+        this._renderTestGroupLazily.evaluate(this._testGroup, this._analysisResultsView);
     }
 
-    render()
+    _renderTestGroup(testGroup, analysisResults)
     {
-        if (this._renderedTestGroup == this._testGroup)
+        if (!analysisResults)
             return;
-        this._renderedTestGroup = this._testGroup;
-        super.render();
+        const rowGroups = this._buildRowGroups();
+        this.renderTable(
+            analysisResults.metric().makeFormatter(4),
+            rowGroups,
+            'Configuration');
     }
 
-    buildRowGroups()
+    _buildRowGroups()
     {
         const testGroup = this._testGroup;
         if (!testGroup)
             return [];
 
         const commitSets = this._testGroup.requestedCommitSets();
-        const groups = commitSets.map(function (commitSet) {
-            const rows = [new ResultsTableRow('Mean', commitSet)];
-            var results = [];
-
-            for (var request of testGroup.requestsForCommitSet(commitSet)) {
-                var result = request.result();
-                // Call result.commitSet() for each result since the set of revisions used in testing maybe different from requested ones.
-                var row = new ResultsTableRow(1 + +request.order(), result ? result.commitSet() : null);
-                rows.push(row);
-                if (result) {
-                    row.setLink(result.build().url(), result.build().label());
-                    row.setResult(result);
-                    results.push(result);
-                } else
-                    row.setLink(request.statusUrl(), request.statusLabel());
-            }
-
-            var aggregatedResult = MeasurementAdaptor.aggregateAnalysisResults(results);
-            if (!isNaN(aggregatedResult.value))
-                rows[0].setResult(aggregatedResult);
-
-            return {heading: testGroup.labelForCommitSet(commitSet), rows};
+        const resultsByCommitSet = new Map;
+        const groups = commitSets.map((commitSet) => {
+            const group = this._buildRowGroupForCommitSet(testGroup, commitSet, resultsByCommitSet);
+            resultsByCommitSet.set(commitSet, group.results);
+            return group;
         });
 
         const comparisonRows = [];
-        for (let i = 0; i < commitSets.length; i++) {
+        for (let i = 0; i < commitSets.length - 1; i++) {
+            const startCommit = commitSets[i];
             for (let j = i + 1; j < commitSets.length; j++) {
-                const startConfig = testGroup.labelForCommitSet(commitSets[i]);
-                const endConfig = testGroup.labelForCommitSet(commitSets[j]);
-
-                const result = this._testGroup.compareTestResults(commitSets[i], commitSets[j]);
-                if (result.changeType == null)
+                const endCommit = commitSets[j];
+                const startResults = resultsByCommitSet.get(startCommit) || [];
+                const endResults = resultsByCommitSet.get(endCommit) || [];
+                const row = this._buildComparisonRow(testGroup, startCommit, startResults, endCommit, endResults);
+                if (!row)
                     continue;
-
-                var row = new ResultsTableRow(`${startConfig} to ${endConfig}`, null);
-                var element = ComponentBase.createElement;
-                row.setLabelForWholeRow(element('span', {class: 'results-label ' + result.status}, result.fullLabel));
                 comparisonRows.push(row);
             }
         }
@@ -80,6 +63,48 @@ class TestGroupResultsTable extends ResultsTable {
         return groups;
     }
 
+    _buildRowGroupForCommitSet(testGroup, commitSet)
+    {
+        const rows = [new ResultsTableRow('Mean', commitSet)];
+        const results = [];
+
+        for (const request of testGroup.requestsForCommitSet(commitSet)) {
+            const result = this._analysisResultsView.resultForBuildId(request.buildId());
+            // Call result.commitSet() for each result since the set of revisions used in testing maybe different from requested ones.
+            const row = new ResultsTableRow(1 + +request.order(), result ? result.commitSet() : null);
+            rows.push(row);
+            if (result) {
+                row.setLink(result.build().url(), result.build().label());
+                row.setResult(result);
+                results.push(result);
+            } else
+                row.setLink(request.statusUrl(), request.statusLabel());
+        }
+
+        const aggregatedResult = MeasurementAdaptor.aggregateAnalysisResults(results);
+        if (!isNaN(aggregatedResult.value))
+            rows[0].setResult(aggregatedResult);
+
+        return {heading: testGroup.labelForCommitSet(commitSet), rows, results};
+    }
+
+    _buildComparisonRow(testGroup, startCommit, startResults, endCommit, endResults)
+    {
+        const startConfig = testGroup.labelForCommitSet(startCommit);
+        const endConfig = testGroup.labelForCommitSet(endCommit);
+
+        const result = this._testGroup.compareTestResults(
+            startResults.map((result) => result.value), endResults.map((result) => result.value));
+        if (result.changeType == null)
+            return null;
+
+        const row = new ResultsTableRow(`${startConfig} to ${endConfig}`, null);
+        const element = ComponentBase.createElement;
+        row.setLabelForWholeRow(element('span',
+            {class: 'results-label ' + result.status}, `${endConfig} is ${result.fullLabel} than ${startConfig}`));
+        return row;
+    }
+
     static cssTemplate()
     {
         return super.cssTemplate() + `
@@ -89,17 +114,21 @@ class TestGroupResultsTable extends ResultsTable {
                 height: 100%;
             }
 
-            .results-label .failed {
+            th {
+                vertical-align: top;
+            }
+
+            .failed {
                 color: rgb(128, 51, 128);
             }
-            .results-label .unchanged {
+            .unchanged {
                 color: rgb(128, 128, 128);
             }
-            .results-label.worse {
+            .worse {
                 color: rgb(255, 102, 102);
                 font-weight: bold;
             }
-            .results-label.better {
+            .better {
                 color: rgb(102, 102, 255);
                 font-weight: bold;
             }
index a078a9b..20a5d4f 100644 (file)
@@ -89,6 +89,7 @@ Run tools/bundle-v3-scripts to speed up the load time for production.`);
         <script src="components/chart-revision-range.js"></script>
         <script src="components/chart-pane-base.js"></script>
         <script src="components/mutable-list-view.js"></script>
+        <script src="components/analysis-task-bug-list.js"></script>
         <script src="components/ratio-bar-graph.js"></script>
 
         <script src="pages/page.js"></script>
index 234ce36..bb08590 100644 (file)
@@ -2,25 +2,32 @@
 class AnalysisResults {
     constructor()
     {
-        this._buildToMetricsMap = {};
+        this._metricToBuildMap = {};
     }
 
-    find(buildId, metric)
+    find(buildId, metricId)
     {
-        var map = this._buildToMetricsMap[buildId];
+        const map = this._metricToBuildMap[metricId];
         if (!map)
             return null;
-        return map[metric.id()];
+        return map[buildId];
     }
 
     add(measurement)
     {
         console.assert(measurement.configType == 'current');
-        if (!this._buildToMetricsMap[measurement.buildId])
-            this._buildToMetricsMap[measurement.buildId] = {};
-        var map = this._buildToMetricsMap[measurement.buildId];
-        console.assert(!map[measurement.metricId]);
-        map[measurement.metricId] = measurement;
+        const metricId = measurement.metricId;
+        if (!(metricId in this._metricToBuildMap))
+            this._metricToBuildMap[metricId] = {};
+        var map = this._metricToBuildMap[metricId];
+        console.assert(!map[measurement.buildId]);
+        map[measurement.buildId] = measurement;
+    }
+
+    viewForMetric(metric)
+    {
+        console.assert(metric instanceof Metric);
+        return new AnalysisResultsView(this, metric);
     }
 
     static fetch(taskId)
@@ -30,9 +37,9 @@ class AnalysisResults {
 
             Instrumentation.startMeasuringTime('AnalysisResults', 'fetch');
 
-            var adaptor = new MeasurementAdaptor(response['formatMap']);
-            var results = new AnalysisResults;
-            for (var rawMeasurement of response['measurements'])
+            const adaptor = new MeasurementAdaptor(response['formatMap']);
+            const results = new AnalysisResults;
+            for (const rawMeasurement of response['measurements'])
                 results.add(adaptor.applyToAnalysisResults(rawMeasurement));
 
             Instrumentation.endMeasuringTime('AnalysisResults', 'fetch');
@@ -41,3 +48,20 @@ class AnalysisResults {
         });
     }
 }
+
+class AnalysisResultsView {
+    constructor(analysisResults, metric)
+    {
+        console.assert(analysisResults instanceof AnalysisResults);
+        console.assert(metric instanceof Metric);
+        this._results = analysisResults;
+        this._metric = metric;
+    }
+
+    metric() { return this._metric; }
+
+    resultForBuildId(buildId)
+    {
+        return this._results.find(buildId, this._metric.id());
+    }
+}
index 4535602..4f30945 100644 (file)
@@ -108,13 +108,6 @@ class BuildRequest extends DataModelObject {
         return label;
     }
 
-    result() { return this._result; }
-    setResult(result)
-    {
-        this._result = result;
-        this._testGroup.didSetResult(this);
-    }
-
     static fetchForTriggerable(triggerable)
     {
         return RemoteAPI.getJSONWithStatus('/api/build-requests/' + triggerable).then(function (data) {
index c4c9446..2ca2649 100644 (file)
@@ -14,7 +14,6 @@ class TestGroup extends LabeledObject {
         this._repositories = null;
         this._requestedCommitSets = null;
         this._commitSetToLabel = new Map;
-        this._allCommitSets = null;
         console.assert(!object.platform || object.platform instanceof Platform);
         this._platform = object.platform;
     }
@@ -95,11 +94,6 @@ class TestGroup extends LabeledObject {
         this._requestsAreInOrder = true;
     }
 
-    didSetResult(request)
-    {
-        this._allCommitSets = null;
-    }
-
     hasFinished()
     {
         return this._buildRequests.every(function (request) { return request.hasFinished(); });
@@ -115,10 +109,8 @@ class TestGroup extends LabeledObject {
         return this._buildRequests.some(function (request) { return request.isPending(); });
     }
 
-    compareTestResults(commitSetA, commitSetB)
+    compareTestResults(beforeValues, afterValues)
     {
-        const beforeValues = this._valuesForCommitSet(commitSetA);
-        const afterValues = this._valuesForCommitSet(commitSetB);
         const beforeMean = Statistics.sum(beforeValues) / beforeValues.length;
         const afterMean = Statistics.sum(afterValues) / afterValues.length;
 
@@ -160,17 +152,6 @@ class TestGroup extends LabeledObject {
         return result;
     }
 
-    _valuesForCommitSet(commitSet)
-    {
-        const requests = this.requestsForCommitSet(commitSet);
-        const values = [];
-        for (let request of requests) {
-            if (request.result())
-                values.push(request.result().value);
-        }
-        return values;
-    }
-
     updateName(newName)
     {
         var self = this;
index 00ec5ec..81491ca 100644 (file)
@@ -4,37 +4,331 @@ class AnalysisTaskChartPane extends ChartPaneBase {
     {
         super('analysis-task-chart-pane');
         this._page = null;
+        this._showForm = false;
     }
 
     setPage(page) { this._page = page; }
+    setShowForm(show)
+    {
+        this._showForm = show;
+        this.enqueueToRender();
+    }
     router() { return this._page.router(); }
 
     _mainSelectionDidChange(selection, didEndDrag)
     {
         super._mainSelectionDidChange(selection);
         if (didEndDrag)
-            this._page._chartSelectionDidChange();
+            this.enqueueToRender();
     }
 
-    selectedPoints()
+    didConstructShadowTree()
     {
-        return this._mainChart ? this._mainChart.selectedPoints('current') : null;
+        this.part('form').listenToAction('startTesting', (repetitionCount, name, commitSetMap) => {
+            this.dispatchAction('newTestGroup', name, repetitionCount, commitSetMap);
+        });
+    }
+
+    render()
+    {
+        super.render();
+        const points = this._mainChart ? this._mainChart.selectedPoints('current') : null;
+
+        this.content('form').style.display = this._showForm ? null : 'none';
+        if (this._showForm) {
+            const form = this.part('form');
+            form.setCommitSetMap(points && points.length() >= 2 ? {'A': points.firstPoint().commitSet(), 'B': points.lastPoint().commitSet()} : null);
+            form.enqueueToRender();
+        }
+    }
+
+    static paneFooterTemplate() { return '<customizable-test-group-form id="form"></customizable-test-group-form>'; }
+
+    static cssTemplate()
+    {
+        return super.cssTemplate() + `
+            #form {
+                margin: 0.5rem;
+            }
+        `;
     }
 }
 
 ComponentBase.defineElement('analysis-task-chart-pane', AnalysisTaskChartPane);
 
+class AnalysisTaskResultsPane extends ComponentBase {
+    constructor()
+    {
+        super('analysis-task-results-pane');
+        this._showForm = false;
+    }
+
+    setPoints(startPoint, endPoint, metric)
+    {
+        const resultsViewer = this.part('results-viewer');
+        resultsViewer.setPoints(startPoint, endPoint, metric);
+        resultsViewer.enqueueToRender();
+    }
+
+    setTestGroups(testGroups, currentGroup)
+    {
+        this.part('results-viewer').setTestGroups(testGroups, currentGroup);
+        this.enqueueToRender();
+    }
+
+    setAnalysisResultsView(analysisResultsView)
+    {
+        this.part('results-viewer').setAnalysisResultsView(analysisResultsView);
+        this.enqueueToRender();
+    }
+
+    setShowForm(show)
+    {
+        this._showForm = show;
+        this.enqueueToRender();
+    }
+
+    didConstructShadowTree()
+    {
+        const resultsViewer = this.part('results-viewer');
+        resultsViewer.listenToAction('testGroupClick', (testGroup) => this.dispatchAction('showTestGroup', testGroup));
+        resultsViewer.setRangeSelectorLabels(['A', 'B']);
+        resultsViewer.listenToAction('rangeSelectorClick', () => this.enqueueToRender());
+
+        this.part('form').listenToAction('startTesting', (repetitionCount, name, commitSetMap) => {
+            this.dispatchAction('newTestGroup', name, repetitionCount, commitSetMap);
+        });
+    }
+
+    render()
+    {
+        this.part('results-viewer').enqueueToRender();
+
+        this.content('form').style.display = this._showForm ? null : 'none';
+        if (!this._showForm)
+            return;
+
+        const selectedRange = this.part('results-viewer').selectedRange();
+        const firstCommitSet = selectedRange['A'];
+        const secondCommitSet = selectedRange['B'];
+        const form = this.part('form');
+        form.setCommitSetMap(firstCommitSet && secondCommitSet ? {'A': firstCommitSet, 'B': secondCommitSet} : null);
+        form.enqueueToRender();
+    }
+
+    static htmlTemplate()
+    {
+        return `<analysis-results-viewer id="results-viewer"></analysis-results-viewer><customizable-test-group-form id="form"></customizable-test-group-form>`;
+    }
+
+    static cssTemplate()
+    {
+        return `
+            #form {
+                margin: 0.5rem;
+            }
+        `;
+    }
+}
+
+ComponentBase.defineElement('analysis-task-results-pane', AnalysisTaskResultsPane);
+
+class AnalysisTaskTestGroupPane extends ComponentBase {
+
+    constructor()
+    {
+        super('analysis-task-test-group-pane');
+        this._renderTestGroupsLazily = new LazilyEvaluatedFunction(this._renderTestGroups.bind(this));
+        this._renderTestGroupVisibilityLazily = new LazilyEvaluatedFunction(this._renderTestGroupVisibility.bind(this));
+        this._renderTestGroupNamesLazily = new LazilyEvaluatedFunction(this._renderTestGroupNames.bind(this));
+        this._renderCurrentTestGroupLazily = new LazilyEvaluatedFunction(this._renderCurrentTestGroup.bind(this));
+        this._testGroupMap = new Map;
+        this._testGroups = [];
+        this._currentTestGroup = null;
+        this._showHiddenGroups = false;
+    }
+
+    didConstructShadowTree()
+    {
+        this.content('hide-button').onclick = () => this.dispatchAction('toggleTestGroupVisibility', this._currentTestGroup);
+        this.part('retry-form').listenToAction('startTesting', (repetitionCount) => {
+            this.dispatchAction('retryTestGroup', this._currentTestGroup, repetitionCount);
+        });
+    }
+
+    setTestGroups(testGroups, currentTestGroup, showHiddenGroups)
+    {
+        this._testGroups = testGroups;
+        this._currentTestGroup = currentTestGroup;
+        this._showHiddenGroups = showHiddenGroups;
+        this.part('results-table').setTestGroup(currentTestGroup);
+        this.enqueueToRender();
+    }
+
+    setAnalysisResultsView(analysisResultsView)
+    {
+        this.part('results-table').setAnalysisResultsView(analysisResultsView);
+        this.enqueueToRender();
+    }
+
+    render()
+    {
+        this._renderTestGroupsLazily.evaluate(this._showHiddenGroups, ...this._testGroups);
+        this._renderTestGroupVisibilityLazily.evaluate(...this._testGroups.map((group) => group.isHidden() ? 'hidden' : 'visible'));
+        this._renderTestGroupNamesLazily.evaluate(...this._testGroups.map((group) => group.label()));
+        this._renderCurrentTestGroup(this._currentTestGroup);
+        this.part('results-table').enqueueToRender();
+        this.part('retry-form').enqueueToRender();
+    }
+
+    _renderTestGroups(showHiddenGroups, ...testGroups)
+    {
+        const element = ComponentBase.createElement;
+        const link = ComponentBase.createLink;
+
+        this._testGroupMap = new Map;
+        const testGroupItems = testGroups.map((group) => {
+            const text = new EditableText(group.label());
+            text.listenToAction('update', () => this.dispatchAction('renameTestGroup', group, text.editedText()));
+
+            const listItem = element('li', link(text, group.label(), () => this.dispatchAction('showTestGroup', group)));
+
+            this._testGroupMap.set(group, {text, listItem});
+            return listItem;
+        });
+
+        this.renderReplace(this.content('test-group-list'), [testGroupItems,
+            showHiddenGroups ? [] : element('li', {class: 'test-group-list-show-all'}, link('Show hidden tests', () => {
+                this.dispatchAction('showHiddenTestGroups');
+            }))]);
+    }
+
+    _renderTestGroupVisibility(...groupVisibilities)
+    {
+        for (let i = 0; i < groupVisibilities.length; i++)
+            this._testGroupMap.get(this._testGroups[i]).listItem.className = groupVisibilities[i];
+    }
+
+    _renderTestGroupNames(...groupNames)
+    {
+        for (let i = 0; i < groupNames.length; i++)
+            this._testGroupMap.get(this._testGroups[i]).text.setText(groupNames[i]);
+    }
+
+    _renderCurrentTestGroup(currentGroup)
+    {
+        const selected = this.content('test-group-list').querySelector('.selected');
+        if (selected)
+            selected.classList.remove('selected');
+        if (currentGroup)
+            this._testGroupMap.get(currentGroup).listItem.classList.add('selected');
+
+        if (currentGroup)
+            this.part('retry-form').setRepetitionCount(currentGroup.repetitionCount());
+        this.content('retry-form').style.display = currentGroup ? null : 'none';
+
+        const hideButton = this.content('hide-button');
+        hideButton.textContent = currentGroup && currentGroup.isHidden() ? 'Unhide' : 'Hide';
+        hideButton.style.display = currentGroup ? null : 'none';
+
+        this.content('pending-request-cancel-warning').style.display = currentGroup && currentGroup.hasPending() ? null : 'none';
+    }
+
+    static htmlTemplate()
+    {
+        return `
+            <ul id="test-group-list"></ul>
+            <div id="test-group-details">
+                <test-group-results-table id="results-table"></test-group-results-table>
+                <test-group-form id="retry-form">Retry</test-group-form>
+                <button id="hide-button">Hide</button>
+                <span id="pending-request-cancel-warning">(cancels pending requests)</span>
+            </div>`;
+    }
+
+    static cssTemplate()
+    {
+        return `
+            :host {
+                display: flex !important;
+            }
+
+            #test-group-list {
+                margin: 0;
+                padding: 0.2rem 0;
+                list-style: none;
+                border-right: solid 1px #ccc;
+                white-space: nowrap;
+                min-width: 8rem;
+            }
+
+            li {
+                display: block;
+                font-size: 0.9rem;
+            }
+
+            li > a {
+                display: block;
+                color: inherit;
+                text-decoration: none;
+                margin: 0;
+                padding: 0.2rem;
+            }
+
+            li.test-group-list-show-all {
+                font-size: 0.8rem;
+                margin-top: 0.5rem;
+                padding-right: 1rem;
+                text-align: center;
+                color: #999;
+            }
+
+            li.test-group-list-show-all:not(.selected) a:hover {
+                background: inherit;
+            }
+
+            li.selected > a {
+                background: rgba(204, 153, 51, 0.1);
+            }
+
+            li.hidden {
+                color: #999;
+            }
+
+            li:not(.selected) > a:hover {
+                background: #eee;
+            }
+
+            #test-group-details {
+                display: table-cell;
+                margin-bottom: 1rem;
+                padding: 0;
+                margin: 0;
+            }
+
+            #retry-form {
+                display: block;
+                margin: 0.5rem;
+            }
+
+            #hide-button {
+                margin: 0.5rem;
+            }`;
+    }
+}
+
+ComponentBase.defineElement('analysis-task-test-group-pane', AnalysisTaskTestGroupPane);
+
 class AnalysisTaskPage extends PageWithHeading {
     constructor()
     {
         super('Analysis Task');
         this._task = null;
+        this._metric = null;
         this._triggerable = null;
         this._relatedTasks = null;
         this._testGroups = null;
-        this._renderedTestGroups = null;
         this._testGroupLabelMap = new Map;
-        this._renderedCurrentTestGroup = undefined;
         this._analysisResults = null;
         this._measurementSet = null;
         this._startPoint = null;
@@ -43,42 +337,10 @@ class AnalysisTaskPage extends PageWithHeading {
         this._currentTestGroup = null;
         this._filteredTestGroups = null;
         this._showHiddenTestGroups = false;
-        this._selectionWasModifiedByUser = false;
 
-        this._chartPane = this.content().querySelector('analysis-task-chart-pane').component();
-        this._chartPane.setPage(this);
-        this._analysisResultsViewer = this.content().querySelector('analysis-results-viewer').component();
-        this._analysisResultsViewer.setTestGroupCallback(this._showTestGroup.bind(this));
-        this._analysisResultsViewer.setRangeSelectorLabels(['A', 'B']);
-        this._analysisResultsViewer.setRangeSelectorCallback(this._selectedRowInAnalysisResultsViewer.bind(this));
-        this._testGroupResultsTable = this.content().querySelector('test-group-results-table').component();
-
-        this._taskNameLabel = this.content().querySelector('.analysis-task-name editable-text').component();
-        this._taskNameLabel.listenToAction('update', () => this._updateTaskName());
-
-        this.content().querySelector('.change-type-form').onsubmit = this._updateChangeType.bind(this);
-        this._taskStatusControl = this.content().querySelector('.change-type-form select');
-
-        this._bugList = this.content().querySelector('.associated-bugs mutable-list-view').component();
-        this._bugList.setKindList(BugTracker.all());
-        this._bugList.setAddCallback(this._associateBug.bind(this));
-
-        this._causeList = this.content().querySelector('.cause-list mutable-list-view').component();
-        this._causeList.setAddCallback(this._associateCommit.bind(this, 'cause'));
-
-        this._fixList = this.content().querySelector('.fix-list mutable-list-view').component();
-        this._fixList.setAddCallback(this._associateCommit.bind(this, 'fix'));
-
-        this._newTestGroupFormForChart = this.content().querySelector('.overview-chart customizable-test-group-form').component();
-        this._newTestGroupFormForChart.setStartCallback(this._createNewTestGroupFromChart.bind(this));
-
-        this._newTestGroupFormForViewer = this.content().querySelector('.analysis-results-view customizable-test-group-form').component();
-        this._newTestGroupFormForViewer.setStartCallback(this._createNewTestGroupFromViewer.bind(this));
-
-        this._retryForm = this.content().querySelector('.test-group-retry-form test-group-form').component();
-        this._retryForm.setStartCallback(this._retryCurrentTestGroup.bind(this));
-        this._hideButton = this.content().querySelector('.test-group-hide-button');
-        this._hideButton.onclick = this._hideCurrentTestGroup.bind(this);
+        this._renderTaskNameAndStatusLazily = new LazilyEvaluatedFunction(this._renderTaskNameAndStatus.bind(this));
+        this._renderCauseAndFixesLazily = new LazilyEvaluatedFunction(this._renderCauseAndFixes.bind(this));
+        this._renderRelatedTasksLazily = new LazilyEvaluatedFunction(this._renderRelatedTasks.bind(this));
     }
 
     title() { return this._task ? this._task.label() : 'Analysis Task'; }
@@ -105,11 +367,41 @@ class AnalysisTaskPage extends PageWithHeading {
         }
     }
 
+    didConstructShadowTree()
+    {
+        this.part('analysis-task-name').listenToAction('update', () => this._updateTaskName(this.part('analysis-task-name').editedText()));
+
+        this.content('change-type-form').onsubmit = ComponentBase.createEventHandler((event) => this._updateChangeType(event));
+
+        this.part('chart-pane').listenToAction('newTestGroup', this._createTestGroupAfterVerifyingCommitSetList.bind(this));
+
+        const resultsPane = this.part('results-pane');
+        resultsPane.listenToAction('showTestGroup', (testGroup) => this._showTestGroup(testGroup));
+        resultsPane.listenToAction('newTestGroup', this._createTestGroupAfterVerifyingCommitSetList.bind(this));
+
+        const groupPane = this.part('group-pane');
+        groupPane.listenToAction('showTestGroup', (testGroup) => this._showTestGroup(testGroup));
+        groupPane.listenToAction('showHiddenTestGroups', () => this._showAllTestGroups());
+        groupPane.listenToAction('renameTestGroup', (testGroup, newName) => this._updateTestGroupName(testGroup, newName));
+        groupPane.listenToAction('toggleTestGroupVisibility', (testGroup) => this._hideCurrentTestGroup(testGroup));
+        groupPane.listenToAction('retryTestGroup', (testGroup, repetitionCount) => this._retryCurrentTestGroup(testGroup, repetitionCount));
+
+        this.part('cause-list').listenToAction('addItem', (repository, revision) => {
+            this._associateCommit('cause', repository, revision);
+        });
+        this.part('fix-list').listenToAction('addItem', (repository, revision) => {
+            this._associateCommit('fix', repository, revision);
+        });
+    }
+
     _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));
+        AnalysisTask.fetchRelatedTasks(taskId).then((relatedTasks) => {
+            this._relatedTasks = relatedTasks;
+            this.enqueueToRender();
+        });
     }
 
     _didFetchTask(task)
@@ -117,36 +409,30 @@ class AnalysisTaskPage extends PageWithHeading {
         console.assert(!this._task);
 
         this._task = task;
+
         const platform = task.platform();
         const metric = task.metric();
         const lastModified = platform.lastModified(metric);
-
         this._triggerable = Triggerable.findByTestConfiguration(metric.test(), platform);
+        this._metric = metric;
 
         this._measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
         this._measurementSet.fetchBetween(task.startTime(), task.endTime(), this._didFetchMeasurement.bind(this));
 
-        const formatter = metric.makeFormatter(4);
-        this._analysisResultsViewer.setValueFormatter(formatter);
-        this._testGroupResultsTable.setValueFormatter(formatter);
-
-        this._chartPane.configure(platform.id(), metric.id());
-
+        const chart = this.part('chart-pane');
         const domain = ChartsPage.createDomainForAnalysisTask(task);
-        this._chartPane.setOverviewDomain(domain[0], domain[1]);
-        this._chartPane.setMainDomain(domain[0], domain[1]);
+        chart.configure(platform.id(), metric.id());
+        chart.setOverviewDomain(domain[0], domain[1]);
+        chart.setMainDomain(domain[0], domain[1]);
+
+        const bugList = this.part('bug-list');
+        this.part('bug-list').setTask(this._task);
 
         this.enqueueToRender();
 
         return task;
     }
 
-    _didFetchRelatedAnalysisTasks(relatedTasks)
-    {
-        this._relatedTasks = relatedTasks;
-        this.enqueueToRender();
-    }
-
     _didFetchMeasurement()
     {
         console.assert(this._task);
@@ -154,10 +440,11 @@ class AnalysisTaskPage extends PageWithHeading {
         var series = this._measurementSet.fetchedTimeSeries('current', false, false);
         var startPoint = series.findById(this._task.startMeasurementId());
         var endPoint = series.findById(this._task.endMeasurementId());
+
         if (!startPoint || !endPoint || !this._measurementSet.hasFetchedRange(startPoint.time, endPoint.time))
             return;
 
-        this._analysisResultsViewer.setPoints(startPoint, endPoint);
+        this.part('results-pane').setPoints(startPoint, endPoint, this._task.metric());
 
         this._startPoint = startPoint;
         this._endPoint = endPoint;
@@ -181,15 +468,11 @@ class AnalysisTaskPage extends PageWithHeading {
 
     _didUpdateTestGroupHiddenState()
     {
-        this._renderedCurrentTestGroup = null;
-        this._renderedTestGroups = null;
         if (!this._showHiddenTestGroups)
             this._filteredTestGroups = this._testGroups.filter(function (group) { return !group.isHidden(); });
         else
             this._filteredTestGroups = this._testGroups;
-        this._currentTestGroup = this._filteredTestGroups ? this._filteredTestGroups[this._filteredTestGroups.length - 1] : null;
-        this._analysisResultsViewer.setTestGroups(this._filteredTestGroups);
-        this._testGroupResultsTable.setTestGroup(this._currentTestGroup);
+        this._showTestGroup(this._filteredTestGroups ? this._filteredTestGroups[this._filteredTestGroups.length - 1] : null);
     }
 
     _didFetchAnalysisResults(results)
@@ -201,16 +484,12 @@ class AnalysisTaskPage extends PageWithHeading {
 
     _assignTestResultsIfPossible()
     {
-        if (!this._task || !this._testGroups || !this._analysisResults)
+        if (!this._task || !this._metric || !this._testGroups || !this._analysisResults)
             return false;
 
-        for (var group of this._testGroups) {
-            for (var request of group.buildRequests())
-                request.setResult(this._analysisResults.find(request.buildId(), this._task.metric()));
-        }
-
-        this._analysisResultsViewer.didUpdateResults();
-        this._testGroupResultsTable.didUpdateResults();
+        const view = this._analysisResults.viewForMetric(this._metric);
+        this.part('group-pane').setAnalysisResultsView(view);
+        this.part('results-pane').setAnalysisResultsView(view);
 
         return true;
     }
@@ -223,196 +502,87 @@ class AnalysisTaskPage extends PageWithHeading {
 
         this.content().querySelector('.error-message').textContent = this._errorMessage || '';
 
-        this._chartPane.enqueueToRender();
-
-        var element = ComponentBase.createElement;
-        var link = ComponentBase.createLink;
-        if (this._task) {
-            this._taskNameLabel.setText(this._task.name());
-            var platform = this._task.platform();
-            var metric = this._task.metric();
-            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 self = this;
-            this._bugList.setList(this._task.bugs().map(function (bug) {
-                return new MutableListItem(bug.bugTracker(), bug.label(), bug.title(), bug.url(),
-                    'Dissociate this bug', self._dissociateBug.bind(self, bug));
-            }));
-
-            this._causeList.setList(this._task.causes().map(this._makeCommitListItem.bind(this)));
-            this._fixList.setList(this._task.fixes().map(this._makeCommitListItem.bind(this)));
-
-            this._taskStatusControl.value = this._task.changeType() || 'unconfirmed';
-        }
-
-        var repositoryList;
-        if (this._startPoint) {
-            var commitSet = this._startPoint.commitSet();
-            repositoryList = Repository.sortByNamePreferringOnesWithURL(commitSet.repositories());
-        } else
-            repositoryList = Repository.sortByNamePreferringOnesWithURL(Repository.all());
-
-        this._bugList.enqueueToRender();
-
-        this._causeList.setKindList(repositoryList);
-        this._causeList.enqueueToRender();
-
-        this._fixList.setKindList(repositoryList);
-        this._fixList.enqueueToRender();
-
-        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 && this._testGroups && this._testGroups.length ? null : 'none';
-        this._taskNameLabel.enqueueToRender();
-
-        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]);
-                }));
-        }
-
-        var selectedRange = this._analysisResultsViewer.selectedRange();
-        var a = selectedRange['A'];
-        var b = selectedRange['B'];
-        this._newTestGroupFormForViewer.setCommitSetMap(a && b ? {'A': a.commitSet(), 'B': b.commitSet()} : null);
-        this._newTestGroupFormForViewer.enqueueToRender();
-        this._newTestGroupFormForViewer.element().style.display = this._triggerable ? null : 'none';
-
-        this._renderTestGroupList();
-        this._renderTestGroupDetails();
-
-        if (!this._renderedCurrentTestGroup && !this._selectionWasModifiedByUser && this._startPoint && this._endPoint)
-            this._chartPane.setMainSelection([this._startPoint.time, this._endPoint.time]);
+        this._renderTaskNameAndStatusLazily.evaluate(this._task, this._task ? this._task.name() : null, this._task ? this._task.changeType() : null);
+        this._renderCauseAndFixesLazily.evaluate(this._startPoint, this._task);
+        this._renderRelatedTasksLazily.evaluate(this._task, this._relatedTasks);
 
-        var points = this._chartPane.selectedPoints();
-        this._newTestGroupFormForChart.setCommitSetMap(points && points.length() >= 2 ?
-                {'A': points.firstPoint().commitSet(), 'B': points.lastPoint().commitSet()} : null);
-        this._newTestGroupFormForChart.enqueueToRender();
-        this._newTestGroupFormForChart.element().style.display = this._triggerable ? null : 'none';
+        this.content('chart-pane').style.display = this._task ? null : 'none';
+        this.part('chart-pane').setShowForm(!!this._triggerable);
 
-        this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
-        this._analysisResultsViewer.enqueueToRender();
-
-        this._testGroupResultsTable.enqueueToRender();
+        this.content('results-pane').style.display = this._task ? null : 'none';
+        this.part('results-pane').setShowForm(!!this._triggerable);
 
         Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
     }
 
-    _makeCommitListItem(commit)
-    {
-        return new MutableListItem(commit.repository(), commit.label(), commit.title(), commit.url(),
-            'Disassociate this commit', this._dissociateCommit.bind(this, commit));
-    }
-
-    _renderTestGroupList()
+    _renderTaskNameAndStatus(task, taskName, changeType)
     {
-        var element = ComponentBase.createElement;
-        var link = ComponentBase.createLink;
-        if (this._testGroups != this._renderedTestGroups) {
-            this._renderedTestGroups = this._testGroups;
-            this._testGroupLabelMap.clear();
-
-            var unhiddenTestGroups = this._filteredTestGroups.filter(function (group) { return !group.isHidden(); });
-            var hiddenTestGroups = this._filteredTestGroups.filter(function (group) { return group.isHidden(); });
-
-            var listItems = [];
-            for (var group of hiddenTestGroups)
-                listItems.unshift(this._createTestGroupListItem(group));
-            for (var group of unhiddenTestGroups)
-                listItems.unshift(this._createTestGroupListItem(group));
-
-            if (this._testGroups.length != this._filteredTestGroups.length) {
-                listItems.push(element('li', {class: 'test-group-list-show-all'},
-                    link('Show hidden tests', this._showAllTestGroups.bind(this))));
-            }
-
-            this.renderReplace(this.content().querySelector('.test-group-list'), listItems);
-
-            this._renderedCurrentTestGroup = null;
-        }
-
-        if (this._testGroups) {
-            for (var testGroup of this._filteredTestGroups) {
-                var label = this._testGroupLabelMap.get(testGroup);
-                label.setText(testGroup.label());
-                label.enqueueToRender();
-            }
+        this.part('analysis-task-name').setText(taskName);
+        if (task) {
+            const link = ComponentBase.createLink;
+            const platform = task.platform();
+            const metric = task.metric();
+            const subtitle = `${metric.fullName()} on ${platform.label()}`;
+            this.renderReplace(this.content('platform-metric-names'), 
+                link(subtitle, this.router().url('charts', ChartsPage.createStateForAnalysisTask(task))));
         }
+        this.content('change-type').value = changeType || 'unconfirmed';
     }
 
-    _createTestGroupListItem(group)
+    _renderRelatedTasks(task, relatedTasks)
     {
-        var text = new EditableText(group.label());
-        text.listenToAction('update', () => this._updateTestGroupName(group));
-
-        this._testGroupLabelMap.set(group, text);
-        return ComponentBase.createElement('li', {class: 'test-group-list-' + group.id()},
-            ComponentBase.createLink(text, group.label(), this._showTestGroup.bind(this, group)));
+        const element = ComponentBase.createElement;
+        const link = ComponentBase.createLink;
+        this.renderReplace(this.content('related-tasks-list'), (task && relatedTasks ? relatedTasks : []).map((otherTask) => {
+                let suffix = '';
+                const taskLabel = otherTask.label();
+                if (otherTask.metric() != task.metric() && taskLabel.indexOf(otherTask.metric().label()) < 0)
+                    suffix += ` with "${otherTask.metric().label()}"`;
+                if (otherTask.platform() != task.platform() && taskLabel.indexOf(otherTask.platform().label()) < 0)
+                    suffix += ` on ${otherTask.platform().label()}`;
+                return element('li', [link(taskLabel, this.router().url(`analysis/task/${otherTask.id()}`)), suffix]);
+            }));
     }
 
-    _renderTestGroupDetails()
+    _renderCauseAndFixes(startPoint, task)
     {
-        if (this._renderedCurrentTestGroup !== this._currentTestGroup) {
-            if (this._renderedCurrentTestGroup) {
-                var element = this.content().querySelector('.test-group-list-' + this._renderedCurrentTestGroup.id());
-                if (element)
-                    element.classList.remove('selected');
-            }
-            if (this._currentTestGroup) {
-                var element = this.content().querySelector('.test-group-list-' + this._currentTestGroup.id());
-                if (element)
-                    element.classList.add('selected');
-            }
-
-            this._chartPane.setMainSelection(null);
-            if (this._currentTestGroup) {
-                const commitSetsInTestGroup = this._currentTestGroup.requestedCommitSets();
-                const startTime = commitSetsInTestGroup[0].latestCommitTime();
-                const endTime = commitSetsInTestGroup[commitSetsInTestGroup.length - 1].latestCommitTime();
-                if (startTime != endTime)
-                    this._chartPane.setMainSelection([startTime, endTime]);
-            }
+        const hasData = startPoint && task;
+        this.content('cause-fix').style.display = hasData ? null : 'none';
+        if (!hasData)
+            return;
 
-            this._retryForm.setLabel('Retry');
-            if (this._currentTestGroup)
-                this._retryForm.setRepetitionCount(this._currentTestGroup.repetitionCount());
-            this._retryForm.element().style.display = this._currentTestGroup ? null : 'none';
+        const commitSet = startPoint.commitSet();
+        const repositoryList = Repository.sortByNamePreferringOnesWithURL(commitSet.repositories());
 
-            this.content().querySelector('.test-group-hide-button').textContent
-                = this._currentTestGroup && this._currentTestGroup.isHidden() ? 'Unhide' : 'Hide';
+        const makeItem = (commit) => {
+            return new MutableListItem(commit.repository(), commit.label(), commit.title(), commit.url(),
+                'Disassociate this commit', this._dissociateCommit.bind(this, commit));
+        }
 
-            this.content().querySelector('.pending-request-cancel-warning').style.display
-                = this._currentTestGroup && this._currentTestGroup.hasPending() ? null : 'none';
+        const causeList = this.part('cause-list');
+        causeList.setKindList(repositoryList);
+        causeList.setList(task.causes().map((commit) => makeItem(commit)));
 
-            this._renderedCurrentTestGroup = this._currentTestGroup;
-        }
-        this._retryForm.enqueueToRender();
+        const fixList = this.part('fix-list');
+        fixList.setKindList(repositoryList);
+        fixList.setList(task.fixes().map((commit) => makeItem(commit)));
     }
 
     _showTestGroup(testGroup)
     {
-        this._currentTestGroup = testGroup;        
-        this._testGroupResultsTable.setTestGroup(this._currentTestGroup);
+        this._currentTestGroup = testGroup;
+        this.part('results-pane').setTestGroups(this._filteredTestGroups, this._currentTestGroup);
+        const groupsInReverseChronology = this._filteredTestGroups.slice(0).reverse();
+        const showHiddenGroups = !this._testGroups.some((group) => group.isHidden()) || this._showHiddenTestGroups;
+        this.part('group-pane').setTestGroups(groupsInReverseChronology, this._currentTestGroup, showHiddenGroups);
         this.enqueueToRender();
     }
 
-    _updateTaskName()
+    _updateTaskName(newName)
     {
         console.assert(this._task);
-        this._taskNameLabel.enqueueToRender();
 
-        return this._task.updateName(this._taskNameLabel.editedText()).then(() => {
+        return this._task.updateName(newName).then(() => {
             this.enqueueToRender();
         }, (error) => {
             this.enqueueToRender();
@@ -420,12 +590,10 @@ class AnalysisTaskPage extends PageWithHeading {
         });
     }
 
-    _updateTestGroupName(testGroup)
+    _updateTestGroupName(testGroup, newName)
     {
-        var label = this._testGroupLabelMap.get(testGroup);
-        label.enqueueToRender();
-
-        return testGroup.updateName(label.editedText()).then(() => {
+        return testGroup.updateName(newName).then(() => {
+            this._showTestGroup(this._currentTestGroup);
             this.enqueueToRender();
         }, (error) => {
             this.enqueueToRender();
@@ -433,10 +601,9 @@ class AnalysisTaskPage extends PageWithHeading {
         });
     }
 
-    _hideCurrentTestGroup()
+    _hideCurrentTestGroup(testGroup)
     {
-        console.assert(this._currentTestGroup);
-        return this._currentTestGroup.updateHiddenFlag(!this._currentTestGroup.isHidden()).then(() => {
+        return testGroup.updateHiddenFlag(!testGroup.isHidden()).then(() => {
             this._didUpdateTestGroupHiddenState();
             this.enqueueToRender();
         }, function (error) {
@@ -451,7 +618,7 @@ class AnalysisTaskPage extends PageWithHeading {
         event.preventDefault();
         console.assert(this._task);
 
-        var newChangeType = this._taskStatusControl.value;
+        let newChangeType = this.content('change-type').value;
         if (newChangeType == 'unconfirmed')
             newChangeType = null;
 
@@ -465,27 +632,6 @@ class AnalysisTaskPage extends PageWithHeading {
         });
     }
 
-    _associateBug(tracker, bugNumber)
-    {
-        console.assert(tracker instanceof BugTracker);
-        bugNumber = parseInt(bugNumber);
-
-        const updateRendering = () => { this.enqueueToRender(); };
-        return this._task.associateBug(tracker, bugNumber).then(updateRendering, (error) => {
-            updateRendering();
-            alert('Failed to associate the bug: ' + error);
-        });
-    }
-
-    _dissociateBug(bug)
-    {
-        const updateRendering = () => { this.enqueueToRender(); };
-        return this._task.dissociateBug(bug).then(updateRendering, (error) => {
-            updateRendering();
-            alert('Failed to dissociate the bug: ' + error);
-        });
-    }
-
     _associateCommit(kind, repository, revision)
     {
         const updateRendering = () => { this.enqueueToRender(); };
@@ -509,10 +655,8 @@ class AnalysisTaskPage extends PageWithHeading {
         });
     }
 
-    _retryCurrentTestGroup(repetitionCount)
+    _retryCurrentTestGroup(testGroup, repetitionCount)
     {
-        console.assert(this._currentTestGroup);
-        const testGroup = this._currentTestGroup;
         const newName = this._createRetryNameForTestGroup(testGroup.name());
         const commitSetList = testGroup.requestedCommitSets();
 
@@ -523,27 +667,6 @@ class AnalysisTaskPage extends PageWithHeading {
         return this._createTestGroupAfterVerifyingCommitSetList(newName, repetitionCount, commitSetMap);
     }
 
-    _chartSelectionDidChange()
-    {
-        this._selectionWasModifiedByUser = true;
-        this.enqueueToRender();
-    }
-
-    _createNewTestGroupFromChart(name, repetitionCount, commitSetMap)
-    {
-        return this._createTestGroupAfterVerifyingCommitSetList(name, repetitionCount, commitSetMap);
-    }
-
-    _selectedRowInAnalysisResultsViewer()
-    {
-        this.enqueueToRender();
-    }
-
-    _createNewTestGroupFromViewer(name, repetitionCount, commitSetMap)
-    {
-        return this._createTestGroupAfterVerifyingCommitSetList(name, repetitionCount, commitSetMap);
-    }
-
     _createTestGroupAfterVerifyingCommitSetList(testGroupName, repetitionCount, commitSetMap)
     {
         if (this._hasDuplicateTestGroupName(testGroupName)) {
@@ -608,14 +731,14 @@ class AnalysisTaskPage extends PageWithHeading {
     {
         return `
             <div class="analysis-task-page">
-                <h2 class="analysis-task-name"><editable-text></editable-text></h2>
-                <h3 class="platform-metric-names"><a href=""></a></h3>
+                <h2 class="analysis-task-name"><editable-text id="analysis-task-name"></editable-text></h2>
+                <h3 id="platform-metric-names"></h3>
                 <p class="error-message"></p>
                 <div class="analysis-task-status">
                     <section>
                         <h3>Status</h3>
-                        <form class="change-type-form">
-                            <select>
+                        <form id="change-type-form">
+                            <select id="change-type">
                                 <option value="unconfirmed">Unconfirmed</option>
                                 <option value="regression">Definite regression</option>
                                 <option value="progression">Definite progression</option>
@@ -627,36 +750,22 @@ class AnalysisTaskPage extends PageWithHeading {
                     </section>
                     <section class="associated-bugs">
                         <h3>Associated Bugs</h3>
-                        <mutable-list-view></mutable-list-view>
+                        <analysis-task-bug-list id="bug-list"></analysis-task-bug-list>
                     </section>
-                    <section class="cause-fix">
+                    <section id="cause-fix">
                         <h3>Caused by</h3>
-                        <span class="cause-list"><mutable-list-view></mutable-list-view></span>
+                        <mutable-list-view id="cause-list"></mutable-list-view>
                         <h3>Fixed by</h3>
-                        <span class="fix-list"><mutable-list-view></mutable-list-view></span>
+                        <mutable-list-view id="fix-list"></mutable-list-view>
                     </section>
                     <section class="related-tasks">
                         <h3>Related Tasks</h3>
-                        <ul class="related-tasks-list"></ul>
+                        <ul id="related-tasks-list"></ul>
                     </section>
                 </div>
-                <section class="overview-chart">
-                    <analysis-task-chart-pane></analysis-task-chart-pane>
-                    <div class="new-test-group-form"><customizable-test-group-form></customizable-test-group-form></div>
-                </section>
-                <section class="analysis-results-view">
-                    <analysis-results-viewer></analysis-results-viewer>
-                    <div class="new-test-group-form"><customizable-test-group-form></customizable-test-group-form></div>
-                </section>
-                <section class="test-group-view">
-                    <ul class="test-group-list"></ul>
-                    <div class="test-group-details">
-                        <test-group-results-table></test-group-results-table>
-                        <div class="test-group-retry-form"><test-group-form></test-group-form></div>
-                        <button class="test-group-hide-button">Hide</button>
-                        <span class="pending-request-cancel-warning">(cancels pending requests)</span>
-                    </div>
-                </section>
+                <analysis-task-chart-pane id="chart-pane"></analysis-task-chart-pane>
+                <analysis-task-results-pane id="results-pane"></analysis-task-results-pane>
+                <analysis-task-test-group-pane id="group-pane"></analysis-task-test-group-pane>
             </div>
 `;
     }
@@ -675,7 +784,7 @@ class AnalysisTaskPage extends PageWithHeading {
                 padding: 0;
             }
 
-            .platform-metric-names {
+            #platform-metric-names {
                 font-size: 1rem;
                 font-weight: inherit;
                 color: #c93;
@@ -683,13 +792,17 @@ class AnalysisTaskPage extends PageWithHeading {
                 padding: 0;
             }
 
-            .platform-metric-names a {
+            #platform-metric-names a {
                 text-decoration: none;
                 color: inherit;
             }
 
-            .platform-metric-names:empty {
-                margin: 0;
+            #platform-metric-names:empty {
+                display: none;
+            }
+
+            .error-message:empty {
+                display: none;
             }
 
             .error-message:not(:empty) {
@@ -697,8 +810,20 @@ class AnalysisTaskPage extends PageWithHeading {
                 padding: 0;
             }
 
-            .overview-chart {
-                margin: 0 1rem;
+            #chart-pane,
+            #results-pane {
+                display: block;
+                padding: 0 1rem;
+                border-bottom: solid 1px #ccc;
+            }
+            
+            #results-pane {
+                margin-top: 1rem;
+            }
+
+            #group-pane {
+                margin: 1rem;
+                margin-bottom: 2rem;
             }
 
             .analysis-task-status {
@@ -743,93 +868,12 @@ class AnalysisTaskPage extends PageWithHeading {
                 overflow-y: scroll;
             }
 
-
-            .analysis-results-view {
-                border-top: solid 1px #ccc;
-                border-bottom: solid 1px #ccc;
-                margin: 1rem 0;
-                padding: 1rem;
-            }
-
             .test-configuration h3 {
                 font-size: 1rem;
                 font-weight: inherit;
                 color: inherit;
                 margin: 0 1rem;
                 padding: 0;
-            }
-
-            .test-group-view {
-                display: table;
-                margin: 0 1rem;
-                margin-bottom: 2rem;
-            }
-
-            .test-group-details {
-                display: table-cell;
-                margin-bottom: 1rem;
-                padding: 0;
-                margin: 0;
-            }
-
-            .new-test-group-form,
-            .test-group-retry-form {
-                padding: 0;
-                margin: 0.5rem;
-            }
-
-            .test-group-hide-button {
-                margin: 0.5rem;
-            }
-
-            .test-group-list {
-                display: table-cell;
-                margin: 0;
-                padding: 0.2rem 0;
-                list-style: none;
-                border-right: solid 1px #ccc;
-                white-space: nowrap;
-                min-width: 8rem;
-            }
-
-            .test-group-list:empty {
-                margin: 0;
-                padding: 0;
-                border-right: none;
-            }
-
-            .test-group-list > li {
-                display: block;
-                font-size: 0.9rem;
-            }
-
-            .test-group-list > li > a {
-                display: block;
-                color: inherit;
-                text-decoration: none;
-                margin: 0;
-                padding: 0.2rem;
-            }
-            
-            .test-group-list > li.test-group-list-show-all {
-                font-size: 0.8rem;
-                margin-top: 0.5rem;
-                padding-right: 1rem;
-                text-align: center;
-                color: #999;
-            }
-
-            .test-group-list > li.test-group-list-show-all:not(.selected) a:hover {
-                background: inherit;
-            }
-
-            .test-group-list > li.selected > a {
-                background: rgba(204, 153, 51, 0.1);
-            }
-
-            .test-group-list > li:not(.selected) > a:hover {
-                background: #eee;
-            }
-`;
+            }`;
     }
 }
index 51a196e..6c667de 100644 (file)
@@ -142,7 +142,6 @@ describe('TestGroup', function () {
             assert.ok(buildRequests[0].isPending());
             assert.equal(buildRequests[0].statusLabel(), 'Waiting');
             assert.equal(buildRequests[0].buildId(), null);
-            assert.equal(buildRequests[0].result(), null);
 
             assert.equal(buildRequests[1].id(), 16986);
             assert.equal(buildRequests[1].order(), 1);
@@ -151,7 +150,6 @@ describe('TestGroup', function () {
             assert.ok(buildRequests[1].isPending());
             assert.equal(buildRequests[1].statusLabel(), 'Waiting');
             assert.equal(buildRequests[1].buildId(), null);
-            assert.equal(buildRequests[1].result(), null);
         });
 
         it('should create root sets for each group', function () {