diff --git a/ui/app/components/image-file.js b/ui/app/components/image-file.js index 2c84ff893..fe28201ba 100644 --- a/ui/app/components/image-file.js +++ b/ui/app/components/image-file.js @@ -4,6 +4,7 @@ import { computed } from '@ember/object'; export default Component.extend({ tagName: 'figure', classNames: 'image-file', + 'data-test-image-file': true, src: null, alt: null, diff --git a/ui/app/components/task-file.js b/ui/app/components/task-file.js index 8f5666b09..a37126aab 100644 --- a/ui/app/components/task-file.js +++ b/ui/app/components/task-file.js @@ -29,7 +29,7 @@ export default Component.extend({ mode: 'head', - fileComponent: computed('stat', function() { + fileComponent: computed('stat.ContentType', function() { const contentType = this.stat.ContentType || ''; if (contentType.startsWith('image/')) { @@ -41,7 +41,7 @@ export default Component.extend({ } }), - isLarge: computed('stat', function() { + isLarge: computed('stat.Size', function() { return this.stat.Size > 50000; }), @@ -107,7 +107,6 @@ export default Component.extend({ this.set('noConnection', true); } else { this.send('failoverToServer'); - this.stream.perform(); } throw error; } diff --git a/ui/app/templates/components/task-file.hbs b/ui/app/templates/components/task-file.hbs index dbe4d0ddc..3d9e1f518 100644 --- a/ui/app/templates/components/task-file.hbs +++ b/ui/app/templates/components/task-file.hbs @@ -4,7 +4,7 @@

The files for this task are inaccessible. Check the condition of the client the allocation is on.

{{/if}} -
+
{{yield}} @@ -28,7 +28,7 @@ {{else if (eq fileComponent "image")}} {{image-file src=catUrl alt=stat.Name size=stat.Size}} {{else}} -
+

Unsupported File Type

The Nomad UI could not render this file, but you can still call view the file directly.

diff --git a/ui/tests/integration/task-file-test.js b/ui/tests/integration/task-file-test.js new file mode 100644 index 000000000..e23ef26fc --- /dev/null +++ b/ui/tests/integration/task-file-test.js @@ -0,0 +1,226 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, settled } from '@ember/test-helpers'; +import { find } from 'ember-native-dom-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import Pretender from 'pretender'; +import { logEncode } from '../../mirage/data/logs'; + +const { assign } = Object; +const HOST = '1.1.1.1:1111'; + +module('Integration | Component | task file', function(hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function() { + this.server = new Pretender(function() { + this.get('/v1/regions', () => [200, {}, JSON.stringify(['default'])]); + this.get('/v1/client/fs/stream/:alloc_id', () => [200, {}, logEncode(['Hello World'], 0)]); + this.get('/v1/client/fs/cat/:alloc_id', () => [200, {}, 'Hello World']); + this.get('/v1/client/fs/readat/:alloc_id', () => [200, {}, 'Hello World']); + }); + }); + + hooks.afterEach(function() { + this.server.shutdown(); + }); + + const commonTemplate = hbs` + {{task-file allocation=allocation task=task file=file stat=stat}} + `; + + const fileStat = (type, size = 0) => ({ + stat: { + Size: size, + ContentType: type, + }, + }); + const makeProps = (props = {}) => + assign( + {}, + { + allocation: { + id: 'alloc-1', + node: { + httpAddr: HOST, + }, + }, + task: { + name: 'task-name', + }, + file: 'path/to/file', + stat: { + Size: 12345, + ContentType: 'text/plain', + }, + }, + props + ); + + test('When a file is text-based, the file mode is streaming', async function(assert) { + const props = makeProps(fileStat('text/plain', 500)); + this.setProperties(props); + + await render(commonTemplate); + + assert.ok( + find('[data-test-file-box] [data-test-log-cli]'), + 'The streaming file component was rendered' + ); + assert.notOk( + find('[data-test-file-box] [data-test-image-file]'), + 'The image file component was not rendered' + ); + }); + + test('When a file is an image, the file mode is image', async function(assert) { + const props = makeProps(fileStat('image/png', 1234)); + this.setProperties(props); + + await render(commonTemplate); + + assert.ok( + find('[data-test-file-box] [data-test-image-file]'), + 'The image file component was rendered' + ); + assert.notOk( + find('[data-test-file-box] [data-test-log-cli]'), + 'The streaming file component was not rendered' + ); + }); + + test('When the file is neither text-based or an image, the unsupported file type empty state is shown', async function(assert) { + const props = makeProps(fileStat('wat/ohno', 1234)); + this.setProperties(props); + + await render(commonTemplate); + + assert.notOk( + find('[data-test-file-box] [data-test-image-file]'), + 'The image file component was not rendered' + ); + assert.notOk( + find('[data-test-file-box] [data-test-log-cli]'), + 'The streaming file component was not rendered' + ); + assert.ok(find('[data-test-unsupported-type]'), 'Unsupported file type message is shown'); + }); + + test('The unsupported file type empty state includes a link to the raw file', async function(assert) { + const props = makeProps(fileStat('wat/ohno', 1234)); + this.setProperties(props); + + await render(commonTemplate); + + assert.ok( + find('[data-test-unsupported-type] [data-test-log-action="raw"]'), + 'Unsupported file type message includes a link to the raw file' + ); + + assert.notOk( + find('[data-test-header] [data-test-log-action="raw"]'), + 'Raw link is no longer in the header' + ); + }); + + test('The view raw button goes to the correct API url', async function(assert) { + const props = makeProps(fileStat('image/png', 1234)); + this.setProperties(props); + + await render(commonTemplate); + + const rawLink = find('[data-test-log-action="raw"]'); + assert.equal( + rawLink.getAttribute('href'), + `/v1/client/fs/cat/${props.allocation.id}?path=${props.task.name}/${props.file}`, + 'Raw link href is correct' + ); + + assert.equal(rawLink.getAttribute('target'), '_blank', 'Raw link opens in a new tab'); + assert.equal( + rawLink.getAttribute('rel'), + 'noopener noreferrer', + 'Raw link rel correctly bars openers and referrers' + ); + }); + + test('The head and tail buttons are not shown when the file is small', async function(assert) { + const props = makeProps(fileStat('application/json', 5000)); + this.setProperties(props); + + await render(commonTemplate); + + assert.notOk(find('[data-test-log-action="head"]'), 'No head button'); + assert.notOk(find('[data-test-log-action="tail"]'), 'No tail button'); + + this.set('stat.Size', 100000); + + await settled(); + + assert.ok(find('[data-test-log-action="head"]'), 'Head button is shown'); + assert.ok(find('[data-test-log-action="tail"]'), 'Tail button is shown'); + }); + + test('The head and tail buttons are not shown for an image file', async function(assert) { + const props = makeProps(fileStat('image/svg', 5000)); + this.setProperties(props); + + await render(commonTemplate); + + assert.notOk(find('[data-test-log-action="head"]'), 'No head button'); + assert.notOk(find('[data-test-log-action="tail"]'), 'No tail button'); + + this.set('stat.Size', 100000); + + await settled(); + + assert.notOk(find('[data-test-log-action="head"]'), 'Still no head button'); + assert.notOk(find('[data-test-log-action="tail"]'), 'Still no tail button'); + }); + + test('Yielded content goes in the top-left header area', async function(assert) { + const props = makeProps(fileStat('image/svg', 5000)); + this.setProperties(props); + + await render(hbs` + {{#task-file allocation=allocation task=task file=file stat=stat}} +

Yielded content
+ {{/task-file}} + `); + + assert.ok( + find('[data-test-header] [data-test-yield-spy]'), + 'Yielded content shows up in the header' + ); + }); + + test('The body is full-bleed and dark when the file is streaming', async function(assert) { + const props = makeProps(fileStat('application/json', 5000)); + this.setProperties(props); + + await render(commonTemplate); + + const classes = Array.from(find('[data-test-file-box]').classList); + assert.ok(classes.includes('is-dark'), 'Body is dark'); + assert.ok(classes.includes('is-full-bleed'), 'Body is full-bleed'); + }); + + test('The body has padding and a light background when the file is not streaming', async function(assert) { + const props = makeProps(fileStat('image/jpeg', 5000)); + this.setProperties(props); + + await render(commonTemplate); + + let classes = Array.from(find('[data-test-file-box]').classList); + assert.notOk(classes.includes('is-dark'), 'Body is not dark'); + assert.notOk(classes.includes('is-full-bleed'), 'Body is not full-bleed'); + + this.set('stat.ContentType', 'something/unknown'); + + await settled(); + + classes = Array.from(find('[data-test-file-box]').classList); + assert.notOk(classes.includes('is-dark'), 'Body is still not dark'); + assert.notOk(classes.includes('is-full-bleed'), 'Body is still not full-bleed'); + }); +});