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