Bug 1317649 - Implement Cookies Panel;r=jsnajdr,honza,rickychien draft
authorFred Lin <gasolin@mozilla.com>
Thu, 24 Nov 2016 17:05:31 +0800
changeset 460562 c25e4b9b652a7d7b281d06e15e51c064518437ac
parent 460465 b71ba665a19f80d6b89c0fe2a083349cdcf791dd
child 542073 d7cbd66338b2113cb1537698bf1eb2004143acd7
push id41419
push userbmo:gasolin@mozilla.com
push dateFri, 13 Jan 2017 08:34:19 +0000
reviewersjsnajdr, honza, rickychien
bugs1317649
milestone53.0a1
Bug 1317649 - Implement Cookies Panel;r=jsnajdr,honza,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 {