From eb7f901289dce3f9fe3341da8c0b938ea839f79d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurk=C3=B3=20Mih=C3=A1ly?= Date: Mon, 16 Jul 2018 10:43:58 +0300 Subject: dashboard: fix CSS, escape special HTML chars, clean up code (#17167) * dashboard: fix CSS, escape special HTML chars, clean up code * dashboard: change 0 to 1 * dashboard: add escape-html npm package --- dashboard/assets/components/Body.jsx | 2 +- dashboard/assets/components/Header.jsx | 14 ++++- dashboard/assets/components/Logs.jsx | 110 +++++++++++++++++++-------------- dashboard/assets/package.json | 5 +- dashboard/assets/yarn.lock | 2 +- 5 files changed, 80 insertions(+), 53 deletions(-) (limited to 'dashboard/assets') diff --git a/dashboard/assets/components/Body.jsx b/dashboard/assets/components/Body.jsx index abf8c2f0e..da9f96839 100644 --- a/dashboard/assets/components/Body.jsx +++ b/dashboard/assets/components/Body.jsx @@ -27,7 +27,7 @@ const styles = { body: { display: 'flex', width: '100%', - height: '100%', + height: '92%', }, }; diff --git a/dashboard/assets/components/Header.jsx b/dashboard/assets/components/Header.jsx index ccdfbc6f0..43bd2bede 100644 --- a/dashboard/assets/components/Header.jsx +++ b/dashboard/assets/components/Header.jsx @@ -26,6 +26,16 @@ import Icon from 'material-ui/Icon'; import MenuIcon from 'material-ui-icons/Menu'; import Typography from 'material-ui/Typography'; +// styles contains the constant styles of the component. +const styles = { + header: { + height: '8%', + }, + toolbar: { + height: '100%', + }, +}; + // themeStyles returns the styles generated from the theme for the component. const themeStyles = (theme: Object) => ({ header: { @@ -54,8 +64,8 @@ class Header extends Component { const {classes} = this.props; return ( - - + + 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) => { content += `${lvl}[${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) => { 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 { 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 { 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 { // 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 { }; // 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 { 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 { render() { return (
{ this.content = ref; }}> +
+ {this.props.content.logs.endTop ? 'No more logs.' : 'Waiting for server...'} +
{this.props.content.logs.chunks.map((c, index) => ( @@ -302,6 +317,7 @@ class Logs extends Component { ))} + {this.props.content.logs.endBottom ||
Waiting for server...
}
); } diff --git a/dashboard/assets/package.json b/dashboard/assets/package.json index 5ac179b9e..fb1a68e5b 100644 --- a/dashboard/assets/package.json +++ b/dashboard/assets/package.json @@ -13,6 +13,7 @@ "babel-runtime": "^6.26.0", "classnames": "^2.2.5", "css-loader": "^0.28.9", + "escape-html": "^1.0.3", "eslint": "^4.16.0", "eslint-config-airbnb": "^16.1.0", "eslint-loader": "^2.0.0", @@ -41,7 +42,7 @@ "scripts": { "build": "NODE_ENV=production webpack", "stats": "webpack --profile --json > stats.json", - "dev": "webpack-dev-server --port 8081", - "flow": "flow-typed install" + "dev": "webpack-dev-server --port 8081", + "flow": "flow-typed install" } } diff --git a/dashboard/assets/yarn.lock b/dashboard/assets/yarn.lock index 0098d202b..10277c988 100644 --- a/dashboard/assets/yarn.lock +++ b/dashboard/assets/yarn.lock @@ -2248,7 +2248,7 @@ es6-weak-map@^2.0.1: es6-iterator "^2.0.1" es6-symbol "^3.1.1" -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" -- cgit v1.2.3