github.com/v2pro/plz@v0.0.0-20221028024117-e5f9aec5b631/witch/webroot/log-viewer.html (about)

     1  <script type="text/x-template" id="log-viewer-template">
     2      <div>
     3          <el-row>
     4              <el-col :span="20">
     5                  <el-button @click="scrollUp" :loading="loading" :disabled="autoLoad">&nbsp;&nbsp;&nbsp;Load More&nbsp;&nbsp;&nbsp;<i
     6                          class="el-icon-arrow-up el-icon--right"></i></el-button>
     7                  <el-switch
     8                          v-model="autoLoad"
     9                          active-text="Auto Load">
    10                  </el-switch>
    11                  <span style="float: right;">downloaded {{ events.length }} events, and loaded {{ viewerData.length }} into the viewer</span>
    12                  <el-table
    13                          style="margin-top: 8px; margin-bottom: 8px; width: 100%;"
    14                          highlight-current-row
    15                          ref="viewer"
    16                          :data="viewerData"
    17                          :row-key="getRowKey"
    18                          @row-click="onRowClick">
    19                      <el-table-column type="expand">
    20                          <template slot-scope="tbl">
    21                              <div @dblclick="onRowDbClick(tbl.row)"><span v-for="kv in formatKV(tbl.row)"
    22                                                                           class="event-kv">{{ kv }}</span></div>
    23                          </template>
    24                      </el-table-column>
    25                      <el-table-column label="timestamp" :formatter="formatTimestamp" width="140" v-if="showTimestamp"/>
    26                      <el-table-column label="level" width="140" v-if="showLevel">
    27                          <template slot-scope="tbl">
    28                              <span :class="'level level-' + tbl.row.level">{{ tbl.row.level }}</span>
    29                          </template>
    30                      </el-table-column>
    31                      <el-table-column v-for="item in userDefinedColumns" :prop="item" :key="item" :label="item"
    32                                       :formatter="formatUserDefinedColumn"
    33                                       resizable/>
    34                      <el-table-column label="event" prop="event">
    35                          <template slot-scope="scope">
    36                              <span class="event-cell" :id="'row-' + scope.row.id">{{ scope.row.event }}</span>
    37                          </template>
    38                      </el-table-column>
    39                  </el-table>
    40                  <el-button @click="scrollDown" :loading="loading" :disabled="autoLoad">&nbsp;&nbsp;&nbsp;Load More&nbsp;&nbsp;&nbsp;<i
    41                          class="el-icon-arrow-down el-icon--right"></i></el-button>
    42              </el-col>
    43              <el-col :span="4" style="pdding-right: 1em; padding-left: 1em;">
    44                  <data-sources :dataSource.sync="dataSource"/>
    45                  <columns ref="columns" :namespace="dataSource"
    46                           :columns.sync="userDefinedColumns"
    47                           :showTimestamp.sync="showTimestamp"
    48                           :showLevel.sync="showLevel"
    49                           :propKeys="propKeys" @expandAll="expandAll"/>
    50                  <filters ref="filters" :propValues="propValues" :filters.sync="filters"/>
    51              </el-col>
    52          </el-row>
    53          <el-dialog
    54                  title="Event Details"
    55                  :visible.sync="showDetails"
    56                  width="70%">
    57              <div v-if="currentEvent" style="word-wrap: break-word;">
    58                  <el-button style="margin-bottom: 8px;" @click="showEventsNearby">
    59                      Show events nearby regardless of the whitelist filters, within range of
    60                  </el-button>
    61                  <el-input-number v-model="nearSeconds" :min="1" :max="10" label="seconds"></el-input-number>
    62                  Seconds
    63                  <div v-for="(propValue, propKey) in currentEvent" v-if="propKey.indexOf('__') !== 0">
    64                      <div>
    65                          <i class="el-icon-zoom-in" style="cursor: pointer"
    66                             @click="addFilter(propKey, 'equals', propValue)"></i>
    67                          <i class="el-icon-zoom-out" style="cursor: pointer"
    68                             @click="addFilter(propKey, 'not-equals', propValue)"></i>
    69                          <b>{{ propKey }}</b>
    70                          <svg class="icon" aria-hidden="true" style="cursor: pointer" @click="copyPropertyValue">
    71                              <use xlink:href="#icon-copy"></use>
    72                          </svg>
    73                      </div>
    74                      <pre style="white-space: pre-wrap;word-wrap: break-word;">{{ propValue }}</pre>
    75                      <hr/>
    76                  </div>
    77              </div>
    78          </el-dialog>
    79      </div>
    80  </script>
    81  <style>
    82      .level-FATAL {
    83          background-color: red;
    84      }
    85  
    86      .level-ERROR {
    87          background-color: #ff9999;
    88      }
    89  
    90      .level-WARN {
    91          background-color: #ffc8c8;
    92      }
    93  
    94      .event-kv {
    95          margin-right: 1em;
    96          padding: 4px;
    97          color: #ccc;
    98          word-wrap: break-word;
    99          line-height: 2em;
   100      }
   101  
   102      tr:hover .event-kv {
   103          background-color: #eee;
   104          color: #000;
   105      }
   106  
   107      .event-cell {
   108          padding: 4px;
   109          background-color: #eee;
   110      }
   111  
   112      .el-table__expanded-cell[class*=cell] {
   113          padding: 4px 0;
   114      }
   115  
   116      .el-table td, .el-table th {
   117          padding: 2px 0;
   118      }
   119  </style>
   120  <script>
   121      var MAXIMUM_EVENTS_LENGTH = 10000 * 8;
   122      Vue.component('log-viewer', {
   123          template: '#log-viewer-template',
   124          data: function () {
   125              return {
   126                  /* controls the ui layout */
   127                  userDefinedColumns: [],
   128                  hiddenKeys: {
   129                      'level': true,
   130                      '__filtered_by_0': true,
   131                      'timestamp': true,
   132                      'lineNumber': true,
   133                      'event': true,
   134                      'id': true
   135                  },
   136                  showTimestamp: false,
   137                  showLevel: false,
   138                  showDetails: false,
   139                  currentEvent: null,
   140                  /* how to fill the events */
   141                  dataSource: window.location.href,
   142                  propValues: {}, // property dictionary, only keep last 100 unique values
   143                  events: [], // the backing data store, only events in the window range is filled into viewer
   144                  /* how to fill the viewer data */
   145                  autoLoad: true,
   146                  autoLoadDisabledManually: false,
   147                  nearSeconds: 1, // used when add nearby events
   148                  filters: {
   149                      cacheKey: '__filtered_by_0',
   150                      filterFunc: function (event) {
   151                          return true;
   152                      }
   153                  },
   154                  viewerData: [], // the actual viewer data, will be refilled to match offset/window
   155                  viewerOffset: 0, // [viewerOffset, viewerOffset+viewerWindow) defines the expected viewer data
   156                  viewerWindow: 200,
   157                  loading: false,
   158                  eventIds: {}
   159              }
   160          },
   161          watch: {
   162              dataSource: function () {
   163                  this.events = [];
   164                  this.propValues = [];
   165                  this.userDefinedColumns = [];
   166              },
   167              filters: function () {
   168                  this.updateViewerData();
   169              },
   170              autoLoad: function () {
   171                  if (this.autoLoad) {
   172                      this.autoLoadDisabledManually = false;
   173                      this.updateViewerData();
   174                  } else {
   175                      if (window.scrollY === 0) {
   176                          this.autoLoadDisabledManually = true;
   177                      }
   178                  }
   179              }
   180          },
   181          methods: {
   182              assignId: function (event) {
   183                  if (this.eventIds[event.timestamp]) {
   184                      while (true) {
   185                          var id = event.timestamp + '-' + Math.floor(Math.random() * 100000);
   186                          if (this.eventIds[id]) {
   187                              continue;
   188                          }
   189                          event.id = id;
   190                          break;
   191                      }
   192                  } else {
   193                      event.id = event.timestamp;
   194                  }
   195                  this.eventIds[event.id] = true;
   196              },
   197              onRowClick: function (row, e, column) {
   198                  if (column.type !== 'expand') {
   199                      this.$refs.viewer.toggleRowExpansion(row);
   200                  }
   201              },
   202              onRowDbClick: function (row) {
   203                  this.currentEvent = row;
   204                  this.showDetails = true;
   205              },
   206              getRowKey: function (row) {
   207                  return row.id;
   208              },
   209              formatTimestamp: function (row, column, cellValue) {
   210                  var d = new Date(row.timestamp / 1000000);
   211                  return d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds() + '.' + d.getMilliseconds();
   212              },
   213              formatUserDefinedColumn: function (row, column, cellValue) {
   214                  if (cellValue) {
   215                      if (cellValue.length > 512) {
   216                          return cellValue.substr(0, 512) + '...capped';
   217                      } else {
   218                          return cellValue;
   219                      }
   220                  } else {
   221                      return ''
   222                  }
   223              },
   224              formatKV: function (event) {
   225                  var kv = [];
   226                  for (var propKey in event) {
   227                      if (this.hiddenKeys[propKey]) {
   228                          continue;
   229                      }
   230                      if (this.userDefinedColumns.indexOf(propKey) !== -1) {
   231                          continue;
   232                      }
   233                      var propValue = event[propKey];
   234                      if (propValue.length > 512) {
   235                          kv.push(propKey + '=' + propValue.substr(0, 512) + '...capped');
   236                      } else {
   237                          kv.push(propKey + '=' + propValue);
   238                      }
   239                  }
   240                  return kv;
   241              },
   242              shouldShow: function (event, cacheKey, filterFunc) {
   243                  var cacheValue = event[cacheKey];
   244                  if (cacheValue !== undefined) {
   245                      return cacheValue;
   246                  }
   247                  if (filterFunc(event)) {
   248                      event[cacheKey] = true;
   249                      return true;
   250                  }
   251                  event[cacheKey] = false;
   252                  return false;
   253              },
   254              expandAll: function (e) {
   255                  this.$refs.viewer.$data.store.states.defaultExpandAll = e;
   256                  for (var i in this.viewerData) {
   257                      this.$refs.viewer.toggleRowExpansion(this.viewerData[i], e);
   258                  }
   259              },
   260              handleScroll: function () {
   261                  if (window.scrollY > 0) {
   262                      if (this.autoLoad) {
   263                          this.autoLoad = false;
   264                      }
   265                  } else {
   266                      if (!this.autoLoad && !this.autoLoadDisabledManually) {
   267                          this.autoLoad = true;
   268                      }
   269                  }
   270              },
   271              updateViewerData: function () {
   272                  var filtered = [];
   273                  var cacheKey = this.filters['cacheKey'];
   274                  var filterFunc = this.filters['filterFunc'];
   275                  this.hiddenKeys[cacheKey] = true;
   276                  if (this.autoLoad) {
   277                      this.viewerOffset = this.events.length - 1;
   278                  }
   279                  var i = this.viewerOffset;
   280                  for (; i >= 0; i--) {
   281                      var event = this.events[i];
   282                      if (!this.shouldShow(event, cacheKey, filterFunc)) {
   283                          continue;
   284                      }
   285                      filtered.push(event);
   286                      if (filtered.length === this.viewerWindow) {
   287                          break;
   288                      }
   289                  }
   290                  this.viewerData = filtered;
   291              },
   292              scrollUp: function () {
   293                  var originalTarget = null;
   294                  if (this.viewerData.length > 0) {
   295                      originalTarget = this.viewerData[0];
   296                  }
   297                  this.viewerOffset += (this.viewerWindow / 2);
   298                  var limit = this.events.length - 1;
   299                  if (this.viewerOffset >= limit) {
   300                      this.viewerOffset = limit;
   301                  }
   302                  this.updateViewerData();
   303                  this.scrollToTarget(originalTarget);
   304              },
   305              scrollDown: function () {
   306                  var originalTarget = null;
   307                  if (this.viewerData.length > 0) {
   308                      originalTarget = this.viewerData[this.viewerData.length - 1];
   309                  }
   310                  this.viewerOffset -= (this.viewerWindow / 2);
   311                  var limit = this.viewerWindow - 1;
   312                  var eventsCount = this.events.length;
   313                  if (eventsCount - 1 < limit) {
   314                      limit = eventsCount - 1;
   315                  }
   316                  if (this.viewerOffset < limit) {
   317                      this.viewerOffset = limit;
   318                  }
   319                  this.updateViewerData();
   320                  this.scrollToTarget(originalTarget);
   321              },
   322              scrollToTarget: function (originalTarget) {
   323                  var me = this;
   324                  me.loading = true;
   325                  if (!originalTarget) {
   326                      return;
   327                  }
   328                  var targetId = 'row-' + originalTarget.id;
   329                  var ele = document.getElementById(targetId);
   330                  if (!ele) {
   331                      return;
   332                  }
   333                  var originalTop = ele.getBoundingClientRect().top;
   334                  Vue.nextTick(function () {
   335                      var ele = document.getElementById(targetId);
   336                      if (!ele) {
   337                          setTimeout(arguments.callee, 50);
   338                          return;
   339                      }
   340                      me.$refs.viewer.setCurrentRow(originalTarget);
   341                      me.$scrollTo('#' + targetId, 1, {
   342                          offset: -originalTop
   343                      });
   344                      me.loading = false;
   345                  });
   346              },
   347              addFilter: function (propKey, operator, propValue) {
   348                  this.showDetails = false;
   349                  this.$refs.filters.addFilter(propKey, operator, propValue);
   350              },
   351              showEventsNearby: function () {
   352                  this.addFilter('timestamp', 'between', [
   353                      parseInt(this.currentEvent.timestamp) - this.nearSeconds * 1000000000,
   354                      parseInt(this.currentEvent.timestamp) + this.nearSeconds * 1000000000])
   355              },
   356              copyPropertyValue: function (e) {
   357                  var node = e.target.parentElement.parentElement.getElementsByTagName('pre')[0];
   358                  if (node === undefined) {
   359                      return
   360                  }
   361                  var range = document.createRange();
   362                  range.selectNodeContents(node);
   363                  var selection = window.getSelection();
   364                  selection.removeAllRanges();
   365                  selection.addRange(range);
   366                  document.execCommand('copy');
   367              }
   368          },
   369          computed: {
   370              propKeys: function () {
   371                  var keys = ['timestamp'];
   372                  for (var propKey in this.propValues) {
   373                      if (propKey === 'event') {
   374                          continue;
   375                      }
   376                      keys.push(propKey);
   377                  }
   378                  return keys;
   379              }
   380          },
   381          created: function () {
   382              window.addEventListener('scroll', this.handleScroll);
   383              var me = this;
   384              (function () {
   385                  var callback = arguments.callee;
   386                  axios.get(me.dataSource + '/more-events?ts=' + Date.now())
   387                      .then(function (resp) {
   388                          if (!resp || !Array.isArray(resp.data)) {
   389                              setTimeout(callback, 10000);
   390                              return
   391                          }
   392                          for (var i in resp.data) {
   393                              var event = resp.data[i];
   394                              for (var propKey in event) {
   395                                  if (propKey === 'timestamp' || propKey === 'lineNumber') {
   396                                      continue;
   397                                  }
   398                                  var propValue = event[propKey];
   399                                  if (!(propKey in me.propValues)) {
   400                                      Vue.set(me.propValues, propKey, []);
   401                                  } else {
   402                                      var vals = me.propValues[propKey];
   403                                      if (vals.length < 100 && vals.indexOf(propValue) === -1) {
   404                                          vals.push(propValue);
   405                                      }
   406                                  }
   407                              }
   408                              me.assignId(event);
   409                          }
   410                          me.events = me.events.concat(resp.data);
   411                          if (me.events.length > MAXIMUM_EVENTS_LENGTH) {
   412                              me.events = me.events.slice(me.events.length - MAXIMUM_EVENTS_LENGTH)
   413                          }
   414                          if (me.autoLoad) {
   415                              me.updateViewerData();
   416                          }
   417                          setTimeout(callback, 500);
   418                      });
   419              })();
   420          },
   421          destroyed: function () {
   422              window.removeEventListener('scroll', this.handleScroll);
   423          }
   424      });
   425  </script>