Web Inspector: Network Tab - Headers Detail View
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 6 Oct 2017 22:48:28 +0000 (22:48 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 6 Oct 2017 22:48:28 +0000 (22:48 +0000)
https://bugs.webkit.org/show_bug.cgi?id=177896
<rdar://problem/34071924>

Source/WebInspectorUI:

Reviewed by Devin Rousso.

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

* UserInterface/Base/URLUtilities.js:
(parseURL):
(WI.h2Authority):
(WI.h2Path):
Utility methods to get the :authority and :path pseudo-headers from a URL.
This required adding user info (user:pass@) support to URL parsing.

* UserInterface/Views/NetworkTabContentView.js:
(WI.NetworkTabContentView):
* UserInterface/Views/NetworkTableContentView.js:
(WI.NetworkTableContentView.prototype.get navigationItems):
(WI.NetworkTableContentView.prototype.get filterNavigationItems):
Move the NetworkTab's filter controls to the left. Since these are not
dynamic just vend them from the TableContentView and place them in the
navigation bar.

* UserInterface/Models/Resource.js:
(WI.Resource.prototype.updateWithMetrics):
New event whenever metrics change. This is the first event that will allow
a client to react to a resource.protocol change.

* UserInterface/Views/NetworkResourceDetailView.css:
(.content-view.resource-details):
Base styles for the sub detail views.

* UserInterface/Views/NetworkResourceDetailView.js:
(WI.NetworkResourceDetailView):
(WI.NetworkResourceDetailView.prototype.headersContentViewGoToRequestData):
(WI.NetworkResourceDetailView.prototype.initialLayout):
(WI.NetworkResourceDetailView.prototype._showPreferredContentView):
(WI.NetworkResourceDetailView.prototype._showContentViewForNavigationItem):
Create a Header view and provide a way to switch to a particular view. This wil
be useful to jump from the Header's Request Data directly to the Preview's
Request ContentView.

* UserInterface/Views/ResourceDetailsSection.css:
(.resource-details > section):
(.resource-details > section > .title):
(.resource-details > section > .details):
(.resource-details > section > .details > p):
(.resource-details > section.incomplete > .details):
* UserInterface/Views/ResourceDetailsSection.js:
(WI.ResourceDetailsSection):
(WI.ResourceDetailsSection.prototype.get element):
(WI.ResourceDetailsSection.prototype.get titleElement):
(WI.ResourceDetailsSection.prototype.get detailsElement):
(WI.ResourceDetailsSection.prototype.toggleIncomplete):
(WI.ResourceDetailsSection.prototype.toggleError):
Simple sections with a title and details div with a border.
It may be common to have an incomplete load / error so this
provides some APIs and styles for sections marked incomplete
or with errors.

* UserInterface/Views/ResourceHeadersContentView.css:
(.resource-headers > section > .details):
(.resource-headers > section.headers > .details):
(.resource-headers > section.error > .details):
(.resource-headers > section.error .key):
Style the left border different colors for different sections or cases.

(.resource-headers .details):
(.resource-headers .details .pair):
(.resource-headers .details .key):
(.resource-headers .value):
(.resource-headers .header > .key):
(.resource-headers .h1-status > .key):
(.resource-headers .h2-pseudo-header > .key):
Wrapped text for key/value pairs and different colors for different
sections or cases.

(.resource-headers .go-to-arrow):
Go-to arrow styles for a request data section.

* UserInterface/Views/ResourceHeadersContentView.js: Added.
(WI.ResourceHeadersContentView):
(WI.ResourceHeadersContentView.prototype.initialLayout):
(WI.ResourceHeadersContentView.prototype.layout):
(WI.ResourceHeadersContentView.prototype._incompleteSectionWithMessage):
(WI.ResourceHeadersContentView.prototype._incompleteSectionWithLoadingIndicator):
(WI.ResourceHeadersContentView.prototype._appendKeyValuePair):
(WI.ResourceHeadersContentView.prototype._responseSourceDisplayString):
(WI.ResourceHeadersContentView.prototype._refreshSummarySection):
(WI.ResourceHeadersContentView.prototype._refreshRequestHeadersSection):
(WI.ResourceHeadersContentView.prototype._refreshResponseHeadersSection):
(WI.ResourceHeadersContentView.prototype._refreshQueryStringSection):
(WI.ResourceHeadersContentView.prototype._refreshRequestDataSection):
(WI.ResourceHeadersContentView.prototype._resourceMetricsDidChange):
(WI.ResourceHeadersContentView.prototype._resourceRequestHeadersDidChange):
(WI.ResourceHeadersContentView.prototype._resourceResponseReceived):
(WI.ResourceHeadersContentView.prototype._goToRequestDataClicked):
Summary, Request, Response, Query String, and Request Data sections.
The sections refresh as data becomes available.

* UserInterface/Views/Table.css:
(.table):
These variables are already defined globally.

* UserInterface/Views/Variables.css:
(:root):
New variables for the colors we use. They closely match, and are
sometimes identical to ones used in Timelines / Memory views.

Source/WebInspectorUI/../../LayoutTests:

Reviewed by NOBODY (OOPS!).

* inspector/unit-tests/url-utilities-expected.txt:
* inspector/unit-tests/url-utilities.html:
Tests for new utility functions.

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

18 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/unit-tests/url-utilities-expected.txt
LayoutTests/inspector/unit-tests/url-utilities.html
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/URLUtilities.js
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/Resource.js
Source/WebInspectorUI/UserInterface/Views/NetworkResourceDetailView.css
Source/WebInspectorUI/UserInterface/Views/NetworkResourceDetailView.js
Source/WebInspectorUI/UserInterface/Views/NetworkTabContentView.js
Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js
Source/WebInspectorUI/UserInterface/Views/ResourceDetailsSection.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/ResourceDetailsSection.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/ResourceHeadersContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/ResourceHeadersContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/Table.css
Source/WebInspectorUI/UserInterface/Views/Variables.css

index 3105ea5..d82ff11 100644 (file)
@@ -1,3 +1,15 @@
+2017-10-05  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Network Tab - Headers Detail View
+        https://bugs.webkit.org/show_bug.cgi?id=177896
+        <rdar://problem/34071924>
+
+        Reviewed by Devin Rousso.
+
+        * inspector/unit-tests/url-utilities-expected.txt:
+        * inspector/unit-tests/url-utilities.html:
+        Tests for new utility functions.
+
 2017-10-06  Nan Wang  <n_wang@apple.com>
 
         AX: [iOS] Layout Test accessibility/ios-simulator/video-elements-ios.html is failing
index 4ef5afe..8d3674a 100644 (file)
@@ -12,6 +12,7 @@ PASS: URL constructor thinks this is invalid
 
 Test Valid: http://example.com
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
 PASS: path should be: 'null'
@@ -21,6 +22,7 @@ PASS: lastPathComponent should be: 'null'
 
 Test Valid: http://example.com/
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
 PASS: path should be: '/'
@@ -30,6 +32,7 @@ PASS: lastPathComponent should be: 'null'
 
 Test Valid: http://example.com:80/
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: '80'
 PASS: path should be: '/'
@@ -39,6 +42,7 @@ PASS: lastPathComponent should be: 'null'
 
 Test Valid: http://example.com/path/to/page.html
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
 PASS: path should be: '/path/to/page.html'
@@ -48,6 +52,7 @@ PASS: lastPathComponent should be: 'page.html'
 
 Test Valid: http://example.com/path/to/page.html?
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
 PASS: path should be: '/path/to/page.html'
@@ -57,6 +62,7 @@ PASS: lastPathComponent should be: 'page.html'
 
 Test Valid: http://example.com/path/to/page.html?a=1
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
 PASS: path should be: '/path/to/page.html'
@@ -66,6 +72,7 @@ PASS: lastPathComponent should be: 'page.html'
 
 Test Valid: http://example.com/path/to/page.html?a=1&b=2
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
 PASS: path should be: '/path/to/page.html'
@@ -75,6 +82,7 @@ PASS: lastPathComponent should be: 'page.html'
 
 Test Valid: http://example.com/path/to/page.html?a=1&b=2#test
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
 PASS: path should be: '/path/to/page.html'
@@ -84,6 +92,7 @@ PASS: lastPathComponent should be: 'page.html'
 
 Test Valid: http://example.com:123/path/to/page.html?a=1&b=2#test
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: '123'
 PASS: path should be: '/path/to/page.html'
@@ -93,6 +102,7 @@ PASS: lastPathComponent should be: 'page.html'
 
 Test Valid: http://example.com/path/to/page.html#test
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
 PASS: path should be: '/path/to/page.html'
@@ -102,6 +112,7 @@ PASS: lastPathComponent should be: 'page.html'
 
 Test Valid: http://example.com#alpha/beta
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
 PASS: path should be: 'null'
@@ -111,6 +122,7 @@ PASS: lastPathComponent should be: 'null'
 
 Test Valid: app-specific://example.com
 PASS: scheme should be: 'app-specific'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
 PASS: path should be: 'null'
@@ -120,6 +132,7 @@ PASS: lastPathComponent should be: 'null'
 
 Test Valid: http://example
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'example'
 PASS: port should be: 'null'
 PASS: path should be: 'null'
@@ -129,6 +142,7 @@ PASS: lastPathComponent should be: 'null'
 
 Test Valid: http://my.example.com
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'my.example.com'
 PASS: port should be: 'null'
 PASS: path should be: 'null'
@@ -138,6 +152,7 @@ PASS: lastPathComponent should be: 'null'
 
 Test Valid: data:text/plain,test
 PASS: scheme should be: 'data'
+PASS: userinfo should be: 'null'
 PASS: host should be: 'null'
 PASS: port should be: 'null'
 PASS: path should be: 'null'
@@ -163,6 +178,7 @@ Test Valid: http:example.com/
 FAIL: scheme should be: 'http'
     Expected: "http"
     Actual: null
+PASS: userinfo should be: 'null'
 FAIL: host should be: 'example.com'
     Expected: "example.com"
     Actual: null
@@ -178,6 +194,7 @@ Test Valid: http:/example.com/
 FAIL: scheme should be: 'http'
     Expected: "http"
     Actual: null
+PASS: userinfo should be: 'null'
 FAIL: host should be: 'example.com'
     Expected: "example.com"
     Actual: null
@@ -189,10 +206,21 @@ PASS: queryString should be: 'null'
 PASS: fragment should be: 'null'
 PASS: lastPathComponent should be: 'null'
 
+Test Valid: http://user:pass@example.com/
+PASS: scheme should be: 'http'
+PASS: userinfo should be: 'user:pass'
+PASS: host should be: 'example.com'
+PASS: port should be: 'null'
+PASS: path should be: '/'
+PASS: queryString should be: 'null'
+PASS: fragment should be: 'null'
+PASS: lastPathComponent should be: 'null'
+
 Test Valid: http://user@pass:example.com/
 FAIL: scheme should be: 'http'
     Expected: "http"
     Actual: null
+PASS: userinfo should be: 'null'
 FAIL: host should be: 'example.com'
     Expected: "example.com"
     Actual: null
@@ -206,6 +234,7 @@ PASS: lastPathComponent should be: 'null'
 
 Test Valid: http://example.com?key=alpha/beta
 PASS: scheme should be: 'http'
+PASS: userinfo should be: 'null'
 FAIL: host should be: 'example.com'
     Expected: "example.com"
     Actual: "example.com?key=alpha"
@@ -306,3 +335,46 @@ PASS: charset should be: 'US-ASCII'
 PASS: base64 should be: 'true'
 PASS: data should be: 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
 
+-- Running test case: WI.h2Authority
+PASS: HTTP/2 :authority of 'http://example.com' should be 'example.com'.
+PASS: HTTP/2 :authority of 'https://example.com' should be 'example.com'.
+PASS: HTTP/2 :authority of 'ftp://example.com' should be 'example.com'.
+PASS: HTTP/2 :authority of 'http://example.com/foo' should be 'example.com'.
+PASS: HTTP/2 :authority of 'https://example.com/foo' should be 'example.com'.
+PASS: HTTP/2 :authority of 'ftp://example.com/foo' should be 'example.com'.
+PASS: HTTP/2 :authority of 'http://example.com:123' should be 'example.com:123'.
+PASS: HTTP/2 :authority of 'https://example.com:123' should be 'example.com:123'.
+PASS: HTTP/2 :authority of 'ftp://example.com:123' should be 'example.com:123'.
+PASS: HTTP/2 :authority of 'ftp://user:pass@example.com/foo' should be 'user:pass@example.com'.
+PASS: HTTP/2 :authority of 'http://user:pass@example.com/foo' should be 'example.com'.
+PASS: HTTP/2 :authority of 'https://user:pass@example.com/foo' should be 'example.com'.
+PASS: HTTP/2 :authority of 'ftp://user:pass@example.com:123/foo' should be 'user:pass@example.com:123'.
+PASS: HTTP/2 :authority of 'http://user:pass@example.com:123/foo' should be 'example.com:123'.
+PASS: HTTP/2 :authority of 'https://user:pass@example.com:123/foo' should be 'example.com:123'.
+
+-- Running test case: WI.h2Path
+PASS: HTTP/2 :path of 'http://example.com' should be '/'.
+PASS: HTTP/2 :path of 'https://example.com' should be '/'.
+PASS: HTTP/2 :path of 'ftp://example.com' should be ''.
+PASS: HTTP/2 :path of 'http://example.com/foo' should be '/foo'.
+PASS: HTTP/2 :path of 'https://example.com/foo' should be '/foo'.
+PASS: HTTP/2 :path of 'ftp://example.com/foo' should be '/foo'.
+PASS: HTTP/2 :path of 'http://example.com/foo#hash' should be '/foo'.
+PASS: HTTP/2 :path of 'https://example.com/foo#hash' should be '/foo'.
+PASS: HTTP/2 :path of 'ftp://example.com/foo#hash' should be '/foo'.
+PASS: HTTP/2 :path of 'http://example.com/foo/bar.js' should be '/foo/bar.js'.
+PASS: HTTP/2 :path of 'https://example.com/foo/bar.js' should be '/foo/bar.js'.
+PASS: HTTP/2 :path of 'ftp://example.com/foo/bar.js' should be '/foo/bar.js'.
+PASS: HTTP/2 :path of 'http://example.com/foo/bar.js#hash' should be '/foo/bar.js'.
+PASS: HTTP/2 :path of 'https://example.com/foo/bar.js#hash' should be '/foo/bar.js'.
+PASS: HTTP/2 :path of 'ftp://example.com/foo/bar.js#hash' should be '/foo/bar.js'.
+PASS: HTTP/2 :path of 'http://example.com/?t=1' should be '/?t=1'.
+PASS: HTTP/2 :path of 'https://example.com/?t=1' should be '/?t=1'.
+PASS: HTTP/2 :path of 'ftp://example.com/?t=1' should be '/?t=1'.
+PASS: HTTP/2 :path of 'http://example.com/foo/bar.js?t=1' should be '/foo/bar.js?t=1'.
+PASS: HTTP/2 :path of 'https://example.com/foo/bar.js?t=1' should be '/foo/bar.js?t=1'.
+PASS: HTTP/2 :path of 'ftp://example.com/foo/bar.js?t=1' should be '/foo/bar.js?t=1'.
+PASS: HTTP/2 :path of 'http://example.com/foo/bar.js?t=1#hash' should be '/foo/bar.js?t=1'.
+PASS: HTTP/2 :path of 'https://example.com/foo/bar.js?t=1#hash' should be '/foo/bar.js?t=1'.
+PASS: HTTP/2 :path of 'ftp://example.com/foo/bar.js?t=1#hash' should be '/foo/bar.js?t=1'.
+
index a74fcfa..6325557 100644 (file)
@@ -27,10 +27,11 @@ function test()
                 InspectorTest.log("");
                 InspectorTest.log("Test Valid: " + url);
 
-                let {scheme: expectedScheme, host: expectedHost, port: expectedPort, path: expectedPath, queryString: expectedQueryString, fragment: expectedFragment, lastPathComponent: expectedLastPathComponent} = expected;
-                let {scheme: actualScheme, host: actualHost, port: actualPort, path: actualPath, queryString: actualQueryString, fragment: actualFragment, lastPathComponent: actualLastPathComponent} = parseURL(url);
+                let {scheme: expectedScheme, userinfo: expectedUserInfo, host: expectedHost, port: expectedPort, path: expectedPath, queryString: expectedQueryString, fragment: expectedFragment, lastPathComponent: expectedLastPathComponent} = expected;
+                let {scheme: actualScheme, userinfo: actualUserInfo, host: actualHost, port: actualPort, path: actualPath, queryString: actualQueryString, fragment: actualFragment, lastPathComponent: actualLastPathComponent} = parseURL(url);
 
                 InspectorTest.expectEqual(actualScheme, expectedScheme, `scheme should be: '${expectedScheme}'`);
+                InspectorTest.expectEqual(actualUserInfo, expectedUserInfo, `userinfo should be: '${expectedUserInfo}'`);
                 InspectorTest.expectEqual(actualHost, expectedHost, `host should be: '${expectedHost}'`);
                 InspectorTest.expectEqual(actualPort, expectedPort, `port should be: '${expectedPort}'`);
                 InspectorTest.expectEqual(actualPath, expectedPath, `path should be: '${expectedPath}'`);
@@ -44,6 +45,7 @@ function test()
 
             testValid("http://example.com", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: null,
@@ -54,6 +56,7 @@ function test()
 
             testValid("http://example.com/", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: "/",
@@ -64,6 +67,7 @@ function test()
 
             testValid("http://example.com:80/", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: 80,
                 path: "/",
@@ -74,6 +78,7 @@ function test()
 
             testValid("http://example.com/path/to/page.html", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: "/path/to/page.html",
@@ -84,6 +89,7 @@ function test()
 
             testValid("http://example.com/path/to/page.html?", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: "/path/to/page.html",
@@ -94,6 +100,7 @@ function test()
 
             testValid("http://example.com/path/to/page.html?a=1", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: "/path/to/page.html",
@@ -104,6 +111,7 @@ function test()
 
             testValid("http://example.com/path/to/page.html?a=1&b=2", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: "/path/to/page.html",
@@ -114,6 +122,7 @@ function test()
 
             testValid("http://example.com/path/to/page.html?a=1&b=2#test", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: "/path/to/page.html",
@@ -124,6 +133,7 @@ function test()
 
             testValid("http://example.com:123/path/to/page.html?a=1&b=2#test", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: 123,
                 path: "/path/to/page.html",
@@ -134,6 +144,7 @@ function test()
 
             testValid("http://example.com/path/to/page.html#test", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: "/path/to/page.html",
@@ -144,6 +155,7 @@ function test()
 
             testValid("http://example.com#alpha/beta", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: null,
@@ -154,6 +166,7 @@ function test()
 
             testValid("app-specific://example.com", {
                 scheme: "app-specific",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: null,
@@ -164,6 +177,7 @@ function test()
 
             testValid("http://example", {
                 scheme: "http",
+                userinfo: null,
                 host: "example",
                 port: null,
                 path: null,
@@ -174,6 +188,7 @@ function test()
 
             testValid("http://my.example.com", {
                 scheme: "http",
+                userinfo: null,
                 host: "my.example.com",
                 port: null,
                 path: null,
@@ -185,6 +200,7 @@ function test()
             // Data URLs just spit back the scheme.
             testValid("data:text/plain,test", {
                 scheme: "data",
+                userinfo: null,
                 host: null,
                 port: null,
                 path: null,
@@ -202,6 +218,7 @@ function test()
 
             testValid("http:example.com/", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: "/",
@@ -212,6 +229,18 @@ function test()
 
             testValid("http:/example.com/", {
                 scheme: "http",
+                userinfo: null,
+                host: "example.com",
+                port: null,
+                path: "/",
+                queryString: null,
+                fragment: null,
+                lastPathComponent: null,
+            });
+
+            testValid("http://user:pass@example.com/", {
+                scheme: "http",
+                userinfo: "user:pass",
                 host: "example.com",
                 port: null,
                 path: "/",
@@ -222,6 +251,7 @@ function test()
 
             testValid("http://user@pass:example.com/", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: "/",
@@ -232,6 +262,7 @@ function test()
 
             testValid("http://example.com?key=alpha/beta", {
                 scheme: "http",
+                userinfo: null,
                 host: "example.com",
                 port: null,
                 path: null,
@@ -244,7 +275,6 @@ function test()
         }
     });
 
-
     suite.addTestCase({
         name: "parseDataURL",
         test() {
@@ -355,6 +385,79 @@ function test()
         }
     });
 
+    suite.addTestCase({
+        name: "WI.h2Authority",
+        test() {
+            function test(url, expected) {
+                let components = parseURL(url);
+                InspectorTest.expectEqual(WI.h2Authority(components), expected, `HTTP/2 :authority of '${url}' should be '${expected}'.`);
+            }
+
+            test("http://example.com", "example.com");
+            test("https://example.com", "example.com");
+            test("ftp://example.com", "example.com");
+
+            test("http://example.com/foo", "example.com");
+            test("https://example.com/foo", "example.com");
+            test("ftp://example.com/foo", "example.com");
+
+            test("http://example.com:123", "example.com:123");
+            test("https://example.com:123", "example.com:123");
+            test("ftp://example.com:123", "example.com:123");
+
+            test("ftp://user:pass@example.com/foo", "user:pass@example.com");
+            test("http://user:pass@example.com/foo", "example.com");
+            test("https://user:pass@example.com/foo", "example.com");
+
+            test("ftp://user:pass@example.com:123/foo", "user:pass@example.com:123");
+            test("http://user:pass@example.com:123/foo", "example.com:123");
+            test("https://user:pass@example.com:123/foo", "example.com:123");
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "WI.h2Path",
+        test() {
+            function test(url, expected) {
+                let components = parseURL(url);
+                InspectorTest.expectEqual(WI.h2Path(components), expected, `HTTP/2 :path of '${url}' should be '${expected}'.`);
+            }
+
+            test("http://example.com", "/");
+            test("https://example.com", "/");
+            test("ftp://example.com", "");
+
+            test("http://example.com/foo", "/foo");
+            test("https://example.com/foo", "/foo");
+            test("ftp://example.com/foo", "/foo");
+            test("http://example.com/foo#hash", "/foo");
+            test("https://example.com/foo#hash", "/foo");
+            test("ftp://example.com/foo#hash", "/foo");
+
+            test("http://example.com/foo/bar.js", "/foo/bar.js");
+            test("https://example.com/foo/bar.js", "/foo/bar.js");
+            test("ftp://example.com/foo/bar.js", "/foo/bar.js");
+            test("http://example.com/foo/bar.js#hash", "/foo/bar.js");
+            test("https://example.com/foo/bar.js#hash", "/foo/bar.js");
+            test("ftp://example.com/foo/bar.js#hash", "/foo/bar.js");
+
+            test("http://example.com/?t=1", "/?t=1");
+            test("https://example.com/?t=1", "/?t=1");
+            test("ftp://example.com/?t=1", "/?t=1");
+
+            test("http://example.com/foo/bar.js?t=1", "/foo/bar.js?t=1");
+            test("https://example.com/foo/bar.js?t=1", "/foo/bar.js?t=1");
+            test("ftp://example.com/foo/bar.js?t=1", "/foo/bar.js?t=1");
+            test("http://example.com/foo/bar.js?t=1#hash", "/foo/bar.js?t=1");
+            test("https://example.com/foo/bar.js?t=1#hash", "/foo/bar.js?t=1");
+            test("ftp://example.com/foo/bar.js?t=1#hash", "/foo/bar.js?t=1");
+
+            return true;
+        }
+    });
+
     suite.runTestCasesAndFinish();
 }
 </script>
index 0cbd1c2..ec61e3d 100644 (file)
@@ -1,5 +1,119 @@
 2017-10-06  Joseph Pecoraro  <pecoraro@apple.com>
 
+        Web Inspector: Network Tab - Headers Detail View
+        https://bugs.webkit.org/show_bug.cgi?id=177896
+        <rdar://problem/34071924>
+
+        Reviewed by Devin Rousso.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Main.html:
+        New resources and strings.
+
+        * UserInterface/Base/URLUtilities.js:
+        (parseURL):
+        (WI.h2Authority):
+        (WI.h2Path):
+        Utility methods to get the :authority and :path pseudo-headers from a URL.
+        This required adding user info (user:pass@) support to URL parsing.
+
+        * UserInterface/Views/NetworkTabContentView.js:
+        (WI.NetworkTabContentView):
+        * UserInterface/Views/NetworkTableContentView.js:
+        (WI.NetworkTableContentView.prototype.get navigationItems):
+        (WI.NetworkTableContentView.prototype.get filterNavigationItems):
+        Move the NetworkTab's filter controls to the left. Since these are not
+        dynamic just vend them from the TableContentView and place them in the
+        navigation bar.
+
+        * UserInterface/Models/Resource.js:
+        (WI.Resource.prototype.updateWithMetrics):
+        New event whenever metrics change. This is the first event that will allow
+        a client to react to a resource.protocol change.
+
+        * UserInterface/Views/NetworkResourceDetailView.css:
+        (.content-view.resource-details):
+        Base styles for the sub detail views.
+
+        * UserInterface/Views/NetworkResourceDetailView.js:
+        (WI.NetworkResourceDetailView):
+        (WI.NetworkResourceDetailView.prototype.headersContentViewGoToRequestData):
+        (WI.NetworkResourceDetailView.prototype.initialLayout):
+        (WI.NetworkResourceDetailView.prototype._showPreferredContentView):
+        (WI.NetworkResourceDetailView.prototype._showContentViewForNavigationItem):
+        Create a Header view and provide a way to switch to a particular view. This wil
+        be useful to jump from the Header's Request Data directly to the Preview's
+        Request ContentView.
+
+        * UserInterface/Views/ResourceDetailsSection.css:
+        (.resource-details > section):
+        (.resource-details > section > .title):
+        (.resource-details > section > .details):
+        (.resource-details > section > .details > p):
+        (.resource-details > section.incomplete > .details):
+        * UserInterface/Views/ResourceDetailsSection.js:
+        (WI.ResourceDetailsSection):
+        (WI.ResourceDetailsSection.prototype.get element):
+        (WI.ResourceDetailsSection.prototype.get titleElement):
+        (WI.ResourceDetailsSection.prototype.get detailsElement):
+        (WI.ResourceDetailsSection.prototype.toggleIncomplete):
+        (WI.ResourceDetailsSection.prototype.toggleError):
+        Simple sections with a title and details div with a border.
+        It may be common to have an incomplete load / error so this
+        provides some APIs and styles for sections marked incomplete
+        or with errors.
+
+        * UserInterface/Views/ResourceHeadersContentView.css:
+        (.resource-headers > section > .details):
+        (.resource-headers > section.headers > .details):
+        (.resource-headers > section.error > .details):
+        (.resource-headers > section.error .key):
+        Style the left border different colors for different sections or cases.
+
+        (.resource-headers .details):
+        (.resource-headers .details .pair):
+        (.resource-headers .details .key):
+        (.resource-headers .value):
+        (.resource-headers .header > .key):
+        (.resource-headers .h1-status > .key):
+        (.resource-headers .h2-pseudo-header > .key):
+        Wrapped text for key/value pairs and different colors for different
+        sections or cases.
+
+        (.resource-headers .go-to-arrow):
+        Go-to arrow styles for a request data section.
+
+        * UserInterface/Views/ResourceHeadersContentView.js: Added.
+        (WI.ResourceHeadersContentView):
+        (WI.ResourceHeadersContentView.prototype.initialLayout):
+        (WI.ResourceHeadersContentView.prototype.layout):
+        (WI.ResourceHeadersContentView.prototype._incompleteSectionWithMessage):
+        (WI.ResourceHeadersContentView.prototype._incompleteSectionWithLoadingIndicator):
+        (WI.ResourceHeadersContentView.prototype._appendKeyValuePair):
+        (WI.ResourceHeadersContentView.prototype._responseSourceDisplayString):
+        (WI.ResourceHeadersContentView.prototype._refreshSummarySection):
+        (WI.ResourceHeadersContentView.prototype._refreshRequestHeadersSection):
+        (WI.ResourceHeadersContentView.prototype._refreshResponseHeadersSection):
+        (WI.ResourceHeadersContentView.prototype._refreshQueryStringSection):
+        (WI.ResourceHeadersContentView.prototype._refreshRequestDataSection):
+        (WI.ResourceHeadersContentView.prototype._resourceMetricsDidChange):
+        (WI.ResourceHeadersContentView.prototype._resourceRequestHeadersDidChange):
+        (WI.ResourceHeadersContentView.prototype._resourceResponseReceived):
+        (WI.ResourceHeadersContentView.prototype._goToRequestDataClicked):
+        Summary, Request, Response, Query String, and Request Data sections.
+        The sections refresh as data becomes available.
+
+        * UserInterface/Views/Table.css:
+        (.table):
+        These variables are already defined globally.
+
+        * UserInterface/Views/Variables.css:
+        (:root):
+        New variables for the colors we use. They closely match, and are
+        sometimes identical to ones used in Timelines / Memory views.
+
+2017-10-06  Joseph Pecoraro  <pecoraro@apple.com>
+
         Web Inspector: Network Tab - Make selection in the table more reliable (mousedown instead of click)
         https://bugs.webkit.org/show_bug.cgi?id=177990
 
index 65697d1..18707e8 100644 (file)
@@ -295,6 +295,7 @@ localizedStrings["Disable Program"] = "Disable Program";
 localizedStrings["Disable all breakpoints (%s)"] = "Disable all breakpoints (%s)";
 localizedStrings["Disable paint flashing"] = "Disable paint flashing";
 localizedStrings["Disabled"] = "Disabled";
+localizedStrings["Disk Cache"] = "Disk Cache";
 localizedStrings["Display"] = "Display";
 localizedStrings["Do not fade unexecuted code"] = "Do not fade unexecuted code";
 localizedStrings["Dock to bottom of window"] = "Dock to bottom of window";
@@ -561,6 +562,7 @@ localizedStrings["Maximum maximum memory size in this recording"] = "Maximum max
 localizedStrings["Media: "] = "Media: ";
 localizedStrings["Medium"] = "Medium";
 localizedStrings["Memory"] = "Memory";
+localizedStrings["Memory Cache"] = "Memory Cache";
 localizedStrings["Memory usage of this canvas"] = "Memory usage of this canvas";
 localizedStrings["Memory: %s"] = "Memory: %s";
 localizedStrings["Message"] = "Message";
@@ -605,6 +607,10 @@ localizedStrings["No Search Results"] = "No Search Results";
 localizedStrings["No Watch Expressions"] = "No Watch Expressions";
 localizedStrings["No matching ARIA role"] = "No matching ARIA role";
 localizedStrings["No preview available"] = "No preview available";
+localizedStrings["No request headers"] = "No request headers";
+localizedStrings["No request, served from the disk cache."] = "No request, served from the disk cache.";
+localizedStrings["No request, served from the memory cache."] = "No request, served from the memory cache.";
+localizedStrings["No response headers"] = "No response headers";
 localizedStrings["Node"] = "Node";
 localizedStrings["Node Removed"] = "Node Removed";
 localizedStrings["Nodes"] = "Nodes";
@@ -876,6 +882,7 @@ localizedStrings["Styles \u2014 Visual"] = "Styles \u2014 Visual";
 localizedStrings["Stylesheet"] = "Stylesheet";
 localizedStrings["Stylesheets"] = "Stylesheets";
 localizedStrings["Subtree Modified"] = "Subtree Modified";
+localizedStrings["Summary"] = "Summary";
 localizedStrings["Tab width:"] = "Tab width:";
 localizedStrings["Tabs"] = "Tabs";
 localizedStrings["Take snapshot"] = "Take snapshot";
index c8ca134..afbd451 100644 (file)
@@ -96,23 +96,24 @@ function parseURL(url)
     url = url ? url.trim() : "";
 
     if (url.startsWith("data:"))
-        return {scheme: "data", host: null, port: null, path: null, queryString: null, fragment: null, lastPathComponent: null};
+        return {scheme: "data", userinfo: null, host: null, port: null, path: null, queryString: null, fragment: null, lastPathComponent: null};
 
-    var match = url.match(/^(?<scheme>[^\/:]+):\/\/(?<host>[^\/#:]*)(?::(?<port>[\d]+))?(?:(?<path>\/[^#]*)?(?:#(?<fragment>.*))?)?$/i);
+    let match = url.match(/^(?<scheme>[^\/:]+):\/\/(?:(?<userinfo>[^#@\/]+)@)?(?<host>[^\/#:]*)(?::(?<port>[\d]+))?(?:(?<path>\/[^#]*)?(?:#(?<fragment>.*))?)?$/i);
     if (!match)
-        return {scheme: null, host: null, port: null, path: null, queryString: null, fragment: null, lastPathComponent: null};
+        return {scheme: null, userinfo: null, host: null, port: null, path: null, queryString: null, fragment: null, lastPathComponent: null};
 
-    var scheme = match.groups.scheme.toLowerCase();
-    var host = match.groups.host.toLowerCase();
-    var port = Number(match.groups.port) || null;
-    var wholePath = match.groups.path || null;
-    var fragment = match.groups.fragment || null;
-    var path = wholePath;
-    var queryString = null;
+    let scheme = match.groups.scheme.toLowerCase();
+    let userinfo = match.groups.userinfo || null;
+    let host = match.groups.host.toLowerCase();
+    let port = Number(match.groups.port) || null;
+    let wholePath = match.groups.path || null;
+    let fragment = match.groups.fragment || null;
+    let path = wholePath;
+    let queryString = null;
 
     // Split the path and the query string.
     if (wholePath) {
-        var indexOfQuery = wholePath.indexOf("?");
+        let indexOfQuery = wholePath.indexOf("?");
         if (indexOfQuery !== -1) {
             path = wholePath.substring(0, indexOfQuery);
             queryString = wholePath.substring(indexOfQuery + 1);
@@ -121,16 +122,16 @@ function parseURL(url)
     }
 
     // Find last path component.
-    var lastPathComponent = null;
+    let lastPathComponent = null;
     if (path && path !== "/") {
         // Skip the trailing slash if there is one.
-        var endOffset = path[path.length - 1] === "/" ? 1 : 0;
-        var lastSlashIndex = path.lastIndexOf("/", path.length - 1 - endOffset);
+        let endOffset = path[path.length - 1] === "/" ? 1 : 0;
+        let lastSlashIndex = path.lastIndexOf("/", path.length - 1 - endOffset);
         if (lastSlashIndex !== -1)
             lastPathComponent = path.substring(lastSlashIndex + 1, path.length - endOffset);
     }
 
-    return {scheme, host, port, path, queryString, fragment, lastPathComponent};
+    return {scheme, userinfo, host, port, path, queryString, fragment, lastPathComponent};
 }
 
 function absoluteURL(partialURL, baseURL)
@@ -263,3 +264,37 @@ WI.displayNameForHost = function(host)
     // FIXME <rdar://problem/11237413>: This should decode punycode hostnames.
     return host;
 };
+
+// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
+WI.h2Authority = function(components)
+{
+    let {scheme, userinfo, host, port} = components;
+    let result = host || "";
+
+    // The authority MUST NOT include the deprecated "userinfo"
+    // subcomponent for "http" or "https" schemed URIs.
+    if (userinfo && (scheme !== "http" && scheme !== "https"))
+        result = userinfo + "@" + result;
+    if (port)
+        result += ":" + port;
+
+    return result;
+};
+
+// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
+WI.h2Path = function(components)
+{
+    let {scheme, path, queryString} = components;
+    let result = path || "";
+
+    // The ":path" pseudo-header field includes the path and query parts
+    // of the target URI. [...] This pseudo-header field MUST NOT be empty
+    // for "http" or "https" URIs; "http" or "https" URIs that do not contain
+    // a path component MUST include a value of '/'.
+    if (!path && (scheme === "http" || scheme === "https"))
+        result = "/";
+    if (queryString)
+        result += "?" + queryString;
+
+    return result;
+};
index e39992b..77a70ae 100644 (file)
     <link rel="stylesheet" href="Views/RenderingFrameTimelineOverviewGraph.css">
     <link rel="stylesheet" href="Views/RenderingFrameTimelineView.css">
     <link rel="stylesheet" href="Views/Resizer.css">
+    <link rel="stylesheet" href="Views/ResourceDetailsSection.css">
     <link rel="stylesheet" href="Views/ResourceDetailsSidebarPanel.css">
+    <link rel="stylesheet" href="Views/ResourceHeadersContentView.css">
     <link rel="stylesheet" href="Views/ResourceIcons.css">
     <link rel="stylesheet" href="Views/ResourceSidebarPanel.css">
     <link rel="stylesheet" href="Views/ResourceTimelineDataGridNode.css">
     <script src="Views/Resizer.js"></script>
     <script src="Views/ResourceClusterContentView.js"></script>
     <script src="Views/ResourceCollectionContentView.js"></script>
+    <script src="Views/ResourceDetailsSection.js"></script>
     <script src="Views/ResourceDetailsSidebarPanel.js"></script>
+    <script src="Views/ResourceHeadersContentView.js"></script>
     <script src="Views/ResourceSidebarPanel.js"></script>
     <script src="Views/ResourceTimelineDataGridNode.js"></script>
     <script src="Views/ResourceTimingPopoverDataGridNode.js"></script>
index 5440d52..215a469 100644 (file)
@@ -721,6 +721,8 @@ WI.Resource = class Resource extends WI.SourceCode
             this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize: this._estimatedSize});
             this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
         }
+
+        this.dispatchEventToListeners(WI.Resource.Event.MetricsDidChange);
     }
 
     setCachedResponseBodySize(size)
@@ -1005,6 +1007,7 @@ WI.Resource.Event = {
     SizeDidChange: "resource-size-did-change",
     TransferSizeDidChange: "resource-transfer-size-did-change",
     CacheStatusDidChange: "resource-cached-did-change",
+    MetricsDidChange: "resource-metrics-did-change",
     InitiatedResourcesDidChange: "resource-initiated-resources-did-change",
 };
 
index 11ec802..5bb14ed 100644 (file)
     right: 0;
     bottom: 0;
 }
+
+.content-view.resource-details {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+
+    padding: 0 20px 20px;
+    overflow: scroll;
+
+    -webkit-user-select: text;
+    white-space: nowrap;
+}
index 0f46eab..d9b1c11 100644 (file)
@@ -37,6 +37,7 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
         this.element.classList.add("network-resource-detail");
 
         this._contentBrowser = null;
+        this._resourceContentView = null;
         this._headersContentView = null;
         this._cookiesContentView = null;
         this._timingContentView = null;
@@ -68,6 +69,15 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
         this._contentBrowser.contentViewContainer.closeAllContentViews();
     }
 
+    // ResourceHeadersContentView delegate
+
+    headersContentViewGoToRequestData(headersContentView)
+    {
+        this._contentBrowser.navigationBar.selectedNavigationItem = this._previewNavigationItem;
+
+        this._resourceContentView.showRequest();
+    }
+
     // Protected
 
     initialLayout()
@@ -84,15 +94,21 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
         const disableFindBanner = false;
         this._contentBrowser = new WI.ContentBrowser(null, this, disableBackForward, disableFindBanner, contentViewNavigationItemsFlexItem, contentViewNavigationItemsGroup);
 
+        this._previewNavigationItem = new WI.RadioButtonNavigationItem("preview", WI.UIString("Preview"));
+        this._headersNavigationItem = new WI.RadioButtonNavigationItem("headers", WI.UIString("Headers"));
+        this._cookiesNavigationItem = new WI.RadioButtonNavigationItem("cookies", WI.UIString("Cookies"));
+        this._timingNavigationItem = new WI.RadioButtonNavigationItem("timing", WI.UIString("Timing"));
+        this._detailsNavigationItem = new WI.RadioButtonNavigationItem("details", WI.UIString("Details"));
+
         // Insert all of our custom navigation items at the start of the ContentBrowser's NavigationBar.
         let index = 0;
         this._contentBrowser.navigationBar.insertNavigationItem(closeNavigationItem, index++);
         this._contentBrowser.navigationBar.insertNavigationItem(new WI.FlexibleSpaceNavigationItem, index++);
-        this._contentBrowser.navigationBar.insertNavigationItem(new WI.RadioButtonNavigationItem("preview", WI.UIString("Preview")), index++);
-        this._contentBrowser.navigationBar.insertNavigationItem(new WI.RadioButtonNavigationItem("headers", WI.UIString("Headers")), index++);
-        this._contentBrowser.navigationBar.insertNavigationItem(new WI.RadioButtonNavigationItem("cookies", WI.UIString("Cookies")), index++);
-        this._contentBrowser.navigationBar.insertNavigationItem(new WI.RadioButtonNavigationItem("timing", WI.UIString("Timing")), index++);
-        this._contentBrowser.navigationBar.insertNavigationItem(new WI.RadioButtonNavigationItem("details", WI.UIString("Details")), index++);
+        this._contentBrowser.navigationBar.insertNavigationItem(this._previewNavigationItem, index++);
+        this._contentBrowser.navigationBar.insertNavigationItem(this._headersNavigationItem, index++);
+        this._contentBrowser.navigationBar.insertNavigationItem(this._cookiesNavigationItem, index++);
+        this._contentBrowser.navigationBar.insertNavigationItem(this._timingNavigationItem, index++);
+        this._contentBrowser.navigationBar.insertNavigationItem(this._detailsNavigationItem, index++);
         this._contentBrowser.navigationBar.addEventListener(WI.NavigationBar.Event.NavigationItemSelected, this._navigationItemSelected, this);
 
         this.addSubview(this._contentBrowser);
@@ -111,6 +127,13 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
             if (!(navigationItem instanceof WI.RadioButtonNavigationItem))
                 continue;
 
+            if (navigationItem !== this._previewNavigationItem
+                && navigationItem !== this._headersNavigationItem
+                && navigationItem !== this._cookiesNavigationItem
+                && navigationItem !== this._timingNavigationItem
+                && navigationItem !== this._detailsNavigationItem)
+                continue;
+
             if (!firstNavigationItem)
                 firstNavigationItem = navigationItem;
 
@@ -128,12 +151,13 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
     {
         switch (navigationItem.identifier) {
         case "preview":
-            this._contentBrowser.showContentViewForRepresentedObject(this._resource);
+            if (!this._resourceContentView)
+                this._resourceContentView = this._contentBrowser.showContentViewForRepresentedObject(this._resource);
+            this._contentBrowser.showContentView(this._resourceContentView);
             break;
         case "headers":
-            // FIXME: Provide a Resource Headers View.
             if (!this._headersContentView)
-                this._headersContentView = new WI.DebugContentView("Headers");
+                this._headersContentView = new WI.ResourceHeadersContentView(this._resource, this);
             this._contentBrowser.showContentView(this._headersContentView);
             break;
         case "cookies":
index 0b4de58..4eaf35a 100644 (file)
@@ -39,6 +39,10 @@ WI.NetworkTabContentView = class NetworkTabContentView extends WI.TabContentView
         this._contentBrowser = new WI.ContentBrowser(null, this, disableBackForward, disableFindBanner);
         this._contentBrowser.showContentView(this._networkTableContentView);
 
+        let filterNavigationItems = this._networkTableContentView.filterNavigationItems;
+        for (let i = 0; i < filterNavigationItems.length; ++i)
+            this._contentBrowser.navigationBar.insertNavigationItem(filterNavigationItems[i], i);
+
         this.addSubview(this._contentBrowser);
     }
 
index b333a3b..134b258 100644 (file)
@@ -133,15 +133,18 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     get navigationItems()
     {
-        let items = [this._typeFilterScopeBar];
-
+        let items = [];
         if (this._disableResourceCacheNavigationItem)
             items.push(this._disableResourceCacheNavigationItem);
         items.push(this._clearNetworkItemsNavigationItem);
-
         return items;
     }
 
+    get filterNavigationItems()
+    {
+        return [this._typeFilterScopeBar];
+    }
+
     shown()
     {
         super.shown();
diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceDetailsSection.css b/Source/WebInspectorUI/UserInterface/Views/ResourceDetailsSection.css
new file mode 100644 (file)
index 0000000..0e4351c
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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-details > section {
+    padding-top: 8px;
+}
+
+.resource-details > section > .title {
+    margin: 10px 0;
+}
+
+.resource-details > section > .details {
+    -webkit-margin-start: 10px;
+}
+
+body[dir=ltr] .resource-details > section > .details {
+    border-left: 2px solid var(--border-color);
+}
+    
+body[dir=rtl] .resource-details > section > .details {
+    border-right: 2px solid var(--border-color);
+}
+
+.resource-details > section > .details > p {
+    margin: 0;
+    padding: 2px 0;
+    -webkit-padding-start: 7px;
+}
+
+.resource-details > section.incomplete > .details {
+    color: var(--console-secondary-text-color) !important;
+    border-color: var(--console-secondary-text-color) !important;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceDetailsSection.js b/Source/WebInspectorUI/UserInterface/Views/ResourceDetailsSection.js
new file mode 100644 (file)
index 0000000..d44ca32
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * 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.ResourceDetailsSection = class ResourceDetailsSection
+{
+    constructor(title, className)
+    {
+        this._element = document.createElement("section");
+        if (className)
+            this._element.className = className;
+
+        this._titleElement = this._element.appendChild(document.createElement("div"));
+        this._titleElement.className = "title";
+        this._titleElement.textContent = title;
+
+        this._detailsElement = this._element.appendChild(document.createElement("div"));
+        this._detailsElement.className = "details";
+    }
+
+    // Public
+
+    get element() { return this._element; }
+    get titleElement() { return this._titleElement; }
+    get detailsElement() { return this._detailsElement; }
+
+    toggleIncomplete(isIncomplete)
+    {
+        console.assert(typeof isIncomplete === "boolean");
+        this.element.classList.toggle("incomplete", isIncomplete);
+    }
+
+    toggleError(isError)
+    {
+        console.assert(typeof isError === "boolean");
+        this.element.classList.toggle("error", isError);
+    }
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceHeadersContentView.css b/Source/WebInspectorUI/UserInterface/Views/ResourceHeadersContentView.css
new file mode 100644 (file)
index 0000000..9475fa2
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+body[dir] .resource-headers > section > .details {
+    border-color: var(--network-system-color);
+}
+
+body[dir] .resource-headers > section.headers > .details {
+    border-color: var(--network-header-color);
+}
+
+body[dir] .resource-headers > section.error > .details {
+    border-color: var(--network-error-color);
+}
+
+.resource-headers > section.error .key {
+    color: var(--network-error-color);
+}
+
+.resource-headers .details {
+    white-space: normal;
+    word-break: break-all;
+}
+
+.resource-headers .details .pair {
+    --resource-headers-value-indent: 15px;
+    -webkit-margin-start: var(--resource-headers-value-indent);
+}
+
+body[dir=rtl] .resource-headers .details .pair {
+    /* Don't include indents in RTL */
+    --resource-headers-value-indent: 0px;
+}
+
+.resource-headers .details .key {
+    color: var(--network-system-color);
+    font-weight: 500;
+    -webkit-margin-start: calc(var(--resource-headers-value-indent) * -1);
+}
+
+.resource-headers .value {
+    color: black;
+}
+
+.resource-headers .header > .key {
+    color: var(--network-header-color);
+}
+
+.resource-headers .h1-status > .key,
+.resource-headers .h2-pseudo-header > .key {
+    color: var(--network-pseudo-header-color);
+}
+
+.resource-headers .go-to-arrow {
+    vertical-align: top;
+    bottom: 1px;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceHeadersContentView.js b/Source/WebInspectorUI/UserInterface/Views/ResourceHeadersContentView.js
new file mode 100644 (file)
index 0000000..197e90a
--- /dev/null
@@ -0,0 +1,335 @@
+/*
+ * 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.ResourceHeadersContentView = class ResourceHeadersContentView extends WI.ContentView
+{
+    constructor(resource, delegate)
+    {
+        super(null);
+
+        console.assert(resource instanceof WI.Resource);
+
+        this._resource = resource;
+        this._resource.addEventListener(WI.Resource.Event.MetricsDidChange, this._resourceMetricsDidChange, this);
+        this._resource.addEventListener(WI.Resource.Event.RequestHeadersDidChange, this._resourceRequestHeadersDidChange, this);
+        this._resource.addEventListener(WI.Resource.Event.ResponseReceived, this._resourceResponseReceived, this);
+
+        this._delegate = delegate || null;
+
+        this.element.classList.add("resource-details", "resource-headers");
+
+        this._needsSummaryRefresh = false;
+        this._needsRequestHeadersRefresh = false;
+        this._needsResponseHeadersRefresh = false;
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        this._summarySection = new WI.ResourceDetailsSection(WI.UIString("Summary"), "summary");
+        this.element.appendChild(this._summarySection.element);
+        this._refreshSummarySection();
+
+        this._requestHeadersSection = new WI.ResourceDetailsSection(WI.UIString("Request"), "headers");
+        this.element.appendChild(this._requestHeadersSection.element);
+        this._refreshRequestHeadersSection();
+
+        // FIXME: <https://webkit.org/b/150005> Web Inspector: Redirect requests are not shown in either Network or Timeline tabs
+
+        this._responseHeadersSection = new WI.ResourceDetailsSection(WI.UIString("Response"), "headers");
+        this.element.appendChild(this._responseHeadersSection.element);
+        this._refreshResponseHeadersSection();
+
+        if (this._resource.urlComponents.queryString) {
+            this._queryStringSection = new WI.ResourceDetailsSection(WI.UIString("Query String"));
+            this.element.appendChild(this._queryStringSection.element);
+            this._refreshQueryStringSection();
+        }
+
+        if (this._resource.requestData) {
+            this._requestDataSection = new WI.ResourceDetailsSection(WI.UIString("Request Data"));
+            this.element.appendChild(this._requestDataSection.element);
+            this._refreshRequestDataSection();
+        }
+
+        this._needsSummaryRefresh = false;
+        this._needsRequestHeadersRefresh = false;
+        this._needsResponseHeadersRefresh = false;
+    }
+
+    layout()
+    {
+        super.layout();
+
+        if (this._needsSummaryRefresh) {
+            this._refreshSummarySection();
+            this._needsSummaryRefresh = false;
+        }
+
+        if (this._needsRequestHeadersRefresh) {
+            this._refreshRequestHeadersSection();
+            this._needsRequestHeadersRefresh = false;
+        }
+
+        if (this._needsResponseHeadersRefresh) {
+            this._refreshResponseHeadersSection();
+            this._needsResponseHeadersRefresh = false;
+        }
+    }
+
+    closed()
+    {
+        this._resource.removeEventListener(null, null, this);
+
+        super.closed();
+    }
+
+    // Private
+
+    _incompleteSectionWithMessage(section, message)
+    {
+        section.toggleIncomplete(true);
+
+        let p = section.detailsElement.appendChild(document.createElement("p"));
+        p.textContent = message;
+    }
+
+    _incompleteSectionWithLoadingIndicator(section)
+    {
+        section.toggleIncomplete(true);
+
+        let p = section.detailsElement.appendChild(document.createElement("p"));
+        let spinner = new WI.IndeterminateProgressSpinner;
+        p.appendChild(spinner.element);
+    }
+
+    _appendKeyValuePair(parentElement, key, value, className)
+    {
+        let p = parentElement.appendChild(document.createElement("p"));
+        p.className = "pair";
+        if (className)
+            p.classList.add(className);
+
+        // Don't include a colon if no value.
+        console.assert(typeof key === "string");
+        let displayKey = key + (!!value ? ": " : "");
+
+        let keyElement = p.appendChild(document.createElement("span"));
+        keyElement.className = "key";
+        keyElement.textContent = displayKey;
+
+        let valueElement = p.appendChild(document.createElement("span"));
+        valueElement.className = "value";
+        if (value instanceof Node)
+            valueElement.appendChild(value);
+        else
+            valueElement.textContent = value;
+    }
+
+    _responseSourceDisplayString(responseSource)
+    {
+        switch (responseSource) {
+        case WI.Resource.ResponseSource.Network:
+            return WI.UIString("Network");
+        case WI.Resource.ResponseSource.MemoryCache:
+            return WI.UIString("Memory Cache");
+        case WI.Resource.ResponseSource.DiskCache:
+            return WI.UIString("Disk Cache");
+        case WI.Resource.ResponseSource.Unknown:
+        default:
+            return null;
+        }
+    }
+
+    _refreshSummarySection()
+    {
+        let detailsElement = this._summarySection.detailsElement;
+        detailsElement.removeChildren();
+
+        this._summarySection.toggleError(this._resource.hadLoadingError());
+
+        this._appendKeyValuePair(detailsElement, WI.UIString("URL"), this._resource.url.insertWordBreakCharacters());
+
+        let status = emDash;
+        if (this._resource.hasResponse())
+            status = this._resource.statusCode + (this._resource.statusText ? " " + this._resource.statusText : "");
+        this._appendKeyValuePair(detailsElement, WI.UIString("Status"), status);
+
+        let source = this._responseSourceDisplayString(this._resource.responseSource) || emDash;
+        this._appendKeyValuePair(detailsElement, WI.UIString("Source"), source);
+    }
+
+    _refreshRequestHeadersSection()
+    {
+        let detailsElement = this._requestHeadersSection.detailsElement;
+        detailsElement.removeChildren();
+
+        // A revalidation request still sends a request even though we served from cache, so show the request.
+        if (this._resource.statusCode !== 304) {
+            if (this._resource.responseSource === WI.Resource.ResponseSource.MemoryCache) {
+                this._incompleteSectionWithMessage(this._requestHeadersSection, WI.UIString("No request, served from the memory cache."));
+                return;
+            }
+            if (this._resource.responseSource === WI.Resource.ResponseSource.DiskCache) {
+                this._incompleteSectionWithMessage(this._requestHeadersSection, WI.UIString("No request, served from the disk cache."));
+                return;
+            }
+        }
+
+        let protocol = this._resource.protocol || "";
+        let urlComponents = this._resource.urlComponents;
+        if (protocol.startsWith("http/1")) {
+            // HTTP/1.1 request line:
+            // https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1
+            let requestLine = `${this._resource.requestMethod} ${urlComponents.path} ${protocol.toUpperCase()}`
+            this._appendKeyValuePair(detailsElement, requestLine, null, "h1-status");
+        } else if (protocol === "h2") {
+            // HTTP/2 Request pseudo headers:
+            // https://tools.ietf.org/html/rfc7540#section-8.1.2.3
+            this._appendKeyValuePair(detailsElement, ":method", this._resource.requestMethod, "h2-pseudo-header");
+            this._appendKeyValuePair(detailsElement, ":scheme", urlComponents.scheme, "h2-pseudo-header");
+            this._appendKeyValuePair(detailsElement, ":authority", WI.h2Authority(urlComponents), "h2-pseudo-header");
+            this._appendKeyValuePair(detailsElement, ":path", WI.h2Path(urlComponents), "h2-pseudo-header");
+        }
+
+        let requestHeaders = this._resource.requestHeaders;
+        for (let key in requestHeaders)
+            this._appendKeyValuePair(detailsElement, key, requestHeaders[key], "header");
+
+        if (!detailsElement.firstChild)
+            this._incompleteSectionWithMessage(this._requestHeadersSection, WI.UIString("No request headers"));
+    }
+
+    _refreshResponseHeadersSection()
+    {
+        let detailsElement = this._responseHeadersSection.detailsElement;
+        detailsElement.removeChildren();
+
+        if (!this._resource.hasResponse()) {
+            this._incompleteSectionWithLoadingIndicator(this._responseHeadersSection);
+            return;
+        }
+
+        this._responseHeadersSection.toggleIncomplete(false);
+
+        let protocol = this._resource.protocol || "";
+        if (protocol.startsWith("http/1")) {
+            // HTTP/1.1 response status line:
+            // https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
+            let responseLine = `${protocol.toUpperCase()} ${this._resource.statusCode} ${this._resource.statusText}`;
+            this._appendKeyValuePair(detailsElement, responseLine, null, "h1-status");
+        } else if (protocol === "h2") {
+            // HTTP/2 Response pseudo headers:
+            // https://tools.ietf.org/html/rfc7540#section-8.1.2.4
+            this._appendKeyValuePair(detailsElement, ":status", this._resource.statusCode, "h2-pseudo-header");
+        }
+
+        let responseHeaders = this._resource.responseHeaders;
+        for (let key in responseHeaders)
+            this._appendKeyValuePair(detailsElement, key, responseHeaders[key], "header");
+
+        if (!detailsElement.firstChild)
+            this._incompleteSectionWithMessage(this._responseHeadersSection, WI.UIString("No response headers"));
+    }
+
+    _refreshQueryStringSection()
+    {
+        if (!this._queryStringSection)
+            return;
+
+        let detailsElement = this._queryStringSection.detailsElement;
+        detailsElement.removeChildren();
+
+        let queryString = this._resource.urlComponents.queryString;
+        let queryStringPairs = parseQueryString(queryString, true);
+        for (let {name, value} of queryStringPairs)
+            this._appendKeyValuePair(detailsElement, name, value);
+    }
+
+    _refreshRequestDataSection()
+    {
+        if (!this._requestDataSection)
+            return;
+
+        let detailsElement = this._requestDataSection.detailsElement;
+        detailsElement.removeChildren();
+
+        let requestData = this._resource.requestData;
+        let requestDataContentType = this._resource.requestDataContentType || "";
+
+        if (requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) {
+            // Simple form data that should be parsable like a query string.
+            this._appendKeyValuePair(detailsElement, WI.UIString("MIME Type"), requestDataContentType);
+            let queryStringPairs = parseQueryString(requestData, true)
+            for (let {name, value} of queryStringPairs)
+                this._appendKeyValuePair(detailsElement, name, value);
+            return;
+        }
+
+        let mimeTypeComponents = parseMIMEType(requestDataContentType);
+        let mimeType = mimeTypeComponents.type;
+        let boundary = mimeTypeComponents.boundary;
+        let encoding = mimeTypeComponents.encoding;
+
+        this._appendKeyValuePair(detailsElement, WI.UIString("MIME Type"), mimeType);
+        if (boundary)
+            this._appendKeyValuePair(detailsElement, WI.UIString("Boundary"), boundary);
+        if (encoding)
+            this._appendKeyValuePair(detailsElement, WI.UIString("Encoding"), encoding);
+
+        let goToButton = detailsElement.appendChild(WI.createGoToArrowButton());
+        goToButton.addEventListener("click", this._goToRequestDataClicked.bind(this));
+        this._appendKeyValuePair(detailsElement, WI.UIString("Request Data"), goToButton);
+    }
+
+    _resourceMetricsDidChange(event)
+    {
+        this._needsRequestHeadersRefresh = true;
+        this._needsResponseHeadersRefresh = true;
+        this.needsLayout();
+    }
+
+    _resourceRequestHeadersDidChange(event)
+    {
+        this._needsRequestHeadersRefresh = true;
+        this.needsLayout();
+    }
+
+    _resourceResponseReceived(event)
+    {
+        this._needsSummaryRefresh = true;
+        this._needsResponseHeadersRefresh = true;
+        this.needsLayout();
+    }
+
+    _goToRequestDataClicked(event)
+    {
+        if (this._delegate)
+            this._delegate.headersContentViewGoToRequestData(this);
+    }
+};
index 3d84ae9..c2dd22f 100644 (file)
@@ -30,8 +30,6 @@
     height: 100%;
     background: white;
 
-    --even-zebra-stripe-row-background-color: white;
-    --odd-zebra-stripe-row-background-color: hsl(0, 0%, 95%);
     --table-column-border-start: 1px solid transparent;
     --table-column-border-end: 0.5px solid var(--border-color);
 }
index d784491..caa9ad4 100644 (file)
     --memory-max-comparison-fill-color: hsl(220, 10%, 75%);
     --memory-max-comparison-stroke-color: hsl(220, 10%, 55%);
 
+    --network-header-color: hsl(204, 52%, 55%);
+    --network-system-color: hsl(79, 32%, 50%);
+    --network-pseudo-header-color: hsl(312, 35%, 51%);
+    --network-error-color: hsl(0, 54%, 50%);
+
     --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;