diff options
Diffstat (limited to 'devtools/client/webconsole/net/components')
18 files changed, 1508 insertions, 0 deletions
diff --git a/devtools/client/webconsole/net/components/cookies-tab.js b/devtools/client/webconsole/net/components/cookies-tab.js new file mode 100644 index 000000000..d76414679 --- /dev/null +++ b/devtools/client/webconsole/net/components/cookies-tab.js @@ -0,0 +1,75 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); +const NetInfoGroupList = React.createFactory(require("./net-info-group-list")); +const Spinner = React.createFactory(require("./spinner")); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents 'Cookies' tab displayed when the user + * expands network log in the Console panel. It's responsible for rendering + * sent and received cookies. + */ +var CookiesTab = React.createClass({ + propTypes: { + actions: PropTypes.shape({ + requestData: PropTypes.func.isRequired + }), + data: PropTypes.object.isRequired, + }, + + displayName: "CookiesTab", + + componentDidMount() { + let { actions, data } = this.props; + let requestCookies = data.request.cookies; + let responseCookies = data.response.cookies; + + // TODO: use async action objects as soon as Redux is in place + if (!requestCookies || !requestCookies.length) { + actions.requestData("requestCookies"); + } + + if (!responseCookies || !responseCookies.length) { + actions.requestData("responseCookies"); + } + }, + + render() { + let { actions, data: file } = this.props; + let requestCookies = file.request.cookies; + let responseCookies = file.response.cookies; + + // The cookie panel displays two groups of cookies: + // 1) Response Cookies + // 2) Request Cookies + let groups = [{ + key: "responseCookies", + name: Locale.$STR("responseCookies"), + params: responseCookies + }, { + key: "requestCookies", + name: Locale.$STR("requestCookies"), + params: requestCookies + }]; + + return ( + DOM.div({className: "cookiesTabBox"}, + DOM.div({className: "panelContent"}, + NetInfoGroupList({ + groups: groups + }) + ) + ) + ); + } +}); + +// Exports from this module +module.exports = CookiesTab; diff --git a/devtools/client/webconsole/net/components/headers-tab.js b/devtools/client/webconsole/net/components/headers-tab.js new file mode 100644 index 000000000..2eca3fd2f --- /dev/null +++ b/devtools/client/webconsole/net/components/headers-tab.js @@ -0,0 +1,79 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); +const NetInfoGroupList = React.createFactory(require("./net-info-group-list")); +const Spinner = React.createFactory(require("./spinner")); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents 'Headers' tab displayed when the user + * expands network log in the Console panel. It's responsible for rendering + * request and response HTTP headers. + */ +var HeadersTab = React.createClass({ + propTypes: { + actions: PropTypes.shape({ + requestData: PropTypes.func.isRequired + }), + data: PropTypes.object.isRequired, + }, + + displayName: "HeadersTab", + + componentDidMount() { + let { actions, data } = this.props; + let requestHeaders = data.request.headers; + let responseHeaders = data.response.headers; + + // Request headers if they are not available yet. + // TODO: use async action objects as soon as Redux is in place + if (!requestHeaders) { + actions.requestData("requestHeaders"); + } + + if (!responseHeaders) { + actions.requestData("responseHeaders"); + } + }, + + render() { + let { data } = this.props; + let requestHeaders = data.request.headers; + let responseHeaders = data.response.headers; + + // TODO: Another groups to implement: + // 1) Cached Headers + // 2) Headers from upload stream + let groups = [{ + key: "responseHeaders", + name: Locale.$STR("responseHeaders"), + params: responseHeaders + }, { + key: "requestHeaders", + name: Locale.$STR("requestHeaders"), + params: requestHeaders + }]; + + // If response headers are not available yet, display a spinner + if (!responseHeaders || !responseHeaders.length) { + groups[0].content = Spinner(); + } + + return ( + DOM.div({className: "headersTabBox"}, + DOM.div({className: "panelContent"}, + NetInfoGroupList({groups: groups}) + ) + ) + ); + } +}); + +// Exports from this module +module.exports = HeadersTab; diff --git a/devtools/client/webconsole/net/components/moz.build b/devtools/client/webconsole/net/components/moz.build new file mode 100644 index 000000000..0053de780 --- /dev/null +++ b/devtools/client/webconsole/net/components/moz.build @@ -0,0 +1,25 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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-tab.js', + 'headers-tab.js', + 'net-info-body.css', + 'net-info-body.js', + 'net-info-group-list.js', + 'net-info-group.css', + 'net-info-group.js', + 'net-info-params.css', + 'net-info-params.js', + 'params-tab.js', + 'post-tab.js', + 'response-tab.css', + 'response-tab.js', + 'size-limit.css', + 'size-limit.js', + 'spinner.js', + 'stacktrace-tab.js', +) diff --git a/devtools/client/webconsole/net/components/net-info-body.css b/devtools/client/webconsole/net/components/net-info-body.css new file mode 100644 index 000000000..2d0bac70e --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-body.css @@ -0,0 +1,112 @@ +/* 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/. */ + +/******************************************************************************/ +/* Network Info Body */ + +.netInfoBody { + margin: 10px 0 0 0; + width: 100%; + cursor: default; + display: block; +} + +.netInfoBody *:focus { + outline: 0 !important; +} + +.netInfoBody .panelContent { + word-break: break-all; +} + +/******************************************************************************/ +/* Network Info Body Tabs */ + +.netInfoBody > .tabs { + background-color: transparent; + background-image: none; + height: 100%; +} + +.netInfoBody > .tabs .tabs-navigation { + border-bottom-color: var(--net-border); + background-color: transparent; + text-decoration: none; + padding-top: 3px; + padding-left: 7px; + padding-bottom: 1px; + border-bottom: 1px solid var(--net-border); +} + +.netInfoBody > .tabs .tabs-menu { + display: table; + list-style: none; + padding: 0; + margin: 0; +} + +/* This is the trick that makes the tab bottom border invisible */ +.netInfoBody > .tabs .tabs-menu-item { + position: relative; + bottom: -2px; + float: left; +} + +.netInfoBody > .tabs .tabs-menu-item a { + display: block; + border: 1px solid transparent; + text-decoration: none; + padding: 5px 8px 4px 8px;; + font-weight: bold; + color: var(--theme-body-color); + border-radius: 4px 4px 0 0; +} + +.netInfoBody > .tabs .tab-panel { + background-color: var(--theme-body-background); + border: 1px solid transparent; + border-top: none; + padding: 10px; + overflow: auto; + height: calc(100% - 31px); /* minus the height of the tab bar */ +} + +.netInfoBody > .tabs .tab-panel > div, +.netInfoBody > .tabs .tab-panel > div > div { + height: 100%; +} + +.netInfoBody > .tabs .tabs-menu-item.is-active a, +.netInfoBody > .tabs .tabs-menu-item.is-active a:focus, +.netInfoBody > .tabs .tabs-menu-item.is-active:hover a { + background-color: var(--theme-body-background); + border: 1px solid transparent; + border-bottom-color: var(--theme-highlight-bluegrey); + color: var(--theme-highlight-bluegrey); +} + +.netInfoBody > .tabs .tabs-menu-item:hover a { + border: 1px solid transparent; + border-bottom: 1px solid var(--net-border); + background-color: var(--theme-body-background); +} + + +/******************************************************************************/ +/* Themes */ + +.theme-firebug .netInfoBody > .tabs .tab-panel { + border-color: var(--net-border); +} + +.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active a, +.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active:hover a, +.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active a:focus { + border: 1px solid var(--net-border); + border-bottom-color: transparent; +} + +.theme-firebug .netInfoBody > .tabs .tabs-menu-item:hover a { + border-bottom-color: transparent; +} diff --git a/devtools/client/webconsole/net/components/net-info-body.js b/devtools/client/webconsole/net/components/net-info-body.js new file mode 100644 index 000000000..c5eccd458 --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-body.js @@ -0,0 +1,179 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); +const { createFactories } = require("devtools/client/shared/components/reps/rep-utils"); +const { Tabs, TabPanel } = createFactories(require("devtools/client/shared/components/tabs/tabs")); + +// Network +const HeadersTab = React.createFactory(require("./headers-tab")); +const ResponseTab = React.createFactory(require("./response-tab")); +const ParamsTab = React.createFactory(require("./params-tab")); +const CookiesTab = React.createFactory(require("./cookies-tab")); +const PostTab = React.createFactory(require("./post-tab")); +const StackTraceTab = React.createFactory(require("./stacktrace-tab")); +const NetUtils = require("../utils/net"); + +// Shortcuts +const PropTypes = React.PropTypes; + +/** + * This template renders the basic Network log info body. It's not + * visible by default, the user needs to expand the network log + * to see it. + * + * This is the set of tabs displaying details about network events: + * 1) Headers - request and response headers + * 2) Params - URL parameters + * 3) Response - response body + * 4) Cookies - request and response cookies + * 5) Post - posted data + */ +var NetInfoBody = React.createClass({ + propTypes: { + tabActive: PropTypes.number.isRequired, + actions: PropTypes.object.isRequired, + data: PropTypes.shape({ + request: PropTypes.object.isRequired, + response: PropTypes.object.isRequired + }) + }, + + displayName: "NetInfoBody", + + getDefaultProps() { + return { + tabActive: 0 + }; + }, + + getInitialState() { + return { + data: { + request: {}, + response: {} + }, + tabActive: this.props.tabActive, + }; + }, + + onTabChanged(index) { + this.setState({tabActive: index}); + }, + + hasCookies() { + let {request, response} = this.state.data; + return this.state.hasCookies || + NetUtils.getHeaderValue(request.headers, "Cookie") || + NetUtils.getHeaderValue(response.headers, "Set-Cookie"); + }, + + hasStackTrace() { + let {cause} = this.state.data; + return cause && cause.stacktrace && cause.stacktrace.length > 0; + }, + + getTabPanels() { + let actions = this.props.actions; + let data = this.state.data; + let {request} = data; + + // Flags for optional tabs. Some tabs are visible only if there + // are data to display. + let hasParams = request.queryString && request.queryString.length; + let hasPostData = request.bodySize > 0; + + let panels = []; + + // Headers tab + panels.push( + TabPanel({ + className: "headers", + key: "headers", + title: Locale.$STR("netRequest.headers")}, + HeadersTab({data: data, actions: actions}) + ) + ); + + // URL parameters tab + if (hasParams) { + panels.push( + TabPanel({ + className: "params", + key: "params", + title: Locale.$STR("netRequest.params")}, + ParamsTab({data: data, actions: actions}) + ) + ); + } + + // Posted data tab + if (hasPostData) { + panels.push( + TabPanel({ + className: "post", + key: "post", + title: Locale.$STR("netRequest.post")}, + PostTab({data: data, actions: actions}) + ) + ); + } + + // Response tab + panels.push( + TabPanel({className: "response", key: "response", + title: Locale.$STR("netRequest.response")}, + ResponseTab({data: data, actions: actions}) + ) + ); + + // Cookies tab + if (this.hasCookies()) { + panels.push( + TabPanel({ + className: "cookies", + key: "cookies", + title: Locale.$STR("netRequest.cookies")}, + CookiesTab({ + data: data, + actions: actions + }) + ) + ); + } + + // Stacktrace tab + if (this.hasStackTrace()) { + panels.push( + TabPanel({ + className: "stacktrace-tab", + key: "stacktrace", + title: Locale.$STR("netRequest.callstack")}, + StackTraceTab({ + data: data, + actions: actions + }) + ) + ); + } + + return panels; + }, + + render() { + let tabActive = this.state.tabActive; + let tabPanels = this.getTabPanels(); + return ( + Tabs({ + tabActive: tabActive, + onAfterChange: this.onTabChanged}, + tabPanels + ) + ); + } +}); + +// Exports from this module +module.exports = NetInfoBody; diff --git a/devtools/client/webconsole/net/components/net-info-group-list.js b/devtools/client/webconsole/net/components/net-info-group-list.js new file mode 100644 index 000000000..247a23bb7 --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-group-list.js @@ -0,0 +1,47 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); +const NetInfoGroup = React.createFactory(require("./net-info-group")); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template is responsible for rendering sections/groups inside tabs. + * It's used e.g to display Response and Request headers as separate groups. + */ +var NetInfoGroupList = React.createClass({ + propTypes: { + groups: PropTypes.array.isRequired, + }, + + displayName: "NetInfoGroupList", + + render() { + let groups = this.props.groups; + + // Filter out empty groups. + groups = groups.filter(group => { + return group && ((group.params && group.params.length) || group.content); + }); + + // Render groups + groups = groups.map(group => { + group.type = group.key; + return NetInfoGroup(group); + }); + + return ( + DOM.div({className: "netInfoGroupList"}, + groups + ) + ); + } +}); + +// Exports from this module +module.exports = NetInfoGroupList; diff --git a/devtools/client/webconsole/net/components/net-info-group.css b/devtools/client/webconsole/net/components/net-info-group.css new file mode 100644 index 000000000..43800019f --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-group.css @@ -0,0 +1,80 @@ +/* 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/. */ + +/******************************************************************************/ +/* Net Info Group */ + +.netInfoBody .netInfoGroup { + padding-bottom: 6px; +} + +/* Last group doesn't need bottom padding */ +.netInfoBody .netInfoGroup:last-child { + padding-bottom: 0; +} + +.netInfoBody .netInfoGroup:last-child .netInfoGroupContent { + padding-bottom: 0; +} + +.netInfoBody .netInfoGroupTitle { + cursor: pointer; + font-weight: bold; + -moz-user-select: none; + cursor: pointer; + padding-left: 3px; +} + +.netInfoBody .netInfoGroupTwisty { + background-image: url("chrome://devtools/skin/images/controls.png"); + background-size: 56px 28px; + background-position: 0 -14px; + background-repeat: no-repeat; + width: 14px; + height: 14px; + cursor: pointer; + display: inline-block; + vertical-align: middle; +} + +.netInfoBody .netInfoGroup.opened .netInfoGroupTwisty { + background-position: -14px -14px; +} + +/* Group content is expandable/collapsible by clicking on the title */ +.netInfoBody .netInfoGroupContent { + padding-top: 7px; + margin-top: 3px; + padding-bottom: 14px; + border-top: 1px solid var(--net-border); + display: none; +} + +/* Toggle group visibility */ +.netInfoBody .netInfoGroup.opened .netInfoGroupContent { + display: block; +} + +/******************************************************************************/ +/* Themes */ + +.theme-dark .netInfoBody .netInfoGroup { + color: var(--theme-body-color); +} + +.theme-dark .netInfoBody .netInfoGroup .netInfoGroupTwisty { + filter: invert(1); +} + +/* Twisties */ +.theme-firebug .netInfoBody .netInfoGroup .netInfoGroupTwisty { + background-image: url("chrome://devtools/skin/images/firebug/twisty-closed-firebug.svg"); + background-position: 0 2px; + background-size: 11px 11px; + width: 15px; +} + +.theme-firebug .netInfoBody .netInfoGroup.opened .netInfoGroupTwisty { + background-image: url("chrome://devtools/skin/images/firebug/twisty-open-firebug.svg"); +} diff --git a/devtools/client/webconsole/net/components/net-info-group.js b/devtools/client/webconsole/net/components/net-info-group.js new file mode 100644 index 000000000..d9794652e --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-group.js @@ -0,0 +1,80 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); +const NetInfoParams = React.createFactory(require("./net-info-params")); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents a group of data within a tab. For example, + * Headers tab has two groups 'Request Headers' and 'Response Headers' + * The Response tab can also have two groups 'Raw Data' and 'JSON' + */ +var NetInfoGroup = React.createClass({ + propTypes: { + type: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + params: PropTypes.array, + content: PropTypes.element, + open: PropTypes.bool + }, + + displayName: "NetInfoGroup", + + getDefaultProps() { + return { + open: true, + }; + }, + + getInitialState() { + return { + open: this.props.open, + }; + }, + + onToggle(event) { + this.setState({ + open: !this.state.open + }); + }, + + render() { + let content = this.props.content; + + if (!content && this.props.params) { + content = NetInfoParams({ + params: this.props.params + }); + } + + let open = this.state.open; + let className = open ? "opened" : ""; + + return ( + DOM.div({className: "netInfoGroup" + " " + className + " " + + this.props.type}, + DOM.span({ + className: "netInfoGroupTwisty", + onClick: this.onToggle + }), + DOM.span({ + className: "netInfoGroupTitle", + onClick: this.onToggle}, + this.props.name + ), + DOM.div({className: "netInfoGroupContent"}, + content + ) + ) + ); + } +}); + +// Exports from this module +module.exports = NetInfoGroup; diff --git a/devtools/client/webconsole/net/components/net-info-params.css b/devtools/client/webconsole/net/components/net-info-params.css new file mode 100644 index 000000000..4ec7140f8 --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-params.css @@ -0,0 +1,23 @@ +/* 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/. */ + +/******************************************************************************/ +/* Net Info Params */ + +.netInfoBody .netInfoParamName { + padding: 0 10px 0 0; + font-weight: bold; + vertical-align: top; + text-align: right; + white-space: nowrap; +} + +.netInfoBody .netInfoParamValue { + width: 100%; + word-wrap: break-word; +} + +.netInfoBody .netInfoParamValue > code { + font-family: var(--monospace-font-family); +} diff --git a/devtools/client/webconsole/net/components/net-info-params.js b/devtools/client/webconsole/net/components/net-info-params.js new file mode 100644 index 000000000..573257b28 --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-params.js @@ -0,0 +1,58 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template renders list of parameters within a group. + * It's essentially a list of name + value pairs. + */ +var NetInfoParams = React.createClass({ + displayName: "NetInfoParams", + + propTypes: { + params: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired + })).isRequired, + }, + + render() { + let params = this.props.params || []; + + params.sort(function (a, b) { + return a.name > b.name ? 1 : -1; + }); + + let rows = []; + params.forEach((param, index) => { + rows.push( + DOM.tr({key: index}, + DOM.td({className: "netInfoParamName"}, + DOM.span({title: param.name}, param.name) + ), + DOM.td({className: "netInfoParamValue"}, + DOM.code({}, param.value) + ) + ) + ); + }); + + return ( + DOM.table({cellPadding: 0, cellSpacing: 0}, + DOM.tbody({}, + rows + ) + ) + ); + } +}); + +// Exports from this module +module.exports = NetInfoParams; diff --git a/devtools/client/webconsole/net/components/params-tab.js b/devtools/client/webconsole/net/components/params-tab.js new file mode 100644 index 000000000..c3fefc669 --- /dev/null +++ b/devtools/client/webconsole/net/components/params-tab.js @@ -0,0 +1,41 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); +const NetInfoParams = React.createFactory(require("./net-info-params")); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents 'Params' tab displayed when the user + * expands network log in the Console panel. It's responsible for + * displaying URL parameters (query string). + */ +var ParamsTab = React.createClass({ + propTypes: { + data: PropTypes.shape({ + request: PropTypes.object.isRequired + }) + }, + + displayName: "ParamsTab", + + render() { + let data = this.props.data; + + return ( + DOM.div({className: "paramsTabBox"}, + DOM.div({className: "panelContent"}, + NetInfoParams({params: data.request.queryString}) + ) + ) + ); + } +}); + +// Exports from this module +module.exports = ParamsTab; diff --git a/devtools/client/webconsole/net/components/post-tab.js b/devtools/client/webconsole/net/components/post-tab.js new file mode 100644 index 000000000..6d06eb40b --- /dev/null +++ b/devtools/client/webconsole/net/components/post-tab.js @@ -0,0 +1,279 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); + +// Reps +const { createFactories, parseURLEncodedText } = require("devtools/client/shared/components/reps/rep-utils"); +const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view")); +const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep")); + +// Network +const NetInfoParams = React.createFactory(require("./net-info-params")); +const NetInfoGroupList = React.createFactory(require("./net-info-group-list")); +const Spinner = React.createFactory(require("./spinner")); +const SizeLimit = React.createFactory(require("./size-limit")); +const NetUtils = require("../utils/net"); +const Json = require("../utils/json"); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents 'Post' tab displayed when the user + * expands network log in the Console panel. It's responsible for + * displaying posted data (HTTP post body). + */ +var PostTab = React.createClass({ + propTypes: { + data: PropTypes.shape({ + request: PropTypes.object.isRequired + }), + actions: PropTypes.object.isRequired + }, + + displayName: "PostTab", + + isJson(file) { + let text = file.request.postData.text; + let value = NetUtils.getHeaderValue(file.request.headers, "content-type"); + return Json.isJSON(value, text); + }, + + parseJson(file) { + let postData = file.request.postData; + if (!postData) { + return null; + } + + let jsonString = new String(postData.text); + return Json.parseJSONString(jsonString); + }, + + /** + * Render JSON post data as an expandable tree. + */ + renderJson(file) { + let text = file.request.postData.text; + if (!text || isLongString(text)) { + return null; + } + + if (!this.isJson(file)) { + return null; + } + + let json = this.parseJson(file); + if (!json) { + return null; + } + + return { + key: "json", + content: TreeView({ + columns: [{id: "value"}], + object: json, + mode: "tiny", + renderValue: props => Rep(Object.assign({}, props, { + cropLimit: 50, + })), + }), + name: Locale.$STR("jsonScopeName") + }; + }, + + parseXml(file) { + let text = file.request.postData.text; + if (isLongString(text)) { + return null; + } + + return NetUtils.parseXml({ + mimeType: NetUtils.getHeaderValue(file.request.headers, "content-type"), + text: text, + }); + }, + + isXml(file) { + if (isLongString(file.request.postData.text)) { + return false; + } + + let value = NetUtils.getHeaderValue(file.request.headers, "content-type"); + if (!value) { + return false; + } + + return NetUtils.isHTML(value); + }, + + renderXml(file) { + let text = file.request.postData.text; + if (!text || isLongString(text)) { + return null; + } + + if (!this.isXml(file)) { + return null; + } + + let doc = this.parseXml(file); + if (!doc) { + return null; + } + + // Proper component for rendering XML should be used (see bug 1247392) + return null; + }, + + /** + * Multipart post data are parsed and nicely rendered + * as an expandable tree of individual parts. + */ + renderMultiPart(file) { + let text = file.request.postData.text; + if (!text || isLongString(text)) { + return; + } + + if (NetUtils.isMultiPartRequest(file)) { + // TODO: render multi part request (bug: 1247423) + } + + return; + }, + + /** + * URL encoded post data are nicely rendered as a list + * of parameters. + */ + renderUrlEncoded(file) { + let text = file.request.postData.text; + if (!text || isLongString(text)) { + return null; + } + + if (!NetUtils.isURLEncodedRequest(file)) { + return null; + } + + let lines = text.split("\n"); + let params = parseURLEncodedText(lines[lines.length - 1]); + + return { + key: "url-encoded", + content: NetInfoParams({params: params}), + name: Locale.$STR("netRequest.params") + }; + }, + + renderRawData(file) { + let text = file.request.postData.text; + + let group; + + // The post body might reached the limit, so check if we are + // dealing with a long string. + if (typeof text == "object") { + group = { + key: "raw-longstring", + name: Locale.$STR("netRequest.rawData"), + content: DOM.div({className: "netInfoResponseContent"}, + sanitize(text.initial), + SizeLimit({ + actions: this.props.actions, + data: file.request.postData, + message: Locale.$STR("netRequest.sizeLimitMessage"), + link: Locale.$STR("netRequest.sizeLimitMessageLink") + }) + ) + }; + } else { + group = { + key: "raw", + name: Locale.$STR("netRequest.rawData"), + content: DOM.div({className: "netInfoResponseContent"}, + sanitize(text) + ) + }; + } + + return group; + }, + + componentDidMount() { + let { actions, data: file } = this.props; + + if (!file.request.postData) { + // TODO: use async action objects as soon as Redux is in place + actions.requestData("requestPostData"); + } + }, + + render() { + let { actions, data: file } = this.props; + + if (file.discardRequestBody) { + return DOM.span({className: "netInfoBodiesDiscarded"}, + Locale.$STR("netRequest.requestBodyDiscarded") + ); + } + + if (!file.request.postData) { + return ( + Spinner() + ); + } + + // Render post body data. The right representation of the data + // is picked according to the content type. + let groups = []; + groups.push(this.renderUrlEncoded(file)); + // TODO: render multi part request (bug: 1247423) + // groups.push(this.renderMultiPart(file)); + groups.push(this.renderJson(file)); + groups.push(this.renderXml(file)); + groups.push(this.renderRawData(file)); + + // Filter out empty groups. + groups = groups.filter(group => group); + + // The raw response is collapsed by default if a nice formatted + // version is available. + if (groups.length > 1) { + groups[groups.length - 1].open = false; + } + + return ( + DOM.div({className: "postTabBox"}, + DOM.div({className: "panelContent"}, + NetInfoGroupList({ + groups: groups + }) + ) + ) + ); + } +}); + +// Helpers + +/** + * Workaround for a "not well-formed" error that react + * reports when there's multipart data passed to render. + */ +function sanitize(text) { + text = JSON.stringify(text); + text = text.replace(/\\r\\n/g, "\r\n").replace(/\\"/g, "\""); + return text.slice(1, text.length - 1); +} + +function isLongString(text) { + return typeof text == "object"; +} + +// Exports from this module +module.exports = PostTab; diff --git a/devtools/client/webconsole/net/components/response-tab.css b/devtools/client/webconsole/net/components/response-tab.css new file mode 100644 index 000000000..e1c31fca4 --- /dev/null +++ b/devtools/client/webconsole/net/components/response-tab.css @@ -0,0 +1,21 @@ +/* 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/. */ + +/******************************************************************************/ +/* Response Tab */ + +.netInfoBody .netInfoBodiesDiscarded { + font-style: italic; + color: gray; +} + +.netInfoBody .netInfoResponseContent { + font-family: var(--monospace-font-family); + word-wrap: break-word; +} + +.netInfoBody .responseTabBox img { + max-width: 300px; + max-height: 300px; +} diff --git a/devtools/client/webconsole/net/components/response-tab.js b/devtools/client/webconsole/net/components/response-tab.js new file mode 100644 index 000000000..78d8b2f77 --- /dev/null +++ b/devtools/client/webconsole/net/components/response-tab.js @@ -0,0 +1,277 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); + +// Reps +const { createFactories } = require("devtools/client/shared/components/reps/rep-utils"); +const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view")); +const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep")); + +// Network +const SizeLimit = React.createFactory(require("./size-limit")); +const NetInfoGroupList = React.createFactory(require("./net-info-group-list")); +const Spinner = React.createFactory(require("./spinner")); +const Json = require("../utils/json"); +const NetUtils = require("../utils/net"); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents 'Response' tab displayed when the user + * expands network log in the Console panel. It's responsible for + * rendering HTTP response body. + * + * In case of supported response mime-type (e.g. application/json, + * text/xml, etc.), the response is parsed using appropriate parser + * and rendered accordingly. + */ +var ResponseTab = React.createClass({ + propTypes: { + data: PropTypes.shape({ + request: PropTypes.object.isRequired, + response: PropTypes.object.isRequired + }), + actions: PropTypes.object.isRequired + }, + + displayName: "ResponseTab", + + // Response Types + + isJson(content) { + if (isLongString(content.text)) { + return false; + } + + return Json.isJSON(content.mimeType, content.text); + }, + + parseJson(file) { + let content = file.response.content; + if (isLongString(content.text)) { + return null; + } + + let jsonString = new String(content.text); + return Json.parseJSONString(jsonString); + }, + + isImage(content) { + if (isLongString(content.text)) { + return false; + } + + return NetUtils.isImage(content.mimeType); + }, + + isXml(content) { + if (isLongString(content.text)) { + return false; + } + + return NetUtils.isHTML(content.mimeType); + }, + + parseXml(file) { + let content = file.response.content; + if (isLongString(content.text)) { + return null; + } + + return NetUtils.parseXml(content); + }, + + // Rendering + + renderJson(file) { + let content = file.response.content; + if (!this.isJson(content)) { + return null; + } + + let json = this.parseJson(file); + if (!json) { + return null; + } + + return { + key: "json", + content: TreeView({ + columns: [{id: "value"}], + object: json, + mode: "tiny", + renderValue: props => Rep(Object.assign({}, props, { + cropLimit: 50, + })), + }), + name: Locale.$STR("jsonScopeName") + }; + }, + + renderImage(file) { + let content = file.response.content; + if (!this.isImage(content)) { + return null; + } + + let dataUri = "data:" + content.mimeType + ";base64," + content.text; + return { + key: "image", + content: DOM.img({src: dataUri}), + name: Locale.$STR("netRequest.image") + }; + }, + + renderXml(file) { + let content = file.response.content; + if (!this.isXml(content)) { + return null; + } + + let doc = this.parseXml(file); + if (!doc) { + return null; + } + + // Proper component for rendering XML should be used (see bug 1247392) + return null; + }, + + /** + * If full response text is available, let's try to parse and + * present nicely according to the underlying format. + */ + renderFormattedResponse(file) { + let content = file.response.content; + if (typeof content.text == "object") { + return null; + } + + let group = this.renderJson(file); + if (group) { + return group; + } + + group = this.renderImage(file); + if (group) { + return group; + } + + group = this.renderXml(file); + if (group) { + return group; + } + }, + + renderRawResponse(file) { + let group; + let content = file.response.content; + + // The response might reached the limit, so check if we are + // dealing with a long string. + if (typeof content.text == "object") { + group = { + key: "raw-longstring", + name: Locale.$STR("netRequest.rawData"), + content: DOM.div({className: "netInfoResponseContent"}, + content.text.initial, + SizeLimit({ + actions: this.props.actions, + data: content, + message: Locale.$STR("netRequest.sizeLimitMessage"), + link: Locale.$STR("netRequest.sizeLimitMessageLink") + }) + ) + }; + } else { + group = { + key: "raw", + name: Locale.$STR("netRequest.rawData"), + content: DOM.div({className: "netInfoResponseContent"}, + content.text + ) + }; + } + + return group; + }, + + componentDidMount() { + let { actions, data: file } = this.props; + let content = file.response.content; + + if (!content || typeof (content.text) == "undefined") { + // TODO: use async action objects as soon as Redux is in place + actions.requestData("responseContent"); + } + }, + + /** + * The response panel displays two groups: + * + * 1) Formatted response (in case of supported format, e.g. JSON, XML, etc.) + * 2) Raw response data (always displayed if not discarded) + */ + render() { + let { actions, data: file } = this.props; + + // If response bodies are discarded (not collected) let's just + // display a info message indicating what to do to collect even + // response bodies. + if (file.discardResponseBody) { + return DOM.span({className: "netInfoBodiesDiscarded"}, + Locale.$STR("netRequest.responseBodyDiscarded") + ); + } + + // Request for the response content is done only if the response + // is not fetched yet - i.e. the `content.text` is undefined. + // Empty content.text` can also be a valid response either + // empty or not available yet. + let content = file.response.content; + if (!content || typeof (content.text) == "undefined") { + return ( + Spinner() + ); + } + + // Render response body data. The right representation of the data + // is picked according to the content type. + let groups = []; + groups.push(this.renderFormattedResponse(file)); + groups.push(this.renderRawResponse(file)); + + // Filter out empty groups. + groups = groups.filter(group => group); + + // The raw response is collapsed by default if a nice formatted + // version is available. + if (groups.length > 1) { + groups[1].open = false; + } + + return ( + DOM.div({className: "responseTabBox"}, + DOM.div({className: "panelContent"}, + NetInfoGroupList({ + groups: groups + }) + ) + ) + ); + } +}); + +// Helpers + +function isLongString(text) { + return typeof text == "object"; +} + +// Exports from this module +module.exports = ResponseTab; diff --git a/devtools/client/webconsole/net/components/size-limit.css b/devtools/client/webconsole/net/components/size-limit.css new file mode 100644 index 000000000..a5c214d9e --- /dev/null +++ b/devtools/client/webconsole/net/components/size-limit.css @@ -0,0 +1,15 @@ +/* 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/. */ + +/******************************************************************************/ +/* Response Size Limit */ + +.netInfoBody .netInfoSizeLimit { + font-weight: bold; + padding-top: 10px; +} + +.netInfoBody .netInfoSizeLimit .objectLink { + color: var(--theme-highlight-blue); +} diff --git a/devtools/client/webconsole/net/components/size-limit.js b/devtools/client/webconsole/net/components/size-limit.js new file mode 100644 index 000000000..de8839314 --- /dev/null +++ b/devtools/client/webconsole/net/components/size-limit.js @@ -0,0 +1,62 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents a size limit notification message + * used e.g. in the Response tab when response body exceeds + * size limit. The message contains a link allowing the user + * to fetch the rest of the data from the backend (debugger server). + */ +var SizeLimit = React.createClass({ + propTypes: { + data: PropTypes.object.isRequired, + message: PropTypes.string.isRequired, + link: PropTypes.string.isRequired, + actions: PropTypes.shape({ + resolveString: PropTypes.func.isRequired + }), + }, + + displayName: "SizeLimit", + + // Event Handlers + + onClickLimit(event) { + let actions = this.props.actions; + let content = this.props.data; + + actions.resolveString(content, "text"); + }, + + // Rendering + + render() { + let message = this.props.message; + let link = this.props.link; + let reLink = /^(.*)\{\{link\}\}(.*$)/; + let m = message.match(reLink); + + return ( + DOM.div({className: "netInfoSizeLimit"}, + DOM.span({}, m[1]), + DOM.a({ + className: "objectLink", + onClick: this.onClickLimit}, + link + ), + DOM.span({}, m[2]) + ) + ); + } +}); + +// Exports from this module +module.exports = SizeLimit; diff --git a/devtools/client/webconsole/net/components/spinner.js b/devtools/client/webconsole/net/components/spinner.js new file mode 100644 index 000000000..fe79f7dd1 --- /dev/null +++ b/devtools/client/webconsole/net/components/spinner.js @@ -0,0 +1,26 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); + +// Shortcuts +const DOM = React.DOM; + +/** + * This template represents a throbber displayed when the UI + * is waiting for data coming from the backend (debugging server). + */ +var Spinner = React.createClass({ + displayName: "Spinner", + + render() { + return ( + DOM.div({className: "devtools-throbber"}) + ); + } +}); + +// Exports from this module +module.exports = Spinner; diff --git a/devtools/client/webconsole/net/components/stacktrace-tab.js b/devtools/client/webconsole/net/components/stacktrace-tab.js new file mode 100644 index 000000000..51eb7689b --- /dev/null +++ b/devtools/client/webconsole/net/components/stacktrace-tab.js @@ -0,0 +1,29 @@ +/* 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 { PropTypes, createClass, createFactory } = require("devtools/client/shared/vendor/react"); +const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace")); + +const StackTraceTab = createClass({ + displayName: "StackTraceTab", + + propTypes: { + data: PropTypes.object.isRequired, + actions: PropTypes.shape({ + onViewSourceInDebugger: PropTypes.func.isRequired + }) + }, + + render() { + let { stacktrace } = this.props.data.cause; + let { actions } = this.props; + let onViewSourceInDebugger = actions.onViewSourceInDebugger.bind(actions); + + return StackTrace({ stacktrace, onViewSourceInDebugger }); + } +}); + +// Exports from this module +module.exports = StackTraceTab; |