'custom-configuration-test-group-form' should update test name when selected test...
[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             if (!revision) {
358                 const commit = this._fetchedCommits[configurationName].get(repository);
359                 if (commit)
360                     revision = commit.revision();
361             }
362             if (!revision)
363                 return null;
364             let patch = null;
365             if (repositoryGroup.acceptsPatchForRepository(repository)) {
366                 const uploaderMap = this._patchUploaders[configurationName];
367                 const uploader = uploaderMap.get(repository);
368                 if (uploader) {
369                     const files = uploader.uploadedFiles();
370                     console.assert(files.length <= 1);
371                     if (files.length)
372                         patch = files[0];
373                 }
374             }
375             commitSet.setRevisionForRepository(repository, revision, patch);
376         }
377
378         for (let uploadedFile of fileUploader.uploadedFiles())
379             commitSet.addCustomRoot(uploadedFile);
380
381         return commitSet;
382     }
383
384     async _fetchCommitsForConfiguration(configurationName)
385     {
386         const commitSet = this._commitSetMap[configurationName];
387         if (!commitSet)
388             return;
389
390         const specifiedRevisions = this._specifiedRevisions[configurationName];
391         const fetchedCommits = this._fetchedCommits[configurationName];
392         const invalidRevisionForRepository = this._invalidRevisionsByConfiguration[configurationName];
393
394         await Promise.all(Array.from(commitSet.repositories()).map((repository) => {
395             const revision = commitSet.revisionForRepository(repository);
396             return this._resolveRevision(repository, revision, specifiedRevisions, invalidRevisionForRepository, fetchedCommits);
397         }));
398
399         const latestCommitSet = this._commitSetMap[configurationName];
400         if (commitSet != latestCommitSet)
401             return;
402         this.enqueueToRender();
403     }
404
405     async _resolveRevision(repository, revision, specifiedRevisions, invalidRevisionForRepository, fetchedCommits)
406     {
407         const fetchedCommit = fetchedCommits.get(repository);
408         if (fetchedCommit && fetchedCommit.revision() == revision)
409             return;
410
411         fetchedCommits.delete(repository);
412         let commits = [];
413         try {
414             commits = await CommitLog.fetchForSingleRevision(repository, revision);
415         } catch (error) {
416             console.assert(error == 'UnknownCommit');
417             if (revision != specifiedRevisions.get(repository))
418                 return;
419             invalidRevisionForRepository.set(repository, revision);
420             return;
421         }
422         console.assert(commits.length, 1);
423         if (revision != specifiedRevisions.get(repository))
424             return;
425         invalidRevisionForRepository.delete(repository);
426         fetchedCommits.set(repository, commits[0]);
427     }
428
429     _renderRepositoryPanes(triggerable, error, platform, repositoryGroupByConfiguration, showComparison)
430     {
431         this.content('repository-configuration-error-pane').style.display = error ? null : 'none';
432         this.content('error').textContent = error;
433
434         this.content('baseline-configuration-pane').style.display = triggerable ? null : 'none';
435         this.content('specify-comparison-pane').style.display = triggerable && !showComparison ? null : 'none';
436         this.content('comparison-configuration-pane').style.display = triggerable && showComparison ? null : 'none';
437
438         if (!triggerable)
439             return;
440
441         const visibleRepositoryGroups = triggerable.repositoryGroups().filter((group) => !group.isHidden());
442         const repositoryGroups = TriggerableRepositoryGroup.sortByNamePreferringSmallerRepositories(visibleRepositoryGroups);
443
444         const repositorySet = new Set;
445         for (let group of repositoryGroups) {
446             for (let repository of group.repositories())
447                 repositorySet.add(repository);
448         }
449
450         const repositories = Repository.sortByNamePreferringOnesWithURL([...repositorySet]);
451         const requiredRepositories = repositories.filter((repository) => {
452             return repositoryGroups.every((group) => group.repositories().includes(repository));
453         });
454         const alwaysAcceptsCustomRoots = repositoryGroups.every((group) => group.acceptsCustomRoots());
455
456         this._renderBaselineRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots);
457
458         if (showComparison)
459             this._renderComparisonRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots);
460     }
461
462     _renderBaselineRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots)
463     {
464         let currentGroup = repositoryGroupByConfiguration['Baseline'];
465         const optionalRepositoryList = this._optionalRepositoryList(currentGroup, requiredRepositories);
466         this.renderReplace(this.content('baseline-revision-table'),
467             this._buildRevisionTable('Baseline', repositoryGroups, currentGroup, platform, requiredRepositories, optionalRepositoryList, alwaysAcceptsCustomRoots));
468     }
469
470     _renderComparisonRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots)
471     {
472         let currentGroup = repositoryGroupByConfiguration['Comparison'];
473         const optionalRepositoryList = this._optionalRepositoryList(currentGroup, requiredRepositories);
474         this.renderReplace(this.content('comparison-revision-table'),
475             this._buildRevisionTable('Comparison', repositoryGroups, currentGroup, platform, requiredRepositories, optionalRepositoryList, alwaysAcceptsCustomRoots));
476     }
477
478     _optionalRepositoryList(currentGroup, requiredRepositories)
479     {
480         if (!currentGroup)
481             return [];
482         return Repository.sortByNamePreferringOnesWithURL(currentGroup.repositories().filter((repository) => !requiredRepositories.includes(repository)));
483     }
484
485     _buildRevisionTable(configurationName, repositoryGroups, currentGroup, platform, requiredRepositories, optionalRepositoryList, alwaysAcceptsCustomRoots)
486     {
487         const element = ComponentBase.createElement;
488
489         const customRootsTBody = element('tbody', [
490             element('tr', [
491                 element('th', 'Roots'),
492                 element('td', this._customRootUploaders[configurationName]),
493             ]),
494         ]);
495
496         return [
497             element('tbody',
498                 requiredRepositories.map((repository) => {
499                     return element('tr', [
500                         element('th', repository.name()),
501                         element('td', this._buildRevisionInput(configurationName, repository, platform))
502                     ]);
503                 })),
504             alwaysAcceptsCustomRoots ? customRootsTBody : [],
505             element('tbody', [
506                 element('tr', {'class': 'group-row'},
507                     element('td', {colspan: 2}, this._buildRepositoryGroupList(repositoryGroups, currentGroup, configurationName))),
508             ]),
509             !alwaysAcceptsCustomRoots && currentGroup && currentGroup.acceptsCustomRoots() ? customRootsTBody : [],
510             element('tbody',
511                 optionalRepositoryList.map((repository) => {
512                     let uploader = currentGroup.acceptsPatchForRepository(repository)
513                         ? this._ensurePatchUploader(configurationName, repository) : null;
514
515                     return element('tr',[
516                         element('th', repository.name()),
517                         element('td', [
518                             this._buildRevisionInput(configurationName, repository, platform),
519                             uploader || [],
520                         ])
521                     ]);
522                 })
523             )];
524     }
525
526     _buildTestabilityList(commitSet, configurationName, invalidRevisionForRepository)
527     {
528         const element = ComponentBase.createElement;
529         const entries = [];
530
531         if (!commitSet || !commitSet.repositories().length)
532             return [];
533
534         for (const repository of commitSet.repositories()) {
535             const commit = this._fetchedCommits[configurationName].get(repository);
536             if (commit && commit.testability() && !invalidRevisionForRepository.has(repository))
537                 entries.push(element('li', `${commit.repository().name()} - "${commit.label()}": ${commit.testability()}`));
538             if (invalidRevisionForRepository.has(repository))
539                 entries.push(element('li', `${repository.name()} - "${invalidRevisionForRepository.get(repository)}": Invalid revision`));
540         }
541
542         return entries;
543     }
544
545     _buildRepositoryGroupList(repositoryGroups, currentGroup, configurationName)
546     {
547         const element = ComponentBase.createElement;
548         return repositoryGroups.map((group) => {
549             const input = element('input', {
550                 type: 'radio',
551                 name: 'repositoryGroup-for-' + configurationName.toLowerCase(),
552                 checked: currentGroup == group,
553                 onchange: () => this._selectRepositoryGroup(configurationName, group)
554             });
555             return [element('label', [input, group.description()])];
556         });
557     }
558
559     _selectRepositoryGroup(configurationName, group)
560     {
561         const source = this._repositoryGroupByConfiguration;
562         const clone = {};
563         for (let key in source)
564             clone[key] = source[key];
565         clone[configurationName] = group;
566         this._repositoryGroupByConfiguration = clone;
567         this._updateCommitSetMap();
568         this._fetchCommitsForConfiguration(configurationName);
569         this.enqueueToRender();
570     }
571
572     _buildRevisionInput(configurationName, repository, platform)
573     {
574         const revision = this._specifiedRevisions[configurationName].get(repository) || '';
575         const element = ComponentBase.createElement;
576         let scheduledUpdate = null;
577         const input = element('input', {value: revision, oninput: () => {
578             unmodifiedInput = null;
579             const revisionToFetch = input.value;
580             this._specifiedRevisions[configurationName].set(repository, revisionToFetch);
581             this._updateCommitSetMap();
582             if (scheduledUpdate)
583                 clearTimeout(scheduledUpdate);
584             scheduledUpdate = setTimeout(() => {
585                 if (revisionToFetch == input.value)
586                     this._fetchCommitsForConfiguration(configurationName);
587                 scheduledUpdate = null;
588             }, CustomAnalysisTaskConfigurator.commitFetchInterval);
589         }});
590         let unmodifiedInput = input;
591
592         if (!revision) {
593             CommitLog.fetchLatestCommitForPlatform(repository, platform).then((commit) => {
594                 if (commit && unmodifiedInput) {
595                     unmodifiedInput.value = commit.revision();
596                     this._fetchedCommits[configurationName].set(repository, commit);
597                     this._updateCommitSetMap();
598                 }
599             });
600         }
601
602         return input;
603     }
604
605     static htmlTemplate()
606     {
607         return `
608             <section id="test-pane" class="pane">
609                 <h2>1. Select a Test</h2>
610                 <ul id="test-list" class="config-list"></ul>
611             </section>
612             <section id="platform-pane" class="pane">
613                 <h2>2. Select a Platform</h2>
614                 <ul id="platform-list" class="config-list"></ul>
615             </section>
616             <section id="repository-configuration-error-pane" class="pane">
617                 <h2>Incompatible tests</h2>
618                 <p id="error"></p>
619             </section>
620             <section id="baseline-configuration-pane" class="pane">
621                 <h2>3. Configure Baseline</h2>
622                 <table id="baseline-revision-table" class="revision-table"></table>
623                 <ul id="baseline-testability"></ul>
624             </section>
625             <section id="specify-comparison-pane" class="pane">
626                 <button id="specify-comparison-button">Configure to Compare</button>
627             </section>
628             <section id="comparison-configuration-pane" class="pane">
629                 <h2>4. Configure Comparison</h2>
630                 <table id="comparison-revision-table" class="revision-table"></table>
631                 <ul id="comparison-testability"></ul>
632             </section>`;
633     }
634
635     static cssTemplate()
636     {
637         return `
638             :host {
639                 display: flex !important;
640                 flex-direction: row !important;
641             }
642             .pane {
643                 margin-right: 1rem;
644                 padding: 0;
645             }
646             .pane h2 {
647                 padding: 0;
648                 margin: 0;
649                 margin-bottom: 0.5rem;
650                 font-size: 1.2rem;
651                 font-weight: inherit;
652                 text-align: center;
653                 white-space: nowrap;
654             }
655
656             .config-list {
657                 height: 20rem;
658                 overflow: scroll;
659                 display: block;
660                 margin: 0;
661                 padding: 0;
662                 list-style: none;
663                 font-size: inherit;
664                 font-weight: inherit;
665                 border: none;
666                 border-top: solid 1px #ddd;
667                 border-bottom: solid 1px #ddd;
668                 white-space: nowrap;
669             }
670
671             #platform-list:empty:before {
672                 content: "No matching platform";
673                 display: block;
674                 margin: 1rem 0.5rem;
675                 text-align: center;
676             }
677
678             .config-list label {
679                 display: block;
680                 padding: 0.1rem 0.2rem;
681             }
682
683             .config-list label:hover,
684             .config-list a:hover {
685                 background: rgba(204, 153, 51, 0.1);
686             }
687
688             .config-list label.selected,
689             .config-list a.selected {
690                 background: #eee;
691             }
692
693             .config-list a {
694                 display: block;
695                 padding: 0.1rem 0.2rem;
696                 text-decoration: none;
697                 color: inherit;
698             }
699
700             #repository-configuration-pane {
701                 position: relative;
702             }
703
704             #repository-configuration-pane > button  {
705                 margin-left: 19.5rem;
706             }
707
708             .revision-table {
709                 border: none;
710                 border-collapse: collapse;
711                 font-size: 1rem;
712             }
713
714             .revision-table thead {
715                 font-size: 1.2rem;
716             }
717
718             .revision-table tbody:empty {
719                 display: none;
720             }
721
722             .revision-table tbody td,
723             .revision-table tbody th {
724                 border-top: solid 1px #ddd;
725                 padding-top: 0.5rem;
726                 padding-bottom: 0.5rem;
727             }
728
729             .revision-table td,
730             .revision-table th {
731                 width: 15rem;
732                 height: 1.8rem;
733                 padding: 0 0.2rem;
734                 border: none;
735                 font-weight: inherit;
736             }
737
738             .revision-table thead th {
739                 text-align: center;
740             }
741
742             .revision-table th close-button {
743                 vertical-align: bottom;
744             }
745
746             .revision-table td:first-child,
747             .revision-table th:first-child {
748                 width: 6rem;
749             }
750
751             .revision-table tr.group-row td {
752                 padding-left: 5rem;
753             }
754
755             label {
756                 white-space: nowrap;
757                 display: block;
758             }
759
760             input:not([type=radio]) {
761                 width: calc(100% - 0.6rem);
762                 padding: 0.1rem 0.2rem;
763                 font-size: 0.9rem;
764                 font-weight: inherit;
765             }
766
767             #specify-comparison-pane button {
768                 margin-top: 1.5rem;
769                 font-size: 1.1rem;
770                 font-weight: inherit;
771             }
772
773             #start-pane button {
774                 margin-top: 1.5rem;
775                 font-size: 1.2rem;
776                 font-weight: inherit;
777             }
778
779             #baseline-testability li,
780             #comparison-testability li {
781                 color: #c33;
782                 width: 20rem;
783             }
784 `;
785     }
786 }
787
788 CustomAnalysisTaskConfigurator.commitFetchInterval = 100;
789
790 ComponentBase.defineElement('custom-analysis-task-configurator', CustomAnalysisTaskConfigurator);