diff options
Diffstat (limited to 'dashboard/assets/components/Logs.jsx')
-rw-r--r-- | dashboard/assets/components/Logs.jsx | 110 |
1 files changed, 63 insertions, 47 deletions
diff --git a/dashboard/assets/components/Logs.jsx b/dashboard/assets/components/Logs.jsx index 203014276..57d6328d3 100644 --- a/dashboard/assets/components/Logs.jsx +++ b/dashboard/assets/components/Logs.jsx @@ -19,6 +19,7 @@ import React, {Component} from 'react'; import List, {ListItem} from 'material-ui/List'; +import escapeHtml from 'escape-html'; import type {Record, Content, LogsMessage, Logs as LogsType} from '../types/content'; // requestBand says how wide is the top/bottom zone, eg. 0.1 means 10% of the container height. @@ -83,8 +84,8 @@ const createChunk = (records: Array<Record>) => { content += `<span style="color:${color}">${lvl}</span>[${month}-${date}|${hours}:${minutes}:${seconds}] ${msg}`; for (let i = 0; i < ctx.length; i += 2) { - const key = ctx[i]; - const val = ctx[i + 1]; + const key = escapeHtml(ctx[i]); + const val = escapeHtml(ctx[i + 1]); let padding = fieldPadding.get(key); if (typeof padding !== 'number' || padding < val.length) { padding = val.length; @@ -101,11 +102,17 @@ const createChunk = (records: Array<Record>) => { return content; }; +// ADDED, SAME and REMOVED are used to track the change of the log chunk array. +// The scroll position is set using these values. +const ADDED = 1; +const SAME = 0; +const REMOVED = -1; + // inserter is a state updater function for the main component, which inserts the new log chunk into the chunk array. // limit is the maximum length of the chunk array, used in order to prevent the browser from OOM. export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType) => { - prev.topChanged = 0; - prev.bottomChanged = 0; + prev.topChanged = SAME; + prev.bottomChanged = SAME; if (!Array.isArray(update.chunk) || update.chunk.length < 1) { return prev; } @@ -123,7 +130,7 @@ export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType) return [{content, name: '00000000000000.log'}]; } prev.chunks[prev.chunks.length - 1].content += content; - prev.bottomChanged = 1; + prev.bottomChanged = ADDED; return prev; } const chunk = { @@ -137,10 +144,10 @@ export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType) if (prev.chunks.length >= limit) { prev.endBottom = false; prev.chunks.splice(limit - 1, prev.chunks.length - limit + 1); - prev.bottomChanged = -1; + prev.bottomChanged = REMOVED; } prev.chunks = [chunk, ...prev.chunks]; - prev.topChanged = 1; + prev.topChanged = ADDED; return prev; } if (update.source.last) { @@ -149,10 +156,10 @@ export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType) if (prev.chunks.length >= limit) { prev.endTop = false; prev.chunks.splice(0, prev.chunks.length - limit + 1); - prev.topChanged = -1; + prev.topChanged = REMOVED; } prev.chunks = [...prev.chunks, chunk]; - prev.bottomChanged = 1; + prev.bottomChanged = ADDED; return prev; }; @@ -160,6 +167,7 @@ export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType) const styles = { logListItem: { padding: 0, + lineHeight: 1.231, }, logChunk: { color: 'white', @@ -167,6 +175,11 @@ const styles = { whiteSpace: 'nowrap', width: 0, }, + waitMsg: { + textAlign: 'center', + color: 'white', + fontFamily: 'monospace', + }, }; export type Props = { @@ -192,7 +205,17 @@ class Logs extends Component<Props, State> { componentDidMount() { const {container} = this.props; + if (typeof container === 'undefined') { + return; + } container.scrollTop = container.scrollHeight - container.clientHeight; + const {logs} = this.props.content; + if (typeof this.content === 'undefined' || logs.chunks.length < 1) { + return; + } + if (this.content.clientHeight < container.clientHeight && !logs.endTop) { + this.sendRequest(logs.chunks[0].name, true); + } } // onScroll is triggered by the parent component's scroll event, and sends requests if the scroll position is @@ -205,29 +228,23 @@ class Logs extends Component<Props, State> { if (logs.chunks.length < 1) { return; } - if (this.atTop()) { - if (!logs.endTop) { - this.setState({requestAllowed: false}); - this.props.send(JSON.stringify({ - Logs: { - Name: logs.chunks[0].name, - Past: true, - }, - })); - } - } else if (this.atBottom()) { - if (!logs.endBottom) { - this.setState({requestAllowed: false}); - this.props.send(JSON.stringify({ - Logs: { - Name: logs.chunks[logs.chunks.length - 1].name, - Past: false, - }, - })); - } + if (this.atTop() && !logs.endTop) { + this.sendRequest(logs.chunks[0].name, true); + } else if (this.atBottom() && !logs.endBottom) { + this.sendRequest(logs.chunks[logs.chunks.length - 1].name, false); } }; + sendRequest = (name: string, past: boolean) => { + this.setState({requestAllowed: false}); + this.props.send(JSON.stringify({ + Logs: { + Name: name, + Past: past, + }, + })); + }; + // atTop checks if the scroll position it at the top of the container. atTop = () => this.props.container.scrollTop <= this.props.container.scrollHeight * requestBand; @@ -242,8 +259,9 @@ class Logs extends Component<Props, State> { // and the height of the first log chunk, which can be deleted during the insertion. beforeUpdate = () => { let firstHeight = 0; - if (this.content && this.content.children[0] && this.content.children[0].children[0]) { - firstHeight = this.content.children[0].children[0].clientHeight; + let chunkList = this.content.children[1]; + if (chunkList && chunkList.children[0]) { + firstHeight = chunkList.children[0].clientHeight; } return { scrollTop: this.props.container.scrollTop, @@ -252,8 +270,8 @@ class Logs extends Component<Props, State> { }; // didUpdate is called by the parent component, which provides the container. Sends the first request if the - // visible part of the container isn't full, and resets the scroll position in order to avoid jumping when new - // chunk is inserted. + // visible part of the container isn't full, and resets the scroll position in order to avoid jumping when a + // chunk is inserted or removed. didUpdate = (prevProps, prevState, snapshot) => { if (typeof this.props.shouldUpdate.logs === 'undefined' || typeof this.content === 'undefined' || snapshot === null) { return; @@ -264,27 +282,21 @@ class Logs extends Component<Props, State> { return; } if (this.content.clientHeight < container.clientHeight) { - // Only enters here at the beginning, when there isn't enough log to fill the container + // Only enters here at the beginning, when there aren't enough logs to fill the container // and the scroll bar doesn't appear. if (!logs.endTop) { - this.setState({requestAllowed: false}); - this.props.send(JSON.stringify({ - Logs: { - Name: logs.chunks[0].name, - Past: true, - }, - })); + this.sendRequest(logs.chunks[0].name, true); } return; } - const chunks = this.content.children[0].children; let {scrollTop} = snapshot; - if (logs.topChanged > 0) { - scrollTop += chunks[0].clientHeight; - } else if (logs.bottomChanged > 0) { - if (logs.topChanged < 0) { + if (logs.topChanged === ADDED) { + // It would be safer to use a ref to the list, but ref doesn't work well with HOCs. + scrollTop += this.content.children[1].children[0].clientHeight; + } else if (logs.bottomChanged === ADDED) { + if (logs.topChanged === REMOVED) { scrollTop -= snapshot.firstHeight; - } else if (logs.endBottom && this.atBottom()) { + } else if (this.atBottom() && logs.endBottom) { scrollTop = container.scrollHeight - container.clientHeight; } } @@ -295,6 +307,9 @@ class Logs extends Component<Props, State> { render() { return ( <div ref={(ref) => { this.content = ref; }}> + <div style={styles.waitMsg}> + {this.props.content.logs.endTop ? 'No more logs.' : 'Waiting for server...'} + </div> <List> {this.props.content.logs.chunks.map((c, index) => ( <ListItem style={styles.logListItem} key={index}> @@ -302,6 +317,7 @@ class Logs extends Component<Props, State> { </ListItem> ))} </List> + {this.props.content.logs.endBottom || <div style={styles.waitMsg}>Waiting for server...</div>} </div> ); } |