Web Inspector: Network Tab - Improve graphical representation of network waterfall
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 20 Oct 2017 02:11:27 +0000 (02:11 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 20 Oct 2017 02:11:27 +0000 (02:11 +0000)
https://bugs.webkit.org/show_bug.cgi?id=147897
<rdar://problem/27482198>

Reviewed by Brian Burg.

* Localizations/en.lproj/localizedStrings.js:
* UserInterface/Main.html:
New strings and resources.

* UserInterface/Views/Variables.css:
(:root):
Global styles.

* UserInterface/Views/NetworkTableContentView.css:
(.network-table .header .cell.waterfall):
(.network-table .timeline-ruler):
(.network-table .timeline-ruler > .header):
Styles for having a TimelineRuler in the Waterfall's table header.

(.network-table :not(.header) .cell.waterfall):
(.network-table :not(.header) .cell.waterfall .waterfall-container):
(.waterfall .block):
(.waterfall .block.request,):
(.waterfall .block.mouse):
(.waterfall .block.queue):
(.waterfall .block.dns):
(.waterfall .block.connect):
(.waterfall .block.secure):
(.waterfall .block.request):
(.waterfall .block.response):
Waterfall styles inside the Network Table.

* UserInterface/Views/NetworkTableContentView.js:
(WI.NetworkTableContentView):
(WI.NetworkTableContentView.prototype.reset):
(WI.NetworkTableContentView.prototype.tablePopulateCell):
(WI.NetworkTableContentView.prototype.initialLayout):
(WI.NetworkTableContentView.prototype._updateWaterfallTimelineRuler):
(WI.NetworkTableContentView.prototype._updateEntryForResource):
(WI.NetworkTableContentView.prototype._resourceLoadingDidFinish):
(WI.NetworkTableContentView.prototype._resourceLoadingDidFail):
(WI.NetworkTableContentView.prototype._networkTimelineRecordAdded):
(WI.NetworkTableContentView.prototype._tableWaterfallColumnDidChangeWidth):
Update the TimelineRuler and Waterfall column when the column's
size changes or the time bounds change. The time bounds right now
are the earliest and latest time of resources. Later we hope to
bound this by a timeline selection.

(WI.NetworkTableContentView.prototype._waterfallPopoverContentForResource):
(WI.NetworkTableContentView.prototype._handleMousedownWaterfall):
(WI.NetworkTableContentView.prototype._populateWaterfallGraph.appendBlock):
(WI.NetworkTableContentView.prototype._populateWaterfallGraph):
(WI.NetworkTableContentView.prototype._hidePopover):
Create and manage a popover for the waterfall column.

* UserInterface/Views/Popover.js:
(WI.Popover):
(WI.Popover.prototype.get element):
(WI.Popover.prototype.get visible):
(WI.Popover.prototype.get backgroundStyle):
(WI.Popover.prototype.set backgroundStyle):
(WI.Popover.prototype._drawBackground):
Provide an option to have a white background popover.

* UserInterface/Views/ResourceTimingBreakdownView.css: Added.
(.resource-timing-breakdown):
(.resource-timing-breakdown .waterfall):
(.resource-timing-breakdown .waterfall .block):
(.resource-timing-breakdown .waterfall .block.request):
(body[dir=ltr] .resource-timing-breakdown .waterfall .block.queue,):
(body[dir=ltr] .resource-timing-breakdown .waterfall .block.response):
(body[dir=rtl] .resource-timing-breakdown .waterfall .block.queue,):
(body[dir=rtl] .resource-timing-breakdown .waterfall .block.response):
(.resource-timing-breakdown .numbers):
(body[dir=ltr] .resource-timing-breakdown .numbers):
(body[dir=rtl] .resource-timing-breakdown .numbers):
Waterfall styles and sizes in the popover's breakdown view.

(.resource-timing-breakdown .numbers > p):
(.resource-timing-breakdown .numbers > p > .swatch):
(.resource-timing-breakdown .numbers .swatch.queue):
(.resource-timing-breakdown .numbers .swatch.dns):
(.resource-timing-breakdown .numbers .swatch.connect):
(.resource-timing-breakdown .numbers .swatch.secure):
(.resource-timing-breakdown .numbers .swatch.request):
(.resource-timing-breakdown .numbers .swatch.response):
(.resource-timing-breakdown .numbers > p > .label):
(.resource-timing-breakdown .numbers > p.total):
Number and label styles in the popover's breakdown view.

* UserInterface/Views/ResourceTimingBreakdownView.js: Added.
(WI.ResourceTimingBreakdownView):
(WI.ResourceTimingBreakdownView.prototype.initialLayout):
(WI.ResourceTimingBreakdownView.prototype.initialLayout.appendBlock):
(WI.ResourceTimingBreakdownView.prototype.initialLayout.appendRow):
Show a section for a waterfall visualization and a section for the numbers.

* UserInterface/Views/Table.js:
(WI.Table.prototype.reloadVisibleColumnCells):
(WI.Table.prototype.cellForRowAndColumn):
(WI.Table.prototype.addColumn):
(WI.Table.prototype.showColumn):
(WI.Table.prototype.hideColumn):
(WI.Table.prototype.resizerDragging):
(WI.Table.prototype.resizerDragEnded):
(WI.Table.prototype._resizeColumnsAndFiller):
(WI.Table.prototype._applyColumnWidths):
(WI.Table.prototype._positionHeaderViews):
* UserInterface/Views/TableColumn.js:
(WI.TableColumn.prototype.get headerView):
Provide a way to include a WI.View with a TableColumn Header. This
matches what we do with DataGrid, and ends up being pretty concise.

* UserInterface/Views/TimelineRuler.css:
(.timeline-ruler > .header):
Make the height a variable so that other code can work off of it.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@223734 268f45cc-cd09-0410-ab3c-d52691b4dbfc

12 files changed:
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.css
Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js
Source/WebInspectorUI/UserInterface/Views/Popover.js
Source/WebInspectorUI/UserInterface/Views/ResourceTimingBreakdownView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/ResourceTimingBreakdownView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/Table.js
Source/WebInspectorUI/UserInterface/Views/TableColumn.js
Source/WebInspectorUI/UserInterface/Views/TimelineRuler.css
Source/WebInspectorUI/UserInterface/Views/Variables.css

index ab77f82..3122685 100644 (file)
@@ -1,3 +1,123 @@
+2017-10-19  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Network Tab - Improve graphical representation of network waterfall
+        https://bugs.webkit.org/show_bug.cgi?id=147897
+        <rdar://problem/27482198>
+
+        Reviewed by Brian Burg.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Main.html:
+        New strings and resources.
+
+        * UserInterface/Views/Variables.css:
+        (:root):
+        Global styles.
+
+        * UserInterface/Views/NetworkTableContentView.css:
+        (.network-table .header .cell.waterfall):
+        (.network-table .timeline-ruler):
+        (.network-table .timeline-ruler > .header):
+        Styles for having a TimelineRuler in the Waterfall's table header.
+
+        (.network-table :not(.header) .cell.waterfall):
+        (.network-table :not(.header) .cell.waterfall .waterfall-container):
+        (.waterfall .block):
+        (.waterfall .block.request,):
+        (.waterfall .block.mouse):
+        (.waterfall .block.queue):
+        (.waterfall .block.dns):
+        (.waterfall .block.connect):
+        (.waterfall .block.secure):
+        (.waterfall .block.request):
+        (.waterfall .block.response):
+        Waterfall styles inside the Network Table.
+
+        * UserInterface/Views/NetworkTableContentView.js:
+        (WI.NetworkTableContentView):
+        (WI.NetworkTableContentView.prototype.reset):
+        (WI.NetworkTableContentView.prototype.tablePopulateCell):
+        (WI.NetworkTableContentView.prototype.initialLayout):
+        (WI.NetworkTableContentView.prototype._updateWaterfallTimelineRuler):
+        (WI.NetworkTableContentView.prototype._updateEntryForResource):
+        (WI.NetworkTableContentView.prototype._resourceLoadingDidFinish):
+        (WI.NetworkTableContentView.prototype._resourceLoadingDidFail):
+        (WI.NetworkTableContentView.prototype._networkTimelineRecordAdded):
+        (WI.NetworkTableContentView.prototype._tableWaterfallColumnDidChangeWidth):
+        Update the TimelineRuler and Waterfall column when the column's
+        size changes or the time bounds change. The time bounds right now
+        are the earliest and latest time of resources. Later we hope to
+        bound this by a timeline selection.
+
+        (WI.NetworkTableContentView.prototype._waterfallPopoverContentForResource):
+        (WI.NetworkTableContentView.prototype._handleMousedownWaterfall):
+        (WI.NetworkTableContentView.prototype._populateWaterfallGraph.appendBlock):
+        (WI.NetworkTableContentView.prototype._populateWaterfallGraph):
+        (WI.NetworkTableContentView.prototype._hidePopover):
+        Create and manage a popover for the waterfall column.
+
+        * UserInterface/Views/Popover.js:
+        (WI.Popover):
+        (WI.Popover.prototype.get element):
+        (WI.Popover.prototype.get visible):
+        (WI.Popover.prototype.get backgroundStyle):
+        (WI.Popover.prototype.set backgroundStyle):
+        (WI.Popover.prototype._drawBackground):
+        Provide an option to have a white background popover.
+
+        * UserInterface/Views/ResourceTimingBreakdownView.css: Added.
+        (.resource-timing-breakdown):
+        (.resource-timing-breakdown .waterfall):
+        (.resource-timing-breakdown .waterfall .block):
+        (.resource-timing-breakdown .waterfall .block.request):
+        (body[dir=ltr] .resource-timing-breakdown .waterfall .block.queue,):
+        (body[dir=ltr] .resource-timing-breakdown .waterfall .block.response):
+        (body[dir=rtl] .resource-timing-breakdown .waterfall .block.queue,):
+        (body[dir=rtl] .resource-timing-breakdown .waterfall .block.response):
+        (.resource-timing-breakdown .numbers):
+        (body[dir=ltr] .resource-timing-breakdown .numbers):
+        (body[dir=rtl] .resource-timing-breakdown .numbers):
+        Waterfall styles and sizes in the popover's breakdown view.
+
+        (.resource-timing-breakdown .numbers > p):
+        (.resource-timing-breakdown .numbers > p > .swatch):
+        (.resource-timing-breakdown .numbers .swatch.queue):
+        (.resource-timing-breakdown .numbers .swatch.dns):
+        (.resource-timing-breakdown .numbers .swatch.connect):
+        (.resource-timing-breakdown .numbers .swatch.secure):
+        (.resource-timing-breakdown .numbers .swatch.request):
+        (.resource-timing-breakdown .numbers .swatch.response):
+        (.resource-timing-breakdown .numbers > p > .label):
+        (.resource-timing-breakdown .numbers > p.total):
+        Number and label styles in the popover's breakdown view.
+
+        * UserInterface/Views/ResourceTimingBreakdownView.js: Added.
+        (WI.ResourceTimingBreakdownView):
+        (WI.ResourceTimingBreakdownView.prototype.initialLayout):
+        (WI.ResourceTimingBreakdownView.prototype.initialLayout.appendBlock):
+        (WI.ResourceTimingBreakdownView.prototype.initialLayout.appendRow):
+        Show a section for a waterfall visualization and a section for the numbers.
+
+        * UserInterface/Views/Table.js:
+        (WI.Table.prototype.reloadVisibleColumnCells):
+        (WI.Table.prototype.cellForRowAndColumn):
+        (WI.Table.prototype.addColumn):
+        (WI.Table.prototype.showColumn):
+        (WI.Table.prototype.hideColumn):
+        (WI.Table.prototype.resizerDragging):
+        (WI.Table.prototype.resizerDragEnded):
+        (WI.Table.prototype._resizeColumnsAndFiller):
+        (WI.Table.prototype._applyColumnWidths):
+        (WI.Table.prototype._positionHeaderViews):
+        * UserInterface/Views/TableColumn.js:
+        (WI.TableColumn.prototype.get headerView):
+        Provide a way to include a WI.View with a TableColumn Header. This
+        matches what we do with DataGrid, and ends up being pretty concise.
+
+        * UserInterface/Views/TimelineRuler.css:
+        (.timeline-ruler > .header):
+        Make the height a variable so that other code can work off of it.
+
 2017-10-19  Ross Kirsling  <ross.kirsling@sony.com>
 
         Web Inspector: Remove superfluous file External/.eslintrc
index 1199b7f..9841567 100644 (file)
@@ -741,6 +741,7 @@ localizedStrings["Resource Size"] = "Resource Size";
 localizedStrings["Resource Type"] = "Resource Type";
 localizedStrings["Resource failed to load."] = "Resource failed to load.";
 localizedStrings["Resource has no content"] = "Resource has no content";
+localizedStrings["Resource has no timing data"] = "Resource has no timing data";
 localizedStrings["Resource was loaded with the “data“ scheme."] = "Resource was loaded with the “data“ scheme.";
 localizedStrings["Resource was served from the cache."] = "Resource was served from the cache.";
 localizedStrings["Resources"] = "Resources";
@@ -765,6 +766,7 @@ localizedStrings["Samples"] = "Samples";
 localizedStrings["Save File"] = "Save File";
 localizedStrings["Save Selected"] = "Save Selected";
 localizedStrings["Save configuration"] = "Save configuration";
+localizedStrings["Scheduled"] = "Scheduled";
 localizedStrings["Scheme"] = "Scheme";
 localizedStrings["Scope"] = "Scope";
 localizedStrings["Scope Chain"] = "Scope Chain";
@@ -896,6 +898,7 @@ localizedStrings["Stylesheet"] = "Stylesheet";
 localizedStrings["Stylesheets"] = "Stylesheets";
 localizedStrings["Subtree Modified"] = "Subtree Modified";
 localizedStrings["Summary"] = "Summary";
+localizedStrings["TCP"] = "TCP";
 localizedStrings["Tab width:"] = "Tab width:";
 localizedStrings["Tabs"] = "Tabs";
 localizedStrings["Take snapshot"] = "Take snapshot";
@@ -932,6 +935,7 @@ localizedStrings["Timing"] = "Timing";
 localizedStrings["Toggle Classes"] = "Toggle Classes";
 localizedStrings["Top"] = "Top";
 localizedStrings["Top Functions"] = "Top Functions";
+localizedStrings["Total"] = "Total";
 localizedStrings["Total Time"] = "Total Time";
 localizedStrings["Total memory size at the end of the selected time range"] = "Total memory size at the end of the selected time range";
 localizedStrings["Total time"] = "Total time";
@@ -976,6 +980,7 @@ localizedStrings["Vertical"] = "Vertical";
 localizedStrings["View variable value"] = "View variable value";
 localizedStrings["Visibility"] = "Visibility";
 localizedStrings["Visible"] = "Visible";
+localizedStrings["Waiting"] = "Waiting";
 localizedStrings["Warning: "] = "Warning: ";
 localizedStrings["Warnings"] = "Warnings";
 localizedStrings["Watch Expressions"] = "Watch Expressions";
index 73f0c2c..fa97561 100644 (file)
     <link rel="stylesheet" href="Views/ResourceIcons.css">
     <link rel="stylesheet" href="Views/ResourceSidebarPanel.css">
     <link rel="stylesheet" href="Views/ResourceTimelineDataGridNode.css">
+    <link rel="stylesheet" href="Views/ResourceTimingBreakdownView.css">
     <link rel="stylesheet" href="Views/ResourceTreeElement.css">
     <link rel="stylesheet" href="Views/RulesStyleDetailsPanel.css">
     <link rel="stylesheet" href="Views/ScopeBar.css">
     <script src="Views/ResourceCookiesContentView.js"></script>
     <script src="Views/ResourceSidebarPanel.js"></script>
     <script src="Views/ResourceTimelineDataGridNode.js"></script>
+    <script src="Views/ResourceTimingBreakdownView.js"></script>
     <script src="Views/ResourceTimingPopoverDataGridNode.js"></script>
     <script src="Views/RulesStyleDetailsPanel.js"></script>
     <script src="Views/ScopeBar.js"></script>
index 1867a4e..7c48fdd 100644 (file)
@@ -72,10 +72,43 @@ body[dir=rtl] .network-table .cell.name > .status {
     display: none;
 }
 
+.showing-detail .network-table .timeline-ruler {
+    display: none;
+}
+
+.network-table .header .cell.waterfall {
+    /* Hide the label for this column. */
+    color: transparent;
+}
+
 .network-table :not(.header) .cell:first-of-type {
     background: rgba(0, 0, 0, 0.07);
 }
 
+.network-table :not(.header) .cell.waterfall {
+    position: absolute;
+    height: 20px;
+}
+
+.network-table :not(.header) .cell.waterfall .waterfall-container {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+}
+
+.network-table .timeline-ruler {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    overflow: hidden;
+}
+
+.network-table .timeline-ruler > .header {
+    top: calc(var(--navigation-bar-height) - var(--timeline-ruler-height));
+}
+
 .content-view.network .empty-content-placeholder {
     position: absolute;
     top: var(--navigation-bar-height);
@@ -110,3 +143,48 @@ body[dir=rtl] .content-view.network .empty-content-placeholder {
     line-height: 25px;
     text-align: center;
 }
+
+.waterfall .block {
+    position: absolute;
+    top: 7px;
+    min-width: 2px;
+    height: 6px;
+}
+
+.waterfall .block.request,
+.waterfall .block.response {
+    top: 3px;
+    height: 14px;
+}
+
+.waterfall .block.mouse-tracking {
+    top: 1px;
+    z-index: 2;
+    height: 18px;
+}
+
+.waterfall .block.queue {
+    min-width: 3px;
+    -webkit-margin-start: -1px;
+    background-color: var(--network-queue-color);
+}
+
+.waterfall .block.dns {
+    background-color: var(--network-dns-color);
+}
+
+.waterfall .block.connect {
+    background-color: var(--network-connect-color);
+}
+
+.waterfall .block.secure {
+    background-color: var(--network-secure-color);
+}
+
+.waterfall .block.request {
+    background-color: var(--network-request-color);
+}
+
+.waterfall .block.response {
+    background-color: var(--network-response-color);
+}
index 96f3445..3499105 100644 (file)
@@ -43,6 +43,11 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._resourceDetailView = null;
         this._resourceDetailViewMap = new Map;
 
+        this._waterfallStartTime = NaN;
+        this._waterfallEndTime = NaN;
+        this._waterfallTimelineRuler = null;
+        this._waterfallPopover = null;
+
         // FIXME: Network Timeline.
         // FIXME: Throttling.
         // FIXME: HAR Export.
@@ -177,6 +182,8 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     hidden()
     {
+        this._hidePopover();
+
         if (this._resourceDetailView)
             this._resourceDetailView.hidden();
 
@@ -185,6 +192,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     closed()
     {
+        this._hidePopover();
         this._hideResourceDetailView();
 
         for (let detailView of this._resourceDetailViewMap.values())
@@ -209,7 +217,12 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             detailView.dispose();
         this._resourceDetailViewMap.clear();
 
+        this._waterfallStartTime = NaN;
+        this._waterfallEndTime = NaN;
+        this._updateWaterfallTimelineRuler();
+
         if (this._table) {
+            this._hidePopover();
             this._hideResourceDetailView();
             this._selectedResource = null;
             this._table.clearSelectedRow();
@@ -336,8 +349,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             cell.textContent = isNaN(entry.time) ? emDash : Number.secondsToString(Math.max(entry.time, 0));
             break;
         case "waterfall":
-            // FIXME: Waterfall graph.
-            cell.textContent = emDash;
+            this._populateWaterfallGraph(cell, entry);
             break;
         }
 
@@ -385,6 +397,83 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         console.assert(!cell.classList.contains("cache-type"), "Should not have cache-type class on cell.");
     }
 
+    _populateWaterfallGraph(cell, entry)
+    {
+        cell.removeChildren();
+
+        let resource = entry.resource;
+        if (!resource.hasResponse()) {
+            cell.textContent = zeroWidthSpace;
+            return;
+        }
+
+        let {startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
+        if (isNaN(startTime)) {
+            cell.textContent = zeroWidthSpace;
+            return;
+        }
+
+        let graphStartTime = this._waterfallTimelineRuler.startTime;
+        if (responseEnd < graphStartTime) {
+            cell.textContent = zeroWidthSpace;
+            return;
+        }
+
+        let graphEndTime = this._waterfallTimelineRuler.endTime;
+        if (startTime > graphEndTime) {
+            cell.textContent = zeroWidthSpace;
+            return;
+        }
+
+        let secondsPerPixel = this._waterfallTimelineRuler.secondsPerPixel;
+
+        let container = cell.appendChild(document.createElement("div"));
+        container.className = "waterfall-container";
+
+        function appendBlock(startTime, endTime, className) {
+            let startOffset = (startTime - graphStartTime) / secondsPerPixel;
+            let width = (endTime - startTime) / secondsPerPixel;
+            let block = container.appendChild(document.createElement("div"));
+            block.classList.add("block", className);
+            let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right";
+            block.style[styleAttribute] = startOffset + "px";
+            block.style.width = width + "px";
+            return block;
+        }
+
+        // Mouse block sits on top and accepts mouse events on this group.
+        let padSeconds = 10 * secondsPerPixel;
+        let mouseBlock = appendBlock(startTime - padSeconds, responseEnd + padSeconds, "mouse-tracking");
+        mouseBlock.addEventListener("mousedown", (event) => {
+            if (event.button !== 0 || event.ctrlKey)
+                return;
+            this._handleMousedownWaterfall(mouseBlock, entry, event);
+        });
+
+        // Super small visualization.
+        let totalWidth = (responseEnd - startTime) / secondsPerPixel;
+        if (totalWidth <= 3) {
+            appendBlock(startTime, requestStart, "queue");
+            appendBlock(startTime, responseEnd, "response");
+            return;
+        }
+
+        // Each component.
+        if (domainLookupStart) {
+            appendBlock(startTime, domainLookupStart, "queue");
+            appendBlock(domainLookupStart, connectStart || requestStart, "dns");
+        } else if (connectStart)
+            appendBlock(startTime, connectStart, "queue");
+        else if (requestStart)
+            appendBlock(startTime, requestStart, "queue");
+        if (connectStart)
+            appendBlock(connectStart, connectEnd, "connect");
+        if (secureConnectionStart)
+            appendBlock(secureConnectionStart, connectEnd, "secure");
+        appendBlock(requestStart, responseStart, "request");
+        appendBlock(responseStart, responseEnd, "response");
+    }
+
     _generateSortComparator()
     {
         let sortColumnIdentifier = this._table.sortColumnIdentifier;
@@ -480,6 +569,9 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     initialLayout()
     {
+        this._waterfallTimelineRuler = new WI.TimelineRuler;
+        this._waterfallTimelineRuler.allowsClippedLabels = true;
+
         this._nameColumn = new WI.TableColumn("name", WI.UIString("Name"), {
             minWidth: WI.Sidebar.AbsoluteMinimumWidth,
             maxWidth: 500,
@@ -487,8 +579,6 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             resizeType: WI.TableColumn.ResizeType.Locked,
         });
 
-        this._nameColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableNameColumnDidChangeWidth, this);
-
         this._domainColumn = new WI.TableColumn("domain", WI.UIString("Domain"), {
             minWidth: 120,
             maxWidth: 200,
@@ -580,8 +670,12 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
         this._waterfallColumn = new WI.TableColumn("waterfall", WI.UIString("Waterfall"), {
             minWidth: 230,
+            headerView: this._waterfallTimelineRuler,
         });
 
+        this._nameColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableNameColumnDidChangeWidth, this);
+        this._waterfallColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableWaterfallColumnDidChangeWidth, this);
+
         this._table = new WI.Table("network-table", this, this, 20);
 
         this._table.addColumn(this._nameColumn);
@@ -610,6 +704,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     layout()
     {
+        this._updateWaterfallTimelineRuler();
         this._processPendingEntries();
         this._positionDetailView();
         this._positionEmptyFilterMessage();
@@ -622,6 +717,29 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     // Private
 
+    _updateWaterfallTimelineRuler()
+    {
+        if (!this._waterfallTimelineRuler)
+            return;
+
+        if (isNaN(this._waterfallStartTime)) {
+            this._waterfallTimelineRuler.zeroTime = 0;
+            this._waterfallTimelineRuler.startTime = 0;
+            this._waterfallTimelineRuler.endTime = 0.250;
+        } else {
+            this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime;
+            this._waterfallTimelineRuler.startTime = this._waterfallStartTime;
+            this._waterfallTimelineRuler.endTime = this._waterfallEndTime;
+
+            // Add a little bit of padding on the each side.
+            const paddingPixels = 5;
+            let padSeconds = paddingPixels * this._waterfallTimelineRuler.secondsPerPixel;
+            this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime - padSeconds;
+            this._waterfallTimelineRuler.startTime = this._waterfallStartTime - padSeconds;
+            this._waterfallTimelineRuler.endTime = this._waterfallEndTime + padSeconds;
+        }
+    }
+
     _processPendingEntries()
     {
         let needsSort = this._pendingUpdates.length > 0;
@@ -736,6 +854,12 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._filteredEntries[rowIndex] = entry;
     }
 
+    _hidePopover()
+    {
+        if (this._waterfallPopover)
+            this._waterfallPopover.dismiss();
+    }
+
     _hideResourceDetailView()
     {
         if (!this._resourceDetailView)
@@ -869,6 +993,11 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         let resource = event.target;
         this._pendingUpdates.push(resource);
 
+        if (resource.firstTimestamp < this._waterfallStartTime)
+            this._waterfallStartTime = resource.firstTimestamp;
+        if (resource.timingData.responseEnd > this._waterfallEndTime)
+            this._waterfallEndTime = resource.timingData.responseEnd;
+
         if (this._hasTextFilter())
             this._checkTextFilterAgainstFinishedResource(resource);
 
@@ -880,6 +1009,11 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         let resource = event.target;
         this._pendingUpdates.push(resource);
 
+        if (resource.firstTimestamp < this._waterfallStartTime)
+            this._waterfallStartTime = resource.firstTimestamp;
+        if (resource.timingData.responseEnd > this._waterfallEndTime)
+            this._waterfallEndTime = resource.timingData.responseEnd;
+
         if (this._hasTextFilter())
             this._checkTextFilterAgainstFailedResource(resource);
 
@@ -920,6 +1054,9 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         console.assert(resourceTimelineRecord instanceof WI.ResourceTimelineRecord);
 
         let resource = resourceTimelineRecord.resource;
+        if (isNaN(this._waterfallStartTime))
+            this._waterfallStartTime = this._waterfallEndTime = resource.firstTimestamp;
+
         this._insertResourceAndReloadTable(resource);
     }
 
@@ -1223,6 +1360,66 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._table.selectRow(rowIndex);
     }
 
+    _waterfallPopoverContentForResource(resource)
+    {
+        let contentElement = document.createElement("div");
+        contentElement.className = "waterfall-popover";
+
+        if (!resource.hasResponse() || !resource.timingData.startTime || !resource.timingData.responseEnd) {
+            contentElement.textContent = WI.UIString("Resource has no timing data");
+            return contentElement;
+        }
+
+        let breakdownView = new WI.ResourceTimingBreakdownView(resource);
+        contentElement.appendChild(breakdownView.element);
+        breakdownView.updateLayout();
+
+        return contentElement;
+    }
+
+    _handleMousedownWaterfall(mouseBlock, entry, event)
+    {
+        if (!this._waterfallPopover) {
+            this._waterfallPopover = new WI.Popover;
+            this._waterfallPopover.backgroundStyle = WI.Popover.BackgroundStyle.White;
+        }
+
+        if (this._waterfallPopover.visible)
+            return;
+
+        let calculateTargetFrame = () => {
+            let rowIndex = this._rowIndexForResource(entry.resource);
+            let cell = this._table.cellForRowAndColumn(rowIndex, this._waterfallColumn);
+            if (!cell) {
+                this._waterfallPopover.dismiss();
+                return null;
+            }
+
+            let mouseBlock = cell.querySelector(".block.mouse-tracking");
+            if (!mouseBlock) {
+                this._waterfallPopover.dismiss();
+                return null;
+            }
+
+            return WI.Rect.rectFromClientRect(mouseBlock.getBoundingClientRect());
+        };
+
+        let targetFrame = calculateTargetFrame();
+        if (!targetFrame.size.width && !targetFrame.size.height)
+            return;
+
+        let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
+        let preferredEdges = isRTL ? [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X] : [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MIN_X];
+        this._waterfallPopover.windowResizeHandler = () => {
+            let bounds = calculateTargetFrame();
+            if (bounds)
+                this._waterfallPopover.present(bounds, preferredEdges);
+        };
+
+        let popoverContentElement = this._waterfallPopoverContentForResource(entry.resource);
+        this._waterfallPopover.presentNewContentWithFrame(popoverContentElement, targetFrame, preferredEdges);
+    }
+
     _tableNameColumnDidChangeWidth(event)
     {
         this._nameColumnWidthSetting.value = event.target.width;
@@ -1230,4 +1427,9 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._positionDetailView();
         this._positionEmptyFilterMessage();
     }
+
+    _tableWaterfallColumnDidChangeWidth(event)
+    {
+        this._table.reloadVisibleColumnCells(this._waterfallColumn);
+    }
 };
index ca4fe4f..ebd1034 100644 (file)
@@ -37,6 +37,7 @@ WI.Popover = class Popover extends WI.Object
         this._anchorPoint = new WI.Point;
         this._preferredEdges = null;
         this._resizeHandler = null;
+        this._backgroundStyle = WI.Popover.BackgroundStyle.Default;
 
         this._contentNeedsUpdate = false;
         this._dismissing = false;
@@ -51,9 +52,11 @@ WI.Popover = class Popover extends WI.Object
 
     // Public
 
-    get element()
+    get element() { return this._element; }
+
+    get visible()
     {
-        return this._element;
+        return this._element.parentNode === document.body && !this._element.classList.contains(WI.Popover.FadeOutClassName);
     }
 
     get frame()
@@ -61,11 +64,6 @@ WI.Popover = class Popover extends WI.Object
         return this._frame;
     }
 
-    get visible()
-    {
-        return this._element.parentNode === document.body && !this._element.classList.contains(WI.Popover.FadeOutClassName);
-    }
-
     set frame(frame)
     {
         this._element.style.left = frame.minX() + "px";
@@ -76,6 +74,18 @@ WI.Popover = class Popover extends WI.Object
         this._frame = frame;
     }
 
+    get backgroundStyle()
+    {
+        return this._backgroundStyle;
+    }
+
+    set backgroundStyle(style)
+    {
+        console.assert(Object.values(WI.Popover.BackgroundStyle).includes(style));
+
+        this._backgroundStyle = style;
+    }
+
     set content(content)
     {
         if (content === this._content)
@@ -424,7 +434,7 @@ WI.Popover = class Popover extends WI.Object
         ctx.clip();
 
         // Panel background color fill.
-        ctx.fillStyle = "rgb(236, 236, 236)";
+        ctx.fillStyle = this._backgroundStyle === WI.Popover.BackgroundStyle.White ? "white" : "rgb(236, 236, 236)";
         ctx.fillRect(0, 0, width, height);
 
         // Stroke.
@@ -582,6 +592,11 @@ WI.Popover = class Popover extends WI.Object
     }
 };
 
+WI.Popover.BackgroundStyle = {
+    Default: "popover-background-default",
+    White: "popover-background-white",
+};
+
 WI.Popover.FadeOutClassName = "fade-out";
 WI.Popover.CornerRadius = 5;
 WI.Popover.MinWidth = 40;
diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceTimingBreakdownView.css b/Source/WebInspectorUI/UserInterface/Views/ResourceTimingBreakdownView.css
new file mode 100644 (file)
index 0000000..4a7e12c
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.resource-timing-breakdown {
+    width: 300px;
+    padding: 10px 0;
+    text-align: center;
+}
+
+.resource-timing-breakdown .waterfall {
+    position: relative;
+    height: 40px;
+}
+
+.resource-timing-breakdown .waterfall .block {
+    top: 12px;
+    min-width: 3px;
+    height: 12px;
+}
+
+.resource-timing-breakdown .waterfall .block.request,
+.resource-timing-breakdown .waterfall .block.response {
+    top: 4px;
+    height: 28px;
+}
+
+body[dir=ltr] .resource-timing-breakdown .waterfall .block:matches(.queue, .request) {
+    border-top-left-radius: 2px;
+    border-bottom-left-radius: 2px;
+}
+
+body[dir=ltr] .resource-timing-breakdown .waterfall .block.response {
+    border-top-right-radius: 2px;
+    border-bottom-right-radius: 2px;
+}
+
+body[dir=rtl] .resource-timing-breakdown .waterfall .block:matches(.queue, .request) {
+    border-top-right-radius: 2px;
+    border-bottom-right-radius: 2px;
+}
+
+body[dir=rtl] .resource-timing-breakdown .waterfall .block.response {
+    border-top-left-radius: 2px;
+    border-bottom-left-radius: 2px;
+}
+
+.resource-timing-breakdown .numbers {
+    display: inline-block;
+    -webkit-margin-start: 20px;
+    text-align: start;
+}
+
+.resource-timing-breakdown .numbers > p {
+    margin: 5px 0;
+}
+
+.resource-timing-breakdown .numbers > p > .swatch {
+    display: inline-block;
+    width: 10px;
+    height: 10px;
+    -webkit-margin-start: 12px;
+    -webkit-margin-end: 3px;
+    border: 1px solid hsla(0, 0%, 25%, 0.4);
+    border-radius: 2px;
+}
+
+.resource-timing-breakdown .numbers .swatch.queue {
+    background-color: var(--network-queue-color);
+}
+
+.resource-timing-breakdown .numbers .swatch.dns {
+    background-color: var(--network-dns-color);
+}
+
+.resource-timing-breakdown .numbers .swatch.connect {
+    background-color: var(--network-connect-color);
+}
+
+.resource-timing-breakdown .numbers .swatch.secure {
+    background-color: var(--network-secure-color);
+}
+
+.resource-timing-breakdown .numbers .swatch.request {
+    background-color: var(--network-request-color);
+}
+
+.resource-timing-breakdown .numbers .swatch.response {
+    background-color: var(--network-response-color);
+}
+
+.resource-timing-breakdown .numbers > p > .label {
+    font-weight: bold;
+}
+
+.resource-timing-breakdown .numbers > p.total {
+    margin-top: 15px;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceTimingBreakdownView.js b/Source/WebInspectorUI/UserInterface/Views/ResourceTimingBreakdownView.js
new file mode 100644 (file)
index 0000000..72a9b6d
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.ResourceTimingBreakdownView = class ResourceTimingBreakdownView extends WI.View
+{
+    constructor(resource)
+    {
+        super(null);
+
+        console.assert(resource.timingData.startTime && resource.timingData.responseEnd, "Timing breakdown view requires a resource with timing data.");
+
+        this._resource = resource;
+
+        this.element.classList.add("resource-timing-breakdown");
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        const graphWidth = 250;
+        const graphStartOffset = 25;
+
+        let {startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = this._resource.timingData;
+        let graphStartTime = startTime;
+        let graphEndTime = responseEnd;
+        let secondsPerPixel = (responseEnd - startTime) / graphWidth;
+
+        let waterfallElement = this.element.appendChild(document.createElement("div"));
+        waterfallElement.className = "waterfall";
+
+        function appendBlock(startTime, endTime, className) {
+            let startOffset = graphStartOffset + ((startTime - graphStartTime) / secondsPerPixel);
+            let width = (endTime - startTime) / secondsPerPixel;
+            let block = waterfallElement.appendChild(document.createElement("div"));
+            block.classList.add("block", className);
+            let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right";
+            block.style[styleAttribute] = startOffset + "px";
+            block.style.width = width + "px";
+        }
+
+        if (domainLookupStart) {
+            appendBlock(startTime, domainLookupStart, "queue");
+            appendBlock(domainLookupStart, connectStart || requestStart, "dns");
+        } else if (connectStart)
+            appendBlock(startTime, connectStart, "queue");
+        else if (requestStart)
+            appendBlock(startTime, requestStart, "queue");
+        if (connectStart)
+            appendBlock(connectStart, connectEnd, "connect");
+        if (secureConnectionStart)
+            appendBlock(secureConnectionStart, connectEnd, "secure");
+        appendBlock(requestStart, responseStart, "request");
+        appendBlock(responseStart, responseEnd, "response");
+
+        let numbersSection = this.element.appendChild(document.createElement("div"));
+        numbersSection.className = "numbers";
+
+        function appendRow(label, duration, paragraphClass, swatchClass) {
+            let p = numbersSection.appendChild(document.createElement("p"));
+            if (paragraphClass)
+                p.className = paragraphClass;
+
+            if (swatchClass) {
+                let swatch = p.appendChild(document.createElement("span"));
+                swatch.classList.add("swatch", swatchClass);
+            }
+
+            let labelElement = p.appendChild(document.createElement("span"));
+            labelElement.className = "label";
+            labelElement.textContent = label;
+
+            p.append(": ");
+
+            let durationElement = p.appendChild(document.createElement("span"));
+            durationElement.className = "duration";
+            durationElement.textContent = Number.secondsToMillisecondsString(duration);
+        }
+
+        let scheduledDuration = (domainLookupStart || connectStart || requestStart) - startTime;
+        let connectionDuration = (connectEnd || requestStart) - (domainLookupStart || connectStart || connectEnd || requestStart);
+        let requestResponseDuration = responseEnd - requestStart;
+
+        appendRow(WI.UIString("Scheduled"), scheduledDuration);
+        if (connectionDuration) {
+            appendRow(WI.UIString("Connection"), connectionDuration);
+            if (domainLookupStart)
+                appendRow(WI.UIString("DNS"), (domainLookupEnd || connectStart) - domainLookupStart, "sub", "dns");
+            appendRow(WI.UIString("TCP"), connectEnd - connectStart, "sub", "connect");
+            if (secureConnectionStart)
+                appendRow(WI.UIString("Secure"), connectEnd - secureConnectionStart, "sub", "secure");
+        }
+        appendRow(WI.UIString("Request & Response"), responseEnd - requestStart);
+        appendRow(WI.UIString("Waiting"), responseStart - requestStart, "sub", "request");
+        appendRow(WI.UIString("Response"), responseEnd - responseStart, "sub", "response");
+        appendRow(WI.UIString("Total"), responseEnd - startTime, "total");
+    }
+};
index ef93012..de624c4 100644 (file)
@@ -238,6 +238,23 @@ WI.Table = class Table extends WI.View
         this._cachedRows.delete(rowIndex);
     }
 
+    reloadVisibleColumnCells(column)
+    {
+        let columnIndex = this._visibleColumns.indexOf(column);
+        if (columnIndex === -1)
+            return;
+
+        for (let rowIndex = this._visibleRowIndexStart; rowIndex < this._visibleRowIndexEnd; ++rowIndex) {
+            let row = this._cachedRows.get(rowIndex);
+            if (!row)
+                continue;
+            let cell = row.children[columnIndex];
+            if (!cell)
+                continue;
+            this._delegate.tablePopulateCell(this, cell, column, rowIndex);
+        }
+    }
+
     reloadCell(rowIndex, columnIdentifier)
     {
         let column = this._columnSpecs.get(columnIdentifier);
@@ -297,6 +314,22 @@ WI.Table = class Table extends WI.View
         return this._columnSpecs.get(identifier);
     }
 
+    cellForRowAndColumn(rowIndex, column)
+    {
+        if (!this._isRowVisible(rowIndex))
+            return null;
+
+        let row = this._cachedRows.get(rowIndex);
+        if (!row)
+            return null;
+
+        let columnIndex = this._visibleColumns.indexOf(column);
+        if (columnIndex === -1)
+            return null;
+
+        return row.children[columnIndex];
+    }
+
     addColumn(column)
     {
         this._columnSpecs.set(column.identifier, column);
@@ -309,6 +342,8 @@ WI.Table = class Table extends WI.View
             this._visibleColumns.push(column);
             this._headerElement.appendChild(this._createHeaderCell(column));
             this._fillerRow.appendChild(this._createFillerCell(column));
+            if (column.headerView)
+                this.addSubview(column.headerView);
         }
 
         // Restore saved user-specified column visibility.
@@ -356,6 +391,9 @@ WI.Table = class Table extends WI.View
         this._headerElement.insertBefore(this._createHeaderCell(column), this._headerElement.children[newColumnIndex]);
         this._fillerRow.insertBefore(this._createFillerCell(column), this._fillerRow.children[newColumnIndex]);
 
+        if (column.headerView)
+            this.addSubview(column.headerView);
+
         // We haven't yet done any layout, nothing to do.
         if (!this._columnWidths)
             return;
@@ -416,6 +454,9 @@ WI.Table = class Table extends WI.View
         this._headerElement.removeChild(this._headerElement.children[columnIndex]);
         this._fillerRow.removeChild(this._fillerRow.children[columnIndex]);
 
+        if (column.headerView)
+            this.removeSubview(column.headerView);
+
         // We haven't yet done any layout, nothing to do.
         if (!this._columnWidths)
             return;
@@ -567,6 +608,7 @@ WI.Table = class Table extends WI.View
         this._widthGeneration++;
 
         this._applyColumnWidths();
+        this._positionHeaderViews();
     }
 
     resizerDragEnded(resizer)
@@ -581,6 +623,7 @@ WI.Table = class Table extends WI.View
         this._resizeOriginalColumnWidths = null;
 
         this._positionResizerElements();
+        this._positionHeaderViews();
     }
 
     // Private
@@ -869,6 +912,7 @@ WI.Table = class Table extends WI.View
         this._updateFillerRowWithNewHeight();
         this._applyColumnWidths();
         this._positionResizerElements();
+        this._positionHeaderViews();
     }
 
     _updateVisibleRows()
