Commit 621a2c2d authored by Phil Hughes's avatar Phil Hughes

Merge branch '5426-fe-web-terminal-controls-ce' into 'master'

CE Port of "Web Terminal FE"

See merge request gitlab-org/gitlab-ce!23626
parents 498e34c6 38431c8f
...@@ -7,3 +7,8 @@ export const addClassIfElementExists = (element, className) => { ...@@ -7,3 +7,8 @@ export const addClassIfElementExists = (element, className) => {
}; };
export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isInMRPage(); export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isInMRPage();
export const canScrollUp = ({ scrollTop }, margin = 0) => scrollTop > margin;
export const canScrollDown = ({ scrollTop, offsetHeight, scrollHeight }, margin = 0) =>
scrollTop + offsetHeight < scrollHeight - margin;
import Terminal from './terminal'; import Terminal from './terminal';
export default () => new Terminal({ selector: '#terminal' }); export default () => new Terminal(document.getElementById('terminal'));
import _ from 'underscore';
import $ from 'jquery'; import $ from 'jquery';
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit'; import * as fit from 'xterm/lib/addons/fit/fit';
import { canScrollUp, canScrollDown } from '~/lib/utils/dom_utils';
const SCROLL_MARGIN = 5;
Terminal.applyAddon(fit);
export default class GLTerminal { export default class GLTerminal {
constructor(options = {}) { constructor(element, options = {}) {
this.options = Object.assign( this.options = Object.assign(
{}, {},
{ {
...@@ -13,7 +19,8 @@ export default class GLTerminal { ...@@ -13,7 +19,8 @@ export default class GLTerminal {
options, options,
); );
this.container = document.querySelector(options.selector); this.container = element;
this.onDispose = [];
this.setSocketUrl(); this.setSocketUrl();
this.createTerminal(); this.createTerminal();
...@@ -34,8 +41,6 @@ export default class GLTerminal { ...@@ -34,8 +41,6 @@ export default class GLTerminal {
} }
createTerminal() { createTerminal() {
Terminal.applyAddon(fit);
this.terminal = new Terminal(this.options); this.terminal = new Terminal(this.options);
this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']); this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']);
...@@ -72,4 +77,48 @@ export default class GLTerminal { ...@@ -72,4 +77,48 @@ export default class GLTerminal {
handleSocketFailure() { handleSocketFailure() {
this.terminal.write('\r\nConnection failure'); this.terminal.write('\r\nConnection failure');
} }
addScrollListener(onScrollLimit) {
const viewport = this.container.querySelector('.xterm-viewport');
const listener = _.throttle(() => {
onScrollLimit({
canScrollUp: canScrollUp(viewport, SCROLL_MARGIN),
canScrollDown: canScrollDown(viewport, SCROLL_MARGIN),
});
});
this.onDispose.push(() => viewport.removeEventListener('scroll', listener));
viewport.addEventListener('scroll', listener);
// don't forget to initialize value before scroll!
listener({ target: viewport });
}
disable() {
this.terminal.setOption('cursorBlink', false);
this.terminal.setOption('theme', { foreground: '#707070' });
this.terminal.setOption('disableStdin', true);
this.socket.close();
}
dispose() {
this.terminal.off('data');
this.terminal.dispose();
this.socket.close();
this.onDispose.forEach(fn => fn());
this.onDispose.length = 0;
}
scrollToTop() {
this.terminal.scrollToTop();
}
scrollToBottom() {
this.terminal.scrollToBottom();
}
fit() {
this.terminal.fit();
}
} }
...@@ -386,3 +386,4 @@ img.emoji { ...@@ -386,3 +386,4 @@ img.emoji {
.flex-no-shrink { flex-shrink: 0; } .flex-no-shrink { flex-shrink: 0; }
.mw-460 { max-width: 460px; } .mw-460 { max-width: 460px; }
.ws-initial { white-space: initial; } .ws-initial { white-space: initial; }
.min-height-0 { min-height: 0; }
@mixin ide-trace-view {
display: flex;
flex-direction: column;
height: 100%;
margin-top: -$grid-size;
margin-bottom: -$grid-size;
&.build-page .top-bar {
top: 0;
height: auto;
font-size: 12px;
border-top-right-radius: $border-radius-default;
}
.top-bar {
margin-left: -$gl-padding;
}
}
@import 'framework/variables'; @import 'framework/variables';
@import 'framework/mixins'; @import 'framework/mixins';
@import './ide_mixins';
$search-list-icon-width: 18px; $search-list-icon-width: 18px;
$ide-activity-bar-width: 60px; $ide-activity-bar-width: 60px;
...@@ -1111,11 +1112,7 @@ $ide-commit-header-height: 48px; ...@@ -1111,11 +1112,7 @@ $ide-commit-header-height: 48px;
} }
.ide-pipeline { .ide-pipeline {
display: flex; @include ide-trace-view();
flex-direction: column;
height: 100%;
margin-top: -$grid-size;
margin-bottom: -$grid-size;
.empty-state { .empty-state {
margin-top: auto; margin-top: auto;
...@@ -1133,17 +1130,9 @@ $ide-commit-header-height: 48px; ...@@ -1133,17 +1130,9 @@ $ide-commit-header-height: 48px;
} }
} }
.build-trace, .build-trace {
.top-bar {
margin-left: -$gl-padding; margin-left: -$gl-padding;
} }
&.build-page .top-bar {
top: 0;
height: auto;
font-size: 12px;
border-top-right-radius: $border-radius-default;
}
} }
.ide-pipeline-list { .ide-pipeline-list {
......
import { addClassIfElementExists } from '~/lib/utils/dom_utils'; import { addClassIfElementExists, canScrollUp, canScrollDown } from '~/lib/utils/dom_utils';
const TEST_MARGIN = 5;
describe('DOM Utils', () => { describe('DOM Utils', () => {
describe('addClassIfElementExists', () => { describe('addClassIfElementExists', () => {
...@@ -34,4 +36,54 @@ describe('DOM Utils', () => { ...@@ -34,4 +36,54 @@ describe('DOM Utils', () => {
addClassIfElementExists(childElement, className); addClassIfElementExists(childElement, className);
}); });
}); });
describe('canScrollUp', () => {
[1, 100].forEach(scrollTop => {
it(`is true if scrollTop is > 0 (${scrollTop})`, () => {
expect(canScrollUp({ scrollTop })).toBe(true);
});
});
[0, -10].forEach(scrollTop => {
it(`is false if scrollTop is <= 0 (${scrollTop})`, () => {
expect(canScrollUp({ scrollTop })).toBe(false);
});
});
it('is true if scrollTop is > margin', () => {
expect(canScrollUp({ scrollTop: TEST_MARGIN + 1 }, TEST_MARGIN)).toBe(true);
});
it('is false if scrollTop is <= margin', () => {
expect(canScrollUp({ scrollTop: TEST_MARGIN }, TEST_MARGIN)).toBe(false);
});
});
describe('canScrollDown', () => {
let element;
beforeEach(() => {
element = { scrollTop: 7, offsetHeight: 22, scrollHeight: 30 };
});
it('is true if element can be scrolled down', () => {
expect(canScrollDown(element)).toBe(true);
});
it('is false if element cannot be scrolled down', () => {
element.scrollHeight -= 1;
expect(canScrollDown(element)).toBe(false);
});
it('is true if element can be scrolled down, with margin given', () => {
element.scrollHeight += TEST_MARGIN;
expect(canScrollDown(element, TEST_MARGIN)).toBe(true);
});
it('is false if element cannot be scrolled down, with margin given', () => {
expect(canScrollDown(element, TEST_MARGIN)).toBe(false);
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment