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