Bug 1317649 - Implement Cookies Panel;r=jsnajdr,honza draft
authorFred Lin <gasolin@mozilla.com>
Thu, 24 Nov 2016 17:05:31 +0800
changeset 458387 0364b99a2b84e34d0a43cf4f20a03372b936dea3
parent 458377 43a0f30ae01825f3133312c769091a9f39af660a
child 541631 1963a3375b0b530a5d398aee6fb25f9fa7a96e1f
push id40938
push userbmo:gasolin@mozilla.com
push dateTue, 10 Jan 2017 07:26:08 +0000
reviewersjsnajdr, honza
bugs1317649
milestone53.0a1
Bug 1317649 - Implement Cookies Panel;r=jsnajdr,honza use properties-view show other Cookie properties MozReview-Commit-ID: Lo825FFW5ET
devtools/client/netmonitor/actions/filters.js
devtools/client/netmonitor/components/search-box.js
devtools/client/netmonitor/components/toolbar.js
devtools/client/netmonitor/constants.js
devtools/client/netmonitor/details-view.js
devtools/client/netmonitor/netmonitor.xul
devtools/client/netmonitor/reducers/filters.js
devtools/client/netmonitor/requests-menu-view.js
devtools/client/netmonitor/selectors/requests.js
devtools/client/netmonitor/shared/components/cookies-panel.js
devtools/client/netmonitor/shared/components/moz.build
devtools/client/themes/netmonitor.css
--- a/devtools/client/netmonitor/actions/filters.js
+++ b/devtools/client/netmonitor/actions/filters.js
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
+  ENABLE_REQUEST_FILTER_TYPE_ONLY,
   TOGGLE_REQUEST_FILTER_TYPE,
-  ENABLE_REQUEST_FILTER_TYPE_ONLY,
   SET_REQUEST_FILTER_TEXT,
 } = require("../constants");
 
 /**
  * Toggle an existing filter type state.
  * If type 'all' is specified, all the other filter types are set to false.
  * Available filter types are defined in filters reducer.
  *
@@ -35,24 +35,24 @@ function toggleRequestFilterType(filter)
 function enableRequestFilterTypeOnly(filter) {
   return {
     type: ENABLE_REQUEST_FILTER_TYPE_ONLY,
     filter,
   };
 }
 
 /**
- * Set filter text.
+ * Set filter text in toolbar.
  *
  * @param {string} text - A filter text is going to be set
  */
 function setRequestFilterText(text) {
   return {
     type: SET_REQUEST_FILTER_TEXT,
     text,
   };
 }
 
 module.exports = {
+  enableRequestFilterTypeOnly,
   toggleRequestFilterType,
-  enableRequestFilterTypeOnly,
   setRequestFilterText,
 };
--- a/devtools/client/netmonitor/components/search-box.js
+++ b/devtools/client/netmonitor/components/search-box.js
@@ -3,23 +3,21 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const SearchBox = require("devtools/client/shared/components/search-box");
 const { L10N } = require("../l10n");
 const Actions = require("../actions/index");
-const { FREETEXT_FILTER_SEARCH_DELAY } = require("../constants");
+const { FILTER_SEARCH_DELAY } = require("../constants");
 
 module.exports = connect(
   (state) => ({
-    delay: FREETEXT_FILTER_SEARCH_DELAY,
+    delay: FILTER_SEARCH_DELAY,
     keyShortcut: L10N.getStr("netmonitor.toolbar.filterFreetext.key"),
     placeholder: L10N.getStr("netmonitor.toolbar.filterFreetext.label"),
     type: "filter",
   }),
   (dispatch) => ({
-    onChange: (url) => {
-      dispatch(Actions.setRequestFilterText(url));
-    },
+    onChange: text => dispatch(Actions.setRequestFilterText(text)),
   })
 )(SearchBox);
--- a/devtools/client/netmonitor/components/toolbar.js
+++ b/devtools/client/netmonitor/components/toolbar.js
@@ -5,17 +5,17 @@
 "use strict";
 
 const {
   createFactory,
   DOM,
 } = require("devtools/client/shared/vendor/react");
 const ClearButton = createFactory(require("./clear-button"));
 const FilterButtons = createFactory(require("./filter-buttons"));
