Analysis task should look for a git commit based on abridged hashes
[WebKit.git] / Websites / perf.webkit.org / public / v3 / pages / analysis-task-page.js
1
2 class AnalysisTaskChartPane extends ChartPaneBase {
3     constructor()
4     {
5         super('analysis-task-chart-pane');
6         this._page = null;
7     }
8
9     setPage(page) { this._page = page; }
10     router() { return this._page.router(); }
11
12     _mainSelectionDidChange(selection, didEndDrag)
13     {
14         super._mainSelectionDidChange(selection);
15         if (didEndDrag)
16             this._page._chartSelectionDidChange();
17     }
18
19     selectedPoints()
20     {
21         var selection = this._mainChart ? this._mainChart.currentSelection() : null;
22         if (!selection)
23             return null;
24
25         return this._mainChart.sampledDataBetween('current', selection[0], selection[1]);
26     }
27 }
28
29 ComponentBase.defineElement('analysis-task-chart-pane', AnalysisTaskChartPane);
30
31 class AnalysisTaskPage extends PageWithHeading {
32     constructor()
33     {
34         super('Analysis Task');
35         this._task = null;
36         this._relatedTasks = null;
37         this._testGroups = null;
38         this._renderedTestGroups = null;
39         this._testGroupLabelMap = new Map;
40         this._renderedCurrentTestGroup = undefined;
41         this._analysisResults = null;
42         this._measurementSet = null;
43         this._startPoint = null;
44         this._endPoint = null;
45         this._errorMessage = null;
46         this._currentTestGroup = null;
47         this._filteredTestGroups = null;
48         this._showHiddenTestGroups = false;
49         this._selectionWasModifiedByUser = false;
50
51         this._chartPane = this.content().querySelector('analysis-task-chart-pane').component();
52         this._chartPane.setPage(this);
53         this._analysisResultsViewer = this.content().querySelector('analysis-results-viewer').component();
54         this._analysisResultsViewer.setTestGroupCallback(this._showTestGroup.bind(this));
55         this._analysisResultsViewer.setRangeSelectorLabels(['A', 'B']);
56         this._analysisResultsViewer.setRangeSelectorCallback(this._selectedRowInAnalysisResultsViewer.bind(this));
57         this._testGroupResultsTable = this.content().querySelector('test-group-results-table').component();
58         this._taskNameLabel = this.content().querySelector('.analysis-task-name editable-text').component();
59         this._taskNameLabel.setStartedEditingCallback(this._didStartEditingTaskName.bind(this));
60         this._taskNameLabel.setUpdateCallback(this._updateTaskName.bind(this));
61
62         this.content().querySelector('.change-type-form').onsubmit = this._updateChangeType.bind(this);
63         this._taskStatusControl = this.content().querySelector('.change-type-form select');
64
65         this._bugList = this.content().querySelector('.associated-bugs mutable-list-view').component();
66         this._bugList.setKindList(BugTracker.all());
67         this._bugList.setAddCallback(this._associateBug.bind(this));
68
69         this._causeList = this.content().querySelector('.cause-list mutable-list-view').component();
70         this._causeList.setAddCallback(this._associateCommit.bind(this, 'cause'));
71
72         this._fixList = this.content().querySelector('.fix-list mutable-list-view').component();
73         this._fixList.setAddCallback(this._associateCommit.bind(this, 'fix'));
74
75         this._newTestGroupFormForChart = this.content().querySelector('.overview-chart customizable-test-group-form').component();
76         this._newTestGroupFormForChart.setStartCallback(this._createNewTestGroupFromChart.bind(this));
77
78         this._newTestGroupFormForViewer = this.content().querySelector('.analysis-results-view customizable-test-group-form').component();
79         this._newTestGroupFormForViewer.setStartCallback(this._createNewTestGroupFromViewer.bind(this));
80
81         this._retryForm = this.content().querySelector('.test-group-retry-form test-group-form').component();
82         this._retryForm.setStartCallback(this._retryCurrentTestGroup.bind(this));
83         this._hideButton = this.content().querySelector('.test-group-hide-button');
84         this._hideButton.onclick = this._hideCurrentTestGroup.bind(this);
85     }
86
87     title() { return this._task ? this._task.label() : 'Analysis Task'; }
88     routeName() { return 'analysis/task'; }
89
90     updateFromSerializedState(state)
91     {
92         var self = this;
93         if (state.remainingRoute) {
94             var taskId = parseInt(state.remainingRoute);
95             AnalysisTask.fetchById(taskId).then(this._didFetchTask.bind(this), function (error) {
96                 self._errorMessage = `Failed to fetch the analysis task ${state.remainingRoute}: ${error}`;
97                 self.render();
98             });
99             this._fetchRelatedInfoForTaskId(taskId);
100         } else if (state.buildRequest) {
101             var buildRequestId = parseInt(state.buildRequest);
102             AnalysisTask.fetchByBuildRequestId(buildRequestId).then(this._didFetchTask.bind(this)).then(function (task) {
103                 self._fetchRelatedInfoForTaskId(task.id());
104             }, function (error) {
105                 self._errorMessage = `Failed to fetch the analysis task for the build request ${buildRequestId}: ${error}`;
106                 self.render();
107             });
108         }
109     }
110
111     _fetchRelatedInfoForTaskId(taskId)
112     {
113         TestGroup.fetchByTask(taskId).then(this._didFetchTestGroups.bind(this));
114         AnalysisResults.fetch(taskId).then(this._didFetchAnalysisResults.bind(this));
115         AnalysisTask.fetchRelatedTasks(taskId).then(this._didFetchRelatedAnalysisTasks.bind(this));
116     }
117
118     _didFetchTask(task)
119     {
120         console.assert(!this._task);
121
122         this._task = task;
123         var platform = task.platform();
124         var metric = task.metric();
125         var lastModified = platform.lastModified(metric);
126
127         this._measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
128         this._measurementSet.fetchBetween(task.startTime(), task.endTime(), this._didFetchMeasurement.bind(this));
129
130         var formatter = metric.makeFormatter(4);
131         this._analysisResultsViewer.setValueFormatter(formatter);
132         this._testGroupResultsTable.setValueFormatter(formatter);
133
134         this._chartPane.configure(platform.id(), metric.id());
135
136         var domain = ChartsPage.createDomainForAnalysisTask(task);
137         this._chartPane.setOverviewDomain(domain[0], domain[1]);
138         this._chartPane.setMainDomain(domain[0], domain[1]);
139
140         this.render();
141
142         return task;
143     }
144
145     _didFetchRelatedAnalysisTasks(relatedTasks)
146     {
147         this._relatedTasks = relatedTasks;
148         this.render();
149     }
150
151     _didFetchMeasurement()
152     {
153         console.assert(this._task);
154         console.assert(this._measurementSet);
155         var series = this._measurementSet.fetchedTimeSeries('current', false, false);
156         var startPoint = series.findById(this._task.startMeasurementId());
157         var endPoint = series.findById(this._task.endMeasurementId());
158         if (!startPoint || !endPoint || !this._measurementSet.hasFetchedRange(startPoint.time, endPoint.time))
159             return;
160
161         this._analysisResultsViewer.setPoints(startPoint, endPoint);
162
163         this._startPoint = startPoint;
164         this._endPoint = endPoint;
165         this.render();
166     }
167
168     _didFetchTestGroups(testGroups)
169     {
170         this._testGroups = testGroups.sort(function (a, b) { return +a.createdAt() - b.createdAt(); });
171         this._didUpdateTestGroupHiddenState();
172         this._assignTestResultsIfPossible();
173         this.render();
174     }
175
176     _showAllTestGroups()
177     {
178         this._showHiddenTestGroups = true;
179         this._didUpdateTestGroupHiddenState();
180         this.render();
181     }
182
183     _didUpdateTestGroupHiddenState()
184     {
185         this._renderedCurrentTestGroup = null;
186         this._renderedTestGroups = null;
187         if (!this._showHiddenTestGroups)
188             this._filteredTestGroups = this._testGroups.filter(function (group) { return !group.isHidden(); });
189         else
190             this._filteredTestGroups = this._testGroups;
191         this._currentTestGroup = this._filteredTestGroups ? this._filteredTestGroups[this._filteredTestGroups.length - 1] : null;
192         this._analysisResultsViewer.setTestGroups(this._filteredTestGroups);
193         this._testGroupResultsTable.setTestGroup(this._currentTestGroup);
194     }
195
196     _didFetchAnalysisResults(results)
197     {
198         this._analysisResults = results;
199         if (this._assignTestResultsIfPossible())
200             this.render();
201     }
202
203     _assignTestResultsIfPossible()
204     {
205         if (!this._task || !this._testGroups || !this._analysisResults)
206             return false;
207
208         for (var group of this._testGroups) {
209             for (var request of group.buildRequests())
210                 request.setResult(this._analysisResults.find(request.buildId(), this._task.metric()));
211         }
212
213         this._analysisResultsViewer.didUpdateResults();
214         this._testGroupResultsTable.didUpdateResults();
215
216         return true;
217     }
218
219     render()
220     {
221         super.render();
222
223         Instrumentation.startMeasuringTime('AnalysisTaskPage', 'render');
224
225         this.content().querySelector('.error-message').textContent = this._errorMessage || '';
226
227         this._chartPane.render();
228
229         var element = ComponentBase.createElement;
230         var link = ComponentBase.createLink;
231         if (this._task) {
232             this._taskNameLabel.setText(this._task.name());
233             var platform = this._task.platform();
234             var metric = this._task.metric();
235             var anchor = this.content().querySelector('.platform-metric-names a');
236             this.renderReplace(anchor, metric.fullName() + ' on ' + platform.label());
237             anchor.href = this.router().url('charts', ChartsPage.createStateForAnalysisTask(this._task));
238
239             var self = this;
240             this._bugList.setList(this._task.bugs().map(function (bug) {
241                 return new MutableListItem(bug.bugTracker(), bug.label(), bug.title(), bug.url(),
242                     'Dissociate this bug', self._dissociateBug.bind(self, bug));
243             }));
244
245             this._causeList.setList(this._task.causes().map(this._makeCommitListItem.bind(this)));
246             this._fixList.setList(this._task.fixes().map(this._makeCommitListItem.bind(this)));
247
248             this._taskStatusControl.value = this._task.changeType() || 'unconfirmed';
249         }
250
251         var repositoryList;
252         if (this._startPoint) {
253             var rootSet = this._startPoint.rootSet();
254             repositoryList = Repository.sortByNamePreferringOnesWithURL(rootSet.repositories());
255         } else
256             repositoryList = Repository.sortByNamePreferringOnesWithURL(Repository.all());
257
258         this._bugList.render();
259
260         this._causeList.setKindList(repositoryList);
261         this._causeList.render();
262
263         this._fixList.setKindList(repositoryList);
264         this._fixList.render();
265
266         this.content().querySelector('.analysis-task-status').style.display = this._task ? null : 'none';
267         this.content().querySelector('.overview-chart').style.display = this._task ? null : 'none';
268         this.content().querySelector('.test-group-view').style.display = this._task ? null : 'none';
269         this._taskNameLabel.render();
270
271         if (this._relatedTasks && this._task) {
272             var router = this.router();
273             var link = ComponentBase.createLink;
274             var thisTask = this._task;
275             this.renderReplace(this.content().querySelector('.related-tasks-list'),
276                 this._relatedTasks.map(function (otherTask) {
277                     console.assert(otherTask.metric() == thisTask.metric());
278                     var suffix = '';
279                     var taskLabel = otherTask.label();
280                     if (otherTask.platform() != thisTask.platform() && taskLabel.indexOf(otherTask.platform().label()) < 0)
281                         suffix = ` on ${otherTask.platform().label()}`;
282                     return element('li', [link(taskLabel, router.url(`analysis/task/${otherTask.id()}`)), suffix]);
283                 }));
284         }
285
286         var selectedRange = this._analysisResultsViewer.selectedRange();
287         var a = selectedRange['A'];
288         var b = selectedRange['B'];
289         this._newTestGroupFormForViewer.setRootSetMap(a && b ? {'A': a.rootSet(), 'B': b.rootSet()} : null);
290         this._newTestGroupFormForViewer.render();
291
292         this._renderTestGroupList();
293         this._renderTestGroupDetails();
294
295         if (!this._renderedCurrentTestGroup && !this._selectionWasModifiedByUser && this._startPoint && this._endPoint)
296             this._chartPane.setMainSelection([this._startPoint.time, this._endPoint.time]);
297
298         var points = this._chartPane.selectedPoints();
299         this._newTestGroupFormForChart.setRootSetMap(points && points.length >= 2 ?
300                 {'A': points[0].rootSet(), 'B': points[points.length - 1].rootSet()} : null);
301         this._newTestGroupFormForChart.render();
302
303         this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
304         this._analysisResultsViewer.render();
305
306         this._testGroupResultsTable.render();
307
308         Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
309     }
310
311     _makeCommitListItem(commit)
312     {
313         return new MutableListItem(commit.repository(), commit.label(), commit.title(), commit.url(),
314             'Disassociate this commit', this._dissociateCommit.bind(this, commit));
315     }
316
317     _renderTestGroupList()
318     {
319         var element = ComponentBase.createElement;
320         var link = ComponentBase.createLink;
321         if (this._testGroups != this._renderedTestGroups) {
322             this._renderedTestGroups = this._testGroups;
323             this._testGroupLabelMap.clear();
324
325             var unhiddenTestGroups = this._filteredTestGroups.filter(function (group) { return !group.isHidden(); });
326             var hiddenTestGroups = this._filteredTestGroups.filter(function (group) { return group.isHidden(); });
327
328             var listItems = [];
329             for (var group of hiddenTestGroups)
330                 listItems.unshift(this._createTestGroupListItem(group));
331             for (var group of unhiddenTestGroups)
332                 listItems.unshift(this._createTestGroupListItem(group));
333
334             if (this._testGroups.length != this._filteredTestGroups.length) {
335                 listItems.push(element('li', {class: 'test-group-list-show-all'},
336                     link('Show hidden tests', this._showAllTestGroups.bind(this))));
337             }
338
339             this.renderReplace(this.content().querySelector('.test-group-list'), listItems);
340
341             this._renderedCurrentTestGroup = null;
342         }
343
344         if (this._testGroups) {
345             for (var testGroup of this._filteredTestGroups) {
346                 var label = this._testGroupLabelMap.get(testGroup);
347                 label.setText(testGroup.label());
348                 label.render();
349             }
350         }
351     }
352
353     _createTestGroupListItem(group)
354     {
355         var text = new EditableText(group.label());
356         text.setStartedEditingCallback(function () { return text.render(); });
357         text.setUpdateCallback(this._updateTestGroupName.bind(this, group));
358
359         this._testGroupLabelMap.set(group, text);
360         return ComponentBase.createElement('li', {class: 'test-group-list-' + group.id()},
361             ComponentBase.createLink(text, group.label(), this._showTestGroup.bind(this, group)));
362     }
363
364     _renderTestGroupDetails()
365     {
366         if (this._renderedCurrentTestGroup !== this._currentTestGroup) {
367             if (this._renderedCurrentTestGroup) {
368                 var element = this.content().querySelector('.test-group-list-' + this._renderedCurrentTestGroup.id());
369                 if (element)
370                     element.classList.remove('selected');
371             }
372             if (this._currentTestGroup) {
373                 var element = this.content().querySelector('.test-group-list-' + this._currentTestGroup.id());
374                 if (element)
375                     element.classList.add('selected');
376             }
377
378             this._chartPane.setMainSelection(null);
379             if (this._currentTestGroup) {
380                 var rootSetsInTestGroup = this._currentTestGroup.requestedRootSets();
381                 var startTime = rootSetsInTestGroup[0].latestCommitTime();
382                 var endTime = rootSetsInTestGroup[rootSetsInTestGroup.length - 1].latestCommitTime();
383                 if (startTime != endTime)
384                     this._chartPane.setMainSelection([startTime, endTime]);
385             }
386
387             this._retryForm.setLabel('Retry');
388             if (this._currentTestGroup)
389                 this._retryForm.setRepetitionCount(this._currentTestGroup.repetitionCount());
390             this._retryForm.element().style.display = this._currentTestGroup ? null : 'none';
391
392             this.content().querySelector('.test-group-hide-button').textContent
393                 = this._currentTestGroup && this._currentTestGroup.isHidden() ? 'Unhide' : 'Hide';
394
395             this.content().querySelector('.pending-request-cancel-warning').style.display
396                 = this._currentTestGroup && this._currentTestGroup.hasPending() ? null : 'none';
397
398             this._renderedCurrentTestGroup = this._currentTestGroup;
399         }
400         this._retryForm.render();
401     }
402
403     _showTestGroup(testGroup)
404     {
405         this._currentTestGroup = testGroup;        
406         this._testGroupResultsTable.setTestGroup(this._currentTestGroup);
407         this.render();
408     }
409
410     _didStartEditingTaskName()
411     {
412         this._taskNameLabel.render();
413     }
414
415     _updateTaskName()
416     {
417         console.assert(this._task);
418         this._taskNameLabel.render();
419
420         var self = this;
421         return self._task.updateName(self._taskNameLabel.editedText()).then(function () {
422             self.render();
423         }, function (error) {
424             self.render();
425             alert('Failed to update the name: ' + error);
426         });
427     }
428
429     _updateTestGroupName(testGroup)
430     {
431         var label = this._testGroupLabelMap.get(testGroup);
432         label.render();
433
434         var self = this;
435         return testGroup.updateName(label.editedText()).then(function () {
436             self.render();
437         }, function (error) {
438             self.render();
439             alert('Failed to hide the test name: ' + error);
440         });
441     }
442
443     _hideCurrentTestGroup()
444     {
445         var self = this;
446         console.assert(this._currentTestGroup);
447         return this._currentTestGroup.updateHiddenFlag(!this._currentTestGroup.isHidden()).then(function () {
448             self._didUpdateTestGroupHiddenState();
449             self.render();
450         }, function (error) {
451             self._mayHaveMutatedTestGroupHiddenState();
452             self.render();
453             alert('Failed to update the group: ' + error);
454         });
455     }
456
457     _updateChangeType(event)
458     {
459         event.preventDefault();
460         console.assert(this._task);
461
462         var newChangeType = this._taskStatusControl.value;
463         if (newChangeType == 'unconfirmed')
464             newChangeType = null;
465
466         var render = this.render.bind(this);
467         return this._task.updateChangeType(newChangeType).then(render, function (error) {
468             render();
469             alert('Failed to update the status: ' + error);
470         });
471     }
472
473     _associateBug(tracker, bugNumber)
474     {
475         console.assert(tracker instanceof BugTracker);
476         bugNumber = parseInt(bugNumber);
477
478         var render = this.render.bind(this);
479         return this._task.associateBug(tracker, bugNumber).then(render, function (error) {
480             render();
481             alert('Failed to associate the bug: ' + error);
482         });
483     }
484
485     _dissociateBug(bug)
486     {
487         var render = this.render.bind(this);
488         return this._task.dissociateBug(bug).then(render, function (error) {
489             render();
490             alert('Failed to dissociate the bug: ' + error);
491         });
492     }
493
494     _associateCommit(kind, repository, revision)
495     {
496         var render = this.render.bind(this);
497         return this._task.associateCommit(kind, repository, revision).then(render, function (error) {
498             render();
499             if (error == 'AmbiguousRevision')
500                 alert('There are multiple revisions that match the specified string: ' + revision);
501             else if (error == 'CommitNotFound')
502                 alert('There are no revisions that match the specified string:' + revision);
503             else
504                 alert('Failed to associate the commit: ' + error);
505         });
506     }
507
508     _dissociateCommit(commit)
509     {
510         var render = this.render.bind(this);
511         return this._task.dissociateCommit(commit).then(render, function (error) {
512             render();
513             alert('Failed to dissociate the commit: ' + error);
514         });
515     }
516
517     _retryCurrentTestGroup(repetitionCount)
518     {
519         console.assert(this._currentTestGroup);
520         var testGroup = this._currentTestGroup;
521         var newName = this._createRetryNameForTestGroup(testGroup.name());
522         var rootSetList = testGroup.requestedRootSets();
523
524         var rootSetMap = {};
525         for (var rootSet of rootSetList)
526             rootSetMap[testGroup.labelForRootSet(rootSet)] = rootSet;
527
528         return this._createTestGroupAfterVerifyingRootSetList(newName, repetitionCount, rootSetMap);
529     }
530
531     _chartSelectionDidChange()
532     {
533         this._selectionWasModifiedByUser = true;
534         this.render();
535     }
536
537     _createNewTestGroupFromChart(name, repetitionCount, rootSetMap)
538     {
539         return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount, rootSetMap);
540     }
541
542     _selectedRowInAnalysisResultsViewer()
543     {
544         this.render();
545     }
546
547     _createNewTestGroupFromViewer(name, repetitionCount, rootSetMap)
548     {
549         return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount, rootSetMap);
550     }
551
552     _createTestGroupAfterVerifyingRootSetList(testGroupName, repetitionCount, rootSetMap)
553     {
554         if (this._hasDuplicateTestGroupName(testGroupName))
555             alert(`There is already a test group named "${testGroupName}"`);
556
557         var rootSetsByName = {};
558         var firstLabel;
559         for (firstLabel in rootSetMap) {
560             var rootSet = rootSetMap[firstLabel];
561             for (var repository of rootSet.repositories())
562                 rootSetsByName[repository.name()] = [];
563             break;
564         }
565
566         var setIndex = 0;
567         for (var label in rootSetMap) {
568             var rootSet = rootSetMap[label];
569             for (var repository of rootSet.repositories()) {
570                 var list = rootSetsByName[repository.name()];
571                 if (!list) {
572                     alert(`Set ${label} specifies ${repository.label()} but set ${firstLabel} does not.`);
573                     return null;
574                 }
575                 list.push(rootSet.revisionForRepository(repository));
576             }
577             setIndex++;
578             for (var name in rootSetsByName) {
579                 var list = rootSetsByName[name];
580                 if (list.length < setIndex) {
581                     alert(`Set ${firstLabel} specifies ${name} but set ${label} does not.`);
582                     return null;
583                 }
584             }
585         }
586
587         TestGroup.createAndRefetchTestGroups(this._task, testGroupName, repetitionCount, rootSetsByName)
588             .then(this._didFetchTestGroups.bind(this), function (error) {
589             alert('Failed to create a new test group: ' + error);
590         });
591     }
592
593     _createRetryNameForTestGroup(name)
594     {
595         var nameWithNumberMatch = name.match(/(.+?)\s*\(\s*(\d+)\s*\)\s*$/);
596         var number = 1;
597         if (nameWithNumberMatch) {
598             name = nameWithNumberMatch[1];
599             number = parseInt(nameWithNumberMatch[2]);
600         }
601
602         var newName;
603         do {
604             number++;
605             newName = `${name} (${number})`;
606         } while (this._hasDuplicateTestGroupName(newName));
607
608         return newName;
609     }
610
611     _hasDuplicateTestGroupName(name)
612     {
613         console.assert(this._testGroups);
614         for (var group of this._testGroups) {
615             if (group.name() == name)
616                 return true;
617         }
618         return false;
619     }
620
621     static htmlTemplate()
622     {
623         return `
624             <div class="analysis-task-page">
625                 <h2 class="analysis-task-name"><editable-text></editable-text></h2>
626                 <h3 class="platform-metric-names"><a href=""></a></h3>
627                 <p class="error-message"></p>
628                 <div class="analysis-task-status">
629                     <section>
630                         <h3>Status</h3>
631                         <form class="change-type-form">
632                             <select>
633                                 <option value="unconfirmed">Unconfirmed</option>
634                                 <option value="regression">Definite regression</option>
635                                 <option value="progression">Definite progression</option>
636                                 <option value="inconclusive">Inconclusive (Closed)</option>
637                                 <option value="unchanged">No change (Closed)</option>
638                             </select>
639                             <button type="submit">Save</button>
640                         </form>
641                     </section>
642                     <section class="associated-bugs">
643                         <h3>Associated Bugs</h3>
644                         <mutable-list-view></mutable-list-view>
645                     </section>
646                     <section class="cause-fix">
647                         <h3>Caused by</h3>
648                         <span class="cause-list"><mutable-list-view></mutable-list-view></span>
649                         <h3>Fixed by</h3>
650                         <span class="fix-list"><mutable-list-view></mutable-list-view></span>
651                     </section>
652                     <section class="related-tasks">
653                         <h3>Related Tasks</h3>
654                         <ul class="related-tasks-list"></ul>
655                     </section>
656                 </div>
657                 <section class="overview-chart">
658                     <analysis-task-chart-pane></analysis-task-chart-pane>
659                     <div class="new-test-group-form"><customizable-test-group-form></customizable-test-group-form></div>
660                 </section>
661                 <section class="analysis-results-view">
662                     <analysis-results-viewer></analysis-results-viewer>
663                     <div class="new-test-group-form"><customizable-test-group-form></customizable-test-group-form></div>
664                 </section>
665                 <section class="test-group-view">
666                     <ul class="test-group-list"></ul>
667                     <div class="test-group-details">
668                         <test-group-results-table></test-group-results-table>
669                         <div class="test-group-retry-form"><test-group-form></test-group-form></div>
670                         <button class="test-group-hide-button">Hide</button>
671                         <span class="pending-request-cancel-warning">(cancels pending requests)</span>
672                     </div>
673                 </section>
674             </div>
675 `;
676     }
677
678     static cssTemplate()
679     {
680         return `
681             .analysis-task-page {
682             }
683
684             .analysis-task-name {
685                 font-size: 1.2rem;
686                 font-weight: inherit;
687                 color: #c93;
688                 margin: 0 1rem;
689                 padding: 0;
690             }
691
692             .platform-metric-names {
693                 font-size: 1rem;
694                 font-weight: inherit;
695                 color: #c93;
696                 margin: 0 1rem;
697                 padding: 0;
698             }
699
700             .platform-metric-names a {
701                 text-decoration: none;
702                 color: inherit;
703             }
704
705             .platform-metric-names:empty {
706                 margin: 0;
707             }
708
709             .error-message:not(:empty) {
710                 margin: 1rem;
711                 padding: 0;
712             }
713
714             .overview-chart {
715                 margin: 0 1rem;
716             }
717
718             .analysis-task-status {
719                 margin: 0;
720                 display: flex;
721                 padding-bottom: 1rem;
722                 margin-bottom: 1rem;
723                 border-bottom: solid 1px #ccc;
724             }
725
726             .analysis-task-status > section {
727                 flex-grow: 1;
728                 flex-shrink: 0;
729                 border-left: solid 1px #eee;
730                 padding-left: 1rem;
731                 padding-right: 1rem;
732             }
733
734             .analysis-task-status > section.related-tasks {
735                 flex-shrink: 1;
736             }
737
738             .analysis-task-status > section:first-child {
739                 border-left: none;
740             }
741
742             .analysis-task-status h3 {
743                 font-size: 1rem;
744                 font-weight: inherit;
745                 color: #c93;
746             }
747
748             .analysis-task-status ul,
749             .analysis-task-status li {
750                 list-style: none;
751                 padding: 0;
752                 margin: 0;
753             }
754
755             .related-tasks-list {
756                 max-height: 10rem;
757                 overflow-y: scroll;
758             }
759
760
761             .analysis-results-view {
762                 border-top: solid 1px #ccc;
763                 border-bottom: solid 1px #ccc;
764                 margin: 1rem 0;
765                 padding: 1rem;
766             }
767
768             .test-configuration h3 {
769                 font-size: 1rem;
770                 font-weight: inherit;
771                 color: inherit;
772                 margin: 0 1rem;
773                 padding: 0;
774             }
775
776             .test-group-view {
777                 display: table;
778                 margin: 0 1rem;
779                 margin-bottom: 2rem;
780             }
781
782             .test-group-details {
783                 display: table-cell;
784                 margin-bottom: 1rem;
785                 padding: 0;
786                 margin: 0;
787             }
788
789             .new-test-group-form,
790             .test-group-retry-form {
791                 padding: 0;
792                 margin: 0.5rem;
793             }
794
795             .test-group-hide-button {
796                 margin: 0.5rem;
797             }
798
799             .test-group-list {
800                 display: table-cell;
801                 margin: 0;
802                 padding: 0.2rem 0;
803                 list-style: none;
804                 border-right: solid 1px #ccc;
805                 white-space: nowrap;
806                 min-width: 8rem;
807             }
808
809             .test-group-list:empty {
810                 margin: 0;
811                 padding: 0;
812                 border-right: none;
813             }
814
815             .test-group-list > li {
816                 display: block;
817                 font-size: 0.9rem;
818             }
819
820             .test-group-list > li > a {
821                 display: block;
822                 color: inherit;
823                 text-decoration: none;
824                 margin: 0;
825                 padding: 0.2rem;
826             }
827             
828             .test-group-list > li.test-group-list-show-all {
829                 font-size: 0.8rem;
830                 margin-top: 0.5rem;
831                 padding-right: 1rem;
832                 text-align: center;
833                 color: #999;
834             }
835
836             .test-group-list > li.test-group-list-show-all:not(.selected) a:hover {
837                 background: inherit;
838             }
839
840             .test-group-list > li.selected > a {
841                 background: rgba(204, 153, 51, 0.1);
842             }
843
844             .test-group-list > li:not(.selected) > a:hover {
845                 background: #eee;
846             }
847 `;
848     }
849 }