Function with default parameter values that are arrow functions that capture this...
[WebKit-https.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             alert('Failed to associate the commit: ' + error);
500         });
501     }
502
503     _dissociateCommit(commit)
504     {
505         var render = this.render.bind(this);
506         return this._task.dissociateCommit(commit).then(render, function (error) {
507             render();
508             alert('Failed to dissociate the commit: ' + error);
509         });
510     }
511
512     _retryCurrentTestGroup(repetitionCount)
513     {
514         console.assert(this._currentTestGroup);
515         var testGroup = this._currentTestGroup;
516         var newName = this._createRetryNameForTestGroup(testGroup.name());
517         var rootSetList = testGroup.requestedRootSets();
518
519         var rootSetMap = {};
520         for (var rootSet of rootSetList)
521             rootSetMap[testGroup.labelForRootSet(rootSet)] = rootSet;
522
523         return this._createTestGroupAfterVerifyingRootSetList(newName, repetitionCount, rootSetMap);
524     }
525
526     _chartSelectionDidChange()
527     {
528         this._selectionWasModifiedByUser = true;
529         this.render();
530     }
531
532     _createNewTestGroupFromChart(name, repetitionCount, rootSetMap)
533     {
534         return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount, rootSetMap);
535     }
536
537     _selectedRowInAnalysisResultsViewer()
538     {
539         this.render();
540     }
541
542     _createNewTestGroupFromViewer(name, repetitionCount, rootSetMap)
543     {
544         return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount, rootSetMap);
545     }
546
547     _createTestGroupAfterVerifyingRootSetList(testGroupName, repetitionCount, rootSetMap)
548     {
549         if (this._hasDuplicateTestGroupName(testGroupName))
550             alert(`There is already a test group named "${testGroupName}"`);
551
552         var rootSetsByName = {};
553         var firstLabel;
554         for (firstLabel in rootSetMap) {
555             var rootSet = rootSetMap[firstLabel];
556             for (var repository of rootSet.repositories())
557                 rootSetsByName[repository.name()] = [];
558             break;
559         }
560
561         var setIndex = 0;
562         for (var label in rootSetMap) {
563             var rootSet = rootSetMap[label];
564             for (var repository of rootSet.repositories()) {
565                 var list = rootSetsByName[repository.name()];
566                 if (!list) {
567                     alert(`Set ${label} specifies ${repository.label()} but set ${firstLabel} does not.`);
568                     return null;
569                 }
570                 list.push(rootSet.revisionForRepository(repository));
571             }
572             setIndex++;
573             for (var name in rootSetsByName) {
574                 var list = rootSetsByName[name];
575                 if (list.length < setIndex) {
576                     alert(`Set ${firstLabel} specifies ${name} but set ${label} does not.`);
577                     return null;
578                 }
579             }
580         }
581
582         TestGroup.createAndRefetchTestGroups(this._task, testGroupName, repetitionCount, rootSetsByName)
583             .then(this._didFetchTestGroups.bind(this), function (error) {
584             alert('Failed to create a new test group: ' + error);
585         });
586     }
587
588     _createRetryNameForTestGroup(name)
589     {
590         var nameWithNumberMatch = name.match(/(.+?)\s*\(\s*(\d+)\s*\)\s*$/);
591         var number = 1;
592         if (nameWithNumberMatch) {
593             name = nameWithNumberMatch[1];
594             number = parseInt(nameWithNumberMatch[2]);
595         }
596
597         var newName;
598         do {
599             number++;
600             newName = `${name} (${number})`;
601         } while (this._hasDuplicateTestGroupName(newName));
602
603         return newName;
604     }
605
606     _hasDuplicateTestGroupName(name)
607     {
608         console.assert(this._testGroups);
609         for (var group of this._testGroups) {
610             if (group.name() == name)
611                 return true;
612         }
613         return false;
614     }
615
616     static htmlTemplate()
617     {
618         return `
619             <div class="analysis-task-page">
620                 <h2 class="analysis-task-name"><editable-text></editable-text></h2>
621                 <h3 class="platform-metric-names"><a href=""></a></h3>
622                 <p class="error-message"></p>
623                 <div class="analysis-task-status">
624                     <section>
625                         <h3>Status</h3>
626                         <form class="change-type-form">
627                             <select>
628                                 <option value="unconfirmed">Unconfirmed</option>
629                                 <option value="regression">Definite regression</option>
630                                 <option value="progression">Definite progression</option>
631                                 <option value="inconclusive">Inconclusive (Closed)</option>
632                                 <option value="unchanged">No change (Closed)</option>
633                             </select>
634                             <button type="submit">Save</button>
635                         </form>
636                     </section>
637                     <section class="associated-bugs">
638                         <h3>Associated Bugs</h3>
639                         <mutable-list-view></mutable-list-view>
640                     </section>
641                     <section class="cause-fix">
642                         <h3>Caused by</h3>
643                         <span class="cause-list"><mutable-list-view></mutable-list-view></span>
644                         <h3>Fixed by</h3>
645                         <span class="fix-list"><mutable-list-view></mutable-list-view></span>
646                     </section>
647                     <section class="related-tasks">
648                         <h3>Related Tasks</h3>
649                         <ul class="related-tasks-list"></ul>
650                     </section>
651                 </div>
652                 <section class="overview-chart">
653                     <analysis-task-chart-pane></analysis-task-chart-pane>
654                     <div class="new-test-group-form"><customizable-test-group-form></customizable-test-group-form></div>
655                 </section>
656                 <section class="analysis-results-view">
657                     <analysis-results-viewer></analysis-results-viewer>
658                     <div class="new-test-group-form"><customizable-test-group-form></customizable-test-group-form></div>
659                 </section>
660                 <section class="test-group-view">
661                     <ul class="test-group-list"></ul>
662                     <div class="test-group-details">
663                         <test-group-results-table></test-group-results-table>
664                         <div class="test-group-retry-form"><test-group-form></test-group-form></div>
665                         <button class="test-group-hide-button">Hide</button>
666                         <span class="pending-request-cancel-warning">(cancels pending requests)</span>
667                     </div>
668                 </section>
669             </div>
670 `;
671     }
672
673     static cssTemplate()
674     {
675         return `
676             .analysis-task-page {
677             }
678
679             .analysis-task-name {
680                 font-size: 1.2rem;
681                 font-weight: inherit;
682                 color: #c93;
683                 margin: 0 1rem;
684                 padding: 0;
685             }
686
687             .platform-metric-names {
688                 font-size: 1rem;
689                 font-weight: inherit;
690                 color: #c93;
691                 margin: 0 1rem;
692                 padding: 0;
693             }
694
695             .platform-metric-names a {
696                 text-decoration: none;
697                 color: inherit;
698             }
699
700             .platform-metric-names:empty {
701                 margin: 0;
702             }
703
704             .error-message:not(:empty) {
705                 margin: 1rem;
706                 padding: 0;
707             }
708
709             .overview-chart {
710                 margin: 0 1rem;
711             }
712
713             .analysis-task-status {
714                 margin: 0;
715                 display: flex;
716                 padding-bottom: 1rem;
717                 margin-bottom: 1rem;
718                 border-bottom: solid 1px #ccc;
719             }
720
721             .analysis-task-status > section {
722                 flex-grow: 1;
723                 flex-shrink: 0;
724                 border-left: solid 1px #eee;
725                 padding-left: 1rem;
726                 padding-right: 1rem;
727             }
728
729             .analysis-task-status > section.related-tasks {
730                 flex-shrink: 1;
731             }
732
733             .analysis-task-status > section:first-child {
734                 border-left: none;
735             }
736
737             .analysis-task-status h3 {
738                 font-size: 1rem;
739                 font-weight: inherit;
740                 color: #c93;
741             }
742
743             .analysis-task-status ul,
744             .analysis-task-status li {
745                 list-style: none;
746                 padding: 0;
747                 margin: 0;
748             }
749
750             .related-tasks-list {
751                 max-height: 10rem;
752                 overflow-y: scroll;
753             }
754
755
756             .analysis-results-view {
757                 border-top: solid 1px #ccc;
758                 border-bottom: solid 1px #ccc;
759                 margin: 1rem 0;
760                 padding: 1rem;
761             }
762
763             .test-configuration h3 {
764                 font-size: 1rem;
765                 font-weight: inherit;
766                 color: inherit;
767                 margin: 0 1rem;
768                 padding: 0;
769             }
770
771             .test-group-view {
772                 display: table;
773                 margin: 0 1rem;
774                 margin-bottom: 2rem;
775             }
776
777             .test-group-details {
778                 display: table-cell;
779                 margin-bottom: 1rem;
780                 padding: 0;
781                 margin: 0;
782             }
783
784             .new-test-group-form,
785             .test-group-retry-form {
786                 padding: 0;
787                 margin: 0.5rem;
788             }
789
790             .test-group-hide-button {
791                 margin: 0.5rem;
792             }
793
794             .test-group-list {
795                 display: table-cell;
796                 margin: 0;
797                 padding: 0.2rem 0;
798                 list-style: none;
799                 border-right: solid 1px #ccc;
800                 white-space: nowrap;
801                 min-width: 8rem;
802             }
803
804             .test-group-list:empty {
805                 margin: 0;
806                 padding: 0;
807                 border-right: none;
808             }
809
810             .test-group-list > li {
811                 display: block;
812                 font-size: 0.9rem;
813             }
814
815             .test-group-list > li > a {
816                 display: block;
817                 color: inherit;
818                 text-decoration: none;
819                 margin: 0;
820                 padding: 0.2rem;
821             }
822             
823             .test-group-list > li.test-group-list-show-all {
824                 font-size: 0.8rem;
825                 margin-top: 0.5rem;
826                 padding-right: 1rem;
827                 text-align: center;
828                 color: #999;
829             }
830
831             .test-group-list > li.test-group-list-show-all:not(.selected) a:hover {
832                 background: inherit;
833             }
834
835             .test-group-list > li.selected > a {
836                 background: rgba(204, 153, 51, 0.1);
837             }
838
839             .test-group-list > li:not(.selected) > a:hover {
840                 background: #eee;
841             }
842 `;
843     }
844 }