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