Logs.jsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. // @flow
  2. // Copyright 2018 The go-ethereum Authors
  3. // This file is part of the go-ethereum library.
  4. //
  5. // The go-ethereum library is free software: you can redistribute it and/or modify
  6. // it under the terms of the GNU Lesser General Public License as published by
  7. // the Free Software Foundation, either version 3 of the License, or
  8. // (at your option) any later version.
  9. //
  10. // The go-ethereum library is distributed in the hope that it will be useful,
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. // GNU Lesser General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU Lesser General Public License
  16. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
  17. import React, {Component} from 'react';
  18. import List from '@material-ui/core/List';
  19. import ListItem from '@material-ui/core/ListItem';
  20. import escapeHtml from 'escape-html';
  21. import type {Record, Content, LogsMessage, Logs as LogsType} from '../types/content';
  22. // requestBand says how wide is the top/bottom zone, eg. 0.1 means 10% of the container height.
  23. const requestBand = 0.05;
  24. // fieldPadding is a global map with maximum field value lengths seen until now
  25. // to allow padding log contexts in a bit smarter way.
  26. const fieldPadding = new Map();
  27. // createChunk creates an HTML formatted object, which displays the given array similarly to
  28. // the server side terminal.
  29. const createChunk = (records: Array<Record>) => {
  30. let content = '';
  31. records.forEach((record) => {
  32. const {t, ctx} = record;
  33. let {lvl, msg} = record;
  34. let color = '#ce3c23';
  35. switch (lvl) {
  36. case 'trace':
  37. case 'trce':
  38. lvl = 'TRACE';
  39. color = '#3465a4';
  40. break;
  41. case 'debug':
  42. case 'dbug':
  43. lvl = 'DEBUG';
  44. color = '#3d989b';
  45. break;
  46. case 'info':
  47. lvl = 'INFO&nbsp;';
  48. color = '#4c8f0f';
  49. break;
  50. case 'warn':
  51. lvl = 'WARN&nbsp;';
  52. color = '#b79a22';
  53. break;
  54. case 'error':
  55. case 'eror':
  56. lvl = 'ERROR';
  57. color = '#754b70';
  58. break;
  59. case 'crit':
  60. lvl = 'CRIT&nbsp;';
  61. color = '#ce3c23';
  62. break;
  63. default:
  64. lvl = '';
  65. }
  66. const time = new Date(t);
  67. if (lvl === '' || !(time instanceof Date) || isNaN(time) || typeof msg !== 'string' || !Array.isArray(ctx)) {
  68. content += '<span style="color:#ce3c23">Invalid log record</span><br />';
  69. return;
  70. }
  71. if (ctx.length > 0) {
  72. msg += '&nbsp;'.repeat(Math.max(40 - msg.length, 0));
  73. }
  74. const month = `0${time.getMonth() + 1}`.slice(-2);
  75. const date = `0${time.getDate()}`.slice(-2);
  76. const hours = `0${time.getHours()}`.slice(-2);
  77. const minutes = `0${time.getMinutes()}`.slice(-2);
  78. const seconds = `0${time.getSeconds()}`.slice(-2);
  79. content += `<span style="color:${color}">${lvl}</span>[${month}-${date}|${hours}:${minutes}:${seconds}] ${msg}`;
  80. for (let i = 0; i < ctx.length; i += 2) {
  81. const key = escapeHtml(ctx[i]);
  82. const val = escapeHtml(ctx[i + 1]);
  83. let padding = fieldPadding.get(key);
  84. if (typeof padding !== 'number' || padding < val.length) {
  85. padding = val.length;
  86. fieldPadding.set(key, padding);
  87. }
  88. let p = '';
  89. if (i < ctx.length - 2) {
  90. p = '&nbsp;'.repeat(padding - val.length);
  91. }
  92. content += ` <span style="color:${color}">${key}</span>=${val}${p}`;
  93. }
  94. content += '<br />';
  95. });
  96. return content;
  97. };
  98. // ADDED, SAME and REMOVED are used to track the change of the log chunk array.
  99. // The scroll position is set using these values.
  100. export const ADDED = 1;
  101. export const SAME = 0;
  102. export const REMOVED = -1;
  103. // inserter is a state updater function for the main component, which inserts the new log chunk into the chunk array.
  104. // limit is the maximum length of the chunk array, used in order to prevent the browser from OOM.
  105. export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType) => {
  106. prev.topChanged = SAME;
  107. prev.bottomChanged = SAME;
  108. if (!Array.isArray(update.chunk) || update.chunk.length < 1) {
  109. return prev;
  110. }
  111. if (!Array.isArray(prev.chunks)) {
  112. prev.chunks = [];
  113. }
  114. const content = createChunk(update.chunk);
  115. if (!update.source) {
  116. // In case of stream chunk.
  117. if (!prev.endBottom) {
  118. return prev;
  119. }
  120. if (prev.chunks.length < 1) {
  121. // This should never happen, because the first chunk is always a non-stream chunk.
  122. return [{content, name: '00000000000000.log'}];
  123. }
  124. prev.chunks[prev.chunks.length - 1].content += content;
  125. prev.bottomChanged = ADDED;
  126. return prev;
  127. }
  128. const chunk = {
  129. content,
  130. name: update.source.name,
  131. };
  132. if (prev.chunks.length > 0 && update.source.name < prev.chunks[0].name) {
  133. if (update.source.last) {
  134. prev.endTop = true;
  135. }
  136. if (prev.chunks.length >= limit) {
  137. prev.endBottom = false;
  138. prev.chunks.splice(limit - 1, prev.chunks.length - limit + 1);
  139. prev.bottomChanged = REMOVED;
  140. }
  141. prev.chunks = [chunk, ...prev.chunks];
  142. prev.topChanged = ADDED;
  143. return prev;
  144. }
  145. if (update.source.last) {
  146. prev.endBottom = true;
  147. }
  148. if (prev.chunks.length >= limit) {
  149. prev.endTop = false;
  150. prev.chunks.splice(0, prev.chunks.length - limit + 1);
  151. prev.topChanged = REMOVED;
  152. }
  153. prev.chunks = [...prev.chunks, chunk];
  154. prev.bottomChanged = ADDED;
  155. return prev;
  156. };
  157. // styles contains the constant styles of the component.
  158. const styles = {
  159. logListItem: {
  160. padding: 0,
  161. lineHeight: 1.231,
  162. },
  163. logChunk: {
  164. color: 'white',
  165. fontFamily: 'monospace',
  166. whiteSpace: 'nowrap',
  167. width: 0,
  168. },
  169. waitMsg: {
  170. textAlign: 'center',
  171. color: 'white',
  172. fontFamily: 'monospace',
  173. },
  174. };
  175. export type Props = {
  176. container: Object,
  177. content: Content,
  178. shouldUpdate: Object,
  179. send: string => void,
  180. };
  181. type State = {
  182. requestAllowed: boolean,
  183. };
  184. // Logs renders the log page.
  185. class Logs extends Component<Props, State> {
  186. constructor(props: Props) {
  187. super(props);
  188. this.content = React.createRef();
  189. this.state = {
  190. requestAllowed: true,
  191. };
  192. }
  193. componentDidMount() {
  194. const {container} = this.props;
  195. if (typeof container === 'undefined') {
  196. return;
  197. }
  198. container.scrollTop = container.scrollHeight - container.clientHeight;
  199. const {logs} = this.props.content;
  200. if (typeof this.content === 'undefined' || logs.chunks.length < 1) {
  201. return;
  202. }
  203. if (this.content.clientHeight < container.clientHeight && !logs.endTop) {
  204. this.sendRequest(logs.chunks[0].name, true);
  205. }
  206. }
  207. // onScroll is triggered by the parent component's scroll event, and sends requests if the scroll position is
  208. // at the top or at the bottom.
  209. onScroll = () => {
  210. if (!this.state.requestAllowed || typeof this.content === 'undefined') {
  211. return;
  212. }
  213. const {logs} = this.props.content;
  214. if (logs.chunks.length < 1) {
  215. return;
  216. }
  217. if (this.atTop() && !logs.endTop) {
  218. this.sendRequest(logs.chunks[0].name, true);
  219. } else if (this.atBottom() && !logs.endBottom) {
  220. this.sendRequest(logs.chunks[logs.chunks.length - 1].name, false);
  221. }
  222. };
  223. sendRequest = (name: string, past: boolean) => {
  224. this.setState({requestAllowed: false});
  225. this.props.send(JSON.stringify({
  226. Logs: {
  227. Name: name,
  228. Past: past,
  229. },
  230. }));
  231. };
  232. // atTop checks if the scroll position it at the top of the container.
  233. atTop = () => this.props.container.scrollTop <= this.props.container.scrollHeight * requestBand;
  234. // atBottom checks if the scroll position it at the bottom of the container.
  235. atBottom = () => {
  236. const {container} = this.props;
  237. return container.scrollHeight - container.scrollTop
  238. <= container.clientHeight + container.scrollHeight * requestBand;
  239. };
  240. // beforeUpdate is called by the parent component, saves the previous scroll position
  241. // and the height of the first log chunk, which can be deleted during the insertion.
  242. beforeUpdate = () => {
  243. let firstHeight = 0;
  244. const chunkList = this.content.children[1];
  245. if (chunkList && chunkList.children[0]) {
  246. firstHeight = chunkList.children[0].clientHeight;
  247. }
  248. return {
  249. scrollTop: this.props.container.scrollTop,
  250. firstHeight,
  251. };
  252. };
  253. // didUpdate is called by the parent component, which provides the container. Sends the first request if the
  254. // visible part of the container isn't full, and resets the scroll position in order to avoid jumping when a
  255. // chunk is inserted or removed.
  256. didUpdate = (prevProps, prevState, snapshot) => {
  257. if (typeof this.props.shouldUpdate.logs === 'undefined' || typeof this.content === 'undefined' || snapshot === null) {
  258. return;
  259. }
  260. const {logs} = this.props.content;
  261. const {container} = this.props;
  262. if (typeof container === 'undefined' || logs.chunks.length < 1) {
  263. return;
  264. }
  265. if (this.content.clientHeight < container.clientHeight) {
  266. // Only enters here at the beginning, when there aren't enough logs to fill the container
  267. // and the scroll bar doesn't appear.
  268. if (!logs.endTop) {
  269. this.sendRequest(logs.chunks[0].name, true);
  270. }
  271. return;
  272. }
  273. let {scrollTop} = snapshot;
  274. if (logs.topChanged === ADDED) {
  275. // It would be safer to use a ref to the list, but ref doesn't work well with HOCs.
  276. scrollTop += this.content.children[1].children[0].clientHeight;
  277. } else if (logs.bottomChanged === ADDED) {
  278. if (logs.topChanged === REMOVED) {
  279. scrollTop -= snapshot.firstHeight;
  280. } else if (this.atBottom() && logs.endBottom) {
  281. scrollTop = container.scrollHeight - container.clientHeight;
  282. }
  283. }
  284. container.scrollTop = scrollTop;
  285. this.setState({requestAllowed: true});
  286. };
  287. render() {
  288. return (
  289. <div ref={(ref) => { this.content = ref; }}>
  290. <div style={styles.waitMsg}>
  291. {this.props.content.logs.endTop ? 'No more logs.' : 'Waiting for server...'}
  292. </div>
  293. <List>
  294. {this.props.content.logs.chunks.map((c, index) => (
  295. <ListItem style={styles.logListItem} key={index}>
  296. <div style={styles.logChunk} dangerouslySetInnerHTML={{__html: c.content}} />
  297. </ListItem>
  298. ))}
  299. </List>
  300. {this.props.content.logs.endBottom || <div style={styles.waitMsg}>Waiting for server...</div>}
  301. </div>
  302. );
  303. }
  304. }
  305. export default Logs;