From 5ba1d54f5a9cfcafcf0decfc61acd6cecb147b3e Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 1 Jun 2020 08:15:59 -0500 Subject: [PATCH] UI: add filesystem browsing for allocations (#7951) This partially addresses #7799. Task state filesystems are contained within a subdirectory of their parent allocation, so almost everything that existed for browsing task state filesystems was applicable to browsing allocations, just without the task name prepended to the path. I aimed to push this differential handling into as few contained places as possible. The tests also have significant overlap, so this includes an extracted behavior to run the same tests for allocations and task states. --- CHANGELOG.md | 1 + ui/app/adapters/allocation.js | 33 ++ ui/app/adapters/task-state.js | 39 -- ui/app/components/allocation-subnav.js | 14 + .../{fs-breadcrumbs.js => fs/breadcrumbs.js} | 1 + ui/app/components/fs/browser.js | 57 +++ .../directory-entry.js} | 3 + .../components/{task-file.js => fs/file.js} | 6 +- ui/app/components/fs/link.js | 8 + .../allocations/allocation/fs-root.js | 3 + .../controllers/allocations/allocation/fs.js | 28 ++ .../allocations/allocation/task/fs.js | 26 -- ui/app/models/allocation.js | 8 + ui/app/models/task-state.js | 8 - ui/app/router.js | 3 + .../routes/allocations/allocation/fs-root.js | 5 + ui/app/routes/allocations/allocation/fs.js | 42 ++ .../routes/allocations/allocation/task/fs.js | 5 +- .../templates/allocations/allocation/fs.hbs | 10 + .../allocations/allocation/index.hbs | 1 + .../allocations/allocation/task/fs.hbs | 55 +-- .../components/allocation-subnav.hbs | 6 + .../templates/components/fs-breadcrumbs.hbs | 14 - .../templates/components/fs/breadcrumbs.hbs | 14 + ui/app/templates/components/fs/browser.hbs | 47 +++ .../directory-entry.hbs} | 4 +- .../components/{task-file.hbs => fs/file.hbs} | 2 +- ui/app/templates/components/fs/link.hbs | 21 + ui/mirage/config.js | 17 +- ui/tests/acceptance/allocation-fs-test.js | 77 ++++ ui/tests/acceptance/behaviors/fs.js | 339 ++++++++++++++++ ui/tests/acceptance/task-fs-test.js | 363 ++---------------- .../{task-file-test.js => fs/file-test.js} | 10 +- ui/tests/pages/allocations/{task => }/fs.js | 7 +- 34 files changed, 786 insertions(+), 491 deletions(-) delete mode 100644 ui/app/adapters/task-state.js create mode 100644 ui/app/components/allocation-subnav.js rename ui/app/components/{fs-breadcrumbs.js => fs/breadcrumbs.js} (97%) create mode 100644 ui/app/components/fs/browser.js rename ui/app/components/{fs-directory-entry.js => fs/directory-entry.js} (93%) rename ui/app/components/{task-file.js => fs/file.js} (93%) create mode 100644 ui/app/components/fs/link.js create mode 100644 ui/app/controllers/allocations/allocation/fs-root.js create mode 100644 ui/app/controllers/allocations/allocation/fs.js create mode 100644 ui/app/routes/allocations/allocation/fs-root.js create mode 100644 ui/app/routes/allocations/allocation/fs.js create mode 100644 ui/app/templates/allocations/allocation/fs.hbs create mode 100644 ui/app/templates/components/allocation-subnav.hbs delete mode 100644 ui/app/templates/components/fs-breadcrumbs.hbs create mode 100644 ui/app/templates/components/fs/breadcrumbs.hbs create mode 100644 ui/app/templates/components/fs/browser.hbs rename ui/app/templates/components/{fs-directory-entry.hbs => fs/directory-entry.hbs} (79%) rename ui/app/templates/components/{task-file.hbs => fs/file.hbs} (93%) create mode 100644 ui/app/templates/components/fs/link.hbs create mode 100644 ui/tests/acceptance/allocation-fs-test.js create mode 100644 ui/tests/acceptance/behaviors/fs.js rename ui/tests/integration/{task-file-test.js => fs/file-test.js} (96%) rename ui/tests/pages/allocations/{task => }/fs.js (85%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a84b8db888..7f14cefa576 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,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 @@ -
    -
  • - {{#link-to "allocations.allocation.task.fs-root" task.allocation task activeClass="is-active"}} - {{task.name}} - {{/link-to}} -
  • - {{#each breadcrumbs as |breadcrumb|}} -
  • - {{#link-to "allocations.allocation.task.fs" task.allocation task breadcrumb.path activeClass="is-active"}} - {{breadcrumb.name}} - {{/link-to}} -
  • - {{/each}} -
\ 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 @@ +
    +
  • + {{#fs/link allocation=allocation task=task}} + {{if task task.name allocation.shortId}} + {{/fs/link}} +
  • + {{#each breadcrumbs as |breadcrumb|}} +
  • + {{#fs/link allocation=allocation task=task path=breadcrumb.path}} + {{breadcrumb.name}} + {{/fs/link}} +
  • + {{/each}} +
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 367dc67bacf..a4454a49d07 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]',