/** * The finsemble console. It displays the filtered logs */ import React, { CSSProperties } from "react"; import AutoSizer from "react-virtualized/dist/commonjs/AutoSizer"; import { CellMeasurer, CellMeasurerCache, List } from "react-virtualized"; import LoggerStore from "../../../../stores/LoggerStore"; import ObjectInspector from "../../../objectInspector/object-inspector/ObjectInspector"; import inspectorTheme from "../objectInspectorTheme"; import { IconMap } from "./chromeStyleIcons"; import Highlighter from "../../../highlighter/Highlighter"; import { LoggerClient } from "../../../../stores/LoggerStore"; import "./consoleView.css"; import { LogLevels } from "../../../../../types"; import { FormattedLogMessage } from "../../../../../FormattedLogMessage"; const LOG_OUT_OF_ORDER_TOOLTIP = "This log happened after the next log in the list."; /** * React component to render next to the log's icon * in the event that it is out of order relative * to the next log in the list. * There's a pernicious, transient bug that sometimes * causes logs to render out of order. We cannot reliably reproduce * it, so instead, we just warn the developer that something is wrong. */ const OutOfOrder = () => { const Icon = IconMap["Error"]; return ; }; const formatYYYYMMDDHHMMSS = (dateValue: Date) => { // Pad number with leading zeroes so that there are 2 numbers const pad2 = (n: number): string => (n < 10 ? "0" : "") + n; // Pad number with leading zeroes so that there are 3 numbers const pad3 = (n: number): string => { if (n < 10) { return `00${n}`; } if (n < 100) { return `0${n}`; } return String(n); }; return `${pad2(dateValue.getHours())}:${pad2(dateValue.getMinutes())}:${pad2(dateValue.getSeconds())}:${pad3( dateValue.getMilliseconds() )}`; }; const baseLogClass = "fsbl-log-message fsbl-plaintext-message-wrapper console-message-wrapper"; const logTypeClasses = { Verbose: `${baseLogClass} fsbl-log-verbose`, Log: `${baseLogClass} console-log-level`, Info: `${baseLogClass} console-info-level`, Debug: `${baseLogClass} console-debug-level`, Warn: `${baseLogClass} console-warning-level`, Error: `${baseLogClass} console-error-level`, }; const cache = new CellMeasurerCache({ defaultHeight: 50, fixedWidth: true, }); let ListRef: any; const setRef = (ref: any) => { ListRef = ref; (window as any).ListRef = ListRef; }; interface IProps {} interface IState { tableWidth: number; tableHeight: number; logCounts: { master: number; display: number; }; registeredClients: Record; searchBoxIsVisible: boolean; searchBoxText: string; filterStrings: string[]; allowedLogLevels: LogLevels; allClientVisibility: Record; firstMessageTime: number; activeRowTimestamp: number; activeRowIndex: number | null; clientListVisible: boolean; comparisonRows: number[]; comparisonRow?: FormattedLogMessage; } // Rough cut on component for raw table output. Currently not used but may need to enhance if "View JSON" is too slow or limited export default class LogList extends React.Component { update: boolean = false; ctrl: boolean = false; state: IState; constructor(props: IProps) { super(props); this.state = { tableWidth: 100, tableHeight: 100, logCounts: LoggerStore.getLogCounts(), registeredClients: LoggerStore.getRegisteredClients(), searchBoxIsVisible: LoggerStore.getShowSearchBox(), searchBoxText: LoggerStore.getSearchBoxString(), filterStrings: this.getFilterStrings(false), allowedLogLevels: LoggerStore.getLogState(), allClientVisibility: LoggerStore.getAllShowClientState(), firstMessageTime: LoggerStore.getFirstMessageTime(), // These two are used to keep track of your position as you're filtering. activeRowTimestamp: 0, activeRowIndex: null, clientListVisible: LoggerStore.getClientListVisible(), comparisonRows: [], }; this.getRegisteredClients = this.getRegisteredClients.bind(this); this.updateActiveRowState = this.updateActiveRowState.bind(this); this.windowResize = this.windowResize.bind(this); this.rowRenderer = this.rowRenderer.bind(this); this.onRowClicked = this.onRowClicked.bind(this); this.setSearchBoxIsVisible = this.setSearchBoxIsVisible.bind(this); this.updateSearchBoxText = this.updateSearchBoxText.bind(this); this.getFilterStrings = this.getFilterStrings.bind(this); this.setFirstMessageTime = this.setFirstMessageTime.bind(this); this.setBottom = this.setBottom.bind(this); this.setActiveRowTimestamp = this.setActiveRowTimestamp.bind(this); this.scrollToActiveRow = this.scrollToActiveRow.bind(this); this.addToRowDiff = this.addToRowDiff.bind(this); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); this.onActiveRowChanged = this.onActiveRowChanged.bind(this); } componentDidMount() { ListRef.recomputeRowHeights(); LoggerStore.addListener("loggerNewMessages", this.updateActiveRowState); LoggerStore.addListener("newShowClientState", this.updateActiveRowState); LoggerStore.addListener("showSearchBoxChange", this.setSearchBoxIsVisible); LoggerStore.addListener("searchBoxTextChange", this.updateSearchBoxText); LoggerStore.addListener("activeRowChanged", this.onActiveRowChanged); LoggerStore.addListener("NewHighlightValues1", this.getFilterStrings); LoggerStore.addListener("NewHighlightValues2", this.getFilterStrings); LoggerStore.addListener("NewHighlightValues3", this.getFilterStrings); LoggerStore.addListener("NewHighlightValues4", this.getFilterStrings); LoggerStore.addListener("rerenderList", this.rerenderList); LoggerStore.addListener("recalculateHeight", this.recalculateAllRows); LoggerStore.addListener("newFirstMessageTime", this.setFirstMessageTime); LoggerStore.addListener("SetTop", this.setTop); LoggerStore.addListener("SetBottom", this.setBottom); LoggerStore.addListener("scrollToActiveRow", this.scrollToActiveRow); LoggerStore.addListener("newShowClientState", this.getRegisteredClients); window.addEventListener("keydown", this.onKeyDown); window.addEventListener("keyup", this.onKeyUp); } componentWillUnmount() { LoggerStore.removeListener("loggerNewMessages", this.updateActiveRowState); LoggerStore.removeListener("newShowClientState", this.updateActiveRowState); LoggerStore.removeListener("showSearchBoxChange", this.setSearchBoxIsVisible); LoggerStore.removeListener("activeRowChanged", this.onActiveRowChanged); LoggerStore.removeListener("searchBoxTextChange", this.updateSearchBoxText); LoggerStore.removeListener("NewHighlightValues1", this.getFilterStrings); LoggerStore.removeListener("NewHighlightValues2", this.getFilterStrings); LoggerStore.removeListener("NewHighlightValues3", this.getFilterStrings); LoggerStore.removeListener("NewHighlightValues4", this.getFilterStrings); LoggerStore.removeListener("rerenderList", this.rerenderList); LoggerStore.removeListener("recalculateHeight", this.recalculateAllRows); LoggerStore.removeListener("newFirstMessageTime", this.setFirstMessageTime); LoggerStore.removeListener("SetTop", this.setTop); LoggerStore.removeListener("SetBottom", this.setBottom); LoggerStore.removeListener("scrollToActiveRow", this.scrollToActiveRow); LoggerStore.removeListener("newShowClientState", this.getRegisteredClients); window.removeEventListener("keydown", this.onKeyDown); window.removeEventListener("keyup", this.onKeyUp); } getRegisteredClients() { this.setState({ registeredClients: LoggerStore.getRegisteredClients(), }); } setFirstMessageTime() { this.setState({ firstMessageTime: LoggerStore.getFirstMessageTime(), }); } // whether to return or set state...the constructor needs the values. everywhere else is setState getFilterStrings(setState: boolean = true): string[] { let filterStrings = LoggerStore.getFilterStrings().map((filter) => filter.str); if (setState) { this.setState({ filterStrings, }); } return filterStrings; } updateSearchBoxText() { this.setState({ searchBoxText: LoggerStore.getSearchBoxString(), }); } setSearchBoxIsVisible() { this.setState({ searchBoxIsVisible: LoggerStore.getShowSearchBox(), }); } setActiveRowTimestamp(timestamp) { this.setState({ activeRowTimestamp: timestamp, activeRowIndex: this.getActiveRowIndex(timestamp), }); } windowResize() { const newTableWidth = LoggerStore.getWindowWidth(); const windowHeight = LoggerStore.getWindowHeight(); const logHeader = document.querySelector("#message-list"); const offsetTop = logHeader ? logHeader.getBoundingClientRect().top : 600; const newTableHeight = Math.abs(windowHeight - offsetTop - 5); this.update = true; this.setState({ tableWidth: newTableWidth, tableHeight: newTableHeight, }); } updateActiveRowState() { this.update = true; this.setState({ activeRowIndex: this.getActiveRowIndex(), }); } onActiveRowChanged() { const index = LoggerStore.getActiveRowIndex(); this.setState( { comparisonRows: [], activeRowIndex: index, }, () => { this.scrollToActiveRow(); const filteredLog = LoggerStore.getFilteredLog(); if (filteredLog[index]) { this.setActiveRowTimestamp(filteredLog[index].logTimestamp); } this.addToRowDiff(index); } ); } onRowClicked(index: number = 0) { cache.clear(index); this.setActiveRowTimestamp(LoggerStore.getFilteredLog()[index].logTimestamp); this.addToRowDiff(index); } recalculateAllRows() { cache.clearAll(); ListRef.recomputeRowHeights(); } rerenderList() { cache.clearAll(); ListRef.forceUpdate(); } scrollToActiveRow() { if (this.state.activeRowIndex === null || typeof this.state.activeRowIndex === "undefined") return; ListRef.recomputeRowHeights(this.state.activeRowIndex); ListRef.scrollToRow(this.state.activeRowIndex); } setTop() { ListRef.scrollToRow(0); } setBottom() { const filteredLog = LoggerStore.getFilteredLog(); // don't continue if empty list -- the code doesn't handle it and crashes (probably because of length-1 calculation) if (filteredLog.length > 0) { ListRef.recomputeRowHeights(filteredLog.length - 1); ListRef.scrollToRow(filteredLog.length - 1); } } openClient(windowName: string, viewId: number | null = null) { LoggerStore.maybeOpenClient({ windowName, viewId }); } addToRowDiff(index: number) { const filteredLog = LoggerStore.getFilteredLog(); let targetRow = filteredLog[index]; let rowIndexes = this.state.comparisonRows.filter((row) => row !== undefined); let rowDiff: number | null = null; if (this.state.comparisonRow) { if (this.ctrl) { if (rowIndexes.length == 1) { rowIndexes.push(index); } else { rowIndexes = [index]; } targetRow = this.state.comparisonRow; } else { rowIndexes = [index]; } } else { rowIndexes = [index]; } if (rowIndexes.length === 2) { rowDiff = Math.abs( filteredLog[rowIndexes[0]].timeElapsedFromStartup - filteredLog[rowIndexes[1]].timeElapsedFromStartup ); } LoggerStore.setRowTimeDiff(rowDiff); this.update = true; this.setState({ comparisonRow: targetRow, comparisonRows: rowIndexes, }); } getLogContent(strToHighlight: string, ind: number) { let messageClasses = "highlighted-message"; if (typeof strToHighlight === "string" && strToHighlight.includes("Stack")) { messageClasses = `${messageClasses} message-log-stack`; } return (   ); } rowRenderer({ index, key, parent, style }: { index: number; key: string; parent: any; style: CSSProperties }) { const filteredLog = LoggerStore.getFilteredLog(); const log = filteredLog[index]; let row = this.state.registeredClients[log.logClientName]; let viewId = row?.viewId ? row.viewId : null; /** * Occasionally we have had internal reports of log messages being out of order * We could not replicate it. In order to prevent devs from * going down the wrong path when debugging a problem, we check to see * if this log happened after the log next in the list. * * If it is, we will render a small error icon that tells the dev * "Hey, something is awry here". */ const nextLog = filteredLog[index + 1]; let isOutOfOrder = false; if (nextLog && log.logTimestamp > nextLog.logTimestamp) { isOutOfOrder = true; } if (!log) { return null; } log.previousRowTimeDelta = 0; log.timeElapsedFromStartup = Math.round(log.logTimestamp - this.state.firstMessageTime); if (index > 0) { const previousLog = filteredLog[index - 1]; if (previousLog) { log.previousRowTimeDelta = log.timeElapsedFromStartup - previousLog.timeElapsedFromStartup; } } // "style" is a react prop. It is const, so it cannot be modified. Since we want to change // the style here we'll clone it and use the clone instead. const styleClone: CSSProperties = { ...style, wordBreak: "break-all", display: "flex", alignItems: "center", justifyContent: "space-between", padding: "3px", width: "calc(100% - 8px)", }; // log.logMessageWithoutStack = log.allLogArgs.replace(/Log Stack.*/g, ""); const LogIcon = IconMap[log.logType]; let wrapperClasses = logTypeClasses[log.logType]; if (index === this.state.activeRowIndex) { wrapperClasses = `${wrapperClasses} active-console-row`; } if (this.state.comparisonRows.includes(index)) { wrapperClasses = `${wrapperClasses} fsbl-logger-row-selected`; } const timestampClasses = isOutOfOrder ? "time-elapsed out-of-order" : "time-elapsed"; const content = (
{ cache.clear(index); ListRef.recomputeRowHeights(index); }} // to handle expansion of js objects. onClick={() => { this.onRowClicked(index); }} style={styleClone} className={wrapperClasses} key={key} > {isOutOfOrder && }
{formatYYYYMMDDHHMMSS(new Date(log.logTimestamp))}
{log.parsedLogArgs.map((arg: any, i: number) => { if (typeof arg === "object") { return ( ); } return this.getLogContent(arg, i); })}
{ this.openClient(row?.name, viewId); }} > {log.logClientName}
); return ( {content} ); } shouldComponentUpdate() { let update = false; if (LoggerStore.getWindowVisibility() === "visible") { update = true; } return update; } getActiveRowIndex(timestamp: number = this.state.activeRowTimestamp) { const filteredLog = LoggerStore.getFilteredLog(); for (let i = 0; i < filteredLog.length; i++) { const log = filteredLog[i]; if (log.logTimestamp === timestamp) { return i; } } return null; } onKeyDown(e: KeyboardEvent) { this.ctrl = e.ctrlKey; } onKeyUp() { this.ctrl = false; } render() { cache.clearAll(); let heightOffset = document.getElementById("log-level-filters")?.offsetHeight || 63; if (this.state.searchBoxIsVisible) { heightOffset = heightOffset + 20; } return ( {({ width, height }) => (
null} rowCount={LoggerStore.getFilteredLog().length} deferredMeasurementCache={cache} rowHeight={cache.rowHeight || 20} rowRenderer={this.rowRenderer} />
)}
); } }