Web Inspector: fix incorrect curly quote orientation in a UIString
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ResourceTimelineDataGridNode.js
1 /*
2  * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.ResourceTimelineDataGridNode = class ResourceTimelineDataGridNode extends WI.TimelineDataGridNode
27 {
28     constructor(resourceTimelineRecord, includesGraph, graphDataSource, shouldShowPopover)
29     {
30         super(includesGraph, graphDataSource);
31
32         this._resource = resourceTimelineRecord.resource;
33         this._record = resourceTimelineRecord;
34         this._shouldShowPopover = shouldShowPopover;
35
36         this._resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._needsRefresh, this);
37         this._resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._needsRefresh, this);
38         this._resource.addEventListener(WI.Resource.Event.URLDidChange, this._needsRefresh, this);
39
40         if (includesGraph)
41             this._record.addEventListener(WI.TimelineRecord.Event.Updated, this._timelineRecordUpdated, this);
42         else {
43             this._resource.addEventListener(WI.Resource.Event.TypeDidChange, this._needsRefresh, this);
44             this._resource.addEventListener(WI.Resource.Event.SizeDidChange, this._needsRefresh, this);
45             this._resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._needsRefresh, this);
46         }
47     }
48
49     // Public
50
51     get records()
52     {
53         return [this._record];
54     }
55
56     get resource()
57     {
58         return this._resource;
59     }
60
61     get data()
62     {
63         if (this._cachedData)
64             return this._cachedData;
65
66         var resource = this._resource;
67         var data = {};
68
69         if (!this._includesGraph) {
70             var zeroTime = this.graphDataSource ? this.graphDataSource.zeroTime : 0;
71
72             data.domain = WI.displayNameForHost(resource.urlComponents.host);
73             data.scheme = resource.urlComponents.scheme ? resource.urlComponents.scheme.toUpperCase() : "";
74             data.method = resource.requestMethod;
75             data.type = resource.type;
76             data.statusCode = resource.statusCode;
77             data.cached = resource.cached;
78             data.size = resource.size;
79             data.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
80             data.requestSent = resource.requestSentTimestamp - zeroTime;
81             data.duration = resource.receiveDuration;
82             data.latency = resource.latency;
83             data.protocol = resource.protocol;
84             data.priority = resource.priority;
85             data.remoteAddress = resource.remoteAddress;
86             data.connectionIdentifier = resource.connectionIdentifier;
87         }
88
89         data.graph = this._record.startTime;
90
91         this._cachedData = data;
92         return data;
93     }
94
95     createCellContent(columnIdentifier, cell)
96     {
97         let resource = this._resource;
98
99         if (resource.hadLoadingError())
100             cell.classList.add("error");
101
102         let value = this.data[columnIdentifier];
103
104         switch (columnIdentifier) {
105         case "name":
106             cell.classList.add(...this.iconClassNames());
107             cell.title = resource.displayURL;
108             this._updateStatus(cell);
109             return this._createNameCellDocumentFragment();
110
111         case "type":
112             var text = WI.Resource.displayNameForType(value);
113             cell.title = text;
114             return text;
115
116         case "statusCode":
117             cell.title = resource.statusText || "";
118             return value || emDash;
119
120         case "cached":
121             var fragment = this._cachedCellContent();
122             cell.title = fragment.textContent;
123             return fragment;
124
125         case "size":
126         case "transferSize":
127             var text = emDash;
128             if (!isNaN(value)) {
129                 text = Number.bytesToString(value, true);
130                 cell.title = text;
131             }
132             return text;
133
134         case "requestSent":
135         case "latency":
136         case "duration":
137             var text = emDash;
138             if (!isNaN(value)) {
139                 text = Number.secondsToString(value, true);
140                 cell.title = text;
141             }
142             return text;
143
144         case "domain":
145         case "method":
146         case "scheme":
147         case "protocol":
148         case "remoteAddress":
149         case "connectionIdentifier":
150             if (value)
151                 cell.title = value;
152             return value || emDash;
153
154         case "priority":
155             var title = WI.Resource.displayNameForPriority(value);
156             if (title)
157                 cell.title = title;
158             return title || emDash;
159         }
160
161         return super.createCellContent(columnIdentifier, cell);
162     }
163
164     refresh()
165     {
166         if (this._scheduledRefreshIdentifier) {
167             cancelAnimationFrame(this._scheduledRefreshIdentifier);
168             this._scheduledRefreshIdentifier = undefined;
169         }
170
171         this._cachedData = null;
172
173         super.refresh();
174     }
175
176     iconClassNames()
177     {
178         return [WI.ResourceTreeElement.ResourceIconStyleClassName, this.resource.type];
179     }
180
181     appendContextMenuItems(contextMenu)
182     {
183         WI.appendContextMenuItemsForSourceCode(contextMenu, this._resource);
184     }
185
186     // Protected
187
188     didAddRecordBar(recordBar)
189     {
190         if (!this._shouldShowPopover)
191             return;
192
193         if (!recordBar.records.length || recordBar.records[0].type !== WI.TimelineRecord.Type.Network)
194             return;
195
196         console.assert(!this._mouseEnterRecordBarListener);
197         this._mouseEnterRecordBarListener = this._mouseoverRecordBar.bind(this);
198         recordBar.element.addEventListener("mouseenter", this._mouseEnterRecordBarListener);
199     }
200
201     didRemoveRecordBar(recordBar)
202     {
203         if (!this._shouldShowPopover)
204             return;
205
206         if (!recordBar.records.length || recordBar.records[0].type !== WI.TimelineRecord.Type.Network)
207             return;
208
209         recordBar.element.removeEventListener("mouseenter", this._mouseEnterRecordBarListener);
210         this._mouseEnterRecordBarListener = null;
211     }
212
213     filterableDataForColumn(columnIdentifier)
214     {
215         if (columnIdentifier === "name")
216             return this._resource.url;
217         return super.filterableDataForColumn(columnIdentifier);
218     }
219
220     // Private
221
222     _createNameCellDocumentFragment()
223     {
224         let fragment = document.createDocumentFragment();
225         let mainTitle = this.displayName();
226         fragment.append(mainTitle);
227
228         // Show the host as the subtitle if it is different from the main resource or if this is the main frame's main resource.
229         let frame = this._resource.parentFrame;
230         let isMainResource = this._resource.isMainResource();
231         let parentResourceHost;
232         if (frame && isMainResource) {
233             // When the resource is a main resource, get the host from the current frame's parent frame instead of the current frame.
234             parentResourceHost = frame.parentFrame ? frame.parentFrame.mainResource.urlComponents.host : null;
235         } else if (frame) {
236             // When the resource is a normal sub-resource, get the host from the current frame's main resource.
237             parentResourceHost = frame.mainResource.urlComponents.host;
238         }
239
240         if (parentResourceHost !== this._resource.urlComponents.host || frame.isMainFrame() && isMainResource) {
241             let subtitle = WI.displayNameForHost(this._resource.urlComponents.host);
242             if (mainTitle !== subtitle) {
243                 let subtitleElement = document.createElement("span");
244                 subtitleElement.classList.add("subtitle");
245                 subtitleElement.textContent = subtitle;
246                 fragment.append(subtitleElement);
247             }
248         }
249
250         return fragment;
251     }
252
253     _cachedCellContent()
254     {
255         if (!this._resource.hasResponse())
256             return emDash;
257
258         let responseSource = this._resource.responseSource;
259         if (responseSource === WI.Resource.ResponseSource.MemoryCache || responseSource === WI.Resource.ResponseSource.DiskCache) {
260             console.assert(this._resource.cached, "This resource has a cache responseSource it should also be marked as cached", this._resource);
261             let span = document.createElement("span");
262             let cacheType = document.createElement("span");
263             cacheType.classList = "cache-type";
264             cacheType.textContent = responseSource === WI.Resource.ResponseSource.MemoryCache ? WI.UIString("(Memory)") : WI.UIString("(Disk)");
265             span.append(WI.UIString("Yes"), " ", cacheType);
266             return span;
267         }
268
269         let fragment = document.createDocumentFragment();
270         fragment.append(this._resource.cached ? WI.UIString("Yes") : WI.UIString("No"));
271         return fragment;
272     }
273
274     _needsRefresh()
275     {
276         if (this.dataGrid instanceof WI.TimelineDataGrid) {
277             this.dataGrid.dataGridNodeNeedsRefresh(this);
278             return;
279         }
280
281         if (this._scheduledRefreshIdentifier)
282             return;
283
284         this._scheduledRefreshIdentifier = requestAnimationFrame(this.refresh.bind(this));
285     }
286
287     _timelineRecordUpdated(event)
288     {
289         if (this.isRecordVisible(this._record))
290             this.needsGraphRefresh();
291     }
292
293     _dataGridNodeGoToArrowClicked()
294     {
295         const options = {
296             ignoreNetworkTab: true,
297             ignoreSearchTab: true,
298         };
299         WI.showSourceCode(this._resource, options);
300     }
301
302     _updateStatus(cell)
303     {
304         if (this._resource.failed)
305             cell.classList.add("error");
306         else {
307             cell.classList.remove("error");
308
309             if (this._resource.finished)
310                 this.createGoToArrowButton(cell, this._dataGridNodeGoToArrowClicked.bind(this));
311         }
312
313         if (this._resource.isLoading()) {
314             if (!this._spinner)
315                 this._spinner = new WI.IndeterminateProgressSpinner;
316             let contentElement = cell.firstChild;
317             contentElement.appendChild(this._spinner.element);
318         } else {
319             if (this._spinner)
320                 this._spinner.element.remove();
321         }
322     }
323
324     _mouseoverRecordBar(event)
325     {
326         let recordBar = WI.TimelineRecordBar.fromElement(event.target);
327         console.assert(recordBar);
328         if (!recordBar)
329             return;
330
331         let calculateTargetFrame = () => {
332             let columnRect = WI.Rect.rectFromClientRect(this.elementWithColumnIdentifier("graph").getBoundingClientRect());
333             let barRect = WI.Rect.rectFromClientRect(event.target.getBoundingClientRect());
334             return columnRect.intersectionWithRect(barRect);
335         };
336
337         let targetFrame = calculateTargetFrame();
338         if (!targetFrame.size.width && !targetFrame.size.height)
339             return;
340
341         console.assert(recordBar.records.length);
342         let resource = recordBar.records[0].resource;
343         if (!resource.timingData)
344             return;
345
346         if (!resource.timingData.responseEnd)
347             return;
348
349         if (this.dataGrid._dismissPopoverTimeout) {
350             clearTimeout(this.dataGrid._dismissPopoverTimeout);
351             this.dataGrid._dismissPopoverTimeout = undefined;
352         }
353
354         let popoverContentElement = document.createElement("div");
355         popoverContentElement.classList.add("resource-timing-popover-content");
356
357         if (resource.failed || resource.urlComponents.scheme === "data" || (resource.cached && resource.statusCode !== 304)) {
358             let descriptionElement = document.createElement("span");
359             descriptionElement.classList.add("description");
360             if (resource.failed)
361                 descriptionElement.textContent = WI.UIString("Resource failed to load.");
362             else if (resource.urlComponents.scheme === "data")
363                 descriptionElement.textContent = WI.UIString("Resource was loaded with the “data” scheme.");
364             else
365                 descriptionElement.textContent = WI.UIString("Resource was served from the cache.");
366             popoverContentElement.appendChild(descriptionElement);
367         } else {
368             let columns = {
369                 description: {
370                     width: "80px"
371                 },
372                 graph: {
373                     width: `${WI.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels}px`
374                 },
375                 duration: {
376                     width: "70px",
377                     aligned: "right"
378                 }
379             };
380
381             let popoverDataGrid = new WI.DataGrid(columns);
382             popoverDataGrid.inline = true;
383             popoverDataGrid.headerVisible = false;
384             popoverContentElement.appendChild(popoverDataGrid.element);
385
386             let graphDataSource = {
387                 get secondsPerPixel() { return resource.duration / WI.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels; },
388                 get zeroTime() { return resource.firstTimestamp; },
389                 get startTime() { return resource.firstTimestamp; },
390                 get currentTime() { return this.endTime; },
391
392                 get endTime()
393                 {
394                     let endTimePadding = this.secondsPerPixel * WI.TimelineRecordBar.MinimumWidthPixels;
395                     return resource.lastTimestamp + endTimePadding;
396                 }
397             };
398
399             let secondTimestamp = resource.timingData.domainLookupStart || resource.timingData.connectStart || resource.timingData.requestStart;
400             if (secondTimestamp - resource.timingData.startTime)
401                 popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Stalled"), resource.timingData.startTime, secondTimestamp, graphDataSource));
402             if (resource.timingData.domainLookupStart)
403                 popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("DNS"), resource.timingData.domainLookupStart, resource.timingData.domainLookupEnd, graphDataSource));
404             if (resource.timingData.connectStart)
405                 popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Connection"), resource.timingData.connectStart, resource.timingData.connectEnd, graphDataSource));
406             if (resource.timingData.secureConnectionStart)
407                 popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Secure"), resource.timingData.secureConnectionStart, resource.timingData.connectEnd, graphDataSource));
408             popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Request"), resource.timingData.requestStart, resource.timingData.responseStart, graphDataSource));
409             popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Response"), resource.timingData.responseStart, resource.timingData.responseEnd, graphDataSource));
410
411             const higherResolution = true;
412             let totalData = {
413                 description: WI.UIString("Total time"),
414                 duration: Number.secondsToMillisecondsString(resource.timingData.responseEnd - resource.timingData.startTime, higherResolution)
415             };
416             popoverDataGrid.appendChild(new WI.DataGridNode(totalData));
417
418             popoverDataGrid.updateLayout();
419         }
420
421         if (!this.dataGrid._popover)
422             this.dataGrid._popover = new WI.Popover;
423
424         let preferredEdges = [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MIN_X];
425         this.dataGrid._popover.windowResizeHandler = () => {
426             let bounds = calculateTargetFrame();
427             this.dataGrid._popover.present(bounds.pad(2), preferredEdges);
428         };
429
430         recordBar.element.addEventListener("mouseleave", () => {
431             if (!this.dataGrid)
432                 return;
433
434             this.dataGrid._dismissPopoverTimeout = setTimeout(() => {
435                 if (this.dataGrid)
436                     this.dataGrid._popover.dismiss();
437             }, WI.ResourceTimelineDataGridNode.DelayedPopoverDismissalTimeout);
438         }, {once: true});
439
440         this.dataGrid._popover.presentNewContentWithFrame(popoverContentElement, targetFrame.pad(2), preferredEdges);
441     }
442 };
443
444 WI.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels = 110;
445 WI.ResourceTimelineDataGridNode.DelayedPopoverDismissalTimeout = 500;