summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/components/messages/MessageListContent.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/netmonitor/src/components/messages/MessageListContent.js398
1 files changed, 398 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/components/messages/MessageListContent.js b/devtools/client/netmonitor/src/components/messages/MessageListContent.js
new file mode 100644
index 0000000000..f4377911af
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/messages/MessageListContent.js
@@ -0,0 +1,398 @@
+/* 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 {
+ connect,
+} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+const {
+ getDisplayedMessages,
+ isCurrentChannelClosed,
+ getClosedConnectionDetails,
+} = require("resource://devtools/client/netmonitor/src/selectors/index.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const { table, tbody, tr, td, div, input, label, hr, p } = dom;
+const {
+ L10N,
+} = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
+const MESSAGES_EMPTY_TEXT = L10N.getStr("messagesEmptyText");
+const TOGGLE_MESSAGES_TRUNCATION = L10N.getStr("toggleMessagesTruncation");
+const TOGGLE_MESSAGES_TRUNCATION_TITLE = L10N.getStr(
+ "toggleMessagesTruncation.title"
+);
+const CONNECTION_CLOSED_TEXT = L10N.getStr("netmonitor.ws.connection.closed");
+const {
+ MESSAGE_HEADERS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
+
+const {
+ getSelectedMessage,
+} = require("resource://devtools/client/netmonitor/src/selectors/index.js");
+
+// Components
+const MessageListContextMenu = require("resource://devtools/client/netmonitor/src/components/messages/MessageListContextMenu.js");
+loader.lazyGetter(this, "MessageListHeader", function () {
+ return createFactory(
+ require("resource://devtools/client/netmonitor/src/components/messages/MessageListHeader.js")
+ );
+});
+loader.lazyGetter(this, "MessageListItem", function () {
+ return createFactory(
+ require("resource://devtools/client/netmonitor/src/components/messages/MessageListItem.js")
+ );
+});
+
+const LEFT_MOUSE_BUTTON = 0;
+
+/**
+ * Renders the actual contents of the message list.
+ */
+class MessageListContent extends Component {
+ static get propTypes() {
+ return {
+ connector: PropTypes.object.isRequired,
+ startPanelContainer: PropTypes.object,
+ messages: PropTypes.array,
+ selectedMessage: PropTypes.object,
+ selectMessage: PropTypes.func.isRequired,
+ columns: PropTypes.object.isRequired,
+ isClosed: PropTypes.bool.isRequired,
+ closedConnectionDetails: PropTypes.object,
+ channelId: PropTypes.number,
+ onSelectMessageDelta: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onContextMenu = this.onContextMenu.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.messagesLimit = Services.prefs.getIntPref(
+ "devtools.netmonitor.msg.displayed-messages.limit"
+ );
+ this.currentTruncatedNum = 0;
+ this.state = {
+ checked: false,
+ };
+ this.pinnedToBottom = false;
+ this.initIntersectionObserver = false;
+ this.intersectionObserver = null;
+ this.toggleTruncationCheckBox = this.toggleTruncationCheckBox.bind(this);
+ }
+
+ componentDidMount() {
+ const { startPanelContainer } = this.props;
+ const { scrollAnchor } = this.refs;
+
+ if (scrollAnchor) {
+ // Always scroll to anchor when MessageListContent component first mounts.
+ scrollAnchor.scrollIntoView();
+ }
+ this.setupScrollToBottom(startPanelContainer, scrollAnchor);
+ }
+
+ componentDidUpdate(prevProps) {
+ const { startPanelContainer, channelId } = this.props;
+ const { scrollAnchor } = this.refs;
+
+ // When messages are cleared, the previous scrollAnchor would be destroyed, so we need to reset this boolean.
+ if (!scrollAnchor) {
+ this.initIntersectionObserver = false;
+ }
+
+ // In addition to that, we need to reset currentTruncatedNum
+ if (prevProps.messages.length && this.props.messages.length === 0) {
+ this.currentTruncatedNum = 0;
+ }
+
+ // If a new connection is selected, scroll to anchor.
+ if (channelId !== prevProps.channelId && scrollAnchor) {
+ scrollAnchor.scrollIntoView();
+ }
+
+ // Do not autoscroll if the selection changed. This would cause
+ // the newly selected message to jump just after clicking in.
+ // (not user friendly)
+ //
+ // If the selection changed, we need to ensure that the newly
+ // selected message is properly scrolled into the visible area.
+ if (prevProps.selectedMessage === this.props.selectedMessage) {
+ this.setupScrollToBottom(startPanelContainer, scrollAnchor);
+ } else {
+ const head = document.querySelector("thead.message-list-headers-group");
+ const selectedRow = document.querySelector(
+ "tr.message-list-item.selected"
+ );
+
+ if (selectedRow) {
+ const rowRect = selectedRow.getBoundingClientRect();
+ const scrollableRect = startPanelContainer.getBoundingClientRect();
+ const headRect = head.getBoundingClientRect();
+
+ if (rowRect.top <= scrollableRect.top) {
+ selectedRow.scrollIntoView(true);
+
+ // We need to scroll a bit more to get the row out
+ // of the header. The header is sticky and overlaps
+ // part of the scrollable area.
+ startPanelContainer.scrollTop -= headRect.height;
+ } else if (rowRect.bottom > scrollableRect.bottom) {
+ selectedRow.scrollIntoView(false);
+ }
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ // Reset observables and boolean values.
+ const { scrollAnchor } = this.refs;
+
+ if (this.intersectionObserver) {
+ if (scrollAnchor) {
+ this.intersectionObserver.unobserve(scrollAnchor);
+ }
+ this.initIntersectionObserver = false;
+ this.pinnedToBottom = false;
+ }
+ }
+
+ setupScrollToBottom(startPanelContainer, scrollAnchor) {
+ if (startPanelContainer && scrollAnchor) {
+ // Initialize intersection observer.
+ if (!this.initIntersectionObserver) {
+ this.intersectionObserver = new IntersectionObserver(
+ () => {
+ // When scrollAnchor first comes into view, this.pinnedToBottom is set to true.
+ // When the anchor goes out of view, this callback function triggers again and toggles this.pinnedToBottom.
+ // Subsequent scroll into/out of view will toggle this.pinnedToBottom.
+ this.pinnedToBottom = !this.pinnedToBottom;
+ },
+ {
+ root: startPanelContainer,
+ threshold: 0.1,
+ }
+ );
+ if (this.intersectionObserver) {
+ this.intersectionObserver.observe(scrollAnchor);
+ this.initIntersectionObserver = true;
+ }
+ }
+
+ if (this.pinnedToBottom) {
+ scrollAnchor.scrollIntoView();
+ }
+ }
+ }
+
+ toggleTruncationCheckBox() {
+ this.setState({
+ checked: !this.state.checked,
+ });
+ }
+
+ onMouseDown(evt, item) {
+ if (evt.button === LEFT_MOUSE_BUTTON) {
+ this.props.selectMessage(item);
+ }
+ }
+
+ onContextMenu(evt, item) {
+ evt.preventDefault();
+ const { connector } = this.props;
+ this.contextMenu = new MessageListContextMenu({
+ connector,
+ });
+ this.contextMenu.open(evt, item);
+ }
+
+ /**
+ * Handler for keyboard events. For arrow up/down, page up/down, home/end,
+ * move the selection up or down.
+ */
+ onKeyDown(evt) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ let delta;
+
+ switch (evt.key) {
+ case "ArrowUp":
+ delta = -1;
+ break;
+ case "ArrowDown":
+ delta = +1;
+ break;
+ case "PageUp":
+ delta = "PAGE_UP";
+ break;
+ case "PageDown":
+ delta = "PAGE_DOWN";
+ break;
+ case "Home":
+ delta = -Infinity;
+ break;
+ case "End":
+ delta = +Infinity;
+ break;
+ }
+
+ if (delta) {
+ this.props.onSelectMessageDelta(delta);
+ }
+ }
+
+ render() {
+ const {
+ messages,
+ selectedMessage,
+ connector,
+ columns,
+ isClosed,
+ closedConnectionDetails,
+ } = this.props;
+
+ if (messages.length === 0) {
+ return div(
+ { className: "empty-notice message-list-empty-notice" },
+ MESSAGES_EMPTY_TEXT
+ );
+ }
+
+ const visibleColumns = MESSAGE_HEADERS.filter(
+ header => columns[header.name]
+ ).map(col => col.name);
+
+ let displayedMessages;
+ let MESSAGES_TRUNCATED;
+ const shouldTruncate = messages.length > this.messagesLimit;
+ if (shouldTruncate) {
+ // If the checkbox is checked, we display all messages after the currentTruncatedNum limit.
+ // If the checkbox is unchecked, we display all messages after the messagesLimit.
+ this.currentTruncatedNum = this.state.checked
+ ? this.currentTruncatedNum
+ : messages.length - this.messagesLimit;
+ displayedMessages = messages.slice(this.currentTruncatedNum);
+
+ MESSAGES_TRUNCATED = PluralForm.get(
+ this.currentTruncatedNum,
+ L10N.getStr("netmonitor.ws.truncated-messages.warning")
+ ).replace("#1", this.currentTruncatedNum);
+ } else {
+ displayedMessages = messages;
+ }
+
+ let connectionClosedMsg = CONNECTION_CLOSED_TEXT;
+ if (
+ closedConnectionDetails &&
+ closedConnectionDetails.code !== undefined &&
+ closedConnectionDetails.reason !== undefined
+ ) {
+ connectionClosedMsg += `: ${closedConnectionDetails.code} ${closedConnectionDetails.reason}`;
+ }
+ return div(
+ {},
+ table(
+ { className: "message-list-table" },
+ MessageListHeader(),
+ tbody(
+ {
+ className: "message-list-body",
+ onKeyDown: this.onKeyDown,
+ },
+ tr(
+ {
+ tabIndex: 0,
+ },
+ td(
+ {
+ className: "truncated-messages-cell",
+ colSpan: visibleColumns.length,
+ },
+ shouldTruncate &&
+ div(
+ {
+ className: "truncated-messages-header",
+ },
+ div(
+ {
+ className: "truncated-messages-container",
+ },
+ div({
+ className: "truncated-messages-warning-icon",
+ }),
+ div(
+ {
+ className: "truncated-message",
+ title: MESSAGES_TRUNCATED,
+ },
+ MESSAGES_TRUNCATED
+ )
+ ),
+ label(
+ {
+ className: "truncated-messages-checkbox-label",
+ title: TOGGLE_MESSAGES_TRUNCATION_TITLE,
+ },
+ input({
+ type: "checkbox",
+ className: "truncation-checkbox",
+ title: TOGGLE_MESSAGES_TRUNCATION_TITLE,
+ checked: this.state.checked,
+ onChange: this.toggleTruncationCheckBox,
+ }),
+ TOGGLE_MESSAGES_TRUNCATION
+ )
+ )
+ )
+ ),
+ displayedMessages.map((item, index) =>
+ MessageListItem({
+ key: "message-list-item-" + index,
+ item,
+ index,
+ isSelected: item === selectedMessage,
+ onMouseDown: evt => this.onMouseDown(evt, item),
+ onContextMenu: evt => this.onContextMenu(evt, item),
+ connector,
+ visibleColumns,
+ })
+ )
+ )
+ ),
+ isClosed &&
+ p(
+ {
+ className: "msg-connection-closed-message",
+ },
+ connectionClosedMsg
+ ),
+ hr({
+ ref: "scrollAnchor",
+ className: "message-list-scroll-anchor",
+ })
+ );
+ }
+}
+
+module.exports = connect(
+ state => ({
+ selectedMessage: getSelectedMessage(state),
+ messages: getDisplayedMessages(state),
+ columns: state.messages.columns,
+ isClosed: isCurrentChannelClosed(state),
+ closedConnectionDetails: getClosedConnectionDetails(state),
+ }),
+ dispatch => ({
+ selectMessage: item => dispatch(Actions.selectMessage(item)),
+ onSelectMessageDelta: delta => dispatch(Actions.selectMessageDelta(delta)),
+ })
+)(MessageListContent);