github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/app/components/streaming-file.js (about)

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