Web Inspector: Network Tab - Make selection in the table more reliable (mousedown...
[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
38         this._table = null;
39         this._nameColumnWidthSetting = new WI.Setting("network-table-content-view-name-column-width", 250);
40
41         this._selectedResource = null;
42         this._resourceDetailView = null;
43         this._resourceDetailViewMap = new Map;
44
45         // FIXME: Network Timeline.
46         // FIXME: Filter text field.
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._activeTypeFilters = this._generateTypeFilter();
74
75         // COMPATIBILITY (iOS 10.3): Network.setDisableResourceCaching did not exist.
76         if (window.NetworkAgent && NetworkAgent.setResourceCachingDisabled) {
77             let toolTipForDisableResourceCache = WI.UIString("Ignore the resource cache when loading resources");
78             let activatedToolTipForDisableResourceCache = WI.UIString("Use the resource cache when loading resources");
79             this._disableResourceCacheNavigationItem = new WI.ActivateButtonNavigationItem("disable-resource-cache", toolTipForDisableResourceCache, activatedToolTipForDisableResourceCache, "Images/IgnoreCaches.svg", 16, 16);
80             this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
81
82             this._disableResourceCacheNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleDisableResourceCache, this);
83             WI.resourceCachingDisabledSetting.addEventListener(WI.Setting.Event.Changed, this._resourceCachingDisabledSettingChanged, this);
84         }
85
86         this._clearNetworkItemsNavigationItem = new WI.ButtonNavigationItem("clear-network-items", WI.UIString("Clear Network Items (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemClear.svg", 16, 16);
87         this._clearNetworkItemsNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => this.reset());
88
89         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
90         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this);
91         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._resourceLoadingDidFail, this);
92         WI.Resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._resourceTransferSizeDidChange, this);
93         WI.timelineManager.persistentNetworkTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
94     }
95
96     // Static
97
98     static shortDisplayNameForResourceType(type)
99     {
100         switch (type) {
101         case WI.Resource.Type.Document:
102             return WI.UIString("Document");
103         case WI.Resource.Type.Stylesheet:
104             return "CSS";
105         case WI.Resource.Type.Image:
106             return WI.UIString("Image");
107         case WI.Resource.Type.Font:
108             return WI.UIString("Font");
109         case WI.Resource.Type.Script:
110             return "JS";
111         case WI.Resource.Type.XHR:
112         case WI.Resource.Type.Fetch:
113             return "XHR";
114         case WI.Resource.Type.Ping:
115             return WI.UIString("Ping");
116         case WI.Resource.Type.Beacon:
117             return WI.UIString("Beacon");
118         case WI.Resource.Type.WebSocket:
119         case WI.Resource.Type.Other:
120             return WI.UIString("Other");
121         default:
122             console.error("Unknown resource type", type);
123             return null;
124         }
125     }
126
127     // Public
128
129     get selectionPathComponents()
130     {
131         return null;
132     }
133
134     get navigationItems()
135     {
136         let items = [this._typeFilterScopeBar];
137
138         if (this._disableResourceCacheNavigationItem)
139             items.push(this._disableResourceCacheNavigationItem);
140         items.push(this._clearNetworkItemsNavigationItem);
141
142         return items;
143     }
144
145     shown()
146     {
147         super.shown();
148
149         if (this._resourceDetailView)
150             this._resourceDetailView.shown();
151
152         if (this._table)
153             this._table.restoreScrollPosition();
154     }
155
156     hidden()
157     {
158         if (this._resourceDetailView)
159             this._resourceDetailView.hidden();
160
161         super.hidden();
162     }
163
164     closed()
165     {
166         this._hideResourceDetailView();
167
168         for (let detailView of this._resourceDetailViewMap.values())
169             detailView.dispose();
170         this._resourceDetailViewMap.clear();
171
172         WI.Frame.removeEventListener(null, null, this);
173         WI.Resource.removeEventListener(null, null, this);
174         WI.timelineManager.persistentNetworkTimeline.removeEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
175
176         super.closed();
177     }
178
179     reset()
180     {
181         this._entries = [];
182         this._filteredEntries = [];
183         this._pendingInsertions = [];
184
185         for (let detailView of this._resourceDetailViewMap.values())
186             detailView.dispose();
187         this._resourceDetailViewMap.clear();
188
189         if (this._table) {
190             this._hideResourceDetailView();
191             this._selectedResource = null;
192             this._table.clearSelectedRow();
193             this._table.reloadData();
194         }
195     }
196
197     // NetworkResourceDetailView delegate
198
199     networkResourceDetailViewClose(resourceDetailView)
200     {
201         this._hideResourceDetailView();
202         this._selectedResource = null;
203         this._table.clearSelectedRow();
204     }
205
206     // Table dataSource
207
208     tableNumberOfRows(table)
209     {
210         return this._filteredEntries.length;
211     }
212
213     tableSortChanged(table)
214     {
215         this._generateSortComparator();
216
217         if (!this._entriesSortComparator)
218             return;
219
220         this._hideResourceDetailView();
221
222         this._entries = this._entries.sort(this._entriesSortComparator);
223         this._updateFilteredEntries();
224         this._table.reloadData();
225     }
226
227     // Table delegate
228
229     tableCellMouseDown(table, cell, column, rowIndex, event)
230     {
231         if (column !== this._nameColumn)
232             return;
233
234         this._table.selectRow(rowIndex);
235     }
236
237     tableCellContextMenuClicked(table, cell, column, rowIndex, event)
238     {
239         if (column !== this._nameColumn)
240             return;
241
242         this._table.selectRow(rowIndex);
243
244         let entry = this._filteredEntries[rowIndex];
245         let contextMenu = WI.ContextMenu.createFromEvent(event);
246         WI.appendContextMenuItemsForSourceCode(contextMenu, entry.resource);
247     }
248
249     tableSelectedRowChanged(table, rowIndex)
250     {
251         if (isNaN(rowIndex)) {
252             this._selectedResource = null;
253             this._hideResourceDetailView();
254             return;
255         }
256
257         let entry = this._filteredEntries[rowIndex];
258         if (entry.resource === this._selectedResource)
259             return;
260
261         this._selectedResource = entry.resource;
262         this._showResourceDetailView(this._selectedResource);
263     }
264
265     tablePopulateCell(table, cell, column, rowIndex)
266     {
267         let entry = this._filteredEntries[rowIndex];
268
269         cell.classList.toggle("error", entry.resource.hadLoadingError());
270
271         switch (column.identifier) {
272         case "name":
273             this._populateNameCell(cell, entry);
274             break;
275         case "domain":
276             cell.textContent = entry.domain || emDash;
277             break;
278         case "type":
279             cell.textContent = entry.displayType || emDash;
280             break;
281         case "mimeType":
282             cell.textContent = entry.mimeType || emDash;
283             break;
284         case "method":
285             cell.textContent = entry.method || emDash;
286             break;
287         case "scheme":
288             cell.textContent = entry.scheme || emDash;
289             break;
290         case "status":
291             cell.textContent = entry.status || emDash;
292             break;
293         case "protocol":
294             cell.textContent = entry.protocol || emDash;
295             break;
296         case "priority":
297             cell.textContent = WI.Resource.displayNameForPriority(entry.priority) || emDash;
298             break;
299         case "remoteAddress":
300             cell.textContent = entry.remoteAddress || emDash;
301             break;
302         case "connectionIdentifier":
303             cell.textContent = entry.connectionIdentifier || emDash;
304             break;
305         case "resourceSize":
306             cell.textContent = isNaN(entry.resourceSize) ? emDash : Number.bytesToString(entry.resourceSize);
307             break;
308         case "transferSize":
309             this._populateTransferSizeCell(cell, entry);
310             break;
311         case "time":
312             // FIXME: <https://webkit.org/b/176748> Web Inspector: Frontend sometimes receives resources with negative duration (responseEnd - requestStart)
313             cell.textContent = isNaN(entry.time) ? emDash : Number.secondsToString(Math.max(entry.time, 0));
314             break;
315         case "waterfall":
316             // FIXME: Waterfall graph.
317             cell.textContent = emDash;
318             break;
319         }
320
321         return cell;
322     }
323
324     // Private
325
326     _populateNameCell(cell, entry)
327     {
328         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
329
330         let resource = entry.resource;
331         if (resource.isLoading()) {
332             let statusElement = cell.appendChild(document.createElement("div"));
333             statusElement.className = "status";
334             let spinner = new WI.IndeterminateProgressSpinner;
335             statusElement.appendChild(spinner.element);
336         }
337
338         let iconElement = cell.appendChild(document.createElement("img"));
339         iconElement.className = "icon";
340         cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName, entry.resource.type);
341
342         let nameElement = cell.appendChild(document.createElement("span"));
343         nameElement.textContent = entry.name;
344     }
345
346     _populateTransferSizeCell(cell, entry)
347     {
348         let responseSource = entry.resource.responseSource;
349         if (responseSource === WI.Resource.ResponseSource.MemoryCache) {
350             cell.classList.add("cache-type");
351             cell.textContent = WI.UIString("(memory)");
352             return;
353         }
354         if (responseSource === WI.Resource.ResponseSource.DiskCache) {
355             cell.classList.add("cache-type");
356             cell.textContent = WI.UIString("(disk)");
357             return;
358         }
359
360         let transferSize = entry.transferSize;
361         cell.textContent = isNaN(transferSize) ? emDash : Number.bytesToString(transferSize);
362         console.assert(!cell.classList.contains("cache-type"), "Should not have cache-type class on cell.");
363     }
364
365     _generateSortComparator()
366     {
367         let sortColumnIdentifier = this._table.sortColumnIdentifier;
368         if (!sortColumnIdentifier) {
369             this._entriesSortComparator = null;
370             return;
371         }
372
373         let comparator;
374
375         switch (sortColumnIdentifier) {
376         case "name":
377         case "domain":
378         case "mimeType":
379         case "method":
380         case "scheme":
381         case "protocol":
382         case "remoteAddress":
383             // Simple string.
384             comparator = (a, b) => (a[sortColumnIdentifier] || "").extendedLocaleCompare(b[sortColumnIdentifier] || "");
385             break;
386
387         case "status":
388         case "connectionIdentifier":
389         case "resourceSize":
390         case "time":
391             // Simple number.
392             comparator = (a, b) => {
393                 let aValue = a[sortColumnIdentifier];
394                 if (isNaN(aValue))
395                     return 1;
396                 let bValue = b[sortColumnIdentifier];
397                 if (isNaN(bValue))
398                     return -1;
399                 return aValue - bValue;
400             }
401             break;
402
403         case "priority":
404             // Resource.NetworkPriority enum.
405             comparator = (a, b) => WI.Resource.comparePriority(a.priority, b.priority);
406             break;
407
408         case "type":
409             // Sort by displayType string.
410             comparator = (a, b) => (a.displayType || "").extendedLocaleCompare(b.displayType || "");
411             break;
412
413         case "transferSize":
414             // Handle (memory) and (disk) values.
415             comparator = (a, b) => {
416                 let transferSizeA = a.transferSize;
417                 let transferSizeB = b.transferSize;
418
419                 // Treat NaN as the largest value.
420                 if (isNaN(transferSizeA))
421                     return 1;
422                 if (isNaN(transferSizeB))
423                     return -1;
424
425                 // Treat memory cache and disk cache as small values.
426                 let sourceA = a.resource.responseSource;
427                 if (sourceA === WI.Resource.ResponseSource.MemoryCache)
428                     transferSizeA = -20;
429                 else if (sourceA === WI.Resource.ResponseSource.DiskCache)
430                     transferSizeA = -10;
431
432                 let sourceB = b.resource.responseSource;
433                 if (sourceB === WI.Resource.ResponseSource.MemoryCache)
434                     transferSizeB = -20;
435                 else if (sourceB === WI.Resource.ResponseSource.DiskCache)
436                     transferSizeB = -10;
437
438                 return transferSizeA - transferSizeB;
439             };
440             break;
441
442         case "waterfall":
443             // Sort by startTime number.
444             comparator = comparator = (a, b) => a.startTime - b.startTime;
445             break;
446
447         default:
448             console.assert("Unexpected sort column", sortColumnIdentifier);
449             return;
450         }
451
452         let reverseFactor = this._table.sortOrder === WI.Table.SortOrder.Ascending ? 1 : -1;
453         this._entriesSortComparator = (a, b) => reverseFactor * comparator(a, b);
454     }
455
456     // Protected
457
458     initialLayout()
459     {
460         this._nameColumn = new WI.TableColumn("name", WI.UIString("Name"), {
461             initialWidth: this._nameColumnWidthSetting.value,
462             minWidth: WI.Sidebar.AbsoluteMinimumWidth,
463             maxWidth: 500,
464             resizeType: WI.TableColumn.ResizeType.Locked,
465         });
466
467         this._nameColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableNameColumnDidChangeWidth, this);
468
469         this._domainColumn = new WI.TableColumn("domain", WI.UIString("Domain"), {
470             minWidth: 120,
471             maxWidth: 200,
472         });
473
474         this._typeColumn = new WI.TableColumn("type", WI.UIString("Type"), {
475             minWidth: 70,
476             maxWidth: 120,
477         });
478
479         this._mimeTypeColumn = new WI.TableColumn("mimeType", WI.UIString("MIME Type"), {
480             hidden: true,
481             minWidth: 100,
482             maxWidth: 150,
483         });
484
485         this._methodColumn = new WI.TableColumn("method", WI.UIString("Method"), {
486             hidden: true,
487             minWidth: 55,
488             maxWidth: 80,
489         });
490
491         this._schemeColumn = new WI.TableColumn("scheme", WI.UIString("Scheme"), {
492             hidden: true,
493             minWidth: 55,
494             maxWidth: 80,
495         });
496
497         this._statusColumn = new WI.TableColumn("status", WI.UIString("Status"), {
498             hidden: true,
499             minWidth: 50,
500             maxWidth: 50,
501             align: "left",
502         });
503
504         this._protocolColumn = new WI.TableColumn("protocol", WI.UIString("Protocol"), {
505             hidden: true,
506             minWidth: 65,
507             maxWidth: 80,
508         });
509
510         this._priorityColumn = new WI.TableColumn("priority", WI.UIString("Priority"), {
511             hidden: true,
512             minWidth: 65,
513             maxWidth: 80,
514         });
515
516         this._remoteAddressColumn = new WI.TableColumn("remoteAddress", WI.UIString("IP Address"), {
517             hidden: true,
518             minWidth: 150,
519         });
520
521         this._connectionIdentifierColumn = new WI.TableColumn("connectionIdentifier", WI.UIString("Connection ID"), {
522             hidden: true,
523             minWidth: 50,
524             maxWidth: 120,
525             align: "right",
526         });
527
528         this._resourceSizeColumn = new WI.TableColumn("resourceSize", WI.UIString("Resource Size"), {
529             hidden: true,
530             minWidth: 80,
531             maxWidth: 100,
532             align: "right",
533         });
534
535         this._transferSizeColumn = new WI.TableColumn("transferSize", WI.UIString("Transfer Size"), {
536             minWidth: 100,
537             maxWidth: 150,
538             align: "right",
539         });
540
541         this._timeColumn = new WI.TableColumn("time", WI.UIString("Time"), {
542             minWidth: 65,
543             maxWidth: 90,
544             align: "right",
545         });
546
547         this._waterfallColumn = new WI.TableColumn("waterfall", WI.UIString("Waterfall"), {
548             minWidth: 230,
549         });
550
551         this._table = new WI.Table("network-table", this, this, 20);
552
553         this._table.addColumn(this._nameColumn);
554         this._table.addColumn(this._domainColumn);
555         this._table.addColumn(this._typeColumn);
556         this._table.addColumn(this._mimeTypeColumn);
557         this._table.addColumn(this._methodColumn);
558         this._table.addColumn(this._schemeColumn);
559         this._table.addColumn(this._statusColumn);
560         this._table.addColumn(this._protocolColumn);
561         this._table.addColumn(this._priorityColumn);
562         this._table.addColumn(this._remoteAddressColumn);
563         this._table.addColumn(this._connectionIdentifierColumn);
564         this._table.addColumn(this._resourceSizeColumn);
565         this._table.addColumn(this._transferSizeColumn);
566         this._table.addColumn(this._timeColumn);
567         this._table.addColumn(this._waterfallColumn);
568
569         if (!this._table.sortColumnIdentifier) {
570             this._table.sortOrder = WI.Table.SortOrder.Ascending;
571             this._table.sortColumnIdentifier = "waterfall";
572         }
573
574         this.addSubview(this._table);
575     }
576
577     layout()
578     {
579         this._processPendingEntries();
580         this._positionDetailView();
581     }
582
583     handleClearShortcut(event)
584     {
585         this.reset();
586     }
587
588     // Private
589
590     _processPendingEntries()
591     {
592         let needsSort = this._pendingUpdates.length > 0;
593
594         // No global sort is needed, so just insert new records into their sorted position.
595         if (!needsSort) {
596             let originalLength = this._pendingInsertions.length;
597             for (let resource of this._pendingInsertions)
598                 this._insertResourceAndReloadTable(resource);
599             console.assert(this._pendingInsertions.length === originalLength);
600             this._pendingInsertions = [];
601             return;
602         }
603
604         for (let resource of this._pendingInsertions)
605             this._entries.push(this._entryForResource(resource));
606         this._pendingInsertions = [];
607
608         for (let resource of this._pendingUpdates)
609             this._updateEntryForResource(resource);
610         this._pendingUpdates = [];
611
612         this._updateSortAndFilteredEntries();
613         this._table.reloadData();
614     }
615
616     _rowIndexForResource(resource)
617     {
618         return this._filteredEntries.findIndex((x) => x.resource === resource);
619     }
620
621     _updateEntryForResource(resource)
622     {
623         let index = this._entries.findIndex((x) => x.resource === resource);
624         if (index === -1)
625             return;
626
627         let entry = this._entryForResource(resource);
628         this._entries[index] = entry;
629
630         let rowIndex = this._rowIndexForResource(resource);
631         if (rowIndex === -1)
632             return;
633
634         this._filteredEntries[rowIndex] = entry;
635     }
636
637     _hideResourceDetailView()
638     {
639         if (!this._resourceDetailView)
640             return;
641
642         this.element.classList.remove("showing-detail");
643         this._table.scrollContainer.style.removeProperty("width");
644
645         this.removeSubview(this._resourceDetailView);
646
647         this._resourceDetailView.hidden();
648         this._resourceDetailView = null;
649
650         this._table.resize();
651     }
652
653     _showResourceDetailView(resource)
654     {
655         let oldResourceDetailView = this._resourceDetailView;
656
657         this._resourceDetailView = this._resourceDetailViewMap.get(resource);
658         if (!this._resourceDetailView) {
659             this._resourceDetailView = new WI.NetworkResourceDetailView(resource, this);
660             this._resourceDetailViewMap.set(resource, this._resourceDetailView);
661         }
662
663         if (oldResourceDetailView) {
664             oldResourceDetailView.hidden();
665             this.replaceSubview(oldResourceDetailView, this._resourceDetailView);
666         } else
667             this.addSubview(this._resourceDetailView);
668         this._resourceDetailView.shown();
669
670         this.element.classList.add("showing-detail");
671         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
672
673         // FIXME: It would be nice to avoid this.
674         // Currently the ResourceDetailView is in the heirarchy but has not yet done a layout so we
675         // end up seeing the table behind it. This forces us to layout now instead of after a beat.
676         this.updateLayout();
677     }
678
679     _positionDetailView()
680     {
681         if (!this._resourceDetailView)
682             return;
683
684         let side = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
685         this._resourceDetailView.element.style[side] = this._nameColumn.width + "px";
686         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
687     }
688
689     _resourceCachingDisabledSettingChanged()
690     {
691         this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
692     }
693
694     _toggleDisableResourceCache()
695     {
696         WI.resourceCachingDisabledSetting.value = !WI.resourceCachingDisabledSetting.value;
697     }
698
699     _mainResourceDidChange(event)
700     {
701         let frame = event.target;
702         if (!frame.isMainFrame() || !WI.settings.clearNetworkOnNavigate.value)
703             return;
704
705         this.reset();
706
707         this._insertResourceAndReloadTable(frame.mainResource);
708     }
709
710     _resourceLoadingDidFinish(event)
711     {
712         let resource = event.target;
713         this._pendingUpdates.push(resource);
714         this.needsLayout();
715     }
716
717     _resourceLoadingDidFail(event)
718     {
719         let resource = event.target;
720         this._pendingUpdates.push(resource);
721         this.needsLayout();
722     }
723
724     _resourceTransferSizeDidChange(event)
725     {
726         if (!this._table)
727             return;
728
729         let resource = event.target;
730
731         // In the unlikely event that this is the sort column, we may need to resort.
732         if (this._table.sortColumnIdentifier === "transferSize") {
733             this._pendingUpdates.push(resource);
734             this.needsLayout();
735             return;
736         }
737
738         let index = this._entries.findIndex((x) => x.resource === resource);
739         if (index === -1)
740             return;
741
742         let entry = this._entries[index];
743         entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
744
745         let rowIndex = this._rowIndexForResource(resource);
746         if (rowIndex === -1)
747             return;
748
749         this._table.reloadCell(rowIndex, "transferSize");
750     }
751
752     _networkTimelineRecordAdded(event)
753     {
754         let resourceTimelineRecord = event.data.record;
755         console.assert(resourceTimelineRecord instanceof WI.ResourceTimelineRecord);
756
757         let resource = resourceTimelineRecord.resource;
758         this._insertResourceAndReloadTable(resource)
759     }
760
761     _isDefaultSort()
762     {
763         return this._table.sortColumnIdentifier === "waterfall" && this._table.sortOrder === WI.Table.SortOrder.Ascending;
764     }
765
766     _insertResourceAndReloadTable(resource)
767     {
768         if (!(WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView)) {
769             this._pendingInsertions.push(resource);
770             return;
771         }
772
773         console.assert(this._table);
774         if (!this._table)
775             return;
776
777         let entry = this._entryForResource(resource);
778
779         // Default sort has fast path.
780         if (this._isDefaultSort() || !this._entriesSortComparator) {
781             this._entries.push(entry);
782             if (this._passFilter(entry)) {
783                 this._filteredEntries.push(entry);
784                 this._table.reloadDataAddedToEndOnly();
785             }
786             return;
787         }
788
789         insertObjectIntoSortedArray(entry, this._entries, this._entriesSortComparator);
790
791         if (this._passFilter(entry)) {
792             insertObjectIntoSortedArray(entry, this._filteredEntries, this._entriesSortComparator);
793
794             // Probably a useless optimization here, but if we only added this row to the end
795             // we may avoid recreating all visible rows by saying as such.
796             if (this._filteredEntries.lastValue === entry)
797                 this._table.reloadDataAddedToEndOnly();
798             else
799                 this._table.reloadData();
800         }
801     }
802
803     _displayType(resource)
804     {
805         if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font) {
806             let fileExtension;
807             if (resource.mimeType)
808                 fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
809             if (!fileExtension)
810                 fileExtension = WI.fileExtensionForURL(resource.url);
811             if (fileExtension)
812                 return fileExtension;
813         }
814
815         return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase();
816     }
817
818     _entryForResource(resource)
819     {
820         // FIXME: <https://webkit.org/b/143632> Web Inspector: Resources with the same name in different folders aren't distinguished
821         // FIXME: <https://webkit.org/b/176765> Web Inspector: Resource names should be less ambiguous
822
823         return {
824             resource,
825             name: WI.displayNameForURL(resource.url, resource.urlComponents),
826             domain: WI.displayNameForHost(resource.urlComponents.host),
827             scheme: resource.urlComponents.scheme ? resource.urlComponents.scheme.toLowerCase() : "",
828             method: resource.requestMethod,
829             type: resource.type,
830             displayType: this._displayType(resource),
831             mimeType: resource.mimeType,
832             status: resource.statusCode,
833             cached: resource.cached,
834             resourceSize: resource.size,
835             transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize,
836             time: resource.duration,
837             protocol: resource.protocol,
838             priority: resource.priority,
839             remoteAddress: resource.remoteAddress,
840             connectionIdentifier: resource.connectionIdentifier,
841             startTime: resource.firstTimestamp,
842         };
843     }
844
845     _passFilter(entry)
846     {
847         if (!this._activeTypeFilters)
848             return true;
849
850         return this._activeTypeFilters.some((checker) => checker(entry.resource.type));
851     }
852
853     _updateSortAndFilteredEntries()
854     {
855         this._entries = this._entries.sort(this._entriesSortComparator);
856         this._updateFilteredEntries();
857     }
858
859     _updateFilteredEntries()
860     {
861         if (this._activeTypeFilters)
862             this._filteredEntries = this._entries.filter(this._passFilter, this);
863         else
864             this._filteredEntries = this._entries.slice();
865
866         this._restoreSelectedRow();
867     }
868
869     _generateTypeFilter()
870     {
871         let selectedItems = this._typeFilterScopeBar.selectedItems;
872         if (!selectedItems.length || selectedItems.includes(this._typeFilterScopeBarItemAll))
873             return null;
874
875         return selectedItems.map((item) => item.__checker);
876     }
877
878     _areFilterListsIdentical(listA, listB)
879     {
880         if (listA && listB) {
881             if (listA.length !== listB.length)
882                 return false;
883
884             for (let i = 0; i < listA.length; ++i) {
885                 if (listA[i] !== listB[i])
886                     return false;
887             }
888
889             return true;
890         }
891
892         return false;
893     }
894
895     _typeFilterScopeBarSelectionChanged(event)
896     {
897         // FIXME: <https://webkit.org/b/176763> Web Inspector: ScopeBar SelectionChanged event may dispatch multiple times for a single logical change
898         // We can't use shallow equals here because the contents are functions.
899         let oldFilter = this._activeTypeFilters;
900         let newFilter = this._generateTypeFilter();
901         if (this._areFilterListsIdentical(oldFilter, newFilter))
902             return;
903
904         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
905         this._hideResourceDetailView();
906
907         this._activeTypeFilters = newFilter;
908         this._updateFilteredEntries();
909         this._table.reloadData();
910     }
911
912     _restoreSelectedRow()
913     {
914         if (!this._selectedResource)
915             return;
916
917         let rowIndex = this._rowIndexForResource(this._selectedResource);
918         if (rowIndex === -1) {
919             this._selectedResource = null;
920             this._table.clearSelectedRow();
921             return;
922         }
923
924         this._table.selectRow(rowIndex);
925     }
926
927     _tableNameColumnDidChangeWidth(event)
928     {
929         this._nameColumnWidthSetting.value = event.target.width;
930
931         this._positionDetailView();
932     }
933 };