Logs.jsx 8.9 KB

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