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