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  }