170 lines
4.3 KiB
JavaScript
170 lines
4.3 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
*/
|
|
|
|
import Component from '@ember/component';
|
|
import { scheduleOnce, once } from '@ember/runloop';
|
|
import { task } from 'ember-concurrency';
|
|
import WindowResizable from 'nomad-ui/mixins/window-resizable';
|
|
import {
|
|
classNames,
|
|
tagName,
|
|
attributeBindings,
|
|
} from '@ember-decorators/component';
|
|
import classic from 'ember-classic-decorator';
|
|
|
|
const A_KEY = 65;
|
|
|
|
@classic
|
|
@tagName('pre')
|
|
@classNames('cli-window')
|
|
@attributeBindings('data-test-log-cli')
|
|
export default class StreamingFile extends Component.extend(WindowResizable) {
|
|
'data-test-log-cli' = true;
|
|
|
|
mode = 'streaming'; // head, tail, streaming
|
|
isStreaming = true;
|
|
logger = null;
|
|
follow = true;
|
|
shouldFillHeight = true;
|
|
|
|
// Internal bookkeeping to avoid multiple scroll events on one frame
|
|
requestFrame = true;
|
|
|
|
didReceiveAttrs() {
|
|
super.didReceiveAttrs();
|
|
if (!this.logger) {
|
|
return;
|
|
}
|
|
|
|
scheduleOnce('actions', this, this.performTask);
|
|
}
|
|
|
|
performTask() {
|
|
switch (this.mode) {
|
|
case 'head':
|
|
this.set('follow', false);
|
|
this.head.perform();
|
|
break;
|
|
case 'tail':
|
|
this.set('follow', true);
|
|
this.tail.perform();
|
|
break;
|
|
case 'streaming':
|
|
this.set('follow', true);
|
|
if (this.isStreaming) {
|
|
this.stream.perform();
|
|
} else {
|
|
this.logger.stop();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
scrollHandler() {
|
|
const cli = this.element;
|
|
|
|
// Scroll events can fire multiple times per frame, this eliminates
|
|
// redundant computation.
|
|
if (this.requestFrame) {
|
|
window.requestAnimationFrame(() => {
|
|
// If the scroll position is close enough to the bottom, autoscroll to the bottom
|
|
this.set(
|
|
'follow',
|
|
cli.scrollHeight - cli.scrollTop - cli.clientHeight < 20
|
|
);
|
|
this.requestFrame = true;
|
|
});
|
|
}
|
|
this.requestFrame = false;
|
|
}
|
|
|
|
keyDownHandler(e) {
|
|
// Rebind select-all shortcut to only select the text in the
|
|
// streaming file output.
|
|
if ((e.metaKey || e.ctrlKey) && e.keyCode === A_KEY) {
|
|
e.preventDefault();
|
|
const selection = window.getSelection();
|
|
selection.removeAllRanges();
|
|
const range = document.createRange();
|
|
range.selectNode(this.element);
|
|
selection.addRange(range);
|
|
}
|
|
}
|
|
|
|
didInsertElement() {
|
|
super.didInsertElement(...arguments);
|
|
if (this.shouldFillHeight) {
|
|
this.fillAvailableHeight();
|
|
}
|
|
|
|
this.set('_scrollHandler', this.scrollHandler.bind(this));
|
|
this.element.addEventListener('scroll', this._scrollHandler);
|
|
|
|
this.set('_keyDownHandler', this.keyDownHandler.bind(this));
|
|
document.addEventListener('keydown', this._keyDownHandler);
|
|
}
|
|
|
|
willDestroyElement() {
|
|
super.willDestroyElement(...arguments);
|
|
this.element.removeEventListener('scroll', this._scrollHandler);
|
|
document.removeEventListener('keydown', this._keyDownHandler);
|
|
}
|
|
|
|
windowResizeHandler() {
|
|
if (this.shouldFillHeight) {
|
|
once(this, this.fillAvailableHeight);
|
|
}
|
|
}
|
|
|
|
fillAvailableHeight() {
|
|
// This math is arbitrary and far from bulletproof, but the UX
|
|
// of having the log window fill available height is worth the hack.
|
|
const margins = 30; // Account for padding and margin on either side of the CLI
|
|
const cliWindow = this.element;
|
|
cliWindow.style.height = `${
|
|
window.innerHeight - cliWindow.offsetTop - margins
|
|
}px`;
|
|
}
|
|
|
|
@task(function* () {
|
|
yield this.get('logger.gotoHead').perform();
|
|
scheduleOnce('afterRender', this, this.scrollToTop);
|
|
})
|
|
head;
|
|
|
|
scrollToTop() {
|
|
this.element.scrollTop = 0;
|
|
}
|
|
|
|
@task(function* () {
|
|
yield this.get('logger.gotoTail').perform();
|
|
})
|
|
tail;
|
|
|
|
synchronizeScrollPosition() {
|
|
if (this.follow) {
|
|
this.element.scrollTop = this.element.scrollHeight;
|
|
}
|
|
}
|
|
|
|
@task(function* () {
|
|
// Follow the log if the scroll position is near the bottom of the cli window
|
|
this.logger.on('tick', this, 'scheduleScrollSynchronization');
|
|
|
|
yield this.logger.startStreaming();
|
|
this.logger.off('tick', this, 'scheduleScrollSynchronization');
|
|
})
|
|
stream;
|
|
|
|
scheduleScrollSynchronization() {
|
|
scheduleOnce('afterRender', this, this.synchronizeScrollPosition);
|
|
}
|
|
|
|
willDestroy() {
|
|
super.willDestroy(...arguments);
|
|
this.logger.stop();
|
|
}
|
|
}
|