Add the support for scheduling a A/B testing with a patch.
[WebKit-https.git] / Websites / perf.webkit.org / public / v3 / components / instant-file-uploader.js
1 class InstantFileUploader extends ComponentBase {
2     constructor()
3     {
4         super('instant-file-uploader');
5         this._fileInput = null;
6         this._allowMultipleFiles = false;
7         this._uploadedFiles = [];
8         this._preuploadFiles = [];
9         this._uploadProgress = new WeakMap;
10         this._fileSizeFormatter = Metric.makeFormatter('B', 3);
11
12         this._renderUploadedFilesLazily = new LazilyEvaluatedFunction(this._renderUploadedFiles.bind(this));
13         this._renderPreuploadFilesLazily = new LazilyEvaluatedFunction(this._renderPreuploadFiles.bind(this));
14     }
15
16     hasFileToUpload() { return !!this._preuploadFiles.length; }
17     uploadedFiles() { return this._uploadedFiles; }
18
19     allowMultipleFiles()
20     {
21         this._allowMultipleFiles = true;
22         this.enqueueToRender();
23     }
24
25     addUploadedFile(uploadedFile)
26     {
27         console.assert(uploadedFile instanceof UploadedFile);
28         if (this._uploadedFiles.includes(uploadedFile))
29             return;
30         this._uploadedFiles.push(uploadedFile);
31         this.enqueueToRender();
32     }
33
34     didConstructShadowTree()
35     {
36         this.content('file-adder').onclick = () => {
37             inputElement.click();
38         }
39         const inputElement = document.createElement('input');
40         inputElement.type = 'file';
41         inputElement.onchange = () => this._didFileInputChange(inputElement);
42         this._fileInput = inputElement;
43     }
44
45     render()
46     {
47         this._renderUploadedFilesLazily.evaluate(...this._uploadedFiles);
48         const uploadStatusElements = this._renderPreuploadFilesLazily.evaluate(...this._preuploadFiles);
49         this._updateUploadStatus(uploadStatusElements);
50         const fileCount = this._uploadedFiles.length + this._preuploadFiles.length;
51         this.content('file-adder').style.display = this._allowMultipleFiles || !fileCount ? null : 'none';
52     }
53
54     _renderUploadedFiles(...uploadedFiles)
55     {
56         const element = ComponentBase.createElement;
57         this.renderReplace(this.content('uploaded-files'), uploadedFiles.map((uploadedFile) => {
58             const authorInfo = uploadedFile.author() ? ' by ' + uploadedFile.author() : '';
59             const createdAt = Metric.formatTime(uploadedFile.createdAt());
60             const deleteButton = new CloseButton;
61             deleteButton.listenToAction('activate', () => this._removeUploadedFile(uploadedFile));
62             return element('li', [
63                 deleteButton,
64                 element('code', {class: 'filename'}, uploadedFile.filename()),
65                 ' ',
66                 element('small', {class: 'filesize'}, '(' + this._fileSizeFormatter(uploadedFile.size()) + ')'),
67                 element('small', {class: 'meta'}, `Uploaded${authorInfo} on ${createdAt}`),
68             ]);
69         }));
70     }
71
72     _renderPreuploadFiles(...preuploadFiles)
73     {
74         const element = ComponentBase.createElement;
75         const uploadStatusElements = [];
76         this.renderReplace(this.content('preupload-files'), preuploadFiles.map((file) => {
77             const progressBar = element('progress');
78             const meta = element('small', {class: 'meta'}, progressBar);
79             uploadStatusElements.push({file, meta, progressBar});
80
81             return element('li', [
82                 element('code', file.name),
83                 ' ',
84                 element('small', {class: 'filesize'}, '(' + this._fileSizeFormatter(file.size) + ')'),
85                 meta,
86             ]);
87         }));
88         return uploadStatusElements;
89     }
90
91     _updateUploadStatus(uploadStatusElements)
92     {
93         for (let entry of uploadStatusElements) {
94             const progress = this._uploadProgress.get(entry.file);
95             const progressBar = entry.progressBar;
96             if (!progress) {
97                 progressBar.removeAttribute('max');
98                 progressBar.removeAttribute('value');
99                 return;
100             }
101             if (progress.error) {
102                 entry.meta.classList.add('hasError');
103                 entry.meta.textContent = this._formatUploadError(progress.error);
104             } else {
105                 progressBar.max = progress.total;
106                 progressBar.value = progress.loaded;
107             }
108         }
109     }
110
111     _formatUploadError(error)
112     {
113         switch (error) {
114         case 'NotSupported':
115             return 'Failed: File uploading is disabled';
116         case 'FileSizeLimitExceeded':
117             return 'Failed: The uploaded file was too big';
118         case 'FileSizeQuotaExceeded':
119             return 'Failed: Exceeded file upload quota';
120         }
121         return 'Failed to upload the file';
122     }
123
124     _didFileInputChange(input)
125     {
126         if (!input.files.length)
127             return;
128         const limit = UploadedFile.fileUploadSizeLimit;
129         for (let file of input.files) {
130             if (file.size > limit) {
131                 alert(`The specified file "${file.name}" is too big (${this._fileSizeFormatter(file.size)}). It must be smaller than ${this._fileSizeFormatter(limit)}`);
132                 input.value = null;
133                 return;
134             }
135             UploadedFile.fetchUnloadedFileWithIdenticalHash(file).then((uploadedFile) => {
136                 if (uploadedFile) {
137                     this._didUploadFile(file, uploadedFile);
138                     return;
139                 }
140
141                 UploadedFile.uploadFile(file, (progress) => {
142                     this._uploadProgress.set(file, progress);
143                     this.enqueueToRender();
144                 }).then((uploadedFile) => {
145                     this._didUploadFile(file, uploadedFile);
146                 }, (error) => {
147                     this._uploadProgress.set(file, {error: error === 0 ? 'UnknownError' : error});
148                     this.enqueueToRender();
149                 });
150             });
151         }
152         this._preuploadFiles = Array.from(input.files);
153         input.value = null;
154
155         this.enqueueToRender();
156     }
157
158     _removeUploadedFile(uploadedFile)
159     {
160         // FIXME: Send a request to delete the file.
161         console.assert(uploadedFile instanceof UploadedFile);
162         const index = this._uploadedFiles.indexOf(uploadedFile);
163         if (index < 0)
164             return;
165         this._uploadedFiles.splice(index, 1);
166         this.dispatchAction('removedFile', uploadedFile);
167         this.enqueueToRender();
168     }
169
170     _didUploadFile(file, uploadedFile)
171     {
172         console.assert(file instanceof File);
173         const index = this._preuploadFiles.indexOf(file);
174         if (index >= 0)
175             this._preuploadFiles.splice(index, 1);
176         this._uploadedFiles.push(uploadedFile);
177         this.dispatchAction('uploadedFile', uploadedFile);
178         this.enqueueToRender();
179     }
180
181     static htmlTemplate()
182     {
183         return `<ul id="uploaded-files"></ul>
184             <ul id="preupload-files"></ul>
185             <button id="file-adder"><slot>Add a new file</slot></button>`;
186     }
187
188     static cssTemplate()
189     {
190         return `
191             ul:empty {
192                 display: none;
193             }
194
195             ul, li {
196                 margin: 0;
197                 padding: 0;
198                 list-style: none;
199             }
200
201             li {
202                 position: relative;
203                 margin-bottom: 0.25rem;
204                 padding-left: 1.5rem;
205                 padding-bottom: 0.25rem;
206                 border-bottom: solid 1px #eee;
207             }
208
209             li:last-child {
210                 border-bottom: none;
211             }
212
213             li > close-button {
214                 position: absolute;
215                 left: 0;
216                 top: 50%;
217                 margin-top: -0.5rem;
218             }
219
220             li > progress {
221                 display: block;
222             }
223
224             code {
225                 font-size: 1.1rem;
226                 font-weight: inherit;
227             }
228
229             small {
230                 font-size: 0.8rem;
231                 font-weight: inherit;
232                 color: #666;
233             }
234
235             small.meta {
236                 display: block;
237             }
238
239             .hasError {
240                 color: #c60;
241                 font-weight: normal;
242             }
243         `;
244     }
245 }
246
247 ComponentBase.defineElement('instant-file-uploader', InstantFileUploader);