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"> Load More <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"> Load More <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>