-const SearchBox = createFactory(require("./search-box"));
+const ToolbarSearchBox = createFactory(require("./search-box"));
 const SummaryButton = createFactory(require("./summary-button"));
 const ToggleButton = createFactory(require("./toggle-button"));
 
 const { span } = DOM;
 
 /*
  * Network monitor toolbar component
  * Toolbar contains a set of useful tools to control network requests
@@ -23,15 +23,15 @@ const { span } = DOM;
 function Toolbar() {
   return span({ className: "devtools-toolbar devtools-toolbar-container" },
     span({ className: "devtools-toolbar-group" },
       ClearButton(),
       FilterButtons()
     ),
     span({ className: "devtools-toolbar-group" },
       SummaryButton(),
-      SearchBox(),
+      ToolbarSearchBox(),
       ToggleButton()
     )
   );
 }
 
 module.exports = Toolbar;
--- a/devtools/client/netmonitor/constants.js
+++ b/devtools/client/netmonitor/constants.js
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const general = {
   CONTENT_SIZE_DECIMALS: 2,
-  FREETEXT_FILTER_SEARCH_DELAY: 200,
+  FILTER_SEARCH_DELAY: 200,
   REQUEST_TIME_DECIMALS: 2,
 };
 
 const actionTypes = {
   ADD_REQUEST: "ADD_REQUEST",
   ADD_TIMING_MARKER: "ADD_TIMING_MARKER",
   BATCH_ACTIONS: "BATCH_ACTIONS",
   BATCH_ENABLE: "BATCH_ENABLE",
--- a/devtools/client/netmonitor/details-view.js
+++ b/devtools/client/netmonitor/details-view.js
@@ -24,16 +24,17 @@ const {
   getFormDataSections,
   getUrlBaseName,
   getUrlQuery,
   parseQueryString,
 } = require("./request-utils");
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+const CookiesPanel = createFactory(require("./shared/components/cookies-panel"));
 const PreviewPanel = createFactory(require("./shared/components/preview-panel"));
 const SecurityPanel = createFactory(require("./shared/components/security-panel"));
 const TimingsPanel = createFactory(require("./shared/components/timings-panel"));
 
 // 100 KB in bytes
 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400;
 const HEADERS_SIZE_DECIMALS = 3;
 const CONTENT_MIME_TYPE_MAPPINGS = {
@@ -89,16 +90,23 @@ DetailsView.prototype = {
   },
 
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function (store) {
     dumpn("Initializing the DetailsView");
 
+    this._cookiesPanelNode = $("#react-cookies-tabpanel-hook");
+
+    ReactDOM.render(Provider(
+      { store },
+      CookiesPanel()
+    ), this._cookiesPanelNode);
+
     this._previewPanelNode = $("#react-preview-tabpanel-hook");
 
     ReactDOM.render(Provider(
       { store },
       PreviewPanel()
     ), this._previewPanelNode);
 
     this._securityPanelNode = $("#react-security-tabpanel-hook");
@@ -121,21 +129,16 @@ DetailsView.prototype = {
       showAllTabsMenu: true
     });
 
     this._headers = new VariablesView($("#all-headers"),
       Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
         emptyText: L10N.getStr("headersEmptyText"),
         searchPlaceholder: L10N.getStr("headersFilterText")
       }));
-    this._cookies = new VariablesView($("#all-cookies"),
-      Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
-        emptyText: L10N.getStr("cookiesEmptyText"),
-        searchPlaceholder: L10N.getStr("cookiesFilterText")
-      }));
     this._params = new VariablesView($("#request-params"),
       Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
         emptyText: L10N.getStr("paramsEmptyText"),
         searchPlaceholder: L10N.getStr("paramsFilterText")
       }));
     this._json = new VariablesView($("#response-content-json"),
       Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
         onlyEnumVisible: true,
@@ -144,27 +147,26 @@ DetailsView.prototype = {
     VariablesViewController.attach(this._json);
 
     this._paramsQueryString = L10N.getStr("paramsQueryString");
     this._paramsFormData = L10N.getStr("paramsFormData");
     this._paramsPostPayload = L10N.getStr("paramsPostPayload");
     this._requestHeaders = L10N.getStr("requestHeaders");
     this._requestHeadersFromUpload = L10N.getStr("requestHeadersFromUpload");
     this._responseHeaders = L10N.getStr("responseHeaders");
-    this._requestCookies = L10N.getStr("requestCookies");
-    this._responseCookies = L10N.getStr("responseCookies");
 
     $("tabpanels", this.widget).addEventListener("select", this._onTabSelect);
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
    */
   destroy: function () {
     dumpn("Destroying the DetailsView");
+    ReactDOM.unmountComponentAtNode(this._cookiesPanelNode);
     ReactDOM.unmountComponentAtNode(this._previewPanelNode);
     ReactDOM.unmountComponentAtNode(this._securityPanelNode);
     ReactDOM.unmountComponentAtNode(this._timingsPanelNode);
     this.sidebar.destroy();
     $("tabpanels", this.widget).removeEventListener("select",
       this._onTabSelect);
   },
 
