github.com/alexdevranger/node-1.8.27@v0.0.0-20221128213301-aa5841e41d2d/dashboard/assets/components/Logs.jsx (about)

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