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  }