From 7256769437a61350a69846213960ba8cc11b735c Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 13 Nov 2019 14:59:49 -0600 Subject: [PATCH 001/150] Add proxy that properly handles websockets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is adapted from helpful posts in this thread: https://github.com/ember-cli/ember-cli/issues/2508 Without this, the origin header wasn’t being rewritten so I was unable to open a websocket to the API running on a different port. --- ui/.ember-cli | 3 +-- ui/package.json | 3 +++ ui/server/.eslintrc.js | 5 +++++ ui/server/index.js | 23 +++++++++++++++++++++++ ui/server/proxies/api.js | 32 ++++++++++++++++++++++++++++++++ ui/yarn.lock | 35 +++++++++++++++++++++++++++++++++-- 6 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 ui/server/.eslintrc.js create mode 100644 ui/server/index.js create mode 100644 ui/server/proxies/api.js diff --git a/ui/.ember-cli b/ui/.ember-cli index dec208fd9dd..ee64cfed2a8 100644 --- a/ui/.ember-cli +++ b/ui/.ember-cli @@ -5,6 +5,5 @@ Setting `disableAnalytics` to true will prevent any data from being sent. */ - "disableAnalytics": false, - "proxy": "http://127.0.0.1:4646" + "disableAnalytics": false } diff --git a/ui/package.json b/ui/package.json index 48c61f5c094..cbf7c633e6e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -96,12 +96,15 @@ "faker": "^4.1.0", "flat": "^4.0.0", "fuse.js": "^3.4.4", + "glob": "^4.0.5", + "http-proxy": "^1.1.6", "husky": "^1.3.1", "is-ip": "^3.1.0", "ivy-codemirror": "^2.1.0", "lint-staged": "^8.1.5", "loader.js": "^4.7.0", "lodash.intersection": "^4.4.0", + "morgan": "^1.3.2", "pretender": "^3.0.1", "prettier": "^1.4.4", "query-string": "^5.0.0", diff --git a/ui/server/.eslintrc.js b/ui/server/.eslintrc.js new file mode 100644 index 00000000000..1147d299f8b --- /dev/null +++ b/ui/server/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + env: { + node: true + } +}; diff --git a/ui/server/index.js b/ui/server/index.js new file mode 100644 index 00000000000..21919cd5d90 --- /dev/null +++ b/ui/server/index.js @@ -0,0 +1,23 @@ +'use strict'; + +// To use it create some files under `mocks/` +// e.g. `server/mocks/ember-hamsters.js` +// +// module.exports = function(app) { +// app.get('/ember-hamsters', function(req, res) { +// res.send('hello'); +// }); +// }; + +module.exports = function(app, options) { + const globSync = require('glob').sync; + const mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); + const proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require); + + // Log proxy requests + const morgan = require('morgan'); + app.use(morgan('dev')); + + mocks.forEach(route => route(app, options)); + proxies.forEach(route => route(app, options)); +}; diff --git a/ui/server/proxies/api.js b/ui/server/proxies/api.js new file mode 100644 index 00000000000..c16b358b063 --- /dev/null +++ b/ui/server/proxies/api.js @@ -0,0 +1,32 @@ +'use strict'; + +const proxyPath = '/v1'; + +module.exports = function(app, options) { + // For options, see: + // https://github.com/nodejitsu/node-http-proxy + + let server = options.httpServer; + let proxy = require('http-proxy').createProxyServer({ + target: 'http://localhost:4646', + ws: true, + changeOrigin: true + }); + + proxy.on('error', function(err, req) { + console.error(err, req.url); + }); + + app.use(proxyPath, function(req, res, next){ + // include root path in proxied request + req.url = proxyPath + req.url; + proxy.web(req, res, { target: 'http://localhost:4646'}); + }); + + server.on('upgrade', function (req, socket, head) { + if (req.url.startsWith('/v1/client/allocation') && req.url.includes('exec?')) { + req.headers.origin = 'http://localhost:4646'; + proxy.ws(req, socket, head, { target: 'http://localhost:4646'}); + } + }); +}; diff --git a/ui/yarn.lock b/ui/yarn.lock index 9ed59c38c68..1ca0a58bdf3 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -2697,7 +2697,7 @@ bower-endpoint-parser@0.2.2: resolved "https://registry.yarnpkg.com/bower-endpoint-parser/-/bower-endpoint-parser-0.2.2.tgz#00b565adbfab6f2d35addde977e97962acbcb3f6" integrity sha1-ALVlrb+rby01rd3pd+l5Yqy8s/Y= -brace-expansion@^1.1.7: +brace-expansion@^1.0.0, brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== @@ -6133,6 +6133,11 @@ eventemitter3@^3.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== +eventemitter3@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" + integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== + events-to-array@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/events-to-array/-/events-to-array-1.1.2.tgz#2d41f563e1fe400ed4962fe1a4d5c6a7539df7f6" @@ -6897,6 +6902,16 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= +glob@^4.0.5: + version "4.5.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" + integrity sha1-xstz0yJsHv7wTePFbQEvAzd+4V8= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "^2.0.1" + once "^1.3.0" + glob@^5.0.10: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" @@ -7306,6 +7321,15 @@ http-parser-js@>=0.4.0: resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.0.tgz#d65edbede84349d0dc30320815a15d39cc3cbbd8" integrity sha512-cZdEF7r4gfRIq7ezX9J0T+kQmJNOub71dWbgAXVHDct80TKP4MCETtZQ31xyv38UwgzkWPYF/Xc0ge55dW9Z9w== +http-proxy@^1.1.6: + version "1.18.0" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" + integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + http-proxy@^1.13.1, http-proxy@^1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" @@ -9197,6 +9221,13 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" +minimatch@^2.0.1: + version "2.0.10" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" + integrity sha1-jQh8OcazjAAbl/ynzm0OHoCvusc= + dependencies: + brace-expansion "^1.0.0" + minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -9280,7 +9311,7 @@ moment-timezone@^0.5.13: resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== -morgan@^1.9.1: +morgan@^1.3.2, morgan@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.1.tgz#0a8d16734a1d9afbc824b99df87e738e58e2da59" integrity sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA== From 11b0da4aa64e0c0bceb3e1ef2f7e6f2025314191 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 13 Nov 2019 15:41:34 -0600 Subject: [PATCH 002/150] Add xterm.js --- ui/ember-cli-build.js | 2 ++ ui/package.json | 3 ++- ui/yarn.lock | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ui/ember-cli-build.js b/ui/ember-cli-build.js index cfa0fb99667..2bc2396b61b 100644 --- a/ui/ember-cli-build.js +++ b/ui/ember-cli-build.js @@ -52,5 +52,7 @@ module.exports = function(defaults) { // please specify an object with the list of modules as keys // along with the exports of each module as its value. + app.import('node_modules/xterm/css/xterm.css'); + return app.toTree(); }; diff --git a/ui/package.json b/ui/package.json index cbf7c633e6e..23781382912 100644 --- a/ui/package.json +++ b/ui/package.json @@ -109,7 +109,8 @@ "prettier": "^1.4.4", "query-string": "^5.0.0", "qunit-dom": "^0.9.0", - "sass": "^1.17.3" + "sass": "^1.17.3", + "xterm": "^4.2.0-vscode1" }, "engines": { "node": "8.* || >= 10.*" diff --git a/ui/yarn.lock b/ui/yarn.lock index 1ca0a58bdf3..d104813ea69 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -12757,6 +12757,11 @@ xtend@~4.0.0: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= +xterm@^4.2.0-vscode1: + version "4.2.0-vscode1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.2.0-vscode1.tgz#dbd67e52536578ea6b772d69a97da1487acf29e3" + integrity sha512-frw5Kprzkko8+G0vp4jOcwTeOpYFbSb2+QShpeQGJ7l3Hg3MTM7kt1G+nLxBPKnkFAmei/JcJTUGJdp/lldAfA== + y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" From a6aed6226d338488162033523674371d8e090a27 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 13 Nov 2019 15:43:31 -0600 Subject: [PATCH 003/150] Add comment about proxy being preliminary --- ui/server/proxies/api.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/server/proxies/api.js b/ui/server/proxies/api.js index c16b358b063..db5ada504c6 100644 --- a/ui/server/proxies/api.js +++ b/ui/server/proxies/api.js @@ -1,5 +1,7 @@ 'use strict'; +// FIXME this is adapted from https://github.com/ember-cli/ember-cli/issues/2508 +// and could use some refinement. const proxyPath = '/v1'; module.exports = function(app, options) { From caccb4bfd1b9c2e8679b44cea3099769d04e3b60 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 13 Nov 2019 16:32:22 -0600 Subject: [PATCH 004/150] Add prototype task-attached exec terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The communication between the socket and xterm.js terminal instance might better be extracted, I’m not sure whether the component integration test even makes sense. I originally tried an acceptance test, but that seemed worse. --- ui/app/components/exec-terminal.js | 21 +++++++ ui/app/router.js | 1 + .../allocations/allocation/task/exec.js | 26 ++++++++ .../allocations/allocation/task/exec.hbs | 4 ++ ui/app/templates/components/exec-terminal.hbs | 3 + ui/app/templates/components/task-subnav.hbs | 1 + .../components/exec-terminal-test.js | 62 +++++++++++++++++++ 7 files changed, 118 insertions(+) create mode 100644 ui/app/components/exec-terminal.js create mode 100644 ui/app/routes/allocations/allocation/task/exec.js create mode 100644 ui/app/templates/allocations/allocation/task/exec.hbs create mode 100644 ui/app/templates/components/exec-terminal.hbs create mode 100644 ui/tests/integration/components/exec-terminal-test.js diff --git a/ui/app/components/exec-terminal.js b/ui/app/components/exec-terminal.js new file mode 100644 index 00000000000..4253b27f9db --- /dev/null +++ b/ui/app/components/exec-terminal.js @@ -0,0 +1,21 @@ +import Component from '@ember/component'; +import { Terminal } from 'xterm'; + +export default Component.extend({ + didInsertElement() { + this.terminal = new Terminal(); + this.terminal.open(this.element.querySelector('.terminal')); + + this.terminal.onKey(e => { + this.socket.send(JSON.stringify({ stdin: { data: btoa(e.key) } })); + }); + + // FIXME this is a hack to provide access in an integration test 🧐 + window.xterm = this.terminal; + + this.socket.onmessage = e => { + const json = JSON.parse(e.data); + this.terminal.write(atob(json.stdout.data)); + }; + }, +}); diff --git a/ui/app/router.js b/ui/app/router.js index d5ade3e838d..04a5f726cd2 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -33,6 +33,7 @@ Router.map(function() { this.route('logs'); this.route('fs-root', { path: '/fs' }); this.route('fs', { path: '/fs/*path' }); + this.route('exec'); }); }); }); diff --git a/ui/app/routes/allocations/allocation/task/exec.js b/ui/app/routes/allocations/allocation/task/exec.js new file mode 100644 index 00000000000..c15cc96193e --- /dev/null +++ b/ui/app/routes/allocations/allocation/task/exec.js @@ -0,0 +1,26 @@ +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; +import notifyError from 'nomad-ui/utils/notify-error'; + +export default Route.extend({ + model() { + const task = this.modelFor('allocations.allocation.task'); + const taskName = task.name; + const allocationId = task.allocation.id; + + // FIXME generalise host + const socket = new WebSocket( + `ws://localhost:4400/v1/client/allocation/${allocationId}/exec?task=${taskName}&tty=true&command=%5B%22%2Fbin%2Fbash%22%5D` + ); + + return { + socket, + task, + }; + }, + + setupController(controller, { socket, task }) { + this._super(...arguments); + controller.setProperties({ socket, task }); + }, +}); diff --git a/ui/app/templates/allocations/allocation/task/exec.hbs b/ui/app/templates/allocations/allocation/task/exec.hbs new file mode 100644 index 00000000000..6b92d63b501 --- /dev/null +++ b/ui/app/templates/allocations/allocation/task/exec.hbs @@ -0,0 +1,4 @@ +{{title "Task " task.name " exec"}} +{{task-subnav task=task}} + +{{exec-terminal socket=socket}} diff --git a/ui/app/templates/components/exec-terminal.hbs b/ui/app/templates/components/exec-terminal.hbs new file mode 100644 index 00000000000..6b60e189b30 --- /dev/null +++ b/ui/app/templates/components/exec-terminal.hbs @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/ui/app/templates/components/task-subnav.hbs b/ui/app/templates/components/task-subnav.hbs index 4872d032e30..bbe9c3ecbe7 100644 --- a/ui/app/templates/components/task-subnav.hbs +++ b/ui/app/templates/components/task-subnav.hbs @@ -3,5 +3,6 @@
  • {{#link-to "allocations.allocation.task.index" task.allocation task activeClass="is-active"}}Overview{{/link-to}}
  • {{#link-to "allocations.allocation.task.logs" task.allocation task activeClass="is-active"}}Logs{{/link-to}}
  • {{#link-to "allocations.allocation.task.fs-root" task.allocation task class=(if filesLinkActive "is-active")}}Files{{/link-to}}
  • +
  • {{#link-to "allocations.allocation.task.exec" task.allocation task activeClass="is-active"}}Exec{{/link-to}}
  • diff --git a/ui/tests/integration/components/exec-terminal-test.js b/ui/tests/integration/components/exec-terminal-test.js new file mode 100644 index 00000000000..718db7d074a --- /dev/null +++ b/ui/tests/integration/components/exec-terminal-test.js @@ -0,0 +1,62 @@ +import { module, skip, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, triggerKeyEvent } from '@ember/test-helpers'; +import { next } from '@ember/runloop'; +import hbs from 'htmlbars-inline-precompile'; +import sinon from 'sinon'; + +module('Integration | Component | exec-terminal', function(hooks) { + setupRenderingTest(hooks); + + test('it renders an incoming message', async function(assert) { + const done = assert.async(); + + const socket = { + mockMessage(message) { + if (this.onmessage) { + this.onmessage({ data: JSON.stringify(message) }); + } + }, + }; + this.set('socket', socket); + + await render(hbs`{{exec-terminal socket=socket}}`); + + window.xterm.onRender(() => { + assert.ok( + window.xterm.buffer + .getLine(0) + .translateToString() + .includes('bash') + ); + done(); + }); + + socket.mockMessage({ + stdout: { + data: btoa('bash'), + }, + }); + }); + + skip('it routes encoded input to the socket', async function(assert) { + const done = assert.async(); + + const socket = { + send: sinon.spy(), + }; + this.set('socket', socket); + + await render(hbs`{{exec-terminal socket=socket}}`); + + await triggerKeyEvent('textarea', 'keydown', '!'); + + // FIXME can’t figure out how to trigger a send + // BUT is this even a sensible layer to test at? + next(() => { + assert.ok(socket.send.calledOnce); + assert.ok(socket.send.calledWith({})); + done(); + }); + }); +}); From 3f9fcb4dbfb3f4f1155f609318107685c4660888 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 13 Nov 2019 16:37:17 -0600 Subject: [PATCH 005/150] Fix lint errors --- ui/app/routes/allocations/allocation/task/exec.js | 2 -- ui/server/proxies/api.js | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/app/routes/allocations/allocation/task/exec.js b/ui/app/routes/allocations/allocation/task/exec.js index c15cc96193e..0a4ce9911e8 100644 --- a/ui/app/routes/allocations/allocation/task/exec.js +++ b/ui/app/routes/allocations/allocation/task/exec.js @@ -1,6 +1,4 @@ import Route from '@ember/routing/route'; -import RSVP from 'rsvp'; -import notifyError from 'nomad-ui/utils/notify-error'; export default Route.extend({ model() { diff --git a/ui/server/proxies/api.js b/ui/server/proxies/api.js index db5ada504c6..b75d1e65ba3 100644 --- a/ui/server/proxies/api.js +++ b/ui/server/proxies/api.js @@ -16,10 +16,11 @@ module.exports = function(app, options) { }); proxy.on('error', function(err, req) { + // eslint-disable-next-line console.error(err, req.url); }); - app.use(proxyPath, function(req, res, next){ + app.use(proxyPath, function(req, res){ // include root path in proxied request req.url = proxyPath + req.url; proxy.web(req, res, { target: 'http://localhost:4646'}); From 9bef4adc4c177f5186f15789d379a5d063f8f818 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 14 Nov 2019 16:42:56 -0600 Subject: [PATCH 006/150] =?UTF-8?q?Fix=20encoding=20to=20handle=20emoji=20?= =?UTF-8?q?=F0=9F=A5=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/app/components/exec-terminal.js | 18 +++++++++++++++--- ui/package.json | 2 ++ .../components/exec-terminal-test.js | 4 ++-- ui/yarn.lock | 10 ++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/ui/app/components/exec-terminal.js b/ui/app/components/exec-terminal.js index 4253b27f9db..aa97217a030 100644 --- a/ui/app/components/exec-terminal.js +++ b/ui/app/components/exec-terminal.js @@ -1,13 +1,15 @@ import Component from '@ember/component'; import { Terminal } from 'xterm'; +import base64js from 'base64-js'; +import { TextDecoderLite, TextEncoderLite } from 'text-encoder-lite'; export default Component.extend({ didInsertElement() { this.terminal = new Terminal(); this.terminal.open(this.element.querySelector('.terminal')); - this.terminal.onKey(e => { - this.socket.send(JSON.stringify({ stdin: { data: btoa(e.key) } })); + this.terminal.onData(e => { + this.socket.send(JSON.stringify({ stdin: { data: encodeString(e) } })); }); // FIXME this is a hack to provide access in an integration test 🧐 @@ -15,7 +17,17 @@ export default Component.extend({ this.socket.onmessage = e => { const json = JSON.parse(e.data); - this.terminal.write(atob(json.stdout.data)); + this.terminal.write(decodeString(json.stdout.data)); }; }, }); + +function encodeString(string) { + var encoded = new TextEncoderLite('utf-8').encode(string); + return base64js.fromByteArray(encoded); +} + +function decodeString(b64String) { + var uint8array = base64js.toByteArray(b64String); + return new TextDecoderLite('utf-8').decode(uint8array); +} diff --git a/ui/package.json b/ui/package.json index 23781382912..1fa19759050 100644 --- a/ui/package.json +++ b/ui/package.json @@ -36,6 +36,7 @@ "@ember/optional-features": "^0.7.0", "@hashicorp/structure-icons": "^1.3.0", "anser": "^1.4.8", + "base64-js": "^1.3.1", "broccoli-asset-rev": "^3.0.0", "bulma": "0.6.1", "core-js": "^2.4.1", @@ -110,6 +111,7 @@ "query-string": "^5.0.0", "qunit-dom": "^0.9.0", "sass": "^1.17.3", + "text-encoder-lite": "^2.0.0", "xterm": "^4.2.0-vscode1" }, "engines": { diff --git a/ui/tests/integration/components/exec-terminal-test.js b/ui/tests/integration/components/exec-terminal-test.js index 718db7d074a..ae3d0658b76 100644 --- a/ui/tests/integration/components/exec-terminal-test.js +++ b/ui/tests/integration/components/exec-terminal-test.js @@ -27,14 +27,14 @@ module('Integration | Component | exec-terminal', function(hooks) { window.xterm.buffer .getLine(0) .translateToString() - .includes('bash') + .includes('exec 🥳') ); done(); }); socket.mockMessage({ stdout: { - data: btoa('bash'), + data: 'ZXhlYyDwn6Wz', // FIXME? }, }); }); diff --git a/ui/yarn.lock b/ui/yarn.lock index d104813ea69..fffe6e3915a 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -2548,6 +2548,11 @@ base64-js@^1.0.2: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw== +base64-js@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + base64id@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" @@ -11987,6 +11992,11 @@ testem@^2.14.0: tmp "0.0.33" xmldom "^0.1.19" +text-encoder-lite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/text-encoder-lite/-/text-encoder-lite-2.0.0.tgz#3c865dd6f3720b279c9e370f8f36c831d2cee175" + integrity sha512-bo08ND8LlBwPeU23EluRUcO3p2Rsb/eN5EIfOVqfRmblNDEVKK5IzM9Qfidvo+odT0hhV8mpXQcP/M5MMzABXw== + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" From 1b92691611ce60cb2230ab8281b4432f26a2655f Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 27 Jan 2020 15:52:46 -0600 Subject: [PATCH 007/150] Add preliminary exec popup sidebar --- ui/app/router.js | 2 ++ ui/app/routes/exec.js | 27 +++++++++++++++++++++++++++ ui/app/templates/exec.hbs | 17 +++++++++++++++++ ui/tests/acceptance/exec-test.js | 27 +++++++++++++++++++++++++++ ui/tests/pages/exec.js | 13 +++++++++++++ 5 files changed, 86 insertions(+) create mode 100644 ui/app/routes/exec.js create mode 100644 ui/app/templates/exec.hbs create mode 100644 ui/tests/acceptance/exec-test.js create mode 100644 ui/tests/pages/exec.js diff --git a/ui/app/router.js b/ui/app/router.js index 1dd63b569e7..d1f0a387618 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -7,6 +7,8 @@ const Router = EmberRouter.extend({ }); Router.map(function() { + this.route('exec', { path: '/exec/:job_name' }); + this.route('jobs', function() { this.route('run'); this.route('job', { path: '/:job_name' }, function() { diff --git a/ui/app/routes/exec.js b/ui/app/routes/exec.js new file mode 100644 index 00000000000..e15843f703f --- /dev/null +++ b/ui/app/routes/exec.js @@ -0,0 +1,27 @@ +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; +import notifyError from 'nomad-ui/utils/notify-error'; + +// FIXME copied from jobs/job + +export default Route.extend({ + store: service(), + token: service(), + + serialize(model) { + return { job_name: model.get('plainId') }; + }, + + model(params, transition) { + const namespace = transition.to.queryParams.namespace || this.get('system.activeNamespace.id'); + const name = params.job_name; + const fullId = JSON.stringify([name, namespace || 'default']); + return this.store + .findRecord('job', fullId, { reload: true }) + .then(job => { + return RSVP.all([job.get('allocations'), job.get('evaluations')]).then(() => job); + }) + .catch(notifyError(this)); + }, +}); diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs new file mode 100644 index 00000000000..88a8ba04e62 --- /dev/null +++ b/ui/app/templates/exec.hbs @@ -0,0 +1,17 @@ +{{title "Exec"}} + +
    + tasks +
      + {{#each model.taskGroups as |taskGroup|}} +
    • + {{taskGroup.name}} +
        + {{#each taskGroup.tasks as |task|}} +
      • {{task.name}}
      • + {{/each}} +
      +
    • + {{/each}} +
    +
    \ No newline at end of file diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js new file mode 100644 index 00000000000..7efcbcb9705 --- /dev/null +++ b/ui/tests/acceptance/exec-test.js @@ -0,0 +1,27 @@ +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import Exec from 'nomad-ui/tests/pages/exec'; + +module('Acceptance | exec', function(hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function() { + server.create('agent'); + server.create('node'); + this.job = server.create('job', { createAllocations: false }); + server.create('allocation', 'withTaskWithPorts', { clientStatus: 'running' }); + }); + + test('/exec/:job should show the task groups and tasks', async function(assert) { + await Exec.visit({ job: this.job.id }); + + assert.equal(document.title, 'Exec - Nomad'); + + assert.equal(Exec.taskGroups.length, this.job.task_groups.length); + + assert.equal(Exec.taskGroups[0].name, this.job.task_groups.models[0].name); + assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); + }); +}); diff --git a/ui/tests/pages/exec.js b/ui/tests/pages/exec.js new file mode 100644 index 00000000000..1f3c6b36444 --- /dev/null +++ b/ui/tests/pages/exec.js @@ -0,0 +1,13 @@ +import { collection, create, text, visitable } from 'ember-cli-page-object'; + +export default create({ + visit: visitable('/exec/:job'), + + taskGroups: collection('[data-test-task-group]', { + name: text('[data-test-task-group-name]'), + + tasks: collection('[data-test-task]', { + name: text(), + }), + }), +}); From 55ef27358020284a7cd2274b42656e4a8f48cf7e Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 28 Jan 2020 10:09:06 -0600 Subject: [PATCH 008/150] Add show/hide of task group task list --- ui/app/components/task-group-parent.js | 11 +++++++++++ ui/app/templates/components/task-group-parent.hbs | 8 ++++++++ ui/app/templates/exec.hbs | 7 +------ ui/tests/acceptance/exec-test.js | 8 +++++++- ui/tests/pages/exec.js | 3 ++- 5 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 ui/app/components/task-group-parent.js create mode 100644 ui/app/templates/components/task-group-parent.hbs diff --git a/ui/app/components/task-group-parent.js b/ui/app/components/task-group-parent.js new file mode 100644 index 00000000000..88fb10c6116 --- /dev/null +++ b/ui/app/components/task-group-parent.js @@ -0,0 +1,11 @@ +import Component from '@ember/component'; + +export default Component.extend({ + isOpen: true, + + actions: { + toggleOpen() { + this.toggleProperty('isOpen'); + }, + }, +}); diff --git a/ui/app/templates/components/task-group-parent.hbs b/ui/app/templates/components/task-group-parent.hbs new file mode 100644 index 00000000000..5fe784e0ee6 --- /dev/null +++ b/ui/app/templates/components/task-group-parent.hbs @@ -0,0 +1,8 @@ + +{{#if isOpen}} +
      + {{#each taskGroup.tasks as |task|}} +
    • {{task.name}}
    • + {{/each}} +
    +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index 88a8ba04e62..df728e8afd4 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -5,12 +5,7 @@
      {{#each model.taskGroups as |taskGroup|}}
    • - {{taskGroup.name}} -
        - {{#each taskGroup.tasks as |task|}} -
      • {{task.name}}
      • - {{/each}} -
      + {{task-group-parent taskGroup=taskGroup}}
    • {{/each}}
    diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 7efcbcb9705..ea6710826f4 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -14,7 +14,7 @@ module('Acceptance | exec', function(hooks) { server.create('allocation', 'withTaskWithPorts', { clientStatus: 'running' }); }); - test('/exec/:job should show the task groups and tasks', async function(assert) { + test('/exec/:job should show the task groups and tasks and allow task groups to be collapsed', async function(assert) { await Exec.visit({ job: this.job.id }); assert.equal(document.title, 'Exec - Nomad'); @@ -23,5 +23,11 @@ module('Acceptance | exec', function(hooks) { assert.equal(Exec.taskGroups[0].name, this.job.task_groups.models[0].name); assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); + + await Exec.taskGroups[0].click(); + assert.equal(Exec.taskGroups[0].tasks.length, 0); + + await Exec.taskGroups[0].click(); + assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); }); }); diff --git a/ui/tests/pages/exec.js b/ui/tests/pages/exec.js index 1f3c6b36444..39a702902cf 100644 --- a/ui/tests/pages/exec.js +++ b/ui/tests/pages/exec.js @@ -1,9 +1,10 @@ -import { collection, create, text, visitable } from 'ember-cli-page-object'; +import { clickable, collection, create, text, visitable } from 'ember-cli-page-object'; export default create({ visit: visitable('/exec/:job'), taskGroups: collection('[data-test-task-group]', { + click: clickable('[data-test-task-group-name]'), name: text('[data-test-task-group-name]'), tasks: collection('[data-test-task]', { From dce93ddb0c9cccd06186c2439960429aa5e51e83 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 28 Jan 2020 15:58:39 -0600 Subject: [PATCH 009/150] Add preliminary styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This seems quite fiddly, hopefully not Too Much™ 😳 --- ui/app/styles/components.scss | 1 + ui/app/styles/components/task-group-tree.scss | 74 +++++++++++++++++++ ui/app/styles/core.scss | 1 + ui/app/styles/utils/structure-colors.scss | 4 + .../components/task-group-parent.hbs | 15 +++- ui/app/templates/exec.hbs | 4 +- ui/tests/acceptance/exec-test.js | 2 + ui/tests/pages/exec.js | 8 +- 8 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 ui/app/styles/components/task-group-tree.scss create mode 100644 ui/app/styles/utils/structure-colors.scss diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index 7f8e0478401..f4e75e94f31 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -24,6 +24,7 @@ @import './components/search-box'; @import './components/simple-list'; @import './components/status-text'; +@import './components/task-group-tree'; @import './components/timeline'; @import './components/toggle'; @import './components/toolbar'; diff --git a/ui/app/styles/components/task-group-tree.scss b/ui/app/styles/components/task-group-tree.scss new file mode 100644 index 00000000000..b7695f46396 --- /dev/null +++ b/ui/app/styles/components/task-group-tree.scss @@ -0,0 +1,74 @@ +.task-group-tree { + background-color: $ui-gray-900; + color: white; + padding: 16px; + + .title { + text-transform: uppercase; + color: $grey-lighter; // This is lightness 86% but the design is 85% + font-size: 11px; + } + + .toggle-button { + background: transparent; + border: 0; + color: white; + font-size: inherit; + width: 100%; + text-align: left; + padding-left: 0; + + .icon { + color: $ui-gray-500; + padding: 3px 3px 0 0; + margin-left: -3px; + } + } + + .task-list { + .task-item { + padding: 0 8px 0 19px; + + color: white; + display: flex; + align-items: center; + justify-content: space-between; + + .border-and-label { + display: flex; + align-items: center; + height: 100%; + } + + .border { + padding-left: 8px; + padding-right: 5px; + border-left: 1px solid $ui-gray-700; + height: 100%; + } + + .task-label { + padding-bottom: 1px; + } + + .icon { + display: none; + } + + &:hover .icon { + display: block; + } + } + } + + .toggle-button, + .task-item { + height: 32px; + font-weight: 500; + + &:hover { + background-color: $ui-gray-800; + border-radius: 4px; + } + } +} diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index a92c17adea1..db9fdb95d81 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -2,6 +2,7 @@ @import './utils/reset.scss'; @import './utils/z-indices'; @import './utils/product-colors'; +@import './utils/structure-colors'; @import './utils/bumper'; @import './utils/layout'; diff --git a/ui/app/styles/utils/structure-colors.scss b/ui/app/styles/utils/structure-colors.scss new file mode 100644 index 00000000000..26ed1f3a7cc --- /dev/null +++ b/ui/app/styles/utils/structure-colors.scss @@ -0,0 +1,4 @@ +$ui-gray-500: #6f7682; +$ui-gray-700: #525761; +$ui-gray-800: #373a42; +$ui-gray-900: #1f2124; diff --git a/ui/app/templates/components/task-group-parent.hbs b/ui/app/templates/components/task-group-parent.hbs index 5fe784e0ee6..3d1116b5bc0 100644 --- a/ui/app/templates/components/task-group-parent.hbs +++ b/ui/app/templates/components/task-group-parent.hbs @@ -1,8 +1,17 @@ - + {{#if isOpen}} -
      +
        {{#each taskGroup.tasks as |task|}} -
      • {{task.name}}
      • +
      • +
        +
        +
        {{task.name}}
        +
        + {{x-icon "exit"}} +
      • {{/each}}
      {{/if}} \ No newline at end of file diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index df728e8afd4..d9b46f35a4b 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -1,7 +1,7 @@ {{title "Exec"}} -
      - tasks +
      +

      Tasks

        {{#each model.taskGroups as |taskGroup|}}
      • diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index ea6710826f4..c11124a123e 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -26,8 +26,10 @@ module('Acceptance | exec', function(hooks) { await Exec.taskGroups[0].click(); assert.equal(Exec.taskGroups[0].tasks.length, 0); + assert.ok(Exec.taskGroups[0].chevron.isRight); await Exec.taskGroups[0].click(); assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); + assert.ok(Exec.taskGroups[0].chevron.isDown); }); }); diff --git a/ui/tests/pages/exec.js b/ui/tests/pages/exec.js index 39a702902cf..46477347432 100644 --- a/ui/tests/pages/exec.js +++ b/ui/tests/pages/exec.js @@ -1,4 +1,4 @@ -import { clickable, collection, create, text, visitable } from 'ember-cli-page-object'; +import { clickable, collection, create, hasClass, text, visitable } from 'ember-cli-page-object'; export default create({ visit: visitable('/exec/:job'), @@ -7,6 +7,12 @@ export default create({ click: clickable('[data-test-task-group-name]'), name: text('[data-test-task-group-name]'), + chevron: { + scope: '.icon', + isDown: hasClass('icon-is-chevron-down'), + isRight: hasClass('icon-is-chevron-right'), + }, + tasks: collection('[data-test-task]', { name: text(), }), From a0a8f88543c991eefc4e620a844882204b4e831a Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 28 Jan 2020 16:36:28 -0600 Subject: [PATCH 010/150] Change icon selector to have correct scope --- ui/tests/pages/exec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/pages/exec.js b/ui/tests/pages/exec.js index 46477347432..628d17d0ddb 100644 --- a/ui/tests/pages/exec.js +++ b/ui/tests/pages/exec.js @@ -8,7 +8,7 @@ export default create({ name: text('[data-test-task-group-name]'), chevron: { - scope: '.icon', + scope: '.toggle-button .icon', isDown: hasClass('icon-is-chevron-down'), isRight: hasClass('icon-is-chevron-right'), }, From 4bc5838ed73bef0124eb44e91249db9057bec58d Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 28 Jan 2020 17:10:19 -0600 Subject: [PATCH 011/150] Correct colour for exit icon --- ui/app/styles/components/task-group-tree.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/app/styles/components/task-group-tree.scss b/ui/app/styles/components/task-group-tree.scss index b7695f46396..5dbca2c2c12 100644 --- a/ui/app/styles/components/task-group-tree.scss +++ b/ui/app/styles/components/task-group-tree.scss @@ -9,6 +9,10 @@ font-size: 11px; } + .icon { + color: $ui-gray-500; + } + .toggle-button { background: transparent; border: 0; @@ -19,7 +23,6 @@ padding-left: 0; .icon { - color: $ui-gray-500; padding: 3px 3px 0 0; margin-left: -3px; } From 16160e1f20b44654561eebe82b985d1a4a02fa81 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 29 Jan 2020 14:44:07 -0600 Subject: [PATCH 012/150] Add preliminary header with job details --- ui/app/templates/exec.hbs | 6 ++++++ ui/tests/acceptance/exec-test.js | 23 +++++++++++++++++++---- ui/tests/pages/exec.js | 6 ++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index d9b46f35a4b..02d632c2b3a 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -1,5 +1,11 @@ {{title "Exec"}} +
        + region: {{model.region}} + namespace: {{model.namespace.id}} + job: {{model.name}} +
        +

        Tasks

          diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index c11124a123e..34a99975261 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -8,16 +8,31 @@ module('Acceptance | exec', function(hooks) { setupMirage(hooks); hooks.beforeEach(async function() { + server.create('namespace'); + this.namespace = server.create('namespace'); + server.create('agent'); server.create('node'); - this.job = server.create('job', { createAllocations: false }); + + this.job = server.create('job', { createAllocations: false, namespaceId: this.namespace.id }); server.create('allocation', 'withTaskWithPorts', { clientStatus: 'running' }); + + server.create('region', { id: 'global' }); + server.create('region', { id: 'region-2' }); }); - test('/exec/:job should show the task groups and tasks and allow task groups to be collapsed', async function(assert) { - await Exec.visit({ job: this.job.id }); + test('/exec/:job should show the region, namespace, and job name', async function(assert) { + await Exec.visit({ job: this.job.id, namespace: this.namespace.id, region: 'region-2' }); - assert.equal(document.title, 'Exec - Nomad'); + assert.equal(document.title, 'Exec - region-2 - Nomad'); + + assert.equal(Exec.header.region, this.job.region); + assert.equal(Exec.header.namespace, this.job.namespace); + assert.equal(Exec.header.job, this.job.name); + }); + + test('/exec/:job should show the task groups and tasks and allow task groups to be collapsed', async function(assert) { + await Exec.visit({ job: this.job.id, namespace: this.namespace.id }); assert.equal(Exec.taskGroups.length, this.job.task_groups.length); diff --git a/ui/tests/pages/exec.js b/ui/tests/pages/exec.js index 628d17d0ddb..41edd9ea6ce 100644 --- a/ui/tests/pages/exec.js +++ b/ui/tests/pages/exec.js @@ -3,6 +3,12 @@ import { clickable, collection, create, hasClass, text, visitable } from 'ember- export default create({ visit: visitable('/exec/:job'), + header: { + region: text('[data-test-region]'), + namespace: text('[data-test-namespace]'), + job: text('[data-test-job]'), + }, + taskGroups: collection('[data-test-task-group]', { click: clickable('[data-test-task-group-name]'), name: text('[data-test-task-group-name]'), From 43370ea327a08467ebeba8fc3248120214b16e97 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 29 Jan 2020 15:09:04 -0600 Subject: [PATCH 013/150] Change region/namespace to show conditionally --- ui/app/controllers/exec.js | 6 ++++++ ui/app/templates/exec.hbs | 9 +++++++-- ui/tests/acceptance/exec-test.js | 29 +++++++++++++++++++---------- ui/tests/pages/exec.js | 4 ++-- 4 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 ui/app/controllers/exec.js diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js new file mode 100644 index 00000000000..3520bbfafc1 --- /dev/null +++ b/ui/app/controllers/exec.js @@ -0,0 +1,6 @@ +import { inject as service } from '@ember/service'; +import Controller from '@ember/controller'; + +export default Controller.extend({ + system: service(), +}); diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index 02d632c2b3a..a6478eae88c 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -1,8 +1,13 @@ {{title "Exec"}}
          - region: {{model.region}} - namespace: {{model.namespace.id}} + {{#if system.shouldShowRegions}} + region: {{model.region}} + {{/if}} + + {{#if system.shouldShowNamespaces}} + namespace: {{model.namespace.id}} + {{/if}} job: {{model.name}}
          diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 34a99975261..08b9bcccb10 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -8,31 +8,40 @@ module('Acceptance | exec', function(hooks) { setupMirage(hooks); hooks.beforeEach(async function() { - server.create('namespace'); - this.namespace = server.create('namespace'); - server.create('agent'); server.create('node'); - this.job = server.create('job', { createAllocations: false, namespaceId: this.namespace.id }); + this.job = server.create('job', { createAllocations: false }); server.create('allocation', 'withTaskWithPorts', { clientStatus: 'running' }); + }); + + test('/exec/:job should show the region, namespace, and job name', async function(assert) { + server.create('namespace'); + const namespace = server.create('namespace'); server.create('region', { id: 'global' }); server.create('region', { id: 'region-2' }); - }); - test('/exec/:job should show the region, namespace, and job name', async function(assert) { - await Exec.visit({ job: this.job.id, namespace: this.namespace.id, region: 'region-2' }); + this.job = server.create('job', { createAllocations: false, namespaceId: namespace.id }); + + await Exec.visit({ job: this.job.id, namespace: namespace.id, region: 'region-2' }); assert.equal(document.title, 'Exec - region-2 - Nomad'); - assert.equal(Exec.header.region, this.job.region); - assert.equal(Exec.header.namespace, this.job.namespace); + assert.equal(Exec.header.region.text, this.job.region); + assert.equal(Exec.header.namespace.text, this.job.namespace); assert.equal(Exec.header.job, this.job.name); }); + test('/exec/:job should not show region and namespace when there are none', async function(assert) { + await Exec.visit({ job: this.job.id }); + + assert.ok(Exec.header.region.isHidden); + assert.ok(Exec.header.namespace.isHidden); + }); + test('/exec/:job should show the task groups and tasks and allow task groups to be collapsed', async function(assert) { - await Exec.visit({ job: this.job.id, namespace: this.namespace.id }); + await Exec.visit({ job: this.job.id }); assert.equal(Exec.taskGroups.length, this.job.task_groups.length); diff --git a/ui/tests/pages/exec.js b/ui/tests/pages/exec.js index 41edd9ea6ce..4438c773da9 100644 --- a/ui/tests/pages/exec.js +++ b/ui/tests/pages/exec.js @@ -4,8 +4,8 @@ export default create({ visit: visitable('/exec/:job'), header: { - region: text('[data-test-region]'), - namespace: text('[data-test-namespace]'), + region: { scope: '[data-test-region]' }, + namespace: { scope: '[data-test-namespace]' }, job: text('[data-test-job]'), }, From 922ebd92fd9fad7a83fbc1facf71620dfe9c8de3 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 29 Jan 2020 16:21:29 -0600 Subject: [PATCH 014/150] Add styling for popup navbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s not that much of a navbar even… just not sure about how much sharing is even worth it here 🤔 --- ui/app/styles/core/navbar.scss | 46 ++++++++++++++++++++++++++++++++++ ui/app/templates/exec.hbs | 37 ++++++++++++++++++++------- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/ui/app/styles/core/navbar.scss b/ui/app/styles/core/navbar.scss index 4bc6767bfa0..9dad60a68d1 100644 --- a/ui/app/styles/core/navbar.scss +++ b/ui/app/styles/core/navbar.scss @@ -75,6 +75,52 @@ } } + // FIMXE how questionable is this, is it worth trying to share at all? + &.is-popup { + background-color: $nomad-green-dark; + height: 3.5rem; + color: $primary-invert; + padding-left: 20px; + padding-right: 20px; + overflow: hidden; + + .navbar-brand { + margin-right: 8px; + } + + .navbar-item { + color: white; + + .navbar-label { + font-weight: 600; + margin-right: 1rem; + } + } + + .navbar-end { + display: flex; + align-items: center; + justify-content: flex-end; + margin-left: auto; + } + + .navbar-end > a.navbar-item { + color: rgba($primary-invert, 0.8); + text-decoration: none; + + &:hover { + color: $primary-invert; + background: transparent; + } + } + + .navbar-brand > a.navbar-item { + &:hover { + background: transparent; + } + } + } + .navbar-item { display: flex; align-items: center; diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index a6478eae88c..bbc3ec12023 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -1,15 +1,34 @@ {{title "Exec"}} -
          - {{#if system.shouldShowRegions}} - region: {{model.region}} - {{/if}} +
          + {{#if system.shouldShowNamespaces}} + + {{/if}} + + + +

          Tasks

          From 0eaf14579fb526d3b31801214bd326b5ec4d1b8e Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 29 Jan 2020 16:27:06 -0600 Subject: [PATCH 015/150] Remove new-window target for documentation for now --- ui/app/templates/exec.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index bbc3ec12023..bdfffd31d98 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -25,7 +25,7 @@ {{model.name}}
          From 17107512a873d8f6c910d51ca076cf8a8aadbac1 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 30 Jan 2020 16:42:47 -0600 Subject: [PATCH 016/150] Add terminal with task selection message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This approach… not sure about it: • how to test the contents of the terminal without storing on window? • passing the terminal in seems a bit weird but otherwise was considering some event interface, but that seems like overkill…?!? --- ui/app/components/exec-terminal.js | 26 ------------------- ui/app/controllers/exec.js | 10 +++++++ ui/app/styles/components.scss | 2 +- .../{task-group-tree.scss => exec.scss} | 8 ++++++ ui/app/templates/exec.hbs | 21 ++++++++------- ui/tests/acceptance/exec-test.js | 12 +++++++++ 6 files changed, 43 insertions(+), 36 deletions(-) rename ui/app/styles/components/{task-group-tree.scss => exec.scss} (94%) diff --git a/ui/app/components/exec-terminal.js b/ui/app/components/exec-terminal.js index aa97217a030..efd030939b2 100644 --- a/ui/app/components/exec-terminal.js +++ b/ui/app/components/exec-terminal.js @@ -1,33 +1,7 @@ import Component from '@ember/component'; -import { Terminal } from 'xterm'; -import base64js from 'base64-js'; -import { TextDecoderLite, TextEncoderLite } from 'text-encoder-lite'; export default Component.extend({ didInsertElement() { - this.terminal = new Terminal(); this.terminal.open(this.element.querySelector('.terminal')); - - this.terminal.onData(e => { - this.socket.send(JSON.stringify({ stdin: { data: encodeString(e) } })); - }); - - // FIXME this is a hack to provide access in an integration test 🧐 - window.xterm = this.terminal; - - this.socket.onmessage = e => { - const json = JSON.parse(e.data); - this.terminal.write(decodeString(json.stdout.data)); - }; }, }); - -function encodeString(string) { - var encoded = new TextEncoderLite('utf-8').encode(string); - return base64js.fromByteArray(encoded); -} - -function decodeString(b64String) { - var uint8array = base64js.toByteArray(b64String); - return new TextDecoderLite('utf-8').decode(uint8array); -} diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 3520bbfafc1..edf31c21488 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -1,6 +1,16 @@ import { inject as service } from '@ember/service'; import Controller from '@ember/controller'; +import { Terminal } from 'xterm'; export default Controller.extend({ system: service(), + + init() { + this._super(...arguments); + + this.terminal = new Terminal(); + window.execTerminal = this.terminal; // FIXME tragique, for acceptance tests…? + + this.terminal.write('Select a task to start your session.'); + }, }); diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index f4e75e94f31..109ea093cab 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -8,6 +8,7 @@ @import './components/ember-power-select'; @import './components/empty-message'; @import './components/error-container'; +@import './components/exec'; @import './components/fs-explorer'; @import './components/gutter'; @import './components/gutter-toggle'; @@ -24,7 +25,6 @@ @import './components/search-box'; @import './components/simple-list'; @import './components/status-text'; -@import './components/task-group-tree'; @import './components/timeline'; @import './components/toggle'; @import './components/toolbar'; diff --git a/ui/app/styles/components/task-group-tree.scss b/ui/app/styles/components/exec.scss similarity index 94% rename from ui/app/styles/components/task-group-tree.scss rename to ui/app/styles/components/exec.scss index 5dbca2c2c12..56e3879a126 100644 --- a/ui/app/styles/components/task-group-tree.scss +++ b/ui/app/styles/components/exec.scss @@ -1,3 +1,11 @@ +.tree-and-terminal { + display: flex; + + .terminal { + flex-grow: 1; + } +} + .task-group-tree { background-color: $ui-gray-900; color: white; diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index bdfffd31d98..e834624894b 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -30,13 +30,16 @@
        -
        -

        Tasks

        -
          - {{#each model.taskGroups as |taskGroup|}} -
        • - {{task-group-parent taskGroup=taskGroup}} -
        • - {{/each}} -
        +
        +
        +

        Tasks

        +
          + {{#each model.taskGroups as |taskGroup|}} +
        • + {{task-group-parent taskGroup=taskGroup}} +
        • + {{/each}} +
        +
        + {{exec-terminal terminal=terminal}}
        \ No newline at end of file diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 08b9bcccb10..9fdd9ee63a6 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -56,4 +56,16 @@ module('Acceptance | exec', function(hooks) { assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); assert.ok(Exec.taskGroups[0].chevron.isDown); }); + + test('/exec/:job should require selecting a task', async function(assert) { + await Exec.visit({ job: this.job.id }); + + assert.equal( + window.execTerminal.buffer + .getLine(0) + .translateToString() + .trim(), + 'Select a task to start your session.' + ); + }); }); From 61c09b67ce124d5f7235ccc795462707d3f06e68 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 30 Jan 2020 16:49:57 -0600 Subject: [PATCH 017/150] Remove integration test for now --- ui/tests/integration/components/exec-terminal-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/tests/integration/components/exec-terminal-test.js b/ui/tests/integration/components/exec-terminal-test.js index ae3d0658b76..a252c53287f 100644 --- a/ui/tests/integration/components/exec-terminal-test.js +++ b/ui/tests/integration/components/exec-terminal-test.js @@ -1,4 +1,4 @@ -import { module, skip, test } from 'qunit'; +import { module, skip } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, triggerKeyEvent } from '@ember/test-helpers'; import { next } from '@ember/runloop'; @@ -8,7 +8,7 @@ import sinon from 'sinon'; module('Integration | Component | exec-terminal', function(hooks) { setupRenderingTest(hooks); - test('it renders an incoming message', async function(assert) { + skip('it renders an incoming message', async function(assert) { const done = assert.async(); const socket = { From 75c2c5764e85c4e0ccd9eac87225affae8e25e47 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 3 Feb 2020 10:11:46 -0600 Subject: [PATCH 018/150] Add auto-fit for terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This isn’t responsive to resize events etc yet but at least looks better. --- ui/app/components/exec-terminal.js | 8 ++++++++ ui/app/styles/components/exec.scss | 2 +- ui/package.json | 3 ++- ui/yarn.lock | 5 +++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/ui/app/components/exec-terminal.js b/ui/app/components/exec-terminal.js index efd030939b2..e9e00c50775 100644 --- a/ui/app/components/exec-terminal.js +++ b/ui/app/components/exec-terminal.js @@ -1,7 +1,15 @@ import Component from '@ember/component'; +import { FitAddon } from 'xterm-addon-fit'; export default Component.extend({ + classNames: ['terminal-container'], + didInsertElement() { + const fitAddon = new FitAddon(); + this.terminal.loadAddon(fitAddon); + this.terminal.open(this.element.querySelector('.terminal')); + + fitAddon.fit(); }, }); diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index 56e3879a126..57353322765 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -1,7 +1,7 @@ .tree-and-terminal { display: flex; - .terminal { + .terminal-container { flex-grow: 1; } } diff --git a/ui/package.json b/ui/package.json index 03e7efdedc6..fb1a217fd40 100644 --- a/ui/package.json +++ b/ui/package.json @@ -117,7 +117,8 @@ "qunit-dom": "^0.9.0", "sass": "^1.17.3", "text-encoder-lite": "^2.0.0", - "xterm": "^4.2.0-vscode1" + "xterm": "^4.2.0-vscode1", + "xterm-addon-fit": "^0.3.0" }, "optionalDependencies": { "@babel/plugin-transform-member-expression-literals": "^7.2.0", diff --git a/ui/yarn.lock b/ui/yarn.lock index 36516bb4ce5..c079795ecec 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -16163,6 +16163,11 @@ xtend@~4.0.0: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= +xterm-addon-fit@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.3.0.tgz#341710741027de9d648a9f84415a01ddfdbbe715" + integrity sha512-kvkiqHVrnMXgyCH9Xn0BOBJ7XaWC/4BgpSWQy3SueqximgW630t/QOankgqkvk11iTOCwWdAY9DTyQBXUMN3lw== + xterm@^4.2.0-vscode1: version "4.2.0-vscode1" resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.2.0-vscode1.tgz#dbd67e52536578ea6b772d69a97da1487acf29e3" From 6bd195e6c7d4e8954dc4d06018257c6baea72cc2 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 3 Feb 2020 10:44:24 -0600 Subject: [PATCH 019/150] Add width for exec sidebar --- ui/app/styles/components/exec.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index 57353322765..4114f346282 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -10,6 +10,7 @@ background-color: $ui-gray-900; color: white; padding: 16px; + width: 200px; // these px sizes… 🧐 .title { text-transform: uppercase; From 025601408a4ee1192edc36d55ccdaacb82bd75fd Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 3 Feb 2020 11:23:53 -0600 Subject: [PATCH 020/150] Add padding around terminal element --- ui/app/styles/components/exec.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index 4114f346282..0be659e878c 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -3,6 +3,8 @@ .terminal-container { flex-grow: 1; + background: black; + padding: 16px; } } From 8747919fcff44686a11ecd0baffdad215e12991d Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 3 Feb 2020 15:14:22 -0600 Subject: [PATCH 021/150] Change task group to be closed by default --- ui/app/components/task-group-parent.js | 2 +- ui/tests/acceptance/exec-test.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/app/components/task-group-parent.js b/ui/app/components/task-group-parent.js index 88fb10c6116..8cdbe8d682c 100644 --- a/ui/app/components/task-group-parent.js +++ b/ui/app/components/task-group-parent.js @@ -1,7 +1,7 @@ import Component from '@ember/component'; export default Component.extend({ - isOpen: true, + isOpen: false, actions: { toggleOpen() { diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 9fdd9ee63a6..2129ac43b4d 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -40,21 +40,21 @@ module('Acceptance | exec', function(hooks) { assert.ok(Exec.header.namespace.isHidden); }); - test('/exec/:job should show the task groups and tasks and allow task groups to be collapsed', async function(assert) { + test('/exec/:job should show the task groups collapsed by default allow the tasks to be shown', async function(assert) { await Exec.visit({ job: this.job.id }); assert.equal(Exec.taskGroups.length, this.job.task_groups.length); assert.equal(Exec.taskGroups[0].name, this.job.task_groups.models[0].name); - assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); - - await Exec.taskGroups[0].click(); assert.equal(Exec.taskGroups[0].tasks.length, 0); assert.ok(Exec.taskGroups[0].chevron.isRight); await Exec.taskGroups[0].click(); assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); assert.ok(Exec.taskGroups[0].chevron.isDown); + + await Exec.taskGroups[0].click(); + assert.equal(Exec.taskGroups[0].tasks.length, 0); }); test('/exec/:job should require selecting a task', async function(assert) { From 3ff8f49c844086844b84a51e8c534956d77cc0b2 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 3 Feb 2020 16:34:06 -0600 Subject: [PATCH 022/150] Change task group to be opened by routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This seems a bit strange but…? Maybe a component would make sense to wrap everything in but then would it be rerendering as you navigate deeper into the hierarchy…? Maybe there’s some breakage if you navigate between task groups and something closes that you opened yourself but since I don’t anticipate actually changing the route when you click a task group (vs a task, which will), it seems safe to ignore. --- ui/app/components/task-group-parent.js | 26 ++++++++++++++++++++++++-- ui/app/router.js | 4 +++- ui/tests/acceptance/exec-test.js | 16 ++++++++++++---- ui/tests/pages/exec.js | 3 ++- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/ui/app/components/task-group-parent.js b/ui/app/components/task-group-parent.js index 8cdbe8d682c..27095c655ae 100644 --- a/ui/app/components/task-group-parent.js +++ b/ui/app/components/task-group-parent.js @@ -1,11 +1,33 @@ import Component from '@ember/component'; +import { inject as service } from '@ember/service'; +import { computed } from '@ember/object'; +import { or } from '@ember/object/computed'; export default Component.extend({ - isOpen: false, + router: service(), + + isOpen: or('clickedOpen', 'currentRouteIsThisTaskGroup'), + + currentRouteIsThisTaskGroup: computed('router.currentRoute', function() { + const route = this.router.currentRoute; + + if (route.name === 'exec.task-group') { + const execRoute = route.parent; + + return ( + execRoute.params.job_name === this.taskGroup.job.name && + route.params.task_group_name === this.taskGroup.name + ); + } else { + return false; + } + }), + + clickedOpen: false, actions: { toggleOpen() { - this.toggleProperty('isOpen'); + this.toggleProperty('clickedOpen'); }, }, }); diff --git a/ui/app/router.js b/ui/app/router.js index d1f0a387618..b8495f7791d 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -7,7 +7,9 @@ const Router = EmberRouter.extend({ }); Router.map(function() { - this.route('exec', { path: '/exec/:job_name' }); + this.route('exec', { path: '/exec/:job_name' }, function() { + this.route('task-group', { path: '/:task_group_name' }); + }); this.route('jobs', function() { this.route('run'); diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 2129ac43b4d..2fe204abf03 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -24,7 +24,7 @@ module('Acceptance | exec', function(hooks) { this.job = server.create('job', { createAllocations: false, namespaceId: namespace.id }); - await Exec.visit({ job: this.job.id, namespace: namespace.id, region: 'region-2' }); + await Exec.visitJob({ job: this.job.id, namespace: namespace.id, region: 'region-2' }); assert.equal(document.title, 'Exec - region-2 - Nomad'); @@ -34,14 +34,14 @@ module('Acceptance | exec', function(hooks) { }); test('/exec/:job should not show region and namespace when there are none', async function(assert) { - await Exec.visit({ job: this.job.id }); + await Exec.visitJob({ job: this.job.id }); assert.ok(Exec.header.region.isHidden); assert.ok(Exec.header.namespace.isHidden); }); test('/exec/:job should show the task groups collapsed by default allow the tasks to be shown', async function(assert) { - await Exec.visit({ job: this.job.id }); + await Exec.visitJob({ job: this.job.id }); assert.equal(Exec.taskGroups.length, this.job.task_groups.length); @@ -58,7 +58,7 @@ module('Acceptance | exec', function(hooks) { }); test('/exec/:job should require selecting a task', async function(assert) { - await Exec.visit({ job: this.job.id }); + await Exec.visitJob({ job: this.job.id }); assert.equal( window.execTerminal.buffer @@ -68,4 +68,12 @@ module('Acceptance | exec', function(hooks) { 'Select a task to start your session.' ); }); + + test('/exec/:job/:task_group should open that task group by default', async function(assert) { + const taskGroup = this.job.task_groups.models[0]; + await Exec.visitTaskGroup({ job: this.job.id, task_group: taskGroup.name }); + + assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); + assert.ok(Exec.taskGroups[0].chevron.isDown); + }); }); diff --git a/ui/tests/pages/exec.js b/ui/tests/pages/exec.js index 4438c773da9..a8c02e3f343 100644 --- a/ui/tests/pages/exec.js +++ b/ui/tests/pages/exec.js @@ -1,7 +1,8 @@ import { clickable, collection, create, hasClass, text, visitable } from 'ember-cli-page-object'; export default create({ - visit: visitable('/exec/:job'), + visitJob: visitable('/exec/:job'), + visitTaskGroup: visitable('/exec/:job/:task_group'), header: { region: { scope: '[data-test-region]' }, From 94e50b272cf7e2e966e48caa48b84903408819b1 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 4 Feb 2020 12:19:54 -0600 Subject: [PATCH 023/150] Change task items to links --- ui/app/components/task-group-parent.js | 7 ++++--- ui/app/router.js | 4 +++- ui/app/styles/components/exec.scss | 1 + .../components/task-group-parent.hbs | 5 +++-- ui/tests/acceptance/exec-test.js | 20 ++++++++++++++++++- ui/tests/pages/exec.js | 1 + 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/ui/app/components/task-group-parent.js b/ui/app/components/task-group-parent.js index 27095c655ae..1f010f2d166 100644 --- a/ui/app/components/task-group-parent.js +++ b/ui/app/components/task-group-parent.js @@ -11,12 +11,13 @@ export default Component.extend({ currentRouteIsThisTaskGroup: computed('router.currentRoute', function() { const route = this.router.currentRoute; - if (route.name === 'exec.task-group') { - const execRoute = route.parent; + if (route.name.includes('task-group')) { + const taskGroupRoute = route.parent; + const execRoute = taskGroupRoute.parent; return ( execRoute.params.job_name === this.taskGroup.job.name && - route.params.task_group_name === this.taskGroup.name + taskGroupRoute.params.task_group_name === this.taskGroup.name ); } else { return false; diff --git a/ui/app/router.js b/ui/app/router.js index b8495f7791d..2e562271ac2 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -8,7 +8,9 @@ const Router = EmberRouter.extend({ Router.map(function() { this.route('exec', { path: '/exec/:job_name' }, function() { - this.route('task-group', { path: '/:task_group_name' }); + this.route('task-group', { path: '/:task_group_name' }, function() { + this.route('task', { path: '/:task_name' }); + }); }); this.route('jobs', function() { diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index 0be659e878c..1eba28d6ffc 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -44,6 +44,7 @@ padding: 0 8px 0 19px; color: white; + text-decoration: none; display: flex; align-items: center; justify-content: space-between; diff --git a/ui/app/templates/components/task-group-parent.hbs b/ui/app/templates/components/task-group-parent.hbs index 3d1116b5bc0..6c4b6024f62 100644 --- a/ui/app/templates/components/task-group-parent.hbs +++ b/ui/app/templates/components/task-group-parent.hbs @@ -5,13 +5,14 @@ {{#if isOpen}}
          {{#each taskGroup.tasks as |task|}} -
        • + {{!-- FIXME this needs to include namespace etc query parameters --}} + {{#link-to "exec.task-group.task" taskGroup.job.name taskGroup.name task.name class="task-item" data-test-task=true}}
          {{task.name}}
          {{x-icon "exit"}} -
        • + {{/link-to}} {{/each}}
        {{/if}} \ No newline at end of file diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 2fe204abf03..a357cc44668 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -1,4 +1,5 @@ import { module, test } from 'qunit'; +import { currentURL } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import Exec from 'nomad-ui/tests/pages/exec'; @@ -69,11 +70,28 @@ module('Acceptance | exec', function(hooks) { ); }); - test('/exec/:job/:task_group should open that task group by default', async function(assert) { + test('visiting a path with a task group should open the group by default', async function(assert) { const taskGroup = this.job.task_groups.models[0]; await Exec.visitTaskGroup({ job: this.job.id, task_group: taskGroup.name }); assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); assert.ok(Exec.taskGroups[0].chevron.isDown); + + const task = taskGroup.tasks.models[0]; + await Exec.visitTask({ job: this.job.id, task_group: taskGroup.name, task_name: task.name }); + + assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); + assert.ok(Exec.taskGroups[0].chevron.isDown); + }); + + test('navigating to a task adds its name to the route', async function(assert) { + await Exec.visitJob({ job: this.job.id }); + await Exec.taskGroups[0].click(); + await Exec.taskGroups[0].tasks[0].click(); + + const taskGroup = this.job.task_groups.models[0]; + const task = taskGroup.tasks.models[0]; + + assert.equal(currentURL(), `/exec/${this.job.id}/${taskGroup.name}/${task.name}`); }); }); diff --git a/ui/tests/pages/exec.js b/ui/tests/pages/exec.js index a8c02e3f343..05eb204e572 100644 --- a/ui/tests/pages/exec.js +++ b/ui/tests/pages/exec.js @@ -3,6 +3,7 @@ import { clickable, collection, create, hasClass, text, visitable } from 'ember- export default create({ visitJob: visitable('/exec/:job'), visitTaskGroup: visitable('/exec/:job/:task_group'), + visitTask: visitable('/exec/:job/:task_group/:task_name'), header: { region: { scope: '[data-test-region]' }, From e6c469a6e2f4b53b240de9c8da157da179c589dd Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 4 Feb 2020 13:50:22 -0600 Subject: [PATCH 024/150] Add first step toward editable command output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I haven’t started to include allocations in all of this yet so this is pretty unrealistic. --- ui/app/controllers/exec.js | 13 ++++++++++++- ui/app/routes/exec/task-group/task.js | 11 +++++++++++ ui/tests/acceptance/exec-test.js | 22 ++++++++++++++++++++-- 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 ui/app/routes/exec/task-group/task.js diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index edf31c21488..a78e7f0c615 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -11,6 +11,17 @@ export default Controller.extend({ this.terminal = new Terminal(); window.execTerminal = this.terminal; // FIXME tragique, for acceptance tests…? - this.terminal.write('Select a task to start your session.'); + this.terminal.writeln('Select a task to start your session.'); + }, + + actions: { + setTask({ task_name }) { + this.terminal.writeln(''); + this.terminal.writeln( + 'To start the session, customize your command, then hit ‘return’ to run.' + ); + this.terminal.writeln(''); + this.terminal.writeln(`$ nomad alloc exec -i -t -task ${task_name} ALLOCATION /bin/bash`); + }, }, }); diff --git a/ui/app/routes/exec/task-group/task.js b/ui/app/routes/exec/task-group/task.js new file mode 100644 index 00000000000..56a947d1de0 --- /dev/null +++ b/ui/app/routes/exec/task-group/task.js @@ -0,0 +1,11 @@ +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; + +export default Route.extend({ + store: service(), + + afterModel(model) { + // FIXME model isn’t a task, it’s just { task_name }… allocations!! + this.controllerFor('exec').send('setTask', model); + }, +}); diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index a357cc44668..1646568ba75 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -1,5 +1,5 @@ import { module, test } from 'qunit'; -import { currentURL } from '@ember/test-helpers'; +import { currentURL, settled } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import Exec from 'nomad-ui/tests/pages/exec'; @@ -84,7 +84,7 @@ module('Acceptance | exec', function(hooks) { assert.ok(Exec.taskGroups[0].chevron.isDown); }); - test('navigating to a task adds its name to the route', async function(assert) { + test('navigating to a task adds its name to the route and allows the command to be customised', async function(assert) { await Exec.visitJob({ job: this.job.id }); await Exec.taskGroups[0].click(); await Exec.taskGroups[0].tasks[0].click(); @@ -92,6 +92,24 @@ module('Acceptance | exec', function(hooks) { const taskGroup = this.job.task_groups.models[0]; const task = taskGroup.tasks.models[0]; + await settled(); + assert.equal(currentURL(), `/exec/${this.job.id}/${taskGroup.name}/${task.name}`); + + assert.equal( + window.execTerminal.buffer + .getLine(2) + .translateToString() + .trim(), + 'To start the session, customize your command, then hit ‘return’ to run.' + ); + + assert.equal( + window.execTerminal.buffer + .getLine(4) + .translateToString() + .trim(), + `$ nomad alloc exec -i -t -task ${task.name} ALLOCATION /bin/bash` + ); }); }); From 20b7b1450b51d20c3f954e21523129a199d10e5d Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 4 Feb 2020 22:44:15 -0600 Subject: [PATCH 025/150] Add handling for assigning/specifying allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is becoming quite sprawling but I believe having good acceptance tests will help me clean up when I’ve covered the myriad approaches. --- ui/app/controllers/exec.js | 16 +++++++-- ui/app/routes/exec/task-group/task.js | 28 ++++++++++++++-- ui/tests/acceptance/exec-test.js | 47 ++++++++++++++++++++++++--- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index a78e7f0c615..aacab3771e5 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -5,6 +5,8 @@ import { Terminal } from 'xterm'; export default Controller.extend({ system: service(), + queryParams: ['allocation'], + init() { this._super(...arguments); @@ -15,13 +17,23 @@ export default Controller.extend({ }, actions: { - setTask({ task_name }) { + setAllocationAndTask({ allocation, allocationSpecified, task_name }) { this.terminal.writeln(''); + + if (!allocationSpecified) { + this.terminal.writeln( + 'Multiple instances of this task are running. The allocation below was selected by random draw.' + ); + this.terminal.writeln(''); + } + this.terminal.writeln( 'To start the session, customize your command, then hit ‘return’ to run.' ); this.terminal.writeln(''); - this.terminal.writeln(`$ nomad alloc exec -i -t -task ${task_name} ALLOCATION /bin/bash`); + this.terminal.writeln( + `$ nomad alloc exec -i -t -task ${task_name} ${allocation.shortId} /bin/bash` + ); }, }, }); diff --git a/ui/app/routes/exec/task-group/task.js b/ui/app/routes/exec/task-group/task.js index 56a947d1de0..f3ca7fc2385 100644 --- a/ui/app/routes/exec/task-group/task.js +++ b/ui/app/routes/exec/task-group/task.js @@ -4,8 +4,32 @@ import Route from '@ember/routing/route'; export default Route.extend({ store: service(), + model({ task_name }) { + const allocationQueryParam = this.paramsFor('exec').allocation; + + return this.modelFor('exec').allocations.then(allocations => { + if (allocationQueryParam) { + return { + allocation: allocations.findBy('shortId', allocationQueryParam), + allocationSpecified: true, + task_name, + }; + } else { + return { + allocation: allocations.objectAt(0), + allocationSpecified: false, + task_name, + }; + } + }); + }, + afterModel(model) { - // FIXME model isn’t a task, it’s just { task_name }… allocations!! - this.controllerFor('exec').send('setTask', model); + // FIXME model doesn’t have a task, just a task_name + this.controllerFor('exec').send('setAllocationAndTask', model); + }, + + setupController(controller, { allocation }) { + controller.setProperties({ allocation }); }, }); diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 1646568ba75..1497e14d94b 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -12,8 +12,8 @@ module('Acceptance | exec', function(hooks) { server.create('agent'); server.create('node'); - this.job = server.create('job', { createAllocations: false }); - server.create('allocation', 'withTaskWithPorts', { clientStatus: 'running' }); + this.job = server.create('job', { groupsCount: 2 }); + // server.create('allocation', 'withTaskWithPorts', { clientStatus: 'running' }); }); test('/exec/:job should show the region, namespace, and job name', async function(assert) { @@ -84,13 +84,17 @@ module('Acceptance | exec', function(hooks) { assert.ok(Exec.taskGroups[0].chevron.isDown); }); - test('navigating to a task adds its name to the route and allows the command to be customised', async function(assert) { + test('navigating to a task adds its name to the route, chooses an allocation, and allows the command to be customised', async function(assert) { await Exec.visitJob({ job: this.job.id }); await Exec.taskGroups[0].click(); await Exec.taskGroups[0].tasks[0].click(); const taskGroup = this.job.task_groups.models[0]; const task = taskGroup.tasks.models[0]; + const allocation = this.server.db.allocations.findBy({ + jobId: this.job.id, + taskGroup: taskGroup.name, + }); await settled(); @@ -101,15 +105,50 @@ module('Acceptance | exec', function(hooks) { .getLine(2) .translateToString() .trim(), + 'Multiple instances of this task are running. The allocation below was selected by random draw.' + ); + + assert.equal( + window.execTerminal.buffer + .getLine(4) + .translateToString() + .trim(), 'To start the session, customize your command, then hit ‘return’ to run.' ); + assert.equal( + window.execTerminal.buffer + .getLine(6) + .translateToString() + .trim(), + `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]} /bin/bash` + ); + }); + + test('an allocation can be specified', async function(assert) { + const taskGroup = this.job.task_groups.models[0]; + const task = taskGroup.tasks.models[0]; + const allocations = this.server.db.allocations.where({ + jobId: this.job.id, + taskGroup: taskGroup.name, + }); + const allocation = allocations[allocations.length - 1]; + + await Exec.visitTask({ + job: this.job.id, + task_group: taskGroup.name, + task_name: task.name, + allocation: allocation.id.split('-')[0], + }); + + await settled(); + assert.equal( window.execTerminal.buffer .getLine(4) .translateToString() .trim(), - `$ nomad alloc exec -i -t -task ${task.name} ALLOCATION /bin/bash` + `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]} /bin/bash` ); }); }); From 417f31710d8a740783df5e993ba75f784d6af02d Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 4 Feb 2020 23:29:22 -0600 Subject: [PATCH 026/150] Add foreground text colouring ANSI escapes: wild times --- ui/app/controllers/exec.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index aacab3771e5..eaa0e03acf9 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -13,6 +13,8 @@ export default Controller.extend({ this.terminal = new Terminal(); window.execTerminal = this.terminal; // FIXME tragique, for acceptance tests…? + // Sets the foreground colour to Structure’s ui-gray-400 + this.terminal.write('\x1b[1;38;2;142;150;163m'); this.terminal.writeln('Select a task to start your session.'); }, @@ -31,9 +33,12 @@ export default Controller.extend({ 'To start the session, customize your command, then hit ‘return’ to run.' ); this.terminal.writeln(''); - this.terminal.writeln( - `$ nomad alloc exec -i -t -task ${task_name} ${allocation.shortId} /bin/bash` - ); + this.terminal.write(`$ nomad alloc exec -i -t -task ${task_name} ${allocation.shortId} `); + + // Sets the foreground colour to white + this.terminal.write('\x1b[0m'); + + this.terminal.write('/bin/bash'); }, }, }); From 9cbf6dc3df0a17b59df6fc2679db514e042f9b3c Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 4 Feb 2020 23:29:54 -0600 Subject: [PATCH 027/150] Add note about wrapping task names in faux command --- ui/app/controllers/exec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index eaa0e03acf9..d8de439b146 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -34,6 +34,7 @@ export default Controller.extend({ ); this.terminal.writeln(''); this.terminal.write(`$ nomad alloc exec -i -t -task ${task_name} ${allocation.shortId} `); + // FIXME task names might need quotes…? // Sets the foreground colour to white this.terminal.write('\x1b[0m'); From b255650f127002577dbc5db56b766c425399775a Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 4 Feb 2020 23:41:51 -0600 Subject: [PATCH 028/150] Change typeface to match design --- ui/app/controllers/exec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index d8de439b146..832d3fddcfe 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -10,11 +10,12 @@ export default Controller.extend({ init() { this._super(...arguments); - this.terminal = new Terminal(); + // FIXME this hardcoding of a font is questionable, but can the monospace be determined from the CSS font stack? 🤔 + this.terminal = new Terminal({ fontFamily: 'SF Mono', fontWeight: '400' }); window.execTerminal = this.terminal; // FIXME tragique, for acceptance tests…? // Sets the foreground colour to Structure’s ui-gray-400 - this.terminal.write('\x1b[1;38;2;142;150;163m'); + this.terminal.write('\x1b[38;2;142;150;163m'); this.terminal.writeln('Select a task to start your session.'); }, From 4a698230eecc46ededb41ab4c182d9462d790b70 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 4 Feb 2020 23:48:30 -0600 Subject: [PATCH 029/150] Add note about sidebar allocation mismatches --- ui/app/templates/components/task-group-parent.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/templates/components/task-group-parent.hbs b/ui/app/templates/components/task-group-parent.hbs index 6c4b6024f62..5281b0efba0 100644 --- a/ui/app/templates/components/task-group-parent.hbs +++ b/ui/app/templates/components/task-group-parent.hbs @@ -5,7 +5,7 @@ {{#if isOpen}}
          {{#each taskGroup.tasks as |task|}} - {{!-- FIXME this needs to include namespace etc query parameters --}} + {{!-- FIXME this needs to include namespace etc query parameters AND be blocked for wrong allocations etc --}} {{#link-to "exec.task-group.task" taskGroup.job.name taskGroup.name task.name class="task-item" data-test-task=true}}
          From fbc4ce15626fd6ca4a89145a4fa1ebf57b4ee2e5 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 5 Feb 2020 12:03:09 -0600 Subject: [PATCH 030/150] Remove obsolete test setup --- ui/tests/acceptance/exec-test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 1497e14d94b..7c2f4c96b09 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -13,7 +13,6 @@ module('Acceptance | exec', function(hooks) { server.create('node'); this.job = server.create('job', { groupsCount: 2 }); - // server.create('allocation', 'withTaskWithPorts', { clientStatus: 'running' }); }); test('/exec/:job should show the region, namespace, and job name', async function(assert) { From fd3d37962bf2d751e7396257cd446ac4e8ddb313 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 5 Feb 2020 16:16:45 -0600 Subject: [PATCH 031/150] Add preliminary incoming socket communication --- ui/app/controllers/exec.js | 23 +++++++++++- ui/app/routes/exec/task-group/task.js | 27 +++++++------- ui/app/services/sockets.js | 9 +++++ ui/tests/acceptance/exec-test.js | 54 +++++++++++++++++++++++++++ ui/tests/pages/exec.js | 15 +++++++- 5 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 ui/app/services/sockets.js diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 832d3fddcfe..1b4b2bac17f 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -3,6 +3,7 @@ import Controller from '@ember/controller'; import { Terminal } from 'xterm'; export default Controller.extend({ + sockets: service(), system: service(), queryParams: ['allocation'], @@ -20,7 +21,7 @@ export default Controller.extend({ }, actions: { - setAllocationAndTask({ allocation, allocationSpecified, task_name }) { + setTaskState({ allocationSpecified, taskState }) { this.terminal.writeln(''); if (!allocationSpecified) { @@ -34,13 +35,31 @@ export default Controller.extend({ 'To start the session, customize your command, then hit ‘return’ to run.' ); this.terminal.writeln(''); - this.terminal.write(`$ nomad alloc exec -i -t -task ${task_name} ${allocation.shortId} `); + this.terminal.write( + `$ nomad alloc exec -i -t -task ${taskState.name} ${taskState.allocation.shortId} ` + ); // FIXME task names might need quotes…? // Sets the foreground colour to white this.terminal.write('\x1b[0m'); this.terminal.write('/bin/bash'); + + this.terminal.onKey(e => { + if (e.domEvent.key === 'Enter') { + this.openAndConnectSocket(taskState); + this.terminal.writeln(''); + } + }); }, }, + + openAndConnectSocket(taskState) { + this.socket = this.sockets.getTaskStateSocket(taskState); + + this.socket.onmessage = e => { + const json = JSON.parse(e.data); + this.terminal.write(atob(json.stdout.data)); + }; + }, }); diff --git a/ui/app/routes/exec/task-group/task.js b/ui/app/routes/exec/task-group/task.js index f3ca7fc2385..941153080bb 100644 --- a/ui/app/routes/exec/task-group/task.js +++ b/ui/app/routes/exec/task-group/task.js @@ -8,28 +8,27 @@ export default Route.extend({ const allocationQueryParam = this.paramsFor('exec').allocation; return this.modelFor('exec').allocations.then(allocations => { + let allocation; + if (allocationQueryParam) { - return { - allocation: allocations.findBy('shortId', allocationQueryParam), - allocationSpecified: true, - task_name, - }; + allocation = allocations.findBy('shortId', allocationQueryParam); } else { - return { - allocation: allocations.objectAt(0), - allocationSpecified: false, - task_name, - }; + allocation = allocations.objectAt(0); } + + return { + allocation, + allocationSpecified: allocationQueryParam ? true : false, + taskState: allocation.states.find(state => state.name === task_name), // FIXME is this enough? + }; }); }, afterModel(model) { - // FIXME model doesn’t have a task, just a task_name - this.controllerFor('exec').send('setAllocationAndTask', model); + this.controllerFor('exec').send('setTaskState', model); }, - setupController(controller, { allocation }) { - controller.setProperties({ allocation }); + setupController(controller, { allocation, taskState }) { + controller.setProperties({ allocation, taskState }); }, }); diff --git a/ui/app/services/sockets.js b/ui/app/services/sockets.js new file mode 100644 index 00000000000..7db157573f1 --- /dev/null +++ b/ui/app/services/sockets.js @@ -0,0 +1,9 @@ +import Service from '@ember/service'; + +export default Service.extend({ + getTaskStateSocket(taskState) { + return new WebSocket( + `ws://localhost:4200/v1/client/allocation/${taskState.allocation.id}/exec?task=${taskState.name}&tty=true&command=%5B%22%2Fbin%2Fbash%22%5D` + ); + }, +}); diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 7c2f4c96b09..e35f02fc3b4 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -2,6 +2,7 @@ import { module, test } from 'qunit'; import { currentURL, settled } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; +import Service from '@ember/service'; import Exec from 'nomad-ui/tests/pages/exec'; module('Acceptance | exec', function(hooks) { @@ -150,4 +151,57 @@ module('Acceptance | exec', function(hooks) { `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]} /bin/bash` ); }); + + test('running the command opens the socket for reading', async function(assert) { + const mockSocket = {}; + + const mockSockets = Service.extend({ + getTaskStateSocket(taskState) { + assert.equal(taskState.name, task.name); + assert.equal(taskState.allocation.id, allocation.id); + assert.step('Socket built'); + + return mockSocket; + }, + }); + + this.owner.register('service:sockets', mockSockets); + + const taskGroup = this.job.task_groups.models[0]; + const task = taskGroup.tasks.models[0]; + const allocations = this.server.db.allocations.where({ + jobId: this.job.id, + taskGroup: taskGroup.name, + }); + const allocation = allocations[allocations.length - 1]; + + await Exec.visitTask({ + job: this.job.id, + task_group: taskGroup.name, + task_name: task.name, + allocation: allocation.id.split('-')[0], + }); + + await settled(); + + await Exec.terminal.pressEnter(); + + await settled(); + + assert.verifySteps(['Socket built']); + + mockSocket.onmessage({ + data: '{"stdout":{"data":"c2gtMy4yJCA="}}', + }); + + await settled(); + + assert.equal( + window.execTerminal.buffer + .getLine(5) + .translateToString() + .trim(), + 'sh-3.2$' + ); + }); }); diff --git a/ui/tests/pages/exec.js b/ui/tests/pages/exec.js index 05eb204e572..182cb9ebca6 100644 --- a/ui/tests/pages/exec.js +++ b/ui/tests/pages/exec.js @@ -1,4 +1,12 @@ -import { clickable, collection, create, hasClass, text, visitable } from 'ember-cli-page-object'; +import { + clickable, + collection, + create, + hasClass, + text, + triggerable, + visitable, +} from 'ember-cli-page-object'; export default create({ visitJob: visitable('/exec/:job'), @@ -25,4 +33,9 @@ export default create({ name: text(), }), }), + + terminal: { + scope: '.xterm-helper-textarea', + pressEnter: triggerable('keydown', '', { eventProperties: { keyCode: 13 } }), + }, }); From 56a70805fb5ddb6447cf4e93a52be450f96e1a28 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 5 Feb 2020 17:57:22 -0600 Subject: [PATCH 032/150] Add preliminary outgoing socket communication --- ui/app/controllers/exec.js | 7 ++++++- ui/tests/acceptance/exec-test.js | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 1b4b2bac17f..dfb3f980fee 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -45,10 +45,15 @@ export default Controller.extend({ this.terminal.write('/bin/bash'); + let socketOpen = false; + this.terminal.onKey(e => { - if (e.domEvent.key === 'Enter') { + if (e.domEvent.key === 'Enter' && !socketOpen) { this.openAndConnectSocket(taskState); this.terminal.writeln(''); + socketOpen = true; + } else { + this.socket.send(JSON.stringify({ stdin: { data: btoa(e.key) } })); } }); }, diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index e35f02fc3b4..90d29eb37a1 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -152,8 +152,14 @@ module('Acceptance | exec', function(hooks) { ); }); - test('running the command opens the socket for reading', async function(assert) { - const mockSocket = {}; + test('running the command opens the socket for reading/writing', async function(assert) { + const mockSocket = new Object({ + sent: [], + + send(message) { + this.sent.push(message); + }, + }); const mockSockets = Service.extend({ getTaskStateSocket(taskState) { @@ -185,7 +191,6 @@ module('Acceptance | exec', function(hooks) { await settled(); await Exec.terminal.pressEnter(); - await settled(); assert.verifySteps(['Socket built']); @@ -203,5 +208,10 @@ module('Acceptance | exec', function(hooks) { .trim(), 'sh-3.2$' ); + + await Exec.terminal.pressEnter(); + await settled(); + + assert.deepEqual(mockSocket.sent, ['{"stdin":{"data":"DQ=="}}']); }); }); From f7397f1b94b03701357266d50f6f312894e59b8f Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 5 Feb 2020 18:03:55 -0600 Subject: [PATCH 033/150] Change sent message format for readability --- ui/tests/acceptance/exec-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 90d29eb37a1..27544616154 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -196,7 +196,7 @@ module('Acceptance | exec', function(hooks) { assert.verifySteps(['Socket built']); mockSocket.onmessage({ - data: '{"stdout":{"data":"c2gtMy4yJCA="}}', + data: `{"stdout":{"data":"${btoa('sh-3.2$')}"}}`, }); await settled(); From 14b1883ece2ad8910135d9d69329bc860998bdc0 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 6 Feb 2020 09:36:22 -0600 Subject: [PATCH 034/150] Change terminal font to monospace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I checked the default and it was this: “courier-new, courier, monospace” So a stack-ish thing does seem to be supported. I’m using monospace now because that’s what’s used elsewhere in the application, like job definitions. --- ui/app/controllers/exec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index dfb3f980fee..07abebb8cca 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -11,8 +11,7 @@ export default Controller.extend({ init() { this._super(...arguments); - // FIXME this hardcoding of a font is questionable, but can the monospace be determined from the CSS font stack? 🤔 - this.terminal = new Terminal({ fontFamily: 'SF Mono', fontWeight: '400' }); + this.terminal = new Terminal({ fontFamily: 'monospace', fontWeight: '400' }); window.execTerminal = this.terminal; // FIXME tragique, for acceptance tests…? // Sets the foreground colour to Structure’s ui-gray-400 From f4dd4df47b3ff7d3ebd64f9e5060c407a244f954 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 6 Feb 2020 14:52:16 -0600 Subject: [PATCH 035/150] Add ability to customise exec command --- ui/app/controllers/exec.js | 40 ++++++++++++++++++++------ ui/app/services/sockets.js | 6 ++-- ui/tests/acceptance/exec-test.js | 49 ++++++++++++++++++++++++++++++-- 3 files changed, 82 insertions(+), 13 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 07abebb8cca..f48536b66ae 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -8,6 +8,10 @@ export default Controller.extend({ queryParams: ['allocation'], + command: '/bin/bash', + socketOpen: false, + taskState: null, + init() { this._super(...arguments); @@ -21,6 +25,8 @@ export default Controller.extend({ actions: { setTaskState({ allocationSpecified, taskState }) { + this.taskState = taskState; + this.terminal.writeln(''); if (!allocationSpecified) { @@ -44,26 +50,42 @@ export default Controller.extend({ this.terminal.write('/bin/bash'); - let socketOpen = false; - this.terminal.onKey(e => { - if (e.domEvent.key === 'Enter' && !socketOpen) { - this.openAndConnectSocket(taskState); - this.terminal.writeln(''); - socketOpen = true; + if (this.socketOpen) { + this.handleSocketKeyEvent(e); } else { - this.socket.send(JSON.stringify({ stdin: { data: btoa(e.key) } })); + this.handleCommandKeyEvent(e); } }); + + this.terminal.simulateCommandKeyEvent = this.handleCommandKeyEvent.bind(this); }, }, - openAndConnectSocket(taskState) { - this.socket = this.sockets.getTaskStateSocket(taskState); + openAndConnectSocket() { + this.socket = this.sockets.getTaskStateSocket(this.taskState, this.command); this.socket.onmessage = e => { const json = JSON.parse(e.data); this.terminal.write(atob(json.stdout.data)); }; }, + + handleCommandKeyEvent(e) { + if (e.domEvent.key === 'Enter') { + this.openAndConnectSocket(); + this.terminal.writeln(''); + this.socketOpen = true; + } else if (e.domEvent.key === 'Backspace') { + this.terminal.write('\b \b'); + this.command = this.command.slice(0, -1); + } else if (e.key.length > 0) { + this.terminal.write(e.key); + this.command = `${this.command}${e.key}`; + } + }, + + handleSocketKeyEvent(e) { + this.socket.send(JSON.stringify({ stdin: { data: btoa(e.key) } })); + }, }); diff --git a/ui/app/services/sockets.js b/ui/app/services/sockets.js index 7db157573f1..5eda5a51211 100644 --- a/ui/app/services/sockets.js +++ b/ui/app/services/sockets.js @@ -1,9 +1,11 @@ import Service from '@ember/service'; export default Service.extend({ - getTaskStateSocket(taskState) { + getTaskStateSocket(taskState, command) { return new WebSocket( - `ws://localhost:4200/v1/client/allocation/${taskState.allocation.id}/exec?task=${taskState.name}&tty=true&command=%5B%22%2Fbin%2Fbash%22%5D` + `ws://localhost:4200/v1/client/allocation/${taskState.allocation.id}` + + `/exec?task=${taskState.name}&tty=true` + + `&command=${encodeURIComponent(`["${command}"]`)}` ); }, }); diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 27544616154..2554193b434 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -84,7 +84,7 @@ module('Acceptance | exec', function(hooks) { assert.ok(Exec.taskGroups[0].chevron.isDown); }); - test('navigating to a task adds its name to the route, chooses an allocation, and allows the command to be customised', async function(assert) { + test('navigating to a task adds its name to the route, chooses an allocation, and assigns a default command', async function(assert) { await Exec.visitJob({ job: this.job.id }); await Exec.taskGroups[0].click(); await Exec.taskGroups[0].tasks[0].click(); @@ -162,9 +162,12 @@ module('Acceptance | exec', function(hooks) { }); const mockSockets = Service.extend({ - getTaskStateSocket(taskState) { + getTaskStateSocket(taskState, command) { assert.equal(taskState.name, task.name); assert.equal(taskState.allocation.id, allocation.id); + + assert.equal(command, '/bin/bash'); + assert.step('Socket built'); return mockSocket; @@ -214,4 +217,46 @@ module('Acceptance | exec', function(hooks) { assert.deepEqual(mockSocket.sent, ['{"stdin":{"data":"DQ=="}}']); }); + + test('the command can be customised', async function(assert) { + const mockSocket = new Object({ + sent: [], + + send(message) { + this.sent.push(message); + }, + }); + + const mockSockets = Service.extend({ + getTaskStateSocket(taskState, command) { + assert.equal(command, '/bin/sh'); + + assert.step('Socket built'); + + return mockSocket; + }, + }); + + this.owner.register('service:sockets', mockSockets); + + await Exec.visitJob({ job: this.job.id }); + await Exec.taskGroups[0].click(); + await Exec.taskGroups[0].tasks[0].click(); + + await settled(); + + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandKeyEvent({ key: '/', domEvent: {} }); + await window.execTerminal.simulateCommandKeyEvent({ key: 's', domEvent: {} }); + await window.execTerminal.simulateCommandKeyEvent({ key: 'h', domEvent: {} }); + + await Exec.terminal.pressEnter(); + await settled(); + + assert.verifySteps(['Socket built']); + }); }); From 2524fb68f428530ae873c5ec6333341b43d5f27a Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 6 Feb 2020 15:19:31 -0600 Subject: [PATCH 036/150] Add guard against backspacing beyond command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is edging toward overkill for acceptance tests. I’ll perhaps extract some kind of utility or class to handle connecting the editing of the command string to a terminal so it can be more fully tested. --- ui/app/controllers/exec.js | 6 ++++-- ui/tests/acceptance/exec-test.js | 30 +++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index f48536b66ae..14f2c999697 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -77,8 +77,10 @@ export default Controller.extend({ this.terminal.writeln(''); this.socketOpen = true; } else if (e.domEvent.key === 'Backspace') { - this.terminal.write('\b \b'); - this.command = this.command.slice(0, -1); + if (this.command.length > 0) { + this.terminal.write('\b \b'); + this.command = this.command.slice(0, -1); + } } else if (e.key.length > 0) { this.terminal.write(e.key); this.command = `${this.command}${e.key}`; diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 2554193b434..c2f1b288e99 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -229,7 +229,7 @@ module('Acceptance | exec', function(hooks) { const mockSockets = Service.extend({ getTaskStateSocket(taskState, command) { - assert.equal(command, '/bin/sh'); + assert.equal(command, '/sh'); assert.step('Socket built'); @@ -243,13 +243,41 @@ module('Acceptance | exec', function(hooks) { await Exec.taskGroups[0].click(); await Exec.taskGroups[0].tasks[0].click(); + const taskGroup = this.job.task_groups.models[0]; + const task = taskGroup.tasks.models[0]; + const allocation = this.server.db.allocations.findBy({ + jobId: this.job.id, + taskGroup: taskGroup.name, + }); + await settled(); + // Delete /bash + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + + // Delete /bin and try to go beyond await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + + await settled(); + + assert.equal( + window.execTerminal.buffer + .getLine(6) + .translateToString() + .trim(), + `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]}` + ); + await window.execTerminal.simulateCommandKeyEvent({ key: '/', domEvent: {} }); await window.execTerminal.simulateCommandKeyEvent({ key: 's', domEvent: {} }); await window.execTerminal.simulateCommandKeyEvent({ key: 'h', domEvent: {} }); From a1960d25ffabd7079e5b19c6922518414be60308 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 6 Feb 2020 16:00:58 -0600 Subject: [PATCH 037/150] Add placeholder socket for Mirage context --- ui/app/services/sockets.js | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/ui/app/services/sockets.js b/ui/app/services/sockets.js index 5eda5a51211..cb5cd02470c 100644 --- a/ui/app/services/sockets.js +++ b/ui/app/services/sockets.js @@ -1,11 +1,30 @@ import Service from '@ember/service'; +import config from 'nomad-ui/config/environment'; export default Service.extend({ getTaskStateSocket(taskState, command) { - return new WebSocket( - `ws://localhost:4200/v1/client/allocation/${taskState.allocation.id}` + - `/exec?task=${taskState.name}&tty=true` + - `&command=${encodeURIComponent(`["${command}"]`)}` - ); + const mirageEnabled = + config['ember-cli-mirage'] && config['ember-cli-mirage'].enabled !== false; + + if (mirageEnabled) { + return new Object({ + messageDisplayed: false, + + send(e) { + if (!this.messageDisplayed) { + this.messageDisplayed = true; + this.onmessage({ data: `{"stdout":{"data":"${btoa('unsupported in Mirage\n\r')}"}}` }); + } else { + this.onmessage({ data: e.replace('stdin', 'stdout') }); + } + }, + }); + } else { + return new WebSocket( + `ws://localhost:4200/v1/client/allocation/${taskState.allocation.id}` + + `/exec?task=${taskState.name}&tty=true` + + `&command=${encodeURIComponent(`["${command}"]`)}` + ); + } }, }); From 91a5d389bf9a8a0d4af401cdf072cc9a5ae0f4ab Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 7 Feb 2020 16:04:26 -0600 Subject: [PATCH 038/150] Add preliminary handling for socket closure --- ui/app/controllers/exec.js | 7 +++++++ ui/tests/acceptance/exec-test.js | 13 ++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 14f2c999697..4f15f934596 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -69,6 +69,13 @@ export default Controller.extend({ const json = JSON.parse(e.data); this.terminal.write(atob(json.stdout.data)); }; + + this.socket.onclose = e => { + this.terminal.writeln(''); + this.terminal.write('\x1b[38;2;142;150;163m'); + this.terminal.writeln('The connection has closed.'); + // FIXME interpret different close events + }; }, handleCommandKeyEvent(e) { diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index c2f1b288e99..19562b86aa1 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -152,7 +152,7 @@ module('Acceptance | exec', function(hooks) { ); }); - test('running the command opens the socket for reading/writing', async function(assert) { + test('running the command opens the socket for reading/writing and detects it closing', async function(assert) { const mockSocket = new Object({ sent: [], @@ -216,6 +216,17 @@ module('Acceptance | exec', function(hooks) { await settled(); assert.deepEqual(mockSocket.sent, ['{"stdin":{"data":"DQ=="}}']); + + await mockSocket.onclose(); + await settled(); + + assert.equal( + window.execTerminal.buffer + .getLine(6) + .translateToString() + .trim(), + 'The connection has closed.' + ); }); test('the command can be customised', async function(assert) { From 183c86a21326580fc93c8f70bb84dfacd13d4043 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 10 Feb 2020 09:10:38 -0600 Subject: [PATCH 039/150] Fix handling of emoji --- ui/app/controllers/exec.js | 17 +++++++++++++++-- ui/tests/acceptance/exec-test.js | 4 ++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 4f15f934596..d059e173088 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -1,6 +1,8 @@ import { inject as service } from '@ember/service'; import Controller from '@ember/controller'; import { Terminal } from 'xterm'; +import base64js from 'base64-js'; +import { TextDecoderLite, TextEncoderLite } from 'text-encoder-lite'; export default Controller.extend({ sockets: service(), @@ -67,7 +69,7 @@ export default Controller.extend({ this.socket.onmessage = e => { const json = JSON.parse(e.data); - this.terminal.write(atob(json.stdout.data)); + this.terminal.write(decodeString(json.stdout.data)); }; this.socket.onclose = e => { @@ -95,6 +97,17 @@ export default Controller.extend({ }, handleSocketKeyEvent(e) { - this.socket.send(JSON.stringify({ stdin: { data: btoa(e.key) } })); + this.socket.send(JSON.stringify({ stdin: { data: encodeString(e.key) } })); + // FIXME this is untested, difficult with restriction on simulating key events }, }); + +function encodeString(string) { + var encoded = new TextEncoderLite('utf-8').encode(string); + return base64js.fromByteArray(encoded); +} + +function decodeString(b64String) { + var uint8array = base64js.toByteArray(b64String); + return new TextDecoderLite('utf-8').decode(uint8array); +} diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 19562b86aa1..d190e83b225 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -199,7 +199,7 @@ module('Acceptance | exec', function(hooks) { assert.verifySteps(['Socket built']); mockSocket.onmessage({ - data: `{"stdout":{"data":"${btoa('sh-3.2$')}"}}`, + data: `{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}`, }); await settled(); @@ -209,7 +209,7 @@ module('Acceptance | exec', function(hooks) { .getLine(5) .translateToString() .trim(), - 'sh-3.2$' + 'sh-3.2 🥳$' ); await Exec.terminal.pressEnter(); From 6d90caba510f50823d175c47804cede9086b5c2f Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 10 Feb 2020 14:25:35 -0600 Subject: [PATCH 040/150] Change allocation choice to ensure task is present --- ui/app/routes/exec/task-group/task.js | 4 +++- ui/tests/acceptance/exec-test.js | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ui/app/routes/exec/task-group/task.js b/ui/app/routes/exec/task-group/task.js index 941153080bb..f6c106cc17b 100644 --- a/ui/app/routes/exec/task-group/task.js +++ b/ui/app/routes/exec/task-group/task.js @@ -13,7 +13,9 @@ export default Route.extend({ if (allocationQueryParam) { allocation = allocations.findBy('shortId', allocationQueryParam); } else { - allocation = allocations.objectAt(0); + allocation = allocations.find(allocation => + allocation.states.mapBy('name').includes(task_name) + ); } return { diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index d190e83b225..93586c21f05 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -91,10 +91,11 @@ module('Acceptance | exec', function(hooks) { const taskGroup = this.job.task_groups.models[0]; const task = taskGroup.tasks.models[0]; - const allocation = this.server.db.allocations.findBy({ - jobId: this.job.id, - taskGroup: taskGroup.name, + + const taskStates = this.server.db.taskStates.where({ + name: task.name, }); + const allocationId = taskStates.find(ts => ts.allocationId).allocationId; await settled(); @@ -121,7 +122,7 @@ module('Acceptance | exec', function(hooks) { .getLine(6) .translateToString() .trim(), - `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]} /bin/bash` + `$ nomad alloc exec -i -t -task ${task.name} ${allocationId.split('-')[0]} /bin/bash` ); }); From 80c4c298136a27dd719d846480ea6786f07bff2c Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 10 Feb 2020 15:22:26 -0600 Subject: [PATCH 041/150] Change sidebar links to maybe open in new window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This isn’t exactly correct and is only manually tested at the moment 😐 --- ui/app/components/task-group-parent.js | 9 +++++++ ui/app/controllers/exec.js | 2 +- .../components/task-group-parent.hbs | 24 +++++++++++++------ ui/app/templates/exec.hbs | 2 +- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/ui/app/components/task-group-parent.js b/ui/app/components/task-group-parent.js index 1f010f2d166..aaf403c08d3 100644 --- a/ui/app/components/task-group-parent.js +++ b/ui/app/components/task-group-parent.js @@ -30,5 +30,14 @@ export default Component.extend({ toggleOpen() { this.toggleProperty('clickedOpen'); }, + + openInNewWindow(job, taskGroup, task) { + // FIXME any way to construct this path via the router? + window.open( + `/ui/exec/${job.name}/${taskGroup.name}/${task.name}`, + '_blank', + 'width=973,height=490,location=1' + ); + }, }, }); diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index d059e173088..730f2698970 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -84,7 +84,7 @@ export default Controller.extend({ if (e.domEvent.key === 'Enter') { this.openAndConnectSocket(); this.terminal.writeln(''); - this.socketOpen = true; + this.set('socketOpen', true); } else if (e.domEvent.key === 'Backspace') { if (this.command.length > 0) { this.terminal.write('\b \b'); diff --git a/ui/app/templates/components/task-group-parent.hbs b/ui/app/templates/components/task-group-parent.hbs index 5281b0efba0..56c882d1cca 100644 --- a/ui/app/templates/components/task-group-parent.hbs +++ b/ui/app/templates/components/task-group-parent.hbs @@ -6,13 +6,23 @@
            {{#each taskGroup.tasks as |task|}} {{!-- FIXME this needs to include namespace etc query parameters AND be blocked for wrong allocations etc --}} - {{#link-to "exec.task-group.task" taskGroup.job.name taskGroup.name task.name class="task-item" data-test-task=true}} -
            -
            -
            {{task.name}}
            -
            - {{x-icon "exit"}} - {{/link-to}} + {{!-- also this should be when a task has been chosen, not when the socket is open… right? or…? --}} + {{#if openInNewWindow}} + +
            +
            +
            {{task.name}}
            +
            + {{x-icon "exit"}} +
            + {{else}} + {{#link-to "exec.task-group.task" taskGroup.job.name taskGroup.name task.name class="task-item" data-test-task=true}} +
            +
            +
            {{task.name}}
            +
            + {{/link-to}} + {{/if}} {{/each}}
          {{/if}} \ No newline at end of file diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index e834624894b..a52a893e882 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -36,7 +36,7 @@
            {{#each model.taskGroups as |taskGroup|}}
          • - {{task-group-parent taskGroup=taskGroup}} + {{task-group-parent taskGroup=taskGroup openInNewWindow=socketOpen}}
          • {{/each}}
          From f1cdf22ae532fce39e3d50ad3536eb60d062faa2 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 11 Feb 2020 09:42:07 -0600 Subject: [PATCH 042/150] Add indicator for active task --- ui/app/controllers/exec.js | 2 +- ui/app/styles/components/exec.scss | 15 +++++++++++++++ ui/app/templates/components/task-group-parent.hbs | 10 ++++++++++ ui/app/templates/exec.hbs | 2 +- ui/tests/acceptance/exec-test.js | 2 ++ ui/tests/pages/exec.js | 2 ++ 6 files changed, 31 insertions(+), 2 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 730f2698970..929fd227eaf 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -27,7 +27,7 @@ export default Controller.extend({ actions: { setTaskState({ allocationSpecified, taskState }) { - this.taskState = taskState; + this.set('taskState', taskState); this.terminal.writeln(''); diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index 1eba28d6ffc..1e32e00c799 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -53,6 +53,7 @@ display: flex; align-items: center; height: 100%; + position: relative; } .border { @@ -62,6 +63,16 @@ height: 100%; } + .active { + position: absolute; + top: 6.5px; + left: -9.75px; + + stroke: $ui-gray-900; + stroke-width: 5px; + fill: white; + } + .task-label { padding-bottom: 1px; } @@ -84,6 +95,10 @@ &:hover { background-color: $ui-gray-800; border-radius: 4px; + + .active { + stroke: $ui-gray-800; + } } } } diff --git a/ui/app/templates/components/task-group-parent.hbs b/ui/app/templates/components/task-group-parent.hbs index 56c882d1cca..9e976aa6d17 100644 --- a/ui/app/templates/components/task-group-parent.hbs +++ b/ui/app/templates/components/task-group-parent.hbs @@ -12,6 +12,11 @@
          {{task.name}}
          + {{#if (eq task activeTaskState.task)}} + + + + {{/if}}
          {{x-icon "exit"}} @@ -20,6 +25,11 @@
          {{task.name}}
          + {{#if (eq task activeTaskState.task)}} + + + + {{/if}}
          {{/link-to}} {{/if}} diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index a52a893e882..50053a281a7 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -36,7 +36,7 @@
            {{#each model.taskGroups as |taskGroup|}}
          • - {{task-group-parent taskGroup=taskGroup openInNewWindow=socketOpen}} + {{task-group-parent taskGroup=taskGroup openInNewWindow=socketOpen activeTaskState=taskState}}
          • {{/each}}
          diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 93586c21f05..d2c43814f3b 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -52,6 +52,7 @@ module('Acceptance | exec', function(hooks) { await Exec.taskGroups[0].click(); assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); + assert.notOk(Exec.taskGroups[0].tasks[0].isActive); assert.ok(Exec.taskGroups[0].chevron.isDown); await Exec.taskGroups[0].click(); @@ -100,6 +101,7 @@ module('Acceptance | exec', function(hooks) { await settled(); assert.equal(currentURL(), `/exec/${this.job.id}/${taskGroup.name}/${task.name}`); + assert.ok(Exec.taskGroups[0].tasks[0].isActive); assert.equal( window.execTerminal.buffer diff --git a/ui/tests/pages/exec.js b/ui/tests/pages/exec.js index 182cb9ebca6..3194504e811 100644 --- a/ui/tests/pages/exec.js +++ b/ui/tests/pages/exec.js @@ -3,6 +3,7 @@ import { collection, create, hasClass, + isPresent, text, triggerable, visitable, @@ -31,6 +32,7 @@ export default create({ tasks: collection('[data-test-task]', { name: text(), + isActive: isPresent('[data-test-task-active]'), }), }), From 73668c5c1036a4096e97818bbdea03cfc8a3986f Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 11 Feb 2020 09:53:36 -0600 Subject: [PATCH 043/150] Move exec-specific component to subdirectory This is in preparation for extracting the item component. --- ui/app/components/{ => exec}/task-group-parent.js | 0 ui/app/templates/components/{ => exec}/task-group-parent.hbs | 0 ui/app/templates/exec.hbs | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename ui/app/components/{ => exec}/task-group-parent.js (100%) rename ui/app/templates/components/{ => exec}/task-group-parent.hbs (100%) diff --git a/ui/app/components/task-group-parent.js b/ui/app/components/exec/task-group-parent.js similarity index 100% rename from ui/app/components/task-group-parent.js rename to ui/app/components/exec/task-group-parent.js diff --git a/ui/app/templates/components/task-group-parent.hbs b/ui/app/templates/components/exec/task-group-parent.hbs similarity index 100% rename from ui/app/templates/components/task-group-parent.hbs rename to ui/app/templates/components/exec/task-group-parent.hbs diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index 50053a281a7..179367add87 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -36,7 +36,7 @@
            {{#each model.taskGroups as |taskGroup|}}
          • - {{task-group-parent taskGroup=taskGroup openInNewWindow=socketOpen activeTaskState=taskState}} + {{exec/task-group-parent taskGroup=taskGroup openInNewWindow=socketOpen activeTaskState=taskState}}
          • {{/each}}
          From 568a860e7745ac1c2182b32c89481547821b8304 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 11 Feb 2020 10:01:59 -0600 Subject: [PATCH 044/150] Extract component for task items --- ui/app/components/exec/task-contents.js | 5 +++++ .../components/exec/task-contents.hbs | 12 +++++++++++ .../components/exec/task-group-parent.hbs | 21 ++----------------- 3 files changed, 19 insertions(+), 19 deletions(-) create mode 100644 ui/app/components/exec/task-contents.js create mode 100644 ui/app/templates/components/exec/task-contents.hbs diff --git a/ui/app/components/exec/task-contents.js b/ui/app/components/exec/task-contents.js new file mode 100644 index 00000000000..4798652642b --- /dev/null +++ b/ui/app/components/exec/task-contents.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui/app/templates/components/exec/task-contents.hbs b/ui/app/templates/components/exec/task-contents.hbs new file mode 100644 index 00000000000..a99547499e9 --- /dev/null +++ b/ui/app/templates/components/exec/task-contents.hbs @@ -0,0 +1,12 @@ +
          +
          +
          {{task.name}}
          + {{#if active}} + + + + {{/if}} +
          +{{#if openInNewWindow}} + {{x-icon "exit"}} +{{/if}} diff --git a/ui/app/templates/components/exec/task-group-parent.hbs b/ui/app/templates/components/exec/task-group-parent.hbs index 9e976aa6d17..9885f14c444 100644 --- a/ui/app/templates/components/exec/task-group-parent.hbs +++ b/ui/app/templates/components/exec/task-group-parent.hbs @@ -9,28 +9,11 @@ {{!-- also this should be when a task has been chosen, not when the socket is open… right? or…? --}} {{#if openInNewWindow}} -
          -
          -
          {{task.name}}
          - {{#if (eq task activeTaskState.task)}} - - - - {{/if}} -
          - {{x-icon "exit"}} + {{exec/task-contents task=task active=(eq task activeTaskState.task) openInNewWindow=openInNewWindow}}
          {{else}} {{#link-to "exec.task-group.task" taskGroup.job.name taskGroup.name task.name class="task-item" data-test-task=true}} -
          -
          -
          {{task.name}}
          - {{#if (eq task activeTaskState.task)}} - - - - {{/if}} -
          + {{exec/task-contents task=task active=(eq task activeTaskState.task) openInNewWindow=openInNewWindow}} {{/link-to}} {{/if}} {{/each}} From 80f598daa934fa6c98e19f837fcb3a1ad8f52eb3 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 11 Feb 2020 16:34:38 -0600 Subject: [PATCH 045/150] Add temporary logging of close events --- ui/app/controllers/exec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 929fd227eaf..fbe3ef18da8 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -76,6 +76,8 @@ export default Controller.extend({ this.terminal.writeln(''); this.terminal.write('\x1b[38;2;142;150;163m'); this.terminal.writeln('The connection has closed.'); + // eslint-disable-next-line + console.log('Socket close event', e); // FIXME interpret different close events }; }, From ad1f10b91cdfc0c6ca8f7ea9c776c9df853405da Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 11 Feb 2020 16:50:07 -0600 Subject: [PATCH 046/150] Fix lint error --- ui/tests/acceptance/exec-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index d2c43814f3b..997e56adc42 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -202,7 +202,7 @@ module('Acceptance | exec', function(hooks) { assert.verifySteps(['Socket built']); mockSocket.onmessage({ - data: `{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}`, + data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}', }); await settled(); From 07330e545c163c4a97c1eacf1c4c038ce45602bd Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 11 Feb 2020 16:50:33 -0600 Subject: [PATCH 047/150] Add temporary fixed Faker seed --- ui/mirage/faker.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/mirage/faker.js b/ui/mirage/faker.js index 2baad57fb4c..4100a105ba3 100644 --- a/ui/mirage/faker.js +++ b/ui/mirage/faker.js @@ -11,6 +11,9 @@ if (config.environment !== 'test' || searchIncludesSeed) { } else { faker.seed(1); } +} else { + // FIXME + faker.seed(3); } export default faker; From 71801d021bb1625f1371abd707f290f7e2297e07 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 12 Feb 2020 14:47:08 -0600 Subject: [PATCH 048/150] Extract ANSI colour-changing codes --- ui/app/controllers/exec.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index fbe3ef18da8..ce99b69bc20 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -4,6 +4,9 @@ import { Terminal } from 'xterm'; import base64js from 'base64-js'; import { TextDecoderLite, TextEncoderLite } from 'text-encoder-lite'; +const ANSI_UI_GRAY_400 = '\x1b[38;2;142;150;163m'; +const ANSI_WHITE = '\x1b[0m'; + export default Controller.extend({ sockets: service(), system: service(), @@ -21,7 +24,7 @@ export default Controller.extend({ window.execTerminal = this.terminal; // FIXME tragique, for acceptance tests…? // Sets the foreground colour to Structure’s ui-gray-400 - this.terminal.write('\x1b[38;2;142;150;163m'); + this.terminal.write(ANSI_UI_GRAY_400); this.terminal.writeln('Select a task to start your session.'); }, @@ -48,7 +51,7 @@ export default Controller.extend({ // FIXME task names might need quotes…? // Sets the foreground colour to white - this.terminal.write('\x1b[0m'); + this.terminal.write(ANSI_WHITE); this.terminal.write('/bin/bash'); @@ -74,7 +77,7 @@ export default Controller.extend({ this.socket.onclose = e => { this.terminal.writeln(''); - this.terminal.write('\x1b[38;2;142;150;163m'); + this.terminal.write(ANSI_UI_GRAY_400); this.terminal.writeln('The connection has closed.'); // eslint-disable-next-line console.log('Socket close event', e); From 5b0ff52dc3dd3c70d83ff25b1ffd6fd581490ffb Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 12 Feb 2020 15:40:04 -0600 Subject: [PATCH 049/150] Add escaping for faux CLI task names --- ui/app/controllers/exec.js | 7 +++++-- ui/app/utils/escape-task-name.js | 3 +++ ui/tests/acceptance/exec-test.js | 8 +++++++- ui/tests/unit/utils/escape-task-name-test.js | 10 ++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 ui/app/utils/escape-task-name.js create mode 100644 ui/tests/unit/utils/escape-task-name-test.js diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index ce99b69bc20..be52431343a 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -1,5 +1,7 @@ import { inject as service } from '@ember/service'; import Controller from '@ember/controller'; +import escapeTaskName from 'nomad-ui/utils/escape-task-name'; + import { Terminal } from 'xterm'; import base64js from 'base64-js'; import { TextDecoderLite, TextEncoderLite } from 'text-encoder-lite'; @@ -46,9 +48,10 @@ export default Controller.extend({ ); this.terminal.writeln(''); this.terminal.write( - `$ nomad alloc exec -i -t -task ${taskState.name} ${taskState.allocation.shortId} ` + `$ nomad alloc exec -i -t -task ${escapeTaskName(taskState.name)} ${ + taskState.allocation.shortId + } ` ); - // FIXME task names might need quotes…? // Sets the foreground colour to white this.terminal.write(ANSI_WHITE); diff --git a/ui/app/utils/escape-task-name.js b/ui/app/utils/escape-task-name.js new file mode 100644 index 00000000000..878698fe6c4 --- /dev/null +++ b/ui/app/utils/escape-task-name.js @@ -0,0 +1,3 @@ +export default function escapeTaskName(taskName) { + return taskName.replace(/[^a-zA-Z0-9,._+@%/-]/g, '\\$&'); +} diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 997e56adc42..f9fd75375e0 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -137,6 +137,12 @@ module('Acceptance | exec', function(hooks) { }); const allocation = allocations[allocations.length - 1]; + const oldName = task.name; + task.name = 'spaced name!'; + task.save(); + + const taskState = this.server.db.taskStates.update({ name: oldName }, { name: 'spaced name!' }); + await Exec.visitTask({ job: this.job.id, task_group: taskGroup.name, @@ -151,7 +157,7 @@ module('Acceptance | exec', function(hooks) { .getLine(4) .translateToString() .trim(), - `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]} /bin/bash` + `$ nomad alloc exec -i -t -task spaced\\ name\\! ${allocation.id.split('-')[0]} /bin/bash` ); }); diff --git a/ui/tests/unit/utils/escape-task-name-test.js b/ui/tests/unit/utils/escape-task-name-test.js new file mode 100644 index 00000000000..b34ab2430ea --- /dev/null +++ b/ui/tests/unit/utils/escape-task-name-test.js @@ -0,0 +1,10 @@ +import escapeTaskName from 'nomad-ui/utils/escape-task-name'; +import { module, test } from 'qunit'; + +module('Unit | Utility | escape-task-name', function() { + test('it escapes task names for the faux exec CLI', function(assert) { + assert.equal(escapeTaskName('plain'), 'plain'); + assert.equal(escapeTaskName('a space'), 'a\\ space'); + assert.equal(escapeTaskName('dollar $ign'), 'dollar\\ \\$ign'); + }); +}); From 0ffe645af689ddbc2a46c35da110877a5fc68f6f Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 12 Feb 2020 15:49:07 -0600 Subject: [PATCH 050/150] Add link to origin of escaping regular expression --- ui/app/utils/escape-task-name.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/utils/escape-task-name.js b/ui/app/utils/escape-task-name.js index 878698fe6c4..fff0e4741f8 100644 --- a/ui/app/utils/escape-task-name.js +++ b/ui/app/utils/escape-task-name.js @@ -1,3 +1,4 @@ export default function escapeTaskName(taskName) { + // Regular expression is taken from here: https://stackoverflow.com/a/20053121 return taskName.replace(/[^a-zA-Z0-9,._+@%/-]/g, '\\$&'); } From 16497056cdfae4e99c99728f7a1f1f06920836d3 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 12 Feb 2020 16:26:22 -0600 Subject: [PATCH 051/150] Remove superfluous comments These are superseded by the variable names. --- ui/app/controllers/exec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index be52431343a..1de60afbba6 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -25,7 +25,6 @@ export default Controller.extend({ this.terminal = new Terminal({ fontFamily: 'monospace', fontWeight: '400' }); window.execTerminal = this.terminal; // FIXME tragique, for acceptance tests…? - // Sets the foreground colour to Structure’s ui-gray-400 this.terminal.write(ANSI_UI_GRAY_400); this.terminal.writeln('Select a task to start your session.'); }, @@ -53,7 +52,6 @@ export default Controller.extend({ } ` ); - // Sets the foreground colour to white this.terminal.write(ANSI_WHITE); this.terminal.write('/bin/bash'); From 844de6868f286c8365065e3d02c785f39311551f Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 13 Feb 2020 10:25:34 -0600 Subject: [PATCH 052/150] Fix sidebar for multi-line task names --- ui/app/styles/components/exec.scss | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index 1e32e00c799..bd1cbf08b02 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -31,7 +31,7 @@ font-size: inherit; width: 100%; text-align: left; - padding-left: 0; + padding: 6px 0 5px 0; .icon { padding: 3px 3px 0 0; @@ -57,15 +57,14 @@ } .border { - padding-left: 8px; - padding-right: 5px; + position: absolute; border-left: 1px solid $ui-gray-700; height: 100%; } .active { position: absolute; - top: 6.5px; + top: 7.5px; left: -9.75px; stroke: $ui-gray-900; @@ -74,7 +73,7 @@ } .task-label { - padding-bottom: 1px; + padding: 6px 0 5px 13px; } .icon { @@ -89,7 +88,6 @@ .toggle-button, .task-item { - height: 32px; font-weight: 500; &:hover { From f117e1c9060cf3001f1914d50d83d54c7d473720 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 13 Feb 2020 10:32:39 -0600 Subject: [PATCH 053/150] Fix sidebar for multi-line task group names --- ui/app/styles/components/exec.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index bd1cbf08b02..dc71667b225 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -25,15 +25,19 @@ } .toggle-button { + position: relative; background: transparent; border: 0; color: white; font-size: inherit; + line-height: 1.5; width: 100%; text-align: left; - padding: 6px 0 5px 0; + padding: 6px 0 5px 17px; .icon { + position: absolute; + left: 0; padding: 3px 3px 0 0; margin-left: -3px; } From 967b35260e6defdfaaa5633b55bd93879d3ac163 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 13 Feb 2020 10:50:02 -0600 Subject: [PATCH 054/150] Add FIXME about keeping opened groups open --- ui/app/components/exec/task-group-parent.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/components/exec/task-group-parent.js b/ui/app/components/exec/task-group-parent.js index aaf403c08d3..dd87ffb3d59 100644 --- a/ui/app/components/exec/task-group-parent.js +++ b/ui/app/components/exec/task-group-parent.js @@ -6,6 +6,7 @@ import { or } from '@ember/object/computed'; export default Component.extend({ router: service(), + // FIXME this shouldn’t auto-close when switching to another task group if it has been manually opened already isOpen: or('clickedOpen', 'currentRouteIsThisTaskGroup'), currentRouteIsThisTaskGroup: computed('router.currentRoute', function() { From 66551eafb47597ee5f81197d6e4bb3465c26fc5b Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 13 Feb 2020 13:46:46 -0600 Subject: [PATCH 055/150] Add rejection of inactive task states --- ui/app/routes/exec/task-group/task.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/app/routes/exec/task-group/task.js b/ui/app/routes/exec/task-group/task.js index f6c106cc17b..6876a4f0e4b 100644 --- a/ui/app/routes/exec/task-group/task.js +++ b/ui/app/routes/exec/task-group/task.js @@ -14,7 +14,10 @@ export default Route.extend({ allocation = allocations.findBy('shortId', allocationQueryParam); } else { allocation = allocations.find(allocation => - allocation.states.mapBy('name').includes(task_name) + allocation.states + .filterBy('isActive') + .mapBy('name') + .includes(task_name) ); } From 5a86b9d4411b2846fcc1ca9bfdf999fa39ff79eb Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 13 Feb 2020 16:14:10 -0600 Subject: [PATCH 056/150] Add text colour change when switching tasks --- ui/app/controllers/exec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 1de60afbba6..31baed0a6fe 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -33,6 +33,7 @@ export default Controller.extend({ setTaskState({ allocationSpecified, taskState }) { this.set('taskState', taskState); + this.terminal.write(ANSI_UI_GRAY_400); this.terminal.writeln(''); if (!allocationSpecified) { From 4c2713c1fb2ef5533e1e4c2dbed24bd1f919a78d Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 13 Feb 2020 17:03:32 -0600 Subject: [PATCH 057/150] Extract class for terminal command-editing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This has felt necessary for a while but evaded me until now. This should allow me to me more easily implement/test a workaround for the lack of reverse-wraparound mode as I describe here: https://github.com/xtermjs/xterm.js/issues/2716 I’ll likely extract another class that connects the exec WebSocket to the terminal so everything’s not concentrated in the controller and more edge cases can be easily exercised in automated tests. --- ui/app/controllers/exec.js | 38 ++++++------------- .../exec-command-editor-xterm-adapter.js | 30 +++++++++++++++ 2 files changed, 42 insertions(+), 26 deletions(-) create mode 100644 ui/app/utils/classes/exec-command-editor-xterm-adapter.js diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 31baed0a6fe..a5d07da323a 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -1,6 +1,7 @@ import { inject as service } from '@ember/service'; import Controller from '@ember/controller'; import escapeTaskName from 'nomad-ui/utils/escape-task-name'; +import ExecCommandEditorXtermAdapter from 'nomad-ui/utils/classes/exec-command-editor-xterm-adapter'; import { Terminal } from 'xterm'; import base64js from 'base64-js'; @@ -25,6 +26,7 @@ export default Controller.extend({ this.terminal = new Terminal({ fontFamily: 'monospace', fontWeight: '400' }); window.execTerminal = this.terminal; // FIXME tragique, for acceptance tests…? + // this.terminal.write('\x1b[?45h;'); this.terminal.write(ANSI_UI_GRAY_400); this.terminal.writeln('Select a task to start your session.'); }, @@ -57,20 +59,20 @@ export default Controller.extend({ this.terminal.write('/bin/bash'); - this.terminal.onKey(e => { - if (this.socketOpen) { - this.handleSocketKeyEvent(e); - } else { - this.handleCommandKeyEvent(e); - } - }); + new ExecCommandEditorXtermAdapter(this.terminal, this.openAndConnectSocket.bind(this)); - this.terminal.simulateCommandKeyEvent = this.handleCommandKeyEvent.bind(this); + // FIXME + // this.terminal.simulateCommandKeyEvent = this.handleCommandKeyEvent.bind(this); }, }, - openAndConnectSocket() { - this.socket = this.sockets.getTaskStateSocket(this.taskState, this.command); + openAndConnectSocket(command) { + this.set('socketOpen', true); + this.socket = this.sockets.getTaskStateSocket(this.taskState, command); + + this.terminal.onKey(e => { + this.handleSocketKeyEvent(e); + }); this.socket.onmessage = e => { const json = JSON.parse(e.data); @@ -87,22 +89,6 @@ export default Controller.extend({ }; }, - handleCommandKeyEvent(e) { - if (e.domEvent.key === 'Enter') { - this.openAndConnectSocket(); - this.terminal.writeln(''); - this.set('socketOpen', true); - } else if (e.domEvent.key === 'Backspace') { - if (this.command.length > 0) { - this.terminal.write('\b \b'); - this.command = this.command.slice(0, -1); - } - } else if (e.key.length > 0) { - this.terminal.write(e.key); - this.command = `${this.command}${e.key}`; - } - }, - handleSocketKeyEvent(e) { this.socket.send(JSON.stringify({ stdin: { data: encodeString(e.key) } })); // FIXME this is untested, difficult with restriction on simulating key events diff --git a/ui/app/utils/classes/exec-command-editor-xterm-adapter.js b/ui/app/utils/classes/exec-command-editor-xterm-adapter.js new file mode 100644 index 00000000000..507d95d8c8e --- /dev/null +++ b/ui/app/utils/classes/exec-command-editor-xterm-adapter.js @@ -0,0 +1,30 @@ +export default class ExecCommandEditorXtermAdapter { + constructor(terminal, setCommandCallback) { + this.terminal = terminal; + this.setCommandCallback = setCommandCallback; + + this.command = '/bin/bash'; + + this.keyListener = terminal.onKey(e => { + this.handleKeyEvent(e); + }); + + terminal.simulateCommandKeyEvent = this.handleKeyEvent.bind(this); + } + + handleKeyEvent(e) { + if (e.domEvent.key === 'Enter') { + this.terminal.writeln(''); + this.setCommandCallback(this.command); + this.keyListener.dispose(); + } else if (e.domEvent.key === 'Backspace') { + if (this.command.length > 0) { + this.terminal.write('\b \b'); + this.command = this.command.slice(0, -1); + } + } else if (e.key.length > 0) { + this.terminal.write(e.key); + this.command = `${this.command}${e.key}`; + } + } +} From 0f6a6fafd2079484f8ad47aaef119d060f7c5ec9 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 14 Feb 2020 12:55:04 -0600 Subject: [PATCH 058/150] Change const to let I keep forgetting this is the local pattern. --- ui/app/controllers/exec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index a5d07da323a..856ada7db60 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -75,7 +75,7 @@ export default Controller.extend({ }); this.socket.onmessage = e => { - const json = JSON.parse(e.data); + let json = JSON.parse(e.data); this.terminal.write(decodeString(json.stdout.data)); }; From 123eaabab60cb68fad8224f0fac8a102dce26bc8 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 14 Feb 2020 13:39:40 -0600 Subject: [PATCH 059/150] Add failing test for backspace wrapping This leaves me well-positioned to add more extensive testing for the command-editing, including copy/paste and maybe even ^U? This pattern can also apply to an extracted socket- terminal adapter. --- ui/app/controllers/exec.js | 6 ++- .../exec-command-editor-xterm-adapter.js | 4 +- .../exec-command-editor-xterm-adapter-test.js | 53 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 856ada7db60..fc7a3c8cac9 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -59,7 +59,11 @@ export default Controller.extend({ this.terminal.write('/bin/bash'); - new ExecCommandEditorXtermAdapter(this.terminal, this.openAndConnectSocket.bind(this)); + new ExecCommandEditorXtermAdapter( + this.terminal, + this.openAndConnectSocket.bind(this), + '/bin/bash' + ); // FIXME // this.terminal.simulateCommandKeyEvent = this.handleCommandKeyEvent.bind(this); diff --git a/ui/app/utils/classes/exec-command-editor-xterm-adapter.js b/ui/app/utils/classes/exec-command-editor-xterm-adapter.js index 507d95d8c8e..268216938b6 100644 --- a/ui/app/utils/classes/exec-command-editor-xterm-adapter.js +++ b/ui/app/utils/classes/exec-command-editor-xterm-adapter.js @@ -1,9 +1,9 @@ export default class ExecCommandEditorXtermAdapter { - constructor(terminal, setCommandCallback) { + constructor(terminal, setCommandCallback, command) { this.terminal = terminal; this.setCommandCallback = setCommandCallback; - this.command = '/bin/bash'; + this.command = command; this.keyListener = terminal.onKey(e => { this.handleKeyEvent(e); diff --git a/ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js b/ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js new file mode 100644 index 00000000000..7ffc9f93959 --- /dev/null +++ b/ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js @@ -0,0 +1,53 @@ +import ExecCommandEditorXtermAdapter from 'nomad-ui/utils/classes/exec-command-editor-xterm-adapter'; +import { setupRenderingTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import { render, settled } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import { Terminal } from 'xterm'; + +module('Integration | Utility | exec-command-editor-xterm-adapter', function(hooks) { + setupRenderingTest(hooks); + + test('it can wrap to a previous line while backspacing', async function(assert) { + let done = assert.async(); + + await render(hbs` +
          + `); + + const terminal = new Terminal({ cols: 10 }); + terminal.open(document.getElementById('terminal')); + + terminal.write('/bin/long-command'); + + new ExecCommandEditorXtermAdapter( + terminal, + command => { + assert.equal(command, '/bin/long'); + done(); + }, + '/bin/long-command' + ); + + await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + + await settled(); + + assert.equal( + terminal.buffer + .getLine(0) + .translateToString() + .trim(), + '/bin/long' + ); + + await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Enter' } }); + }); +}); From c87c365e5b5eac04e06031d878dc16d2a1cb771d Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 18 Feb 2020 10:14:19 -0500 Subject: [PATCH 060/150] Add wraparound backspacing for command-editing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This seems to work…?! --- .../exec-command-editor-xterm-adapter.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ui/app/utils/classes/exec-command-editor-xterm-adapter.js b/ui/app/utils/classes/exec-command-editor-xterm-adapter.js index 268216938b6..301962baa30 100644 --- a/ui/app/utils/classes/exec-command-editor-xterm-adapter.js +++ b/ui/app/utils/classes/exec-command-editor-xterm-adapter.js @@ -1,3 +1,9 @@ +const ANSI_MOVE_CURSOR_UP_ONE = '\x1b[1A'; + +function ansi_move_cursor_right_n(n) { + return `\x1b[${n}C`; +} + export default class ExecCommandEditorXtermAdapter { constructor(terminal, setCommandCallback, command) { this.terminal = terminal; @@ -19,7 +25,16 @@ export default class ExecCommandEditorXtermAdapter { this.keyListener.dispose(); } else if (e.domEvent.key === 'Backspace') { if (this.command.length > 0) { - this.terminal.write('\b \b'); + const cursorX = this.terminal.buffer.cursorX; + + if (cursorX === 0) { + this.terminal.write(ANSI_MOVE_CURSOR_UP_ONE); + this.terminal.write(ansi_move_cursor_right_n(this.terminal.cols)); + this.terminal.write(' '); + } else { + this.terminal.write('\b \b'); + } + this.command = this.command.slice(0, -1); } } else if (e.key.length > 0) { From e0f2e60095a327dd43dac441e53ebe156deb83f9 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 18 Feb 2020 11:15:36 -0500 Subject: [PATCH 061/150] Change new window icon to not shrink --- ui/app/styles/components/exec.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index dc71667b225..e0a7dc538b0 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -81,11 +81,13 @@ } .icon { - display: none; + visibility: hidden; + width: 16px; + flex-shrink: 0; } &:hover .icon { - display: block; + visibility: visible; } } } From a47f06e6cedf0fc7c7ffacf118d52c3a9281f290 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 18 Feb 2020 11:37:15 -0500 Subject: [PATCH 062/150] Extract adapter between Xterm.js and exec socket Similarly to the command-editing adapter, this will facilitate more involved tests than the acceptance tests. --- ui/app/controllers/exec.js | 36 +-------------- .../classes/exec-socket-xterm-adapter.js | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 34 deletions(-) create mode 100644 ui/app/utils/classes/exec-socket-xterm-adapter.js diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index fc7a3c8cac9..24d6a13689e 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -2,10 +2,9 @@ import { inject as service } from '@ember/service'; import Controller from '@ember/controller'; import escapeTaskName from 'nomad-ui/utils/escape-task-name'; import ExecCommandEditorXtermAdapter from 'nomad-ui/utils/classes/exec-command-editor-xterm-adapter'; +import ExecSocketXtermAdapter from 'nomad-ui/utils/classes/exec-socket-xterm-adapter'; import { Terminal } from 'xterm'; -import base64js from 'base64-js'; -import { TextDecoderLite, TextEncoderLite } from 'text-encoder-lite'; const ANSI_UI_GRAY_400 = '\x1b[38;2;142;150;163m'; const ANSI_WHITE = '\x1b[0m'; @@ -74,37 +73,6 @@ export default Controller.extend({ this.set('socketOpen', true); this.socket = this.sockets.getTaskStateSocket(this.taskState, command); - this.terminal.onKey(e => { - this.handleSocketKeyEvent(e); - }); - - this.socket.onmessage = e => { - let json = JSON.parse(e.data); - this.terminal.write(decodeString(json.stdout.data)); - }; - - this.socket.onclose = e => { - this.terminal.writeln(''); - this.terminal.write(ANSI_UI_GRAY_400); - this.terminal.writeln('The connection has closed.'); - // eslint-disable-next-line - console.log('Socket close event', e); - // FIXME interpret different close events - }; - }, - - handleSocketKeyEvent(e) { - this.socket.send(JSON.stringify({ stdin: { data: encodeString(e.key) } })); - // FIXME this is untested, difficult with restriction on simulating key events + new ExecSocketXtermAdapter(this.terminal, this.socket); }, }); - -function encodeString(string) { - var encoded = new TextEncoderLite('utf-8').encode(string); - return base64js.fromByteArray(encoded); -} - -function decodeString(b64String) { - var uint8array = base64js.toByteArray(b64String); - return new TextDecoderLite('utf-8').decode(uint8array); -} diff --git a/ui/app/utils/classes/exec-socket-xterm-adapter.js b/ui/app/utils/classes/exec-socket-xterm-adapter.js new file mode 100644 index 00000000000..67c6fc0776b --- /dev/null +++ b/ui/app/utils/classes/exec-socket-xterm-adapter.js @@ -0,0 +1,44 @@ +const ANSI_UI_GRAY_400 = '\x1b[38;2;142;150;163m'; + +import base64js from 'base64-js'; +import { TextDecoderLite, TextEncoderLite } from 'text-encoder-lite'; + +export default class ExecSocketXtermAdapter { + constructor(terminal, socket) { + this.terminal = terminal; + this.socket = socket; + + terminal.onKey(e => { + this.handleKeyEvent(e); + }); + + socket.onmessage = e => { + let json = JSON.parse(e.data); + terminal.write(decodeString(json.stdout.data)); + }; + + socket.onclose = e => { + this.terminal.writeln(''); + this.terminal.write(ANSI_UI_GRAY_400); + this.terminal.writeln('The connection has closed.'); + // eslint-disable-next-line + console.log('Socket close event', e); + // FIXME interpret different close events + }; + } + + handleKeyEvent(e) { + this.socket.send(JSON.stringify({ stdin: { data: encodeString(e.key) } })); + // FIXME this is untested, difficult with restriction on simulating key events + } +} + +function encodeString(string) { + var encoded = new TextEncoderLite('utf-8').encode(string); + return base64js.fromByteArray(encoded); +} + +function decodeString(b64String) { + var uint8array = base64js.toByteArray(b64String); + return new TextDecoderLite('utf-8').decode(uint8array); +} From 21e3d5aa90ff087ad202e6de0e3d5e3b2c2120e8 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 18 Feb 2020 15:02:20 -0500 Subject: [PATCH 063/150] Change sidebar to not shrink --- ui/app/styles/components/exec.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index e0a7dc538b0..1a7d190bd0c 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -13,6 +13,7 @@ color: white; padding: 16px; width: 200px; // these px sizes… 🧐 + flex-shrink: 0; .title { text-transform: uppercase; From 8d65a27e3bac5d33653a6e5c16b461e7f88e1ee7 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 19 Feb 2020 13:22:17 -0500 Subject: [PATCH 064/150] Change new window icon to always take up space This prevents the layout/text-wrapping of the sidebar from changing when a connexion has been opened. --- ui/app/styles/components/exec.scss | 2 +- ui/app/templates/components/exec/task-contents.hbs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index 1a7d190bd0c..85f761729a8 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -87,7 +87,7 @@ flex-shrink: 0; } - &:hover .icon { + &:hover .icon.show-on-hover { visibility: visible; } } diff --git a/ui/app/templates/components/exec/task-contents.hbs b/ui/app/templates/components/exec/task-contents.hbs index a99547499e9..d80d7e3bcf7 100644 --- a/ui/app/templates/components/exec/task-contents.hbs +++ b/ui/app/templates/components/exec/task-contents.hbs @@ -7,6 +7,4 @@ {{/if}}
          -{{#if openInNewWindow}} - {{x-icon "exit"}} -{{/if}} +{{x-icon "exit" class=(if openInNewWindow "show-on-hover")}} From 840f2ff48171dc0008bc8d5c730c1ce6dde31869 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 20 Feb 2020 10:18:38 -0500 Subject: [PATCH 065/150] Add tooltip for new window icon --- ui/app/templates/components/exec/task-contents.hbs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/app/templates/components/exec/task-contents.hbs b/ui/app/templates/components/exec/task-contents.hbs index d80d7e3bcf7..012688610ea 100644 --- a/ui/app/templates/components/exec/task-contents.hbs +++ b/ui/app/templates/components/exec/task-contents.hbs @@ -7,4 +7,10 @@ {{/if}}
        -{{x-icon "exit" class=(if openInNewWindow "show-on-hover")}} +{{#if openInNewWindow}} + + {{x-icon "exit" class="show-on-hover"}} + +{{else}} + {{x-icon "exit"}} +{{/if}} \ No newline at end of file From 7fdf5eaf431ccbb752c607af2bbcb1c5488f92ff Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 21 Feb 2020 14:14:52 -0500 Subject: [PATCH 066/150] Add TTY-sizing message when socket opens This is a step toward properly informing the remote end of the size of the terminal. Without it, line-wrapping in the exec session is broken, among other things. This will need to be expanded to handle when the window is resized. --- ui/app/utils/classes/exec-socket-xterm-adapter.js | 11 +++++++++++ ui/tests/acceptance/exec-test.js | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/ui/app/utils/classes/exec-socket-xterm-adapter.js b/ui/app/utils/classes/exec-socket-xterm-adapter.js index 67c6fc0776b..c61ef54ab7e 100644 --- a/ui/app/utils/classes/exec-socket-xterm-adapter.js +++ b/ui/app/utils/classes/exec-socket-xterm-adapter.js @@ -8,6 +8,11 @@ export default class ExecSocketXtermAdapter { this.terminal = terminal; this.socket = socket; + socket.onopen = () => { + this.sendTtySize(); + }; + // FIXME the onKey handler also shouldn’t be set until this happens + terminal.onKey(e => { this.handleKeyEvent(e); }); @@ -27,6 +32,12 @@ export default class ExecSocketXtermAdapter { }; } + sendTtySize() { + this.socket.send( + JSON.stringify({ tty_size: { width: this.terminal.cols, height: this.terminal.rows } }) + ); + } + handleKeyEvent(e) { this.socket.send(JSON.stringify({ stdin: { data: encodeString(e.key) } })); // FIXME this is untested, difficult with restriction on simulating key events diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index f9fd75375e0..7f38991f15a 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -204,6 +204,7 @@ module('Acceptance | exec', function(hooks) { await Exec.terminal.pressEnter(); await settled(); + mockSocket.onopen(); assert.verifySteps(['Socket built']); @@ -224,7 +225,10 @@ module('Acceptance | exec', function(hooks) { await Exec.terminal.pressEnter(); await settled(); - assert.deepEqual(mockSocket.sent, ['{"stdin":{"data":"DQ=="}}']); + assert.deepEqual(mockSocket.sent, [ + '{"tty_size":{"width":228,"height":24}}', + '{"stdin":{"data":"DQ=="}}', + ]); await mockSocket.onclose(); await settled(); From 5de6f89652b17431179be5b3fafa7768731b3075 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 21 Feb 2020 14:32:12 -0500 Subject: [PATCH 067/150] Add note about other formats for incoming messages --- ui/app/utils/classes/exec-socket-xterm-adapter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/utils/classes/exec-socket-xterm-adapter.js b/ui/app/utils/classes/exec-socket-xterm-adapter.js index c61ef54ab7e..bcb1f6dbd94 100644 --- a/ui/app/utils/classes/exec-socket-xterm-adapter.js +++ b/ui/app/utils/classes/exec-socket-xterm-adapter.js @@ -19,6 +19,7 @@ export default class ExecSocketXtermAdapter { socket.onmessage = e => { let json = JSON.parse(e.data); + // FIXME could be stderr, or stdout.close/sterr.close, or exited, or even {}! terminal.write(decodeString(json.stdout.data)); }; From 7e5bcee1c2c5c5f0066261213744721d324ffd56 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 2 Mar 2020 14:15:16 -0600 Subject: [PATCH 068/150] Add handling for window resizing --- ui/app/components/exec-terminal.js | 18 +++++++- .../classes/exec-socket-xterm-adapter.js | 4 ++ .../util/exec-socket-xterm-adapter-test.js | 42 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 ui/tests/integration/util/exec-socket-xterm-adapter-test.js diff --git a/ui/app/components/exec-terminal.js b/ui/app/components/exec-terminal.js index e9e00c50775..c62733584b5 100644 --- a/ui/app/components/exec-terminal.js +++ b/ui/app/components/exec-terminal.js @@ -1,15 +1,31 @@ import Component from '@ember/component'; import { FitAddon } from 'xterm-addon-fit'; +import $ from 'jquery'; export default Component.extend({ classNames: ['terminal-container'], didInsertElement() { - const fitAddon = new FitAddon(); + let fitAddon = new FitAddon(); + this.fitAddon = fitAddon; this.terminal.loadAddon(fitAddon); this.terminal.open(this.element.querySelector('.terminal')); fitAddon.fit(); + + this._windowResizeHandler = this.windowResizeHandler.bind(this); + $(window).on('resize', this._windowResizeHandler); + }, + + willDestroyElement() { + $(window).off('resize', this._windowResizeHandler); + }, + + windowResizeHandler(e) { + this.fitAddon.fit(); + if (this.terminal.resized) { + this.terminal.resized(e); + } }, }); diff --git a/ui/app/utils/classes/exec-socket-xterm-adapter.js b/ui/app/utils/classes/exec-socket-xterm-adapter.js index bcb1f6dbd94..590080d577f 100644 --- a/ui/app/utils/classes/exec-socket-xterm-adapter.js +++ b/ui/app/utils/classes/exec-socket-xterm-adapter.js @@ -31,6 +31,10 @@ export default class ExecSocketXtermAdapter { console.log('Socket close event', e); // FIXME interpret different close events }; + + terminal.resized = () => { + this.sendTtySize(); + }; } sendTtySize() { diff --git a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js new file mode 100644 index 00000000000..37737c6c1ab --- /dev/null +++ b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js @@ -0,0 +1,42 @@ +import ExecSocketXtermAdapter from 'nomad-ui/utils/classes/exec-socket-xterm-adapter'; +import { setupRenderingTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import { find, render, settled } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import { Terminal } from 'xterm'; + +module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { + setupRenderingTest(hooks); + + test('resizing the window passes a resize message through the socket', async function(assert) { + let done = assert.async(); + + let terminal = new Terminal(); + this.set('terminal', terminal); + + await render(hbs` + {{exec-terminal terminal=terminal}} + `); + + terminal.write('/bin/long-command'); + + await settled(); + + const mockSocket = new Object({ + send(message) { + assert.deepEqual(message, JSON.stringify({ tty_size: { width: 138, height: 24 } })); + assert.equal(terminal.cols, 138); + done(); + }, + }); + + new ExecSocketXtermAdapter(terminal, mockSocket); + + let terminalElement = find('.terminal'); + terminalElement.style.width = '50%'; + // FIXME height doesn’t work yet + window.dispatchEvent(new Event('resize')); + + await settled(); + }); +}); From 551d37aefb5384167517f77d9641737261bef012 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 3 Mar 2020 10:11:11 -0600 Subject: [PATCH 069/150] Change more const to let --- ui/tests/acceptance/exec-test.js | 48 +++++++++---------- .../exec-command-editor-xterm-adapter-test.js | 2 +- .../util/exec-socket-xterm-adapter-test.js | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 7f38991f15a..23d836a31c4 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -18,7 +18,7 @@ module('Acceptance | exec', function(hooks) { test('/exec/:job should show the region, namespace, and job name', async function(assert) { server.create('namespace'); - const namespace = server.create('namespace'); + let namespace = server.create('namespace'); server.create('region', { id: 'global' }); server.create('region', { id: 'region-2' }); @@ -72,13 +72,13 @@ module('Acceptance | exec', function(hooks) { }); test('visiting a path with a task group should open the group by default', async function(assert) { - const taskGroup = this.job.task_groups.models[0]; + let taskGroup = this.job.task_groups.models[0]; await Exec.visitTaskGroup({ job: this.job.id, task_group: taskGroup.name }); assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); assert.ok(Exec.taskGroups[0].chevron.isDown); - const task = taskGroup.tasks.models[0]; + let task = taskGroup.tasks.models[0]; await Exec.visitTask({ job: this.job.id, task_group: taskGroup.name, task_name: task.name }); assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length); @@ -90,13 +90,13 @@ module('Acceptance | exec', function(hooks) { await Exec.taskGroups[0].click(); await Exec.taskGroups[0].tasks[0].click(); - const taskGroup = this.job.task_groups.models[0]; - const task = taskGroup.tasks.models[0]; + let taskGroup = this.job.task_groups.models[0]; + let task = taskGroup.tasks.models[0]; - const taskStates = this.server.db.taskStates.where({ + let taskStates = this.server.db.taskStates.where({ name: task.name, }); - const allocationId = taskStates.find(ts => ts.allocationId).allocationId; + let allocationId = taskStates.find(ts => ts.allocationId).allocationId; await settled(); @@ -129,19 +129,19 @@ module('Acceptance | exec', function(hooks) { }); test('an allocation can be specified', async function(assert) { - const taskGroup = this.job.task_groups.models[0]; - const task = taskGroup.tasks.models[0]; - const allocations = this.server.db.allocations.where({ + let taskGroup = this.job.task_groups.models[0]; + let task = taskGroup.tasks.models[0]; + let allocations = this.server.db.allocations.where({ jobId: this.job.id, taskGroup: taskGroup.name, }); - const allocation = allocations[allocations.length - 1]; + let allocation = allocations[allocations.length - 1]; - const oldName = task.name; + let oldName = task.name; task.name = 'spaced name!'; task.save(); - const taskState = this.server.db.taskStates.update({ name: oldName }, { name: 'spaced name!' }); + let taskState = this.server.db.taskStates.update({ name: oldName }, { name: 'spaced name!' }); await Exec.visitTask({ job: this.job.id, @@ -162,7 +162,7 @@ module('Acceptance | exec', function(hooks) { }); test('running the command opens the socket for reading/writing and detects it closing', async function(assert) { - const mockSocket = new Object({ + let mockSocket = new Object({ sent: [], send(message) { @@ -170,7 +170,7 @@ module('Acceptance | exec', function(hooks) { }, }); - const mockSockets = Service.extend({ + let mockSockets = Service.extend({ getTaskStateSocket(taskState, command) { assert.equal(taskState.name, task.name); assert.equal(taskState.allocation.id, allocation.id); @@ -185,13 +185,13 @@ module('Acceptance | exec', function(hooks) { this.owner.register('service:sockets', mockSockets); - const taskGroup = this.job.task_groups.models[0]; - const task = taskGroup.tasks.models[0]; - const allocations = this.server.db.allocations.where({ + let taskGroup = this.job.task_groups.models[0]; + let task = taskGroup.tasks.models[0]; + let allocations = this.server.db.allocations.where({ jobId: this.job.id, taskGroup: taskGroup.name, }); - const allocation = allocations[allocations.length - 1]; + let allocation = allocations[allocations.length - 1]; await Exec.visitTask({ job: this.job.id, @@ -243,7 +243,7 @@ module('Acceptance | exec', function(hooks) { }); test('the command can be customised', async function(assert) { - const mockSocket = new Object({ + let mockSocket = new Object({ sent: [], send(message) { @@ -251,7 +251,7 @@ module('Acceptance | exec', function(hooks) { }, }); - const mockSockets = Service.extend({ + let mockSockets = Service.extend({ getTaskStateSocket(taskState, command) { assert.equal(command, '/sh'); @@ -267,9 +267,9 @@ module('Acceptance | exec', function(hooks) { await Exec.taskGroups[0].click(); await Exec.taskGroups[0].tasks[0].click(); - const taskGroup = this.job.task_groups.models[0]; - const task = taskGroup.tasks.models[0]; - const allocation = this.server.db.allocations.findBy({ + let taskGroup = this.job.task_groups.models[0]; + let task = taskGroup.tasks.models[0]; + let allocation = this.server.db.allocations.findBy({ jobId: this.job.id, taskGroup: taskGroup.name, }); diff --git a/ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js b/ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js index 7ffc9f93959..a6ac3100cf6 100644 --- a/ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js +++ b/ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js @@ -15,7 +15,7 @@ module('Integration | Utility | exec-command-editor-xterm-adapter', function(hoo
        `); - const terminal = new Terminal({ cols: 10 }); + let terminal = new Terminal({ cols: 10 }); terminal.open(document.getElementById('terminal')); terminal.write('/bin/long-command'); diff --git a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js index 37737c6c1ab..a27dc978eb9 100644 --- a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js +++ b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js @@ -22,7 +22,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { await settled(); - const mockSocket = new Object({ + let mockSocket = new Object({ send(message) { assert.deepEqual(message, JSON.stringify({ tty_size: { width: 138, height: 24 } })); assert.equal(terminal.cols, 138); From 4ef3388b32db4118d7cdccaea7c866eabce3ea60 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 3 Mar 2020 10:14:42 -0600 Subject: [PATCH 070/150] Change documentation link to open in new tab --- ui/app/templates/exec.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index 179367add87..381913e73cd 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -25,7 +25,7 @@ {{model.name}}
      From e138e7115912c36d6d14a9d28648021ae0bb3db2 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 3 Mar 2020 10:17:32 -0600 Subject: [PATCH 071/150] Change var to let --- ui/app/utils/classes/exec-socket-xterm-adapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/utils/classes/exec-socket-xterm-adapter.js b/ui/app/utils/classes/exec-socket-xterm-adapter.js index 590080d577f..6bcb31e2507 100644 --- a/ui/app/utils/classes/exec-socket-xterm-adapter.js +++ b/ui/app/utils/classes/exec-socket-xterm-adapter.js @@ -50,11 +50,11 @@ export default class ExecSocketXtermAdapter { } function encodeString(string) { - var encoded = new TextEncoderLite('utf-8').encode(string); + let encoded = new TextEncoderLite('utf-8').encode(string); return base64js.fromByteArray(encoded); } function decodeString(b64String) { - var uint8array = base64js.toByteArray(b64String); + let uint8array = base64js.toByteArray(b64String); return new TextDecoderLite('utf-8').decode(uint8array); } From 83fef114ea31fdaf0dd8f444333bbf587aa3266f Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 3 Mar 2020 11:40:30 -0600 Subject: [PATCH 072/150] Fix bug with multiple socket-openings --- ui/app/controllers/exec.js | 6 +++- .../exec-command-editor-xterm-adapter.js | 4 +++ ui/tests/acceptance/exec-test.js | 35 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 24d6a13689e..072a52c5a34 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -58,7 +58,11 @@ export default Controller.extend({ this.terminal.write('/bin/bash'); - new ExecCommandEditorXtermAdapter( + if (this.commandEditorAdapter) { + this.commandEditorAdapter.destroy(); + } + + this.commandEditorAdapter = new ExecCommandEditorXtermAdapter( this.terminal, this.openAndConnectSocket.bind(this), '/bin/bash' diff --git a/ui/app/utils/classes/exec-command-editor-xterm-adapter.js b/ui/app/utils/classes/exec-command-editor-xterm-adapter.js index 301962baa30..431956a681b 100644 --- a/ui/app/utils/classes/exec-command-editor-xterm-adapter.js +++ b/ui/app/utils/classes/exec-command-editor-xterm-adapter.js @@ -42,4 +42,8 @@ export default class ExecCommandEditorXtermAdapter { this.command = `${this.command}${e.key}`; } } + + destroy() { + this.keyListener.dispose(); + } } diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 23d836a31c4..0cf4bcff537 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -242,6 +242,41 @@ module('Acceptance | exec', function(hooks) { ); }); + test('only one socket is opened after switching between tasks', async function(assert) { + let mockSocket = new Object({ + sent: [], + + send(message) { + this.sent.push(message); + }, + }); + + let mockSockets = Service.extend({ + getTaskStateSocket() { + assert.step('Socket built'); + return mockSocket; + }, + }); + + this.owner.register('service:sockets', mockSockets); + + await Exec.visitJob({ + job: this.job.id, + }); + + await settled(); + + await Exec.taskGroups[0].click(); + await Exec.taskGroups[0].tasks[0].click(); + + await Exec.taskGroups[1].click(); + await Exec.taskGroups[1].tasks[0].click(); + + await Exec.terminal.pressEnter(); + + assert.verifySteps(['Socket built']); + }); + test('the command can be customised', async function(assert) { let mockSocket = new Object({ sent: [], From 50a082db10bdc10de680f7ea450789eef73b4259 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 3 Mar 2020 11:45:22 -0600 Subject: [PATCH 073/150] Extract MockSocket class --- ui/tests/acceptance/exec-test.js | 37 ++++++++++---------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 0cf4bcff537..118abc9b7e8 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -162,14 +162,7 @@ module('Acceptance | exec', function(hooks) { }); test('running the command opens the socket for reading/writing and detects it closing', async function(assert) { - let mockSocket = new Object({ - sent: [], - - send(message) { - this.sent.push(message); - }, - }); - + let mockSocket = new MockSocket(); let mockSockets = Service.extend({ getTaskStateSocket(taskState, command) { assert.equal(taskState.name, task.name); @@ -243,18 +236,10 @@ module('Acceptance | exec', function(hooks) { }); test('only one socket is opened after switching between tasks', async function(assert) { - let mockSocket = new Object({ - sent: [], - - send(message) { - this.sent.push(message); - }, - }); - let mockSockets = Service.extend({ getTaskStateSocket() { assert.step('Socket built'); - return mockSocket; + return new MockSocket(); }, }); @@ -278,21 +263,13 @@ module('Acceptance | exec', function(hooks) { }); test('the command can be customised', async function(assert) { - let mockSocket = new Object({ - sent: [], - - send(message) { - this.sent.push(message); - }, - }); - let mockSockets = Service.extend({ getTaskStateSocket(taskState, command) { assert.equal(command, '/sh'); assert.step('Socket built'); - return mockSocket; + return new MockSocket(); }, }); @@ -347,3 +324,11 @@ module('Acceptance | exec', function(hooks) { assert.verifySteps(['Socket built']); }); }); + +class MockSocket { + sent = []; + + send(message) { + this.sent.push(message); + } +} From 894c5ecbf405d7a57c118552a4c12ec497c4e6de Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 3 Mar 2020 14:09:38 -0600 Subject: [PATCH 074/150] Update vertical height of terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is better but still lacking; before it wasn’t getting taller than the default, but now it’s slightly too tall… meh --- ui/app/styles/components/exec.scss | 9 +++++++++ .../integration/util/exec-socket-xterm-adapter-test.js | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index 85f761729a8..9910cd3ef9a 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -1,10 +1,19 @@ .tree-and-terminal { display: flex; + height: 100%; .terminal-container { flex-grow: 1; background: black; padding: 16px; + height: 100%; + box-sizing: border-box; + + .terminal { + box-sizing: border-box; + height: 100%; + // FIXME this produces a scrollbar when terminal output reaches the final lines 😞 + } } } diff --git a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js index a27dc978eb9..0b49fdd076a 100644 --- a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js +++ b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js @@ -24,7 +24,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { let mockSocket = new Object({ send(message) { - assert.deepEqual(message, JSON.stringify({ tty_size: { width: 138, height: 24 } })); + assert.deepEqual(message, JSON.stringify({ tty_size: { width: 138, height: 12 } })); assert.equal(terminal.cols, 138); done(); }, @@ -34,7 +34,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { let terminalElement = find('.terminal'); terminalElement.style.width = '50%'; - // FIXME height doesn’t work yet + terminalElement.style.height = '110px'; window.dispatchEvent(new Event('resize')); await settled(); From 1429b528ce31152a0b85d48e601379870e72615d Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 3 Mar 2020 15:08:19 -0600 Subject: [PATCH 075/150] Fix height of tree-and-terminal container The problem the FIXME referred to was actually that the height was 100% of the window, which failed to exclude the height of the navbar. --- ui/app/styles/components/exec.scss | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/app/styles/components/exec.scss b/ui/app/styles/components/exec.scss index 9910cd3ef9a..123682d411d 100644 --- a/ui/app/styles/components/exec.scss +++ b/ui/app/styles/components/exec.scss @@ -1,18 +1,20 @@ .tree-and-terminal { display: flex; - height: 100%; + position: absolute; + left: 0; + right: 0; + top: 3.5rem; // nav.navbar.is-popup height + bottom: 0; .terminal-container { flex-grow: 1; background: black; padding: 16px; height: 100%; - box-sizing: border-box; + position: relative; .terminal { - box-sizing: border-box; height: 100%; - // FIXME this produces a scrollbar when terminal output reaches the final lines 😞 } } } From ae3a465b5e719b265fef0be1a4878402058898c0 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 3 Mar 2020 15:42:15 -0600 Subject: [PATCH 076/150] Add assertion for terminal row count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I should have included this when checking the message sent… though maybe this is unnecessary duplication? --- ui/tests/integration/util/exec-socket-xterm-adapter-test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js index 0b49fdd076a..76f9df14051 100644 --- a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js +++ b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js @@ -26,6 +26,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { send(message) { assert.deepEqual(message, JSON.stringify({ tty_size: { width: 138, height: 12 } })); assert.equal(terminal.cols, 138); + assert.equal(terminal.rows, 12); done(); }, }); From 646e52caa629739007ec6ac9320ddc634e552786 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 08:32:55 -0800 Subject: [PATCH 077/150] Remove extraneous copypaste MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This doesn’t do anything in this test. --- ui/tests/integration/util/exec-socket-xterm-adapter-test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js index 76f9df14051..4286a6d6305 100644 --- a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js +++ b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js @@ -18,8 +18,6 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { {{exec-terminal terminal=terminal}} `); - terminal.write('/bin/long-command'); - await settled(); let mockSocket = new Object({ From 240d0e026cc28e97c1dcda3533423f9773e6c4a1 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 08:59:45 -0800 Subject: [PATCH 078/150] Add filter for what incoming frames are processed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I attempted to produce stderr frames but learned from @notnoop that they won’t happen because of the tty flag when opening the socket. There still needs to be handling for other messages, such as stdout.close vs stdout.data. --- .../classes/exec-socket-xterm-adapter.js | 8 +++-- .../util/exec-socket-xterm-adapter-test.js | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/ui/app/utils/classes/exec-socket-xterm-adapter.js b/ui/app/utils/classes/exec-socket-xterm-adapter.js index 6bcb31e2507..a0ec4eb8fc7 100644 --- a/ui/app/utils/classes/exec-socket-xterm-adapter.js +++ b/ui/app/utils/classes/exec-socket-xterm-adapter.js @@ -19,8 +19,12 @@ export default class ExecSocketXtermAdapter { socket.onmessage = e => { let json = JSON.parse(e.data); - // FIXME could be stderr, or stdout.close/sterr.close, or exited, or even {}! - terminal.write(decodeString(json.stdout.data)); + + // FIXME what to do with stdout.close/sterr.close, or exited, or even {}! + // stderr messages will not be produced as the socket is opened with the tty flag + if (json.stdout) { + terminal.write(decodeString(json.stdout.data)); + } }; socket.onclose = e => { diff --git a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js index 4286a6d6305..856961de10e 100644 --- a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js +++ b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js @@ -38,4 +38,39 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { await settled(); }); + + test('stderr frames are ignored', async function(assert) { + let terminal = new Terminal(); + this.set('terminal', terminal); + + await render(hbs` + {{exec-terminal terminal=terminal}} + `); + + await settled(); + + let mockSocket = new Object({ + send() {}, + }); + + new ExecSocketXtermAdapter(terminal, mockSocket); + + mockSocket.onmessage({ + data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}', + }); + + mockSocket.onmessage({ + data: '{"stderr":{"data":"c2gtMy4yIPCfpbMk"}}', + }); + + await settled(); + + assert.equal( + terminal.buffer + .getLine(0) + .translateToString() + .trim(), + 'sh-3.2 🥳$' + ); + }); }); From 6c74c633ed7c5e5329b09f64768818b897328371 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 09:04:25 -0800 Subject: [PATCH 079/150] Update outdated comment --- ui/app/utils/classes/exec-socket-xterm-adapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/utils/classes/exec-socket-xterm-adapter.js b/ui/app/utils/classes/exec-socket-xterm-adapter.js index a0ec4eb8fc7..2a32001bf5b 100644 --- a/ui/app/utils/classes/exec-socket-xterm-adapter.js +++ b/ui/app/utils/classes/exec-socket-xterm-adapter.js @@ -20,7 +20,7 @@ export default class ExecSocketXtermAdapter { socket.onmessage = e => { let json = JSON.parse(e.data); - // FIXME what to do with stdout.close/sterr.close, or exited, or even {}! + // FIXME what to do with stdout.close or exited, or even {}! // stderr messages will not be produced as the socket is opened with the tty flag if (json.stdout) { terminal.write(decodeString(json.stdout.data)); From 34a2db32339bbfa0403dac9cbc22150a06f6fadf Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 11:49:25 -0800 Subject: [PATCH 080/150] Move public field into constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I don’t know why I was getting this error but it’s not important enough to me to figure out vs just changing: 329:8 error Parsing error: Unexpected token = --- ui/tests/acceptance/exec-test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 118abc9b7e8..a9614ce4e10 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -326,7 +326,9 @@ module('Acceptance | exec', function(hooks) { }); class MockSocket { - sent = []; + constructor() { + this.sent = []; + } send(message) { this.sent.push(message); From b210afed89beb71d437bcb5ee09545302e30cf7d Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 11:51:17 -0800 Subject: [PATCH 081/150] Remove unused variables --- ui/tests/acceptance/exec-test.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index a9614ce4e10..2b67644cf0e 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -137,12 +137,9 @@ module('Acceptance | exec', function(hooks) { }); let allocation = allocations[allocations.length - 1]; - let oldName = task.name; task.name = 'spaced name!'; task.save(); - let taskState = this.server.db.taskStates.update({ name: oldName }, { name: 'spaced name!' }); - await Exec.visitTask({ job: this.job.id, task_group: taskGroup.name, From 8c90b7641d7a57c8ebdc2084b6d0296bdc259cf7 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 11:59:45 -0800 Subject: [PATCH 082/150] Add required attribute This is a template lint requirement. --- ui/app/templates/exec.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index 381913e73cd..999b3e42fb0 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -25,7 +25,7 @@ {{model.name}}
      From 2524bda59c373432a432970b0b4f325869cbef1a Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 12:12:01 -0800 Subject: [PATCH 083/150] Change assertion on initial terminal size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I should have included this in 894c5ec but sadly the unreliable test situation is a problem I haven’t been able to address yet. --- ui/tests/acceptance/exec-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 2b67644cf0e..2b03d7f59ee 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -216,7 +216,7 @@ module('Acceptance | exec', function(hooks) { await settled(); assert.deepEqual(mockSocket.sent, [ - '{"tty_size":{"width":228,"height":24}}', + '{"tty_size":{"width":228,"height":75}}', '{"stdin":{"data":"DQ=="}}', ]); From 3d3230751dc2511c9cccf544f364919e94518cf7 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 12:23:22 -0800 Subject: [PATCH 084/150] Change TTY size assertion to reference terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was passing for me locally but failing on Circle CI, not sure why, but the purpose is to make sure the numbers match, the actual numbers aren’t important. --- ui/tests/acceptance/exec-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 2b03d7f59ee..e98469f16e3 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -216,7 +216,7 @@ module('Acceptance | exec', function(hooks) { await settled(); assert.deepEqual(mockSocket.sent, [ - '{"tty_size":{"width":228,"height":75}}', + `{"tty_size":{"width":${window.execTerminal.cols},"height":${window.execTerminal.rows}}}`, '{"stdin":{"data":"DQ=="}}', ]); From f87ba12838066da2ae1ef196a4d72b8b224cfae0 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 14:52:54 -0800 Subject: [PATCH 085/150] Change resize integration test to dynamic values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is similar to the change needed for the acceptance test. My theory now is that it’s because of font differences across platforms. --- .../integration/util/exec-socket-xterm-adapter-test.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js index 856961de10e..2bc18744159 100644 --- a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js +++ b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js @@ -22,9 +22,10 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { let mockSocket = new Object({ send(message) { - assert.deepEqual(message, JSON.stringify({ tty_size: { width: 138, height: 12 } })); - assert.equal(terminal.cols, 138); - assert.equal(terminal.rows, 12); + assert.deepEqual( + message, + JSON.stringify({ tty_size: { width: terminal.cols, height: terminal.rows } }) + ); done(); }, }); From a7a528df1052d1b2e9bb59264001d5a46f971096 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 15:03:27 -0800 Subject: [PATCH 086/150] Remove resize test dimension changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There’s no need to actually change the container if the specifics of the rows and columns aren’t being asserted against. --- ui/tests/integration/util/exec-socket-xterm-adapter-test.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js index 2bc18744159..6d42edddbd8 100644 --- a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js +++ b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js @@ -1,7 +1,7 @@ import ExecSocketXtermAdapter from 'nomad-ui/utils/classes/exec-socket-xterm-adapter'; import { setupRenderingTest } from 'ember-qunit'; import { module, test } from 'qunit'; -import { find, render, settled } from '@ember/test-helpers'; +import { render, settled } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { Terminal } from 'xterm'; @@ -32,9 +32,6 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { new ExecSocketXtermAdapter(terminal, mockSocket); - let terminalElement = find('.terminal'); - terminalElement.style.width = '50%'; - terminalElement.style.height = '110px'; window.dispatchEvent(new Event('resize')); await settled(); From 97b4756ce0ba8e607407a757f423db51e83d5fe6 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 16:58:27 -0800 Subject: [PATCH 087/150] Add renaming of task state to match task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This test was failing because the route’s attempt to find the task state by name was impossible. --- ui/tests/acceptance/exec-test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index e98469f16e3..edc90629c56 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -137,6 +137,8 @@ module('Acceptance | exec', function(hooks) { }); let allocation = allocations[allocations.length - 1]; + this.server.db.taskStates.update({ name: task.name }, { name: 'spaced name!' }); + task.name = 'spaced name!'; task.save(); From 479d9e752d3cd811c22e9d8696c3c03f83501a78 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Mar 2020 17:08:22 -0800 Subject: [PATCH 088/150] Add temporary logging of test application errors --- ui/app/controllers/application.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/app/controllers/application.js b/ui/app/controllers/application.js index 7a2934fab7e..e01f4f9bcee 100644 --- a/ui/app/controllers/application.js +++ b/ui/app/controllers/application.js @@ -44,6 +44,9 @@ export default Controller.extend({ }), throwError: observer('error', function() { + // FIXME committing because it keeps getting erased…? + // eslint-disable-next-line + console.log('error!', this.error); if (this.get('config.isDev')) { run.next(() => { throw this.error; From 17a5db53961c02fa4ce7b403bf9924b6464bad2a Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 5 Mar 2020 09:11:05 -0800 Subject: [PATCH 089/150] Remove pending clientStatus allocations for now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Later in this file, if the clientStatus is pending, the task states don’t get connected to the allocation. This causes tests to fail because that connexion is used in the exec task route. Maybe when constructing the job another flag needs to be threaded through… meh --- ui/mirage/factories/allocation.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js index b6e52917ba5..72e5c19cd15 100644 --- a/ui/mirage/factories/allocation.js +++ b/ui/mirage/factories/allocation.js @@ -6,7 +6,8 @@ import { provide, pickOne } from '../utils'; import { generateResources } from '../common'; const UUIDS = provide(100, faker.random.uuid.bind(faker.random)); -const CLIENT_STATUSES = ['pending', 'running', 'complete', 'failed', 'lost']; +const CLIENT_STATUSES = [/*'pending', */ 'running', 'complete', 'failed', 'lost']; +// FIXME restore pending const DESIRED_STATUSES = ['run', 'stop', 'evict']; const REF_TIME = new Date(); From 4c5746749e7e6d5c5f45ea0006addafe3d0fa09b Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 5 Mar 2020 09:11:36 -0800 Subject: [PATCH 090/150] Remove Faker override --- ui/mirage/faker.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui/mirage/faker.js b/ui/mirage/faker.js index 4100a105ba3..2baad57fb4c 100644 --- a/ui/mirage/faker.js +++ b/ui/mirage/faker.js @@ -11,9 +11,6 @@ if (config.environment !== 'test' || searchIncludesSeed) { } else { faker.seed(1); } -} else { - // FIXME - faker.seed(3); } export default faker; From 20a9f6d10b37995735420ac1b24f0aa5df22b750 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 5 Mar 2020 13:52:50 -0800 Subject: [PATCH 091/150] Add flag to ensure no pending allocations This guarantees that the task, task state, and allocation can be properly connected in the acceptance tests. --- ui/mirage/factories/allocation.js | 14 +++++++++++--- ui/tests/acceptance/exec-test.js | 10 +++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js index 72e5c19cd15..2d47d826a11 100644 --- a/ui/mirage/factories/allocation.js +++ b/ui/mirage/factories/allocation.js @@ -6,8 +6,8 @@ import { provide, pickOne } from '../utils'; import { generateResources } from '../common'; const UUIDS = provide(100, faker.random.uuid.bind(faker.random)); -const CLIENT_STATUSES = [/*'pending', */ 'running', 'complete', 'failed', 'lost']; -// FIXME restore pending +const CLIENT_STATUSES = ['pending', 'running', 'complete', 'failed', 'lost']; +const NON_PENDING_CLIENT_STATUSES = ['running', 'complete', 'failed', 'lost']; const DESIRED_STATUSES = ['run', 'stop', 'evict']; const REF_TIME = new Date(); @@ -26,12 +26,20 @@ export default Factory.extend({ namespace: null, - clientStatus: () => faker.helpers.randomize(CLIENT_STATUSES), + clientStatus() { + return this.allowPendingClientStatus + ? faker.helpers.randomize(CLIENT_STATUSES) + : faker.helpers.randomize(NON_PENDING_CLIENT_STATUSES); + }, + desiredStatus: () => faker.helpers.randomize(DESIRED_STATUSES), // When true, doesn't create any resources, state, or events shallow: false, + // When true, allows the client status to be pending + allowPendingClientStatus: true, + withTaskWithPorts: trait({ afterCreate(allocation, server) { const taskGroup = server.db.taskGroups.findBy({ name: allocation.taskGroup }); diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index edc90629c56..158c86b1022 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -13,7 +13,15 @@ module('Acceptance | exec', function(hooks) { server.create('agent'); server.create('node'); - this.job = server.create('job', { groupsCount: 2 }); + this.job = server.create('job', { groupsCount: 2, createAllocations: false }); + + this.job.task_group_ids.forEach(taskGroupId => { + server.create('allocation', { + jobId: this.job.id, + taskGroup: server.db.taskGroups.find(taskGroupId).name, + allowPendingClientStatus: false, + }); + }); }); test('/exec/:job should show the region, namespace, and job name', async function(assert) { From 7afcda225f5fd3a488bb2794219612cb4418a435 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 5 Mar 2020 14:09:18 -0800 Subject: [PATCH 092/150] Remove outdated comment --- ui/app/controllers/exec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index 072a52c5a34..8a7e074a80c 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -67,9 +67,6 @@ export default Controller.extend({ this.openAndConnectSocket.bind(this), '/bin/bash' ); - - // FIXME - // this.terminal.simulateCommandKeyEvent = this.handleCommandKeyEvent.bind(this); }, }, From f24ea6846d3995c005ccbf843083c400a111daa7 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 5 Mar 2020 14:15:30 -0800 Subject: [PATCH 093/150] Remove prototype implementation --- ui/app/router.js | 1 - .../allocations/allocation/task/exec.js | 24 ------- .../allocations/allocation/task/exec.hbs | 4 -- ui/app/templates/components/task-subnav.hbs | 1 - .../components/exec-terminal-test.js | 62 ------------------- 5 files changed, 92 deletions(-) delete mode 100644 ui/app/routes/allocations/allocation/task/exec.js delete mode 100644 ui/app/templates/allocations/allocation/task/exec.hbs delete mode 100644 ui/tests/integration/components/exec-terminal-test.js diff --git a/ui/app/router.js b/ui/app/router.js index 2e562271ac2..f1c94fe4172 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -39,7 +39,6 @@ Router.map(function() { this.route('logs'); this.route('fs-root', { path: '/fs' }); this.route('fs', { path: '/fs/*path' }); - this.route('exec'); }); }); }); diff --git a/ui/app/routes/allocations/allocation/task/exec.js b/ui/app/routes/allocations/allocation/task/exec.js deleted file mode 100644 index 0a4ce9911e8..00000000000 --- a/ui/app/routes/allocations/allocation/task/exec.js +++ /dev/null @@ -1,24 +0,0 @@ -import Route from '@ember/routing/route'; - -export default Route.extend({ - model() { - const task = this.modelFor('allocations.allocation.task'); - const taskName = task.name; - const allocationId = task.allocation.id; - - // FIXME generalise host - const socket = new WebSocket( - `ws://localhost:4400/v1/client/allocation/${allocationId}/exec?task=${taskName}&tty=true&command=%5B%22%2Fbin%2Fbash%22%5D` - ); - - return { - socket, - task, - }; - }, - - setupController(controller, { socket, task }) { - this._super(...arguments); - controller.setProperties({ socket, task }); - }, -}); diff --git a/ui/app/templates/allocations/allocation/task/exec.hbs b/ui/app/templates/allocations/allocation/task/exec.hbs deleted file mode 100644 index 6b92d63b501..00000000000 --- a/ui/app/templates/allocations/allocation/task/exec.hbs +++ /dev/null @@ -1,4 +0,0 @@ -{{title "Task " task.name " exec"}} -{{task-subnav task=task}} - -{{exec-terminal socket=socket}} diff --git a/ui/app/templates/components/task-subnav.hbs b/ui/app/templates/components/task-subnav.hbs index bbe9c3ecbe7..4872d032e30 100644 --- a/ui/app/templates/components/task-subnav.hbs +++ b/ui/app/templates/components/task-subnav.hbs @@ -3,6 +3,5 @@
    • {{#link-to "allocations.allocation.task.index" task.allocation task activeClass="is-active"}}Overview{{/link-to}}
    • {{#link-to "allocations.allocation.task.logs" task.allocation task activeClass="is-active"}}Logs{{/link-to}}
    • {{#link-to "allocations.allocation.task.fs-root" task.allocation task class=(if filesLinkActive "is-active")}}Files{{/link-to}}
    • -
    • {{#link-to "allocations.allocation.task.exec" task.allocation task activeClass="is-active"}}Exec{{/link-to}}
    diff --git a/ui/tests/integration/components/exec-terminal-test.js b/ui/tests/integration/components/exec-terminal-test.js deleted file mode 100644 index a252c53287f..00000000000 --- a/ui/tests/integration/components/exec-terminal-test.js +++ /dev/null @@ -1,62 +0,0 @@ -import { module, skip } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, triggerKeyEvent } from '@ember/test-helpers'; -import { next } from '@ember/runloop'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; - -module('Integration | Component | exec-terminal', function(hooks) { - setupRenderingTest(hooks); - - skip('it renders an incoming message', async function(assert) { - const done = assert.async(); - - const socket = { - mockMessage(message) { - if (this.onmessage) { - this.onmessage({ data: JSON.stringify(message) }); - } - }, - }; - this.set('socket', socket); - - await render(hbs`{{exec-terminal socket=socket}}`); - - window.xterm.onRender(() => { - assert.ok( - window.xterm.buffer - .getLine(0) - .translateToString() - .includes('exec 🥳') - ); - done(); - }); - - socket.mockMessage({ - stdout: { - data: 'ZXhlYyDwn6Wz', // FIXME? - }, - }); - }); - - skip('it routes encoded input to the socket', async function(assert) { - const done = assert.async(); - - const socket = { - send: sinon.spy(), - }; - this.set('socket', socket); - - await render(hbs`{{exec-terminal socket=socket}}`); - - await triggerKeyEvent('textarea', 'keydown', '!'); - - // FIXME can’t figure out how to trigger a send - // BUT is this even a sensible layer to test at? - next(() => { - assert.ok(socket.send.calledOnce); - assert.ok(socket.send.calledWith({})); - done(); - }); - }); -}); From 0566d3e4a647789e546dd00b5b971706ce5763a3 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 5 Mar 2020 15:36:51 -0800 Subject: [PATCH 094/150] Remove note about Nomad logo in exec nav --- ui/app/templates/exec.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/templates/exec.hbs b/ui/app/templates/exec.hbs index 999b3e42fb0..102fc34c5d0 100644 --- a/ui/app/templates/exec.hbs +++ b/ui/app/templates/exec.hbs @@ -3,7 +3,7 @@