Bug 1317649 - Implement Cookies Panel;r=Honza,jsnajdr,rickychien
authorFred Lin <gasolin@mozilla.com>
Thu, 24 Nov 2016 17:05:31 +0800
changeset 374290 088802f273183b11a7c27bfd00da391a17a8a68d
parent 374289 86e55de5106e177f2e78e7b26bed620d7c51240d
child 374291 91a65ad6d3ca65ab06bf5822befbefe84c1343b5
push id6996
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 20:48:21 +0000
treeherdermozilla-beta@d89512dab048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza, jsnajdr, rickychien
bugs1317649
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1317649 - Implement Cookies Panel;r=Honza,jsnajdr,rickychien use properties-view show other Cookie properties MozReview-Commit-ID: 7526Hm2ORbL
devtools/client/netmonitor/actions/filters.js
devtools/client/netmonitor/components/search-box.js
devtools/client/netmonitor/components/toolbar.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/details-view.js
+++ b/devtools/client/netmonitor/details-view.js
@@ -1,48 +1,34 @@
 /* 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/. */
 
 /* eslint-disable mozilla/reject-some-requires */
-/* globals window, dumpn, $, gNetwork */
+/* globals window, dumpn, $ */
 
 "use strict";
 
 const promise = require("promise");
 const EventEmitter = require("devtools/shared/event-emitter");
-const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
 const { Task } = require("devtools/shared/task");
 const { ToolSidebar } = require("devtools/client/framework/sidebar");
-const { VariablesView } = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
 const { EVENTS } = require("./events");
-const { L10N } = require("./l10n");
 const { Filters } = require("./filter-predicates");
 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 HeadersPanel = createFactory(require("./shared/components/headers-panel"));
 const ParamsPanel = createFactory(require("./shared/components/params-panel"));
 const PreviewPanel = createFactory(require("./shared/components/preview-panel"));
 const ResponsePanel = createFactory(require("./shared/components/response-panel"));
 const SecurityPanel = createFactory(require("./shared/components/security-panel"));
 const TimingsPanel = createFactory(require("./shared/components/timings-panel"));
 
-const GENERIC_VARIABLES_VIEW_SETTINGS = {
-  lazyEmpty: true,
-  // ms
-  lazyEmptyDelay: 10,
-  searchEnabled: true,
-  editableValueTooltip: "",
-  editableNameTooltip: "",
-  preventDisableOnChange: true,
-  preventDescriptorModifiers: true,
-  eval: () => {}
-};
-
 /**
  * Functions handling the requests details view.
  */
 function DetailsView() {
   dumpn("DetailsView was instantiated");
 
   // The ToolSidebar requires the panel object to be able to emit events.
   EventEmitter.decorate(this);
@@ -65,16 +51,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._headersPanelNode = $("#react-headers-tabpanel-hook");
 
     ReactDOM.render(Provider(
       { store },
       HeadersPanel()
     ), this._headersPanelNode);
 
     this._paramsPanelNode = $("#react-params-tabpanel-hook");
@@ -112,36 +105,27 @@ DetailsView.prototype = {
       TimingsPanel()
     ), this._timingsPanelNode);
 
     this.widget = $("#event-details-pane");
     this.sidebar = new ToolSidebar(this.widget, this, "netmonitor", {
       disableTelemetry: true,
       showAllTabsMenu: true
     });
-
-    this._cookies = new VariablesView($("#all-cookies"),
-      Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
-        emptyText: L10N.getStr("cookiesEmptyText"),
-        searchPlaceholder: L10N.getStr("cookiesFilterText")
-      }));
-
-    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._headersPanelNode);
     ReactDOM.unmountComponentAtNode(this._paramsPanelNode);
-    ReactDOM.unmountComponentAtNode(this._headersPanelNode);
     ReactDOM.unmountComponentAtNode(this._previewPanelNode);
     ReactDOM.unmountComponentAtNode(this._responsePanelNode);
     ReactDOM.unmountComponentAtNode(this._securityPanelNode);
     ReactDOM.unmountComponentAtNode(this._timingsPanelNode);
     this.sidebar.destroy();
     $("tabpanels", this.widget).removeEventListener("select",
       this._onTabSelect);
   },
