summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/components/Output
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/webconsole/components/Output
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/webconsole/components/Output')
-rw-r--r--devtools/client/webconsole/components/Output/CollapseButton.js33
-rw-r--r--devtools/client/webconsole/components/Output/ConsoleOutput.js378
-rw-r--r--devtools/client/webconsole/components/Output/ConsoleTable.js272
-rw-r--r--devtools/client/webconsole/components/Output/GripMessageBody.js114
-rw-r--r--devtools/client/webconsole/components/Output/LazyMessageList.js393
-rw-r--r--devtools/client/webconsole/components/Output/Message.js482
-rw-r--r--devtools/client/webconsole/components/Output/MessageContainer.js128
-rw-r--r--devtools/client/webconsole/components/Output/MessageIcon.js71
-rw-r--r--devtools/client/webconsole/components/Output/MessageIndent.js41
-rw-r--r--devtools/client/webconsole/components/Output/MessageRepeat.js35
-rw-r--r--devtools/client/webconsole/components/Output/message-types/CSSWarning.js173
-rw-r--r--devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js221
-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.js124
-rw-r--r--devtools/client/webconsole/components/Output/message-types/NavigationMarker.js62
-rw-r--r--devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js243
-rw-r--r--devtools/client/webconsole/components/Output/message-types/PageError.js130
-rw-r--r--devtools/client/webconsole/components/Output/message-types/SimpleTable.js134
-rw-r--r--devtools/client/webconsole/components/Output/message-types/WarningGroup.js80
-rw-r--r--devtools/client/webconsole/components/Output/message-types/moz.build17
-rw-r--r--devtools/client/webconsole/components/Output/moz.build21
22 files changed, 3272 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..c0594a5855
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/CollapseButton.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 dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+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,
+ });
+}
+
+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..064d7ee052
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/ConsoleOutput.js
@@ -0,0 +1,378 @@
+/* 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,
+ createRef,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
+const {
+ initialize,
+} = require("resource://devtools/client/webconsole/actions/ui.js");
+const LazyMessageList = require("resource://devtools/client/webconsole/components/Output/LazyMessageList.js");
+
+const {
+ getMutableMessagesById,
+ getAllMessagesUiById,
+ getAllDisabledMessagesById,
+ getAllCssMessagesMatchingElements,
+ getAllNetworkMessagesUpdateById,
+ getLastMessageId,
+ getVisibleMessages,
+ getAllRepeatById,
+ getAllWarningGroupsById,
+ isMessageInWarningGroup,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "MessageContainer",
+ "resource://devtools/client/webconsole/components/Output/MessageContainer.js",
+ true
+);
+loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js");
+
+const {
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+class ConsoleOutput extends Component {
+ static get propTypes() {
+ return {
+ initialized: PropTypes.bool.isRequired,
+ mutableMessages: PropTypes.object.isRequired,
+ messageCount: PropTypes.number.isRequired,
+ messagesUi: PropTypes.array.isRequired,
+ disabledMessages: PropTypes.array.isRequired,
+ serviceContainer: PropTypes.shape({
+ attachRefToWebConsoleUI: PropTypes.func.isRequired,
+ openContextMenu: PropTypes.func.isRequired,
+ sourceMapURLService: PropTypes.object,
+ }),
+ dispatch: PropTypes.func.isRequired,
+ timestampsVisible: PropTypes.bool,
+ cssMatchingElements: 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,
+ cacheGeneration: PropTypes.number.isRequired,
+ disableVirtualization: PropTypes.bool,
+ lastMessageId: PropTypes.string.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onContextMenu = this.onContextMenu.bind(this);
+ this.maybeScrollToBottom = this.maybeScrollToBottom.bind(this);
+ this.messageIdsToKeepAlive = new Set();
+ this.ref = createRef();
+ this.lazyMessageListRef = createRef();
+
+ 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.disableVirtualization) {
+ return;
+ }
+
+ if (this.props.visibleMessages.length) {
+ this.scrollToBottom();
+ }
+
+ this.scrollDetectionIntersectionObserver = new IntersectionObserver(
+ entries => {
+ for (const entry of entries) {
+ // Consider that we're not pinned to the bottom anymore if the bottom of the
+ // scrollable area is within 10px of visible (half the typical element height.)
+ this.scrolledToBottom = entry.intersectionRatio > 0;
+ }
+ },
+ { root: this.outputNode, rootMargin: "10px" }
+ );
+
+ this.resizeObserver.observe(this.getElementToObserve());
+
+ const { serviceContainer, onFirstMeaningfulPaint, dispatch } = this.props;
+ serviceContainer.attachRefToWebConsoleUI(
+ "outputScroller",
+ this.ref.current
+ );
+
+ // 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);
+ });
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillUpdate(nextProps, nextState) {
+ this.isUpdating = true;
+ if (nextProps.cacheGeneration !== this.props.cacheGeneration) {
+ this.messageIdsToKeepAlive = new Set();
+ }
+
+ 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;
+ this.scrolledToBottom = true;
+ return;
+ }
+
+ const bottomBuffer = this.lazyMessageListRef.current.bottomBuffer;
+ this.scrollDetectionIntersectionObserver.unobserve(bottomBuffer);
+
+ // 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.messageCount - this.props.messageCount;
+ // Evaluation results are never filtered out, so if it's in the store, it will be
+ // visible in the output.
+ const isNewMessageEvaluationResult =
+ messagesDelta > 0 &&
+ nextProps.lastMessageId &&
+ nextProps.mutableMessages.get(nextProps.lastMessageId)?.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.isUpdating = false;
+ this.maybeScrollToBottom();
+ if (this?.outputNode?.lastChild) {
+ const bottomBuffer = this.lazyMessageListRef.current.bottomBuffer;
+ this.scrollDetectionIntersectionObserver.observe(bottomBuffer);
+ }
+
+ if (prevProps.editorMode !== this.props.editorMode) {
+ this.resizeObserver.observe(this.getElementToObserve());
+ }
+ }
+
+ get outputNode() {
+ return this.ref.current;
+ }
+
+ maybeScrollToBottom() {
+ if (this.outputNode && this.shouldScrollBottom) {
+ this.scrollToBottom();
+ }
+ }
+
+ // The maybeScrollToBottom callback we provide to messages needs to be a little bit more
+ // strict than the one we normally use, because they can potentially interrupt a user
+ // scroll (between when the intersection observer registers the scroll break and when
+ // a componentDidUpdate comes through to reconcile it.)
+ maybeScrollToBottomMessageCallback(index) {
+ if (
+ this.outputNode &&
+ this.shouldScrollBottom &&
+ this.scrolledToBottom &&
+ this.lazyMessageListRef.current?.isItemNearBottom(index)
+ ) {
+ this.scrollToBottom();
+ }
+ }
+
+ scrollToBottom() {
+ if (flags.testing && this.outputNode.hasAttribute("disable-autoscroll")) {
+ return;
+ }
+ 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() {
+ const {
+ cacheGeneration,
+ dispatch,
+ visibleMessages,
+ disabledMessages,
+ mutableMessages,
+ messagesUi,
+ cssMatchingElements,
+ messagesRepeat,
+ warningGroups,
+ networkMessagesUpdate,
+ networkMessageActiveTabId,
+ serviceContainer,
+ timestampsVisible,
+ } = this.props;
+
+ const renderMessage = (messageId, index) => {
+ return createElement(MessageContainer, {
+ dispatch,
+ key: messageId,
+ messageId,
+ serviceContainer,
+ open: messagesUi.includes(messageId),
+ cssMatchingElements: cssMatchingElements.get(messageId),
+ timestampsVisible,
+ disabled: disabledMessages.includes(messageId),
+ repeat: messagesRepeat[messageId],
+ badge: warningGroups.has(messageId)
+ ? warningGroups.get(messageId).length
+ : null,
+ inWarningGroup:
+ warningGroups && warningGroups.size > 0
+ ? isMessageInWarningGroup(
+ mutableMessages.get(messageId),
+ visibleMessages
+ )
+ : false,
+ networkMessageUpdate: networkMessagesUpdate[messageId],
+ networkMessageActiveTabId,
+ getMessage: () => mutableMessages.get(messageId),
+ maybeScrollToBottom: () =>
+ this.maybeScrollToBottomMessageCallback(index),
+ // Whenever a node is expanded, we want to make sure we keep the
+ // message node alive so as to not lose the expanded state.
+ setExpanded: () => this.messageIdsToKeepAlive.add(messageId),
+ });
+ };
+
+ // scrollOverdrawCount tells the list to draw extra elements above and
+ // below the scrollport so that we can avoid flashes of blank space
+ // when scrolling. When `disableVirtualization` is passed we make it as large as the
+ // number of messages to render them all and effectively disabling virtualization (this
+ // should only be used for some actions that requires all the messages to be rendered
+ // in the DOM, like "Copy All Messages").
+ const scrollOverdrawCount = this.props.disableVirtualization
+ ? visibleMessages.length
+ : 20;
+
+ const attrs = {
+ className: "webconsole-output",
+ role: "main",
+ onContextMenu: this.onContextMenu,
+ ref: this.ref,
+ };
+ if (flags.testing) {
+ attrs["data-visible-messages"] = JSON.stringify(visibleMessages);
+ }
+ return dom.div(
+ attrs,
+ createElement(LazyMessageList, {
+ viewportRef: this.ref,
+ items: visibleMessages,
+ itemDefaultHeight: 21,
+ editorMode: this.props.editorMode,
+ scrollOverdrawCount,
+ ref: this.lazyMessageListRef,
+ renderItem: renderMessage,
+ itemsToKeepAlive: this.messageIdsToKeepAlive,
+ serviceContainer,
+ cacheGeneration,
+ shouldScrollBottom: () => this.shouldScrollBottom && this.isUpdating,
+ })
+ );
+ }
+}
+
+function mapStateToProps(state, props) {
+ const mutableMessages = getMutableMessagesById(state);
+ return {
+ initialized: state.ui.initialized,
+ cacheGeneration: state.ui.cacheGeneration,
+ // We need to compute this so lifecycle methods can compare the global message count
+ // on state change (since we can't do it with mutableMessagesById).
+ messageCount: mutableMessages.size,
+ mutableMessages,
+ lastMessageId: getLastMessageId(state),
+ visibleMessages: getVisibleMessages(state),
+ disabledMessages: getAllDisabledMessagesById(state),
+ messagesUi: getAllMessagesUiById(state),
+ cssMatchingElements: getAllCssMessagesMatchingElements(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..f41afce96d
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/ConsoleTable.js
@@ -0,0 +1,272 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ getArrayTypeNames,
+} = require("resource://devtools/shared/webconsole/messages.js");
+const {
+ l10n,
+ getDescriptorValue,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .MODE;
+});
+
+const GripMessageBody = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+
+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,
+ setExpanded: PropTypes.func,
+ };
+ }
+
+ 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.th(
+ {
+ key,
+ title: value,
+ },
+ value
+ )
+ )
+ );
+ return dom.thead({}, dom.tr({}, headerItems));
+ }
+
+ getRows(columns, items) {
+ const { dispatch, serviceContainer, setExpanded } = this.props;
+
+ const rows = [];
+ items.forEach((item, index) => {
+ const cells = [];
+
+ columns.forEach((value, key) => {
+ const cellValue = item[key];
+ const cellContent =
+ typeof cellValue === "undefined"
+ ? ""
+ : GripMessageBody({
+ grip: cellValue,
+ mode: MODE.SHORT,
+ useQuotes: false,
+ serviceContainer,
+ dispatch,
+ setExpanded,
+ });
+
+ cells.push(
+ dom.td(
+ {
+ key,
+ },
+ cellContent
+ )
+ );
+ });
+ rows.push(dom.tr({}, cells));
+ });
+ return dom.tbody({}, rows);
+ }
+
+ 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);
+
+ // We need to wrap the <table> in a div so we can have the max-height set properly
+ // without changing the table display.
+ return dom.div(
+ { className: "consoletable-wrapper" },
+ dom.table(
+ {
+ className: "consoletable",
+ },
+ 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..6ecfe55b8e
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/GripMessageBody.js
@@ -0,0 +1,114 @@
+/* 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("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ MESSAGE_TYPE,
+ JSTERM_COMMANDS,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ cleanupStyle,
+} = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js");
+const {
+ getObjectInspector,
+} = require("resource://devtools/client/webconsole/utils/object-inspector.js");
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+
+loader.lazyGetter(this, "objectInspector", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .objectInspector;
+});
+
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .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,
+ setExpanded: PropTypes.func,
+};
+
+GripMessageBody.defaultProps = {
+ mode: MODE.LONG,
+};
+
+function GripMessageBody(props) {
+ const {
+ grip,
+ userProvidedStyle,
+ serviceContainer,
+ useQuotes,
+ escapeWhitespace,
+ mode = MODE.LONG,
+ dispatch,
+ maybeScrollToBottom,
+ setExpanded,
+ customFormat = false,
+ } = props;
+
+ let styleObject;
+ if (userProvidedStyle && userProvidedStyle !== "") {
+ styleObject = cleanupStyle(
+ userProvidedStyle,
+ serviceContainer.createElement
+ );
+ }
+
+ const objectInspectorProps = {
+ autoExpandDepth: shouldAutoExpandObjectInspector(props) ? 1 : 0,
+ mode,
+ maybeScrollToBottom,
+ setExpanded,
+ 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);
+}
+
+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/LazyMessageList.js b/devtools/client/webconsole/components/Output/LazyMessageList.js
new file mode 100644
index 0000000000..931b5bb8bd
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/LazyMessageList.js
@@ -0,0 +1,393 @@
+/* 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/.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * MIT License
+ *
+ * Copyright (c) 2019 Oleg Grishechkin
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+"use strict";
+
+const {
+ Fragment,
+ Component,
+ createElement,
+ createRef,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+
+// This element is a webconsole optimization for handling large numbers of
+// console messages. The purpose is to only create DOM elements for messages
+// which are actually visible within the scrollport. This code was based on
+// Oleg Grishechkin's react-viewport-list element - however, it has been quite
+// heavily modified, to the point that it is mostly unrecognizable. The most
+// notable behavioral modification is that the list implements the behavior of
+// pinning the scrollport to the bottom of the scroll container.
+class LazyMessageList extends Component {
+ static get propTypes() {
+ return {
+ viewportRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) })
+ .isRequired,
+ items: PropTypes.array.isRequired,
+ itemsToKeepAlive: PropTypes.shape({
+ has: PropTypes.func,
+ keys: PropTypes.func,
+ size: PropTypes.number,
+ }).isRequired,
+ editorMode: PropTypes.bool.isRequired,
+ itemDefaultHeight: PropTypes.number.isRequired,
+ scrollOverdrawCount: PropTypes.number.isRequired,
+ renderItem: PropTypes.func.isRequired,
+ shouldScrollBottom: PropTypes.func.isRequired,
+ cacheGeneration: PropTypes.number.isRequired,
+ serviceContainer: PropTypes.shape({
+ emitForTests: PropTypes.func.isRequired,
+ }),
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.#initialized = false;
+ this.#topBufferRef = createRef();
+ this.#bottomBufferRef = createRef();
+ this.#viewportHeight = window.innerHeight;
+ this.#startIndex = 0;
+ this.#resizeObserver = null;
+ this.#cachedHeights = [];
+
+ this.#scrollHandlerBinding = this.#scrollHandler.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillUpdate(nextProps, nextState) {
+ if (nextProps.cacheGeneration !== this.props.cacheGeneration) {
+ this.#cachedHeights = [];
+ this.#startIndex = 0;
+ } else if (
+ (this.props.shouldScrollBottom() &&
+ nextProps.items.length > this.props.items.length) ||
+ this.#startIndex > nextProps.items.length - this.#numItemsToDraw
+ ) {
+ this.#startIndex = Math.max(
+ 0,
+ nextProps.items.length - this.#numItemsToDraw
+ );
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ const { viewportRef, serviceContainer } = this.props;
+ if (!viewportRef.current || !this.#topBufferRef.current) {
+ return;
+ }
+
+ if (!this.#initialized) {
+ // We set these up from a one-time call in componentDidUpdate, rather than in
+ // componentDidMount, because we need the parent to be mounted first, to add
+ // listeners to it, and React orders things such that children mount before
+ // parents.
+ this.#addListeners();
+ }
+
+ if (!this.#initialized || prevProps.editorMode !== this.props.editorMode) {
+ this.#resizeObserver.observe(viewportRef.current);
+ }
+
+ this.#initialized = true;
+
+ // Since we updated, we're now going to compute the heights of all visible
+ // elements and store them in a cache. This allows us to get more accurate
+ // buffer regions to make scrolling correct when these elements no longer
+ // exist.
+ let index = this.#startIndex;
+ let element = this.#topBufferRef.current.nextSibling;
+ let elementRect = element?.getBoundingClientRect();
+ while (
+ Element.isInstance(element) &&
+ index < this.#clampedEndIndex &&
+ element !== this.#bottomBufferRef.current
+ ) {
+ const next = element.nextSibling;
+ const nextRect = next.getBoundingClientRect();
+ this.#cachedHeights[index] = nextRect.top - elementRect.top;
+ element = next;
+ elementRect = nextRect;
+ index++;
+ }
+
+ serviceContainer.emitForTests("lazy-message-list-updated-or-noop");
+ }
+
+ componentWillUnmount() {
+ this.#removeListeners();
+ }
+
+ #initialized;
+ #topBufferRef;
+ #bottomBufferRef;
+ #viewportHeight;
+ #startIndex;
+ #resizeObserver;
+ #cachedHeights;
+ #scrollHandlerBinding;
+
+ get #maxIndex() {
+ return this.props.items.length - 1;
+ }
+
+ get #overdrawHeight() {
+ return this.props.scrollOverdrawCount * this.props.itemDefaultHeight;
+ }
+
+ get #numItemsToDraw() {
+ const scrollingWindowCount = Math.ceil(
+ this.#viewportHeight / this.props.itemDefaultHeight
+ );
+ return scrollingWindowCount + 2 * this.props.scrollOverdrawCount;
+ }
+
+ get #unclampedEndIndex() {
+ return this.#startIndex + this.#numItemsToDraw;
+ }
+
+ // Since the "end index" is computed based off a fixed offset from the start
+ // index, it can exceed the length of our items array. This is just a helper
+ // to ensure we don't exceed that.
+ get #clampedEndIndex() {
+ return Math.min(this.#unclampedEndIndex, this.props.items.length);
+ }
+
+ /**
+ * Increases our start index until we've passed enough elements to cover
+ * the difference in px between where we are and where we want to be.
+ *
+ * @param Number startIndex
+ * The current value of our start index.
+ * @param Number deltaPx
+ * The difference in pixels between where we want to be and
+ * where we are.
+ * @return {Number} The new computed start index.
+ */
+ #increaseStartIndex(startIndex, deltaPx) {
+ for (let i = startIndex + 1; i < this.props.items.length; i++) {
+ deltaPx -= this.#cachedHeights[i];
+ startIndex = i;
+
+ if (deltaPx <= 0) {
+ break;
+ }
+ }
+ return startIndex;
+ }
+
+ /**
+ * Decreases our start index until we've passed enough elements to cover
+ * the difference in px between where we are and where we want to be.
+ *
+ * @param Number startIndex
+ * The current value of our start index.
+ * @param Number deltaPx
+ * The difference in pixels between where we want to be and
+ * where we are.
+ * @return {Number} The new computed start index.
+ */
+ #decreaseStartIndex(startIndex, diff) {
+ for (let i = startIndex - 1; i >= 0; i--) {
+ diff -= this.#cachedHeights[i];
+ startIndex = i;
+
+ if (diff <= 0) {
+ break;
+ }
+ }
+ return startIndex;
+ }
+
+ #scrollHandler() {
+ if (!this.props.viewportRef.current || !this.#topBufferRef.current) {
+ return;
+ }
+
+ const scrollportMin =
+ this.props.viewportRef.current.getBoundingClientRect().top -
+ this.#overdrawHeight;
+ const uppermostItemRect =
+ this.#topBufferRef.current.nextSibling.getBoundingClientRect();
+ const uppermostItemMin = uppermostItemRect.top;
+ const uppermostItemMax = uppermostItemRect.bottom;
+
+ let nextStartIndex = this.#startIndex;
+ const downwardPx = scrollportMin - uppermostItemMax;
+ const upwardPx = uppermostItemMin - scrollportMin;
+ if (downwardPx > 0) {
+ nextStartIndex = this.#increaseStartIndex(nextStartIndex, downwardPx);
+ } else if (upwardPx > 0) {
+ nextStartIndex = this.#decreaseStartIndex(nextStartIndex, upwardPx);
+ }
+
+ nextStartIndex = Math.max(
+ 0,
+ Math.min(nextStartIndex, this.props.items.length - this.#numItemsToDraw)
+ );
+
+ if (nextStartIndex !== this.#startIndex) {
+ this.#startIndex = nextStartIndex;
+ this.forceUpdate();
+ } else {
+ const { serviceContainer } = this.props;
+ serviceContainer.emitForTests("lazy-message-list-updated-or-noop");
+ }
+ }
+
+ #addListeners() {
+ const { viewportRef } = this.props;
+ viewportRef.current.addEventListener("scroll", this.#scrollHandlerBinding);
+ this.#resizeObserver = new ResizeObserver(entries => {
+ this.#viewportHeight =
+ viewportRef.current.parentNode.parentNode.clientHeight;
+ this.forceUpdate();
+ });
+ }
+
+ #removeListeners() {
+ const { viewportRef } = this.props;
+ this.#resizeObserver?.disconnect();
+ viewportRef.current?.removeEventListener(
+ "scroll",
+ this.#scrollHandlerBinding
+ );
+ }
+
+ get bottomBuffer() {
+ return this.#bottomBufferRef.current;
+ }
+
+ isItemNearBottom(index) {
+ return index >= this.props.items.length - this.#numItemsToDraw;
+ }
+
+ render() {
+ const { items, itemDefaultHeight, renderItem, itemsToKeepAlive } =
+ this.props;
+ if (!items.length) {
+ return createElement(Fragment, {
+ key: "LazyMessageList",
+ });
+ }
+
+ // Resize our cached heights to fit if necessary.
+ const countUncached = items.length - this.#cachedHeights.length;
+ if (countUncached > 0) {
+ // It would be lovely if javascript allowed us to resize an array in one
+ // go. I think this is the closest we can get to that. This in theory
+ // allows us to realloc, and doesn't require copying the whole original
+ // array like concat does.
+ this.#cachedHeights.push(...Array(countUncached).fill(itemDefaultHeight));
+ }
+
+ let topBufferHeight = 0;
+ let bottomBufferHeight = 0;
+ // We can't compute the bottom buffer height until the end, so we just
+ // store the index of where it needs to go.
+ let bottomBufferIndex = 0;
+ let currentChild = 0;
+ const startIndex = this.#startIndex;
+ const endIndex = this.#clampedEndIndex;
+ // We preallocate this array to avoid allocations in the loop. The minimum,
+ // and typical length for it is the size of the body plus 2 for the top and
+ // bottom buffers. It can be bigger due to itemsToKeepAlive, but we can't just
+ // add the size, since itemsToKeepAlive could in theory hold items which are
+ // not even in the list.
+ const children = new Array(endIndex - startIndex + 2);
+ const pushChild = c => {
+ if (currentChild >= children.length) {
+ children.push(c);
+ } else {
+ children[currentChild] = c;
+ }
+ return currentChild++;
+ };
+ for (let i = 0; i < items.length; i++) {
+ const itemId = items[i];
+ if (i < startIndex) {
+ if (i == 0 || itemsToKeepAlive.has(itemId)) {
+ // If this is our first item, and we wouldn't otherwise be rendering
+ // it, we want to ensure that it's at the beginning of our children
+ // array to ensure keyboard navigation functions properly.
+ pushChild(renderItem(itemId, i));
+ } else {
+ topBufferHeight += this.#cachedHeights[i];
+ }
+ } else if (i < endIndex) {
+ if (i == startIndex) {
+ pushChild(
+ createElement("div", {
+ key: "LazyMessageListTop",
+ className: "lazy-message-list-top",
+ ref: this.#topBufferRef,
+ style: { height: topBufferHeight },
+ })
+ );
+ }
+ pushChild(renderItem(itemId, i));
+ if (i == endIndex - 1) {
+ // We're just reserving the bottom buffer's spot in the children
+ // array here. We will create the actual element and assign it at
+ // this index after the loop.
+ bottomBufferIndex = pushChild(null);
+ }
+ } else if (i == items.length - 1 || itemsToKeepAlive.has(itemId)) {
+ // Similarly to the logic for our first item, we also want to ensure
+ // that our last item is always rendered as the last item in our
+ // children array.
+ pushChild(renderItem(itemId, i));
+ } else {
+ bottomBufferHeight += this.#cachedHeights[i];
+ }
+ }
+
+ children[bottomBufferIndex] = createElement("div", {
+ key: "LazyMessageListBottom",
+ className: "lazy-message-list-bottom",
+ ref: this.#bottomBufferRef,
+ style: { height: bottomBufferHeight },
+ });
+
+ return createElement(
+ Fragment,
+ {
+ key: "LazyMessageList",
+ },
+ children
+ );
+ }
+}
+
+module.exports = LazyMessageList;
diff --git a/devtools/client/webconsole/components/Output/Message.js b/devtools/client/webconsole/components/Output/Message.js
new file mode 100644
index 0000000000..ee65c8947a
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/Message.js
@@ -0,0 +1,482 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ MESSAGE_LEVEL,
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ MessageIndent,
+} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
+const MessageIcon = require("resource://devtools/client/webconsole/components/Output/MessageIcon.js");
+const FrameView = createFactory(
+ require("resource://devtools/client/shared/components/Frame.js")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "CollapseButton",
+ "resource://devtools/client/webconsole/components/Output/CollapseButton.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "MessageRepeat",
+ "resource://devtools/client/webconsole/components/Output/MessageRepeat.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "SmartTrace",
+ "resource://devtools/client/shared/components/SmartTrace.js"
+);
+
+class Message extends Component {
+ static get propTypes() {
+ return {
+ open: PropTypes.bool,
+ collapsible: PropTypes.bool,
+ collapseTitle: PropTypes.string,
+ disabled: PropTypes.bool,
+ 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,
+ preventStacktraceInitialRenderDelay: PropTypes.bool,
+ }),
+ 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, disabled } = this.props;
+
+ if (disabled) {
+ return;
+ }
+
+ // 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, disabled } =
+ this.props;
+
+ if (inWarningGroup) {
+ return undefined;
+ }
+
+ if (disabled) {
+ return MessageIcon({
+ level: MESSAGE_LEVEL.INFO,
+ type,
+ title: l10n.getStr("webconsole.disableIcon.title"),
+ });
+ }
+
+ 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,
+ disabled,
+ 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");
+ }
+
+ if (disabled) {
+ topLevelClasses.push("disabled");
+ }
+
+ 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) {
+ const smartTraceAttributes = {
+ stacktrace,
+ onViewSourceInDebugger:
+ serviceContainer.onViewSourceInDebugger ||
+ serviceContainer.onViewSource,
+ onViewSource: serviceContainer.onViewSource,
+ onReady: this.props.maybeScrollToBottom,
+ sourceMapURLService: serviceContainer.sourceMapURLService,
+ };
+
+ if (serviceContainer.preventStacktraceInitialRenderDelay) {
+ smartTraceAttributes.initialRenderDelay = 0;
+ }
+
+ attachment = dom.div(
+ {
+ className: "stacktrace devtools-monospace",
+ },
+ createElement(SmartTrace, smartTraceAttributes)
+ );
+ }
+
+ // If there is an expandable part, make it collapsible.
+ let collapse = null;
+ if (collapsible && !disabled) {
+ 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 = frame
+ ? FrameView({
+ className: "message-location devtools-monospace",
+ 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,
+ "data-indent": indent || 0,
+ "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..db856e909c
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageContainer.js
@@ -0,0 +1,128 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "isWarningGroup",
+ "resource://devtools/client/webconsole/utils/messages.js",
+ true
+);
+
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const ConsoleApiCall = require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js");
+const ConsoleCommand = require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js");
+const CSSWarning = require("resource://devtools/client/webconsole/components/Output/message-types/CSSWarning.js");
+const DefaultRenderer = require("resource://devtools/client/webconsole/components/Output/message-types/DefaultRenderer.js");
+const EvaluationResult = require("resource://devtools/client/webconsole/components/Output/message-types/EvaluationResult.js");
+const NavigationMarker = require("resource://devtools/client/webconsole/components/Output/message-types/NavigationMarker.js");
+const NetworkEventMessage = require("resource://devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js");
+const PageError = require("resource://devtools/client/webconsole/components/Output/message-types/PageError.js");
+const SimpleTable = require("resource://devtools/client/webconsole/components/Output/message-types/SimpleTable.js");
+const WarningGroup = require("resource://devtools/client/webconsole/components/Output/message-types/WarningGroup.js");
+
+class MessageContainer extends Component {
+ static get propTypes() {
+ return {
+ messageId: PropTypes.string.isRequired,
+ open: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ cssMatchingElements: 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,
+ disabled: PropTypes.bool,
+ };
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const triggeringUpdateProps = [
+ "repeat",
+ "open",
+ "cssMatchingElements",
+ "timestampsVisible",
+ "networkMessageUpdate",
+ "badge",
+ "inWarningGroup",
+ "disabled",
+ ];
+
+ 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 DefaultRenderer;
+ }
+
+ switch (message.source) {
+ case MESSAGE_SOURCE.CONSOLE_API:
+ return ConsoleApiCall;
+ case MESSAGE_SOURCE.NETWORK:
+ return NetworkEventMessage;
+ case MESSAGE_SOURCE.CSS:
+ return CSSWarning;
+ case MESSAGE_SOURCE.JAVASCRIPT:
+ switch (message.type) {
+ case MESSAGE_TYPE.COMMAND:
+ return ConsoleCommand;
+ case MESSAGE_TYPE.RESULT:
+ return 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 PageError;
+ default:
+ return DefaultRenderer;
+ }
+ case MESSAGE_SOURCE.CONSOLE_FRONTEND:
+ if (isWarningGroup(message)) {
+ return WarningGroup;
+ }
+ if (message.type === MESSAGE_TYPE.SIMPLE_TABLE) {
+ return SimpleTable;
+ }
+ if (message.type === MESSAGE_TYPE.NAVIGATION_MARKER) {
+ return NavigationMarker;
+ }
+ break;
+ }
+
+ return 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..6e3fb69aaa
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageIcon.js
@@ -0,0 +1,71 @@
+/* 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("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+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, title) {
+ title = title || l10n.getStr(l10nLevels[level] || level);
+ const classnames = ["icon"];
+
+ if (type === "logPoint") {
+ title = l10n.getStr("logpoint.title");
+ classnames.push("logpoint");
+ } else if (type === "logTrace") {
+ title = l10n.getStr("logtrace.title");
+ classnames.push("logtrace");
+ } else if (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,
+ title: PropTypes.string,
+};
+
+function MessageIcon(props) {
+ const { level, type, title } = props;
+
+ if (type) {
+ return getIconElement(level, type, title);
+ }
+
+ 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..2abd7b5301
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageIndent.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const INDENT_WIDTH = 12;
+
+// Store common indents so they can be used without recreating the element during render.
+const CONSTANT_INDENTS = [getIndentElement(1)];
+const IN_WARNING_GROUP_INDENT = getIndentElement(1, "warning-indent");
+
+function getIndentElement(indent, className) {
+ return dom.span({
+ className: `indent${className ? " " + className : ""}`,
+ style: {
+ width: indent * INDENT_WIDTH,
+ },
+ });
+}
+
+function MessageIndent(props) {
+ const { indent, inWarningGroup } = props;
+
+ if (inWarningGroup) {
+ return IN_WARNING_GROUP_INDENT;
+ }
+
+ if (!indent) {
+ return null;
+ }
+
+ 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..7bf846bcb0
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageRepeat.js
@@ -0,0 +1,35 @@
+/* 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("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+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..cef91c22be
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/CSSWarning.js
@@ -0,0 +1,173 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "GripMessageBody",
+ "resource://devtools/client/webconsole/components/Output/GripMessageBody.js"
+);
+
+/**
+ * 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,
+ cssMatchingElements: PropTypes.object,
+ repeat: PropTypes.any,
+ serviceContainer: PropTypes.object,
+ timestampsVisible: PropTypes.bool.isRequired,
+ setExpanded: PropTypes.func,
+ };
+ }
+
+ 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, cssMatchingElements, open } = this.props;
+
+ if (open) {
+ dispatch(actions.messageClose(messageId));
+ } else if (cssMatchingElements) {
+ // 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 cssMatchingElements 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(message));
+ dispatch(actions.messageOpen(messageId));
+ }
+ }
+
+ render() {
+ const {
+ dispatch,
+ message,
+ open,
+ cssMatchingElements,
+ repeat,
+ serviceContainer,
+ timestampsVisible,
+ inWarningGroup,
+ setExpanded,
+ } = 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 &&
+ cssMatchingElements !== undefined &&
+ dom.div(
+ { className: "devtools-monospace" },
+ dom.div(
+ { className: "elements-label" },
+ l10n.getFormatStr("webconsole.cssWarningElements.label", [
+ cssSelectors,
+ ])
+ ),
+ GripMessageBody({
+ dispatch,
+ escapeWhitespace: false,
+ grip: cssMatchingElements,
+ serviceContainer,
+ setExpanded,
+ })
+ );
+
+ 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..155075731f
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js
@@ -0,0 +1,221 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const GripMessageBody = require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js");
+const ConsoleTable = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/ConsoleTable.js")
+);
+const {
+ isGroupType,
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+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,
+ serviceContainer,
+ timestampsVisible,
+ repeat,
+ maybeScrollToBottom,
+ setExpanded,
+ } = 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,
+ setExpanded,
+ // 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
+ ? [" "].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,
+ setExpanded,
+ type,
+ });
+ }
+
+ let attachment = null;
+ if (type === "table") {
+ attachment = ConsoleTable({
+ dispatch,
+ id: message.id,
+ serviceContainer,
+ parameters: message.parameters,
+ });
+ }
+
+ 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,
+ setExpanded,
+ customFormat,
+ } = options;
+
+ const elements = [];
+ const parametersLength = parameters.length;
+ for (let i = 0; i < parametersLength; i++) {
+ elements.push(
+ GripMessageBody({
+ dispatch,
+ messageId,
+ grip: parameters[i],
+ key: i,
+ userProvidedStyle: userProvidedStyles ? userProvidedStyles[i] : null,
+ serviceContainer,
+ useQuotes: false,
+ loadedObjectProperties,
+ loadedObjectEntries,
+ type,
+ maybeScrollToBottom,
+ setExpanded,
+ customFormat,
+ })
+ );
+
+ // 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.
+ if (
+ i !== parametersLength - 1 &&
+ (!userProvidedStyles ||
+ userProvidedStyles[i] === undefined ||
+ userProvidedStyles[i + 1] === undefined)
+ ) {
+ elements.push(" ");
+ }
+ }
+
+ return elements;
+}
+
+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..5cfb87113c
--- /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("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const { ELLIPSIS } = require("resource://devtools/shared/l10n.js");
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+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..893e6b04c6
--- /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("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+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..60d44a9f99
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/EvaluationResult.js
@@ -0,0 +1,124 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+const GripMessageBody = require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js");
+
+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,
+ setExpanded,
+ } = 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,
+ setExpanded,
+ 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/NavigationMarker.js b/devtools/client/webconsole/components/Output/message-types/NavigationMarker.js
new file mode 100644
index 0000000000..7d14206a6a
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/NavigationMarker.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// React & Redux
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+NavigationMarker.displayName = "NavigationMarker";
+
+NavigationMarker.propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ maybeScrollToBottom: PropTypes.func,
+};
+
+function NavigationMarker(props) {
+ const {
+ dispatch,
+ message,
+ serviceContainer,
+ timestampsVisible,
+ maybeScrollToBottom,
+ } = props;
+ const {
+ id: messageId,
+ indent,
+ source,
+ type,
+ level,
+ timeStamp,
+ messageText,
+ } = message;
+
+ return Message({
+ messageId,
+ source,
+ type,
+ level,
+ messageBody: messageText,
+ serviceContainer,
+ dispatch,
+ indent,
+ timeStamp,
+ timestampsVisible,
+ topLevelClasses: [],
+ message,
+ maybeScrollToBottom,
+ });
+}
+
+module.exports = NavigationMarker;
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..ce0961668b
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js
@@ -0,0 +1,243 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ isMessageNetworkError,
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+loader.lazyRequireGetter(
+ this,
+ "TabboxPanel",
+ "resource://devtools/client/netmonitor/src/components/TabboxPanel.js"
+);
+const {
+ getHTTPStatusCodeURL,
+} = require("resource://devtools/client/netmonitor/src/utils/doc-utils.js");
+const {
+ getUnicodeUrl,
+} = require("resource://devtools/client/shared/unicode-url.js");
+loader.lazyRequireGetter(
+ this,
+ "BLOCKED_REASON_MESSAGES",
+ "resource://devtools/client/netmonitor/src/constants.js",
+ true
+);
+
+const LEARN_MORE = l10n.getStr("webConsoleMoreInfoLabel");
+
+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,
+ disabled,
+}) {
+ 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");
+ }
+
+ // Message body components.
+ const requestMethod = dom.span({ className: "method" }, method);
+ const xhr = isXHR
+ ? dom.span({ className: "xhr" }, l10n.getStr("webConsoleXhrIndicator"))
+ : null;
+ const unicodeURL = getUnicodeUrl(url);
+ const requestUrl = dom.a(
+ {
+ className: "url",
+ title: unicodeURL,
+ href: url,
+ onClick: e => {
+ // The href of the <a> is the actual URL, so we need to prevent the navigation
+ // within the console panel.
+ // We only want to handle Ctrl/Cmd + click to open the link in a new tab.
+ e.preventDefault();
+ const shouldOpenLink =
+ (isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey);
+ if (shouldOpenLink) {
+ e.stopPropagation();
+ serviceContainer.openLink(url, e);
+ }
+ },
+ },
+ unicodeURL
+ );
+ 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);
+ },
+ triggerActivity: () => {},
+ requestData: (requestId, dataType) => {
+ return serviceContainer.requestData(requestId, dataType);
+ },
+ };
+
+ // Only render the attachment if the network-event is
+ // actually opened (performance optimization) and its not disabled.
+ const attachment =
+ open &&
+ !disabled &&
+ 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,
+ disabled,
+ 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..01828e968e
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/PageError.js
@@ -0,0 +1,130 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+const GripMessageBody = require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js");
+loader.lazyGetter(this, "REPS", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .REPS;
+});
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .MODE;
+});
+
+PageError.displayName = "PageError";
+
+PageError.propTypes = {
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool,
+ timestampsVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object,
+ maybeScrollToBottom: PropTypes.func,
+ setExpanded: PropTypes.func,
+ inWarningGroup: PropTypes.bool.isRequired,
+};
+
+PageError.defaultProps = {
+ open: false,
+};
+
+function PageError(props) {
+ const {
+ dispatch,
+ message,
+ open,
+ repeat,
+ serviceContainer,
+ timestampsVisible,
+ maybeScrollToBottom,
+ setExpanded,
+ 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,
+ setExpanded,
+ ...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/SimpleTable.js b/devtools/client/webconsole/components/Output/message-types/SimpleTable.js
new file mode 100644
index 0000000000..4f0414e562
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/SimpleTable.js
@@ -0,0 +1,134 @@
+/* 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,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const GripMessageBody = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .MODE;
+});
+
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+SimpleTable.displayName = "SimpleTable";
+
+SimpleTable.propTypes = {
+ columns: PropTypes.object.isRequired,
+ items: PropTypes.array.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+};
+
+function SimpleTable(props) {
+ const {
+ dispatch,
+ message,
+ serviceContainer,
+ timestampsVisible,
+ badge,
+ open,
+ } = props;
+
+ const {
+ source,
+ type,
+ level,
+ id: messageId,
+ indent,
+ timeStamp,
+ columns,
+ items,
+ } = message;
+
+ // if we don't have any data, don't show anything.
+ if (!items.length) {
+ return null;
+ }
+ const headerItems = [];
+ columns.forEach((value, key) =>
+ headerItems.push(
+ dom.th(
+ {
+ key,
+ title: value,
+ },
+ value
+ )
+ )
+ );
+
+ const rowItems = items.map((item, index) => {
+ const cells = [];
+
+ columns.forEach((_, key) => {
+ const cellValue = item[key];
+ const cellContent =
+ typeof cellValue === "undefined"
+ ? ""
+ : GripMessageBody({
+ grip: cellValue,
+ mode: MODE.SHORT,
+ useQuotes: false,
+ serviceContainer,
+ dispatch,
+ });
+
+ cells.push(
+ dom.td(
+ {
+ key,
+ },
+ cellContent
+ )
+ );
+ });
+ return dom.tr({ key: index }, cells);
+ });
+
+ const attachment = dom.table(
+ {
+ className: "simple-table",
+ role: "grid",
+ },
+ dom.thead({}, dom.tr({ className: "simple-table-header" }, headerItems)),
+ dom.tbody({}, rowItems)
+ );
+
+ const topLevelClasses = ["cm-s-mozilla"];
+ return Message({
+ attachment,
+ badge,
+ dispatch,
+ indent,
+ level,
+ messageId,
+ open,
+ serviceContainer,
+ source,
+ timeStamp,
+ timestampsVisible,
+ topLevelClasses,
+ type,
+ message,
+ messageBody: [],
+ });
+}
+
+module.exports = SimpleTable;
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..d54976dbcd
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/WarningGroup.js
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// React & Redux
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+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..ac1019bf05
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/moz.build
@@ -0,0 +1,17 @@
+# 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",
+ "NavigationMarker.js",
+ "NetworkEventMessage.js",
+ "PageError.js",
+ "SimpleTable.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..9844c2fdeb
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/moz.build
@@ -0,0 +1,21 @@
+# 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",
+ "LazyMessageList.js",
+ "Message.js",
+ "MessageContainer.js",
+ "MessageIcon.js",
+ "MessageIndent.js",
+ "MessageRepeat.js",
+)