@@ -204,17 +206,16 @@ DetailsView.prototype = {
 
     if (!isHtml && this.widget.selectedPanel === $("#preview-tabpanel") ||
         !hasSecurityInfo && this.widget.selectedPanel ===
           $("#security-tabpanel")) {
       this.widget.selectedIndex = 0;
     }
 
     this._headers.empty();
-    this._cookies.empty();
     this._params.empty();
     this._json.empty();
 
     this._dataSrc = { src: data, populated: [] };
     this._onTabSelect();
     window.emit(EVENTS.NETWORKDETAILSVIEW_POPULATED);
 
     return promise.resolve();
@@ -250,21 +251,16 @@ DetailsView.prototype = {
         // "Headers"
         case 0:
           yield view._setSummary(src);
           yield view._setResponseHeaders(src.responseHeaders);
           yield view._setRequestHeaders(
             src.requestHeaders,
             src.requestHeadersFromUploadStream);
           break;
-        // "Cookies"
-        case 1:
-          yield view._setResponseCookies(src.responseCookies);
-          yield view._setRequestCookies(src.requestCookies);
-          break;
         // "Params"
         case 2:
           yield view._setRequestGetParams(src.url);
           yield view._setRequestPostParams(
             src.requestHeaders,
             src.requestHeadersFromUploadStream,
             src.requestPostData);
           break;
@@ -413,84 +409,16 @@ DetailsView.prototype = {
     for (let header of response.headers) {
       let headerVar = headersScope.addItem(header.name, {}, {relaxed: true});
       let headerValue = yield gNetwork.getString(header.value);
       headerVar.setGrip(headerValue);
     }
   }),
 
   /**
-   * Sets the network request cookies shown in this view.
-   *
-   * @param object response
-   *        The message received from the server.
-   * @return object
-   *        A promise that is resolved when the request cookies are set.
-   */
-  _setRequestCookies: Task.async(function* (response) {
-    if (response && response.cookies.length) {
-      response.cookies.sort((a, b) => a.name > b.name);
-      yield this._addCookies(this._requestCookies, response);
-    }
-  }),
-
-  /**
-   * Sets the network response cookies shown in this view.
-   *
-   * @param object response
-   *        The message received from the server.
-   * @return object
-   *        A promise that is resolved when the response cookies are set.
-   */
-  _setResponseCookies: Task.async(function* (response) {
-    if (response && response.cookies.length) {
-      yield this._addCookies(this._responseCookies, response);
-    }
-  }),
-
-  /**
-   * Populates the cookies container in this view with the specified data.
-   *
-   * @param string name
-   *        The type of cookies to populate (request or response).
-   * @param object response
-   *        The message received from the server.
-   * @return object
-   *        Returns a promise that resolves upon the adding of cookies.
-   */
-  _addCookies: Task.async(function* (name, response) {
-    let cookiesScope = this._cookies.addScope(name);
-    cookiesScope.expanded = true;
-
-    for (let cookie of response.cookies) {
-      let cookieVar = cookiesScope.addItem(cookie.name, {}, {relaxed: true});
-      let cookieValue = yield gNetwork.getString(cookie.value);
-      cookieVar.setGrip(cookieValue);
-
-      // By default the cookie name and value are shown. If this is the only
-      // information available, then nothing else is to be displayed.
-      let cookieProps = Object.keys(cookie);
-      if (cookieProps.length == 2) {
-        continue;
-      }
-
-      // Display any other information other than the cookie name and value
-      // which may be available.
-      let rawObject = Object.create(null);
-      let otherProps = cookieProps.filter(e => e != "name" && e != "value");
-      for (let prop of otherProps) {
-        rawObject[prop] = cookie[prop];
-      }
-      cookieVar.populate(rawObject);
-      cookieVar.twisty = true;
-      cookieVar.expanded = true;
-    }
-  }),
-
-  /**
    * Sets the network request get params shown in this view.
    *
    * @param string url
    *        The request's url.
    */
   _setRequestGetParams: function (url) {
     let query = getUrlQuery(url);
     if (query) {
@@ -699,21 +627,18 @@ DetailsView.prototype = {
       }
     }
 
     window.emit(EVENTS.RESPONSE_BODY_DISPLAYED);
   }),
 
   _dataSrc: null,
   _headers: null,