@@ -172,18 +156,16 @@ DetailsView.prototype = {
     // request has no security information.
 
     if (!isHtml && this.widget.selectedPanel === $("#preview-tabpanel") ||
         !hasSecurityInfo && this.widget.selectedPanel ===
           $("#security-tabpanel")) {
       this.widget.selectedIndex = 0;
     }
 
-    this._cookies.empty();
-
     this._dataSrc = { src: data, populated: [] };
     this._onTabSelect();
     window.emit(EVENTS.NETWORKDETAILSVIEW_POPULATED);
 
     return promise.resolve();
   },
 
   /**
@@ -206,24 +188,16 @@ DetailsView.prototype = {
       // 997065 and 984687. As there's no way to stop the current task mark the
       // tab dirty and refresh the panel once the current task finishes.
       viewState.dirty[tab] = true;
       viewState.latestData = src;
       return;
     }
 
     Task.spawn(function* () {
-      viewState.updating[tab] = true;
-      switch (tab) {
-        // "Cookies"
-        case 1:
-          yield view._setResponseCookies(src.responseCookies);
-          yield view._setRequestCookies(src.requestCookies);
-          break;
-      }
       viewState.updating[tab] = false;
     }).then(() => {
       if (tab == this.widget.selectedIndex) {
         if (viewState.dirty[tab]) {
           // The request information was updated while the task was running.
           viewState.dirty[tab] = false;
           view.populate(viewState.latestData);
         } else {
@@ -234,83 +208,12 @@ DetailsView.prototype = {
       } else if (viewState.dirty[tab]) {
         // Tab is dirty but no longer selected. Don't refresh it now, it'll be
         // done if the tab is shown again.
         viewState.dirty[tab] = false;
       }
     }, e => console.error(e));
   },
 
-  /**
-   * 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;
-    }
-  }),
-
   _dataSrc: null,
-  _cookies: null,
-  _requestCookies: "",
-  _responseCookies: ""
 };
 
 exports.DetailsView = DetailsView;
--- a/devtools/client/netmonitor/netmonitor.xul
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -124,17 +124,18 @@
               <tabpanel id="headers-tabpanel"
                         class="tabpanel-content">
                 <html:div xmlns="http://www.w3.org/1999/xhtml"
                           id="react-headers-tabpanel-hook"/>
               </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">
                 <html:div xmlns="http://www.w3.org/1999/xhtml"
                           id="react-params-tabpanel-hook"/>
               </tabpanel>
               <tabpanel id="response-tabpanel"
--- 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,
@@ -62,22 +62,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
@@ -23,20 +23,20 @@ const { Prefs } = require("./prefs");
 const {
   fetchHeaders,
   formDataURI,
   getFormDataSections,
 } = require("./request-utils");
 
 const {
   getActiveFilters,
-  getSortedRequests,
   getDisplayedRequests,
   getRequestById,
   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;
@@ -219,34 +219,36 @@ RequestsMenuView.prototype = {
       id,
       {
         startedMillis,
         method,
         url,
         isXHR,
         cause,
         fromCache,
-        fromServiceWorker
+        fromServiceWorker,
       },
       true
     );
 
     this.store.dispatch(action).then(() => window.emit(EVENTS.REQUEST_ADDED, action.id));
   },
 
   updateRequest: Task.async(function* (id, data) {
     const action = Actions.updateRequest(id, data, true);
     yield this.store.dispatch(action);
-
     let {
+      responseContent,
+      responseCookies,
+      responseHeaders,
+      requestCookies,
       requestHeaders,
       requestPostData,
-      responseContent,
-      responseHeaders,
     } = action.data;
+    let request = getRequestById(this.store.getState(), action.id);
 
     if (requestHeaders && requestHeaders.headers && requestHeaders.headers.length) {
       let headers = yield fetchHeaders(
         requestHeaders, gNetwork.getString.bind(gNetwork));
       if (headers) {
         yield this.store.dispatch(Actions.updateRequest(
           action.id,
           { requestHeaders: headers },
@@ -262,36 +264,33 @@ RequestsMenuView.prototype = {
         yield this.store.dispatch(Actions.updateRequest(
           action.id,
           { responseHeaders: headers },
           true,
         ));
       }
     }
 
-    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);
+      }
 
-        responseContent.content.text = response;
-        payload.responseContent = responseContent;
+      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);
@@ -301,16 +300,61 @@ 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));
     }
+
+    // Fetch request and 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.
+    if (requestCookies) {
+      let reqCookies = [];
+      // request store cookies in requestCookies or requestCookies.cookies
+      let cookies = requestCookies.cookies ?
+        requestCookies.cookies : requestCookies;
+      // make sure cookies is iterable
+      if (typeof cookies[Symbol.iterator] === "function") {
+        for (let cookie of cookies) {
+          reqCookies.push(Object.assign({}, cookie, {
+            value: yield gNetwork.getString(cookie.value),
+          }));
+        }
+        if (reqCookies.length) {
+          yield this.store.dispatch(Actions.updateRequest(
+            action.id,
+            { requestCookies: reqCookies },
+            true));
+        }
+      }
+    }
+
+    if (responseCookies) {
+      let resCookies = [];
+      // response store cookies in responseCookies or responseCookies.cookies
+      let cookies = responseCookies.cookies ?
+        responseCookies.cookies : responseCookies;
+      // make sure cookies is iterable
+      if (typeof cookies[Symbol.iterator] === "function") {
+        for (let cookie of cookies) {
+          resCookies.push(Object.assign({}, cookie, {
+            value: yield gNetwork.getString(cookie.value),
+          }));
+        }
+        if (resCookies.length) {
+          yield this.store.dispatch(Actions.updateRequest(
+            action.id,
+            { responseCookies: resCookies },
+            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
@@ -92,24 +92,52 @@ const getDisplayedRequestsSummary = crea
   }
 );
 
 const getSelectedRequest = createSelector(
   state => state.requests,
   ({ selectedId, requests }) => selectedId ? requests.get(selectedId) : null
 );
 
+const getSelectedRequestCookies = createSelector(
+  getSelectedRequest,
+  selectedRequest => {
+    // request store cookies in requestCookies or requestCookies.cookies
+    if (selectedRequest && selectedRequest.requestCookies) {
+      return selectedRequest.requestCookies.cookies ?
+        selectedRequest.requestCookies.cookies : selectedRequest.requestCookies;
+    }
+
+    return [];
+  }
+);
+
+const getSelectedResponseCookies = createSelector(
+  getSelectedRequest,
+  selectedRequest => {
+    // response store cookies in responseCookies or responseCookies.cookies
+    if (selectedRequest && selectedRequest.responseCookies) {
+      return selectedRequest.responseCookies.cookies ?
+        selectedRequest.responseCookies.cookies : selectedRequest.responseCookies;
+    }
+
+    return [];
+  }
+);
+
 function getRequestById(state, id) {
   return state.requests.requests.get(id);
 }
 
 function getDisplayedRequestById(state, id) {
   return getDisplayedRequests(state).find(r => r.id === id);
 }
 
 module.exports = {
   getDisplayedRequestById,
   getDisplayedRequests,
   getDisplayedRequestsSummary,
   getRequestById,
   getSelectedRequest,
+  getSelectedRequestCookies,
+  getSelectedResponseCookies,
   getSortedRequests,
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/shared/components/cookies-panel.js
@@ -0,0 +1,99 @@
+/* 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 {
+  getSelectedRequestCookies,
+  getSelectedResponseCookies,
+} = require("../../selectors/index");
+
+// Component
+const PropertiesView = createFactory(require("./properties-view"));
+
+const { div } = DOM;
+
+const COOKIES_EMPTY_TEXT = L10N.getStr("cookiesEmptyText");
+const COOKIES_FILTER_TEXT = L10N.getStr("cookiesFilterText");
+const REQUEST_COOKIES = L10N.getStr("requestCookies");
+const RESPONSE_COOKIES = L10N.getStr("responseCookies");
+const SECTION_NAMES = [
+  RESPONSE_COOKIES,
+  REQUEST_COOKIES,
+];
+
+/*
+ * Cookies panel component
+ * This tab lists full details of any cookies sent with the request or response
+ */
+function CookiesPanel({
+  request,
+  response,
+}) {
+  if (!response.length && !request.length) {
+    return div({ className: "empty-notice" },
+      COOKIES_EMPTY_TEXT
+    );
+  }
+
+  let object = {};
+  if (response.length) {
+    object[RESPONSE_COOKIES] = getProperties(response);
+  }
+  if (request.length) {
+    object[REQUEST_COOKIES] = getProperties(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 getProperties(arr) {
+  return arr.reduce((map, obj) => {
+    // Generally cookies object contains only name and value properties and can
+    // be rendered as name: value pair.
+    // When there are more properties in cookies object such as extra or path,
+    // We will pass the object to display these extra information
+    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 => ({
+    request: getSelectedRequestCookies(state),
+    response: getSelectedResponseCookies(state),
+  })
+)(CookiesPanel);
--- a/devtools/client/netmonitor/shared/components/moz.build
+++ b/devtools/client/netmonitor/shared/components/moz.build
@@ -1,13 +1,14 @@
 # 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',
     'headers-panel.js',
     'params-panel.js',
     'preview-panel.js',
     'properties-view.js',
     'response-panel.js',
     'security-panel.js',
     'timings-panel.js',
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -1267,30 +1267,32 @@
   width: 100%;
   height: 100%;
 }
 
 /*
  * 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-headers-tabpanel-hook,
 #react-params-tabpanel-hook,
 #react-preview-tabpanel-hook,
 #react-response-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;
 }
 
 /* For vbox */
+#react-cookies-tabpanel-hook,
 #react-headers-tabpanel-hook,
 #react-params-tabpanel-hook,
 #react-preview-tabpanel-hook,
 #react-response-tabpanel-hook,
 #react-security-tabpanel-hook,
 #react-timings-tabpanel-hook,
 #primed-cache-chart,
 #empty-cache-chart {