diff options
Diffstat (limited to 'devtools/client/webconsole/new-console-output/components')
20 files changed, 1545 insertions, 0 deletions
diff --git a/devtools/client/webconsole/new-console-output/components/collapse-button.js b/devtools/client/webconsole/new-console-output/components/collapse-button.js new file mode 100644 index 000000000..ab72fcf4d --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/collapse-button.js @@ -0,0 +1,50 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + createClass, + DOM: dom, + PropTypes, +} = require("devtools/client/shared/vendor/react"); + +const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages"); + +const CollapseButton = createClass({ + + displayName: "CollapseButton", + + propTypes: { + open: PropTypes.bool.isRequired, + title: PropTypes.string, + }, + + getDefaultProps: function () { + return { + title: l10n.getStr("messageToggleDetails") + }; + }, + + render: function () { + const { open, onClick, title } = this.props; + + let classes = ["theme-twisty"]; + + if (open) { + classes.push("open"); + } + + return dom.a({ + className: classes.join(" "), + onClick, + title: title, + }); + } +}); + +module.exports = CollapseButton; diff --git a/devtools/client/webconsole/new-console-output/components/console-output.js b/devtools/client/webconsole/new-console-output/components/console-output.js new file mode 100644 index 000000000..1ba7f8dda --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/console-output.js @@ -0,0 +1,125 @@ +/* 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 { + createClass, + createFactory, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const ReactDOM = require("devtools/client/shared/vendor/react-dom"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); + +const { + getAllMessages, + getAllMessagesUiById, + getAllMessagesTableDataById, + getAllGroupsById, +} = require("devtools/client/webconsole/new-console-output/selectors/messages"); +const { getScrollSetting } = require("devtools/client/webconsole/new-console-output/selectors/ui"); +const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer); + +const ConsoleOutput = createClass({ + + displayName: "ConsoleOutput", + + propTypes: { + messages: PropTypes.object.isRequired, + messagesUi: PropTypes.object.isRequired, + serviceContainer: PropTypes.shape({ + attachRefToHud: PropTypes.func.isRequired, + }), + autoscroll: PropTypes.bool.isRequired, + }, + + componentDidMount() { + scrollToBottom(this.outputNode); + this.props.serviceContainer.attachRefToHud("outputScroller", this.outputNode); + }, + + componentWillUpdate(nextProps, nextState) { + if (!this.outputNode) { + return; + } + + const outputNode = this.outputNode; + + // Figure out if we are at the bottom. If so, then any new message should be scrolled + // into view. + if (this.props.autoscroll && outputNode.lastChild) { + this.shouldScrollBottom = isScrolledToBottom(outputNode.lastChild, outputNode); + } + }, + + componentDidUpdate() { + if (this.shouldScrollBottom) { + scrollToBottom(this.outputNode); + } + }, + + render() { + let { + dispatch, + autoscroll, + messages, + messagesUi, + messagesTableData, + serviceContainer, + groups, + } = this.props; + + let messageNodes = messages.map((message) => { + const parentGroups = message.groupId ? ( + (groups.get(message.groupId) || []) + .concat([message.groupId]) + ) : []; + + return ( + MessageContainer({ + dispatch, + message, + key: message.id, + serviceContainer, + open: messagesUi.includes(message.id), + tableData: messagesTableData.get(message.id), + autoscroll, + indent: parentGroups.length, + }) + ); + }); + return ( + dom.div({ + className: "webconsole-output", + ref: node => { + this.outputNode = node; + }, + }, messageNodes + ) + ); + } +}); + +function scrollToBottom(node) { + node.scrollTop = node.scrollHeight; +} + +function isScrolledToBottom(outputNode, scrollNode) { + let lastNodeHeight = outputNode.lastChild ? + outputNode.lastChild.clientHeight : 0; + return scrollNode.scrollTop + scrollNode.clientHeight >= + scrollNode.scrollHeight - lastNodeHeight / 2; +} + +function mapStateToProps(state, props) { + return { + messages: getAllMessages(state), + messagesUi: getAllMessagesUiById(state), + messagesTableData: getAllMessagesTableDataById(state), + autoscroll: getScrollSetting(state), + groups: getAllGroupsById(state), + }; +} + +module.exports = connect(mapStateToProps)(ConsoleOutput); diff --git a/devtools/client/webconsole/new-console-output/components/console-table.js b/devtools/client/webconsole/new-console-output/components/console-table.js new file mode 100644 index 000000000..bf8fdcbd8 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/console-table.js @@ -0,0 +1,202 @@ +/* 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 { + createClass, + createFactory, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const { ObjectClient } = require("devtools/shared/client/main"); +const actions = require("devtools/client/webconsole/new-console-output/actions/messages"); +const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages"); +const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body")); + +const TABLE_ROW_MAX_ITEMS = 1000; +const TABLE_COLUMN_MAX_ITEMS = 10; + +const ConsoleTable = createClass({ + + displayName: "ConsoleTable", + + propTypes: { + dispatch: PropTypes.func.isRequired, + parameters: PropTypes.array.isRequired, + serviceContainer: PropTypes.shape({ + hudProxyClient: PropTypes.object.isRequired, + }), + id: PropTypes.string.isRequired, + }, + + componentWillMount: function () { + const {id, dispatch, serviceContainer, parameters} = this.props; + + if (!Array.isArray(parameters) || parameters.length === 0) { + return; + } + + const client = new ObjectClient(serviceContainer.hudProxyClient, parameters[0]); + let dataType = getParametersDataType(parameters); + + // Get all the object properties. + dispatch(actions.messageTableDataGet(id, client, dataType)); + }, + + getHeaders: function (columns) { + let headerItems = []; + columns.forEach((value, key) => headerItems.push(dom.th({}, value))); + return headerItems; + }, + + getRows: function (columns, items) { + return items.map(item => { + let cells = []; + columns.forEach((value, key) => { + cells.push( + dom.td( + {}, + GripMessageBody({ + grip: item[key] + }) + ) + ); + }); + return dom.tr({}, cells); + }); + }, + + render: function () { + const {parameters, tableData} = this.props; + const headersGrip = parameters[1]; + const headers = headersGrip && headersGrip.preview ? headersGrip.preview.items : null; + + // if tableData is nullable, we don't show anything. + if (!tableData) { + return null; + } + + const {columns, items} = getTableItems( + tableData, + getParametersDataType(parameters), + headers + ); + + return ( + dom.table({className: "new-consoletable devtools-monospace"}, + dom.thead({}, this.getHeaders(columns)), + dom.tbody({}, this.getRows(columns, items)) + ) + ); + } +}); + +function getParametersDataType(parameters = null) { + if (!Array.isArray(parameters) || parameters.length === 0) { + return null; + } + return parameters[0].class; +} + +function getTableItems(data = {}, type, headers = null) { + const INDEX_NAME = "_index"; + const VALUE_NAME = "_value"; + const namedIndexes = { + [INDEX_NAME]: ( + ["Object", "Array"].includes(type) ? + l10n.getStr("table.index") : l10n.getStr("table.iterationIndex") + ), + [VALUE_NAME]: l10n.getStr("table.value"), + key: l10n.getStr("table.key") + }; + + let columns = new Map(); + let items = []; + + let addItem = function (item) { + items.push(item); + Object.keys(item).forEach(key => addColumn(key)); + }; + + let addColumn = function (columnIndex) { + let columnExists = columns.has(columnIndex); + let hasMaxColumns = columns.size == TABLE_COLUMN_MAX_ITEMS; + let hasCustomHeaders = Array.isArray(headers); + + if ( + !columnExists && + !hasMaxColumns && ( + !hasCustomHeaders || + headers.includes(columnIndex) || + columnIndex === INDEX_NAME + ) + ) { + columns.set(columnIndex, namedIndexes[columnIndex] || columnIndex); + } + }; + + for (let index of Object.keys(data)) { + if (type !== "Object" && index == parseInt(index, 10)) { + index = parseInt(index, 10); + } + + let item = { + [INDEX_NAME]: index + }; + + let property = data[index].value; + + if (property.preview) { + let {preview} = property; + let entries = preview.ownProperties || preview.items; + if (entries) { + for (let key of Object.keys(entries)) { + let entry = entries[key]; + item[key] = entry.value || entry; + } + } else { + if (preview.key) { + item.key = preview.key; + } + + item[VALUE_NAME] = preview.value || property; + } + } else { + item[VALUE_NAME] = property; + } + + addItem(item); + + if (items.length === TABLE_ROW_MAX_ITEMS) { + break; + } + } + + // Some headers might not be present in the items, so we make sure to + // return all the headers set by the user. + if (Array.isArray(headers)) { + headers.forEach(header => addColumn(header)); + } + + // We want to always have the index column first + if (columns.has(INDEX_NAME)) { + let index = columns.get(INDEX_NAME); + columns.delete(INDEX_NAME); + columns = new Map([[INDEX_NAME, index], ...columns.entries()]); + } + + // We want to always have the values column last + if (columns.has(VALUE_NAME)) { + let index = columns.get(VALUE_NAME); + columns.delete(VALUE_NAME); + columns.set(VALUE_NAME, index); + } + + return { + columns, + items + }; +} + +module.exports = ConsoleTable; diff --git a/devtools/client/webconsole/new-console-output/components/filter-bar.js b/devtools/client/webconsole/new-console-output/components/filter-bar.js new file mode 100644 index 000000000..a386a414a --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/filter-bar.js @@ -0,0 +1,170 @@ +/* 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, + createClass, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters"); +const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui"); +const { filterTextSet, filtersClear } = require("devtools/client/webconsole/new-console-output/actions/index"); +const { messagesClear } = require("devtools/client/webconsole/new-console-output/actions/index"); +const uiActions = require("devtools/client/webconsole/new-console-output/actions/index"); +const { + MESSAGE_LEVEL +} = require("../constants"); +const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button")); + +const FilterBar = createClass({ + + displayName: "FilterBar", + + propTypes: { + filter: PropTypes.object.isRequired, + serviceContainer: PropTypes.shape({ + attachRefToHud: PropTypes.func.isRequired, + }).isRequired, + ui: PropTypes.object.isRequired + }, + + componentDidMount() { + this.props.serviceContainer.attachRefToHud("filterBox", + this.wrapperNode.querySelector(".text-filter")); + }, + + onClickMessagesClear: function () { + this.props.dispatch(messagesClear()); + }, + + onClickFilterBarToggle: function () { + this.props.dispatch(uiActions.filterBarToggle()); + }, + + onClickFiltersClear: function () { + this.props.dispatch(filtersClear()); + }, + + onSearchInput: function (e) { + this.props.dispatch(filterTextSet(e.target.value)); + }, + + render() { + const {dispatch, filter, ui} = this.props; + let filterBarVisible = ui.filterBarVisible; + let children = []; + + children.push(dom.div({className: "devtools-toolbar webconsole-filterbar-primary"}, + dom.button({ + className: "devtools-button devtools-clear-icon", + title: "Clear output", + onClick: this.onClickMessagesClear + }), + dom.button({ + className: "devtools-button devtools-filter-icon" + ( + filterBarVisible ? " checked" : ""), + title: "Toggle filter bar", + onClick: this.onClickFilterBarToggle + }), + dom.input({ + className: "devtools-plaininput text-filter", + type: "search", + value: filter.text, + placeholder: "Filter output", + onInput: this.onSearchInput + }) + )); + + if (filterBarVisible) { + children.push( + dom.div({className: "devtools-toolbar webconsole-filterbar-secondary"}, + FilterButton({ + active: filter.error, + label: "Errors", + filterKey: MESSAGE_LEVEL.ERROR, + dispatch + }), + FilterButton({ + active: filter.warn, + label: "Warnings", + filterKey: MESSAGE_LEVEL.WARN, + dispatch + }), + FilterButton({ + active: filter.log, + label: "Logs", + filterKey: MESSAGE_LEVEL.LOG, + dispatch + }), + FilterButton({ + active: filter.info, + label: "Info", + filterKey: MESSAGE_LEVEL.INFO, + dispatch + }), + FilterButton({ + active: filter.debug, + label: "Debug", + filterKey: MESSAGE_LEVEL.DEBUG, + dispatch + }), + dom.span({ + className: "devtools-separator", + }), + FilterButton({ + active: filter.netxhr, + label: "XHR", + filterKey: "netxhr", + dispatch + }), + FilterButton({ + active: filter.net, + label: "Requests", + filterKey: "net", + dispatch + }) + ) + ); + } + + if (ui.filteredMessageVisible) { + children.push( + dom.div({className: "devtools-toolbar"}, + dom.span({ + className: "clear"}, + "You have filters set that may hide some results. " + + "Learn more about our filtering syntax ", + dom.a({}, "here"), + "."), + dom.button({ + className: "menu-filter-button", + onClick: this.onClickFiltersClear + }, "Remove filters") + ) + ); + } + + return ( + dom.div({ + className: "webconsole-filteringbar-wrapper", + ref: node => { + this.wrapperNode = node; + } + }, ...children + ) + ); + } +}); + +function mapStateToProps(state) { + return { + filter: getAllFilters(state), + ui: getAllUi(state) + }; +} + +module.exports = connect(mapStateToProps)(FilterBar); diff --git a/devtools/client/webconsole/new-console-output/components/filter-button.js b/devtools/client/webconsole/new-console-output/components/filter-button.js new file mode 100644 index 000000000..4116bb524 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/filter-button.js @@ -0,0 +1,46 @@ +/* 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 { + createClass, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const actions = require("devtools/client/webconsole/new-console-output/actions/index"); + +const FilterButton = createClass({ + + displayName: "FilterButton", + + propTypes: { + label: PropTypes.string.isRequired, + filterKey: PropTypes.string.isRequired, + active: PropTypes.bool.isRequired, + dispatch: PropTypes.func.isRequired, + }, + + onClick: function () { + this.props.dispatch(actions.filterToggle(this.props.filterKey)); + }, + + render() { + const {active, label, filterKey} = this.props; + + let classList = [ + "menu-filter-button", + filterKey, + ]; + if (active) { + classList.push("checked"); + } + + return dom.button({ + className: classList.join(" "), + onClick: this.onClick + }, label); + } +}); + +module.exports = FilterButton; diff --git a/devtools/client/webconsole/new-console-output/components/grip-message-body.js b/devtools/client/webconsole/new-console-output/components/grip-message-body.js new file mode 100644 index 000000000..29c2e6a4f --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js @@ -0,0 +1,102 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// If this is being run from Mocha, then the browser loader hasn't set up +// define. We need to do that before loading Rep. +if (typeof define === "undefined") { + require("amd-loader"); +} + +// React +const { + createFactory, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const { createFactories } = require("devtools/client/shared/components/reps/rep-utils"); +const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep")); +const StringRep = createFactories(require("devtools/client/shared/components/reps/string").StringRep).rep; +const VariablesViewLink = createFactory(require("devtools/client/webconsole/new-console-output/components/variables-view-link")); +const { Grip } = require("devtools/client/shared/components/reps/grip"); + +GripMessageBody.displayName = "GripMessageBody"; + +GripMessageBody.propTypes = { + grip: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.object, + ]).isRequired, + serviceContainer: PropTypes.shape({ + createElement: PropTypes.func.isRequired, + }), + userProvidedStyle: PropTypes.string, +}; + +function GripMessageBody(props) { + const { grip, userProvidedStyle, serviceContainer } = props; + + let styleObject; + if (userProvidedStyle && userProvidedStyle !== "") { + styleObject = cleanupStyle(userProvidedStyle, serviceContainer.createElement); + } + + return ( + // @TODO once there is a longString rep, also turn off quotes for those. + typeof grip === "string" + ? StringRep({ + object: grip, + useQuotes: false, + mode: props.mode, + style: styleObject + }) + : Rep({ + object: grip, + objectLink: VariablesViewLink, + defaultRep: Grip, + mode: props.mode, + }) + ); +} + +function cleanupStyle(userProvidedStyle, createElement) { + // Regular expression that matches the allowed CSS property names. + const allowedStylesRegex = new RegExp( + "^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|" + + "margin|padding|text|transition|outline|white-space|word|writing|" + + "(?:min-|max-)?width|(?:min-|max-)?height)" + ); + + // Regular expression that matches the forbidden CSS property values. + const forbiddenValuesRegexs = [ + // url(), -moz-element() + /\b(?:url|(?:-moz-)?element)[\s('"]+/gi, + + // various URL protocols + /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi, + ]; + + // Use a dummy element to parse the style string. + let dummy = createElement("div"); + dummy.style = userProvidedStyle; + + // Return a style object as expected by React DOM components, e.g. + // {color: "red"} + // without forbidden properties and values. + return [...dummy.style] + .filter(name => { + return allowedStylesRegex.test(name) + && !forbiddenValuesRegexs.some(regex => regex.test(dummy.style[name])); + }) + .reduce((object, name) => { + return Object.assign({ + [name]: dummy.style[name] + }, object); + }, {}); +} + +module.exports = GripMessageBody; diff --git a/devtools/client/webconsole/new-console-output/components/message-container.js b/devtools/client/webconsole/new-console-output/components/message-container.js new file mode 100644 index 000000000..115e9e291 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-container.js @@ -0,0 +1,92 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + createClass, + createFactory, + PropTypes +} = require("devtools/client/shared/vendor/react"); + +const { + MESSAGE_SOURCE, + MESSAGE_TYPE +} = require("devtools/client/webconsole/new-console-output/constants"); + +const componentMap = new Map([ + ["ConsoleApiCall", require("./message-types/console-api-call")], + ["ConsoleCommand", require("./message-types/console-command")], + ["DefaultRenderer", require("./message-types/default-renderer")], + ["EvaluationResult", require("./message-types/evaluation-result")], + ["NetworkEventMessage", require("./message-types/network-event-message")], + ["PageError", require("./message-types/page-error")] +]); + +const MessageContainer = createClass({ + displayName: "MessageContainer", + + propTypes: { + message: PropTypes.object.isRequired, + open: PropTypes.bool.isRequired, + serviceContainer: PropTypes.object.isRequired, + autoscroll: PropTypes.bool.isRequired, + indent: PropTypes.number.isRequired, + }, + + getDefaultProps: function () { + return { + open: false, + indent: 0, + }; + }, + + shouldComponentUpdate(nextProps, nextState) { + const repeatChanged = this.props.message.repeat !== nextProps.message.repeat; + const openChanged = this.props.open !== nextProps.open; + const tableDataChanged = this.props.tableData !== nextProps.tableData; + return repeatChanged || openChanged || tableDataChanged; + }, + + render() { + const { message } = this.props; + + let MessageComponent = createFactory(getMessageComponent(message)); + return MessageComponent(this.props); + } +}); + +function getMessageComponent(message) { + switch (message.source) { + case MESSAGE_SOURCE.CONSOLE_API: + return componentMap.get("ConsoleApiCall"); + case MESSAGE_SOURCE.NETWORK: + return componentMap.get("NetworkEventMessage"); + case MESSAGE_SOURCE.JAVASCRIPT: + switch (message.type) { + case MESSAGE_TYPE.COMMAND: + return componentMap.get("ConsoleCommand"); + case MESSAGE_TYPE.RESULT: + return componentMap.get("EvaluationResult"); + // @TODO this is probably not the right behavior, but works for now. + // Chrome doesn't distinguish between page errors and log messages. We + // may want to remove the PageError component and just handle errors + // with ConsoleApiCall. + case MESSAGE_TYPE.LOG: + return componentMap.get("PageError"); + default: + return componentMap.get("DefaultRenderer"); + } + } + + return componentMap.get("DefaultRenderer"); +} + +module.exports.MessageContainer = MessageContainer; + +// Exported so we can test it with unit tests. +module.exports.getMessageComponent = getMessageComponent; diff --git a/devtools/client/webconsole/new-console-output/components/message-icon.js b/devtools/client/webconsole/new-console-output/components/message-icon.js new file mode 100644 index 000000000..b4c32fda0 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-icon.js @@ -0,0 +1,32 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages"); + +MessageIcon.displayName = "MessageIcon"; + +MessageIcon.propTypes = { + level: PropTypes.string.isRequired, +}; + +function MessageIcon(props) { + const { level } = props; + + const title = l10n.getStr("level." + level); + return dom.div({ + className: "icon", + title + }); +} + +module.exports = MessageIcon; diff --git a/devtools/client/webconsole/new-console-output/components/message-indent.js b/devtools/client/webconsole/new-console-output/components/message-indent.js new file mode 100644 index 000000000..354e13589 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-indent.js @@ -0,0 +1,37 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + createClass, + DOM: dom, + PropTypes, +} = require("devtools/client/shared/vendor/react"); + +const INDENT_WIDTH = 12; +const MessageIndent = createClass({ + + displayName: "MessageIndent", + + propTypes: { + indent: PropTypes.number.isRequired, + }, + + render: function () { + const { indent } = this.props; + return dom.span({ + className: "indent", + style: {"width": indent * INDENT_WIDTH} + }); + } +}); + +module.exports.MessageIndent = MessageIndent; + +// Exported so we can test it with unit tests. +module.exports.INDENT_WIDTH = INDENT_WIDTH; diff --git a/devtools/client/webconsole/new-console-output/components/message-repeat.js b/devtools/client/webconsole/new-console-output/components/message-repeat.js new file mode 100644 index 000000000..1820340ea --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-repeat.js @@ -0,0 +1,36 @@ + +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const { PluralForm } = require("devtools/shared/plural-form"); +const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages"); + +MessageRepeat.displayName = "MessageRepeat"; + +MessageRepeat.propTypes = { + repeat: PropTypes.number.isRequired +}; + +function MessageRepeat(props) { + const { repeat } = props; + const visibility = repeat > 1 ? "visible" : "hidden"; + + return dom.span({ + className: "message-repeats", + style: {visibility}, + title: PluralForm.get(repeat, l10n.getStr("messageRepeats.tooltip2")) + .replace("#1", repeat) + }, repeat); +} + +module.exports = MessageRepeat; diff --git a/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js new file mode 100644 index 000000000..7200648fa --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js @@ -0,0 +1,132 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + createFactory, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body")); +const ConsoleTable = createFactory(require("devtools/client/webconsole/new-console-output/components/console-table")); +const {isGroupType, l10n} = require("devtools/client/webconsole/new-console-output/utils/messages"); + +const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message")); + +ConsoleApiCall.displayName = "ConsoleApiCall"; + +ConsoleApiCall.propTypes = { + message: PropTypes.object.isRequired, + open: PropTypes.bool, + serviceContainer: PropTypes.object.isRequired, + indent: PropTypes.number.isRequired, +}; + +ConsoleApiCall.defaultProps = { + open: false, + indent: 0, +}; + +function ConsoleApiCall(props) { + const { + dispatch, + message, + open, + tableData, + serviceContainer, + indent, + } = props; + const { + id: messageId, + source, + type, + level, + repeat, + stacktrace, + frame, + parameters, + messageText, + userProvidedStyles, + } = message; + + let messageBody; + if (type === "trace") { + messageBody = dom.span({className: "cm-variable"}, "console.trace()"); + } else if (type === "assert") { + let reps = formatReps(parameters); + messageBody = dom.span({ className: "cm-variable" }, "Assertion failed: ", reps); + } else if (type === "table") { + // TODO: Chrome does not output anything, see if we want to keep this + messageBody = dom.span({className: "cm-variable"}, "console.table()"); + } else if (parameters) { + messageBody = formatReps(parameters, userProvidedStyles, serviceContainer); + } else { + messageBody = messageText; + } + + let attachment = null; + if (type === "table") { + attachment = ConsoleTable({ + dispatch, + id: message.id, + serviceContainer, + parameters: message.parameters, + tableData + }); + } + + let collapseTitle = null; + if (isGroupType(type)) { + collapseTitle = l10n.getStr("groupToggle"); + } + + const collapsible = isGroupType(type) + || (type === "error" && Array.isArray(stacktrace)); + const topLevelClasses = ["cm-s-mozilla"]; + + return Message({ + messageId, + open, + collapsible, + collapseTitle, + source, + type, + level, + topLevelClasses, + messageBody, + repeat, + frame, + stacktrace, + attachment, + serviceContainer, + dispatch, + indent, + }); +} + +function formatReps(parameters, userProvidedStyles, serviceContainer) { + return ( + parameters + // Get all the grips. + .map((grip, key) => GripMessageBody({ + grip, + key, + userProvidedStyle: userProvidedStyles ? userProvidedStyles[key] : null, + serviceContainer + })) + // Interleave spaces. + .reduce((arr, v, i) => { + return i + 1 < parameters.length + ? arr.concat(v, dom.span({}, " ")) + : arr.concat(v); + }, []) + ); +} + +module.exports = ConsoleApiCall; + diff --git a/devtools/client/webconsole/new-console-output/components/message-types/console-command.js b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js new file mode 100644 index 000000000..d87229fa9 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + createFactory, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message")); + +ConsoleCommand.displayName = "ConsoleCommand"; + +ConsoleCommand.propTypes = { + message: PropTypes.object.isRequired, + autoscroll: PropTypes.bool.isRequired, + indent: PropTypes.number.isRequired, +}; + +ConsoleCommand.defaultProps = { + indent: 0, +}; + +/** + * Displays input from the console. + */ +function ConsoleCommand(props) { + const { autoscroll, indent, message } = props; + const { + source, + type, + level, + messageText: messageBody, + } = message; + + const { + serviceContainer, + } = props; + + const childProps = { + source, + type, + level, + topLevelClasses: [], + messageBody, + scrollToMessage: autoscroll, + serviceContainer, + indent: indent, + }; + return Message(childProps); +} + +module.exports = ConsoleCommand; diff --git a/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js b/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js new file mode 100644 index 000000000..d07089531 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js @@ -0,0 +1,22 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + DOM: dom, +} = require("devtools/client/shared/vendor/react"); + +DefaultRenderer.displayName = "DefaultRenderer"; + +function DefaultRenderer(props) { + return dom.div({}, + "This message type is not supported yet." + ); +} + +module.exports = DefaultRenderer; diff --git a/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js new file mode 100644 index 000000000..992dc62cf --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js @@ -0,0 +1,64 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + createFactory, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message")); +const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body")); + +EvaluationResult.displayName = "EvaluationResult"; + +EvaluationResult.propTypes = { + message: PropTypes.object.isRequired, + indent: PropTypes.number.isRequired, +}; + +EvaluationResult.defaultProps = { + indent: 0, +}; + +function EvaluationResult(props) { + const { message, serviceContainer, indent } = props; + const { + source, + type, + level, + id: messageId, + exceptionDocURL, + frame, + } = message; + + let messageBody; + if (message.messageText) { + messageBody = message.messageText; + } else { + messageBody = GripMessageBody({grip: message.parameters}); + } + + const topLevelClasses = ["cm-s-mozilla"]; + + const childProps = { + source, + type, + level, + indent, + topLevelClasses, + messageBody, + messageId, + scrollToMessage: props.autoscroll, + serviceContainer, + exceptionDocURL, + frame, + }; + return Message(childProps); +} + +module.exports = EvaluationResult; diff --git a/devtools/client/webconsole/new-console-output/components/message-types/moz.build b/devtools/client/webconsole/new-console-output/components/message-types/moz.build new file mode 100644 index 000000000..9b9f72017 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/moz.build @@ -0,0 +1,13 @@ +# 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( + 'console-api-call.js', + 'console-command.js', + 'default-renderer.js', + 'evaluation-result.js', + 'network-event-message.js', + 'page-error.js', +) diff --git a/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js new file mode 100644 index 000000000..e3c81a487 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js @@ -0,0 +1,63 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + createFactory, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message")); +const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages"); + +NetworkEventMessage.displayName = "NetworkEventMessage"; + +NetworkEventMessage.propTypes = { + message: PropTypes.object.isRequired, + serviceContainer: PropTypes.shape({ + openNetworkPanel: PropTypes.func.isRequired, + }), + indent: PropTypes.number.isRequired, +}; + +NetworkEventMessage.defaultProps = { + indent: 0, +}; + +function NetworkEventMessage(props) { + const { message, serviceContainer, indent } = props; + const { actor, source, type, level, request, isXHR } = message; + + const topLevelClasses = [ "cm-s-mozilla" ]; + + function onUrlClick() { + serviceContainer.openNetworkPanel(actor); + } + + const method = dom.span({className: "method" }, request.method); + const xhr = isXHR + ? dom.span({ className: "xhr" }, l10n.getStr("webConsoleXhrIndicator")) + : null; + const url = dom.a({ className: "url", title: request.url, onClick: onUrlClick }, + request.url.replace(/\?.+/, "")); + + const messageBody = dom.span({}, method, xhr, url); + + const childProps = { + source, + type, + level, + indent, + topLevelClasses, + messageBody, + serviceContainer, + }; + return Message(childProps); +} + +module.exports = NetworkEventMessage; diff --git a/devtools/client/webconsole/new-console-output/components/message-types/page-error.js b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js new file mode 100644 index 000000000..77ea75ff7 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js @@ -0,0 +1,69 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + createFactory, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message")); + +PageError.displayName = "PageError"; + +PageError.propTypes = { + message: PropTypes.object.isRequired, + open: PropTypes.bool, + indent: PropTypes.number.isRequired, +}; + +PageError.defaultProps = { + open: false, + indent: 0, +}; + +function PageError(props) { + const { + dispatch, + message, + open, + serviceContainer, + indent, + } = props; + const { + id: messageId, + source, + type, + level, + messageText: messageBody, + repeat, + stacktrace, + frame, + exceptionDocURL, + } = message; + + const childProps = { + dispatch, + messageId, + open, + collapsible: Array.isArray(stacktrace), + source, + type, + level, + topLevelClasses: [], + indent, + messageBody, + repeat, + frame, + stacktrace, + serviceContainer, + exceptionDocURL, + }; + return Message(childProps); +} + +module.exports = PageError; diff --git a/devtools/client/webconsole/new-console-output/components/message.js b/devtools/client/webconsole/new-console-output/components/message.js new file mode 100644 index 000000000..f36bff7e4 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message.js @@ -0,0 +1,176 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + createClass, + createFactory, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages"); +const actions = require("devtools/client/webconsole/new-console-output/actions/index"); +const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button")); +const MessageIndent = createFactory(require("devtools/client/webconsole/new-console-output/components/message-indent").MessageIndent); +const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon")); +const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat")); +const FrameView = createFactory(require("devtools/client/shared/components/frame")); +const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace")); + +const Message = createClass({ + displayName: "Message", + + propTypes: { + open: PropTypes.bool, + collapsible: PropTypes.bool, + collapseTitle: PropTypes.string, + source: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + level: PropTypes.string.isRequired, + indent: PropTypes.number.isRequired, + topLevelClasses: PropTypes.array.isRequired, + messageBody: PropTypes.any.isRequired, + repeat: PropTypes.any, + frame: PropTypes.any, + attachment: PropTypes.any, + stacktrace: PropTypes.any, + messageId: PropTypes.string, + scrollToMessage: PropTypes.bool, + exceptionDocURL: PropTypes.string, + serviceContainer: PropTypes.shape({ + emitNewMessage: PropTypes.func.isRequired, + onViewSourceInDebugger: PropTypes.func.isRequired, + sourceMapService: PropTypes.any, + }), + }, + + getDefaultProps: function () { + return { + indent: 0 + }; + }, + + componentDidMount() { + if (this.messageNode) { + if (this.props.scrollToMessage) { + this.messageNode.scrollIntoView(); + } + // Event used in tests. Some message types don't pass it in because existing tests + // did not emit for them. + if (this.props.serviceContainer) { + this.props.serviceContainer.emitNewMessage(this.messageNode, this.props.messageId); + } + } + }, + + onLearnMoreClick: function () { + let {exceptionDocURL} = this.props; + this.props.serviceContainer.openLink(exceptionDocURL); + }, + + render() { + const { + messageId, + open, + collapsible, + collapseTitle, + source, + type, + level, + indent, + topLevelClasses, + messageBody, + frame, + stacktrace, + serviceContainer, + dispatch, + exceptionDocURL, + } = this.props; + + topLevelClasses.push("message", source, type, level); + if (open) { + topLevelClasses.push("open"); + } + + const icon = MessageIcon({level}); + + // Figure out if there is an expandable part to the message. + let attachment = null; + if (this.props.attachment) { + attachment = this.props.attachment; + } else if (stacktrace) { + const child = open ? StackTrace({ + stacktrace: stacktrace, + onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger + }) : null; + attachment = dom.div({ className: "stacktrace devtools-monospace" }, child); + } + + // If there is an expandable part, make it collapsible. + let collapse = null; + if (collapsible) { + collapse = CollapseButton({ + open, + title: collapseTitle, + onClick: function () { + if (open) { + dispatch(actions.messageClose(messageId)); + } else { + dispatch(actions.messageOpen(messageId)); + } + }, + }); + } + + const repeat = this.props.repeat ? MessageRepeat({repeat: this.props.repeat}) : null; + + // Configure the location. + const location = dom.span({ className: "message-location devtools-monospace" }, + frame ? FrameView({ + frame, + onClick: serviceContainer ? serviceContainer.onViewSourceInDebugger : undefined, + showEmptyPathAsHost: true, + sourceMapService: serviceContainer ? serviceContainer.sourceMapService : undefined + }) : null + ); + + let learnMore; + if (exceptionDocURL) { + learnMore = dom.a({ + className: "learn-more-link webconsole-learn-more-link", + title: exceptionDocURL.split("?")[0], + onClick: this.onLearnMoreClick, + }, `[${l10n.getStr("webConsoleMoreInfoLabel")}]`); + } + + return dom.div({ + className: topLevelClasses.join(" "), + ref: node => { + this.messageNode = node; + } + }, + // @TODO add timestamp + MessageIndent({indent}), + icon, + collapse, + dom.span({ className: "message-body-wrapper" }, + dom.span({ className: "message-flex-body" }, + dom.span({ className: "message-body devtools-monospace" }, + messageBody, + learnMore + ), + repeat, + location + ), + attachment + ) + ); + } +}); + +module.exports = Message; diff --git a/devtools/client/webconsole/new-console-output/components/moz.build b/devtools/client/webconsole/new-console-output/components/moz.build new file mode 100644 index 000000000..8c0022314 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/moz.build @@ -0,0 +1,23 @@ +# 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/. + +DIRS += [ + 'message-types' +] + +DevToolsModules( + 'collapse-button.js', + 'console-output.js', + 'console-table.js', + 'filter-bar.js', + 'filter-button.js', + 'grip-message-body.js', + 'message-container.js', + 'message-icon.js', + 'message-indent.js', + 'message-repeat.js', + 'message.js', + 'variables-view-link.js' +) diff --git a/devtools/client/webconsole/new-console-output/components/variables-view-link.js b/devtools/client/webconsole/new-console-output/components/variables-view-link.js new file mode 100644 index 000000000..4d79c322f --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/variables-view-link.js @@ -0,0 +1,34 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=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"; + +// React & Redux +const { + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const {openVariablesView} = require("devtools/client/webconsole/new-console-output/utils/variables-view"); + +VariablesViewLink.displayName = "VariablesViewLink"; + +VariablesViewLink.propTypes = { + object: PropTypes.object.isRequired +}; + +function VariablesViewLink(props) { + const { object, children } = props; + + return ( + dom.a({ + onClick: openVariablesView.bind(null, object), + className: "cm-variable", + draggable: false, + }, children) + ); +} + +module.exports = VariablesViewLink; |