e2c5342d753b87a8a1cfad52f5c04f46ad89f264
[WebKit.git] / Websites / perf.webkit.org / public / v3 / components / custom-analysis-task-configurator.js
1 class CustomAnalysisTaskConfigurator extends ComponentBase {
2
3     constructor()
4     {
5         super('custom-analysis-task-configurator');
6
7         this._selectedTests = [];
8         this._triggerablePlatforms = [];
9         this._selectedPlatform = null;
10         this._showComparison = false;
11         this._commitSetMap = {};
12         this._specifiedRevisions = {'Baseline': new Map, 'Comparison': new Map};
13         this._patchUploaders = {'Baseline': new Map, 'Comparison': new Map};
14         this._customRootUploaders = {'Baseline': null, 'Comparison': null};
15         this._fetchedCommits = {'Baseline': new Map, 'Comparison': new Map};
16         this._repositoryGroupByConfiguration = {'Baseline': null, 'Comparison': null};
17         this._invalidRevisionsByConfiguration = {'Baseline': new Map, 'Comparison': new Map};
18
19         this._updateTriggerableLazily = new LazilyEvaluatedFunction(this._updateTriggerable.bind(this));
20         this._renderTriggerableTestsLazily = new LazilyEvaluatedFunction(this._renderTriggerableTests.bind(this));
21         this._renderTriggerablePlatformsLazily = new LazilyEvaluatedFunction(this._renderTriggerablePlatforms.bind(this));
22         this._renderRepositoryPanesLazily = new LazilyEvaluatedFunction(this._renderRepositoryPanes.bind(this));
23     }
24
25     tests() { return this._selectedTests; }
26     platform() { return this._selectedPlatform; }
27     commitSets()
28     {
29         const map = this._commitSetMap;
30         if (!map['Baseline'] || !map['Comparison'])
31             return null;
32         return [map['Baseline'], map['Comparison']];
33     }
34
35     selectTests(selectedTests)
36     {
37         this._selectedTests = selectedTests;
38
39         this._triggerablePlatforms = Triggerable.triggerablePlatformsForTests(this._selectedTests);
40         if (this._selectedTests.length && !this._triggerablePlatforms.includes(this._selectedPlatform))
41             this._selectedPlatform = null;
42
43         this._didUpdateSelectedPlatforms();
44     }
45
46     selectPlatform(selectedPlatform)
47     {
48         this._selectedPlatform = selectedPlatform;
49
50         const [triggerable, error] = this._updateTriggerableLazily.evaluate(this._selectedTests, this._selectedPlatform);
51         this._updateRepositoryGroups(triggerable);
52
53         this._didUpdateSelectedPlatforms();
54     }
55
56     _didUpdateSelectedPlatforms()
57     {
58         for (const configuration of ['Baseline', 'Comparison']) {
59             this._updateMapFromSpecifiedRevisionsForConfiguration(this._fetchedCommits, configuration);
60             this._updateMapFromSpecifiedRevisionsForConfiguration(this._invalidRevisionsByConfiguration, configuration);
61         }
62         this._updateCommitSetMap();
63         this.dispatchAction('testConfigChange');
64         this.enqueueToRender();
65     }
66
67     _updateMapFromSpecifiedRevisionsForConfiguration(map, configuration)
68     {
69         const referenceMap = this._specifiedRevisions[configuration];
70         const newValue = new Map;
71         for (const [key, value] of map[configuration].entries()) {
72             if (!referenceMap.has(key))
73                 continue;
74             newValue.set(key, value);
75         }
76         if (newValue.size !== map[configuration].size)
77             map[configuration] = newValue;
78     }
79
80     setCommitSets(baselineCommitSet, comparisonCommitSet)
81     {
82         const [triggerable, error] = this._updateTriggerableLazily.evaluate(this._selectedTests, this._selectedPlatform);
83
84         if (!triggerable)
85             return;
86
87         const baselineRepositoryGroup = triggerable.repositoryGroups().find((repositoryGroup) => repositoryGroup.accepts(baselineCommitSet));
88         if (baselineRepositoryGroup) {
89             this._repositoryGroupByConfiguration['Baseline'] = baselineRepositoryGroup;
90             this._setUploadedFilesToUploader(this._customRootUploaders['Baseline'], baselineCommitSet.customRoots());
91             this._specifiedRevisions['Baseline'] = this._revisionMapFromCommitSet(baselineCommitSet);
92             this._setPatchFiles('Baseline', baselineCommitSet);
93         }
94
95         const comparisonRepositoryGroup = triggerable.repositoryGroups().find((repositoryGroup) => repositoryGroup.accepts(baselineCommitSet));
96         if (comparisonRepositoryGroup) {
97             this._repositoryGroupByConfiguration['Comparison'] = comparisonRepositoryGroup;
98             this._setUploadedFilesToUploader(this._customRootUploaders['Comparison'], comparisonCommitSet.customRoots());
99             this._specifiedRevisions['Comparison'] = this._revisionMapFromCommitSet(comparisonCommitSet);
100             this._setPatchFiles('Comparison', comparisonCommitSet);
101         }
102
103         this._showComparison = true;
104         this._updateCommitSetMap();
105     }
106
107     _setUploadedFilesToUploader(uploader, files)
108     {
109         if (!uploader || uploader.hasFileToUpload() || uploader.uploadedFiles().length)
110             return;
111         uploader.clearUploads();
112         for (const uploadedFile of files)
113             uploader.addUploadedFile(uploadedFile);
114     }
115
116     _setPatchFiles(configurationName, commitSet)
117     {
118         for (const repository of commitSet.repositories()) {
119             const patch = commitSet.patchForRepository(repository);
120             if (patch)
121                 this._setUploadedFilesToUploader(this._ensurePatchUploader(configurationName, repository), [patch]);
122         }
123     }
124
125     _revisionMapFromCommitSet(commitSet)
126     {
127         const revisionMap = new Map;
128         for (const repository of commitSet.repositories())
129             revisionMap.set(repository, commitSet.revisionForRepository(repository));
130         return revisionMap;
131     }
132
133     didConstructShadowTree()
134     {
135         this.content('specify-comparison-button').onclick = this.createEventHandler(() => this._configureComparison());
136
137         const createRootUploader = () => {
138             const uploader = new InstantFileUploader;
139             uploader.allowMultipleFiles();
140             uploader.element().textContent = 'Add a new root';
141             uploader.listenToAction('removedFile', () => this._updateCommitSetMap());
142             return uploader;
143         }
144
145         const baselineRootsUploader = createRootUploader();
146         baselineRootsUploader.listenToAction('uploadedFile', (uploadedFile) => this._updateCommitSetMap());
147         this._customRootUploaders['Baseline'] = baselineRootsUploader;
148
149         const comparisonRootsUploader = createRootUploader();
150         comparisonRootsUploader.listenToAction('uploadedFile', () => this._updateCommitSetMap());
151         this._customRootUploaders['Comparison'] = comparisonRootsUploader;
152     }
153
154     _ensurePatchUploader(configurationName, repository)
155     {
156         const uploaderMap = this._patchUploaders[configurationName];
157         let uploader = uploaderMap.get(repository);
158         if (uploader)
159             return uploader;
160
161         uploader = new InstantFileUploader;
162         uploader.element().textContent = 'Apply a patch';
163         uploader.listenToAction('uploadedFile', () => this._updateCommitSetMap());
164         uploader.listenToAction('removedFile', () => this._updateCommitSetMap());
165         uploaderMap.set(repository, uploader);
166
167         return uploader;
168     }
169
170     _configureComparison()
171     {
172         this._showComparison = true;
173         this._repositoryGroupByConfiguration['Comparison'] = this._repositoryGroupByConfiguration['Baseline'];
174
175         const specifiedBaselineRevisions = this._specifiedRevisions['Baseline'];
176         const specifiedComparisonRevisions = new Map;
177         for (let key of specifiedBaselineRevisions.keys())
178             specifiedComparisonRevisions.set(key, specifiedBaselineRevisions.get(key));
179         this._specifiedRevisions['Comparison'] = specifiedComparisonRevisions;
180
181         for (const [repository, patchUploader] of this._patchUploaders['Baseline']) {
182             const files = patchUploader.uploadedFiles();
183             if (!files.length)
184                 continue;
185             const comparisonPatchUploader = this._ensurePatchUploader('Comparison', repository);
186             for (const uploadedFile of files)
187                 comparisonPatchUploader.addUploadedFile(uploadedFile);
188         }
189
190         const comparisonRootUploader = this._customRootUploaders['Comparison'];
191         for (const uploadedFile of this._customRootUploaders['Baseline'].uploadedFiles())
192             comparisonRootUploader.addUploadedFile(uploadedFile);
193
194         this.enqueueToRender();
195     }
196
197     render()
198     {
199         super.render();
200
201         const updateSelectedTestsLazily = this._renderTriggerableTestsLazily.evaluate();
202         updateSelectedTestsLazily.evaluate(...this._selectedTests);
203         const updateSelectedPlatformsLazily = this._renderTriggerablePlatformsLazily.evaluate(this._selectedTests, this._triggerablePlatforms);
204         if (updateSelectedPlatformsLazily)
205             updateSelectedPlatformsLazily.evaluate(this._selectedPlatform);
206
207         const [triggerable, error] = this._updateTriggerableLazily.evaluate(this._selectedTests, this._selectedPlatform);
208
209         this._renderRepositoryPanesLazily.evaluate(triggerable, error, this._selectedPlatform, this._repositoryGroupByConfiguration, this._showComparison);
210
211         this.renderReplace(this.content('baseline-testability'), this._buildTestabilityList(this._commitSetMap['Baseline'],
212             'Baseline', this._invalidRevisionsByConfiguration['Baseline']));
213
214         this.renderReplace(this.content('comparison-testability'), !this._showComparison ? null :
215             this._buildTestabilityList(this._commitSetMap['Comparison'], 'Comparison', this._invalidRevisionsByConfiguration['Comparison']));
216     }
217
218     _renderTriggerableTests()
219     {
220         const enabledTriggerables = Triggerable.all().filter((triggerable) => !triggerable.isDisabled());
221
222         const acceptedTests = new Set;
223         for (const triggerable of enabledTriggerables) {
224             for (const test of triggerable.acceptedTests())
225                 acceptedTests.add(test);
226         }
227
228         let tests = Test.all().filter((test) => acceptedTests.has(test) && (!test.parentTest() || !acceptedTests.has(test.parentTest())));
229         return this._renderRadioButtonList(this.content('test-list'), 'test', tests, this.selectTests.bind(this), (test) => test.fullName());
230     }
231
232     _renderTriggerablePlatforms(selectedTests, triggerablePlatforms)
233     {
234         if (!selectedTests.length) {
235             this.content('platform-pane').style.display = 'none';
236             return null;
237         }
238         this.content('platform-pane').style.display = null;
239
240         return this._renderRadioButtonList(this.content('platform-list'), 'platform', triggerablePlatforms, (selectedPlatforms) => {
241             this.selectPlatform(selectedPlatforms.length ? selectedPlatforms[0] : null);
242         });
243     }
244
245     _renderRadioButtonList(listContainer, name, objects, callback, labelForObject = (object) => object.label())
246     {
247         const listItems = [];
248         let selectedListItems = [];
249         const checkSelectedRadioButtons = (newSelectedListItems) => {
250             selectedListItems.forEach((item) => {
251                 item.label.classList.remove('selected');
252                 item.radioButton.checked = false;
253             });
254             selectedListItems = newSelectedListItems;
255             selectedListItems.forEach((item) => {
256                 item.label.classList.add('selected');
257                 item.radioButton.checked = true;
258             });
259         }
260
261         const element = ComponentBase.createElement;
262         this.renderReplace(listContainer, objects.map((object) => {
263             const radioButton = element('input', {type: 'radio', name: name, onchange: () => {
264                 checkSelectedRadioButtons(listItems.filter((item) => item.radioButton.checked));
265                 callback(selectedListItems.map((item) => item.object));
266                 this.enqueueToRender();
267             }});
268             const label = element('label', [radioButton, labelForObject(object)]);
269             listItems.push({radioButton, label, object});
270             return element('li', label);
271         }));
272
273         return new LazilyEvaluatedFunction((...selectedObjects) => {
274             const objects = new Set(selectedObjects);
275             checkSelectedRadioButtons(listItems.filter((item) => objects.has(item.object)));
276         });
277     }
278
279     _updateTriggerable(tests, platform)
280     {
281         let triggerable = null;
282         let error = null;
283         if (tests.length && platform) {
284             triggerable = Triggerable.findByTestConfiguration(tests[0], platform);
285             let matchingTests = new Set;
286             let mismatchingTests = new Set;
287             for (let test of tests) {
288                 if (Triggerable.findByTestConfiguration(test, platform) == triggerable)
289                     matchingTests.add(test);
290                 else
291                     mismatchingTests.add(test);
292             }
293             if (matchingTests.size < tests.length) {
294                 const matchingTestNames = [...matchingTests].map((test) => test.fullName()).sort().join('", "');
295                 const mismathingTestNames = [...mismatchingTests].map((test) => test.fullName()).sort().join('", "');
296                 error = `Tests "${matchingTestNames}" and "${mismathingTestNames}" cannot be scheduled
297                     simultenosuly on "${platform.name()}". Please select one of them at a time.`;
298             }
299         }
300
301         return [triggerable, error];
302     }
303
304     _updateRepositoryGroups(triggerable)
305     {
306         const repositoryGroups = triggerable ? TriggerableRepositoryGroup.sortByNamePreferringSmallerRepositories(triggerable.repositoryGroups()) : [];
307         for (let name in this._repositoryGroupByConfiguration) {
308             const currentGroup = this._repositoryGroupByConfiguration[name];
309             let matchingGroup = null;
310             if (currentGroup) {
311                 if (repositoryGroups.includes(currentGroup))
312                     matchingGroup = currentGroup;
313                 else
314                     matchingGroup = repositoryGroups.find((group) => group.name() == currentGroup.name());
315             }
316             if (!matchingGroup && repositoryGroups.length)
317                 matchingGroup = repositoryGroups[0];
318             this._repositoryGroupByConfiguration[name] = matchingGroup;
319         }
320     }
321
322     _updateCommitSetMap()
323     {
324         const newBaseline = this._computeCommitSet('Baseline');
325         let newComparison = this._computeCommitSet('Comparison');
326         if (newBaseline && newComparison && newBaseline.equals(newComparison))
327             newComparison = null;
328
329         const currentBaseline = this._commitSetMap['Baseline'];
330         const currentComparison = this._commitSetMap['Comparison'];
331         const areCommitSetsEqual = (commitSetA, commitSetB) => commitSetA == commitSetB || (commitSetA && commitSetB && commitSetA.equals(commitSetB));
332         const sameBaselineConfig = areCommitSetsEqual(currentBaseline, newBaseline);
333         const sameComparisionConfig = areCommitSetsEqual(currentComparison, newComparison);
334
335         if (sameBaselineConfig && sameComparisionConfig)
336             return;
337
338         this._commitSetMap = {'Baseline': newBaseline, 'Comparison': newComparison};
339
340         this.dispatchAction('testConfigChange');
341         this.enqueueToRender();
342     }
343
344     _computeCommitSet(configurationName)
345     {
346         const repositoryGroup = this._repositoryGroupByConfiguration[configurationName];
347         if (!repositoryGroup)
348             return null;
349
350         const fileUploader = this._customRootUploaders[configurationName];
351         if (!fileUploader || fileUploader.hasFileToUpload())
352             return null;
353
354         const commitSet = new CustomCommitSet;
355         for (let repository of repositoryGroup.repositories()) {
356             let revision = this._specifiedRevisions[configurationName].get(repository);
357             const commit = this._fetchedCommits[configurationName].get(repository);
358             if (commit) {
359                 const commitLabel = commit.label();
360                 if (!revision || commit.revision().startsWith(revision) || commitLabel.startsWith(revision) || revision.startsWith(commitLabel))
361                     revision = commit.revision();
362             }
363             if (!revision)
364                 return null;
365             let patch = null;
366             if (repositoryGroup.acceptsPatchForRepository(repository)) {
367                 const uploaderMap = this._patchUploaders[configurationName];
368                 const uploader = uploaderMap.get(repository);
369                 if (uploader) {
370                     const files = uploader.uploadedFiles();
371                     console.assert(files.length <= 1);
372                     if (files.length)
373                         patch = files[0];
374                 }
375             }
376             commitSet.setRevisionForRepository(repository, revision, patch);
377         }
378
379         for (let uploadedFile of fileUploader.uploadedFiles())
380             commitSet.addCustomRoot(uploadedFile);
381
382         return commitSet;
383     }
384
385     async _fetchCommitsForConfiguration(configurationName)
386     {
387         const commitSet = this._commitSetMap[configurationName];
388         if (!commitSet)
389             return;
390
391         const specifiedRevisions = this._specifiedRevisions[configurationName];
392         const fetchedCommits = this._fetchedCommits[configurationName];
393         const invalidRevisionForRepository = this._invalidRevisionsByConfiguration[configurationName];
394
395         await Promise.all(Array.from(commitSet.repositories()).map((repository) => {
396             const revision = commitSet.revisionForRepository(repository);
397             return this._resolveRevision(repository, revision, specifiedRevisions, invalidRevisionForRepository, fetchedCommits);
398         }));
399
400         const latestCommitSet = this._commitSetMap[configurationName];
401         if (commitSet != latestCommitSet)
402             return;
403         this.enqueueToRender();
404     }
405
406     async _resolveRevision(repository, revision, specifiedRevisions, invalidRevisionForRepository, fetchedCommits)
407     {
408         const fetchedCommit = fetchedCommits.get(repository);
409         const specifiedRevision = specifiedRevisions.get(repository);
410         if (fetchedCommit && fetchedCommit.revision() == revision && (!specifiedRevision || specifiedRevision == revision))
411             return;
412
413         fetchedCommits.delete(repository);
414         let commits = [];
415         const revisionToFetch = specifiedRevision || revision;
416         try {
417             commits = await CommitLog.fetchForSingleRevision(repository, revisionToFetch, true);
418         } catch (error) {
419             console.assert(error == 'UnknownCommit' || error == 'AmbiguousRevisionPrefix');
420             if (revisionToFetch != specifiedRevisions.get(repository))
421                 return;
422             invalidRevisionForRepository.set(repository, `"${revisionToFetch}": ${error == 'UnknownCommit' ? 'Invalid revision' : 'Ambiguous revision prefix'}`);
423             return;
424         }
425         console.assert(commits.length, 1);
426         if (revisionToFetch != specifiedRevisions.get(repository))
427             return;
428         invalidRevisionForRepository.delete(repository);
429         fetchedCommits.set(repository, commits[0]);
430         if (revisionToFetch != commits[0].revision())
431             this._updateCommitSetMap();
432     }
433
434     _renderRepositoryPanes(triggerable, error, platform, repositoryGroupByConfiguration, showComparison)
435     {
436         this.content('repository-configuration-error-pane').style.display = error ? null : 'none';
437         this.content('error').textContent = error;
438
439         this.content('baseline-configuration-pane').style.display = triggerable ? null : 'none';
440         this.content('specify-comparison-pane').style.display = triggerable && !showComparison ? null : 'none';
441         this.content('comparison-configuration-pane').style.display = triggerable && showComparison ? null : 'none';
442
443         if (!triggerable)
444             return;
445
446         const visibleRepositoryGroups = triggerable.repositoryGroups().filter((group) => !group.isHidden());
447         const repositoryGroups = TriggerableRepositoryGroup.sortByNamePreferringSmallerRepositories(visibleRepositoryGroups);
448
449         const repositorySet = new Set;
450         for (let group of repositoryGroups) {
451             for (let repository of group.repositories())
452                 repositorySet.add(repository);
453         }
454
455         const repositories = Repository.sortByNamePreferringOnesWithURL([...repositorySet]);
456         const requiredRepositories = repositories.filter((repository) => {
457             return repositoryGroups.every((group) => group.repositories().includes(repository));
458         });
459         const alwaysAcceptsCustomRoots = repositoryGroups.every((group) => group.acceptsCustomRoots());
460
461         this._renderBaselineRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots);
462
463         if (showComparison)
464             this._renderComparisonRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots);
465     }
466
467     _renderBaselineRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots)
468     {
469         let currentGroup = repositoryGroupByConfiguration['Baseline'];
470         const optionalRepositoryList = this._optionalRepositoryList(currentGroup, requiredRepositories);
471         this.renderReplace(this.content('baseline-revision-table'),
472             this._buildRevisionTable('Baseline', repositoryGroups, currentGroup, platform, requiredRepositories, optionalRepositoryList, alwaysAcceptsCustomRoots));
473     }
474
475     _renderComparisonRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots)
476     {
477         let currentGroup = repositoryGroupByConfiguration['Comparison'];
478         const optionalRepositoryList = this._optionalRepositoryList(currentGroup, requiredRepositories);
479         this.renderReplace(this.content('comparison-revision-table'),
480             this._buildRevisionTable('Comparison', repositoryGroups, currentGroup, platform, requiredRepositories, optionalRepositoryList, alwaysAcceptsCustomRoots));
481     }
482
483     _optionalRepositoryList(currentGroup, requiredRepositories)
484     {
485         if (!currentGroup)
486             return [];
487         return Repository.sortByNamePreferringOnesWithURL(currentGroup.repositories().filter((repository) => !requiredRepositories.includes(repository)));
488     }
489
490     _buildRevisionTable(configurationName, repositoryGroups, currentGroup, platform, requiredRepositories, optionalRepositoryList, alwaysAcceptsCustomRoots)
491     {
492         const element = ComponentBase.createElement;
493
494         const customRootsTBody = element('tbody', [
495             element('tr', [
496                 element('th', 'Roots'),
497                 element('td', this._customRootUploaders[configurationName]),
498             ]),
499         ]);
500
501         return [
502             element('tbody',
503                 requiredRepositories.map((repository) => {
504                     return element('tr', [
505                         element('th', repository.name()),
506                         element('td', this._buildRevisionInput(configurationName, repository, platform))
507                     ]);
508                 })),
509             alwaysAcceptsCustomRoots ? customRootsTBody : [],
510             element('tbody', [
511                 element('tr', {'class': 'group-row'},
512                     element('td', {colspan: 2}, this._buildRepositoryGroupList(repositoryGroups, currentGroup, configurationName))),
513             ]),
514             !alwaysAcceptsCustomRoots && currentGroup && currentGroup.acceptsCustomRoots() ? customRootsTBody : [],
515             element('tbody',
516                 optionalRepositoryList.map((repository) => {
517                     let uploader = currentGroup.acceptsPatchForRepository(repository)
518                         ? this._ensurePatchUploader(configurationName, repository) : null;
519
520                     return element('tr',[
521                         element('th', repository.name()),
522                         element('td', [
523                             this._buildRevisionInput(configurationName, repository, platform),
524                             uploader || [],
525                         ])
526                     ]);
527                 })
528             )];
529     }
530
531     _buildTestabilityList(commitSet, configurationName, invalidRevisionForRepository)
532     {
533         const element = ComponentBase.createElement;
534         const entries = [];
535
536         if (!commitSet || !commitSet.repositories().length)
537             return [];
538
539         for (const repository of commitSet.repositories()) {
540             const commit = this._fetchedCommits[configurationName].get(repository);
541             if (commit && commit.testability() && !invalidRevisionForRepository.has(repository))
542                 entries.push(element('li', `${commit.repository().name()} - "${commit.label()}": ${commit.testability()}`));
543             if (invalidRevisionForRepository.has(repository))
544                 entries.push(element('li', `${repository.name()} - ${invalidRevisionForRepository.get(repository)}`));
545         }
546
547         return entries;
548     }
549
550     _buildRepositoryGroupList(repositoryGroups, currentGroup, configurationName)
551     {
552         const element = ComponentBase.createElement;
553         return repositoryGroups.map((group) => {
554             const input = element('input', {
555                 type: 'radio',
556                 name: 'repositoryGroup-for-' + configurationName.toLowerCase(),
557                 checked: currentGroup == group,
558                 onchange: () => this._selectRepositoryGroup(configurationName, group)
559             });
560             return [element('label', [input, group.description()])];
561         });
562     }
563
564     _selectRepositoryGroup(configurationName, group)
565     {
566         const source = this._repositoryGroupByConfiguration;
567         const clone = {};
568         for (let key in source)
569             clone[key] = source[key];
570         clone[configurationName] = group;
571         this._repositoryGroupByConfiguration = clone;
572         this._updateCommitSetMap();
573         this._fetchCommitsForConfiguration(configurationName);
574         this.enqueueToRender();
575     }
576
577     _buildRevisionInput(configurationName, repository, platform)
578     {
579         const revision = this._specifiedRevisions[configurationName].get(repository) || '';
580         const element = ComponentBase.createElement;
581         let scheduledUpdate = null;
582         const input = element('input', {value: revision, oninput: () => {
583             unmodifiedInput = null;
584             const revisionToFetch = input.value;
585             this._specifiedRevisions[configurationName].set(repository, revisionToFetch);
586             this._updateCommitSetMap();
587             if (scheduledUpdate)
588                 clearTimeout(scheduledUpdate);
589             scheduledUpdate = setTimeout(() => {
590                 if (revisionToFetch == input.value)
591                     this._fetchCommitsForConfiguration(configurationName);
592                 scheduledUpdate = null;
593             }, CustomAnalysisTaskConfigurator.commitFetchInterval);
594         }});
595         let unmodifiedInput = input;
596
597         if (!revision) {
598             CommitLog.fetchLatestCommitForPlatform(repository, platform).then((commit) => {
599                 if (commit && unmodifiedInput) {
600                     unmodifiedInput.value = commit.revision();
601                     this._fetchedCommits[configurationName].set(repository, commit);
602                     this._updateCommitSetMap();
603                 }
604             });
605         }
606
607         return input;
608     }
609
610     static htmlTemplate()
611     {
612         return `
613             <section id="test-pane" class="pane">
614                 <h2>1. Select a Test</h2>
615                 <ul id="test-list" class="config-list"></ul>
616             </section>
617             <section id="platform-pane" class="pane">
618                 <h2>2. Select a Platform</h2>
619                 <ul id="platform-list" class="config-list"></ul>
620             </section>
621             <section id="repository-configuration-error-pane" class="pane">
622                 <h2>Incompatible tests</h2>
623                 <p id="error"></p>
624             </section>
625             <section id="baseline-configuration-pane" class="pane">
626                 <h2>3. Configure Baseline</h2>
627                 <table id="baseline-revision-table" class="revision-table"></table>
628                 <ul id="baseline-testability"></ul>
629             </section>
630             <section id="specify-comparison-pane" class="pane">
631                 <button id="specify-comparison-button">Configure to Compare</button>
632             </section>
633             <section id="comparison-configuration-pane" class="pane">
634                 <h2>4. Configure Comparison</h2>
635                 <table id="comparison-revision-table" class="revision-table"></table>
636                 <ul id="comparison-testability"></ul>
637             </section>`;
638     }
639
640     static cssTemplate()
641     {
642         return `
643             :host {
644                 display: flex !important;
645                 flex-direction: row !important;
646             }
647             .pane {
648                 margin-right: 1rem;
649                 padding: 0;
650             }
651             .pane h2 {
652                 padding: 0;
653                 margin: 0;
654                 margin-bottom: 0.5rem;
655                 font-size: 1.2rem;
656                 font-weight: inherit;
657                 text-align: center;
658                 white-space: nowrap;
659             }
660
661             .config-list {
662                 height: 20rem;
663                 overflow: scroll;
664                 display: block;
665                 margin: 0;
666                 padding: 0;
667                 list-style: none;
668                 font-size: inherit;
669                 font-weight: inherit;
670                 border: none;
671                 border-top: solid 1px #ddd;
672                 border-bottom: solid 1px #ddd;
673                 white-space: nowrap;
674             }
675
676             #platform-list:empty:before {
677                 content: "No matching platform";
678                 display: block;
679                 margin: 1rem 0.5rem;
680                 text-align: center;
681             }
682
683             .config-list label {
684                 display: block;
685                 padding: 0.1rem 0.2rem;
686             }
687
688             .config-list label:hover,
689             .config-list a:hover {
690                 background: rgba(204, 153, 51, 0.1);
691             }
692
693             .config-list label.selected,
694             .config-list a.selected {
695                 background: #eee;
696             }
697
698             .config-list a {
699                 display: block;
700                 padding: 0.1rem 0.2rem;
701                 text-decoration: none;
702                 color: inherit;
703             }
704
705             #repository-configuration-pane {
706                 position: relative;
707             }
708
709             #repository-configuration-pane > button  {
710                 margin-left: 19.5rem;
711             }
712
713             .revision-table {
714                 border: none;
715                 border-collapse: collapse;
716                 font-size: 1rem;
717             }
718
719             .revision-table thead {
720                 font-size: 1.2rem;
721             }
722
723             .revision-table tbody:empty {
724                 display: none;
725             }
726
727             .revision-table tbody td,
728             .revision-table tbody th {
729                 border-top: solid 1px #ddd;
730                 padding-top: 0.5rem;
731                 padding-bottom: 0.5rem;
732             }
733
734             .revision-table td,
735             .revision-table th {
736                 width: 15rem;
737                 height: 1.8rem;
738                 padding: 0 0.2rem;
739                 border: none;
740                 font-weight: inherit;
741             }
742
743             .revision-table thead th {
744                 text-align: center;
745             }
746
747             .revision-table th close-button {
748                 vertical-align: bottom;
749             }
750
751             .revision-table td:first-child,
752             .revision-table th:first-child {
753                 width: 6rem;
754             }
755
756             .revision-table tr.group-row td {
757                 padding-left: 5rem;
758             }
759
760             label {
761                 white-space: nowrap;
762                 display: block;
763             }
764
765             input:not([type=radio]) {
766                 width: calc(100% - 0.6rem);
767                 padding: 0.1rem 0.2rem;
768                 font-size: 0.9rem;
769                 font-weight: inherit;
770             }
771
772             #specify-comparison-pane button {
773                 margin-top: 1.5rem;
774                 font-size: 1.1rem;
775                 font-weight: inherit;
776             }
777
778             #start-pane button {
779                 margin-top: 1.5rem;
780                 font-size: 1.2rem;
781                 font-weight: inherit;
782             }
783
784             #baseline-testability li,
785             #comparison-testability li {
786                 color: #c33;
787                 width: 20rem;
788             }
789 `;
790     }
791 }
792
793 CustomAnalysisTaskConfigurator.commitFetchInterval = 100;
794
795 ComponentBase.defineElement('custom-analysis-task-configurator', CustomAnalysisTaskConfigurator);