summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/components/Output
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/webconsole/components/Output
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--devtools/client/webconsole/components/Output/CollapseButton.js31
-rw-r--r--devtools/client/webconsole/components/Output/ConsoleOutput.js294
-rw-r--r--devtools/client/webconsole/components/Output/ConsoleTable.js271
-rw-r--r--devtools/client/webconsole/components/Output/GripMessageBody.js148
-rw-r--r--devtools/client/webconsole/components/Output/Message.js455
-rw-r--r--devtools/client/webconsole/components/Output/MessageContainer.js148
-rw-r--r--devtools/client/webconsole/components/Output/MessageIcon.js67
-rw-r--r--devtools/client/webconsole/components/Output/MessageIndent.js38
-rw-r--r--devtools/client/webconsole/components/Output/MessageRepeat.js33
-rw-r--r--devtools/client/webconsole/components/Output/message-types/CSSWarning.js170
-rw-r--r--devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js217
-rw-r--r--devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js105
-rw-r--r--devtools/client/webconsole/components/Output/message-types/DefaultRenderer.js15
-rw-r--r--devtools/client/webconsole/components/Output/message-types/EvaluationResult.js120
-rw-r--r--devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js234
-rw-r--r--devtools/client/webconsole/components/Output/message-types/PageError.js123
-rw-r--r--devtools/client/webconsole/components/Output/message-types/WarningGroup.js76
-rw-r--r--devtools/client/webconsole/components/Output/message-types/moz.build15
-rw-r--r--devtools/client/webconsole/components/Output/moz.build20
19 files changed, 2580 insertions, 0 deletions
diff --git a/devtools/client/webconsole/components/Output/CollapseButton.js b/devtools/client/webconsole/components/Output/CollapseButton.js
new file mode 100644
index 0000000000..4a1463e7fc
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/CollapseButton.js
@@ -0,0 +1,31 @@
+/* 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 dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const { l10n } = require("devtools/client/webconsole/utils/messages");
+const messageToggleDetails = l10n.getStr("messageToggleDetails");
+
+function CollapseButton(props) {
+ const { open, onClick, title = messageToggleDetails } = props;
+
+ return dom.button({
+ "aria-expanded": open ? "true" : "false",
+ "aria-label": title,
+ className: "arrow collapse-button",
+ onClick,
+ onMouseDown: e => {
+ // prevent focus from moving to the disclosure if clicked,
+ // which is annoying if on the input
+ e.preventDefault();
+ // Clearing the text selection to allow the message to collpase.
+ e.target.ownerDocument.defaultView.getSelection().removeAllRanges();
+ },
+ title: title,
+ });
+}
+
+module.exports = CollapseButton;
diff --git a/devtools/client/webconsole/components/Output/ConsoleOutput.js b/devtools/client/webconsole/components/Output/ConsoleOutput.js
new file mode 100644
index 0000000000..6950e01069
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/ConsoleOutput.js
@@ -0,0 +1,294 @@
+/* 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 {
+ Component,
+ createElement,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const {
+ connect,
+} = require("devtools/client/shared/redux/visibility-handler-connect");
+const { initialize } = require("devtools/client/webconsole/actions/ui");
+
+const {
+ getAllMessagesById,
+ getAllMessagesUiById,
+ getAllMessagesPayloadById,
+ getAllNetworkMessagesUpdateById,
+ getVisibleMessages,
+ getAllRepeatById,
+ getAllWarningGroupsById,
+ isMessageInWarningGroup,
+} = require("devtools/client/webconsole/selectors/messages");
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "devtools/client/shared/vendor/react-prop-types"
+);
+loader.lazyRequireGetter(
+ this,
+ "MessageContainer",
+ "devtools/client/webconsole/components/Output/MessageContainer",
+ true
+);
+
+const { MESSAGE_TYPE } = require("devtools/client/webconsole/constants");
+const {
+ getInitialMessageCountForViewport,
+} = require("devtools/client/webconsole/utils/messages.js");
+
+class ConsoleOutput extends Component {
+ static get propTypes() {
+ return {
+ initialized: PropTypes.bool.isRequired,
+ messages: PropTypes.object.isRequired,
+ messagesUi: PropTypes.array.isRequired,
+ serviceContainer: PropTypes.shape({
+ attachRefToWebConsoleUI: PropTypes.func.isRequired,
+ openContextMenu: PropTypes.func.isRequired,
+ sourceMapURLService: PropTypes.object,
+ }),
+ dispatch: PropTypes.func.isRequired,
+ timestampsVisible: PropTypes.bool,
+ messagesPayload: PropTypes.object.isRequired,
+ messagesRepeat: PropTypes.object.isRequired,
+ warningGroups: PropTypes.object.isRequired,
+ networkMessagesUpdate: PropTypes.object.isRequired,
+ visibleMessages: PropTypes.array.isRequired,
+ networkMessageActiveTabId: PropTypes.string.isRequired,
+ onFirstMeaningfulPaint: PropTypes.func.isRequired,
+ editorMode: PropTypes.bool.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onContextMenu = this.onContextMenu.bind(this);
+ this.maybeScrollToBottom = this.maybeScrollToBottom.bind(this);
+
+ this.resizeObserver = new ResizeObserver(entries => {
+ // If we don't have the outputNode reference, or if the outputNode isn't connected
+ // anymore, we disconnect the resize observer (componentWillUnmount is never called
+ // on this component, so we have to do it here).
+ if (!this.outputNode || !this.outputNode.isConnected) {
+ this.resizeObserver.disconnect();
+ return;
+ }
+
+ if (this.scrolledToBottom) {
+ this.scrollToBottom();
+ }
+ });
+ }
+
+ componentDidMount() {
+ if (this.props.visibleMessages.length > 0) {
+ this.scrollToBottom();
+ }
+
+ this.lastMessageIntersectionObserver = new IntersectionObserver(
+ entries => {
+ for (const entry of entries) {
+ // Consider that we're not pinned to the bottom anymore if the last message is
+ // less than half-visible.
+ this.scrolledToBottom = entry.intersectionRatio >= 0.5;
+ }
+ },
+ { root: this.outputNode, threshold: [0.5] }
+ );
+
+ this.resizeObserver.observe(this.getElementToObserve());
+
+ const { serviceContainer, onFirstMeaningfulPaint, dispatch } = this.props;
+ serviceContainer.attachRefToWebConsoleUI("outputScroller", this.outputNode);
+
+ // Waiting for the next paint.
+ new Promise(res => requestAnimationFrame(res)).then(() => {
+ if (onFirstMeaningfulPaint) {
+ onFirstMeaningfulPaint();
+ }
+
+ // Dispatching on next tick so we don't block on action execution.
+ setTimeout(() => {
+ dispatch(initialize());
+ }, 0);
+ });
+ }
+
+ componentWillUpdate(nextProps, nextState) {
+ if (nextProps.editorMode !== this.props.editorMode) {
+ this.resizeObserver.disconnect();
+ }
+
+ const { outputNode } = this;
+ if (!outputNode?.lastChild) {
+ // Force a scroll to bottom when messages are added to an empty console.
+ // This makes the console stay pinned to the bottom if a batch of messages
+ // are added after a page refresh (Bug 1402237).
+ this.shouldScrollBottom = true;
+ return;
+ }
+
+ const { lastChild } = outputNode;
+ this.lastMessageIntersectionObserver.unobserve(lastChild);
+
+ // We need to scroll to the bottom if:
+ // - we are reacting to "initialize" action, and we are already scrolled to the bottom
+ // - the number of messages displayed changed and we are already scrolled to the
+ // bottom, but not if we are reacting to a group opening.
+ // - the number of messages in the store changed and the new message is an evaluation
+ // result.
+
+ const visibleMessagesDelta =
+ nextProps.visibleMessages.length - this.props.visibleMessages.length;
+ const messagesDelta = nextProps.messages.size - this.props.messages.size;
+ const isNewMessageEvaluationResult =
+ messagesDelta > 0 &&
+ [...nextProps.messages.values()][nextProps.messages.size - 1].type ===
+ MESSAGE_TYPE.RESULT;
+
+ const messagesUiDelta =
+ nextProps.messagesUi.length - this.props.messagesUi.length;
+ const isOpeningGroup =
+ messagesUiDelta > 0 &&
+ nextProps.messagesUi.some(
+ id =>
+ !this.props.messagesUi.includes(id) &&
+ nextProps.messagesUi.includes(id) &&
+ this.props.visibleMessages.includes(id) &&
+ nextProps.visibleMessages.includes(id)
+ );
+
+ this.shouldScrollBottom =
+ (!this.props.initialized &&
+ nextProps.initialized &&
+ this.scrolledToBottom) ||
+ isNewMessageEvaluationResult ||
+ (this.scrolledToBottom && visibleMessagesDelta > 0 && !isOpeningGroup);
+ }
+
+ componentDidUpdate(prevProps) {
+ this.maybeScrollToBottom();
+ if (this?.outputNode?.lastChild) {
+ this.lastMessageIntersectionObserver.observe(this.outputNode.lastChild);
+ }
+
+ if (prevProps.editorMode !== this.props.editorMode) {
+ this.resizeObserver.observe(this.getElementToObserve());
+ }
+ }
+
+ maybeScrollToBottom() {
+ if (this.outputNode && this.shouldScrollBottom) {
+ this.scrollToBottom();
+ }
+ }
+
+ scrollToBottom() {
+ if (this.outputNode.scrollHeight > this.outputNode.clientHeight) {
+ this.outputNode.scrollTop = this.outputNode.scrollHeight;
+ }
+
+ this.scrolledToBottom = true;
+ }
+
+ getElementToObserve() {
+ // In inline mode, we need to observe the output node parent, which contains both the
+ // output and the input, so we don't trigger the resizeObserver callback when only the
+ // output size changes (e.g. when a network request is expanded).
+ return this.props.editorMode
+ ? this.outputNode
+ : this.outputNode?.parentNode;
+ }
+
+ onContextMenu(e) {
+ this.props.serviceContainer.openContextMenu(e);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ render() {
+ let {
+ dispatch,
+ visibleMessages,
+ messages,
+ messagesUi,
+ messagesPayload,
+ messagesRepeat,
+ warningGroups,
+ networkMessagesUpdate,
+ networkMessageActiveTabId,
+ serviceContainer,
+ timestampsVisible,
+ initialized,
+ } = this.props;
+
+ if (!initialized) {
+ const numberMessagesFitViewport = getInitialMessageCountForViewport(
+ window
+ );
+ if (numberMessagesFitViewport < visibleMessages.length) {
+ visibleMessages = visibleMessages.slice(
+ visibleMessages.length - numberMessagesFitViewport
+ );
+ }
+ }
+
+ const messageNodes = visibleMessages.map(messageId =>
+ createElement(MessageContainer, {
+ dispatch,
+ key: messageId,
+ messageId,
+ serviceContainer,
+ open: messagesUi.includes(messageId),
+ payload: messagesPayload.get(messageId),
+ timestampsVisible,
+ repeat: messagesRepeat[messageId],
+ badge: warningGroups.has(messageId)
+ ? warningGroups.get(messageId).length
+ : null,
+ inWarningGroup:
+ warningGroups && warningGroups.size > 0
+ ? isMessageInWarningGroup(messages.get(messageId), visibleMessages)
+ : false,
+ networkMessageUpdate: networkMessagesUpdate[messageId],
+ networkMessageActiveTabId,
+ getMessage: () => messages.get(messageId),
+ maybeScrollToBottom: this.maybeScrollToBottom,
+ })
+ );
+
+ return dom.div(
+ {
+ className: "webconsole-output",
+ role: "main",
+ onContextMenu: this.onContextMenu,
+ ref: node => {
+ this.outputNode = node;
+ },
+ },
+ messageNodes
+ );
+ }
+}
+
+function mapStateToProps(state, props) {
+ return {
+ initialized: state.ui.initialized,
+ messages: getAllMessagesById(state),
+ visibleMessages: getVisibleMessages(state),
+ messagesUi: getAllMessagesUiById(state),
+ messagesPayload: getAllMessagesPayloadById(state),
+ messagesRepeat: getAllRepeatById(state),
+ warningGroups: getAllWarningGroupsById(state),
+ networkMessagesUpdate: getAllNetworkMessagesUpdateById(state),
+ timestampsVisible: state.ui.timestampsVisible,
+ networkMessageActiveTabId: state.ui.networkMessageActiveTabId,
+ };
+}
+
+module.exports = connect(mapStateToProps)(ConsoleOutput);
diff --git a/devtools/client/webconsole/components/Output/ConsoleTable.js b/devtools/client/webconsole/components/Output/ConsoleTable.js
new file mode 100644
index 0000000000..49d1fda2db
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/ConsoleTable.js
@@ -0,0 +1,271 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const {
+ l10n,
+ getArrayTypeNames,
+ getDescriptorValue,
+} = require("devtools/client/webconsole/utils/messages");
+loader.lazyGetter(this, "MODE", function() {
+ return require("devtools/client/shared/components/reps/index").MODE;
+});
+
+const GripMessageBody = createFactory(
+ require("devtools/client/webconsole/components/Output/GripMessageBody")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "devtools/client/shared/vendor/react-prop-types"
+);
+
+const TABLE_ROW_MAX_ITEMS = 1000;
+// Match Chrome max column number.
+const TABLE_COLUMN_MAX_ITEMS = 21;
+
+class ConsoleTable extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ parameters: PropTypes.array.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ id: PropTypes.string.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.getHeaders = this.getHeaders.bind(this);
+ this.getRows = this.getRows.bind(this);
+ }
+
+ getHeaders(columns) {
+ const headerItems = [];
+ columns.forEach((value, key) =>
+ headerItems.push(
+ dom.div(
+ {
+ className: "new-consoletable-header",
+ role: "columnheader",
+ key,
+ title: value,
+ },
+ value
+ )
+ )
+ );
+ return headerItems;
+ }
+
+ getRows(columns, items) {
+ const { dispatch, serviceContainer } = this.props;
+
+ return items.map((item, index) => {
+ const cells = [];
+ const className = index % 2 ? "odd" : "even";
+
+ columns.forEach((value, key) => {
+ const cellValue = item[key];
+ const cellContent =
+ typeof cellValue === "undefined"
+ ? ""
+ : GripMessageBody({
+ grip: cellValue,
+ mode: MODE.SHORT,
+ useQuotes: false,
+ serviceContainer,
+ dispatch,
+ });
+
+ cells.push(
+ dom.div(
+ {
+ role: "gridcell",
+ className,
+ key,
+ },
+ cellContent
+ )
+ );
+ });
+ return cells;
+ });
+ }
+
+ render() {
+ const { parameters } = this.props;
+ const { valueGrip, headersGrip } = getValueAndHeadersGrip(parameters);
+
+ const headers = headersGrip?.preview ? headersGrip.preview.items : null;
+
+ const data = valueGrip?.ownProperties;
+
+ // if we don't have any data, don't show anything.
+ if (!data) {
+ return null;
+ }
+
+ const dataType = getParametersDataType(parameters);
+ const { columns, items } = getTableItems(data, dataType, headers);
+
+ return dom.div(
+ {
+ className: "new-consoletable",
+ role: "grid",
+ style: {
+ gridTemplateColumns: `repeat(${columns.size}, calc(100% / ${columns.size}))`,
+ },
+ },
+ this.getHeaders(columns),
+ this.getRows(columns, items)
+ );
+ }
+}
+
+function getValueAndHeadersGrip(parameters) {
+ const [valueFront, headersFront] = parameters;
+
+ const headersGrip = headersFront?.getGrip
+ ? headersFront.getGrip()
+ : headersFront;
+
+ const valueGrip = valueFront?.getGrip ? valueFront.getGrip() : valueFront;
+
+ return { valueGrip, headersGrip };
+}
+
+function getParametersDataType(parameters = null) {
+ if (!Array.isArray(parameters) || parameters.length === 0) {
+ return null;
+ }
+ const [firstParam] = parameters;
+ if (!firstParam || !firstParam.getGrip) {
+ return null;
+ }
+ const grip = firstParam.getGrip();
+ return grip.class;
+}
+
+const INDEX_NAME = "_index";
+const VALUE_NAME = "_value";
+
+function getNamedIndexes(type) {
+ return {
+ [INDEX_NAME]: getArrayTypeNames()
+ .concat("Object")
+ .includes(type)
+ ? l10n.getStr("table.index")
+ : l10n.getStr("table.iterationIndex"),
+ [VALUE_NAME]: l10n.getStr("table.value"),
+ key: l10n.getStr("table.key"),
+ };
+}
+
+function hasValidCustomHeaders(headers) {
+ return (
+ Array.isArray(headers) &&
+ headers.every(
+ header => typeof header === "string" || Number.isInteger(Number(header))
+ )
+ );
+}
+
+function getTableItems(data = {}, type, headers = null) {
+ const namedIndexes = getNamedIndexes(type);
+
+ let columns = new Map();
+ const items = [];
+
+ const addItem = function(item) {
+ items.push(item);
+ Object.keys(item).forEach(key => addColumn(key));
+ };
+
+ const validCustomHeaders = hasValidCustomHeaders(headers);
+
+ const addColumn = function(columnIndex) {
+ const columnExists = columns.has(columnIndex);
+ const hasMaxColumns = columns.size == TABLE_COLUMN_MAX_ITEMS;
+
+ if (
+ !columnExists &&
+ !hasMaxColumns &&
+ (!validCustomHeaders ||
+ headers.includes(columnIndex) ||
+ columnIndex === INDEX_NAME)
+ ) {
+ columns.set(columnIndex, namedIndexes[columnIndex] || columnIndex);
+ }
+ };
+
+ for (let [index, property] of Object.entries(data)) {
+ if (type !== "Object" && index == parseInt(index, 10)) {
+ index = parseInt(index, 10);
+ }
+
+ const item = {
+ [INDEX_NAME]: index,
+ };
+
+ const propertyValue = getDescriptorValue(property);
+ const propertyValueGrip = propertyValue?.getGrip
+ ? propertyValue.getGrip()
+ : propertyValue;
+
+ if (propertyValueGrip?.ownProperties) {
+ const entries = propertyValueGrip.ownProperties;
+ for (const [key, entry] of Object.entries(entries)) {
+ item[key] = getDescriptorValue(entry);
+ }
+ } else if (
+ propertyValueGrip?.preview &&
+ (type === "Map" || type === "WeakMap")
+ ) {
+ item.key = propertyValueGrip.preview.key;
+ item[VALUE_NAME] = propertyValueGrip.preview.value;
+ } else {
+ item[VALUE_NAME] = propertyValue;
+ }
+
+ 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 (validCustomHeaders) {
+ headers.forEach(header => addColumn(header));
+ }
+
+ // We want to always have the index column first
+ if (columns.has(INDEX_NAME)) {
+ const 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)) {
+ const 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/components/Output/GripMessageBody.js b/devtools/client/webconsole/components/Output/GripMessageBody.js
new file mode 100644
index 0000000000..a6ac4231b2
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/GripMessageBody.js
@@ -0,0 +1,148 @@
+/* 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
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const {
+ MESSAGE_TYPE,
+ JSTERM_COMMANDS,
+} = require("devtools/client/webconsole/constants");
+const {
+ getObjectInspector,
+} = require("devtools/client/webconsole/utils/object-inspector");
+const actions = require("devtools/client/webconsole/actions/index");
+
+loader.lazyGetter(this, "objectInspector", function() {
+ return require("devtools/client/shared/components/reps/index")
+ .objectInspector;
+});
+
+loader.lazyGetter(this, "MODE", function() {
+ return require("devtools/client/shared/components/reps/index").MODE;
+});
+
+GripMessageBody.displayName = "GripMessageBody";
+
+GripMessageBody.propTypes = {
+ grip: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ PropTypes.object,
+ ]).isRequired,
+ serviceContainer: PropTypes.shape({
+ createElement: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ }),
+ userProvidedStyle: PropTypes.string,
+ useQuotes: PropTypes.bool,
+ escapeWhitespace: PropTypes.bool,
+ type: PropTypes.string,
+ helperType: PropTypes.string,
+ maybeScrollToBottom: PropTypes.func,
+};
+
+GripMessageBody.defaultProps = {
+ mode: MODE.LONG,
+};
+
+function GripMessageBody(props) {
+ const {
+ grip,
+ userProvidedStyle,
+ serviceContainer,
+ useQuotes,
+ escapeWhitespace,
+ mode = MODE.LONG,
+ dispatch,
+ maybeScrollToBottom,
+ customFormat = false,
+ } = props;
+
+ let styleObject;
+ if (userProvidedStyle && userProvidedStyle !== "") {
+ styleObject = cleanupStyle(
+ userProvidedStyle,
+ serviceContainer.createElement
+ );
+ }
+
+ const objectInspectorProps = {
+ autoExpandDepth: shouldAutoExpandObjectInspector(props) ? 1 : 0,
+ mode,
+ maybeScrollToBottom,
+ customFormat,
+ onCmdCtrlClick: (node, { depth, event, focused, expanded }) => {
+ const front = objectInspector.utils.node.getFront(node);
+ if (front) {
+ dispatch(actions.showObjectInSidebar(front));
+ }
+ },
+ };
+
+ if (
+ typeof grip === "string" ||
+ (grip && grip.type === "longString") ||
+ (grip?.getGrip && grip.getGrip().type === "longString")
+ ) {
+ Object.assign(objectInspectorProps, {
+ useQuotes,
+ transformEmptyString: true,
+ escapeWhitespace,
+ style: styleObject,
+ });
+ }
+
+ return getObjectInspector(grip, serviceContainer, objectInspectorProps);
+}
+
+// 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 = [
+ // -moz-element()
+ /\b((?:-moz-)?element)[\s('"]+/gi,
+
+ // various URL protocols
+ /['"(]*(?:chrome|resource|about|app|https?|ftp|file):+\/*/gi,
+];
+
+function cleanupStyle(userProvidedStyle, createElement) {
+ // Use a dummy element to parse the style string.
+ const 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 Array.from(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
+ );
+ }, {});
+}
+
+function shouldAutoExpandObjectInspector(props) {
+ const { helperType, type } = props;
+
+ return type === MESSAGE_TYPE.DIR || helperType === JSTERM_COMMANDS.INSPECT;
+}
+
+module.exports = GripMessageBody;
diff --git a/devtools/client/webconsole/components/Output/Message.js b/devtools/client/webconsole/components/Output/Message.js
new file mode 100644
index 0000000000..bdcb6825a3
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/Message.js
@@ -0,0 +1,455 @@
+/* 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 {
+ Component,
+ createFactory,
+ createElement,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { l10n } = require("devtools/client/webconsole/utils/messages");
+const actions = require("devtools/client/webconsole/actions/index");
+const {
+ MESSAGE_LEVEL,
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+} = require("devtools/client/webconsole/constants");
+const {
+ MessageIndent,
+} = require("devtools/client/webconsole/components/Output/MessageIndent");
+const MessageIcon = require("devtools/client/webconsole/components/Output/MessageIcon");
+const FrameView = createFactory(
+ require("devtools/client/shared/components/Frame")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "CollapseButton",
+ "devtools/client/webconsole/components/Output/CollapseButton"
+);
+loader.lazyRequireGetter(
+ this,
+ "MessageRepeat",
+ "devtools/client/webconsole/components/Output/MessageRepeat"
+);
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "devtools/client/shared/vendor/react-prop-types"
+);
+loader.lazyRequireGetter(
+ this,
+ "SmartTrace",
+ "devtools/client/shared/components/SmartTrace"
+);
+
+class Message extends Component {
+ static get propTypes() {
+ return {
+ open: PropTypes.bool,
+ collapsible: PropTypes.bool,
+ collapseTitle: PropTypes.string,
+ onToggle: PropTypes.func,
+ source: PropTypes.string.isRequired,
+ type: PropTypes.string.isRequired,
+ level: PropTypes.string.isRequired,
+ indent: PropTypes.number.isRequired,
+ inWarningGroup: PropTypes.bool,
+ isBlockedNetworkMessage: PropTypes.bool,
+ 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,
+ request: PropTypes.object,
+ dispatch: PropTypes.func,
+ timeStamp: PropTypes.number,
+ timestampsVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.shape({
+ emitForTests: PropTypes.func.isRequired,
+ onViewSource: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func,
+ onViewSourceInStyleEditor: PropTypes.func,
+ openContextMenu: PropTypes.func.isRequired,
+ openLink: PropTypes.func.isRequired,
+ sourceMapURLService: PropTypes.any,
+ }),
+ notes: PropTypes.arrayOf(
+ PropTypes.shape({
+ messageBody: PropTypes.string.isRequired,
+ frame: PropTypes.any,
+ })
+ ),
+ maybeScrollToBottom: PropTypes.func,
+ message: PropTypes.object.isRequired,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ indent: 0,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onLearnMoreClick = this.onLearnMoreClick.bind(this);
+ this.toggleMessage = this.toggleMessage.bind(this);
+ this.onContextMenu = this.onContextMenu.bind(this);
+ this.renderIcon = this.renderIcon.bind(this);
+ }
+
+ componentDidMount() {
+ if (this.messageNode) {
+ if (this.props.scrollToMessage) {
+ this.messageNode.scrollIntoView();
+ }
+
+ this.emitNewMessage(this.messageNode);
+ }
+ }
+
+ componentDidCatch(e) {
+ this.setState({ error: e });
+ }
+
+ // Event used in tests. Some message types don't pass it in because existing tests
+ // did not emit for them.
+ emitNewMessage(node) {
+ const { serviceContainer, messageId, timeStamp } = this.props;
+ serviceContainer.emitForTests(
+ "new-messages",
+ new Set([{ node, messageId, timeStamp }])
+ );
+ }
+
+ onLearnMoreClick(e) {
+ const { exceptionDocURL } = this.props;
+ this.props.serviceContainer.openLink(exceptionDocURL, e);
+ e.preventDefault();
+ }
+
+ toggleMessage(e) {
+ // Don't bubble up to the main App component, which redirects focus to input,
+ // making difficult for screen reader users to review output
+ e.stopPropagation();
+ const { open, dispatch, messageId, onToggle } = this.props;
+
+ // Early exit the function to avoid the message to collapse if the user is
+ // selecting a range in the toggle message.
+ const window = e.target.ownerDocument.defaultView;
+ if (window.getSelection && window.getSelection().type === "Range") {
+ return;
+ }
+
+ // If defined on props, we let the onToggle() method handle the toggling,
+ // otherwise we toggle the message open/closed ourselves.
+ if (onToggle) {
+ onToggle(messageId, e);
+ } else if (open) {
+ dispatch(actions.messageClose(messageId));
+ } else {
+ dispatch(actions.messageOpen(messageId));
+ }
+ }
+
+ onContextMenu(e) {
+ const { serviceContainer, source, request, messageId } = this.props;
+ const messageInfo = {
+ source,
+ request,
+ messageId,
+ };
+ serviceContainer.openContextMenu(e, messageInfo);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ renderIcon() {
+ const { level, inWarningGroup, isBlockedNetworkMessage, type } = this.props;
+
+ if (inWarningGroup) {
+ return undefined;
+ }
+
+ if (isBlockedNetworkMessage) {
+ return MessageIcon({
+ level: MESSAGE_LEVEL.ERROR,
+ type: "blockedReason",
+ });
+ }
+
+ return MessageIcon({
+ level,
+ type,
+ });
+ }
+
+ renderTimestamp() {
+ if (!this.props.timestampsVisible) {
+ return null;
+ }
+
+ return dom.span(
+ {
+ className: "timestamp devtools-monospace",
+ },
+ l10n.timestampString(this.props.timeStamp || Date.now())
+ );
+ }
+
+ renderErrorState() {
+ const newBugUrl =
+ "https://bugzilla.mozilla.org/enter_bug.cgi?product=DevTools&component=Console";
+ const timestampEl = this.renderTimestamp();
+
+ return dom.div(
+ {
+ className: "message error message-did-catch",
+ },
+ timestampEl,
+ MessageIcon({ level: "error" }),
+ dom.span(
+ { className: "message-body-wrapper" },
+ dom.span(
+ {
+ className: "message-flex-body",
+ },
+ // Add whitespaces for formatting when copying to the clipboard.
+ timestampEl ? " " : null,
+ dom.span(
+ { className: "message-body devtools-monospace" },
+ l10n.getFormatStr("webconsole.message.componentDidCatch.label", [
+ newBugUrl,
+ ]),
+ dom.button(
+ {
+ className: "devtools-button",
+ onClick: () =>
+ navigator.clipboard.writeText(
+ JSON.stringify(
+ this.props.message,
+ function(key, value) {
+ // The message can hold one or multiple fronts that we need to serialize
+ if (value?.getGrip) {
+ return value.getGrip();
+ }
+ return value;
+ },
+ 2
+ )
+ ),
+ },
+ l10n.getStr(
+ "webconsole.message.componentDidCatch.copyButton.label"
+ )
+ )
+ )
+ )
+ ),
+ dom.br()
+ );
+ }
+
+ // eslint-disable-next-line complexity
+ render() {
+ if (this.state && this.state.error) {
+ return this.renderErrorState();
+ }
+
+ const {
+ open,
+ collapsible,
+ collapseTitle,
+ source,
+ type,
+ level,
+ indent,
+ inWarningGroup,
+ topLevelClasses,
+ messageBody,
+ frame,
+ stacktrace,
+ serviceContainer,
+ exceptionDocURL,
+ messageId,
+ notes,
+ } = this.props;
+
+ topLevelClasses.push("message", source, type, level);
+ if (open) {
+ topLevelClasses.push("open");
+ }
+
+ const timestampEl = this.renderTimestamp();
+ const icon = this.renderIcon();
+
+ // 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 && open) {
+ attachment = dom.div(
+ {
+ className: "stacktrace devtools-monospace",
+ },
+ createElement(SmartTrace, {
+ stacktrace,
+ onViewSourceInDebugger:
+ serviceContainer.onViewSourceInDebugger ||
+ serviceContainer.onViewSource,
+ onViewSource: serviceContainer.onViewSource,
+ onReady: this.props.maybeScrollToBottom,
+ sourceMapURLService: serviceContainer.sourceMapURLService,
+ })
+ );
+ }
+
+ // If there is an expandable part, make it collapsible.
+ let collapse = null;
+ if (collapsible) {
+ collapse = createElement(CollapseButton, {
+ open,
+ title: collapseTitle,
+ onClick: this.toggleMessage,
+ });
+ }
+
+ let notesNodes;
+ if (notes) {
+ notesNodes = notes.map(note =>
+ dom.span(
+ { className: "message-flex-body error-note" },
+ dom.span(
+ { className: "message-body devtools-monospace" },
+ "note: " + note.messageBody
+ ),
+ dom.span(
+ { className: "message-location devtools-monospace" },
+ note.frame
+ ? FrameView({
+ frame: note.frame,
+ onClick: serviceContainer
+ ? serviceContainer.onViewSourceInDebugger ||
+ serviceContainer.onViewSource
+ : undefined,
+ showEmptyPathAsHost: true,
+ sourceMapURLService: serviceContainer
+ ? serviceContainer.sourceMapURLService
+ : undefined,
+ })
+ : null
+ )
+ )
+ );
+ } else {
+ notesNodes = [];
+ }
+
+ const repeat =
+ this.props.repeat && this.props.repeat > 1
+ ? createElement(MessageRepeat, { repeat: this.props.repeat })
+ : null;
+
+ let onFrameClick;
+ if (serviceContainer && frame) {
+ if (source === MESSAGE_SOURCE.CSS) {
+ onFrameClick =
+ serviceContainer.onViewSourceInStyleEditor ||
+ serviceContainer.onViewSource;
+ } else {
+ // Point everything else to debugger, if source not available,
+ // it will fall back to view-source.
+ onFrameClick =
+ serviceContainer.onViewSourceInDebugger ||
+ serviceContainer.onViewSource;
+ }
+ }
+
+ // Configure the location.
+ const location = dom.span(
+ { className: "message-location devtools-monospace" },
+ frame
+ ? FrameView({
+ frame,
+ onClick: onFrameClick,
+ showEmptyPathAsHost: true,
+ sourceMapURLService: serviceContainer
+ ? serviceContainer.sourceMapURLService
+ : undefined,
+ messageSource: source,
+ })
+ : null
+ );
+
+ let learnMore;
+ if (exceptionDocURL) {
+ learnMore = dom.a(
+ {
+ className: "learn-more-link webconsole-learn-more-link",
+ href: exceptionDocURL,
+ title: exceptionDocURL.split("?")[0],
+ onClick: this.onLearnMoreClick,
+ },
+ `[${l10n.getStr("webConsoleMoreInfoLabel")}]`
+ );
+ }
+
+ const bodyElements = Array.isArray(messageBody)
+ ? messageBody
+ : [messageBody];
+
+ return dom.div(
+ {
+ className: topLevelClasses.join(" "),
+ onContextMenu: this.onContextMenu,
+ ref: node => {
+ this.messageNode = node;
+ },
+ "data-message-id": messageId,
+ "aria-live": type === MESSAGE_TYPE.COMMAND ? "off" : "polite",
+ },
+ timestampEl,
+ MessageIndent({
+ indent,
+ inWarningGroup,
+ }),
+ this.props.isBlockedNetworkMessage ? collapse : icon,
+ this.props.isBlockedNetworkMessage ? icon : collapse,
+ dom.span(
+ { className: "message-body-wrapper" },
+ dom.span(
+ {
+ className: "message-flex-body",
+ onClick: collapsible ? this.toggleMessage : undefined,
+ },
+ // Add whitespaces for formatting when copying to the clipboard.
+ timestampEl ? " " : null,
+ dom.span(
+ { className: "message-body devtools-monospace" },
+ ...bodyElements,
+ learnMore
+ ),
+ repeat ? " " : null,
+ repeat,
+ " ",
+ location
+ ),
+ attachment,
+ ...notesNodes
+ ),
+ // If an attachment is displayed, the final newline is handled by the attachment.
+ attachment ? null : dom.br()
+ );
+ }
+}
+
+module.exports = Message;
diff --git a/devtools/client/webconsole/components/Output/MessageContainer.js b/devtools/client/webconsole/components/Output/MessageContainer.js
new file mode 100644
index 0000000000..cbc4ed163b
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageContainer.js
@@ -0,0 +1,148 @@
+/* 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 { Component } = require("devtools/client/shared/vendor/react");
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "devtools/client/shared/vendor/react-prop-types"
+);
+loader.lazyRequireGetter(
+ this,
+ "isWarningGroup",
+ "devtools/client/webconsole/utils/messages",
+ true
+);
+
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+} = require("devtools/client/webconsole/constants");
+
+const componentMap = new Map([
+ [
+ "ConsoleApiCall",
+ require("devtools/client/webconsole/components/Output/message-types/ConsoleApiCall"),
+ ],
+ [
+ "ConsoleCommand",
+ require("devtools/client/webconsole/components/Output/message-types/ConsoleCommand"),
+ ],
+ [
+ "CSSWarning",
+ require("devtools/client/webconsole/components/Output/message-types/CSSWarning"),
+ ],
+ [
+ "DefaultRenderer",
+ require("devtools/client/webconsole/components/Output/message-types/DefaultRenderer"),
+ ],
+ [
+ "EvaluationResult",
+ require("devtools/client/webconsole/components/Output/message-types/EvaluationResult"),
+ ],
+ [
+ "NetworkEventMessage",
+ require("devtools/client/webconsole/components/Output/message-types/NetworkEventMessage"),
+ ],
+ [
+ "PageError",
+ require("devtools/client/webconsole/components/Output/message-types/PageError"),
+ ],
+ [
+ "WarningGroup",
+ require("devtools/client/webconsole/components/Output/message-types/WarningGroup"),
+ ],
+]);
+
+class MessageContainer extends Component {
+ static get propTypes() {
+ return {
+ messageId: PropTypes.string.isRequired,
+ open: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ payload: PropTypes.object,
+ timestampsVisible: PropTypes.bool.isRequired,
+ repeat: PropTypes.number,
+ badge: PropTypes.number,
+ indent: PropTypes.number,
+ networkMessageUpdate: PropTypes.object,
+ getMessage: PropTypes.func.isRequired,
+ inWarningGroup: PropTypes.bool,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ open: false,
+ };
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const triggeringUpdateProps = [
+ "repeat",
+ "open",
+ "payload",
+ "timestampsVisible",
+ "networkMessageUpdate",
+ "badge",
+ "inWarningGroup",
+ ];
+
+ return triggeringUpdateProps.some(
+ prop => this.props[prop] !== nextProps[prop]
+ );
+ }
+
+ render() {
+ const message = this.props.getMessage();
+
+ const MessageComponent = getMessageComponent(message);
+ return MessageComponent(Object.assign({ message }, this.props));
+ }
+}
+
+function getMessageComponent(message) {
+ if (!message) {
+ return componentMap.get("DefaultRenderer");
+ }
+
+ switch (message.source) {
+ case MESSAGE_SOURCE.CONSOLE_API:
+ return componentMap.get("ConsoleApiCall");
+ case MESSAGE_SOURCE.NETWORK:
+ return componentMap.get("NetworkEventMessage");
+ case MESSAGE_SOURCE.CSS:
+ return componentMap.get("CSSWarning");
+ 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");
+ }
+ case MESSAGE_SOURCE.CONSOLE_FRONTEND:
+ if (isWarningGroup(message)) {
+ return componentMap.get("WarningGroup");
+ }
+ break;
+ }
+
+ 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/components/Output/MessageIcon.js b/devtools/client/webconsole/components/Output/MessageIcon.js
new file mode 100644
index 0000000000..3569e8531a
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageIcon.js
@@ -0,0 +1,67 @@
+/* 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 = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { l10n } = require("devtools/client/webconsole/utils/messages");
+
+const l10nLevels = {
+ error: "level.error",
+ warn: "level.warn",
+ info: "level.info",
+ log: "level.log",
+ debug: "level.debug",
+};
+
+// Store common icons so they can be used without recreating the element
+// during render.
+const CONSTANT_ICONS = Object.entries(l10nLevels).reduce(
+ (acc, [key, l10nLabel]) => {
+ acc[key] = getIconElement(l10nLabel);
+ return acc;
+ },
+ {}
+);
+
+function getIconElement(level, type) {
+ let title = l10n.getStr(l10nLevels[level] || level);
+ const classnames = ["icon"];
+
+ if (type && type === "logPoint") {
+ title = l10n.getStr("logpoint.title");
+ classnames.push("logpoint");
+ }
+
+ if (type && type === "blockedReason") {
+ title = l10n.getStr("blockedrequest.label");
+ }
+
+ {
+ return dom.span({
+ className: classnames.join(" "),
+ title,
+ "aria-live": "off",
+ });
+ }
+}
+
+MessageIcon.displayName = "MessageIcon";
+MessageIcon.propTypes = {
+ level: PropTypes.string.isRequired,
+ type: PropTypes.string,
+};
+
+function MessageIcon(props) {
+ const { level, type } = props;
+
+ if (type) {
+ return getIconElement(level, type);
+ }
+
+ return CONSTANT_ICONS[level] || getIconElement(level);
+}
+
+module.exports = MessageIcon;
diff --git a/devtools/client/webconsole/components/Output/MessageIndent.js b/devtools/client/webconsole/components/Output/MessageIndent.js
new file mode 100644
index 0000000000..91b3c31dc3
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageIndent.js
@@ -0,0 +1,38 @@
+/* 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 dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const INDENT_WIDTH = 12;
+
+// Store common indents so they can be used without recreating the element during render.
+const CONSTANT_INDENTS = [getIndentElement(0), getIndentElement(1)];
+const IN_WARNING_GROUP_INDENT = getIndentElement(1, "warning-indent");
+
+function getIndentElement(indent, className) {
+ return dom.span({
+ "data-indent": indent,
+ className: `indent${className ? " " + className : ""}`,
+ style: {
+ width: indent * INDENT_WIDTH,
+ },
+ });
+}
+
+function MessageIndent(props) {
+ const { indent, inWarningGroup } = props;
+
+ if (inWarningGroup) {
+ return IN_WARNING_GROUP_INDENT;
+ }
+
+ return CONSTANT_INDENTS[indent] || getIndentElement(indent);
+}
+
+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/components/Output/MessageRepeat.js b/devtools/client/webconsole/components/Output/MessageRepeat.js
new file mode 100644
index 0000000000..647bd975ad
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageRepeat.js
@@ -0,0 +1,33 @@
+/* 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 = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { PluralForm } = require("devtools/shared/plural-form");
+const { l10n } = require("devtools/client/webconsole/utils/messages");
+const messageRepeatsTooltip = l10n.getStr("messageRepeats.tooltip2");
+
+MessageRepeat.displayName = "MessageRepeat";
+
+MessageRepeat.propTypes = {
+ repeat: PropTypes.number.isRequired,
+};
+
+function MessageRepeat(props) {
+ const { repeat } = props;
+ return dom.span(
+ {
+ className: "message-repeats",
+ title: PluralForm.get(repeat, messageRepeatsTooltip).replace(
+ "#1",
+ repeat
+ ),
+ },
+ repeat
+ );
+}
+
+module.exports = MessageRepeat;
diff --git a/devtools/client/webconsole/components/Output/message-types/CSSWarning.js b/devtools/client/webconsole/components/Output/message-types/CSSWarning.js
new file mode 100644
index 0000000000..9a7cabf442
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/CSSWarning.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 {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { l10n } = require("devtools/client/webconsole/utils/messages");
+const actions = require("devtools/client/webconsole/actions/index");
+
+const Message = createFactory(
+ require("devtools/client/webconsole/components/Output/Message")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "GripMessageBody",
+ "devtools/client/webconsole/components/Output/GripMessageBody"
+);
+
+/**
+ * This component is responsible for rendering CSS warnings in the Console panel.
+ *
+ * CSS warnings are expandable when they have associated CSS selectors so the
+ * user can inspect any matching DOM elements. Not all CSS warnings have
+ * associated selectors (those that don't are not expandable) and not all
+ * selectors match elements in the current page (warnings can appear for styles
+ * which don't apply to the current page).
+ *
+ * @extends Component
+ */
+class CSSWarning extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ inWarningGroup: PropTypes.bool.isRequired,
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool,
+ payload: PropTypes.object,
+ repeat: PropTypes.any,
+ serviceContainer: PropTypes.object,
+ timestampsVisible: PropTypes.bool.isRequired,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ open: false,
+ };
+ }
+
+ static get displayName() {
+ return "CSSWarning";
+ }
+
+ constructor(props) {
+ super(props);
+ this.onToggle = this.onToggle.bind(this);
+ }
+
+ onToggle(messageId) {
+ const { dispatch, message, payload, open } = this.props;
+
+ const { cssSelectors } = message;
+
+ if (open) {
+ dispatch(actions.messageClose(messageId));
+ } else if (payload) {
+ // If the message already has information about the elements matching
+ // the selectors associated with this CSS warning, just open the message.
+ dispatch(actions.messageOpen(messageId));
+ } else {
+ // Query the server for elements matching the CSS selectors associated
+ // with this CSS warning and populate the message's additional data payload with
+ // the result. It's an async operation and potentially expensive, so we only do it
+ // on demand, once, when the component is first expanded.
+ dispatch(actions.messageGetMatchingElements(messageId, cssSelectors));
+ dispatch(actions.messageOpen(messageId));
+ }
+ }
+
+ render() {
+ const {
+ dispatch,
+ message,
+ open,
+ payload,
+ repeat,
+ serviceContainer,
+ timestampsVisible,
+ inWarningGroup,
+ } = this.props;
+
+ const {
+ id: messageId,
+ indent,
+ cssSelectors,
+ source,
+ type,
+ level,
+ messageText,
+ frame,
+ exceptionDocURL,
+ timeStamp,
+ notes,
+ } = message;
+
+ let messageBody;
+ if (typeof messageText === "string") {
+ messageBody = messageText;
+ } else if (
+ typeof messageText === "object" &&
+ messageText.type === "longString"
+ ) {
+ messageBody = `${message.messageText.initial}…`;
+ }
+
+ // Create a message attachment only when the message is open and there is a result
+ // to the query for elements matching the CSS selectors associated with the message.
+ const attachment =
+ open &&
+ payload !== undefined &&
+ dom.div(
+ { className: "devtools-monospace" },
+ dom.div(
+ { className: "elements-label" },
+ l10n.getFormatStr("webconsole.cssWarningElements.label", [
+ cssSelectors,
+ ])
+ ),
+ GripMessageBody({
+ dispatch,
+ escapeWhitespace: false,
+ grip: payload,
+ serviceContainer,
+ })
+ );
+
+ return Message({
+ attachment,
+ collapsible: !!cssSelectors.length,
+ dispatch,
+ exceptionDocURL,
+ frame,
+ indent,
+ inWarningGroup,
+ level,
+ messageBody,
+ messageId,
+ notes,
+ open,
+ onToggle: this.onToggle,
+ repeat,
+ serviceContainer,
+ source,
+ timeStamp,
+ timestampsVisible,
+ topLevelClasses: [],
+ type,
+ message,
+ });
+ }
+}
+
+module.exports = createFactory(CSSWarning);
diff --git a/devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js b/devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js
new file mode 100644
index 0000000000..bce693fab7
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js
@@ -0,0 +1,217 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const GripMessageBody = require("devtools/client/webconsole/components/Output/GripMessageBody");
+const ConsoleTable = createFactory(
+ require("devtools/client/webconsole/components/Output/ConsoleTable")
+);
+const {
+ isGroupType,
+ l10n,
+} = require("devtools/client/webconsole/utils/messages");
+
+const Message = createFactory(
+ require("devtools/client/webconsole/components/Output/Message")
+);
+
+ConsoleApiCall.displayName = "ConsoleApiCall";
+
+ConsoleApiCall.propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool,
+ serviceContainer: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ maybeScrollToBottom: PropTypes.func,
+};
+
+ConsoleApiCall.defaultProps = {
+ open: false,
+};
+
+function ConsoleApiCall(props) {
+ const {
+ dispatch,
+ message,
+ open,
+ payload,
+ serviceContainer,
+ timestampsVisible,
+ repeat,
+ maybeScrollToBottom,
+ } = props;
+ const {
+ id: messageId,
+ indent,
+ source,
+ type,
+ level,
+ stacktrace,
+ frame,
+ timeStamp,
+ parameters,
+ messageText,
+ prefix,
+ userProvidedStyles,
+ } = message;
+
+ let messageBody;
+ const messageBodyConfig = {
+ dispatch,
+ messageId,
+ parameters,
+ userProvidedStyles,
+ serviceContainer,
+ type,
+ maybeScrollToBottom,
+ // When the object is a parameter of a console.dir call, we always want to show its
+ // properties, like regular object (i.e. not showing the DOM tree for an Element, or
+ // only showing the message + stacktrace for Error object).
+ customFormat: type !== "dir",
+ };
+
+ if (type === "trace") {
+ const traceParametersBody =
+ Array.isArray(parameters) && parameters.length > 0
+ ? [" "].concat(formatReps(messageBodyConfig))
+ : [];
+
+ messageBody = [
+ dom.span({ className: "cm-variable" }, "console.trace()"),
+ ...traceParametersBody,
+ ];
+ } else if (type === "assert") {
+ const reps = formatReps(messageBodyConfig);
+ messageBody = dom.span({}, "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(messageBodyConfig);
+ if (prefix) {
+ messageBody.unshift(
+ dom.span(
+ {
+ className: "console-message-prefix",
+ },
+ `${prefix}: `
+ )
+ );
+ }
+ } else if (typeof messageText === "string") {
+ messageBody = messageText;
+ } else if (messageText) {
+ messageBody = GripMessageBody({
+ dispatch,
+ messageId,
+ grip: messageText,
+ serviceContainer,
+ useQuotes: false,
+ transformEmptyString: true,
+ type,
+ });
+ }
+
+ let attachment = null;
+ if (type === "table") {
+ attachment = ConsoleTable({
+ dispatch,
+ id: message.id,
+ serviceContainer,
+ parameters: message.parameters,
+ tableData: payload,
+ });
+ }
+
+ 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,
+ timeStamp,
+ timestampsVisible,
+ parameters,
+ message,
+ maybeScrollToBottom,
+ });
+}
+
+function formatReps(options = {}) {
+ const {
+ dispatch,
+ loadedObjectProperties,
+ loadedObjectEntries,
+ messageId,
+ parameters,
+ serviceContainer,
+ userProvidedStyles,
+ type,
+ maybeScrollToBottom,
+ customFormat,
+ } = options;
+
+ return (
+ parameters
+ // Get all the grips.
+ .map((grip, key) =>
+ GripMessageBody({
+ dispatch,
+ messageId,
+ grip,
+ key,
+ userProvidedStyle: userProvidedStyles
+ ? userProvidedStyles[key]
+ : null,
+ serviceContainer,
+ useQuotes: false,
+ loadedObjectProperties,
+ loadedObjectEntries,
+ type,
+ maybeScrollToBottom,
+ customFormat,
+ })
+ )
+ // Interleave spaces.
+ .reduce((arr, v, i) => {
+ // We need to interleave a space if we are not on the last element AND
+ // if we are not between 2 messages with user provided style.
+ const needSpace =
+ i + 1 < parameters.length &&
+ (!userProvidedStyles ||
+ userProvidedStyles[i] === undefined ||
+ userProvidedStyles[i + 1] === undefined);
+
+ return needSpace ? arr.concat(v, " ") : arr.concat(v);
+ }, [])
+ );
+}
+
+module.exports = ConsoleApiCall;
diff --git a/devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js b/devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js
new file mode 100644
index 0000000000..4c43c6ea53
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js
@@ -0,0 +1,105 @@
+/* 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 {
+ createElement,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { ELLIPSIS } = require("devtools/shared/l10n");
+const Message = createFactory(
+ require("devtools/client/webconsole/components/Output/Message")
+);
+
+ConsoleCommand.displayName = "ConsoleCommand";
+
+ConsoleCommand.propTypes = {
+ message: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object,
+ maybeScrollToBottom: PropTypes.func,
+ open: PropTypes.bool,
+};
+
+ConsoleCommand.defaultProps = {
+ open: false,
+};
+
+/**
+ * Displays input from the console.
+ */
+function ConsoleCommand(props) {
+ const {
+ message,
+ timestampsVisible,
+ serviceContainer,
+ maybeScrollToBottom,
+ dispatch,
+ open,
+ } = props;
+
+ const { indent, source, type, level, timeStamp, id: messageId } = message;
+
+ const messageText = trimCode(message.messageText);
+ const messageLines = messageText.split("\n");
+
+ const collapsible = messageLines.length > 5;
+
+ // Show only first 5 lines if its collapsible and closed
+ const visibleMessageText =
+ collapsible && !open
+ ? `${messageLines.slice(0, 5).join("\n")}${ELLIPSIS}`
+ : messageText;
+
+ // This uses a Custom Element to syntax highlight when possible. If it's not
+ // (no CodeMirror editor), then it will just render text.
+ const messageBody = createElement(
+ "syntax-highlighted",
+ null,
+ visibleMessageText
+ );
+
+ // Enable collapsing the code if it has multiple lines
+
+ return Message({
+ messageId,
+ source,
+ type,
+ level,
+ topLevelClasses: [],
+ messageBody,
+ collapsible,
+ open,
+ dispatch,
+ serviceContainer,
+ indent,
+ timeStamp,
+ timestampsVisible,
+ maybeScrollToBottom,
+ message,
+ });
+}
+
+module.exports = ConsoleCommand;
+
+/**
+ * Trim user input to avoid blank lines before and after messages
+ */
+function trimCode(input) {
+ if (typeof input !== "string") {
+ return input;
+ }
+
+ // Trim on both edges if we have a single line of content
+ if (input.trim().includes("\n") === false) {
+ return input.trim();
+ }
+
+ // For multiline input we want to keep the indentation of the first line
+ // with non-whitespace, so we can't .trim()/.trimStart().
+ return input.replace(/^\s*\n/, "").trimEnd();
+}
diff --git a/devtools/client/webconsole/components/Output/message-types/DefaultRenderer.js b/devtools/client/webconsole/components/Output/message-types/DefaultRenderer.js
new file mode 100644
index 0000000000..2262e54f2c
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/DefaultRenderer.js
@@ -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/. */
+
+"use strict";
+
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+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/components/Output/message-types/EvaluationResult.js b/devtools/client/webconsole/components/Output/message-types/EvaluationResult.js
new file mode 100644
index 0000000000..1d6e6d27d4
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/EvaluationResult.js
@@ -0,0 +1,120 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const Message = createFactory(
+ require("devtools/client/webconsole/components/Output/Message")
+);
+const GripMessageBody = require("devtools/client/webconsole/components/Output/GripMessageBody");
+
+EvaluationResult.displayName = "EvaluationResult";
+
+EvaluationResult.propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object,
+ maybeScrollToBottom: PropTypes.func,
+ open: PropTypes.bool,
+};
+
+EvaluationResult.defaultProps = {
+ open: false,
+};
+
+function EvaluationResult(props) {
+ const {
+ dispatch,
+ message,
+ serviceContainer,
+ timestampsVisible,
+ maybeScrollToBottom,
+ open,
+ } = props;
+
+ const {
+ source,
+ type,
+ helperType,
+ level,
+ id: messageId,
+ indent,
+ hasException,
+ exceptionDocURL,
+ stacktrace,
+ frame,
+ timeStamp,
+ parameters,
+ notes,
+ } = message;
+
+ let messageBody;
+ if (
+ typeof message.messageText !== "undefined" &&
+ message.messageText !== null
+ ) {
+ const messageText = message.messageText?.getGrip
+ ? message.messageText.getGrip()
+ : message.messageText;
+ if (typeof messageText === "string") {
+ messageBody = messageText;
+ } else if (
+ typeof messageText === "object" &&
+ messageText.type === "longString"
+ ) {
+ messageBody = `${messageText.initial}…`;
+ }
+ } else {
+ messageBody = [];
+ if (hasException) {
+ messageBody.push("Uncaught ");
+ }
+ messageBody.push(
+ GripMessageBody({
+ dispatch,
+ messageId,
+ grip: parameters[0],
+ key: "grip",
+ serviceContainer,
+ useQuotes: !hasException,
+ escapeWhitespace: false,
+ type,
+ helperType,
+ maybeScrollToBottom,
+ customFormat: true,
+ })
+ );
+ }
+
+ const topLevelClasses = ["cm-s-mozilla"];
+
+ return Message({
+ dispatch,
+ source,
+ type,
+ level,
+ indent,
+ topLevelClasses,
+ messageBody,
+ messageId,
+ serviceContainer,
+ exceptionDocURL,
+ stacktrace,
+ collapsible: Array.isArray(stacktrace),
+ open,
+ frame,
+ timeStamp,
+ parameters,
+ notes,
+ timestampsVisible,
+ maybeScrollToBottom,
+ message,
+ });
+}
+
+module.exports = EvaluationResult;
diff --git a/devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js b/devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js
new file mode 100644
index 0000000000..69c57777de
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js
@@ -0,0 +1,234 @@
+/* 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,
+ createElement,
+} = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const Message = createFactory(
+ require("devtools/client/webconsole/components/Output/Message")
+);
+const actions = require("devtools/client/webconsole/actions/index");
+const {
+ isMessageNetworkError,
+ l10n,
+} = require("devtools/client/webconsole/utils/messages");
+
+loader.lazyRequireGetter(
+ this,
+ "TabboxPanel",
+ "devtools/client/netmonitor/src/components/TabboxPanel"
+);
+const {
+ getHTTPStatusCodeURL,
+} = require("devtools/client/netmonitor/src/utils/mdn-utils");
+loader.lazyRequireGetter(
+ this,
+ "BLOCKED_REASON_MESSAGES",
+ "devtools/client/netmonitor/src/constants",
+ true
+);
+
+const LEARN_MORE = l10n.getStr("webConsoleMoreInfoLabel");
+
+const Services = require("Services");
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+NetworkEventMessage.displayName = "NetworkEventMessage";
+
+NetworkEventMessage.propTypes = {
+ message: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.shape({
+ openNetworkPanel: PropTypes.func.isRequired,
+ resendNetworkRequest: PropTypes.func.isRequired,
+ }),
+ timestampsVisible: PropTypes.bool.isRequired,
+ networkMessageUpdate: PropTypes.object.isRequired,
+};
+
+/**
+ * This component is responsible for rendering network messages
+ * in the Console panel.
+ *
+ * Network logs are expandable and the user can inspect it inline
+ * within the Console panel (no need to switch to the Network panel).
+ *
+ * HTTP details are rendered using `TabboxPanel` component used to
+ * render contents of the side bar in the Network panel.
+ *
+ * All HTTP details data are fetched from the backend on-demand
+ * when the user is expanding network log for the first time.
+ */
+function NetworkEventMessage({
+ message = {},
+ serviceContainer,
+ timestampsVisible,
+ networkMessageUpdate = {},
+ networkMessageActiveTabId,
+ dispatch,
+ open,
+}) {
+ const {
+ id,
+ indent,
+ source,
+ type,
+ level,
+ url,
+ method,
+ isXHR,
+ timeStamp,
+ blockedReason,
+ httpVersion,
+ status,
+ statusText,
+ totalTime,
+ } = message;
+
+ const topLevelClasses = ["cm-s-mozilla"];
+ if (isMessageNetworkError(message)) {
+ topLevelClasses.push("error");
+ }
+
+ let statusCode, statusInfo;
+
+ if (
+ httpVersion &&
+ status &&
+ statusText !== undefined &&
+ totalTime !== undefined
+ ) {
+ const statusCodeDocURL = getHTTPStatusCodeURL(
+ status.toString(),
+ "webconsole"
+ );
+ statusCode = dom.span(
+ {
+ className: "status-code",
+ "data-code": status,
+ title: LEARN_MORE,
+ onClick: e => {
+ e.stopPropagation();
+ e.preventDefault();
+ serviceContainer.openLink(statusCodeDocURL, e);
+ },
+ },
+ status
+ );
+ statusInfo = dom.span(
+ { className: "status-info" },
+ `[${httpVersion} `,
+ statusCode,
+ ` ${statusText} ${totalTime}ms]`
+ );
+ }
+
+ if (blockedReason) {
+ statusInfo = dom.span(
+ { className: "status-info" },
+ BLOCKED_REASON_MESSAGES[blockedReason]
+ );
+ topLevelClasses.push("network-message-blocked");
+ }
+
+ const onToggle = (messageId, e) => {
+ const shouldOpenLink = (isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey);
+ if (shouldOpenLink) {
+ serviceContainer.openLink(url, e);
+ e.stopPropagation();
+ } else if (open) {
+ dispatch(actions.messageClose(messageId));
+ } else {
+ dispatch(actions.messageOpen(messageId));
+ }
+ };
+
+ // Message body components.
+ const requestMethod = dom.span({ className: "method" }, method);
+ const xhr = isXHR
+ ? dom.span({ className: "xhr" }, l10n.getStr("webConsoleXhrIndicator"))
+ : null;
+ const requestUrl = dom.span({ className: "url", title: url }, url);
+ const statusBody = statusInfo
+ ? dom.a({ className: "status" }, statusInfo)
+ : null;
+
+ const messageBody = [xhr, requestMethod, requestUrl, statusBody];
+
+ // API consumed by Net monitor UI components. Most of the method
+ // are not needed in context of the Console panel (atm) and thus
+ // let's just provide empty implementation.
+ // Individual methods might be implemented step by step as needed.
+ const connector = {
+ viewSourceInDebugger: (srcUrl, line, column) => {
+ serviceContainer.onViewSourceInDebugger({ url: srcUrl, line, column });
+ },
+ getLongString: grip => {
+ return serviceContainer.getLongString(grip);
+ },
+ getTabTarget: () => {},
+ sendHTTPRequest: () => {},
+ setPreferences: () => {},
+ triggerActivity: () => {},
+ requestData: (requestId, dataType) => {
+ return serviceContainer.requestData(requestId, dataType);
+ },
+ };
+
+ // Only render the attachment if the network-event is
+ // actually opened (performance optimization).
+ const attachment =
+ open &&
+ dom.div(
+ {
+ className: "network-info network-monitor",
+ },
+ createElement(TabboxPanel, {
+ connector,
+ activeTabId: networkMessageActiveTabId,
+ request: networkMessageUpdate,
+ sourceMapURLService: serviceContainer.sourceMapURLService,
+ openLink: serviceContainer.openLink,
+ selectTab: tabId => {
+ dispatch(actions.selectNetworkMessageTab(tabId));
+ },
+ openNetworkDetails: enabled => {
+ if (!enabled) {
+ dispatch(actions.messageClose(id));
+ }
+ },
+ hideToggleButton: true,
+ showMessagesView: false,
+ })
+ );
+
+ const request = { url, method };
+ return Message({
+ dispatch,
+ messageId: id,
+ source,
+ type,
+ level,
+ indent,
+ collapsible: true,
+ open,
+ onToggle,
+ attachment,
+ topLevelClasses,
+ timeStamp,
+ messageBody,
+ serviceContainer,
+ request,
+ timestampsVisible,
+ isBlockedNetworkMessage: !!blockedReason,
+ message,
+ });
+}
+
+module.exports = NetworkEventMessage;
diff --git a/devtools/client/webconsole/components/Output/message-types/PageError.js b/devtools/client/webconsole/components/Output/message-types/PageError.js
new file mode 100644
index 0000000000..d093e4346c
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/PageError.js
@@ -0,0 +1,123 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const Message = createFactory(
+ require("devtools/client/webconsole/components/Output/Message")
+);
+const GripMessageBody = require("devtools/client/webconsole/components/Output/GripMessageBody");
+loader.lazyGetter(this, "REPS", function() {
+ return require("devtools/client/shared/components/reps/index").REPS;
+});
+loader.lazyGetter(this, "MODE", function() {
+ return require("devtools/client/shared/components/reps/index").MODE;
+});
+
+PageError.displayName = "PageError";
+
+PageError.propTypes = {
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool,
+ timestampsVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object,
+ maybeScrollToBottom: PropTypes.func,
+ inWarningGroup: PropTypes.bool.isRequired,
+};
+
+PageError.defaultProps = {
+ open: false,
+};
+
+function PageError(props) {
+ const {
+ dispatch,
+ message,
+ open,
+ repeat,
+ serviceContainer,
+ timestampsVisible,
+ maybeScrollToBottom,
+ inWarningGroup,
+ } = props;
+ const {
+ id: messageId,
+ source,
+ type,
+ level,
+ messageText,
+ stacktrace,
+ frame,
+ exceptionDocURL,
+ timeStamp,
+ notes,
+ parameters,
+ hasException,
+ isPromiseRejection,
+ } = message;
+
+ const messageBody = [];
+
+ const repsProps = {
+ useQuotes: false,
+ escapeWhitespace: false,
+ openLink: serviceContainer.openLink,
+ };
+
+ if (hasException) {
+ const prefix = `Uncaught${isPromiseRejection ? " (in promise)" : ""} `;
+ messageBody.push(
+ prefix,
+ GripMessageBody({
+ key: "body",
+ dispatch,
+ messageId,
+ grip: parameters[0],
+ serviceContainer,
+ type,
+ customFormat: true,
+ maybeScrollToBottom,
+ ...repsProps,
+ })
+ );
+ } else {
+ messageBody.push(
+ REPS.StringRep.rep({
+ key: "bodytext",
+ object: messageText,
+ mode: MODE.LONG,
+ ...repsProps,
+ })
+ );
+ }
+
+ return Message({
+ dispatch,
+ messageId,
+ open,
+ collapsible: Array.isArray(stacktrace),
+ source,
+ type,
+ level,
+ topLevelClasses: [],
+ indent: message.indent,
+ inWarningGroup,
+ messageBody,
+ repeat,
+ frame,
+ stacktrace,
+ serviceContainer,
+ exceptionDocURL,
+ timeStamp,
+ notes,
+ timestampsVisible,
+ maybeScrollToBottom,
+ message,
+ });
+}
+
+module.exports = PageError;
diff --git a/devtools/client/webconsole/components/Output/message-types/WarningGroup.js b/devtools/client/webconsole/components/Output/message-types/WarningGroup.js
new file mode 100644
index 0000000000..7ff17d24c5
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/WarningGroup.js
@@ -0,0 +1,76 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const Message = createFactory(
+ require("devtools/client/webconsole/components/Output/Message")
+);
+
+const { PluralForm } = require("devtools/shared/plural-form");
+const { l10n } = require("devtools/client/webconsole/utils/messages");
+const messageCountTooltip = l10n.getStr(
+ "webconsole.warningGroup.messageCount.tooltip"
+);
+
+WarningGroup.displayName = "WarningGroup";
+
+WarningGroup.propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object,
+ badge: PropTypes.number.isRequired,
+};
+
+function WarningGroup(props) {
+ const {
+ dispatch,
+ message,
+ serviceContainer,
+ timestampsVisible,
+ badge,
+ open,
+ } = props;
+
+ const { source, type, level, id: messageId, indent, timeStamp } = message;
+
+ const messageBody = [
+ message.messageText,
+ " ",
+ dom.span(
+ {
+ className: "warning-group-badge",
+ title: PluralForm.get(badge, messageCountTooltip).replace("#1", badge),
+ },
+ badge
+ ),
+ ];
+ const topLevelClasses = ["cm-s-mozilla"];
+
+ return Message({
+ badge,
+ collapsible: true,
+ dispatch,
+ indent,
+ level,
+ messageBody,
+ messageId,
+ open,
+ serviceContainer,
+ source,
+ timeStamp,
+ timestampsVisible,
+ topLevelClasses,
+ type,
+ message,
+ });
+}
+
+module.exports = WarningGroup;
diff --git a/devtools/client/webconsole/components/Output/message-types/moz.build b/devtools/client/webconsole/components/Output/message-types/moz.build
new file mode 100644
index 0000000000..5b24c72b7d
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/moz.build
@@ -0,0 +1,15 @@
+# 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(
+ "ConsoleApiCall.js",
+ "ConsoleCommand.js",
+ "CSSWarning.js",
+ "DefaultRenderer.js",
+ "EvaluationResult.js",
+ "NetworkEventMessage.js",
+ "PageError.js",
+ "WarningGroup.js",
+)
diff --git a/devtools/client/webconsole/components/Output/moz.build b/devtools/client/webconsole/components/Output/moz.build
new file mode 100644
index 0000000000..5721ca2014
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/moz.build
@@ -0,0 +1,20 @@
+# 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(
+ "CollapseButton.js",
+ "ConsoleOutput.js",
+ "ConsoleTable.js",
+ "GripMessageBody.js",
+ "Message.js",
+ "MessageContainer.js",
+ "MessageIcon.js",
+ "MessageIndent.js",
+ "MessageRepeat.js",
+)