d328769d92e87be65715a196990f6f9b7ffbb692
[WebKit-https.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._configurationNames = ['Baseline', 'Comparison'];
11         this._showComparison = false;
12         this._commitSetMap = {};
13         this._specifiedRevisions = {'Baseline': new Map, 'Comparison': new Map};
14         this._fetchedRevisions = {'Baseline': new Map, 'Comparison': new Map};
15         this._repositoryGroupByConfiguration = {'Baseline': null, 'Comparison': null};
16         this._updateTriggerableLazily = new LazilyEvaluatedFunction(this._updateTriggerable.bind(this));
17
18         this._renderTriggerableTestsLazily = new LazilyEvaluatedFunction(this._renderTriggerableTests.bind(this));
19         this._renderTriggerablePlatformsLazily = new LazilyEvaluatedFunction(this._renderTriggerablePlatforms.bind(this));
20         this._renderRepositoryPanesLazily = new LazilyEvaluatedFunction(this._renderRepositoryPanes.bind(this));
21
22         this._fileUploaders = {};
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.enqueueToRender();
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         this._updateCommitSetMap();
53
54         this.enqueueToRender();
55     }
56
57     setCommitSets(baselineCommitSet, comparisonCommitSet)
58     {
59         const [triggerable, error] = this._updateTriggerableLazily.evaluate(this._selectedTests, this._selectedPlatform);
60
61         if (!triggerable)
62             return;
63
64         const baselineRepositoryGroup = triggerable.repositoryGroups().find((repositoryGroup) => repositoryGroup.accepts(baselineCommitSet));
65         if (baselineRepositoryGroup) {
66             this._repositoryGroupByConfiguration['Baseline'] = baselineRepositoryGroup;
67             this._setUploadedFilesIfEmpty(this._fileUploaders['Baseline'], baselineCommitSet);
68             this._specifiedRevisions['Baseline'] = this._revisionMapFromCommitSet(baselineCommitSet);
69         }
70
71         const comparisonRepositoryGroup = triggerable.repositoryGroups().find((repositoryGroup) => repositoryGroup.accepts(baselineCommitSet));
72         if (comparisonRepositoryGroup) {
73             this._repositoryGroupByConfiguration['Comparison'] = comparisonRepositoryGroup;
74             this._setUploadedFilesIfEmpty(this._fileUploaders['Comparison'], comparisonCommitSet);
75             this._specifiedRevisions['Comparison'] = this._revisionMapFromCommitSet(comparisonCommitSet);
76         }
77
78         this._showComparison = true;
79         this._updateCommitSetMap();
80     }
81
82     _setUploadedFilesIfEmpty(uploader, commitSet)
83     {
84         if (uploader.hasFileToUpload() || uploader.uploadedFiles().length)
85             return;
86         for (const uploadedFile of commitSet.customRoots())
87             uploader.addUploadedFile(uploadedFile);
88     }
89
90     _revisionMapFromCommitSet(commitSet)
91     {
92         const revisionMap = new Map;
93         for (const repository of commitSet.repositories())
94             revisionMap.set(repository, commitSet.revisionForRepository(repository));
95         return revisionMap;
96     }
97
98     didConstructShadowTree()
99     {
100         this.content('specify-comparison-button').onclick = this.createEventHandler(() => this._configureComparison());
101
102         const baselineRootsUploader = new InstantFileUploader;
103         baselineRootsUploader.listenToAction('uploadedFile', (uploadedFile) => {
104             comparisonRootsUploader.addUploadedFile(uploadedFile);
105             this._updateCommitSetMap();
106         });
107         baselineRootsUploader.listenToAction('removedFile', () => this._updateCommitSetMap());
108         this._fileUploaders['Baseline'] = baselineRootsUploader;
109
110         const comparisonRootsUploader = new InstantFileUploader;
111         comparisonRootsUploader.listenToAction('uploadedFile', () => this._updateCommitSetMap());
112         comparisonRootsUploader.listenToAction('removedFile', () => this._updateCommitSetMap());
113         this._fileUploaders['Comparison'] = comparisonRootsUploader;
114     }
115
116     _configureComparison()
117     {
118         this._showComparison = true;
119         this._repositoryGroupByConfiguration['Comparison'] = this._repositoryGroupByConfiguration['Baseline'];
120
121         const specifiedBaselineRevisions = this._specifiedRevisions['Baseline'];
122         const specifiedComparisonRevisions = new Map;
123         for (let key of specifiedBaselineRevisions.keys())
124             specifiedComparisonRevisions.set(key, specifiedBaselineRevisions.get(key));
125         this._specifiedRevisions['Comparison'] = specifiedComparisonRevisions;
126
127         this.enqueueToRender();
128     }
129
130     render()
131     {
132         super.render();
133
134         const updateSelectedTestsLazily = this._renderTriggerableTestsLazily.evaluate();
135         updateSelectedTestsLazily.evaluate(...this._selectedTests);
136         const updateSelectedPlatformsLazily = this._renderTriggerablePlatformsLazily.evaluate(this._selectedTests, this._triggerablePlatforms);
137         if (updateSelectedPlatformsLazily)
138             updateSelectedPlatformsLazily.evaluate(this._selectedPlatform);
139
140         const [triggerable, error] = this._updateTriggerableLazily.evaluate(this._selectedTests, this._selectedPlatform);
141
142         this._renderRepositoryPanesLazily.evaluate(triggerable, error, this._selectedPlatform, this._repositoryGroupByConfiguration, this._showComparison);
143     }
144
145     _renderTriggerableTests()
146     {
147         const enabledTriggerables = Triggerable.all().filter((triggerable) => !triggerable.isDisabled());
148
149         let tests = Test.topLevelTests().filter((test) => test.metrics().length && enabledTriggerables.some((triggerable) => triggerable.acceptsTest(test)));
150         return this._renderRadioButtonList(this.content('test-list'), 'test', tests, this.selectTests.bind(this));
151     }
152
153     _renderTriggerablePlatforms(selectedTests, triggerablePlatforms)
154     {
155         if (!selectedTests.length) {
156             this.content('platform-pane').style.display = 'none';
157             return null;
158         }
159         this.content('platform-pane').style.display = null;
160
161         return this._renderRadioButtonList(this.content('platform-list'), 'platform', triggerablePlatforms, (selectedPlatforms) => {
162             this.selectPlatform(selectedPlatforms.length ? selectedPlatforms[0] : null);
163         });
164     }
165
166     _renderRadioButtonList(listContainer, name, objects, callback)
167     {
168         const listItems = [];
169         let selectedListItems = [];
170         const checkSelectedRadioButtons = (newSelectedListItems) => {
171             selectedListItems.forEach((item) => {
172                 item.label.classList.remove('selected');
173                 item.radioButton.checked = false;
174             });
175             selectedListItems = newSelectedListItems;
176             selectedListItems.forEach((item) => {
177                 item.label.classList.add('selected');
178                 item.radioButton.checked = true;
179             });
180         }
181
182         const element = ComponentBase.createElement;
183         this.renderReplace(listContainer, objects.map((object) => {
184             const radioButton = element('input', {type: 'radio', name: name, onchange: () => {
185                 checkSelectedRadioButtons(listItems.filter((item) => item.radioButton.checked));
186                 callback(selectedListItems.map((item) => item.object));
187                 this.enqueueToRender();
188             }});
189             const label = element('label', [radioButton, object.label()]);
190             listItems.push({radioButton, label, object});
191             return element('li', label);
192         }));
193
194         return new LazilyEvaluatedFunction((...selectedObjects) => {
195             const objects = new Set(selectedObjects);
196             checkSelectedRadioButtons(listItems.filter((item) => objects.has(item.object)));
197         });
198     }
199
200     _updateTriggerable(tests, platform)
201     {
202         let triggerable = null;
203         let error = null;
204         if (tests.length && platform) {
205             triggerable = Triggerable.findByTestConfiguration(tests[0], platform);
206             let matchingTests = new Set;
207             let mismatchingTests = new Set;
208             for (let test of tests) {
209                 if (Triggerable.findByTestConfiguration(test, platform) == triggerable)
210                     matchingTests.add(test);
211                 else
212                     mismatchingTests.add(test);
213             }
214             if (matchingTests.size < tests.length) {
215                 const matchingTestNames = [...matchingTests].map((test) => test.fullName()).sort().join('", "');
216                 const mismathingTestNames = [...mismatchingTests].map((test) => test.fullName()).sort().join('", "');
217                 error = `Tests "${matchingTestNames}" and "${mismathingTestNames}" cannot be scheduled
218                     simultenosuly on "${platform.name()}". Please select one of them at a time.`;
219             }
220         }
221
222         return [triggerable, error];
223     }
224
225     _updateRepositoryGroups(triggerable)
226     {
227         const repositoryGroups = triggerable ? TriggerableRepositoryGroup.sortByNamePreferringSmallerRepositories(triggerable.repositoryGroups()) : [];
228         for (let name in this._repositoryGroupByConfiguration) {
229             const currentGroup = this._repositoryGroupByConfiguration[name];
230             let matchingGroup = null;
231             if (currentGroup) {
232                 if (repositoryGroups.includes(currentGroup))
233                     matchingGroup = currentGroup;
234                 else
235                     matchingGroup = repositoryGroups.find((group) => group.name() == currentGroup.name());
236             }
237             if (!matchingGroup && repositoryGroups.length)
238                 matchingGroup = repositoryGroups[0];
239             this._repositoryGroupByConfiguration[name] = matchingGroup;
240         }
241     }
242
243     _updateCommitSetMap()
244     {
245         const newBaseline = this._computeCommitSet('Baseline');
246         let newComparison = this._computeCommitSet('Comparison');
247         if (newBaseline && newComparison && newBaseline.equals(newComparison))
248             newComparison = null;
249
250         const currentBaseline = this._commitSetMap['Baseline'];
251         const currentComparison = this._commitSetMap['Baseline'];
252         if (newBaseline == currentBaseline && newComparison == currentComparison)
253             return; // Both of them are null.
254
255         if (newBaseline && currentBaseline && newBaseline.equals(currentBaseline)
256             && newComparison && currentComparison && newComparison.equals(currentComparison))
257             return;
258
259         this._commitSetMap = {'Baseline': newBaseline, 'Comparison': newComparison};
260
261         this.dispatchAction('commitSetChange');
262         this.enqueueToRender();
263     }
264
265     _computeCommitSet(configurationName)
266     {
267         const repositoryGroup = this._repositoryGroupByConfiguration[configurationName];
268         if (!repositoryGroup)
269             return null;
270
271         const fileUploader = this._fileUploaders[configurationName];
272         if (!fileUploader || fileUploader.hasFileToUpload())
273             return null;
274
275         const commitSet = new CustomCommitSet;
276         for (let repository of repositoryGroup.repositories()) {
277             let revision = this._specifiedRevisions[configurationName].get(repository);
278             if (!revision)
279                 revision = this._fetchedRevisions[configurationName].get(repository);
280             if (!revision)
281                 return null;
282             commitSet.setRevisionForRepository(repository, revision);
283         }
284
285         for (let uploadedFile of fileUploader.uploadedFiles())
286             commitSet.addCustomRoot(uploadedFile);
287
288         return commitSet;
289     }
290
291     _renderRepositoryPanes(triggerable, error, platform, repositoryGroupByConfiguration, showComparison)
292     {
293         this.content('repository-configuration-error-pane').style.display = error ? null : 'none';
294         this.content('error').textContent = error;
295
296         this.content('baseline-configuration-pane').style.display = triggerable ? null : 'none';
297         this.content('specify-comparison-pane').style.display = triggerable && !showComparison ? null : 'none';
298         this.content('comparison-configuration-pane').style.display = triggerable && showComparison ? null : 'none';
299
300         if (!triggerable)
301             return;
302
303         const repositoryGroups = TriggerableRepositoryGroup.sortByNamePreferringSmallerRepositories(triggerable.repositoryGroups());
304
305         const repositorySet = new Set;
306         for (let group of repositoryGroups) {
307             for (let repository of group.repositories())
308                 repositorySet.add(repository);
309         }
310
311         const repositories = Repository.sortByNamePreferringOnesWithURL([...repositorySet]);
312         const requiredRepositories = repositories.filter((repository) => {
313             return repositoryGroups.every((group) => group.repositories().includes(repository));
314         });
315         const alwaysAcceptsCustomRoots = repositoryGroups.every((group) => group.acceptsCustomRoots());
316
317         this._renderBaselineRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots);
318
319         if (showComparison)
320             this._renderComparisonRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots);
321     }
322
323     _renderBaselineRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots)
324     {
325         let currentGroup = repositoryGroupByConfiguration['Baseline'];
326         const optionalRepositoryList = this._optionalRepositoryList(currentGroup, requiredRepositories);
327         this.renderReplace(this.content('baseline-revision-table'),
328             this._buildRevisionTable('Baseline', repositoryGroups, currentGroup, platform, requiredRepositories, optionalRepositoryList, alwaysAcceptsCustomRoots));
329     }
330
331     _renderComparisonRevisionTable(platform, repositoryGroups, requiredRepositories, repositoryGroupByConfiguration, alwaysAcceptsCustomRoots)
332     {
333         let currentGroup = repositoryGroupByConfiguration['Comparison'];
334         const optionalRepositoryList = this._optionalRepositoryList(currentGroup, requiredRepositories);
335         this.renderReplace(this.content('comparison-revision-table'),
336             this._buildRevisionTable('Comparison', repositoryGroups, currentGroup, platform, requiredRepositories, optionalRepositoryList, alwaysAcceptsCustomRoots));
337     }
338
339     _optionalRepositoryList(currentGroup, requiredRepositories)
340     {
341         if (!currentGroup)
342             return [];
343         return Repository.sortByNamePreferringOnesWithURL(currentGroup.repositories().filter((repository) => !requiredRepositories.includes(repository)));
344     }
345
346     _buildRevisionTable(configurationName, repositoryGroups, currentGroup, platform, requiredRepositories, optionalRepositoryList, alwaysAcceptsCustomRoots)
347     {
348         const element = ComponentBase.createElement;
349         const link = ComponentBase.createLink;
350
351         const customRootsTBody = element('tbody', [
352             element('tr', [
353                 element('th', 'Roots'),
354                 element('td', this._fileUploaders[configurationName]),
355             ]),
356         ]);
357
358         return [
359             element('tbody',
360                 requiredRepositories.map((repository) => {
361                     return element('tr', [
362                         element('th', repository.name()),
363                         element('td', this._buildRevisionInput(configurationName, repository, platform))
364                     ]);
365                 })),
366             alwaysAcceptsCustomRoots ? customRootsTBody : [],
367             element('tbody', [
368                 element('tr', {'class': 'group-row'},
369                     element('td', {colspan: 2}, this._buildRepositoryGroupList(repositoryGroups, currentGroup, configurationName))),
370             ]),
371             !alwaysAcceptsCustomRoots && currentGroup && currentGroup.acceptsCustomRoots() ? customRootsTBody : [],
372             element('tbody',
373                 optionalRepositoryList.map((repository) => {
374                     return element('tr',[
375                         element('th', repository.name()),
376                         element('td', this._buildRevisionInput(configurationName, repository, platform))
377                     ]);
378                 })
379             )];
380     }
381
382     _buildRepositoryGroupList(repositoryGroups, currentGroup, configurationName)
383     {
384         const element = ComponentBase.createElement;
385         return repositoryGroups.map((group) => {
386             const input = element('input', {
387                 type: 'radio',
388                 name: 'repositoryGroup-for-' + configurationName.toLowerCase(),
389                 checked: currentGroup == group,
390                 onchange: () => this._selectRepositoryGroup(configurationName, group)
391             });
392             return [element('label', [input, group.description()])];
393         });
394     }
395
396     _selectRepositoryGroup(configurationName, group)
397     {
398         const source = this._repositoryGroupByConfiguration;
399         const clone = {};
400         for (let key in source)
401             clone[key] = source[key];
402         clone[configurationName] = group;
403         this._repositoryGroupByConfiguration = clone;
404         this._updateCommitSetMap();
405         this.enqueueToRender();
406     }
407
408     _buildRevisionInput(configurationName, repository, platform)
409     {
410         const revision = this._specifiedRevisions[configurationName].get(repository) || '';
411         const element = ComponentBase.createElement;
412         const input = element('input', {value: revision, oninput: () => {
413             unmodifiedInput = null;
414             this._specifiedRevisions[configurationName].set(repository, input.value);
415             this._updateCommitSetMap();
416         }});
417         let unmodifiedInput = input;
418
419         if (!revision) {
420             CommitLog.fetchLatestCommitForPlatform(repository, platform).then((commit) => {
421                 if (commit && unmodifiedInput) {
422                     unmodifiedInput.value = commit.revision();
423                     this._fetchedRevisions[configurationName].set(repository, commit.revision());
424                     this._updateCommitSetMap();
425                 }
426             });
427         }
428
429         return input;
430     }
431
432     static htmlTemplate()
433     {
434         return `
435             <section id="test-pane" class="pane">
436                 <h2>1. Select a Test</h2>
437                 <ul id="test-list" class="config-list"></ul>
438             </section>
439             <section id="platform-pane" class="pane">
440                 <h2>2. Select a Platform</h2>
441                 <ul id="platform-list" class="config-list"></ul>
442             </section>
443             <section id="repository-configuration-error-pane" class="pane">
444                 <h2>Incompatible tests</h2>
445                 <p id="error"></p>
446             </section>
447             <section id="baseline-configuration-pane" class="pane">
448                 <h2>3. Configure Baseline</h2>
449                 <table id="baseline-revision-table" class="revision-table"></table>
450             </section>
451             <section id="specify-comparison-pane" class="pane">
452                 <button id="specify-comparison-button">Configure to Compare</button>
453             </section>
454             <section id="comparison-configuration-pane" class="pane">
455                 <h2>4. Configure Comparison</h2>
456                 <table id="comparison-revision-table" class="revision-table"></table>
457             </section>`;
458     }
459
460     static cssTemplate()
461     {
462         return `
463             :host {
464                 display: flex !important;
465                 flex-direction: row !important;
466             }
467             .pane {
468                 margin-right: 1rem;
469                 padding: 0;
470             }
471             .pane h2 {
472                 padding: 0;
473                 margin: 0;
474                 margin-bottom: 0.5rem;
475                 font-size: 1.2rem;
476                 font-weight: inherit;
477                 text-align: center;
478                 white-space: nowrap;
479             }
480
481             .config-list {
482                 height: 20rem;
483                 overflow: scroll;
484                 display: block;
485                 margin: 0;
486                 padding: 0;
487                 list-style: none;
488                 font-size: inherit;
489                 font-weight: inherit;
490                 border: none;
491                 border-top: solid 1px #ddd;
492                 border-bottom: solid 1px #ddd;
493                 white-space: nowrap;
494             }
495
496             #platform-list:empty:before {
497                 content: "No matching platform";
498                 display: block;
499                 margin: 1rem 0.5rem;
500                 text-align: center;
501             }
502
503             .config-list label {
504                 display: block;
505                 padding: 0.1rem 0.2rem;
506             }
507
508             .config-list label:hover,
509             .config-list a:hover {
510                 background: rgba(204, 153, 51, 0.1);
511             }
512
513             .config-list label.selected,
514             .config-list a.selected {
515                 background: #eee;
516             }
517
518             .config-list a {
519                 display: block;
520                 padding: 0.1rem 0.2rem;
521                 text-decoration: none;
522                 color: inherit;
523             }
524
525             #repository-configuration-pane {
526                 position: relative;
527             }
528
529             #repository-configuration-pane > button  {
530                 margin-left: 19.5rem;
531             }
532
533             .revision-table {
534                 border: none;
535                 border-collapse: collapse;
536                 font-size: 1rem;
537             }
538
539             .revision-table thead {
540                 font-size: 1.2rem;
541             }
542
543             .revision-table tbody:empty {
544                 display: none;
545             }
546
547             .revision-table tbody tr:first-child td,
548             .revision-table tbody tr:first-child th {
549                 border-top: solid 1px #ddd;
550                 padding-top: 0.5rem;
551             }
552
553             .revision-table tbody tr:last-child td,
554             .revision-table tbody tr:last-child th {
555                 padding-bottom: 0.5rem;
556             }
557
558             .revision-table td,
559             .revision-table th {
560                 width: 15rem;
561                 height: 1.8rem;
562                 padding: 0 0.2rem;
563                 border: none;
564                 font-weight: inherit;
565             }
566
567             .revision-table thead th {
568                 text-align: center;
569             }
570
571             .revision-table th close-button {
572                 vertical-align: bottom;
573             }
574
575             .revision-table td:first-child,
576             .revision-table th:first-child {
577                 width: 6rem;
578             }
579
580             .revision-table tr.group-row td {
581                 padding-left: 5rem;
582             }
583
584             label {
585                 white-space: nowrap;
586                 display: block;
587             }
588
589             input:not([type=radio]) {
590                 width: calc(100% - 0.6rem);
591                 padding: 0.1rem 0.2rem;
592                 font-size: 0.9rem;
593                 font-weight: inherit;
594             }
595
596             #specify-comparison-pane button {
597                 margin-top: 1.5rem;
598                 font-size: 1.1rem;
599                 font-weight: inherit;
600             }
601
602             #start-pane button {
603                 margin-top: 1.5rem;
604                 font-size: 1.2rem;
605                 font-weight: inherit;
606             }
607 `;
608     }
609 }
610
611 ComponentBase.defineElement('custom-analysis-task-configurator', CustomAnalysisTaskConfigurator);