Web Inspector: Include a full URL tooltip when hovering the name in the Network Tab
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / NetworkTableContentView.js
1 /*
2  * Copyright (C) 2017 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.NetworkTableContentView = class NetworkTableContentView extends WI.ContentView
27 {
28     constructor(representedObject, extraArguments)
29     {
30         super(representedObject);
31
32         this._entries = [];
33         this._entriesSortComparator = null;
34         this._filteredEntries = [];
35         this._pendingInsertions = [];
36         this._pendingUpdates = [];
37         this._pendingFilter = false;
38
39         this._table = null;
40         this._nameColumnWidthSetting = new WI.Setting("network-table-content-view-name-column-width", 250);
41
42         this._selectedResource = null;
43         this._resourceDetailView = null;
44         this._resourceDetailViewMap = new Map;
45
46         this._waterfallStartTime = NaN;
47         this._waterfallEndTime = NaN;
48         this._waterfallTimelineRuler = null;
49         this._waterfallPopover = null;
50
51         // FIXME: Network Timeline.
52         // FIXME: Throttling.
53
54         const exclusive = true;
55         this._typeFilterScopeBarItemAll = new WI.ScopeBarItem("network-type-filter-all", WI.UIString("All"), exclusive);
56         let typeFilterScopeBarItems = [this._typeFilterScopeBarItemAll];
57
58         let uniqueTypes = [
59             ["Document", (type) => type === WI.Resource.Type.Document],
60             ["Stylesheet", (type) => type === WI.Resource.Type.Stylesheet],
61             ["Image", (type) => type === WI.Resource.Type.Image],
62             ["Font", (type) => type === WI.Resource.Type.Font],
63             ["Script", (type) => type === WI.Resource.Type.Script],
64             ["XHR", (type) => type === WI.Resource.Type.XHR || type === WI.Resource.Type.Fetch],
65             ["Other", (type) => {
66                 return type !== WI.Resource.Type.Document
67                     && type !== WI.Resource.Type.Stylesheet
68                     && type !== WI.Resource.Type.Image
69                     && type !== WI.Resource.Type.Font
70                     && type !== WI.Resource.Type.Script
71                     && type !== WI.Resource.Type.XHR
72                     && type !== WI.Resource.Type.Fetch;
73             }],
74         ];
75         for (let [key, checker] of uniqueTypes) {
76             let type = WI.Resource.Type[key];
77             let scopeBarItem = new WI.ScopeBarItem("network-type-filter-" + key, WI.NetworkTableContentView.shortDisplayNameForResourceType(type));
78             scopeBarItem.__checker = checker;
79             typeFilterScopeBarItems.push(scopeBarItem);
80         }
81
82         this._typeFilterScopeBar = new WI.ScopeBar("network-type-filter-scope-bar", typeFilterScopeBarItems, typeFilterScopeBarItems[0]);
83         this._typeFilterScopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._typeFilterScopeBarSelectionChanged, this);
84
85         this._urlFilterSearchText = null;
86         this._urlFilterSearchRegex = null;
87         this._urlFilterIsActive = false;
88
89         this._urlFilterNavigationItem = new WI.FilterBarNavigationItem;
90         this._urlFilterNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._urlFilterDidChange, this);
91         this._urlFilterNavigationItem.filterBar.placeholder = WI.UIString("Filter Full URL");
92
93         this._activeTypeFilters = this._generateTypeFilter();
94         this._activeURLFilterResources = new Set;
95
96         this._emptyFilterResultsMessageElement = null;
97
98         this._clearOnLoadNavigationItem = new WI.CheckboxNavigationItem("perserve-log", WI.UIString("Preserve Log"), !WI.settings.clearNetworkOnNavigate.value);
99         this._clearOnLoadNavigationItem.tooltip = WI.UIString("Do not clear network items on new page loads");
100         this._clearOnLoadNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, () => { WI.settings.clearNetworkOnNavigate.value = !WI.settings.clearNetworkOnNavigate.value; });
101         WI.settings.clearNetworkOnNavigate.addEventListener(WI.Setting.Event.Changed, this._clearNetworkOnNavigateSettingChanged, this);
102
103         this._harExportNavigationItem = new WI.ButtonNavigationItem("har-export", WI.UIString("Export"), "Images/Export.svg", 15, 15);
104         this._harExportNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
105         this._harExportNavigationItem.tooltip = WI.UIString("HAR Export (%s)").format(WI.saveKeyboardShortcut.displayName);
106         this._harExportNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportHAR(); });
107
108         this._checkboxsNavigationItemGroup = new WI.GroupNavigationItem([this._clearOnLoadNavigationItem, new WI.DividerNavigationItem]);
109         this._checkboxsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
110
111         this._buttonsNavigationItemGroup = new WI.GroupNavigationItem([this._harExportNavigationItem, new WI.DividerNavigationItem]);
112         this._buttonsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
113
114         // COMPATIBILITY (iOS 10.3): Network.setDisableResourceCaching did not exist.
115         if (window.NetworkAgent && NetworkAgent.setResourceCachingDisabled) {
116             let toolTipForDisableResourceCache = WI.UIString("Ignore the resource cache when loading resources");
117             let activatedToolTipForDisableResourceCache = WI.UIString("Use the resource cache when loading resources");
118             this._disableResourceCacheNavigationItem = new WI.ActivateButtonNavigationItem("disable-resource-cache", toolTipForDisableResourceCache, activatedToolTipForDisableResourceCache, "Images/IgnoreCaches.svg", 16, 16);
119             this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
120
121             this._disableResourceCacheNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleDisableResourceCache, this);
122             WI.resourceCachingDisabledSetting.addEventListener(WI.Setting.Event.Changed, this._resourceCachingDisabledSettingChanged, this);
123         }
124
125         this._clearNetworkItemsNavigationItem = new WI.ButtonNavigationItem("clear-network-items", WI.UIString("Clear Network Items (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemTrash.svg", 15, 15);
126         this._clearNetworkItemsNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this.reset(); });
127
128         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
129         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this);
130         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._resourceLoadingDidFail, this);
131         WI.Resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._resourceTransferSizeDidChange, this);
132         WI.frameResourceManager.addEventListener(WI.FrameResourceManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
133         WI.timelineManager.persistentNetworkTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
134
135         this._needsInitialPopulate = true;
136     }
137
138     // Static
139
140     static displayNameForResource(resource)
141     {
142         if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font) {
143             let fileExtension;
144             if (resource.mimeType)
145                 fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
146             if (!fileExtension)
147                 fileExtension = WI.fileExtensionForURL(resource.url);
148             if (fileExtension)
149                 return fileExtension;
150         }
151
152         return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase();
153     }
154
155     static shortDisplayNameForResourceType(type)
156     {
157         switch (type) {
158         case WI.Resource.Type.Document:
159             return WI.UIString("Document");
160         case WI.Resource.Type.Stylesheet:
161             return "CSS";
162         case WI.Resource.Type.Image:
163             return WI.UIString("Image");
164         case WI.Resource.Type.Font:
165             return WI.UIString("Font");
166         case WI.Resource.Type.Script:
167             return "JS";
168         case WI.Resource.Type.XHR:
169             return "XHR";
170         case WI.Resource.Type.Fetch:
171             return WI.UIString("Fetch");
172         case WI.Resource.Type.Ping:
173             return WI.UIString("Ping");
174         case WI.Resource.Type.Beacon:
175             return WI.UIString("Beacon");
176         case WI.Resource.Type.WebSocket:
177         case WI.Resource.Type.Other:
178             return WI.UIString("Other");
179         default:
180             console.error("Unknown resource type", type);
181             return null;
182         }
183     }
184
185     // Public
186
187     get selectionPathComponents()
188     {
189         return null;
190     }
191
192     get navigationItems()
193     {
194         let items = [this._checkboxsNavigationItemGroup, this._buttonsNavigationItemGroup];
195         if (this._disableResourceCacheNavigationItem)
196             items.push(this._disableResourceCacheNavigationItem);
197         items.push(this._clearNetworkItemsNavigationItem);
198         return items;
199     }
200
201     get filterNavigationItems()
202     {
203         return [this._urlFilterNavigationItem, this._typeFilterScopeBar];
204     }
205
206     get supportsSave()
207     {
208         return this._filteredEntries.some((entry) => entry.resource.finished);
209     }
210
211     get saveData()
212     {
213         return {customSaveHandler: () => { this._exportHAR(); }};
214     }
215
216     shown()
217     {
218         super.shown();
219
220         if (this._resourceDetailView)
221             this._resourceDetailView.shown();
222
223         if (this._table)
224             this._table.restoreScrollPosition();
225     }
226
227     hidden()
228     {
229         this._hidePopover();
230
231         if (this._resourceDetailView)
232             this._resourceDetailView.hidden();
233
234         super.hidden();
235     }
236
237     closed()
238     {
239         for (let detailView of this._resourceDetailViewMap.values())
240             detailView.dispose();
241         this._resourceDetailViewMap.clear();
242
243         this._hidePopover();
244         this._hideResourceDetailView();
245
246         WI.Frame.removeEventListener(null, null, this);
247         WI.Resource.removeEventListener(null, null, this);
248         WI.resourceCachingDisabledSetting.removeEventListener(null, null, this);
249         WI.settings.clearNetworkOnNavigate.removeEventListener(null, null, this);
250         WI.frameResourceManager.removeEventListener(WI.FrameResourceManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
251         WI.timelineManager.persistentNetworkTimeline.removeEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
252
253         super.closed();
254     }
255
256     reset()
257     {
258         this._entries = [];
259         this._filteredEntries = [];
260         this._pendingInsertions = [];
261
262         for (let detailView of this._resourceDetailViewMap.values())
263             detailView.dispose();
264         this._resourceDetailViewMap.clear();
265
266         this._waterfallStartTime = NaN;
267         this._waterfallEndTime = NaN;
268         this._updateWaterfallTimelineRuler();
269         this._updateExportButton();
270
271         if (this._table) {
272             this._selectedResource = null;
273             this._table.clearSelectedRow();
274             this._table.reloadData();
275             this._hidePopover();
276             this._hideResourceDetailView();
277         }
278     }
279
280     showRepresentedObject(representedObject, cookie)
281     {
282         console.assert(representedObject instanceof WI.Resource);
283
284         let rowIndex = this._rowIndexForResource(representedObject);
285         if (rowIndex === -1) {
286             this._selectedResource = null;
287             this._table.clearSelectedRow();
288             this._hideResourceDetailView();
289             return;
290         }
291
292         this._table.selectRow(rowIndex);
293     }
294
295     // NetworkResourceDetailView delegate
296
297     networkResourceDetailViewClose(resourceDetailView)
298     {
299         this._selectedResource = null;
300         this._table.clearSelectedRow();
301         this._hideResourceDetailView();
302     }
303
304     // Table dataSource
305
306     tableNumberOfRows(table)
307     {
308         return this._filteredEntries.length;
309     }
310
311     tableSortChanged(table)
312     {
313         this._generateSortComparator();
314
315         if (!this._entriesSortComparator)
316             return;
317
318         this._hideResourceDetailView();
319
320         this._entries = this._entries.sort(this._entriesSortComparator);
321         this._updateFilteredEntries();
322         this._table.reloadData();
323     }
324
325     // Table delegate
326
327     tableCellMouseDown(table, cell, column, rowIndex, event)
328     {
329         if (column !== this._nameColumn)
330             return;
331
332         this._table.selectRow(rowIndex);
333     }
334
335     tableCellContextMenuClicked(table, cell, column, rowIndex, event)
336     {
337         if (column !== this._nameColumn)
338             return;
339
340         this._table.selectRow(rowIndex);
341
342         let entry = this._filteredEntries[rowIndex];
343         let contextMenu = WI.ContextMenu.createFromEvent(event);
344         WI.appendContextMenuItemsForSourceCode(contextMenu, entry.resource);
345
346         contextMenu.appendSeparator();
347         contextMenu.appendItem(WI.UIString("Export HAR"), () => { this._exportHAR(); });
348     }
349
350     tableSelectedRowChanged(table, rowIndex)
351     {
352         if (isNaN(rowIndex)) {
353             this._selectedResource = null;
354             this._hideResourceDetailView();
355             return;
356         }
357
358         let entry = this._filteredEntries[rowIndex];
359         if (entry.resource === this._selectedResource)
360             return;
361
362         this._selectedResource = entry.resource;
363         this._showResourceDetailView(this._selectedResource);
364     }
365
366     tablePopulateCell(table, cell, column, rowIndex)
367     {
368         let entry = this._filteredEntries[rowIndex];
369
370         cell.classList.toggle("error", entry.resource.hadLoadingError());
371
372         switch (column.identifier) {
373         case "name":
374             this._populateNameCell(cell, entry);
375             break;
376         case "domain":
377             this._populateDomainCell(cell, entry);
378             break;
379         case "type":
380             cell.textContent = entry.displayType || emDash;
381             break;
382         case "mimeType":
383             cell.textContent = entry.mimeType || emDash;
384             break;
385         case "method":
386             cell.textContent = entry.method || emDash;
387             break;
388         case "scheme":
389             cell.textContent = entry.scheme || emDash;
390             break;
391         case "status":
392             cell.textContent = entry.status || emDash;
393             break;
394         case "protocol":
395             cell.textContent = entry.protocol || emDash;
396             break;
397         case "priority":
398             cell.textContent = WI.Resource.displayNameForPriority(entry.priority) || emDash;
399             break;
400         case "remoteAddress":
401             cell.textContent = entry.remoteAddress || emDash;
402             break;
403         case "connectionIdentifier":
404             cell.textContent = entry.connectionIdentifier || emDash;
405             break;
406         case "resourceSize":
407             cell.textContent = isNaN(entry.resourceSize) ? emDash : Number.bytesToString(entry.resourceSize);
408             break;
409         case "transferSize":
410             this._populateTransferSizeCell(cell, entry);
411             break;
412         case "time":
413             // FIXME: <https://webkit.org/b/176748> Web Inspector: Frontend sometimes receives resources with negative duration (responseEnd - requestStart)
414             cell.textContent = isNaN(entry.time) ? emDash : Number.secondsToString(Math.max(entry.time, 0));
415             break;
416         case "waterfall":
417             this._populateWaterfallGraph(cell, entry);
418             break;
419         }
420
421         return cell;
422     }
423
424     // Private
425
426     _populateNameCell(cell, entry)
427     {
428         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
429
430         let resource = entry.resource;
431         if (resource.isLoading()) {
432             let statusElement = cell.appendChild(document.createElement("div"));
433             statusElement.className = "status";
434             let spinner = new WI.IndeterminateProgressSpinner;
435             statusElement.appendChild(spinner.element);
436         }
437
438         let iconElement = cell.appendChild(document.createElement("img"));
439         iconElement.className = "icon";
440         cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName, entry.resource.type);
441
442         let nameElement = cell.appendChild(document.createElement("span"));
443         nameElement.textContent = entry.name;
444
445         cell.title = entry.resource.url;
446     }
447
448     _populateDomainCell(cell, entry)
449     {
450         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
451
452         if (!entry.domain) {
453             cell.textContent = emDash;
454             return;
455         }
456
457         let secure = entry.scheme === "https" || entry.scheme === "wss";
458         if (secure) {
459             let lockIconElement = cell.appendChild(document.createElement("img"));
460             lockIconElement.className = "lock";
461         }
462
463         cell.append(entry.domain);
464     }
465
466     _populateTransferSizeCell(cell, entry)
467     {
468         let responseSource = entry.resource.responseSource;
469         if (responseSource === WI.Resource.ResponseSource.MemoryCache) {
470             cell.classList.add("cache-type");
471             cell.textContent = WI.UIString("(memory)");
472             return;
473         }
474         if (responseSource === WI.Resource.ResponseSource.DiskCache) {
475             cell.classList.add("cache-type");
476             cell.textContent = WI.UIString("(disk)");
477             return;
478         }
479         if (responseSource === WI.Resource.ResponseSource.ServiceWorker) {
480             cell.classList.add("cache-type");
481             cell.textContent = WI.UIString("(service worker)");
482             return;
483         }
484
485         let transferSize = entry.transferSize;
486         cell.textContent = isNaN(transferSize) ? emDash : Number.bytesToString(transferSize);
487         console.assert(!cell.classList.contains("cache-type"), "Should not have cache-type class on cell.");
488     }
489
490     _populateWaterfallGraph(cell, entry)
491     {
492         cell.removeChildren();
493
494         let resource = entry.resource;
495         if (!resource.hasResponse()) {
496             cell.textContent = zeroWidthSpace;
497             return;
498         }
499
500         let {startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
501         if (isNaN(startTime)) {
502             cell.textContent = zeroWidthSpace;
503             return;
504         }
505
506         let graphStartTime = this._waterfallTimelineRuler.startTime;
507         if (responseEnd < graphStartTime) {
508             cell.textContent = zeroWidthSpace;
509             return;
510         }
511
512         let graphEndTime = this._waterfallTimelineRuler.endTime;
513         if (startTime > graphEndTime) {
514             cell.textContent = zeroWidthSpace;
515             return;
516         }
517
518         let secondsPerPixel = this._waterfallTimelineRuler.secondsPerPixel;
519
520         let container = cell.appendChild(document.createElement("div"));
521         container.className = "waterfall-container";
522
523         function appendBlock(startTime, endTime, className) {
524             let startOffset = (startTime - graphStartTime) / secondsPerPixel;
525             let width = (endTime - startTime) / secondsPerPixel;
526             let block = container.appendChild(document.createElement("div"));
527             block.classList.add("block", className);
528             let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right";
529             block.style[styleAttribute] = startOffset + "px";
530             block.style.width = width + "px";
531             return block;
532         }
533
534         // Mouse block sits on top and accepts mouse events on this group.
535         let padSeconds = 10 * secondsPerPixel;
536         let mouseBlock = appendBlock(startTime - padSeconds, responseEnd + padSeconds, "mouse-tracking");
537         mouseBlock.addEventListener("mousedown", (event) => {
538             if (event.button !== 0 || event.ctrlKey)
539                 return;
540             this._handleMousedownWaterfall(mouseBlock, entry, event);
541         });
542
543         // Super small visualization.
544         let totalWidth = (responseEnd - startTime) / secondsPerPixel;
545         if (totalWidth <= 3) {
546             appendBlock(startTime, requestStart, "queue");
547             appendBlock(startTime, responseEnd, "response");
548             return;
549         }
550
551         // Each component.
552         if (domainLookupStart) {
553             appendBlock(startTime, domainLookupStart, "queue");
554             appendBlock(domainLookupStart, connectStart || requestStart, "dns");
555         } else if (connectStart)
556             appendBlock(startTime, connectStart, "queue");
557         else if (requestStart)
558             appendBlock(startTime, requestStart, "queue");
559         if (connectStart)
560             appendBlock(connectStart, connectEnd, "connect");
561         if (secureConnectionStart)
562             appendBlock(secureConnectionStart, connectEnd, "secure");
563         appendBlock(requestStart, responseStart, "request");
564         appendBlock(responseStart, responseEnd, "response");
565     }
566
567     _generateSortComparator()
568     {
569         let sortColumnIdentifier = this._table.sortColumnIdentifier;
570         if (!sortColumnIdentifier) {
571             this._entriesSortComparator = null;
572             return;
573         }
574
575         let comparator;
576
577         switch (sortColumnIdentifier) {
578         case "name":
579         case "domain":
580         case "mimeType":
581         case "method":
582         case "scheme":
583         case "protocol":
584         case "remoteAddress":
585             // Simple string.
586             comparator = (a, b) => (a[sortColumnIdentifier] || "").extendedLocaleCompare(b[sortColumnIdentifier] || "");
587             break;
588
589         case "status":
590         case "connectionIdentifier":
591         case "resourceSize":
592         case "time":
593             // Simple number.
594             comparator = (a, b) => {
595                 let aValue = a[sortColumnIdentifier];
596                 if (isNaN(aValue))
597                     return 1;
598                 let bValue = b[sortColumnIdentifier];
599                 if (isNaN(bValue))
600                     return -1;
601                 return aValue - bValue;
602             };
603             break;
604
605         case "priority":
606             // Resource.NetworkPriority enum.
607             comparator = (a, b) => WI.Resource.comparePriority(a.priority, b.priority);
608             break;
609
610         case "type":
611             // Sort by displayType string.
612             comparator = (a, b) => (a.displayType || "").extendedLocaleCompare(b.displayType || "");
613             break;
614
615         case "transferSize":
616             // Handle (memory) and (disk) values.
617             comparator = (a, b) => {
618                 let transferSizeA = a.transferSize;
619                 let transferSizeB = b.transferSize;
620
621                 // Treat NaN as the largest value.
622                 if (isNaN(transferSizeA))
623                     return 1;
624                 if (isNaN(transferSizeB))
625                     return -1;
626
627                 // Treat memory cache and disk cache as small values.
628                 let sourceA = a.resource.responseSource;
629                 if (sourceA === WI.Resource.ResponseSource.MemoryCache)
630                     transferSizeA = -20;
631                 else if (sourceA === WI.Resource.ResponseSource.DiskCache)
632                     transferSizeA = -10;
633                 else if (sourceA === WI.Resource.ResponseSource.ServiceWorker)
634                     transferSizeA = -5;
635
636                 let sourceB = b.resource.responseSource;
637                 if (sourceB === WI.Resource.ResponseSource.MemoryCache)
638                     transferSizeB = -20;
639                 else if (sourceB === WI.Resource.ResponseSource.DiskCache)
640                     transferSizeB = -10;
641                 else if (sourceB === WI.Resource.ResponseSource.ServiceWorker)
642                     transferSizeB = -5;
643
644                 return transferSizeA - transferSizeB;
645             };
646             break;
647
648         case "waterfall":
649             // Sort by startTime number.
650             comparator = comparator = (a, b) => a.startTime - b.startTime;
651             break;
652
653         default:
654             console.assert("Unexpected sort column", sortColumnIdentifier);
655             return;
656         }
657
658         let reverseFactor = this._table.sortOrder === WI.Table.SortOrder.Ascending ? 1 : -1;
659         this._entriesSortComparator = (a, b) => reverseFactor * comparator(a, b);
660     }
661
662     // Protected
663
664     initialLayout()
665     {
666         this._waterfallTimelineRuler = new WI.TimelineRuler;
667         this._waterfallTimelineRuler.allowsClippedLabels = true;
668
669         this._nameColumn = new WI.TableColumn("name", WI.UIString("Name"), {
670             minWidth: WI.Sidebar.AbsoluteMinimumWidth,
671             maxWidth: 500,
672             initialWidth: this._nameColumnWidthSetting.value,
673             resizeType: WI.TableColumn.ResizeType.Locked,
674         });
675
676         this._domainColumn = new WI.TableColumn("domain", WI.UIString("Domain"), {
677             minWidth: 120,
678             maxWidth: 200,
679             initialWidth: 150,
680         });
681
682         this._typeColumn = new WI.TableColumn("type", WI.UIString("Type"), {
683             minWidth: 70,
684             maxWidth: 120,
685             initialWidth: 90,
686         });
687
688         this._mimeTypeColumn = new WI.TableColumn("mimeType", WI.UIString("MIME Type"), {
689             hidden: true,
690             minWidth: 100,
691             maxWidth: 150,
692             initialWidth: 120,
693         });
694
695         this._methodColumn = new WI.TableColumn("method", WI.UIString("Method"), {
696             hidden: true,
697             minWidth: 55,
698             maxWidth: 80,
699             initialWidth: 65,
700         });
701
702         this._schemeColumn = new WI.TableColumn("scheme", WI.UIString("Scheme"), {
703             hidden: true,
704             minWidth: 55,
705             maxWidth: 80,
706             initialWidth: 65,
707         });
708
709         this._statusColumn = new WI.TableColumn("status", WI.UIString("Status"), {
710             hidden: true,
711             minWidth: 50,
712             maxWidth: 50,
713             align: "left",
714         });
715
716         this._protocolColumn = new WI.TableColumn("protocol", WI.UIString("Protocol"), {
717             hidden: true,
718             minWidth: 65,
719             maxWidth: 80,
720             initialWidth: 75,
721         });
722
723         this._priorityColumn = new WI.TableColumn("priority", WI.UIString("Priority"), {
724             hidden: true,
725             minWidth: 65,
726             maxWidth: 80,
727             initialWidth: 70,
728         });
729
730         this._remoteAddressColumn = new WI.TableColumn("remoteAddress", WI.UIString("IP Address"), {
731             hidden: true,
732             minWidth: 150,
733         });
734
735         this._connectionIdentifierColumn = new WI.TableColumn("connectionIdentifier", WI.UIString("Connection ID"), {
736             hidden: true,
737             minWidth: 50,
738             maxWidth: 120,
739             initialWidth: 80,
740             align: "right",
741         });
742
743         this._resourceSizeColumn = new WI.TableColumn("resourceSize", WI.UIString("Resource Size"), {
744             hidden: true,
745             minWidth: 80,
746             maxWidth: 100,
747             initialWidth: 80,
748             align: "right",
749         });
750
751         this._transferSizeColumn = new WI.TableColumn("transferSize", WI.UIString("Transfer Size"), {
752             minWidth: 100,
753             maxWidth: 150,
754             initialWidth: 100,
755             align: "right",
756         });
757
758         this._timeColumn = new WI.TableColumn("time", WI.UIString("Time"), {
759             minWidth: 65,
760             maxWidth: 90,
761             initialWidth: 65,
762             align: "right",
763         });
764
765         this._waterfallColumn = new WI.TableColumn("waterfall", WI.UIString("Waterfall"), {
766             minWidth: 230,
767             headerView: this._waterfallTimelineRuler,
768         });
769
770         this._nameColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableNameColumnDidChangeWidth, this);
771         this._waterfallColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableWaterfallColumnDidChangeWidth, this);
772
773         this._table = new WI.Table("network-table", this, this, 20);
774
775         this._table.addColumn(this._nameColumn);
776         this._table.addColumn(this._domainColumn);
777         this._table.addColumn(this._typeColumn);
778         this._table.addColumn(this._mimeTypeColumn);
779         this._table.addColumn(this._methodColumn);
780         this._table.addColumn(this._schemeColumn);
781         this._table.addColumn(this._statusColumn);
782         this._table.addColumn(this._protocolColumn);
783         this._table.addColumn(this._priorityColumn);
784         this._table.addColumn(this._remoteAddressColumn);
785         this._table.addColumn(this._connectionIdentifierColumn);
786         this._table.addColumn(this._resourceSizeColumn);
787         this._table.addColumn(this._transferSizeColumn);
788         this._table.addColumn(this._timeColumn);
789         this._table.addColumn(this._waterfallColumn);
790
791         if (!this._table.sortColumnIdentifier) {
792             this._table.sortOrder = WI.Table.SortOrder.Ascending;
793             this._table.sortColumnIdentifier = "waterfall";
794         }
795
796         this.addSubview(this._table);
797     }
798
799     layout()
800     {
801         this._updateWaterfallTimelineRuler();
802         this._processPendingEntries();
803         this._positionDetailView();
804         this._positionEmptyFilterMessage();
805         this._updateExportButton();
806     }
807
808     handleClearShortcut(event)
809     {
810         this.reset();
811     }
812
813     // Private
814
815     _updateWaterfallTimelineRuler()
816     {
817         if (!this._waterfallTimelineRuler)
818             return;
819
820         if (isNaN(this._waterfallStartTime)) {
821             this._waterfallTimelineRuler.zeroTime = 0;
822             this._waterfallTimelineRuler.startTime = 0;
823             this._waterfallTimelineRuler.endTime = 0.250;
824         } else {
825             this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime;
826             this._waterfallTimelineRuler.startTime = this._waterfallStartTime;
827             this._waterfallTimelineRuler.endTime = this._waterfallEndTime;
828
829             // Add a little bit of padding on the each side.
830             const paddingPixels = 5;
831             let padSeconds = paddingPixels * this._waterfallTimelineRuler.secondsPerPixel;
832             this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime - padSeconds;
833             this._waterfallTimelineRuler.startTime = this._waterfallStartTime - padSeconds;
834             this._waterfallTimelineRuler.endTime = this._waterfallEndTime + padSeconds;
835         }
836     }
837
838     _updateExportButton()
839     {
840         let enabled = this._filteredEntries.length > 0;
841         this._harExportNavigationItem.enabled = enabled;
842     }
843
844     _processPendingEntries()
845     {
846         let needsSort = this._pendingUpdates.length > 0;
847         let needsFilter = this._pendingFilter;
848
849         // No global sort or filter is needed, so just insert new records into their sorted position.
850         if (!needsSort && !needsFilter) {
851             let originalLength = this._pendingInsertions.length;
852             for (let resource of this._pendingInsertions)
853                 this._insertResourceAndReloadTable(resource);
854             console.assert(this._pendingInsertions.length === originalLength);
855             this._pendingInsertions = [];
856             return;
857         }
858
859         for (let resource of this._pendingInsertions)
860             this._entries.push(this._entryForResource(resource));
861         this._pendingInsertions = [];
862
863         for (let resource of this._pendingUpdates)
864             this._updateEntryForResource(resource);
865         this._pendingUpdates = [];
866
867         this._pendingFilter = false;
868
869         this._updateSortAndFilteredEntries();
870         this._table.reloadData();
871     }
872
873     _populateWithInitialResourcesIfNeeded()
874     {
875         if (!this._needsInitialPopulate)
876             return;
877
878         this._needsInitialPopulate = false;
879
880         let populateResourcesForFrame = (frame) => {
881             if (frame.provisionalMainResource)
882                 this._pendingInsertions.push(frame.provisionalMainResource);
883             else if (frame.mainResource)
884                 this._pendingInsertions.push(frame.mainResource);
885
886             for (let resource of frame.resourceCollection)
887                 this._pendingInsertions.push(resource);
888
889             for (let childFrame of frame.childFrameCollection)
890                 populateResourcesForFrame(childFrame);
891         };
892
893         let populateResourcesForTarget = (target) => {
894             if (target.mainResource instanceof WI.Resource)
895                 this._pendingInsertions.push(target.mainResource);
896             for (let resource of target.resourceCollection)
897                 this._pendingInsertions.push(resource);
898         };
899
900         for (let target of WI.targets) {
901             if (target === WI.pageTarget)
902                 populateResourcesForFrame(WI.frameResourceManager.mainFrame);
903             else
904                 populateResourcesForTarget(target);
905         }
906
907         this.needsLayout();
908     }
909
910     _checkURLFilterAgainstResource(resource)
911     {
912         if (this._urlFilterSearchRegex.test(resource.url))
913             this._activeURLFilterResources.add(resource);
914     }
915
916     _rowIndexForResource(resource)
917     {
918         return this._filteredEntries.findIndex((x) => x.resource === resource);
919     }
920
921     _updateEntryForResource(resource)
922     {
923         let index = this._entries.findIndex((x) => x.resource === resource);
924         if (index === -1)
925             return;
926
927         let entry = this._entryForResource(resource);
928         this._entries[index] = entry;
929
930         let rowIndex = this._rowIndexForResource(resource);
931         if (rowIndex === -1)
932             return;
933
934         this._filteredEntries[rowIndex] = entry;
935     }
936
937     _hidePopover()
938     {
939         if (this._waterfallPopover)
940             this._waterfallPopover.dismiss();
941     }
942
943     _hideResourceDetailView()
944     {
945         if (!this._resourceDetailView)
946             return;
947
948         this.element.classList.remove("showing-detail");
949         this._table.scrollContainer.style.removeProperty("width");
950
951         this.removeSubview(this._resourceDetailView);
952
953         this._resourceDetailView.hidden();
954         this._resourceDetailView = null;
955
956         this._table.resize();
957         this._table.reloadVisibleColumnCells(this._waterfallColumn);
958     }
959
960     _showResourceDetailView(resource)
961     {
962         let oldResourceDetailView = this._resourceDetailView;
963
964         this._resourceDetailView = this._resourceDetailViewMap.get(resource);
965         if (!this._resourceDetailView) {
966             this._resourceDetailView = new WI.NetworkResourceDetailView(resource, this);
967             this._resourceDetailViewMap.set(resource, this._resourceDetailView);
968         }
969
970         if (oldResourceDetailView) {
971             oldResourceDetailView.hidden();
972             this.replaceSubview(oldResourceDetailView, this._resourceDetailView);
973         } else
974             this.addSubview(this._resourceDetailView);
975         this._resourceDetailView.shown();
976
977         this.element.classList.add("showing-detail");
978         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
979
980         // FIXME: It would be nice to avoid this.
981         // Currently the ResourceDetailView is in the heirarchy but has not yet done a layout so we
982         // end up seeing the table behind it. This forces us to layout now instead of after a beat.
983         this.updateLayout();
984     }
985
986     _positionDetailView()
987     {
988         if (!this._resourceDetailView)
989             return;
990
991         let side = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
992         this._resourceDetailView.element.style[side] = this._nameColumn.width + "px";
993         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
994     }
995
996     _updateURLFilterActiveIndicator()
997     {
998         this._urlFilterNavigationItem.filterBar.indicatingActive = this._hasURLFilter();
999     }
1000
1001     _updateEmptyFilterResultsMessage()
1002     {
1003         if (this._hasActiveFilter() && !this._filteredEntries.length)
1004             this._showEmptyFilterResultsMessage();
1005         else
1006             this._hideEmptyFilterResultsMessage();
1007     }
1008
1009     _showEmptyFilterResultsMessage()
1010     {
1011         if (!this._emptyFilterResultsMessageElement) {
1012             let buttonElement = document.createElement("button");
1013             buttonElement.textContent = WI.UIString("Clear filters");
1014             buttonElement.addEventListener("click", () => { this._resetFilters(); });
1015
1016             this._emptyFilterResultsMessageElement = WI.createMessageTextView(WI.UIString("No Filter Results"));
1017             this._emptyFilterResultsMessageElement.appendChild(buttonElement);
1018         }
1019
1020         this.element.appendChild(this._emptyFilterResultsMessageElement);
1021         this._positionEmptyFilterMessage();
1022     }
1023
1024     _hideEmptyFilterResultsMessage()
1025     {
1026         if (!this._emptyFilterResultsMessageElement)
1027             return;
1028
1029         this._emptyFilterResultsMessageElement.remove();
1030     }
1031
1032     _positionEmptyFilterMessage()
1033     {
1034         if (!this._emptyFilterResultsMessageElement)
1035             return;
1036
1037         let width = this._nameColumn.width - 1; // For the 1px border.
1038         this._emptyFilterResultsMessageElement.style.width = width + "px";
1039     }
1040
1041     _clearNetworkOnNavigateSettingChanged()
1042     {
1043         this._clearOnLoadNavigationItem.checked = !WI.settings.clearNetworkOnNavigate.value;
1044     }
1045
1046     _resourceCachingDisabledSettingChanged()
1047     {
1048         this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
1049     }
1050
1051     _toggleDisableResourceCache()
1052     {
1053         WI.resourceCachingDisabledSetting.value = !WI.resourceCachingDisabledSetting.value;
1054     }
1055
1056     _mainResourceDidChange(event)
1057     {
1058         let frame = event.target;
1059         if (!frame.isMainFrame() || !WI.settings.clearNetworkOnNavigate.value)
1060             return;
1061
1062         this.reset();
1063
1064         this._insertResourceAndReloadTable(frame.mainResource);
1065     }
1066
1067     _mainFrameDidChange()
1068     {
1069         this._populateWithInitialResourcesIfNeeded();
1070     }
1071
1072     _resourceLoadingDidFinish(event)
1073     {
1074         let resource = event.target;
1075         this._pendingUpdates.push(resource);
1076
1077         if (resource.firstTimestamp < this._waterfallStartTime)
1078             this._waterfallStartTime = resource.firstTimestamp;
1079         if (resource.timingData.responseEnd > this._waterfallEndTime)
1080             this._waterfallEndTime = resource.timingData.responseEnd;
1081
1082         if (this._hasURLFilter())
1083             this._checkURLFilterAgainstResource(resource);
1084
1085         this.needsLayout();
1086     }
1087
1088     _resourceLoadingDidFail(event)
1089     {
1090         let resource = event.target;
1091         this._pendingUpdates.push(resource);
1092
1093         if (resource.firstTimestamp < this._waterfallStartTime)
1094             this._waterfallStartTime = resource.firstTimestamp;
1095         if (resource.timingData.responseEnd > this._waterfallEndTime)
1096             this._waterfallEndTime = resource.timingData.responseEnd;
1097
1098         if (this._hasURLFilter())
1099             this._checkURLFilterAgainstResource(resource);
1100
1101         this.needsLayout();
1102     }
1103
1104     _resourceTransferSizeDidChange(event)
1105     {
1106         if (!this._table)
1107             return;
1108
1109         let resource = event.target;
1110
1111         // In the unlikely event that this is the sort column, we may need to resort.
1112         if (this._table.sortColumnIdentifier === "transferSize") {
1113             this._pendingUpdates.push(resource);
1114             this.needsLayout();
1115             return;
1116         }
1117
1118         let index = this._entries.findIndex((x) => x.resource === resource);
1119         if (index === -1)
1120             return;
1121
1122         let entry = this._entries[index];
1123         entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
1124
1125         let rowIndex = this._rowIndexForResource(resource);
1126         if (rowIndex === -1)
1127             return;
1128
1129         this._table.reloadCell(rowIndex, "transferSize");
1130     }
1131
1132     _networkTimelineRecordAdded(event)
1133     {
1134         let resourceTimelineRecord = event.data.record;
1135         console.assert(resourceTimelineRecord instanceof WI.ResourceTimelineRecord);
1136
1137         let resource = resourceTimelineRecord.resource;
1138         if (isNaN(this._waterfallStartTime))
1139             this._waterfallStartTime = this._waterfallEndTime = resource.firstTimestamp;
1140
1141         this._insertResourceAndReloadTable(resource);
1142     }
1143
1144     _isDefaultSort()
1145     {
1146         return this._table.sortColumnIdentifier === "waterfall" && this._table.sortOrder === WI.Table.SortOrder.Ascending;
1147     }
1148
1149     _insertResourceAndReloadTable(resource)
1150     {
1151         if (!this._table || !(WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView)) {
1152             this._pendingInsertions.push(resource);
1153             this.needsLayout();
1154             return;
1155         }
1156
1157         let entry = this._entryForResource(resource);
1158
1159         // Default sort has fast path.
1160         if (this._isDefaultSort() || !this._entriesSortComparator) {
1161             this._entries.push(entry);
1162             if (this._passFilter(entry)) {
1163                 this._filteredEntries.push(entry);
1164                 this._table.reloadDataAddedToEndOnly();
1165             }
1166             return;
1167         }
1168
1169         insertObjectIntoSortedArray(entry, this._entries, this._entriesSortComparator);
1170
1171         if (this._passFilter(entry)) {
1172             insertObjectIntoSortedArray(entry, this._filteredEntries, this._entriesSortComparator);
1173
1174             // Probably a useless optimization here, but if we only added this row to the end
1175             // we may avoid recreating all visible rows by saying as such.
1176             if (this._filteredEntries.lastValue === entry)
1177                 this._table.reloadDataAddedToEndOnly();
1178             else
1179                 this._table.reloadData();
1180         }
1181     }
1182
1183     _entryForResource(resource)
1184     {
1185         // FIXME: <https://webkit.org/b/143632> Web Inspector: Resources with the same name in different folders aren't distinguished
1186         // FIXME: <https://webkit.org/b/176765> Web Inspector: Resource names should be less ambiguous
1187
1188         return {
1189             resource,
1190             name: WI.displayNameForURL(resource.url, resource.urlComponents),
1191             domain: WI.displayNameForHost(resource.urlComponents.host),
1192             scheme: resource.urlComponents.scheme ? resource.urlComponents.scheme.toLowerCase() : "",
1193             method: resource.requestMethod,
1194             type: resource.type,
1195             displayType: WI.NetworkTableContentView.displayNameForResource(resource),
1196             mimeType: resource.mimeType,
1197             status: resource.statusCode,
1198             cached: resource.cached,
1199             resourceSize: resource.size,
1200             transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize,
1201             time: resource.totalDuration,
1202             protocol: resource.protocol,
1203             priority: resource.priority,
1204             remoteAddress: resource.remoteAddress,
1205             connectionIdentifier: resource.connectionIdentifier,
1206             startTime: resource.firstTimestamp,
1207         };
1208     }
1209
1210     _hasTypeFilter()
1211     {
1212         return !!this._activeTypeFilters;
1213     }
1214
1215     _hasURLFilter()
1216     {
1217         return this._urlFilterIsActive;
1218     }
1219
1220     _hasActiveFilter()
1221     {
1222         return this._hasTypeFilter()
1223             || this._hasURLFilter();
1224     }
1225
1226     _passTypeFilter(entry)
1227     {
1228         if (!this._hasTypeFilter())
1229             return true;
1230         return this._activeTypeFilters.some((checker) => checker(entry.resource.type));
1231     }
1232
1233     _passURLFilter(entry)
1234     {
1235         if (!this._hasURLFilter())
1236             return true;
1237         return this._activeURLFilterResources.has(entry.resource);
1238     }
1239
1240     _passFilter(entry)
1241     {
1242         return this._passTypeFilter(entry)
1243             && this._passURLFilter(entry);
1244     }
1245
1246     _updateSortAndFilteredEntries()
1247     {
1248         if (this._entriesSortComparator)
1249             this._entries = this._entries.sort(this._entriesSortComparator);
1250
1251         this._updateFilteredEntries();
1252     }
1253
1254     _updateFilteredEntries()
1255     {
1256         if (this._hasActiveFilter())
1257             this._filteredEntries = this._entries.filter(this._passFilter, this);
1258         else
1259             this._filteredEntries = this._entries.slice();
1260
1261         this._restoreSelectedRow();
1262
1263         this._updateURLFilterActiveIndicator();
1264         this._updateEmptyFilterResultsMessage();
1265     }
1266
1267     _generateTypeFilter()
1268     {
1269         let selectedItems = this._typeFilterScopeBar.selectedItems;
1270         if (!selectedItems.length || selectedItems.includes(this._typeFilterScopeBarItemAll))
1271             return null;
1272
1273         return selectedItems.map((item) => item.__checker);
1274     }
1275
1276     _resetFilters()
1277     {
1278         console.assert(this._hasActiveFilter());
1279
1280         // Clear url filter.
1281         this._urlFilterSearchText = null;
1282         this._urlFilterSearchRegex = null;
1283         this._urlFilterIsActive = false;
1284         this._activeURLFilterResources.clear();
1285         this._urlFilterNavigationItem.filterBar.clear();
1286         console.assert(!this._hasURLFilter());
1287
1288         // Clear type filter.
1289         this._typeFilterScopeBar.resetToDefault();
1290         console.assert(!this._hasTypeFilter());
1291
1292         console.assert(!this._hasActiveFilter());
1293
1294         this._updateFilteredEntries();
1295         this._table.reloadData();
1296     }
1297
1298     _areFilterListsIdentical(listA, listB)
1299     {
1300         if (listA && listB) {
1301             if (listA.length !== listB.length)
1302                 return false;
1303
1304             for (let i = 0; i < listA.length; ++i) {
1305                 if (listA[i] !== listB[i])
1306                     return false;
1307             }
1308
1309             return true;
1310         }
1311
1312         return false;
1313     }
1314
1315     _typeFilterScopeBarSelectionChanged(event)
1316     {
1317         // FIXME: <https://webkit.org/b/176763> Web Inspector: ScopeBar SelectionChanged event may dispatch multiple times for a single logical change
1318         // We can't use shallow equals here because the contents are functions.
1319         let oldFilter = this._activeTypeFilters;
1320         let newFilter = this._generateTypeFilter();
1321         if (this._areFilterListsIdentical(oldFilter, newFilter))
1322             return;
1323
1324         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1325         this._hideResourceDetailView();
1326
1327         this._activeTypeFilters = newFilter;
1328         this._updateFilteredEntries();
1329         this._table.reloadData();
1330     }
1331
1332     _urlFilterDidChange(event)
1333     {
1334         let searchQuery = this._urlFilterNavigationItem.filterBar.filters.text;
1335         if (searchQuery === this._urlFilterSearchText)
1336             return;
1337
1338         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1339         this._hideResourceDetailView();
1340
1341         // Search cleared.
1342         if (!searchQuery) {
1343             this._urlFilterSearchText = null;
1344             this._urlFilterSearchRegex = null;
1345             this._urlFilterIsActive = false;
1346             this._activeURLFilterResources.clear();
1347
1348             this._updateFilteredEntries();
1349             this._table.reloadData();
1350             return;
1351         }
1352
1353         this._urlFilterIsActive = true;
1354         this._urlFilterSearchText = searchQuery;
1355         this._urlFilterSearchRegex = new RegExp(searchQuery.escapeForRegExp(), "i");
1356
1357         this._activeURLFilterResources.clear();
1358
1359         for (let entry of this._entries)
1360             this._checkURLFilterAgainstResource(entry.resource);
1361
1362         this._updateFilteredEntries();
1363         this._table.reloadData();
1364     }
1365
1366     _restoreSelectedRow()
1367     {
1368         if (!this._selectedResource)
1369             return;
1370
1371         let rowIndex = this._rowIndexForResource(this._selectedResource);
1372         if (rowIndex === -1) {
1373             this._selectedResource = null;
1374             this._table.clearSelectedRow();
1375             return;
1376         }
1377
1378         this._table.selectRow(rowIndex);
1379     }
1380
1381     _HARResources()
1382     {
1383         let resources = this._filteredEntries.map((x) => x.resource);
1384         const supportedHARSchemes = new Set(["http", "https", "ws", "wss"]);
1385         return resources.filter((resource) => resource.finished && supportedHARSchemes.has(resource.urlComponents.scheme));
1386     }
1387
1388     _exportHAR()
1389     {
1390         let resources = this._HARResources();
1391         if (!resources.length) {
1392             InspectorFrontendHost.beep();
1393             return;
1394         }
1395
1396         WI.HARBuilder.buildArchive(resources).then((har) => {
1397             let mainFrame = WI.frameResourceManager.mainFrame;
1398             let archiveName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName || "Archive";
1399             let url = "web-inspector:///" + encodeURI(archiveName) + ".har";
1400             WI.saveDataToFile({
1401                 url,
1402                 content: JSON.stringify(har, null, 2),
1403                 forceSaveAs: true,
1404             });
1405         }).catch(handlePromiseException);
1406     }
1407
1408     _waterfallPopoverContentForResource(resource)
1409     {
1410         let contentElement = document.createElement("div");
1411         contentElement.className = "waterfall-popover-content";
1412
1413         if (!resource.hasResponse() || !resource.timingData.startTime || !resource.timingData.responseEnd) {
1414             contentElement.textContent = WI.UIString("Resource has no timing data");
1415             return contentElement;
1416         }
1417
1418         let breakdownView = new WI.ResourceTimingBreakdownView(resource, 300);
1419         contentElement.appendChild(breakdownView.element);
1420         breakdownView.updateLayout();
1421
1422         return contentElement;
1423     }
1424
1425     _handleMousedownWaterfall(mouseBlock, entry, event)
1426     {
1427         if (!this._waterfallPopover) {
1428             this._waterfallPopover = new WI.Popover;
1429             this._waterfallPopover.element.classList.add("waterfall-popover");
1430         }
1431
1432         if (this._waterfallPopover.visible)
1433             return;
1434
1435         let calculateTargetFrame = () => {
1436             let rowIndex = this._rowIndexForResource(entry.resource);
1437             let cell = this._table.cellForRowAndColumn(rowIndex, this._waterfallColumn);
1438             if (!cell) {
1439                 this._waterfallPopover.dismiss();
1440                 return null;
1441             }
1442
1443             let mouseBlock = cell.querySelector(".block.mouse-tracking");
1444             if (!mouseBlock) {
1445                 this._waterfallPopover.dismiss();
1446                 return null;
1447             }
1448
1449             return WI.Rect.rectFromClientRect(mouseBlock.getBoundingClientRect());
1450         };
1451
1452         let targetFrame = calculateTargetFrame();
1453         if (!targetFrame)
1454             return;
1455         if (!targetFrame.size.width && !targetFrame.size.height)
1456             return;
1457
1458         let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
1459         let preferredEdges = isRTL ? [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X] : [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MIN_X];
1460         this._waterfallPopover.windowResizeHandler = () => {
1461             let bounds = calculateTargetFrame();
1462             if (bounds)
1463                 this._waterfallPopover.present(bounds, preferredEdges);
1464         };
1465
1466         let popoverContentElement = this._waterfallPopoverContentForResource(entry.resource);
1467         this._waterfallPopover.presentNewContentWithFrame(popoverContentElement, targetFrame, preferredEdges);
1468     }
1469
1470     _tableNameColumnDidChangeWidth(event)
1471     {
1472         this._nameColumnWidthSetting.value = event.target.width;
1473
1474         this._positionDetailView();
1475         this._positionEmptyFilterMessage();
1476     }
1477
1478     _tableWaterfallColumnDidChangeWidth(event)
1479     {
1480         this._table.reloadVisibleColumnCells(this._waterfallColumn);
1481     }
1482 };