Web Inspector: Network Tab - Set column initial widths to try allow waterfall column...
[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             minWidth: WI.Sidebar.AbsoluteMinimumWidth,
481             maxWidth: 500,
482             initialWidth: this._nameColumnWidthSetting.value,
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             initialWidth: 150,
492         });
493
494         this._typeColumn = new WI.TableColumn("type", WI.UIString("Type"), {
495             minWidth: 70,
496             maxWidth: 120,
497             initialWidth: 90,
498         });
499
500         this._mimeTypeColumn = new WI.TableColumn("mimeType", WI.UIString("MIME Type"), {
501             hidden: true,
502             minWidth: 100,
503             maxWidth: 150,
504             initialWidth: 120,
505         });
506
507         this._methodColumn = new WI.TableColumn("method", WI.UIString("Method"), {
508             hidden: true,
509             minWidth: 55,
510             maxWidth: 80,
511             initialWidth: 65,
512         });
513
514         this._schemeColumn = new WI.TableColumn("scheme", WI.UIString("Scheme"), {
515             hidden: true,
516             minWidth: 55,
517             maxWidth: 80,
518             initialWidth: 65,
519         });
520
521         this._statusColumn = new WI.TableColumn("status", WI.UIString("Status"), {
522             hidden: true,
523             minWidth: 50,
524             maxWidth: 50,
525             align: "left",
526         });
527
528         this._protocolColumn = new WI.TableColumn("protocol", WI.UIString("Protocol"), {
529             hidden: true,
530             minWidth: 65,
531             maxWidth: 80,
532             initialWidth: 75,
533         });
534
535         this._priorityColumn = new WI.TableColumn("priority", WI.UIString("Priority"), {
536             hidden: true,
537             minWidth: 65,
538             maxWidth: 80,
539             initialWidth: 70,
540         });
541
542         this._remoteAddressColumn = new WI.TableColumn("remoteAddress", WI.UIString("IP Address"), {
543             hidden: true,
544             minWidth: 150,
545         });
546
547         this._connectionIdentifierColumn = new WI.TableColumn("connectionIdentifier", WI.UIString("Connection ID"), {
548             hidden: true,
549             minWidth: 50,
550             maxWidth: 120,
551             initialWidth: 80,
552             align: "right",
553         });
554
555         this._resourceSizeColumn = new WI.TableColumn("resourceSize", WI.UIString("Resource Size"), {
556             hidden: true,
557             minWidth: 80,
558             maxWidth: 100,
559             initialWidth: 80,
560             align: "right",
561         });
562
563         this._transferSizeColumn = new WI.TableColumn("transferSize", WI.UIString("Transfer Size"), {
564             minWidth: 100,
565             maxWidth: 150,
566             initialWidth: 100,
567             align: "right",
568         });
569
570         this._timeColumn = new WI.TableColumn("time", WI.UIString("Time"), {
571             minWidth: 65,
572             maxWidth: 90,
573             initialWidth: 65,
574             align: "right",
575         });
576
577         this._waterfallColumn = new WI.TableColumn("waterfall", WI.UIString("Waterfall"), {
578             minWidth: 230,
579         });
580
581         this._table = new WI.Table("network-table", this, this, 20);
582
583         this._table.addColumn(this._nameColumn);
584         this._table.addColumn(this._domainColumn);
585         this._table.addColumn(this._typeColumn);
586         this._table.addColumn(this._mimeTypeColumn);
587         this._table.addColumn(this._methodColumn);
588         this._table.addColumn(this._schemeColumn);
589         this._table.addColumn(this._statusColumn);
590         this._table.addColumn(this._protocolColumn);
591         this._table.addColumn(this._priorityColumn);
592         this._table.addColumn(this._remoteAddressColumn);
593         this._table.addColumn(this._connectionIdentifierColumn);
594         this._table.addColumn(this._resourceSizeColumn);
595         this._table.addColumn(this._transferSizeColumn);
596         this._table.addColumn(this._timeColumn);
597         this._table.addColumn(this._waterfallColumn);
598
599         if (!this._table.sortColumnIdentifier) {
600             this._table.sortOrder = WI.Table.SortOrder.Ascending;
601             this._table.sortColumnIdentifier = "waterfall";
602         }
603
604         this.addSubview(this._table);
605     }
606
607     layout()
608     {
609         this._processPendingEntries();
610         this._positionDetailView();
611         this._positionEmptyFilterMessage();
612     }
613
614     handleClearShortcut(event)
615     {
616         this.reset();
617     }
618
619     // Private
620
621     _processPendingEntries()
622     {
623         let needsSort = this._pendingUpdates.length > 0;
624         let needsFilter = this._pendingFilter;
625
626         // No global sort or filter is needed, so just insert new records into their sorted position.
627         if (!needsSort && !needsFilter) {
628             let originalLength = this._pendingInsertions.length;
629             for (let resource of this._pendingInsertions)
630                 this._insertResourceAndReloadTable(resource);
631             console.assert(this._pendingInsertions.length === originalLength);
632             this._pendingInsertions = [];
633             return;
634         }
635
636         for (let resource of this._pendingInsertions)
637             this._entries.push(this._entryForResource(resource));
638         this._pendingInsertions = [];
639
640         for (let resource of this._pendingUpdates)
641             this._updateEntryForResource(resource);
642         this._pendingUpdates = [];
643
644         this._pendingFilter = false;
645
646         this._updateSortAndFilteredEntries();
647         this._table.reloadData();
648     }
649
650     _checkTextFilterAgainstFinishedResource(resource)
651     {
652         let frame = resource.parentFrame;
653         if (!frame)
654             return;
655
656         let searchQuery = this._textFilterSearchText;
657         if (resource.url.includes(searchQuery)) {
658             this._activeTextFilterResources.add(resource);
659             return;
660         }
661
662         let searchId = this._textFilterSearchId;
663
664         const isCaseSensitive = true;
665         const isRegex = false;
666         PageAgent.searchInResource(frame.id, resource.url, searchQuery, isCaseSensitive, isRegex, resource.requestIdentifier, (error, searchResults) => {
667             if (searchId !== this._textFilterSearchId)
668                 return;
669
670             if (error || !searchResults || !searchResults.length)
671                 return;
672
673             this._activeTextFilterResources.add(resource);
674
675             this._pendingFilter = true;
676             this.needsLayout();
677         });
678     }
679
680     _checkTextFilterAgainstFailedResource(resource)
681     {
682         let searchQuery = this._textFilterSearchText;
683         if (resource.url.includes(searchQuery))
684             this._activeTextFilterResources.add(resource);
685     }
686
687     _rowIndexForResource(resource)
688     {
689         return this._filteredEntries.findIndex((x) => x.resource === resource);
690     }
691
692     _updateEntryForResource(resource)
693     {
694         let index = this._entries.findIndex((x) => x.resource === resource);
695         if (index === -1)
696             return;
697
698         let entry = this._entryForResource(resource);
699         this._entries[index] = entry;
700
701         let rowIndex = this._rowIndexForResource(resource);
702         if (rowIndex === -1)
703             return;
704
705         this._filteredEntries[rowIndex] = entry;
706     }
707
708     _hideResourceDetailView()
709     {
710         if (!this._resourceDetailView)
711             return;
712
713         this.element.classList.remove("showing-detail");
714         this._table.scrollContainer.style.removeProperty("width");
715
716         this.removeSubview(this._resourceDetailView);
717
718         this._resourceDetailView.hidden();
719         this._resourceDetailView = null;
720
721         this._table.resize();
722     }
723
724     _showResourceDetailView(resource)
725     {
726         let oldResourceDetailView = this._resourceDetailView;
727
728         this._resourceDetailView = this._resourceDetailViewMap.get(resource);
729         if (!this._resourceDetailView) {
730             this._resourceDetailView = new WI.NetworkResourceDetailView(resource, this);
731             this._resourceDetailViewMap.set(resource, this._resourceDetailView);
732         }
733
734         if (oldResourceDetailView) {
735             oldResourceDetailView.hidden();
736             this.replaceSubview(oldResourceDetailView, this._resourceDetailView);
737         } else
738             this.addSubview(this._resourceDetailView);
739         this._resourceDetailView.shown();
740
741         this.element.classList.add("showing-detail");
742         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
743
744         // FIXME: It would be nice to avoid this.
745         // Currently the ResourceDetailView is in the heirarchy but has not yet done a layout so we
746         // end up seeing the table behind it. This forces us to layout now instead of after a beat.
747         this.updateLayout();
748     }
749
750     _positionDetailView()
751     {
752         if (!this._resourceDetailView)
753             return;
754
755         let side = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
756         this._resourceDetailView.element.style[side] = this._nameColumn.width + "px";
757         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
758     }
759
760     _updateTextFilterActiveIndicator()
761     {
762         this._textFilterNavigationItem.filterBar.indicatingActive = this._hasTextFilter();
763     }
764
765     _updateEmptyFilterResultsMessage()
766     {
767         if (this._hasActiveFilter() && !this._filteredEntries.length)
768             this._showEmptyFilterResultsMessage();
769         else
770             this._hideEmptyFilterResultsMessage();
771     }
772
773     _showEmptyFilterResultsMessage()
774     {
775         if (!this._emptyFilterResultsMessageElement) {
776             let message = WI.UIString("No Filter Results");
777             let buttonElement = document.createElement("button");
778             buttonElement.textContent = WI.UIString("Clear filters");
779             buttonElement.addEventListener("click", () => { this._resetFilters(); });
780
781             this._emptyFilterResultsMessageElement = document.createElement("div");
782             this._emptyFilterResultsMessageElement.className = "empty-content-placeholder";
783
784             let messageElement = this._emptyFilterResultsMessageElement.appendChild(document.createElement("div"));
785             messageElement.className = "message";
786             messageElement.append(message, document.createElement("br"), buttonElement);
787         }
788
789         this.element.appendChild(this._emptyFilterResultsMessageElement);
790         this._positionEmptyFilterMessage();
791     }
792
793     _hideEmptyFilterResultsMessage()
794     {
795         if (!this._emptyFilterResultsMessageElement)
796             return;
797
798         this._emptyFilterResultsMessageElement.remove();
799     }
800
801     _positionEmptyFilterMessage()
802     {
803         if (!this._emptyFilterResultsMessageElement)
804             return;
805
806         let width = this._nameColumn.width - 1; // For the 1px border.
807         this._emptyFilterResultsMessageElement.style.width = width + "px";
808     }
809
810     _resourceCachingDisabledSettingChanged()
811     {
812         this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
813     }
814
815     _toggleDisableResourceCache()
816     {
817         WI.resourceCachingDisabledSetting.value = !WI.resourceCachingDisabledSetting.value;
818     }
819
820     _mainResourceDidChange(event)
821     {
822         let frame = event.target;
823         if (!frame.isMainFrame() || !WI.settings.clearNetworkOnNavigate.value)
824             return;
825
826         this.reset();
827
828         this._insertResourceAndReloadTable(frame.mainResource);
829     }
830
831     _resourceLoadingDidFinish(event)
832     {
833         let resource = event.target;
834         this._pendingUpdates.push(resource);
835
836         if (this._hasTextFilter())
837             this._checkTextFilterAgainstFinishedResource(resource);
838
839         this.needsLayout();
840     }
841
842     _resourceLoadingDidFail(event)
843     {
844         let resource = event.target;
845         this._pendingUpdates.push(resource);
846
847         if (this._hasTextFilter())
848             this._checkTextFilterAgainstFailedResource(resource);
849
850         this.needsLayout();
851     }
852
853     _resourceTransferSizeDidChange(event)
854     {
855         if (!this._table)
856             return;
857
858         let resource = event.target;
859
860         // In the unlikely event that this is the sort column, we may need to resort.
861         if (this._table.sortColumnIdentifier === "transferSize") {
862             this._pendingUpdates.push(resource);
863             this.needsLayout();
864             return;
865         }
866
867         let index = this._entries.findIndex((x) => x.resource === resource);
868         if (index === -1)
869             return;
870
871         let entry = this._entries[index];
872         entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
873
874         let rowIndex = this._rowIndexForResource(resource);
875         if (rowIndex === -1)
876             return;
877
878         this._table.reloadCell(rowIndex, "transferSize");
879     }
880
881     _networkTimelineRecordAdded(event)
882     {
883         let resourceTimelineRecord = event.data.record;
884         console.assert(resourceTimelineRecord instanceof WI.ResourceTimelineRecord);
885
886         let resource = resourceTimelineRecord.resource;
887         this._insertResourceAndReloadTable(resource);
888     }
889
890     _isDefaultSort()
891     {
892         return this._table.sortColumnIdentifier === "waterfall" && this._table.sortOrder === WI.Table.SortOrder.Ascending;
893     }
894
895     _insertResourceAndReloadTable(resource)
896     {
897         if (!(WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView)) {
898             this._pendingInsertions.push(resource);
899             this.needsLayout();
900             return;
901         }
902
903         console.assert(this._table);
904         if (!this._table)
905             return;
906
907         let entry = this._entryForResource(resource);
908
909         // Default sort has fast path.
910         if (this._isDefaultSort() || !this._entriesSortComparator) {
911             this._entries.push(entry);
912             if (this._passFilter(entry)) {
913                 this._filteredEntries.push(entry);
914                 this._table.reloadDataAddedToEndOnly();
915             }
916             return;
917         }
918
919         insertObjectIntoSortedArray(entry, this._entries, this._entriesSortComparator);
920
921         if (this._passFilter(entry)) {
922             insertObjectIntoSortedArray(entry, this._filteredEntries, this._entriesSortComparator);
923
924             // Probably a useless optimization here, but if we only added this row to the end
925             // we may avoid recreating all visible rows by saying as such.
926             if (this._filteredEntries.lastValue === entry)
927                 this._table.reloadDataAddedToEndOnly();
928             else
929                 this._table.reloadData();
930         }
931     }
932
933     _displayType(resource)
934     {
935         if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font) {
936             let fileExtension;
937             if (resource.mimeType)
938                 fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
939             if (!fileExtension)
940                 fileExtension = WI.fileExtensionForURL(resource.url);
941             if (fileExtension)
942                 return fileExtension;
943         }
944
945         return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase();
946     }
947
948     _entryForResource(resource)
949     {
950         // FIXME: <https://webkit.org/b/143632> Web Inspector: Resources with the same name in different folders aren't distinguished
951         // FIXME: <https://webkit.org/b/176765> Web Inspector: Resource names should be less ambiguous
952
953         return {
954             resource,
955             name: WI.displayNameForURL(resource.url, resource.urlComponents),
956             domain: WI.displayNameForHost(resource.urlComponents.host),
957             scheme: resource.urlComponents.scheme ? resource.urlComponents.scheme.toLowerCase() : "",
958             method: resource.requestMethod,
959             type: resource.type,
960             displayType: this._displayType(resource),
961             mimeType: resource.mimeType,
962             status: resource.statusCode,
963             cached: resource.cached,
964             resourceSize: resource.size,
965             transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize,
966             time: resource.duration,
967             protocol: resource.protocol,
968             priority: resource.priority,
969             remoteAddress: resource.remoteAddress,
970             connectionIdentifier: resource.connectionIdentifier,
971             startTime: resource.firstTimestamp,
972         };
973     }
974
975     _hasTypeFilter()
976     {
977         return !!this._activeTypeFilters;
978     }
979
980     _hasTextFilter()
981     {
982         return this._textFilterIsActive;
983     }
984
985     _hasActiveFilter()
986     {
987         return this._hasTypeFilter()
988             || this._hasTextFilter();
989     }
990
991     _passTypeFilter(entry)
992     {
993         if (!this._hasTypeFilter())
994             return true;
995         return this._activeTypeFilters.some((checker) => checker(entry.resource.type));
996     }
997
998     _passTextFilter(entry)
999     {
1000         if (!this._hasTextFilter())
1001             return true;
1002         return this._activeTextFilterResources.has(entry.resource);
1003     }
1004
1005     _passFilter(entry)
1006     {
1007         return this._passTypeFilter(entry)
1008             && this._passTextFilter(entry);
1009     }
1010
1011     _updateSortAndFilteredEntries()
1012     {
1013         this._entries = this._entries.sort(this._entriesSortComparator);
1014         this._updateFilteredEntries();
1015     }
1016
1017     _updateFilteredEntries()
1018     {
1019         if (this._hasActiveFilter())
1020             this._filteredEntries = this._entries.filter(this._passFilter, this);
1021         else
1022             this._filteredEntries = this._entries.slice();
1023
1024         this._restoreSelectedRow();
1025
1026         this._updateTextFilterActiveIndicator();
1027         this._updateEmptyFilterResultsMessage();
1028     }
1029
1030     _generateTypeFilter()
1031     {
1032         let selectedItems = this._typeFilterScopeBar.selectedItems;
1033         if (!selectedItems.length || selectedItems.includes(this._typeFilterScopeBarItemAll))
1034             return null;
1035
1036         return selectedItems.map((item) => item.__checker);
1037     }
1038
1039     _resetFilters()
1040     {
1041         console.assert(this._hasActiveFilter());
1042
1043         // Clear text filter.
1044         this._textFilterSearchId++;
1045         this._textFilterNavigationItem.filterBar.indicatingProgress = false;
1046         this._textFilterSearchText = null;
1047         this._textFilterIsActive = false;
1048         this._activeTextFilterResources.clear();
1049         this._textFilterNavigationItem.filterBar.clear();
1050         console.assert(!this._hasTextFilter());
1051
1052         // Clear type filter.
1053         this._typeFilterScopeBar.resetToDefault();
1054         console.assert(!this._hasTypeFilter());
1055
1056         console.assert(!this._hasActiveFilter());
1057
1058         this._updateFilteredEntries();
1059         this._table.reloadData();
1060     }
1061
1062     _areFilterListsIdentical(listA, listB)
1063     {
1064         if (listA && listB) {
1065             if (listA.length !== listB.length)
1066                 return false;
1067
1068             for (let i = 0; i < listA.length; ++i) {
1069                 if (listA[i] !== listB[i])
1070                     return false;
1071             }
1072
1073             return true;
1074         }
1075
1076         return false;
1077     }
1078
1079     _typeFilterScopeBarSelectionChanged(event)
1080     {
1081         // FIXME: <https://webkit.org/b/176763> Web Inspector: ScopeBar SelectionChanged event may dispatch multiple times for a single logical change
1082         // We can't use shallow equals here because the contents are functions.
1083         let oldFilter = this._activeTypeFilters;
1084         let newFilter = this._generateTypeFilter();
1085         if (this._areFilterListsIdentical(oldFilter, newFilter))
1086             return;
1087
1088         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1089         this._hideResourceDetailView();
1090
1091         this._activeTypeFilters = newFilter;
1092         this._updateFilteredEntries();
1093         this._table.reloadData();
1094     }
1095
1096     _textFilterDidChange(event)
1097     {
1098         let searchQuery = this._textFilterNavigationItem.filterBar.filters.text;
1099         if (searchQuery === this._textFilterSearchText)
1100             return;
1101
1102         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1103         this._hideResourceDetailView();
1104
1105         let searchId = ++this._textFilterSearchId;
1106
1107         // Search cleared.
1108         if (!searchQuery) {
1109             this._textFilterNavigationItem.filterBar.indicatingProgress = false;
1110             this._textFilterSearchText = null;
1111             this._textFilterIsActive = false;
1112             this._activeTextFilterResources.clear();
1113
1114             this._updateFilteredEntries();
1115             this._table.reloadData();
1116             return;
1117         }
1118
1119         this._textFilterSearchText = searchQuery;
1120         this._textFilterNavigationItem.filterBar.indicatingProgress = true;
1121
1122         // NetworkTable text filter currently searches:
1123         //   - Resource URL
1124         //   - Resource Text Content
1125         // It does not search all the content in the table (like mimeType, headers, etc).
1126         // For those we should provide more custom filters.
1127
1128         const isCaseSensitive = true;
1129         const isRegex = false;
1130         PageAgent.searchInResources(searchQuery, isCaseSensitive, isRegex, (error, searchResults) => {
1131             if (searchId !== this._textFilterSearchId)
1132                 return;
1133
1134             this._textFilterIsActive = true;
1135             this._activeTextFilterResources.clear();
1136             this._textFilterNavigationItem.filterBar.indicatingProgress = false;
1137
1138             // Add resources based on URL.
1139             for (let entry of this._entries) {
1140                 let resource = entry.resource;
1141                 if (resource.url.includes(searchQuery))
1142                     this._activeTextFilterResources.add(resource);
1143             }
1144
1145             // Add resources based on content.
1146             if (!error) {
1147                 for (let {url, frameId, requestId} of searchResults) {
1148                     if (requestId) {
1149                         let resource = WI.frameResourceManager.resourceForRequestIdentifier(requestId);
1150                         if (resource) {
1151                             this._activeTextFilterResources.add(resource);
1152                             continue;
1153                         }
1154                     }
1155
1156                     if (frameId && url) {
1157                         let frame = WI.frameResourceManager.frameForIdentifier(frameId);
1158                         if (frame) {
1159                             if (frame.mainResource.url === url) {
1160                                 this._activeTextFilterResources.add(frame.mainResource);
1161                                 continue;
1162                             }
1163                             let resource = frame.resourceForURL(url);
1164                             if (resource) {
1165                                 this._activeTextFilterResources.add(resource);
1166                                 continue;
1167                             }
1168                         }
1169                     }
1170                 }
1171             }
1172
1173             // Apply.
1174             this._updateFilteredEntries();
1175             this._table.reloadData();
1176         });
1177     }
1178
1179     _restoreSelectedRow()
1180     {
1181         if (!this._selectedResource)
1182             return;
1183
1184         let rowIndex = this._rowIndexForResource(this._selectedResource);
1185         if (rowIndex === -1) {
1186             this._selectedResource = null;
1187             this._table.clearSelectedRow();
1188             return;
1189         }
1190
1191         this._table.selectRow(rowIndex);
1192     }
1193
1194     _tableNameColumnDidChangeWidth(event)
1195     {
1196         this._nameColumnWidthSetting.value = event.target.width;
1197
1198         this._positionDetailView();
1199         this._positionEmptyFilterMessage();
1200     }
1201 };