-  _cookies: null,
   _params: null,
   _json: null,
   _paramsQueryString: "",
   _paramsFormData: "",
   _paramsPostPayload: "",
   _requestHeaders: "",
-  _responseHeaders: "",
-  _requestCookies: "",
-  _responseCookies: ""
+  _responseHeaders: ""
 };
 
 exports.DetailsView = DetailsView;
--- a/devtools/client/netmonitor/netmonitor.xul
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -202,17 +202,18 @@
                     </vbox>
                   </hbox>
                   <vbox id="all-headers" flex="1"/>
                 </vbox>
               </tabpanel>
               <tabpanel id="cookies-tabpanel"
                         class="tabpanel-content">
                 <vbox flex="1">
-                  <vbox id="all-cookies" flex="1"/>
+                  <html:div xmlns="http://www.w3.org/1999/xhtml"
+                      id="react-cookies-tabpanel-hook"/>
                 </vbox>
               </tabpanel>
               <tabpanel id="params-tabpanel"
                         class="tabpanel-content">
                 <vbox flex="1">
                   <vbox id="request-params-box" flex="1" hidden="true">
                     <vbox id="request-params" flex="1"/>
                   </vbox>
--- a/devtools/client/netmonitor/reducers/filters.js
+++ b/devtools/client/netmonitor/reducers/filters.js
@@ -1,18 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const I = require("devtools/client/shared/vendor/immutable");
 const {
+  ENABLE_REQUEST_FILTER_TYPE_ONLY,
   TOGGLE_REQUEST_FILTER_TYPE,
-  ENABLE_REQUEST_FILTER_TYPE_ONLY,
   SET_REQUEST_FILTER_TEXT,
 } = require("../constants");
 
 const FilterTypes = I.Record({
   all: false,
   html: false,
   css: false,
   js: false,
@@ -23,16 +23,17 @@ const FilterTypes = I.Record({
   flash: false,
   ws: false,
   other: false,
 });
 
 const Filters = I.Record({
   requestFilterTypes: new FilterTypes({ all: true }),
   requestFilterText: "",
+  cookiesFilterText: "",
 });
 
 function toggleRequestFilterType(state, action) {
   let { filter } = action;
   let newState;
 
   // Ignore unknown filter type
   if (!state.has(filter)) {
@@ -62,22 +63,22 @@ function enableRequestFilterTypeOnly(sta
     return state;
   }
 
   return new FilterTypes({ [filter]: true });
 }
 
 function filters(state = new Filters(), action) {
   switch (action.type) {
+    case ENABLE_REQUEST_FILTER_TYPE_ONLY:
+      return state.set("requestFilterTypes",
+        enableRequestFilterTypeOnly(state.requestFilterTypes, action));
     case TOGGLE_REQUEST_FILTER_TYPE:
       return state.set("requestFilterTypes",
         toggleRequestFilterType(state.requestFilterTypes, action));
-    case ENABLE_REQUEST_FILTER_TYPE_ONLY:
-      return state.set("requestFilterTypes",
-        enableRequestFilterTypeOnly(state.requestFilterTypes, action));
     case SET_REQUEST_FILTER_TEXT:
       return state.set("requestFilterText", action.text);
     default:
       return state;
   }
 }
 
 module.exports = filters;
--- a/devtools/client/netmonitor/requests-menu-view.js
+++ b/devtools/client/netmonitor/requests-menu-view.js
@@ -17,26 +17,26 @@ const ReactDOM = require("devtools/clien
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 const RequestList = createFactory(require("./components/request-list"));
 const RequestListContextMenu = require("./request-list-context-menu");
 const Actions = require("./actions/index");
 const { Prefs } = require("./prefs");
 
 const {
   formDataURI,
+  loadCauseString,
   writeHeaderText,
-  loadCauseString
 } = require("./request-utils");
 
 const {
   getActiveFilters,
-  getSortedRequests,
   getDisplayedRequests,
   getRequestById,
-  getSelectedRequest
+  getSelectedRequest,
+  getSortedRequests,
 } = require("./selectors/index");
 
 // ms
 const RESIZE_REFRESH_RATE = 50;
 
 // A smart store watcher to notify store changes as necessary
 function storeWatcher(initialValue, reduceValue, onChange) {
   let currentValue = initialValue;
@@ -187,52 +187,90 @@ RequestsMenuView.prototype = {
       id,
       {
         startedMillis,
         method,
         url,
         isXHR,
         cause,
         fromCache,
-        fromServiceWorker
+        fromServiceWorker,
       },
       true
     );
 
     this.store.dispatch(action).then(() => window.emit(EVENTS.REQUEST_ADDED, action.id));
   },
 
+  // Fetch response cookies long value.
+  // Actor does not provide full sized cookie value when the value is too long
+  // To display values correctly, we need fetch them in each request.
+  fetchResponseCookiesLongValue: function* (request) {
+    let { responseCookies } = request;
+    if (!responseCookies || !responseCookies.cookies) {
+      return null;
+    }
+
+    let rspCookies = [];
+    // response store cookies in responseCookies or responseCookies.cookies
+    let cookies = responseCookies.cookies ? responseCookies.cookies : responseCookies;
+    for (let cookie of cookies) {
+      rspCookies.push(Object.assign({}, cookie, {
+        value: yield gNetwork.getString(cookie.value),
+      }));
+    }
+    return rspCookies.length ? { responseCookies: rspCookies } : null;
+  },
+
+  // Fetch request cookies long value.
+  // Actor does not provide full sized cookie value when the value is too long
+  // To display values correctly, we need fetch them in each request.
+  fetchRequestCookiesLongValue: function* (request) {
+    let { requestCookies } = request;
+    if (!requestCookies || !requestCookies.cookies) {
+      return null;
+    }
+
+    let reqCookies = [];
+    // request store cookies in requestCookies or requestCookies.cookies
+    let cookies = requestCookies.cookies ? requestCookies.cookies : requestCookies;
+    for (let cookie of cookies) {
+      reqCookies.push(Object.assign({}, cookie, {
+        value: yield gNetwork.getString(cookie.value),
+      }));
+    }
+    return reqCookies.length ? { requestCookies: reqCookies } : null;
+  },
+
   updateRequest: Task.async(function* (id, data) {
     const action = Actions.updateRequest(id, data, true);
     yield this.store.dispatch(action);
 
     let { responseContent, requestPostData } = action.data;
+    let request = getRequestById(this.store.getState(), action.id);
 
-    if (responseContent && responseContent.content) {
-      let request = getRequestById(this.store.getState(), action.id);
-      if (request) {
-        let { mimeType } = request;
-        let { text, encoding } = responseContent.content;
-        let response = yield gNetwork.getString(text);
-        let payload = {};
+    if (request && responseContent && responseContent.content) {
+      let { mimeType } = request;
+      let { text, encoding } = responseContent.content;
+      let response = yield gNetwork.getString(text);
+      let payload = {};
 
-        if (mimeType.includes("image/")) {
-          payload.responseContentDataUri = formDataURI(mimeType, encoding, response);
-        }
+      if (mimeType.includes("image/")) {
+        payload.responseContentDataUri = formDataURI(mimeType, encoding, response);
+      }
 
-        if (mimeType.includes("text/")) {
-          responseContent.content.text = response;
-          payload.responseContent = responseContent;
-        }
+      if (mimeType.includes("text/")) {
+        responseContent.content.text = response;
+        payload.responseContent = responseContent;
+      }
 
-        yield this.store.dispatch(Actions.updateRequest(action.id, payload, true));
+      yield this.store.dispatch(Actions.updateRequest(action.id, payload, true));
 
-        if (mimeType.includes("image/")) {
-          window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
-        }
+      if (mimeType.includes("image/")) {
+        window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
       }
     }
 
     // Search the POST data upload stream for request headers and add
     // them as a separate property, different from the classic headers.
     if (requestPostData && requestPostData.postData) {
       let { text } = requestPostData.postData;
       let postData = yield gNetwork.getString(text);
@@ -242,16 +280,31 @@ RequestsMenuView.prototype = {
       }, 0);
       let payload = {};
       requestPostData.postData.text = postData;
       payload.requestPostData = Object.assign({}, requestPostData);
       payload.requestHeadersFromUploadStream = { headers, headersSize };
 
       yield this.store.dispatch(Actions.updateRequest(action.id, payload, true));
     }
+
+    if (request) {
+      // Fetch responseCookies long value when any response cookie is exist
+      let responseObj = yield this.fetchResponseCookiesLongValue(request);
+      if (responseObj) {
+        yield this.store.dispatch(Actions.updateRequest(
+          action.id, responseObj, true));
+      }
+      // Fetch requestCookies long value when any request cookie is exist
+      let requestObj = yield this.fetchRequestCookiesLongValue(request);
+      if (requestObj) {
+        yield this.store.dispatch(Actions.updateRequest(
+          action.id, requestObj, true));
+      }
+    }
   }),
 
   /**
    * Disable batched updates. Used by tests.
    */
   set lazyUpdate(value) {
     this.store.dispatch(Actions.batchEnable(value));
   },
--- a/devtools/client/netmonitor/selectors/requests.js
+++ b/devtools/client/netmonitor/selectors/requests.js
@@ -99,24 +99,60 @@ const getSelectedRequest = createSelecto
     if (!requests.selectedId) {
       return null;
     }
 
     return requests.requests.find(r => r.id === requests.selectedId);
   }
 );
 
+const getSelectedResponseCookies = createSelector(
+  getSelectedRequest,
+  selectedRequest => {
+    if (!selectedRequest) {
+      return [];
+    }
+
+    // response store cookies in responseCookies or responseCookies.cookies
+    if (selectedRequest.responseCookies) {
+      return selectedRequest.responseCookies.cookies ?
+        selectedRequest.responseCookies.cookies : selectedRequest.responseCookies;
+    }
+
+    return [];
+  }
+);
+
+const getSelectedRequestCookies = createSelector(
+  getSelectedRequest,
+  selectedRequest => {
+    if (!selectedRequest) {
+      return [];
+    }
+
+    // request store cookies in requestCookies or requestCookies.cookies
+    if (selectedRequest.requestCookies) {
+      return selectedRequest.requestCookies.cookies ?
+        selectedRequest.requestCookies.cookies : selectedRequest.requestCookies;
+    }
+
+    return [];
+  }
+);
+
 function getRequestById(state, id) {
   return state.requests.requests.find(r => r.id === id);
 }
 
 function getDisplayedRequestById(state, id) {
   return getDisplayedRequests(state).find(r => r.id === id);
 }
 
 module.exports = {
   getDisplayedRequestById,
   getDisplayedRequests,
   getDisplayedRequestsSummary,
   getRequestById,
   getSelectedRequest,
+  getSelectedResponseCookies,
+  getSelectedRequestCookies,
   getSortedRequests,
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/shared/components/cookies-panel.js
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { L10N } = require("../../l10n");
+const { getSelectedResponseCookies, getSelectedRequestCookies } = require("../../selectors/index");
+
+const { div, input } = DOM;
+const PropertiesView = createFactory(require("./properties-view"));
+
+const COOKIES_EMPTY_TEXT = L10N.getStr("cookiesEmptyText");
+const COOKIES_FILTER_TEXT = L10N.getStr("cookiesFilterText");
+const COOKIES_RESPONSE_COOKIES_TEXT = L10N.getStr("responseCookies");
+const COOKIES_REQUEST_COOKIES_TEXT = L10N.getStr("requestCookies");
+const SECTION_NAMES = [
+  COOKIES_RESPONSE_COOKIES_TEXT,
+  COOKIES_REQUEST_COOKIES_TEXT,
+];
+
+/**
+ * This template represents 'Cookies' panel displayed when the user
+ * tap the request menu item in the network panel. It's responsible for rendering
+ * sent and received cookies.
+ */
+function CookiesPanel({
+  request,
+  response,
+}) {
+  if (response.length === 0 && request.length === 0) {
+    return div({ className: "empty-notice" },
+      COOKIES_EMPTY_TEXT
+    );
+  }
+
+  let object = {};
+  if (response.length) {
+    object[COOKIES_RESPONSE_COOKIES_TEXT] = arrayToDict(response);
+  }
+  if (request.length) {
+    object[COOKIES_REQUEST_COOKIES_TEXT] = arrayToDict(request);
+  }
+
+  return (
+    PropertiesView({
+      object,
+      filterPlaceHolder: COOKIES_FILTER_TEXT,
+      sectionNames: SECTION_NAMES,
+    })
+  );
+}
+
+CookiesPanel.displayName = "CookiesPanel";
+
+CookiesPanel.propTypes = {
+  request: PropTypes.array.isRequired,
+  response: PropTypes.array.isRequired,
+};
+
+/**
+ * Mapping array to dict for TreeView usage.
+ * Since TreeView only support Object(dict) format.
+ *
+ * @param {Object[]} arr - key-value pair array like cookies or params
+ * @returns {Object}
+ */
+function arrayToDict(arr) {
+  return arr.reduce(function (map, obj) {
+    // Display any other information other than the cookie name and value
+    // which may be available.
+    if (Object.keys(obj).length > 2) {
+      map[obj.name] = Object.assign({}, obj);
+      delete map[obj.name].name;
+    } else {
+      map[obj.name] = obj.value;
+    }
+    return map;
+  }, {});
+}
+
+module.exports = connect(
+  state => ({
+    response: getSelectedResponseCookies(state),
+    request: getSelectedRequestCookies(state),
+  })
+)(CookiesPanel);
--- a/devtools/client/netmonitor/shared/components/moz.build
+++ b/devtools/client/netmonitor/shared/components/moz.build
@@ -1,11 +1,12 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'cookies-panel.js',
     'editor.js',
     'preview-panel.js',
     'properties-view.js',
     'security-panel.js',
     'timings-panel.js',
 )
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -1127,18 +1127,31 @@
   display: flex;
   flex-direction: column;
 }
 
 .properties-view .searchbox-section {
   flex: 0 1 auto;
 }
 
-.properties-view .devtools-searchbox {
-  padding: 0;
+.theme-light .treeTable .treeValueCell .textbox-input {
+  color: var(--theme-highlight-purple)
+}
+
+.theme-dark .treeTable .treeValueCell .textbox-input {
+  color: var(--theme-highlight-gray);
+}
+
+.theme-light .treeTable .treeValueCell .textbox-input:focus,
+.theme-dark .treeTable .treeValueCell .textbox-input:focus {
+  color: inherit;
+}
+
+.theme-firebug .treeTable .treeLabel {
+  color: var(--theme-body-color);
 }
 
 .properties-view .devtools-searchbox input {
   margin: 1px 3px;
 }
 
 .tree-container {
   position: relative;
@@ -1184,31 +1197,75 @@
 .editor-container,
 .editor-mount,
 .editor-mount iframe {
   border: none;
   width: 100%;
   height: 100%;
 }
 
+.detailsTreeView .treeTable {
+  display: block;
+  overflow-y: auto;
+   /* Minus 72px * 3 for toolbox height + tabpanel height + searchbox height */
+   max-height: calc(100vh - 72px);
+}
+
+.detailsTreeView .devtools-searchbox input {
+  margin-inline-end: 6px;
+}
+
+.detailsTreeView .devtools-searchbox,
+.detailsTreeView .treeTable .treeSection {
+  width: 100%;
+  background-color: var(--theme-toolbar-background);
+}
+
+.detailsTreeView .treeTable .treeSection {
+  height: 22px;
+}
+
+.detailsTreeView .devtools-searchbox,
+.detailsTreeView .treeTable tr:not(:last-child) td:not(:empty) {
+  border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.detailsTreeView .treeTable .treeSection > * {
+  vertical-align: middle;
+}
+
+.detailsTreeView .treeTable .treeRow.treeSection > .treeLabelCell > .treeLabel,
+.detailsTreeView .treeTable .treeRow.treeSection > .treeLabelCell > .treeLabel:hover {
+  font-weight: 400;
+  color: var(--theme-body-color-alt);
+}
+
+.empty-notice {
+  color: var(--theme-body-color-alt);
+  padding: 3px 8px;
+}
+
 /*
  * FIXME: normal html block element cannot fill outer XUL element
  * This workaround should be removed after netmonitor is migrated to react
  */
+#react-cookies-tabpanel-hook,
 #react-preview-tabpanel-hook,
 #react-security-tabpanel-hook,
 #react-timings-tabpanel-hook,
 #network-statistics-charts,
 #primed-cache-chart,
 #empty-cache-chart {
-  display: -moz-box;
   -moz-box-flex: 1;
+  display: flex;
+  flex-flow: column;
 }
 
 /* For vbox */
+#react-cookies-tabpanel-hook,
 #react-preview-tabpanel-hook,
 #react-security-tabpanel-hook,
 #react-timings-tabpanel-hook,
 #primed-cache-chart,
 #empty-cache-chart {
   -moz-box-orient: vertical;
 }