github.com/hernad/nomad@v1.6.112/ui/app/components/streaming-file.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 import Component from '@ember/component'; 7 import { scheduleOnce, once } from '@ember/runloop'; 8 import { task } from 'ember-concurrency'; 9 import WindowResizable from 'nomad-ui/mixins/window-resizable'; 10 import { 11 classNames, 12 tagName, 13 attributeBindings, 14 } from '@ember-decorators/component'; 15 import classic from 'ember-classic-decorator'; 16 17 const A_KEY = 65; 18 19 @classic 20 @tagName('pre') 21 @classNames('cli-window') 22 @attributeBindings('data-test-log-cli') 23 export default class StreamingFile extends Component.extend(WindowResizable) { 24 'data-test-log-cli' = true; 25 26 mode = 'streaming'; // head, tail, streaming 27 isStreaming = true; 28 logger = null; 29 follow = true; 30 shouldFillHeight = true; 31 32 // Internal bookkeeping to avoid multiple scroll events on one frame 33 requestFrame = true; 34 35 didReceiveAttrs() { 36 super.didReceiveAttrs(); 37 if (!this.logger) { 38 return; 39 } 40 41 scheduleOnce('actions', this, this.performTask); 42 } 43 44 performTask() { 45 switch (this.mode) { 46 case 'head': 47 this.set('follow', false); 48 this.head.perform(); 49 break; 50 case 'tail': 51 this.set('follow', true); 52 this.tail.perform(); 53 break; 54 case 'streaming': 55 this.set('follow', true); 56 if (this.isStreaming) { 57 this.stream.perform(); 58 } else { 59 this.logger.stop(); 60 } 61 break; 62 } 63 } 64 65 scrollHandler() { 66 const cli = this.element; 67 68 // Scroll events can fire multiple times per frame, this eliminates 69 // redundant computation. 70 if (this.requestFrame) { 71 window.requestAnimationFrame(() => { 72 // If the scroll position is close enough to the bottom, autoscroll to the bottom 73 this.set( 74 'follow', 75 cli.scrollHeight - cli.scrollTop - cli.clientHeight < 20 76 ); 77 this.requestFrame = true; 78 }); 79 } 80 this.requestFrame = false; 81 } 82 83 keyDownHandler(e) { 84 // Rebind select-all shortcut to only select the text in the 85 // streaming file output. 86 if ((e.metaKey || e.ctrlKey) && e.keyCode === A_KEY) { 87 e.preventDefault(); 88 const selection = window.getSelection(); 89 selection.removeAllRanges(); 90 const range = document.createRange(); 91 range.selectNode(this.element); 92 selection.addRange(range); 93 } 94 } 95 96 didInsertElement() { 97 super.didInsertElement(...arguments); 98 if (this.shouldFillHeight) { 99 this.fillAvailableHeight(); 100 } 101 102 this.set('_scrollHandler', this.scrollHandler.bind(this)); 103 this.element.addEventListener('scroll', this._scrollHandler); 104 105 this.set('_keyDownHandler', this.keyDownHandler.bind(this)); 106 document.addEventListener('keydown', this._keyDownHandler); 107 } 108 109 willDestroyElement() { 110 super.willDestroyElement(...arguments); 111 this.element.removeEventListener('scroll', this._scrollHandler); 112 document.removeEventListener('keydown', this._keyDownHandler); 113 } 114 115 windowResizeHandler() { 116 if (this.shouldFillHeight) { 117 once(this, this.fillAvailableHeight); 118 } 119 } 120 121 fillAvailableHeight() { 122 // This math is arbitrary and far from bulletproof, but the UX 123 // of having the log window fill available height is worth the hack. 124 const margins = 30; // Account for padding and margin on either side of the CLI 125 const cliWindow = this.element; 126 cliWindow.style.height = `${ 127 window.innerHeight - cliWindow.offsetTop - margins 128 }px`; 129 } 130 131 @task(function* () { 132 yield this.get('logger.gotoHead').perform(); 133 scheduleOnce('afterRender', this, this.scrollToTop); 134 }) 135 head; 136 137 scrollToTop() { 138 this.element.scrollTop = 0; 139 } 140 141 @task(function* () { 142 yield this.get('logger.gotoTail').perform(); 143 }) 144 tail; 145 146 synchronizeScrollPosition() { 147 if (this.follow) { 148 this.element.scrollTop = this.element.scrollHeight; 149 } 150 } 151 152 @task(function* () { 153 // Follow the log if the scroll position is near the bottom of the cli window 154 this.logger.on('tick', this, 'scheduleScrollSynchronization'); 155 156 yield this.logger.startStreaming(); 157 this.logger.off('tick', this, 'scheduleScrollSynchronization'); 158 }) 159 stream; 160 161 scheduleScrollSynchronization() { 162 scheduleOnce('afterRender', this, this.synchronizeScrollPosition); 163 } 164 165 willDestroy() { 166 super.willDestroy(...arguments); 167 this.logger.stop(); 168 } 169 }