diff --git a/CHANGELOG.md b/CHANGELOG.md index 551f7d5ff55..b90cbedf807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ IMPROVEMENTS: * csi: Move volume claim releases out of evaluation workers [[GH-8021](https://github.com/hashicorp/nomad/issues/8021)] * csi: Added support for `VolumeContext` and `VolumeParameters` [[GH-7957](https://github.com/hashicorp/nomad/issues/7957)] * logging: Remove spurious error log on task shutdown [[GH-8028](https://github.com/hashicorp/nomad/issues/8028)] + * ui: Added filesystem browsing for allocations [[GH-5871](https://github.com/hashicorp/nomad/pull/7951)] BUG FIXES: diff --git a/ui/app/adapters/allocation.js b/ui/app/adapters/allocation.js index a2c24a5fff1..93981bc57d0 100644 --- a/ui/app/adapters/allocation.js +++ b/ui/app/adapters/allocation.js @@ -11,8 +11,41 @@ export default Watchable.extend({ data: taskName && { TaskName: taskName }, }); }, + + ls(model, path) { + return this.token + .authorizedRequest(`/v1/client/fs/ls/${model.id}?path=${encodeURIComponent(path)}`) + .then(handleFSResponse); + }, + + stat(model, path) { + return this.token + .authorizedRequest( + `/v1/client/fs/stat/${model.id}?path=${encodeURIComponent(path)}` + ) + .then(handleFSResponse); + }, }); +async function handleFSResponse(response) { + if (response.ok) { + return response.json(); + } else { + const body = await response.text(); + + // TODO update this if/when endpoint returns 404 as expected + const statusIs500 = response.status === 500; + const bodyIncludes404Text = body.includes('no such file or directory'); + + const translatedCode = statusIs500 && bodyIncludes404Text ? 404 : response.status; + + throw { + code: translatedCode, + toString: () => body, + }; + } +} + function adapterAction(path, verb = 'POST') { return function(allocation) { const url = addToPath(this.urlForFindRecord(allocation.id, 'allocation'), path); diff --git a/ui/app/adapters/task-state.js b/ui/app/adapters/task-state.js deleted file mode 100644 index f9f98ca6271..00000000000 --- a/ui/app/adapters/task-state.js +++ /dev/null @@ -1,39 +0,0 @@ -import ApplicationAdapter from './application'; -import { inject as service } from '@ember/service'; - -export default ApplicationAdapter.extend({ - token: service(), - - ls(model, path) { - return this.token - .authorizedRequest(`/v1/client/fs/ls/${model.allocation.id}?path=${encodeURIComponent(path)}`) - .then(handleFSResponse); - }, - - stat(model, path) { - return this.token - .authorizedRequest( - `/v1/client/fs/stat/${model.allocation.id}?path=${encodeURIComponent(path)}` - ) - .then(handleFSResponse); - }, -}); - -async function handleFSResponse(response) { - if (response.ok) { - return response.json(); - } else { - const body = await response.text(); - - // TODO update this if/when endpoint returns 404 as expected - const statusIs500 = response.status === 500; - const bodyIncludes404Text = body.includes('no such file or directory'); - - const translatedCode = statusIs500 && bodyIncludes404Text ? 404 : response.status; - - throw { - code: translatedCode, - toString: () => body, - }; - } -} diff --git a/ui/app/components/allocation-subnav.js b/ui/app/components/allocation-subnav.js new file mode 100644 index 00000000000..133d18a27c1 --- /dev/null +++ b/ui/app/components/allocation-subnav.js @@ -0,0 +1,14 @@ +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; +import { equal, or } from '@ember/object/computed'; + +export default Component.extend({ + router: service(), + + tagName: '', + + fsIsActive: equal('router.currentRouteName', 'allocations.allocation.fs'), + fsRootIsActive: equal('router.currentRouteName', 'allocations.allocation.fs-root'), + + filesLinkActive: or('fsIsActive', 'fsRootIsActive'), +}); diff --git a/ui/app/components/fs-breadcrumbs.js b/ui/app/components/fs/breadcrumbs.js similarity index 97% rename from ui/app/components/fs-breadcrumbs.js rename to ui/app/components/fs/breadcrumbs.js index f613a0c4933..9af6a73d505 100644 --- a/ui/app/components/fs-breadcrumbs.js +++ b/ui/app/components/fs/breadcrumbs.js @@ -8,6 +8,7 @@ export default Component.extend({ 'data-test-fs-breadcrumbs': true, + allocation: null, task: null, path: null, diff --git a/ui/app/components/fs/browser.js b/ui/app/components/fs/browser.js new file mode 100644 index 00000000000..453a6aabc9e --- /dev/null +++ b/ui/app/components/fs/browser.js @@ -0,0 +1,57 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { filterBy } from '@ember/object/computed'; + +export default Component.extend({ + tagName: '', + + model: null, + + allocation: computed('model', function() { + if (this.model.allocation) { + return this.model.allocation; + } else { + return this.model; + } + }), + + task: computed('model', function() { + if (this.model.allocation) { + return this.model; + } + }), + + type: computed('task', function() { + if (this.task) { + return 'task'; + } else { + return 'allocation'; + } + }), + + directories: filterBy('directoryEntries', 'IsDir'), + files: filterBy('directoryEntries', 'IsDir', false), + + sortedDirectoryEntries: computed( + 'directoryEntries.[]', + 'sortProperty', + 'sortDescending', + function() { + const sortProperty = this.sortProperty; + + const directorySortProperty = sortProperty === 'Size' ? 'Name' : sortProperty; + + const sortedDirectories = this.directories.sortBy(directorySortProperty); + const sortedFiles = this.files.sortBy(sortProperty); + + const sortedDirectoryEntries = sortedDirectories.concat(sortedFiles); + + if (this.sortDescending) { + return sortedDirectoryEntries.reverse(); + } else { + return sortedDirectoryEntries; + } + } + ), + +}); diff --git a/ui/app/components/fs-directory-entry.js b/ui/app/components/fs/directory-entry.js similarity index 93% rename from ui/app/components/fs-directory-entry.js rename to ui/app/components/fs/directory-entry.js index f50bb84560d..f021b159ee0 100644 --- a/ui/app/components/fs-directory-entry.js +++ b/ui/app/components/fs/directory-entry.js @@ -5,6 +5,9 @@ import { isEmpty } from '@ember/utils'; export default Component.extend({ tagName: '', + allocation: null, + task: null, + pathToEntry: computed('path', 'entry.Name', function() { const pathWithNoLeadingSlash = this.get('path').replace(/^\//, ''); const name = encodeURIComponent(this.get('entry.Name')); diff --git a/ui/app/components/task-file.js b/ui/app/components/fs/file.js similarity index 93% rename from ui/app/components/task-file.js rename to ui/app/components/fs/file.js index 03f4e52c4e0..438b8d62470 100644 --- a/ui/app/components/task-file.js +++ b/ui/app/components/fs/file.js @@ -49,7 +49,8 @@ export default Component.extend({ isStreaming: false, catUrl: computed('allocation.id', 'task.name', 'file', function() { - const encodedPath = encodeURIComponent(`${this.task.name}/${this.file}`); + const taskUrlPrefix = this.task ? `${this.task.name}/` : ''; + const encodedPath = encodeURIComponent(`${taskUrlPrefix}${this.file}`); return `/v1/client/fs/cat/${this.allocation.id}?path=${encodedPath}`; }), @@ -79,7 +80,8 @@ export default Component.extend({ fileParams: computed('task.name', 'file', 'mode', function() { // The Log class handles encoding query params - const path = `${this.task.name}/${this.file}`; + const taskUrlPrefix = this.task ? `${this.task.name}/` : ''; + const path = `${taskUrlPrefix}${this.file}`; switch (this.mode) { case 'head': diff --git a/ui/app/components/fs/link.js b/ui/app/components/fs/link.js new file mode 100644 index 00000000000..a5460143ff1 --- /dev/null +++ b/ui/app/components/fs/link.js @@ -0,0 +1,8 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', + + allocation: null, + task: null, +}); diff --git a/ui/app/controllers/allocations/allocation/fs-root.js b/ui/app/controllers/allocations/allocation/fs-root.js new file mode 100644 index 00000000000..2297a800ea1 --- /dev/null +++ b/ui/app/controllers/allocations/allocation/fs-root.js @@ -0,0 +1,3 @@ +import FSController from './fs'; + +export default FSController.extend(); diff --git a/ui/app/controllers/allocations/allocation/fs.js b/ui/app/controllers/allocations/allocation/fs.js new file mode 100644 index 00000000000..745eeb9d02b --- /dev/null +++ b/ui/app/controllers/allocations/allocation/fs.js @@ -0,0 +1,28 @@ +import Controller from '@ember/controller'; +import { computed } from '@ember/object'; + +export default Controller.extend({ + queryParams: { + sortProperty: 'sort', + sortDescending: 'desc', + }, + + sortProperty: 'Name', + sortDescending: false, + + path: null, + allocation: null, + directoryEntries: null, + isFile: null, + stat: null, + + pathWithLeadingSlash: computed('path', function() { + const path = this.path; + + if (path.startsWith('/')) { + return path; + } else { + return `/${path}`; + } + }), +}); diff --git a/ui/app/controllers/allocations/allocation/task/fs.js b/ui/app/controllers/allocations/allocation/task/fs.js index bfece091338..a0c3b5e697a 100644 --- a/ui/app/controllers/allocations/allocation/task/fs.js +++ b/ui/app/controllers/allocations/allocation/task/fs.js @@ -1,6 +1,5 @@ import Controller from '@ember/controller'; import { computed } from '@ember/object'; -import { filterBy } from '@ember/object/computed'; export default Controller.extend({ queryParams: { @@ -17,9 +16,6 @@ export default Controller.extend({ isFile: null, stat: null, - directories: filterBy('directoryEntries', 'IsDir'), - files: filterBy('directoryEntries', 'IsDir', false), - pathWithLeadingSlash: computed('path', function() { const path = this.path; @@ -29,26 +25,4 @@ export default Controller.extend({ return `/${path}`; } }), - - sortedDirectoryEntries: computed( - 'directoryEntries.[]', - 'sortProperty', - 'sortDescending', - function() { - const sortProperty = this.sortProperty; - - const directorySortProperty = sortProperty === 'Size' ? 'Name' : sortProperty; - - const sortedDirectories = this.directories.sortBy(directorySortProperty); - const sortedFiles = this.files.sortBy(sortProperty); - - const sortedDirectoryEntries = sortedDirectories.concat(sortedFiles); - - if (this.sortDescending) { - return sortedDirectoryEntries.reverse(); - } else { - return sortedDirectoryEntries; - } - } - ), }); diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index 28db0e2cf45..44a8524286d 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -125,4 +125,12 @@ export default Model.extend({ restart(taskName) { return this.store.adapterFor('allocation').restart(this, taskName); }, + + ls(path) { + return this.store.adapterFor('allocation').ls(this, path); + }, + + stat(path) { + return this.store.adapterFor('allocation').stat(this, path); + }, }); diff --git a/ui/app/models/task-state.js b/ui/app/models/task-state.js index e6afba7e862..fb9736ab7e4 100644 --- a/ui/app/models/task-state.js +++ b/ui/app/models/task-state.js @@ -51,12 +51,4 @@ export default Fragment.extend({ restart() { return this.allocation.restart(this.name); }, - - ls(path) { - return this.store.adapterFor('task-state').ls(this, path); - }, - - stat(path) { - return this.store.adapterFor('task-state').stat(this, path); - }, }); diff --git a/ui/app/router.js b/ui/app/router.js index 9f79e0e1adb..9645a7b7014 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -47,6 +47,9 @@ Router.map(function() { this.route('allocations', function() { this.route('allocation', { path: '/:allocation_id' }, function() { + this.route('fs-root', { path: '/fs' }); + this.route('fs', { path: '/fs/*path' }); + this.route('task', { path: '/:name' }, function() { this.route('logs'); this.route('fs-root', { path: '/fs' }); diff --git a/ui/app/routes/allocations/allocation/fs-root.js b/ui/app/routes/allocations/allocation/fs-root.js new file mode 100644 index 00000000000..eb85dc7c30b --- /dev/null +++ b/ui/app/routes/allocations/allocation/fs-root.js @@ -0,0 +1,5 @@ +import FSRoute from './fs'; + +export default FSRoute.extend({ + templateName: 'allocations/allocation/fs', +}); diff --git a/ui/app/routes/allocations/allocation/fs.js b/ui/app/routes/allocations/allocation/fs.js new file mode 100644 index 00000000000..f3580621a02 --- /dev/null +++ b/ui/app/routes/allocations/allocation/fs.js @@ -0,0 +1,42 @@ +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; +import notifyError from 'nomad-ui/utils/notify-error'; + +export default Route.extend({ + model({ path = '/' }) { + const decodedPath = decodeURIComponent(path); + const allocation = this.modelFor('allocations.allocation'); + + if (!allocation.isRunning) { + return { + path: decodedPath, + allocation, + }; + } + + return RSVP.all([allocation.stat(decodedPath), allocation.get('node')]) + .then(([statJson]) => { + if (statJson.IsDir) { + return RSVP.hash({ + path: decodedPath, + allocation, + directoryEntries: allocation.ls(decodedPath).catch(notifyError(this)), + isFile: false, + }); + } else { + return { + path: decodedPath, + allocation, + isFile: true, + stat: statJson, + }; + } + }) + .catch(notifyError(this)); + }, + + setupController(controller, { path, allocation, directoryEntries, isFile, stat } = {}) { + this._super(...arguments); + controller.setProperties({ path, allocation, directoryEntries, isFile, stat }); + }, +}); diff --git a/ui/app/routes/allocations/allocation/task/fs.js b/ui/app/routes/allocations/allocation/task/fs.js index 097ba686a4d..7627a8d61c2 100644 --- a/ui/app/routes/allocations/allocation/task/fs.js +++ b/ui/app/routes/allocations/allocation/task/fs.js @@ -6,6 +6,7 @@ export default Route.extend({ model({ path = '/' }) { const decodedPath = decodeURIComponent(path); const task = this.modelFor('allocations.allocation.task'); + const allocation = task.allocation; const pathWithTaskName = `${task.name}${decodedPath.startsWith('/') ? '' : '/'}${decodedPath}`; @@ -16,13 +17,13 @@ export default Route.extend({ }; } - return RSVP.all([task.stat(pathWithTaskName), task.get('allocation.node')]) + return RSVP.all([allocation.stat(pathWithTaskName), task.get('allocation.node')]) .then(([statJson]) => { if (statJson.IsDir) { return RSVP.hash({ path: decodedPath, task, - directoryEntries: task.ls(pathWithTaskName).catch(notifyError(this)), + directoryEntries: allocation.ls(pathWithTaskName).catch(notifyError(this)), isFile: false, }); } else { diff --git a/ui/app/templates/allocations/allocation/fs.hbs b/ui/app/templates/allocations/allocation/fs.hbs new file mode 100644 index 00000000000..432e84dfbbd --- /dev/null +++ b/ui/app/templates/allocations/allocation/fs.hbs @@ -0,0 +1,10 @@ +{{title pathWithLeadingSlash " - Allocation " allocation.shortId " filesystem"}} +{{allocation-subnav allocation=allocation}} +{{fs/browser + model=allocation + path=path + stat=stat + isFile=isFile + directoryEntries=directoryEntries + sortProperty=sortProperty + sortDescending=sortDescending}} \ No newline at end of file diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index 1fcf2aa8445..10040984ac1 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -1,4 +1,5 @@ {{title "Allocation " model.name}} +{{allocation-subnav allocation=model}}
{{#if error}}
diff --git a/ui/app/templates/allocations/allocation/task/fs.hbs b/ui/app/templates/allocations/allocation/task/fs.hbs index 45a6b437efa..b76b92b69ce 100644 --- a/ui/app/templates/allocations/allocation/task/fs.hbs +++ b/ui/app/templates/allocations/allocation/task/fs.hbs @@ -1,49 +1,10 @@ {{title pathWithLeadingSlash " - Task " task.name " filesystem"}} {{task-subnav task=task}} -
- {{#if task.isRunning}} - {{#if isFile}} - {{#task-file allocation=task.allocation task=task file=path stat=stat class="fs-explorer"}} - {{fs-breadcrumbs task=task path=path}} - {{/task-file}} - {{else}} -
-
- {{fs-breadcrumbs task=task path=path}} -
- {{#if directoryEntries}} - {{#list-table - source=sortedDirectoryEntries - sortProperty=sortProperty - sortDescending=sortDescending - class="boxed-section-body is-full-bleed is-compact" as |t|}} - {{#t.head}} - {{#t.sort-by prop="Name" class="is-two-thirds"}}Name{{/t.sort-by}} - {{#t.sort-by prop="Size" class="has-text-right"}}File Size{{/t.sort-by}} - {{#t.sort-by prop="ModTime" class="has-text-right"}}Last Modified{{/t.sort-by}} - {{/t.head}} - {{#t.body as |row|}} - {{fs-directory-entry path=path task=task entry=row.model}} - {{/t.body}} - {{/list-table}} - {{else}} -
-
-

No Files

-

- Directory is currently empty. -

-
-
- {{/if}} -
- {{/if}} - {{else}} -
-

Task is not Running

-

- Cannot access files of a task that is not running. -

-
- {{/if}} -
+{{fs/browser + model=task + path=path + stat=stat + isFile=isFile + directoryEntries=directoryEntries + sortProperty=sortProperty + sortDescending=sortDescending}} \ No newline at end of file diff --git a/ui/app/templates/components/allocation-subnav.hbs b/ui/app/templates/components/allocation-subnav.hbs new file mode 100644 index 00000000000..14d47d58937 --- /dev/null +++ b/ui/app/templates/components/allocation-subnav.hbs @@ -0,0 +1,6 @@ +
+
    +
  • {{#link-to "allocations.allocation.index" allocation activeClass="is-active"}}Overview{{/link-to}}
  • +
  • {{#link-to "allocations.allocation.fs-root" allocation class=(if filesLinkActive "is-active")}}Files{{/link-to}}
  • +
+
diff --git a/ui/app/templates/components/fs-breadcrumbs.hbs b/ui/app/templates/components/fs-breadcrumbs.hbs deleted file mode 100644 index 97aba6398a5..00000000000 --- a/ui/app/templates/components/fs-breadcrumbs.hbs +++ /dev/null @@ -1,14 +0,0 @@ - \ No newline at end of file diff --git a/ui/app/templates/components/fs/breadcrumbs.hbs b/ui/app/templates/components/fs/breadcrumbs.hbs new file mode 100644 index 00000000000..d89192b6afd --- /dev/null +++ b/ui/app/templates/components/fs/breadcrumbs.hbs @@ -0,0 +1,14 @@ + diff --git a/ui/app/templates/components/fs/browser.hbs b/ui/app/templates/components/fs/browser.hbs new file mode 100644 index 00000000000..147d28bd7a0 --- /dev/null +++ b/ui/app/templates/components/fs/browser.hbs @@ -0,0 +1,47 @@ +
+ {{#if model.isRunning}} + {{#if isFile}} + {{#fs/file allocation=allocation task=task file=path stat=stat class="fs-explorer"}} + {{fs/breadcrumbs allocation=allocation task=task path=path}} + {{/fs/file}} + {{else}} +
+
+ {{fs/breadcrumbs allocation=allocation task=task path=path}} +
+ {{#if directoryEntries}} + {{#list-table + source=sortedDirectoryEntries + sortProperty=sortProperty + sortDescending=sortDescending + class="boxed-section-body is-full-bleed is-compact" as |t|}} + {{#t.head}} + {{#t.sort-by prop="Name" class="is-two-thirds"}}Name{{/t.sort-by}} + {{#t.sort-by prop="Size" class="has-text-right"}}File Size{{/t.sort-by}} + {{#t.sort-by prop="ModTime" class="has-text-right"}}Last Modified{{/t.sort-by}} + {{/t.head}} + {{#t.body as |row|}} + {{fs/directory-entry path=path allocation=allocation task=task entry=row.model}} + {{/t.body}} + {{/list-table}} + {{else}} +
+
+

No Files

+

+ Directory is currently empty. +

+
+
+ {{/if}} +
+ {{/if}} + {{else}} +
+

{{capitalize type}} is not Running

+

+ Cannot access files of a{{if allocation 'n'}} {{type}} that is not running. +

+
+ {{/if}} +
diff --git a/ui/app/templates/components/fs-directory-entry.hbs b/ui/app/templates/components/fs/directory-entry.hbs similarity index 79% rename from ui/app/templates/components/fs-directory-entry.hbs rename to ui/app/templates/components/fs/directory-entry.hbs index cc55d30ff6f..2b33428d177 100644 --- a/ui/app/templates/components/fs-directory-entry.hbs +++ b/ui/app/templates/components/fs/directory-entry.hbs @@ -1,6 +1,6 @@ - {{#link-to "allocations.allocation.task.fs" task.allocation task pathToEntry activeClass="is-active"}} + {{#fs/link allocation=allocation task=task path=pathToEntry}} {{#if entry.IsDir}} {{x-icon "folder-outline"}} {{else}} @@ -8,7 +8,7 @@ {{/if}} {{entry.Name}} - {{/link-to}} + {{/fs/link}} {{#unless entry.IsDir}}{{format-bytes entry.Size}}{{/unless}} {{moment-from entry.ModTime interval=1000}} diff --git a/ui/app/templates/components/task-file.hbs b/ui/app/templates/components/fs/file.hbs similarity index 93% rename from ui/app/templates/components/task-file.hbs rename to ui/app/templates/components/fs/file.hbs index bed5c27620f..83c6dcb5925 100644 --- a/ui/app/templates/components/task-file.hbs +++ b/ui/app/templates/components/fs/file.hbs @@ -1,7 +1,7 @@ {{#if noConnection}}

Cannot fetch file

-

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

+

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

{{/if}}
diff --git a/ui/app/templates/components/fs/link.hbs b/ui/app/templates/components/fs/link.hbs new file mode 100644 index 00000000000..98c37a3609d --- /dev/null +++ b/ui/app/templates/components/fs/link.hbs @@ -0,0 +1,21 @@ +{{#if task}} + {{#if path}} + {{#link-to "allocations.allocation.task.fs" allocation task path activeClass="is-active"}} + {{yield}} + {{/link-to}} + {{else}} + {{#link-to "allocations.allocation.task.fs-root" allocation task activeClass="is-active"}} + {{yield}} + {{/link-to}} + {{/if}} +{{else}} + {{#if path}} + {{#link-to "allocations.allocation.fs" allocation path activeClass="is-active"}} + {{yield}} + {{/link-to}} + {{else}} + {{#link-to "allocations.allocation.fs-root" allocation activeClass="is-active"}} + {{yield}} + {{/link-to}} + {{/if}} +{{/if}} \ No newline at end of file diff --git a/ui/mirage/config.js b/ui/mirage/config.js index 994135eef81..d901e118988 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -393,16 +393,14 @@ export default function() { return logEncode(logFrames, logFrames.length - 1); }; - const clientAllocationFSLsHandler = function({ allocFiles }, { queryParams }) { - // Ignore the task name at the beginning of the path - const filterPath = queryParams.path.substr(queryParams.path.indexOf('/') + 1); + const clientAllocationFSLsHandler = function({ allocFiles }, { queryParams: { path } }) { + const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path; const files = filesForPath(allocFiles, filterPath); return this.serialize(files); }; - const clientAllocationFSStatHandler = function({ allocFiles }, { queryParams }) { - // Ignore the task name at the beginning of the path - const filterPath = queryParams.path.substr(queryParams.path.indexOf('/') + 1); + const clientAllocationFSStatHandler = function({ allocFiles }, { queryParams: { path } }) { + const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path; // Root path if (!filterPath) { @@ -441,15 +439,12 @@ export default function() { }; const fileOrError = function(allocFiles, path, message = 'Operation not allowed on a directory') { - // Ignore the task name at the beginning of the path - const filterPath = path.substr(path.indexOf('/') + 1); - // Root path - if (!filterPath) { + if (path === '/') { return [null, new Response(400, {}, message)]; } - const file = allocFiles.where({ path: filterPath }).models[0]; + const file = allocFiles.where({ path }).models[0]; if (file.isDir) { return [null, new Response(400, {}, message)]; } diff --git a/ui/tests/acceptance/allocation-fs-test.js b/ui/tests/acceptance/allocation-fs-test.js new file mode 100644 index 00000000000..a14fb1fdffa --- /dev/null +++ b/ui/tests/acceptance/allocation-fs-test.js @@ -0,0 +1,77 @@ +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; + +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import Response from 'ember-cli-mirage/response'; + +import browseFilesystem from './behaviors/fs'; + +import FS from 'nomad-ui/tests/pages/allocations/fs'; + +let allocation; +let files; + +module('Acceptance | allocation fs', function(hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function() { + server.create('agent'); + server.create('node', 'forceIPv4'); + const job = server.create('job', { createAllocations: false }); + + allocation = server.create('allocation', { jobId: job.id, clientStatus: 'running' }); + + this.allocation = allocation; + + // Reset files + files = []; + + // Nested files + files.push(server.create('allocFile', { isDir: true, name: 'directory' })); + files.push(server.create('allocFile', { isDir: true, name: 'another', parent: files[0] })); + files.push( + server.create('allocFile', 'file', { + name: 'something.txt', + fileType: 'txt', + parent: files[1], + }) + ); + + files.push(server.create('allocFile', { isDir: true, name: 'empty-directory' })); + files.push(server.create('allocFile', 'file', { fileType: 'txt' })); + files.push(server.create('allocFile', 'file', { fileType: 'txt' })); + + this.files = files; + this.directory = files[0]; + this.nestedDirectory = files[1]; + }); + + test('when the allocation is not running, an empty state is shown', async function(assert) { + // The API 500s on stat when not running + this.server.get('/client/fs/stat/:allocation_id', () => { + return new Response(500, {}, 'no such file or directory'); + }); + + allocation.update({ + clientStatus: 'complete', + }); + + await FS.visitAllocation({ id: allocation.id }); + assert.ok(FS.hasEmptyState, 'Non-running allocation has no files'); + assert.ok( + FS.emptyState.headline.includes('Allocation is not Running'), + 'Empty state explains the condition' + ); + }); + + browseFilesystem({ + visitSegments: ({ allocation }) => ({ id: allocation.id }), + getExpectedPathBase: ({ allocation }) => `/allocations/${allocation.id}/fs/`, + getTitleComponent: ({ allocation }) => `Allocation ${allocation.id.split('-')[0]} filesystem`, + getBreadcrumbComponent: ({ allocation }) => allocation.id.split('-')[0], + getFilesystemRoot: () => '', + pageObjectVisitFunctionName: 'visitAllocation', + pageObjectVisitPathFunctionName: 'visitAllocationPath', + }); +}); diff --git a/ui/tests/acceptance/behaviors/fs.js b/ui/tests/acceptance/behaviors/fs.js new file mode 100644 index 00000000000..0d13c021f59 --- /dev/null +++ b/ui/tests/acceptance/behaviors/fs.js @@ -0,0 +1,339 @@ +import { test } from 'qunit'; +import { currentURL, visit } from '@ember/test-helpers'; + +import { filesForPath } from 'nomad-ui/mirage/config'; +import { formatBytes } from 'nomad-ui/helpers/format-bytes'; + +import Response from 'ember-cli-mirage/response'; +import moment from 'moment'; + +import FS from 'nomad-ui/tests/pages/allocations/fs'; + +const fileSort = (prop, files) => { + let dir = []; + let file = []; + files.forEach(f => { + if (f.isDir) { + dir.push(f); + } else { + file.push(f); + } + }); + + return dir.sortBy(prop).concat(file.sortBy(prop)); +}; + +export default function browseFilesystem({ pageObjectVisitPathFunctionName, pageObjectVisitFunctionName, visitSegments, getExpectedPathBase, getTitleComponent, getBreadcrumbComponent, getFilesystemRoot }) { + test('visiting filesystem root', async function(assert) { + await FS[pageObjectVisitFunctionName](visitSegments({allocation: this.allocation, task: this.task })); + + const pathBaseWithTrailingSlash = getExpectedPathBase({ allocation: this.allocation, task: this.task }); + const pathBaseWithoutTrailingSlash = pathBaseWithTrailingSlash.slice(0, -1); + + assert.equal(currentURL(), pathBaseWithoutTrailingSlash, 'No redirect'); + }); + + test('visiting filesystem paths', async function(assert) { + const paths = ['some-file.log', 'a/deep/path/to/a/file.log', '/', 'Unicode™®']; + + const testPath = async filePath => { + let pathWithLeadingSlash = filePath; + + if (!pathWithLeadingSlash.startsWith('/')) { + pathWithLeadingSlash = `/${filePath}`; + } + + await FS[pageObjectVisitPathFunctionName]({ ...visitSegments({allocation: this.allocation, task: this.task }), path: filePath }); + assert.equal( + currentURL(), + `${getExpectedPathBase({allocation: this.allocation, task: this.task })}${encodeURIComponent(filePath)}`, + 'No redirect' + ); + assert.equal( + document.title, + `${pathWithLeadingSlash} - ${getTitleComponent({allocation: this.allocation, task: this.task})} - Nomad` + ); + assert.equal(FS.breadcrumbsText, `${getBreadcrumbComponent({allocation: this.allocation, task: this.task})} ${filePath.replace(/\//g, ' ')}`.trim()); + }; + + await paths.reduce(async (prev, filePath) => { + await prev; + return testPath(filePath); + }, Promise.resolve()); + }); + + test('navigating allocation filesystem', async function(assert) { + const objects = { allocation: this.allocation, task: this.task }; + await FS[pageObjectVisitPathFunctionName]({ ...visitSegments(objects), path: '/' }); + + const sortedFiles = fileSort('name', filesForPath(this.server.schema.allocFiles, getFilesystemRoot(objects)).models); + + assert.ok(FS.fileViewer.isHidden); + + assert.equal(FS.directoryEntries.length, 4); + + assert.equal(FS.breadcrumbsText, getBreadcrumbComponent(objects)); + + assert.equal(FS.breadcrumbs.length, 1); + assert.ok(FS.breadcrumbs[0].isActive); + assert.equal(FS.breadcrumbs[0].text, getBreadcrumbComponent(objects)); + + FS.directoryEntries[0].as(directory => { + const fileRecord = sortedFiles[0]; + assert.equal(directory.name, fileRecord.name, 'directories should come first'); + assert.ok(directory.isDirectory); + assert.equal(directory.size, '', 'directory sizes are hidden'); + assert.equal(directory.lastModified, moment(fileRecord.modTime).fromNow()); + assert.notOk(directory.path.includes('//'), 'paths shouldn’t have redundant separators'); + }); + + FS.directoryEntries[2].as(file => { + const fileRecord = sortedFiles[2]; + assert.equal(file.name, fileRecord.name); + assert.ok(file.isFile); + assert.equal(file.size, formatBytes([fileRecord.size])); + assert.equal(file.lastModified, moment(fileRecord.modTime).fromNow()); + }); + + await FS.directoryEntries[0].visit(); + + assert.equal(FS.directoryEntries.length, 1); + + assert.equal(FS.breadcrumbs.length, 2); + assert.equal(FS.breadcrumbsText, `${getBreadcrumbComponent(objects)} ${this.directory.name}`); + + assert.notOk(FS.breadcrumbs[0].isActive); + + assert.equal(FS.breadcrumbs[1].text, this.directory.name); + assert.ok(FS.breadcrumbs[1].isActive); + + await FS.directoryEntries[0].visit(); + + assert.equal(FS.directoryEntries.length, 1); + assert.notOk( + FS.directoryEntries[0].path.includes('//'), + 'paths shouldn’t have redundant separators' + ); + + assert.equal(FS.breadcrumbs.length, 3); + assert.equal(FS.breadcrumbsText, `${getBreadcrumbComponent(objects)} ${this.directory.name} ${this.nestedDirectory.name}`); + assert.equal(FS.breadcrumbs[2].text, this.nestedDirectory.name); + + assert.notOk( + FS.breadcrumbs[0].path.includes('//'), + 'paths shouldn’t have redundant separators' + ); + assert.notOk( + FS.breadcrumbs[1].path.includes('//'), + 'paths shouldn’t have redundant separators' + ); + + await FS.breadcrumbs[1].visit(); + assert.equal(FS.breadcrumbsText, `${getBreadcrumbComponent(objects)} ${this.directory.name}`); + assert.equal(FS.breadcrumbs.length, 2); + }); + + test('sorting allocation filesystem directory', async function(assert) { + this.server.get('/client/fs/ls/:allocation_id', () => { + return [ + { + Name: 'aaa-big-old-file', + IsDir: false, + Size: 19190000, + ModTime: moment() + .subtract(1, 'year') + .format(), + }, + { + Name: 'mmm-small-mid-file', + IsDir: false, + Size: 1919, + ModTime: moment() + .subtract(6, 'month') + .format(), + }, + { + Name: 'zzz-med-new-file', + IsDir: false, + Size: 191900, + ModTime: moment().format(), + }, + { + Name: 'aaa-big-old-directory', + IsDir: true, + Size: 19190000, + ModTime: moment() + .subtract(1, 'year') + .format(), + }, + { + Name: 'mmm-small-mid-directory', + IsDir: true, + Size: 1919, + ModTime: moment() + .subtract(6, 'month') + .format(), + }, + { + Name: 'zzz-med-new-directory', + IsDir: true, + Size: 191900, + ModTime: moment().format(), + }, + ]; + }); + + await FS[pageObjectVisitPathFunctionName]({ ...visitSegments({allocation: this.allocation, task: this.task }), path: '/' }); + + assert.deepEqual(FS.directoryEntryNames(), [ + 'aaa-big-old-directory', + 'mmm-small-mid-directory', + 'zzz-med-new-directory', + 'aaa-big-old-file', + 'mmm-small-mid-file', + 'zzz-med-new-file', + ]); + + await FS.sortBy('Name'); + + assert.deepEqual(FS.directoryEntryNames(), [ + 'zzz-med-new-file', + 'mmm-small-mid-file', + 'aaa-big-old-file', + 'zzz-med-new-directory', + 'mmm-small-mid-directory', + 'aaa-big-old-directory', + ]); + + await FS.sortBy('ModTime'); + + assert.deepEqual(FS.directoryEntryNames(), [ + 'zzz-med-new-file', + 'mmm-small-mid-file', + 'aaa-big-old-file', + 'zzz-med-new-directory', + 'mmm-small-mid-directory', + 'aaa-big-old-directory', + ]); + + await FS.sortBy('ModTime'); + + assert.deepEqual(FS.directoryEntryNames(), [ + 'aaa-big-old-directory', + 'mmm-small-mid-directory', + 'zzz-med-new-directory', + 'aaa-big-old-file', + 'mmm-small-mid-file', + 'zzz-med-new-file', + ]); + + await FS.sortBy('Size'); + + assert.deepEqual( + FS.directoryEntryNames(), + [ + 'aaa-big-old-file', + 'zzz-med-new-file', + 'mmm-small-mid-file', + 'zzz-med-new-directory', + 'mmm-small-mid-directory', + 'aaa-big-old-directory', + ], + 'expected files to be sorted by descending size and directories to be sorted by descending name' + ); + + await FS.sortBy('Size'); + + assert.deepEqual( + FS.directoryEntryNames(), + [ + 'aaa-big-old-directory', + 'mmm-small-mid-directory', + 'zzz-med-new-directory', + 'mmm-small-mid-file', + 'zzz-med-new-file', + 'aaa-big-old-file', + ], + 'expected directories to be sorted by name and files to be sorted by ascending size' + ); + }); + + test('viewing a file', async function(assert) { + const objects = { allocation: this.allocation, task: this.task }; + const node = server.db.nodes.find(this.allocation.nodeId); + + server.get(`http://${node.httpAddr}/v1/client/fs/readat/:allocation_id`, function() { + return new Response(500); + }); + + await FS[pageObjectVisitPathFunctionName]({ ...visitSegments(objects), path: '/' }); + + const sortedFiles = fileSort('name', filesForPath(this.server.schema.allocFiles, getFilesystemRoot(objects)).models); + const fileRecord = sortedFiles.find(f => !f.isDir); + const fileIndex = sortedFiles.indexOf(fileRecord); + + await FS.directoryEntries[fileIndex].visit(); + + assert.equal(FS.breadcrumbsText, `${getBreadcrumbComponent(objects)} ${fileRecord.name}`); + + assert.ok(FS.fileViewer.isPresent); + + const requests = this.server.pretender.handledRequests; + const secondAttempt = requests.pop(); + const firstAttempt = requests.pop(); + + assert.equal( + firstAttempt.url.split('?')[0], + `//${node.httpAddr}/v1/client/fs/readat/${this.allocation.id}`, + 'Client is hit first' + ); + assert.equal(firstAttempt.status, 500, 'Client request fails'); + assert.equal( + secondAttempt.url.split('?')[0], + `/v1/client/fs/readat/${this.allocation.id}`, + 'Server is hit second' + ); + }); + + test('viewing an empty directory', async function(assert) { + await FS[pageObjectVisitPathFunctionName]({ ...visitSegments({ allocation: this.allocation, task: this.task }), path: 'empty-directory' }); + + assert.ok(FS.isEmptyDirectory); + }); + + test('viewing paths that produce stat API errors', async function(assert) { + this.server.get('/client/fs/stat/:allocation_id', () => { + return new Response(500, {}, 'no such file or directory'); + }); + + await FS[pageObjectVisitPathFunctionName]({ ...visitSegments({ allocation: this.allocation, task: this.task }), path: '/what-is-this' }); + assert.equal(FS.error.title, 'Not Found', '500 is interpreted as 404'); + + await visit('/'); + + this.server.get('/client/fs/stat/:allocation_id', () => { + return new Response(999); + }); + + await FS[pageObjectVisitPathFunctionName]({ ...visitSegments({ allocation: this.allocation, task: this.task }), path: '/what-is-this' }); + assert.equal(FS.error.title, 'Error', 'other statuses are passed through'); + }); + + test('viewing paths that produce ls API errors', async function(assert) { + this.server.get('/client/fs/ls/:allocation_id', () => { + return new Response(500, {}, 'no such file or directory'); + }); + + await FS[pageObjectVisitPathFunctionName]({ ...visitSegments({ allocation: this.allocation, task: this.task }), path: this.directory.name }); + assert.equal(FS.error.title, 'Not Found', '500 is interpreted as 404'); + + await visit('/'); + + this.server.get('/client/fs/ls/:allocation_id', () => { + return new Response(999); + }); + + await FS[pageObjectVisitPathFunctionName]({ ...visitSegments({ allocation: this.allocation, task: this.task }), path: this.directory.name }); + assert.equal(FS.error.title, 'Error', 'other statuses are passed through'); + }); +} diff --git a/ui/tests/acceptance/task-fs-test.js b/ui/tests/acceptance/task-fs-test.js index cd9d0c40847..4604aa61c3a 100644 --- a/ui/tests/acceptance/task-fs-test.js +++ b/ui/tests/acceptance/task-fs-test.js @@ -1,34 +1,16 @@ -import { currentURL, visit } from '@ember/test-helpers'; -import { Promise } from 'rsvp'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; -import moment from 'moment'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import Response from 'ember-cli-mirage/response'; -import { formatBytes } from 'nomad-ui/helpers/format-bytes'; -import { filesForPath } from 'nomad-ui/mirage/config'; +import browseFilesystem from './behaviors/fs'; -import FS from 'nomad-ui/tests/pages/allocations/task/fs'; +import FS from 'nomad-ui/tests/pages/allocations/fs'; let allocation; let task; -let files; - -const fileSort = (prop, files) => { - let dir = []; - let file = []; - files.forEach(f => { - if (f.isDir) { - dir.push(f); - } else { - file.push(f); - } - }); - - return dir.sortBy(prop).concat(file.sortBy(prop)); -}; +let files, taskDirectory, directory, nestedDirectory; module('Acceptance | task fs', function(hooks) { setupApplicationTest(hooks); @@ -44,28 +26,37 @@ module('Acceptance | task fs', function(hooks) { task.name = 'task-name'; task.save(); + this.task = task; + this.allocation = allocation; + // Reset files files = []; + taskDirectory = server.create('allocFile', { isDir: true, name: task.name }); + files.push(taskDirectory); + // Nested files - files.push(server.create('allocFile', { isDir: true, name: 'directory' })); - files.push(server.create('allocFile', { isDir: true, name: 'another', parent: files[0] })); + directory = server.create('allocFile', { isDir: true, name: 'directory', parent: taskDirectory }); + files.push(directory); + + nestedDirectory = server.create('allocFile', { isDir: true, name: 'another', parent: directory }); + files.push(nestedDirectory); + files.push( server.create('allocFile', 'file', { name: 'something.txt', fileType: 'txt', - parent: files[1], + parent: nestedDirectory, }) ); - files.push(server.create('allocFile', { isDir: true, name: 'empty-directory' })); - files.push(server.create('allocFile', 'file', { fileType: 'txt' })); - files.push(server.create('allocFile', 'file', { fileType: 'txt' })); - }); + files.push(server.create('allocFile', { isDir: true, name: 'empty-directory', parent: taskDirectory })); + files.push(server.create('allocFile', 'file', { fileType: 'txt', parent: taskDirectory })); + files.push(server.create('allocFile', 'file', { fileType: 'txt', parent: taskDirectory })); - test('visiting /allocations/:allocation_id/:task_name/fs', async function(assert) { - await FS.visit({ id: allocation.id, name: task.name }); - assert.equal(currentURL(), `/allocations/${allocation.id}/${task.name}/fs`, 'No redirect'); + this.files = files; + this.directory = directory; + this.nestedDirectory = nestedDirectory; }); test('when the task is not running, an empty state is shown', async function(assert) { @@ -78,7 +69,7 @@ module('Acceptance | task fs', function(hooks) { finishedAt: new Date(), }); - await FS.visit({ id: allocation.id, name: task.name }); + await FS.visitTask({ id: allocation.id, name: task.name }); assert.ok(FS.hasEmptyState, 'Non-running task has no files'); assert.ok( FS.emptyState.headline.includes('Task is not Running'), @@ -86,305 +77,13 @@ module('Acceptance | task fs', function(hooks) { ); }); - test('visiting /allocations/:allocation_id/:task_name/fs/:path', async function(assert) { - const paths = ['some-file.log', 'a/deep/path/to/a/file.log', '/', 'Unicode™®']; - - const testPath = async filePath => { - let pathWithLeadingSlash = filePath; - - if (!pathWithLeadingSlash.startsWith('/')) { - pathWithLeadingSlash = `/${filePath}`; - } - - await FS.visitPath({ id: allocation.id, name: task.name, path: filePath }); - assert.equal( - currentURL(), - `/allocations/${allocation.id}/${task.name}/fs/${encodeURIComponent(filePath)}`, - 'No redirect' - ); - assert.equal( - document.title, - `${pathWithLeadingSlash} - Task ${task.name} filesystem - Nomad` - ); - assert.equal(FS.breadcrumbsText, `${task.name} ${filePath.replace(/\//g, ' ')}`.trim()); - }; - - await paths.reduce(async (prev, filePath) => { - await prev; - return testPath(filePath); - }, Promise.resolve()); - }); - - test('navigating allocation filesystem', async function(assert) { - await FS.visitPath({ id: allocation.id, name: task.name, path: '/' }); - - const sortedFiles = fileSort('name', filesForPath(this.server.schema.allocFiles, '').models); - - assert.ok(FS.fileViewer.isHidden); - - assert.equal(FS.directoryEntries.length, 4); - - assert.equal(FS.breadcrumbsText, task.name); - - assert.equal(FS.breadcrumbs.length, 1); - assert.ok(FS.breadcrumbs[0].isActive); - assert.equal(FS.breadcrumbs[0].text, 'task-name'); - - FS.directoryEntries[0].as(directory => { - const fileRecord = sortedFiles[0]; - assert.equal(directory.name, fileRecord.name, 'directories should come first'); - assert.ok(directory.isDirectory); - assert.equal(directory.size, '', 'directory sizes are hidden'); - assert.equal(directory.lastModified, moment(fileRecord.modTime).fromNow()); - assert.notOk(directory.path.includes('//'), 'paths shouldn’t have redundant separators'); - }); - - FS.directoryEntries[2].as(file => { - const fileRecord = sortedFiles[2]; - assert.equal(file.name, fileRecord.name); - assert.ok(file.isFile); - assert.equal(file.size, formatBytes([fileRecord.size])); - assert.equal(file.lastModified, moment(fileRecord.modTime).fromNow()); - }); - - await FS.directoryEntries[0].visit(); - - assert.equal(FS.directoryEntries.length, 1); - - assert.equal(FS.breadcrumbs.length, 2); - assert.equal(FS.breadcrumbsText, `${task.name} ${files[0].name}`); - - assert.notOk(FS.breadcrumbs[0].isActive); - - assert.equal(FS.breadcrumbs[1].text, files[0].name); - assert.ok(FS.breadcrumbs[1].isActive); - - await FS.directoryEntries[0].visit(); - - assert.equal(FS.directoryEntries.length, 1); - assert.notOk( - FS.directoryEntries[0].path.includes('//'), - 'paths shouldn’t have redundant separators' - ); - - assert.equal(FS.breadcrumbs.length, 3); - assert.equal(FS.breadcrumbsText, `${task.name} ${files[0].name} ${files[1].name}`); - assert.equal(FS.breadcrumbs[2].text, files[1].name); - - assert.notOk( - FS.breadcrumbs[0].path.includes('//'), - 'paths shouldn’t have redundant separators' - ); - assert.notOk( - FS.breadcrumbs[1].path.includes('//'), - 'paths shouldn’t have redundant separators' - ); - - await FS.breadcrumbs[1].visit(); - assert.equal(FS.breadcrumbsText, `${task.name} ${files[0].name}`); - assert.equal(FS.breadcrumbs.length, 2); - }); - - test('sorting allocation filesystem directory', async function(assert) { - this.server.get('/client/fs/ls/:allocation_id', () => { - return [ - { - Name: 'aaa-big-old-file', - IsDir: false, - Size: 19190000, - ModTime: moment() - .subtract(1, 'year') - .format(), - }, - { - Name: 'mmm-small-mid-file', - IsDir: false, - Size: 1919, - ModTime: moment() - .subtract(6, 'month') - .format(), - }, - { - Name: 'zzz-med-new-file', - IsDir: false, - Size: 191900, - ModTime: moment().format(), - }, - { - Name: 'aaa-big-old-directory', - IsDir: true, - Size: 19190000, - ModTime: moment() - .subtract(1, 'year') - .format(), - }, - { - Name: 'mmm-small-mid-directory', - IsDir: true, - Size: 1919, - ModTime: moment() - .subtract(6, 'month') - .format(), - }, - { - Name: 'zzz-med-new-directory', - IsDir: true, - Size: 191900, - ModTime: moment().format(), - }, - ]; - }); - - await FS.visitPath({ id: allocation.id, name: task.name, path: '/' }); - - assert.deepEqual(FS.directoryEntryNames(), [ - 'aaa-big-old-directory', - 'mmm-small-mid-directory', - 'zzz-med-new-directory', - 'aaa-big-old-file', - 'mmm-small-mid-file', - 'zzz-med-new-file', - ]); - - await FS.sortBy('Name'); - - assert.deepEqual(FS.directoryEntryNames(), [ - 'zzz-med-new-file', - 'mmm-small-mid-file', - 'aaa-big-old-file', - 'zzz-med-new-directory', - 'mmm-small-mid-directory', - 'aaa-big-old-directory', - ]); - - await FS.sortBy('ModTime'); - - assert.deepEqual(FS.directoryEntryNames(), [ - 'zzz-med-new-file', - 'mmm-small-mid-file', - 'aaa-big-old-file', - 'zzz-med-new-directory', - 'mmm-small-mid-directory', - 'aaa-big-old-directory', - ]); - - await FS.sortBy('ModTime'); - - assert.deepEqual(FS.directoryEntryNames(), [ - 'aaa-big-old-directory', - 'mmm-small-mid-directory', - 'zzz-med-new-directory', - 'aaa-big-old-file', - 'mmm-small-mid-file', - 'zzz-med-new-file', - ]); - - await FS.sortBy('Size'); - - assert.deepEqual( - FS.directoryEntryNames(), - [ - 'aaa-big-old-file', - 'zzz-med-new-file', - 'mmm-small-mid-file', - 'zzz-med-new-directory', - 'mmm-small-mid-directory', - 'aaa-big-old-directory', - ], - 'expected files to be sorted by descending size and directories to be sorted by descending name' - ); - - await FS.sortBy('Size'); - - assert.deepEqual( - FS.directoryEntryNames(), - [ - 'aaa-big-old-directory', - 'mmm-small-mid-directory', - 'zzz-med-new-directory', - 'mmm-small-mid-file', - 'zzz-med-new-file', - 'aaa-big-old-file', - ], - 'expected directories to be sorted by name and files to be sorted by ascending size' - ); - }); - - test('viewing a file', async function(assert) { - const node = server.db.nodes.find(allocation.nodeId); - - server.get(`http://${node.httpAddr}/v1/client/fs/readat/:allocation_id`, function() { - return new Response(500); - }); - - await FS.visitPath({ id: allocation.id, name: task.name, path: '/' }); - - const sortedFiles = fileSort('name', filesForPath(this.server.schema.allocFiles, '').models); - const fileRecord = sortedFiles.find(f => !f.isDir); - const fileIndex = sortedFiles.indexOf(fileRecord); - - await FS.directoryEntries[fileIndex].visit(); - - assert.equal(FS.breadcrumbsText, `${task.name} ${fileRecord.name}`); - - assert.ok(FS.fileViewer.isPresent); - - const requests = this.server.pretender.handledRequests; - const secondAttempt = requests.pop(); - const firstAttempt = requests.pop(); - - assert.equal( - firstAttempt.url.split('?')[0], - `//${node.httpAddr}/v1/client/fs/readat/${allocation.id}`, - 'Client is hit first' - ); - assert.equal(firstAttempt.status, 500, 'Client request fails'); - assert.equal( - secondAttempt.url.split('?')[0], - `/v1/client/fs/readat/${allocation.id}`, - 'Server is hit second' - ); - }); - - test('viewing an empty directory', async function(assert) { - await FS.visitPath({ id: allocation.id, name: task.name, path: '/empty-directory' }); - - assert.ok(FS.isEmptyDirectory); - }); - - test('viewing paths that produce stat API errors', async function(assert) { - this.server.get('/client/fs/stat/:allocation_id', () => { - return new Response(500, {}, 'no such file or directory'); - }); - - await FS.visitPath({ id: allocation.id, name: task.name, path: '/what-is-this' }); - assert.equal(FS.error.title, 'Not Found', '500 is interpreted as 404'); - - await visit('/'); - - this.server.get('/client/fs/stat/:allocation_id', () => { - return new Response(999); - }); - - await FS.visitPath({ id: allocation.id, name: task.name, path: '/what-is-this' }); - assert.equal(FS.error.title, 'Error', 'other statuses are passed through'); - }); - - test('viewing paths that produce ls API errors', async function(assert) { - this.server.get('/client/fs/ls/:allocation_id', () => { - return new Response(500, {}, 'no such file or directory'); - }); - - await FS.visitPath({ id: allocation.id, name: task.name, path: files[0].name }); - assert.equal(FS.error.title, 'Not Found', '500 is interpreted as 404'); - - await visit('/'); - - this.server.get('/client/fs/ls/:allocation_id', () => { - return new Response(999); - }); - - await FS.visitPath({ id: allocation.id, name: task.name, path: files[0].name }); - assert.equal(FS.error.title, 'Error', 'other statuses are passed through'); + browseFilesystem({ + visitSegments: ({allocation,task}) => ({ id: allocation.id, name: task.name }), + getExpectedPathBase: ({allocation,task}) => `/allocations/${allocation.id}/${task.name}/fs/`, + getTitleComponent: ({task}) => `Task ${task.name} filesystem`, + getBreadcrumbComponent: ({task}) => task.name, + getFilesystemRoot: ({ task }) => task.name, + pageObjectVisitFunctionName: 'visitTask', + pageObjectVisitPathFunctionName: 'visitTaskPath', }); }); diff --git a/ui/tests/integration/task-file-test.js b/ui/tests/integration/fs/file-test.js similarity index 96% rename from ui/tests/integration/task-file-test.js rename to ui/tests/integration/fs/file-test.js index 47a89ade441..52ab9fd2129 100644 --- a/ui/tests/integration/task-file-test.js +++ b/ui/tests/integration/fs/file-test.js @@ -3,12 +3,12 @@ import { setupRenderingTest } from 'ember-qunit'; import { find, render, settled } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import Pretender from 'pretender'; -import { logEncode } from '../../mirage/data/logs'; +import { logEncode } from '../../../mirage/data/logs'; const { assign } = Object; const HOST = '1.1.1.1:1111'; -module('Integration | Component | task file', function(hooks) { +module('Integration | Component | fs/file', function(hooks) { setupRenderingTest(hooks); hooks.beforeEach(function() { @@ -25,7 +25,7 @@ module('Integration | Component | task file', function(hooks) { }); const commonTemplate = hbs` - {{task-file allocation=allocation task=task file=file stat=stat}} + {{fs/file allocation=allocation task=task file=file stat=stat}} `; const fileStat = (type, size = 0) => ({ @@ -184,9 +184,9 @@ module('Integration | Component | task file', function(hooks) { this.setProperties(props); await render(hbs` - {{#task-file allocation=allocation task=task file=file stat=stat}} + {{#fs/file allocation=allocation task=task file=file stat=stat}}
Yielded content
- {{/task-file}} + {{/fs/file}} `); assert.ok( diff --git a/ui/tests/pages/allocations/task/fs.js b/ui/tests/pages/allocations/fs.js similarity index 85% rename from ui/tests/pages/allocations/task/fs.js rename to ui/tests/pages/allocations/fs.js index e6b6155b8cc..fbfc8f9a73b 100644 --- a/ui/tests/pages/allocations/task/fs.js +++ b/ui/tests/pages/allocations/fs.js @@ -10,8 +10,11 @@ import { } from 'ember-cli-page-object'; export default create({ - visit: visitable('/allocations/:id/:name/fs'), - visitPath: visitable('/allocations/:id/:name/fs/:path'), + visitAllocation: visitable('/allocations/:id/fs'), + visitAllocationPath: visitable('/allocations/:id/fs/:path'), + + visitTask: visitable('/allocations/:id/:name/fs'), + visitTaskPath: visitable('/allocations/:id/:name/fs/:path'), fileViewer: { scope: '[data-test-file-viewer]',