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