0916da83d3e9dc923fc7456de7ef3134544a842d
[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         // FIXME: Network Timeline.
47         // FIXME: Throttling.
48         // FIXME: HAR Export.
49
50         const exclusive = true;
51         this._typeFilterScopeBarItemAll = new WI.ScopeBarItem("network-type-filter-all", WI.UIString("All"), exclusive);
52         let typeFilterScopeBarItems = [this._typeFilterScopeBarItemAll];
53
54         let uniqueTypes = [
55             ["Document", (type) => type === WI.Resource.Type.Document],
56             ["Stylesheet", (type) => type === WI.Resource.Type.Stylesheet],
57             ["Image", (type) => type === WI.Resource.Type.Image],
58             ["Font", (type) => type === WI.Resource.Type.Font],
59             ["Script", (type) => type === WI.Resource.Type.Script],
60             ["XHR", (type) => type === WI.Resource.Type.XHR || type === WI.Resource.Type.Fetch],
61             ["Other", (type) => type === WI.Resource.Type.Other || type === WI.Resource.Type.WebSocket],
62         ];
63         for (let [key, checker] of uniqueTypes) {
64             let type = WI.Resource.Type[key];
65             let scopeBarItem = new WI.ScopeBarItem("network-type-filter-" + key, WI.NetworkTableContentView.shortDisplayNameForResourceType(type))
66             scopeBarItem.__checker = checker;
67             typeFilterScopeBarItems.push(scopeBarItem);
68         }
69
70         this._typeFilterScopeBar = new WI.ScopeBar("network-type-filter-scope-bar", typeFilterScopeBarItems, typeFilterScopeBarItems[0]);
71         this._typeFilterScopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._typeFilterScopeBarSelectionChanged, this);
72
73         this._textFilterSearchId = 0;
74         this._textFilterSearchText = null;
75         this._textFilterIsActive = false;
76
77         this._textFilterNavigationItem = new WI.FilterBarNavigationItem;
78         this._textFilterNavigationItem.filterBar.incremental = false;
79         this._textFilterNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._textFilterDidChange, this);
80         this._textFilterNavigationItem.filterBar.placeholder = WI.UIString("Filter Full URL and Text");
81
82         this._activeTypeFilters = this._generateTypeFilter();
83         this._activeTextFilterResources = new Set;
84
85         this._emptyFilterResultsMessageElement = null;
86
87         // COMPATIBILITY (iOS 10.3): Network.setDisableResourceCaching did not exist.
88         if (window.NetworkAgent && NetworkAgent.setResourceCachingDisabled) {
89             let toolTipForDisableResourceCache = WI.UIString("Ignore the resource cache when loading resources");
90             let activatedToolTipForDisableResourceCache = WI.UIString("Use the resource cache when loading resources");
91             this._disableResourceCacheNavigationItem = new WI.ActivateButtonNavigationItem("disable-resource-cache", toolTipForDisableResourceCache, activatedToolTipForDisableResourceCache, "Images/IgnoreCaches.svg", 16, 16);
92             this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
93
94             this._disableResourceCacheNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleDisableResourceCache, this);
95             WI.resourceCachingDisabledSetting.addEventListener(WI.Setting.Event.Changed, this._resourceCachingDisabledSettingChanged, this);
96         }
97
98         this._clearNetworkItemsNavigationItem = new WI.ButtonNavigationItem("clear-network-items", WI.UIString("Clear Network Items (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemClear.svg", 16, 16);
99         this._clearNetworkItemsNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => this.reset());
100
101         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
102         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this);
103         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._resourceLoadingDidFail, this);
104         WI.Resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._resourceTransferSizeDidChange, this);
105         WI.timelineManager.persistentNetworkTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
106     }
107
108     // Static
109
110     static shortDisplayNameForResourceType(type)
111     {
112         switch (type) {
113         case WI.Resource.Type.Document:
114             return WI.UIString("Document");
115         case WI.Resource.Type.Stylesheet:
116             return "CSS";
117         case WI.Resource.Type.Image:
118             return WI.UIString("Image");
119         case WI.Resource.Type.Font:
120             return WI.UIString("Font");
121         case WI.Resource.Type.Script:
122             return "JS";
123         case WI.Resource.Type.XHR:
124         case WI.Resource.Type.Fetch:
125             return "XHR";
126         case WI.Resource.Type.Ping:
127             return WI.UIString("Ping");
128         case WI.Resource.Type.Beacon:
129             return WI.UIString("Beacon");
130         case WI.Resource.Type.WebSocket:
131         case WI.Resource.Type.Other:
132             return WI.UIString("Other");
133         default:
134             console.error("Unknown resource type", type);
135             return null;
136         }
137     }
138
139     // Public
140
141     get selectionPathComponents()
142     {
143         return null;
144     }
145
146     get navigationItems()
147     {
148         let items = [];
149         if (this._disableResourceCacheNavigationItem)
150             items.push(this._disableResourceCacheNavigationItem);
151         items.push(this._clearNetworkItemsNavigationItem);
152         return items;
153     }
154
155     get filterNavigationItems()
156     {
157         let items = [];
158         if (window.PageAgent)
159             items.push(this._textFilterNavigationItem);
160         items.push(this._typeFilterScopeBar);
161         return items;
162     }
163
164     shown()
165     {
166         super.shown();
167
168         if (this._resourceDetailView)
169             this._resourceDetailView.shown();
170
171         if (this._table)
172             this._table.restoreScrollPosition();
173     }
174
175     hidden()
176     {
177         if (this._resourceDetailView)
178             this._resourceDetailView.hidden();
179
180         super.hidden();
181     }
182
183     closed()
184     {
185         this._hideResourceDetailView();
186
187         for (let detailView of this._resourceDetailViewMap.values())
188             detailView.dispose();
189         this._resourceDetailViewMap.clear();
190
191         WI.Frame.removeEventListener(null, null, this);
192         WI.Resource.removeEventListener(null, null, this);
193         WI.timelineManager.persistentNetworkTimeline.removeEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
194
195         super.closed();
196     }
197
198     reset()
199     {
200         this._entries = [];
201         this._filteredEntries = [];
202         this._pendingInsertions = [];
203
204         for (let detailView of this._resourceDetailViewMap.values())
205             detailView.dispose();
206         this._resourceDetailViewMap.clear();
207
208         if (this._table) {
209             this._hideResourceDetailView();
210             this._selectedResource = null;
211             this._table.clearSelectedRow();
212             this._table.reloadData();
213         }
214     }
215
216     // NetworkResourceDetailView delegate
217
218     networkResourceDetailViewClose(resourceDetailView)
219     {
220         this._hideResourceDetailView();
221         this._selectedResource = null;
222         this._table.clearSelectedRow();
223     }
224
225     // Table dataSource
226
227     tableNumberOfRows(table)
228     {
229         return this._filteredEntries.length;
230     }
231
232     tableSortChanged(table)
233     {
234         this._generateSortComparator();
235
236         if (!this._entriesSortComparator)
237             return;
238
239         this._hideResourceDetailView();
240
241         this._entries = this._entries.sort(this._entriesSortComparator);
242         this._updateFilteredEntries();
243         this._table.reloadData();
244     }
245
246     // Table delegate
247
248     tableCellMouseDown(table, cell, column, rowIndex, event)
249     {
250         if (column !== this._nameColumn)
251             return;
252
253         this._table.selectRow(rowIndex);
254     }
255
256     tableCellContextMenuClicked(table, cell, column, rowIndex, event)
257     {
258         if (column !== this._nameColumn)
259             return;
260
261         this._table.selectRow(rowIndex);
262
263         let entry = this._filteredEntries[rowIndex];
264         let contextMenu = WI.ContextMenu.createFromEvent(event);
265         WI.appendContextMenuItemsForSourceCode(contextMenu, entry.resource);
266     }
267
268     tableSelectedRowChanged(table, rowIndex)
269     {
270         if (isNaN(rowIndex)) {
271             this._selectedResource = null;
272             this._hideResourceDetailView();
273             return;
274         }
275
276         let entry = this._filteredEntries[rowIndex];
277         if (entry.resource === this._selectedResource)
278             return;
279
280         this._selectedResource = entry.resource;
281         this._showResourceDetailView(this._selectedResource);
282     }
283
284     tablePopulateCell(table, cell, column, rowIndex)
285     {
286         let entry = this._filteredEntries[rowIndex];
287
288         cell.classList.toggle("error", entry.resource.hadLoadingError());
289
290         switch (column.identifier) {
291         case "name":
292             this._populateNameCell(cell, entry);
293             break;
294         case "domain":
295             cell.textContent = entry.domain || emDash;
296             break;
297         case "type":
298             cell.textContent = entry.displayType || emDash;
299             break;
300         case "mimeType":
301             cell.textContent = entry.mimeType || emDash;
302             break;
303         case "method":
304             cell.textContent = entry.method || emDash;
305             break;
306         case "scheme":
307             cell.textContent = entry.scheme || emDash;
308             break;
309         case "status":
310             cell.textContent = entry.status || emDash;
311             break;
312         case "protocol":
313             cell.textContent = entry.protocol || emDash;
314             break;
315         case "priority":
316             cell.textContent = WI.Resource.displayNameForPriority(entry.priority) || emDash;
317             break;
318         case "remoteAddress":
319             cell.textContent = entry.remoteAddress || emDash;
320             break;
321         case "connectionIdentifier":
322             cell.textContent = entry.connectionIdentifier || emDash;
323             break;
324         case "resourceSize":
325             cell.textContent = isNaN(entry.resourceSize) ? emDash : Number.bytesToString(entry.resourceSize);
326             break;
327         case "transferSize":
328             this._populateTransferSizeCell(cell, entry);
329             break;
330         case "time":
331             // FIXME: <https://webkit.org/b/176748> Web Inspector: Frontend sometimes receives resources with negative duration (responseEnd - requestStart)
332             cell.textContent = isNaN(entry.time) ? emDash : Number.secondsToString(Math.max(entry.time, 0));
333             break;
334         case "waterfall":
335             // FIXME: Waterfall graph.
336             cell.textContent = emDash;
337             break;
338         }
339
340         return cell;
341     }
342
343     // Private
344
345     _populateNameCell(cell, entry)
346     {
347         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
348
349         let resource = entry.resource;
350         if (resource.isLoading()) {
351             let statusElement = cell.appendChild(document.createElement("div"));
352             statusElement.className = "status";
353             let spinner = new WI.IndeterminateProgressSpinner;
354             statusElement.appendChild(spinner.element);
355         }
356
357         let iconElement = cell.appendChild(document.createElement("img"));
358         iconElement.className = "icon";
359         cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName, entry.resource.type);
360
361         let nameElement = cell.appendChild(document.createElement("span"));
362         nameElement.textContent = entry.name;
363     }
364
365     _populateTransferSizeCell(cell, entry)
366     {
367         let responseSource = entry.resource.responseSource;
368         if (responseSource === WI.Resource.ResponseSource.MemoryCache) {
369             cell.classList.add("cache-type");
370             cell.textContent = WI.UIString("(memory)");
371             return;
372         }
373         if (responseSource === WI.Resource.ResponseSource.DiskCache) {
374             cell.classList.add("cache-type");
375             cell.textContent = WI.UIString("(disk)");
376             return;
377         }
378
379         let transferSize = entry.transferSize;
380         cell.textContent = isNaN(transferSize) ? emDash : Number.bytesToString(transferSize);
381         console.assert(!cell.classList.contains("cache-type"), "Should not have cache-type class on cell.");
382     }
383
384     _generateSortComparator()
385     {
386         let sortColumnIdentifier = this._table.sortColumnIdentifier;
387         if (!sortColumnIdentifier) {
388             this._entriesSortComparator = null;
389             return;
390         }
391
392         let comparator;
393
394         switch (sortColumnIdentifier) {
395         case "name":
396         case "domain":
397         case "mimeType":
398         case "method":
399         case "scheme":
400         case "protocol":
401         case "remoteAddress":
402             // Simple string.
403             comparator = (a, b) => (a[sortColumnIdentifier] || "").extendedLocaleCompare(b[sortColumnIdentifier] || "");
404             break;
405
406         case "status":
407         case "connectionIdentifier":
408         case "resourceSize":
409         case "time":
410             // Simple number.
411             comparator = (a, b) => {
412                 let aValue = a[sortColumnIdentifier];
413                 if (isNaN(aValue))
414                     return 1;
415                 let bValue = b[sortColumnIdentifier];
416                 if (isNaN(bValue))
417                     return -1;
418                 return aValue - bValue;
419             }
420             break;
421
422         case "priority":
423             // Resource.NetworkPriority enum.
424             comparator = (a, b) => WI.Resource.comparePriority(a.priority, b.priority);
425             break;
426
427         case "type":
428             // Sort by displayType string.
429             comparator = (a, b) => (a.displayType || "").extendedLocaleCompare(b.displayType || "");
430             break;
431
432         case "transferSize":
433             // Handle (memory) and (disk) values.
434             comparator = (a, b) => {
435                 let transferSizeA = a.transferSize;
436                 let transferSizeB = b.transferSize;
437
438                 // Treat NaN as the largest value.
439                 if (isNaN(transferSizeA))
440                     return 1;
441                 if (isNaN(transferSizeB))
442                     return -1;
443
444                 // Treat memory cache and disk cache as small values.
445                 let sourceA = a.resource.responseSource;
446                 if (sourceA === WI.Resource.ResponseSource.MemoryCache)
447                     transferSizeA = -20;
448                 else if (sourceA === WI.Resource.ResponseSource.DiskCache)
449                     transferSizeA = -10;
450
451                 let sourceB = b.resource.responseSource;
452                 if (sourceB === WI.Resource.ResponseSource.MemoryCache)
453                     transferSizeB = -20;
454                 else if (sourceB === WI.Resource.ResponseSource.DiskCache)
455                     transferSizeB = -10;
456
457                 return transferSizeA - transferSizeB;
458             };
459             break;
460
461         case "waterfall":
462             // Sort by startTime number.
463             comparator = comparator = (a, b) => a.startTime - b.startTime;
464             break;
465
466         default:
467             console.assert("Unexpected sort column", sortColumnIdentifier);
468             return;
469         }
470
471         let reverseFactor = this._table.sortOrder === WI.Table.SortOrder.Ascending ? 1 : -1;
472         this._entriesSortComparator = (a, b) => reverseFactor * comparator(a, b);
473     }
474
475     // Protected
476
477     initialLayout()
478     {
479         this._nameColumn = new WI.TableColumn("name", WI.UIString("Name"), {
480             initialWidth: this._nameColumnWidthSetting.value,
481             minWidth: WI.Sidebar.AbsoluteMinimumWidth,
482             maxWidth: 500,
483             resizeType: WI.TableColumn.ResizeType.Locked,
484         });
485
486         this._nameColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableNameColumnDidChangeWidth, this);
487
488         this._domainColumn = new WI.TableColumn("domain", WI.UIString("Domain"), {
489             minWidth: 120,
490             maxWidth: 200,
491         });
492
493         this._typeColumn = new WI.TableColumn("type", WI.UIString("Type"), {
494             minWidth: 70,
495             maxWidth: 120,
496         });
497
498         this._mimeTypeColumn = new WI.TableColumn("mimeType", WI.UIString("MIME Type"), {
499             hidden: true,
500             minWidth: 100,
501             maxWidth: 150,
502         });
503
504         this._methodColumn = new WI.TableColumn("method", WI.UIString("Method"), {
505             hidden: true,
506             minWidth: 55,
507             maxWidth: 80,
508         });
509
510         this._schemeColumn = new WI.TableColumn("scheme", WI.UIString("Scheme"), {
511             hidden: true,
512             minWidth: 55,
513             maxWidth: 80,
514         });
515
516         this._statusColumn = new WI.TableColumn("status", WI.UIString("Status"), {
517             hidden: true,
518             minWidth: 50,
519             maxWidth: 50,
520             align: "left",
521         });
522
523         this._protocolColumn = new WI.TableColumn("protocol", WI.UIString("Protocol"), {
524             hidden: true,
525             minWidth: 65,
526             maxWidth: 80,
527         });
528
529         this._priorityColumn = new WI.TableColumn("priority", WI.UIString("Priority"), {
530             hidden: true,
531             minWidth: 65,
532             maxWidth: 80,
533         });
534
535         this._remoteAddressColumn = new WI.TableColumn("remoteAddress", WI.UIString("IP Address"), {
536             hidden: true,
537             minWidth: 150,
538         });
539
540         this._connectionIdentifierColumn = new WI.TableColumn("connectionIdentifier", WI.UIString("Connection ID"), {
541             hidden: true,
542             minWidth: 50,
543             maxWidth: 120,
544             align: "right",
545         });
546
547         this._resourceSizeColumn = new WI.TableColumn("resourceSize", WI.UIString("Resource Size"), {
548             hidden: true,
549             minWidth: 80,
550             maxWidth: 100,
551             align: "right",
552         });
553
554         this._transferSizeColumn = new WI.TableColumn("transferSize", WI.UIString("Transfer Size"), {
555             minWidth: 100,
556             maxWidth: 150,
557             align: "right",
558         });
559
560         this._timeColumn = new WI.TableColumn("time", WI.UIString("Time"), {
561             minWidth: 65,
562             maxWidth: 90,
563             align: "right",
564         });
565
566         this._waterfallColumn = new WI.TableColumn("waterfall", WI.UIString("Waterfall"), {
567             minWidth: 230,
568         });
569
570         this._table = new WI.Table("network-table", this, this, 20);
571
572         this._table.addColumn(this._nameColumn);
573         this._table.addColumn(this._domainColumn);
574         this._table.addColumn(this._typeColumn);
575         this._table.addColumn(this._mimeTypeColumn);
576         this._table.addColumn(this._methodColumn);
577         this._table.addColumn(this._schemeColumn);
578         this._table.addColumn(this._statusColumn);
579         this._table.addColumn(this._protocolColumn);
580         this._table.addColumn(this._priorityColumn);
581         this._table.addColumn(this._remoteAddressColumn);
582         this._table.addColumn(this._connectionIdentifierColumn);
583         this._table.addColumn(this._resourceSizeColumn);
584         this._table.addColumn(this._transferSizeColumn);
585         this._table.addColumn(this._timeColumn);
586         this._table.addColumn(this._waterfallColumn);
587
588         if (!this._table.sortColumnIdentifier) {
589             this._table.sortOrder = WI.Table.SortOrder.Ascending;
590             this._table.sortColumnIdentifier = "waterfall";
591         }
592
593         this.addSubview(this._table);
594     }
595
596     layout()
597     {
598         this._processPendingEntries();
599         this._positionDetailView();
600         this._positionEmptyFilterMessage();
601     }
602
603     handleClearShortcut(event)
604     {
605         this.reset();
606     }
607
608     // Private
609
610     _processPendingEntries()
611     {
612         let needsSort = this._pendingUpdates.length > 0;
613         let needsFilter = this._pendingFilter;
614
615         // No global sort or filter is needed, so just insert new records into their sorted position.
616         if (!needsSort && !needsFilter) {
617             let originalLength = this._pendingInsertions.length;
618             for (let resource of this._pendingInsertions)
619                 this._insertResourceAndReloadTable(resource);
620             console.assert(this._pendingInsertions.length === originalLength);
621             this._pendingInsertions = [];
622             return;
623         }
624
625         for (let resource of this._pendingInsertions)
626             this._entries.push(this._entryForResource(resource));
627         this._pendingInsertions = [];
628
629         for (let resource of this._pendingUpdates)
630             this._updateEntryForResource(resource);
631         this._pendingUpdates = [];
632
633         this._pendingFilter = false;
634
635         this._updateSortAndFilteredEntries();
636         this._table.reloadData();
637     }
638
639     _checkTextFilterAgainstFinishedResource(resource)
640     {
641         let frame = resource.parentFrame;
642         if (!frame)
643             return;
644
645         let searchQuery = this._textFilterSearchText;
646         if (resource.url.includes(searchQuery)) {
647             this._activeTextFilterResources.add(resource);
648             return;
649         }
650
651         let searchId = this._textFilterSearchId;
652
653         const isCaseSensitive = true;
654         const isRegex = false;
655         PageAgent.searchInResource(frame.id, resource.url, searchQuery, isCaseSensitive, isRegex, resource.requestIdentifier, (error, searchResults) => {
656             if (searchId !== this._textFilterSearchId)
657                 return;
658
659             if (error || !searchResults || !searchResults.length)
660                 return;
661
662             this._activeTextFilterResources.add(resource);
663
664             this._pendingFilter = true;
665             this.needsLayout();
666         });
667     }
668
669     _checkTextFilterAgainstFailedResource(resource)
670     {
671         let searchQuery = this._textFilterSearchText;
672         if (resource.url.includes(searchQuery))
673             this._activeTextFilterResources.add(resource);
674     }
675
676     _rowIndexForResource(resource)
677     {
678         return this._filteredEntries.findIndex((x) => x.resource === resource);
679     }
680
681     _updateEntryForResource(resource)
682     {
683         let index = this._entries.findIndex((x) => x.resource === resource);
684         if (index === -1)
685             return;
686
687         let entry = this._entryForResource(resource);
688         this._entries[index] = entry;
689
690         let rowIndex = this._rowIndexForResource(resource);
691         if (rowIndex === -1)
692             return;
693
694         this._filteredEntries[rowIndex] = entry;
695     }
696
697     _hideResourceDetailView()
698     {
699         if (!this._resourceDetailView)
700             return;
701
702         this.element.classList.remove("showing-detail");
703         this._table.scrollContainer.style.removeProperty("width");
704
705         this.removeSubview(this._resourceDetailView);
706
707         this._resourceDetailView.hidden();
708         this._resourceDetailView = null;
709
710         this._table.resize();
711     }
712
713     _showResourceDetailView(resource)
714     {
715         let oldResourceDetailView = this._resourceDetailView;
716
717         this._resourceDetailView = this._resourceDetailViewMap.get(resource);
718         if (!this._resourceDetailView) {
719             this._resourceDetailView = new WI.NetworkResourceDetailView(resource, this);
720             this._resourceDetailViewMap.set(resource, this._resourceDetailView);
721         }
722
723         if (oldResourceDetailView) {
724             oldResourceDetailView.hidden();
725             this.replaceSubview(oldResourceDetailView, this._resourceDetailView);
726         } else
727             this.addSubview(this._resourceDetailView);
728         this._resourceDetailView.shown();
729
730         this.element.classList.add("showing-detail");
731         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
732
733         // FIXME: It would be nice to avoid this.
734         // Currently the ResourceDetailView is in the heirarchy but has not yet done a layout so we
735         // end up seeing the table behind it. This forces us to layout now instead of after a beat.
736         this.updateLayout();
737     }
738
739     _positionDetailView()
740     {
741         if (!this._resourceDetailView)
742             return;
743
744         let side = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
745         this._resourceDetailView.element.style[side] = this._nameColumn.width + "px";
746         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
747     }
748
749     _updateTextFilterActiveIndicator()
750     {
751         this._textFilterNavigationItem.filterBar.indicatingActive = this._hasTextFilter();
752     }
753
754     _updateEmptyFilterResultsMessage()
755     {
756         if (this._hasActiveFilter() && !this._filteredEntries.length)
757             this._showEmptyFilterResultsMessage();
758         else
759             this._hideEmptyFilterResultsMessage();
760     }
761
762     _showEmptyFilterResultsMessage()
763     {
764         if (!this._emptyFilterResultsMessageElement) {
765             let message = WI.UIString("No Filter Results");
766             let buttonElement = document.createElement("button");
767             buttonElement.textContent = WI.UIString("Clear filters");
768             buttonElement.addEventListener("click", () => { this._resetFilters(); });
769
770             this._emptyFilterResultsMessageElement = document.createElement("div");
771             this._emptyFilterResultsMessageElement.className = "empty-content-placeholder";
772
773             let messageElement = this._emptyFilterResultsMessageElement.appendChild(document.createElement("div"));
774             messageElement.className = "message";
775             messageElement.append(message, document.createElement("br"), buttonElement);
776         }
777
778         this.element.appendChild(this._emptyFilterResultsMessageElement);
779         this._positionEmptyFilterMessage();
780     }
781
782     _hideEmptyFilterResultsMessage()
783     {
784         if (!this._emptyFilterResultsMessageElement)
785             return;
786
787         this._emptyFilterResultsMessageElement.remove();
788     }
789
790     _positionEmptyFilterMessage()
791     {
792         if (!this._emptyFilterResultsMessageElement)
793             return;
794
795         let width = this._nameColumn.width - 1; // For the 1px border.
796         this._emptyFilterResultsMessageElement.style.width = width + "px";
797     }
798
799     _resourceCachingDisabledSettingChanged()
800     {
801         this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
802     }
803
804     _toggleDisableResourceCache()
805     {
806         WI.resourceCachingDisabledSetting.value = !WI.resourceCachingDisabledSetting.value;
807     }
808
809     _mainResourceDidChange(event)
810     {
811         let frame = event.target;
812         if (!frame.isMainFrame() || !WI.settings.clearNetworkOnNavigate.value)
813             return;
814
815         this.reset();
816
817         this._insertResourceAndReloadTable(frame.mainResource);
818     }
819
820     _resourceLoadingDidFinish(event)
821     {
822         let resource = event.target;
823         this._pendingUpdates.push(resource);
824
825         if (this._hasTextFilter())
826             this._checkTextFilterAgainstFinishedResource(resource);
827
828         this.needsLayout();
829     }
830
831     _resourceLoadingDidFail(event)
832     {
833         let resource = event.target;
834         this._pendingUpdates.push(resource);
835
836         if (this._hasTextFilter())
837             this._checkTextFilterAgainstFailedResource(resource);
838
839         this.needsLayout();
840     }
841
842     _resourceTransferSizeDidChange(event)
843     {
844         if (!this._table)
845             return;
846
847         let resource = event.target;
848
849         // In the unlikely event that this is the sort column, we may need to resort.
850         if (this._table.sortColumnIdentifier === "transferSize") {
851             this._pendingUpdates.push(resource);
852             this.needsLayout();
853             return;
854         }
855
856         let index = this._entries.findIndex((x) => x.resource === resource);
857         if (index === -1)
858             return;
859
860         let entry = this._entries[index];
861         entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
862
863         let rowIndex = this._rowIndexForResource(resource);
864         if (rowIndex === -1)
865             return;
866
867         this._table.reloadCell(rowIndex, "transferSize");
868     }
869
870     _networkTimelineRecordAdded(event)
871     {
872         let resourceTimelineRecord = event.data.record;
873         console.assert(resourceTimelineRecord instanceof WI.ResourceTimelineRecord);
874
875         let resource = resourceTimelineRecord.resource;
876         this._insertResourceAndReloadTable(resource);
877     }
878
879     _isDefaultSort()
880     {
881         return this._table.sortColumnIdentifier === "waterfall" && this._table.sortOrder === WI.Table.SortOrder.Ascending;
882     }
883
884     _insertResourceAndReloadTable(resource)
885     {
886         if (!(WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView)) {
887             this._pendingInsertions.push(resource);
888             this.needsLayout();
889             return;
890         }
891
892         console.assert(this._table);
893         if (!this._table)
894             return;
895
896         let entry = this._entryForResource(resource);
897
898         // Default sort has fast path.
899         if (this._isDefaultSort() || !this._entriesSortComparator) {
900             this._entries.push(entry);
901             if (this._passFilter(entry)) {
902                 this._filteredEntries.push(entry);
903                 this._table.reloadDataAddedToEndOnly();
904             }
905             return;
906         }
907
908         insertObjectIntoSortedArray(entry, this._entries, this._entriesSortComparator);
909
910         if (this._passFilter(entry)) {
911             insertObjectIntoSortedArray(entry, this._filteredEntries, this._entriesSortComparator);
912
913             // Probably a useless optimization here, but if we only added this row to the end
914             // we may avoid recreating all visible rows by saying as such.
915             if (this._filteredEntries.lastValue === entry)
916                 this._table.reloadDataAddedToEndOnly();
917             else
918                 this._table.reloadData();
919         }
920     }
921
922     _displayType(resource)
923     {
924         if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font) {
925             let fileExtension;
926             if (resource.mimeType)
927                 fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
928             if (!fileExtension)
929                 fileExtension = WI.fileExtensionForURL(resource.url);
930             if (fileExtension)
931                 return fileExtension;
932         }
933
934         return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase();
935     }
936
937     _entryForResource(resource)
938     {
939         // FIXME: <https://webkit.org/b/143632> Web Inspector: Resources with the same name in different folders aren't distinguished
940         // FIXME: <https://webkit.org/b/176765> Web Inspector: Resource names should be less ambiguous
941
942         return {
943             resource,
944             name: WI.displayNameForURL(resource.url, resource.urlComponents),
945             domain: WI.displayNameForHost(resource.urlComponents.host),
946             scheme: resource.urlComponents.scheme ? resource.urlComponents.scheme.toLowerCase() : "",
947             method: resource.requestMethod,
948             type: resource.type,
949             displayType: this._displayType(resource),
950             mimeType: resource.mimeType,
951             status: resource.statusCode,
952             cached: resource.cached,
953             resourceSize: resource.size,
954             transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize,
955             time: resource.duration,
956             protocol: resource.protocol,
957             priority: resource.priority,
958             remoteAddress: resource.remoteAddress,
959             connectionIdentifier: resource.connectionIdentifier,
960             startTime: resource.firstTimestamp,
961         };
962     }
963
964     _hasTypeFilter()
965     {
966         return !!this._activeTypeFilters;
967     }
968
969     _hasTextFilter()
970     {
971         return this._textFilterIsActive;
972     }
973
974     _hasActiveFilter()
975     {
976         return this._hasTypeFilter()
977             || this._hasTextFilter();
978     }
979
980     _passTypeFilter(entry)
981     {
982         if (!this._hasTypeFilter())
983             return true;
984         return this._activeTypeFilters.some((checker) => checker(entry.resource.type));
985     }
986
987     _passTextFilter(entry)
988     {
989         if (!this._hasTextFilter())
990             return true;
991         return this._activeTextFilterResources.has(entry.resource);
992     }
993
994     _passFilter(entry)
995     {
996         return this._passTypeFilter(entry)
997             && this._passTextFilter(entry);
998     }
999
1000     _updateSortAndFilteredEntries()
1001     {
1002         this._entries = this._entries.sort(this._entriesSortComparator);
1003         this._updateFilteredEntries();
1004     }
1005
1006     _updateFilteredEntries()
1007     {
1008         if (this._hasActiveFilter())
1009             this._filteredEntries = this._entries.filter(this._passFilter, this);
1010         else
1011             this._filteredEntries = this._entries.slice();
1012
1013         this._restoreSelectedRow();
1014
1015         this._updateTextFilterActiveIndicator();
1016         this._updateEmptyFilterResultsMessage();
1017     }
1018
1019     _generateTypeFilter()
1020     {
1021         let selectedItems = this._typeFilterScopeBar.selectedItems;
1022         if (!selectedItems.length || selectedItems.includes(this._typeFilterScopeBarItemAll))
1023             return null;
1024
1025         return selectedItems.map((item) => item.__checker);
1026     }
1027
1028     _resetFilters()
1029     {
1030         console.assert(this._hasActiveFilter());
1031
1032         // Clear text filter.
1033         this._textFilterSearchId++;
1034         this._textFilterNavigationItem.filterBar.indicatingProgress = false;
1035         this._textFilterSearchText = null;
1036         this._textFilterIsActive = false;
1037         this._activeTextFilterResources.clear();
1038         this._textFilterNavigationItem.filterBar.clear();
1039         console.assert(!this._hasTextFilter());
1040
1041         // Clear type filter.
1042         this._typeFilterScopeBar.resetToDefault();
1043         console.assert(!this._hasTypeFilter());
1044
1045         console.assert(!this._hasActiveFilter());
1046
1047         this._updateFilteredEntries();
1048         this._table.reloadData();
1049     }
1050
1051     _areFilterListsIdentical(listA, listB)
1052     {
1053         if (listA && listB) {
1054             if (listA.length !== listB.length)
1055                 return false;
1056
1057             for (let i = 0; i < listA.length; ++i) {
1058                 if (listA[i] !== listB[i])
1059                     return false;
1060             }
1061
1062             return true;
1063         }
1064
1065         return false;
1066     }
1067
1068     _typeFilterScopeBarSelectionChanged(event)
1069     {
1070         // FIXME: <https://webkit.org/b/176763> Web Inspector: ScopeBar SelectionChanged event may dispatch multiple times for a single logical change
1071         // We can't use shallow equals here because the contents are functions.
1072         let oldFilter = this._activeTypeFilters;
1073         let newFilter = this._generateTypeFilter();
1074         if (this._areFilterListsIdentical(oldFilter, newFilter))
1075             return;
1076
1077         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1078         this._hideResourceDetailView();
1079
1080         this._activeTypeFilters = newFilter;
1081         this._updateFilteredEntries();
1082         this._table.reloadData();
1083     }
1084
1085     _textFilterDidChange(event)
1086     {
1087         let searchQuery = this._textFilterNavigationItem.filterBar.filters.text;
1088         if (searchQuery === this._textFilterSearchText)
1089             return;
1090
1091         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1092         this._hideResourceDetailView();
1093
1094         let searchId = ++this._textFilterSearchId;
1095
1096         // Search cleared.
1097         if (!searchQuery) {
1098             this._textFilterNavigationItem.filterBar.indicatingProgress = false;
1099             this._textFilterSearchText = null;
1100             this._textFilterIsActive = false;
1101             this._activeTextFilterResources.clear();
1102
1103             this._updateFilteredEntries();
1104             this._table.reloadData();
1105             return;
1106         }
1107
1108         this._textFilterSearchText = searchQuery;
1109         this._textFilterNavigationItem.filterBar.indicatingProgress = true;
1110
1111         // NetworkTable text filter currently searches:
1112         //   - Resource URL
1113         //   - Resource Text Content
1114         // It does not search all the content in the table (like mimeType, headers, etc).
1115         // For those we should provide more custom filters.
1116
1117         const isCaseSensitive = true;
1118         const isRegex = false;
1119         PageAgent.searchInResources(searchQuery, isCaseSensitive, isRegex, (error, searchResults) => {
1120             if (searchId !== this._textFilterSearchId)
1121                 return;
1122
1123             this._textFilterIsActive = true;
1124             this._activeTextFilterResources.clear();
1125             this._textFilterNavigationItem.filterBar.indicatingProgress = false;
1126
1127             // Add resources based on URL.
1128             for (let entry of this._entries) {
1129                 let resource = entry.resource;
1130                 if (resource.url.includes(searchQuery))
1131                     this._activeTextFilterResources.add(resource);
1132             }
1133
1134             // Add resources based on content.
1135             if (!error) {
1136                 for (let {url, frameId, requestId} of searchResults) {
1137                     if (requestId) {
1138                         let resource = WI.frameResourceManager.resourceForRequestIdentifier(requestId);
1139                         if (resource) {
1140                             this._activeTextFilterResources.add(resource);
1141                             continue;
1142                         }
1143                     }
1144
1145                     if (frameId && url) {
1146                         let frame = WI.frameResourceManager.frameForIdentifier(frameId);
1147                         if (frame) {
1148                             if (frame.mainResource.url === url) {
1149                                 this._activeTextFilterResources.add(frame.mainResource);
1150                                 continue;
1151                             }
1152                             let resource = frame.resourceForURL(url);
1153                             if (resource) {
1154                                 this._activeTextFilterResources.add(resource);
1155                                 continue;
1156                             }
1157                         }
1158                     }
1159                 }
1160             }
1161
1162             // Apply.
1163             this._updateFilteredEntries();
1164             this._table.reloadData();
1165         });
1166     }
1167
1168     _restoreSelectedRow()
1169     {
1170         if (!this._selectedResource)
1171             return;
1172
1173         let rowIndex = this._rowIndexForResource(this._selectedResource);
1174         if (rowIndex === -1) {
1175             this._selectedResource = null;
1176             this._table.clearSelectedRow();
1177             return;
1178         }
1179
1180         this._table.selectRow(rowIndex);
1181     }
1182
1183     _tableNameColumnDidChangeWidth(event)
1184     {
1185         this._nameColumnWidthSetting.value = event.target.width;
1186
1187         this._positionDetailView();
1188         this._positionEmptyFilterMessage();
1189     }
1190 };