@@ -976,9 +1020,6 @@ WI.Table = class Table extends WI.View
 
     _applyColumnWidths()
     {
-        for (let i = 0; i < this._visibleColumns.length; ++i)
-            this._visibleColumns[i].width = this._columnWidths[i];
-
         for (let i = 0; i < this._headerElement.children.length; ++i)
             this._headerElement.children[i].style.width = this._columnWidths[i] + "px";
 
@@ -988,6 +1029,10 @@ WI.Table = class Table extends WI.View
             row.__widthGeneration = this._widthGeneration;
         }
 
+        // Update Table Columns after cells since events may respond to this.
+        for (let i = 0; i < this._visibleColumns.length; ++i)
+            this._visibleColumns[i].width = this._columnWidths[i];
+
         // Create missing cells after we've sized.
         for (let row of this._listElement.children) {
             if (row !== this._fillerRow) {
@@ -1042,6 +1087,29 @@ WI.Table = class Table extends WI.View
         }
     }
 
+    _positionHeaderViews()
+    {
+        if (!this.subviews.length)
+            return;
+
+        let offset = 0;
+        let updates = [];
+        for (let i = 0; i < this._visibleColumns.length; ++i) {
+            let column = this._visibleColumns[i];
+            let width = this._columnWidths[i];
+            if (column.headerView)
+                updates.push({headerView: column.headerView, offset, width});
+            offset += width;
+        }
+
+        let styleProperty = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
+        for (let {headerView, offset, width} of updates) {
+            headerView.element.style.setProperty(styleProperty, offset + "px");
+            headerView.element.style.width = width + "px";
+            headerView.updateLayout(WI.View.LayoutReason.Resize);
+        }
+    }
+
     _isRowVisible(rowIndex)
     {
         if (!this._previousRevealedRowCount)
index 27099bf..408c2f6 100644 (file)
@@ -25,7 +25,7 @@
 
 WI.TableColumn = class TableColumn extends WI.Object
 {
-    constructor(identifier, name, {initialWidth, minWidth, maxWidth, hidden, sortable, hideable, align, resizeType} = {})
+    constructor(identifier, name, {initialWidth, minWidth, maxWidth, hidden, sortable, hideable, align, resizeType, headerView} = {})
     {
         super();
 
@@ -48,6 +48,7 @@ WI.TableColumn = class TableColumn extends WI.Object
         this._hideable = typeof hideable === "boolean" ? hideable : true;
         this._align = align || null;
         this._resizeType = resizeType || TableColumn.ResizeType.Auto;
+        this._headerView = headerView || null;
 
         console.assert(!this._minWidth || !this._maxWidth || this._minWidth <= this._maxWidth, "Invalid min/max", this._minWidth, this._maxWidth);
         console.assert(isNaN(this._width) || !this._minWidth || (this._width >= this._minWidth), "Initial width is less than min", this._width, this._minWidth);
@@ -65,6 +66,7 @@ WI.TableColumn = class TableColumn extends WI.Object
     get sortable() { return this._sortable; }
     get hideable() { return this._hideable; }
     get align() { return this._align; }
+    get headerView() { return this._headerView; }
 
     get locked() { return this._resizeType === TableColumn.ResizeType.Locked; }
     get flexible() { return this._resizeType === TableColumn.ResizeType.Auto; }
index 4cc4c9a..5781072 100644 (file)
@@ -61,7 +61,9 @@ body[dir=rtl] .timeline-ruler > .markers > :matches(.divider, .marker) {
     top: 0;
     left: 0;
     right: 0;
-    height: 23px;
+    height: var(--timeline-ruler-height);
+
+    --timeline-ruler-height: 23px;
 }
 
 
index de46263..34b3873 100644 (file)
     --network-pseudo-header-color: hsl(312, 35%, 51%);
     --network-error-color: hsl(0, 54%, 50%);
 
+    --network-queue-color: hsl(0, 0%, 54%);
+    --network-dns-color: hsl(265, 82%, 60%);
+    --network-connect-color: hsl(46, 92%, 62%);
+    --network-secure-color: hsl(352, 81%, 59%);
+    --network-request-color: hsl(118, 56%, 65%);
+    --network-response-color: hsl(202, 61%, 59%);
+
     --even-zebra-stripe-row-background-color: white;
     --odd-zebra-stripe-row-background-color: hsl(0, 0%, 95%);
     --transparent-stripe-background-gradient: linear-gradient(to bottom, transparent, transparent 50%, hsla(0, 0%, 0%, 0.03) 50%, hsla(0, 0%, 0%, 0.03)) top left / 100% 40px;