diff --git a/ui/app/controllers/csi/plugins/index.js b/ui/app/controllers/csi/plugins/index.js index 51f6c5a61fe..ba62315b905 100644 --- a/ui/app/controllers/csi/plugins/index.js +++ b/ui/app/controllers/csi/plugins/index.js @@ -1,40 +1,49 @@ import { inject as service } from '@ember/service'; +import { computed } from '@ember/object'; import { alias, readOnly } from '@ember/object/computed'; import Controller, { inject as controller } from '@ember/controller'; import SortableFactory from 'nomad-ui/mixins/sortable-factory'; +import Searchable from 'nomad-ui/mixins/searchable'; import { lazyClick } from 'nomad-ui/helpers/lazy-click'; -export default Controller.extend(SortableFactory([]), { - userSettings: service(), - pluginsController: controller('csi/plugins'), - - isForbidden: alias('pluginsController.isForbidden'), - - queryParams: { - currentPage: 'page', - sortProperty: 'sort', - sortDescending: 'desc', - }, +export default Controller.extend( + SortableFactory([ + 'plainId', + 'controllersHealthyProportion', + 'nodesHealthyProportion', + 'provider', + ]), + Searchable, + { + userSettings: service(), + pluginsController: controller('csi/plugins'), + + isForbidden: alias('pluginsController.isForbidden'), + + queryParams: { + currentPage: 'page', + searchTerm: 'search', + sortProperty: 'sort', + sortDescending: 'desc', + }, - currentPage: 1, - pageSize: readOnly('userSettings.pageSize'), + currentPage: 1, + pageSize: readOnly('userSettings.pageSize'), - sortProperty: 'id', - sortDescending: false, + searchProps: computed(() => ['id']), + fuzzySearchProps: computed(() => ['id']), - listToSort: alias('model'), - sortedPlugins: alias('listSorted'), + sortProperty: 'id', + sortDescending: false, - // TODO: Remove once this page gets search capability - resetPagination() { - if (this.currentPage != null) { - this.set('currentPage', 1); - } - }, + listToSort: alias('model'), + listToSearch: alias('listSorted'), + sortedPlugins: alias('listSearched'), - actions: { - gotoPlugin(plugin, event) { - lazyClick([() => this.transitionToRoute('csi.plugins.plugin', plugin.plainId), event]); + actions: { + gotoPlugin(plugin, event) { + lazyClick([() => this.transitionToRoute('csi.plugins.plugin', plugin.plainId), event]); + }, }, - }, -}); + } +); diff --git a/ui/app/controllers/csi/volumes/index.js b/ui/app/controllers/csi/volumes/index.js index aa63bb51b69..20aad240660 100644 --- a/ui/app/controllers/csi/volumes/index.js +++ b/ui/app/controllers/csi/volumes/index.js @@ -3,6 +3,7 @@ import { computed } from '@ember/object'; import { alias, readOnly } from '@ember/object/computed'; import Controller, { inject as controller } from '@ember/controller'; import SortableFactory from 'nomad-ui/mixins/sortable-factory'; +import Searchable from 'nomad-ui/mixins/searchable'; import { lazyClick } from 'nomad-ui/helpers/lazy-click'; export default Controller.extend( @@ -13,6 +14,7 @@ export default Controller.extend( 'nodesHealthyProportion', 'provider', ]), + Searchable, { system: service(), userSettings: service(), @@ -22,6 +24,7 @@ export default Controller.extend( queryParams: { currentPage: 'page', + searchTerm: 'search', sortProperty: 'sort', sortDescending: 'desc', }, @@ -32,6 +35,10 @@ export default Controller.extend( sortProperty: 'id', sortDescending: false, + searchProps: computed(() => ['name']), + fuzzySearchProps: computed(() => ['name']), + fuzzySearchEnabled: true, + /** Visible volumes are those that match the selected namespace */ @@ -49,14 +56,8 @@ export default Controller.extend( }), listToSort: alias('visibleVolumes'), - sortedVolumes: alias('listSorted'), - - // TODO: Remove once this page gets search capability - resetPagination() { - if (this.currentPage != null) { - this.set('currentPage', 1); - } - }, + listToSearch: alias('listSorted'), + sortedVolumes: alias('listSearched'), actions: { gotoVolume(volume, event) { diff --git a/ui/app/templates/csi/plugins/index.hbs b/ui/app/templates/csi/plugins/index.hbs index 43f7eb8a72f..7d4f5f1f1f9 100644 --- a/ui/app/templates/csi/plugins/index.hbs +++ b/ui/app/templates/csi/plugins/index.hbs @@ -9,6 +9,17 @@ {{#if isForbidden}} {{partial "partials/forbidden-message"}} {{else}} +
+
+ {{#if model.length}} + {{search-box + data-test-plugins-search + searchTerm=(mut searchTerm) + onChange=(action resetPagination) + placeholder="Search plugins..."}} + {{/if}} +
+
{{#if sortedPlugins}} {{#list-pagination source=sortedPlugins @@ -56,10 +67,17 @@ {{/list-pagination}} {{else}}
-

No Plugins

-

- The cluster currently has no registered CSI Plugins. -

+ {{#if (eq model.length 0)}} +

No Plugins

+

+ The cluster currently has no registered CSI Plugins. +

+ {{else if searchTerm}} +

No Matches

+

+ No plugins match the term {{searchTerm}} +

+ {{/if}}
{{/if}} {{/if}} diff --git a/ui/app/templates/csi/volumes/index.hbs b/ui/app/templates/csi/volumes/index.hbs index 6b4b41fafb8..67f5493f751 100644 --- a/ui/app/templates/csi/volumes/index.hbs +++ b/ui/app/templates/csi/volumes/index.hbs @@ -9,6 +9,17 @@ {{#if isForbidden}} {{partial "partials/forbidden-message"}} {{else}} +
+
+ {{#if model.length}} + {{search-box + data-test-volumes-search + searchTerm=(mut searchTerm) + onChange=(action resetPagination) + placeholder="Search volumes..."}} + {{/if}} +
+
{{#if sortedVolumes}} {{#list-pagination source=sortedVolumes @@ -60,10 +71,17 @@ {{/list-pagination}} {{else}}
-

No Volumes

-

- The cluster currently has no CSI Volumes. -

+ {{#if (eq model.length 0)}} +

No Volumes

+

+ The cluster currently has no CSI Volumes. +

+ {{else if searchTerm}} +

No Matches

+

+ No volumes match the term {{searchTerm}} +

+ {{/if}}
{{/if}} {{/if}} diff --git a/ui/mirage/factories/csi-plugin.js b/ui/mirage/factories/csi-plugin.js index e57aea40ea7..b0f95354532 100644 --- a/ui/mirage/factories/csi-plugin.js +++ b/ui/mirage/factories/csi-plugin.js @@ -30,6 +30,9 @@ export default Factory.extend({ // When false, the plugin will not make its own volumes createVolumes: true, + // When true, doesn't create any resources, state, or events for associated allocations + shallow: false, + afterCreate(plugin, server) { let storageNodes; let storageControllers; @@ -37,16 +40,32 @@ export default Factory.extend({ if (plugin.isMonolith) { const pluginJob = server.create('job', { type: 'service', createAllocations: false }); const count = plugin.nodesExpected; - storageNodes = server.createList('storage-node', count, { job: pluginJob }); - storageControllers = server.createList('storage-controller', count, { job: pluginJob }); + storageNodes = server.createList('storage-node', count, { + job: pluginJob, + shallow: plugin.shallow, + }); + storageControllers = server.createList('storage-controller', count, { + job: pluginJob, + shallow: plugin.shallow, + }); } else { - const controllerJob = server.create('job', { type: 'service', createAllocations: false }); - const nodeJob = server.create('job', { type: 'service', createAllocations: false }); + const controllerJob = server.create('job', { + type: 'service', + createAllocations: false, + shallow: plugin.shallow, + }); + const nodeJob = server.create('job', { + type: 'service', + createAllocations: false, + shallow: plugin.shallow, + }); storageNodes = server.createList('storage-node', plugin.nodesExpected, { job: nodeJob, + shallow: plugin.shallow, }); storageControllers = server.createList('storage-controller', plugin.controllersExpected, { job: controllerJob, + shallow: plugin.shallow, }); } diff --git a/ui/mirage/factories/storage-controller.js b/ui/mirage/factories/storage-controller.js index 3557116692a..4399e375bf7 100644 --- a/ui/mirage/factories/storage-controller.js +++ b/ui/mirage/factories/storage-controller.js @@ -17,6 +17,8 @@ export default Factory.extend({ requiresControllerPlugin: true, requiresTopologies: true, + shallow: false, + controllerInfo: () => ({ SupportsReadOnlyAttach: true, SupportsAttachDetach: true, @@ -29,6 +31,7 @@ export default Factory.extend({ jobId: storageController.job.id, forceRunningClientStatus: true, modifyTime: storageController.updateTime * 1000000, + shallow: storageController.shallow, }); storageController.update({ diff --git a/ui/mirage/factories/storage-node.js b/ui/mirage/factories/storage-node.js index 88a70020d4d..b5224195929 100644 --- a/ui/mirage/factories/storage-node.js +++ b/ui/mirage/factories/storage-node.js @@ -17,6 +17,8 @@ export default Factory.extend({ requiresControllerPlugin: true, requiresTopologies: true, + shallow: false, + nodeInfo: () => ({ MaxVolumes: 51, AccessibleTopology: { @@ -29,6 +31,7 @@ export default Factory.extend({ const alloc = server.create('allocation', { jobId: storageNode.job.id, modifyTime: storageNode.updateTime * 1000000, + shallow: storageNode.shallow, }); storageNode.update({ diff --git a/ui/tests/acceptance/plugins-list-test.js b/ui/tests/acceptance/plugins-list-test.js index 3b66a31c0c4..f86e2d66120 100644 --- a/ui/tests/acceptance/plugins-list-test.js +++ b/ui/tests/acceptance/plugins-list-test.js @@ -23,7 +23,7 @@ module('Acceptance | plugins list', function(hooks) { test('/csi/plugins should list the first page of plugins sorted by id', async function(assert) { const pluginCount = PluginsList.pageSize + 1; - server.createList('csi-plugin', pluginCount); + server.createList('csi-plugin', pluginCount, { shallow: true }); await PluginsList.visit(); @@ -35,7 +35,7 @@ module('Acceptance | plugins list', function(hooks) { }); test('each plugin row should contain information about the plugin', async function(assert) { - const plugin = server.create('csi-plugin'); + const plugin = server.create('csi-plugin', { shallow: true }); await PluginsList.visit(); @@ -56,7 +56,7 @@ module('Acceptance | plugins list', function(hooks) { }); test('each plugin row should link to the corresponding plugin', async function(assert) { - const plugin = server.create('csi-plugin'); + const plugin = server.create('csi-plugin', { shallow: true }); await PluginsList.visit(); @@ -77,6 +77,30 @@ module('Acceptance | plugins list', function(hooks) { assert.equal(PluginsList.emptyState.headline, 'No Plugins'); }); + test('when there are plugins, but no matches for a search, there is an empty message', async function(assert) { + server.create('csi-plugin', { id: 'cat 1', shallow: true }); + server.create('csi-plugin', { id: 'cat 2', shallow: true }); + + await PluginsList.visit(); + + await PluginsList.search('dog'); + assert.ok(PluginsList.isEmpty); + assert.equal(PluginsList.emptyState.headline, 'No Matches'); + }); + + test('search resets the current page', async function(assert) { + server.createList('csi-plugin', PluginsList.pageSize + 1, { shallow: true }); + + await PluginsList.visit(); + await PluginsList.nextPage(); + + assert.equal(currentURL(), '/csi/plugins?page=2'); + + await PluginsList.search('foobar'); + + assert.equal(currentURL(), '/csi/plugins?search=foobar'); + }); + test('when accessing plugins is forbidden, a message is shown with a link to the tokens page', async function(assert) { server.pretender.get('/v1/plugins', () => [403, {}, null]); @@ -92,7 +116,7 @@ module('Acceptance | plugins list', function(hooks) { pageObject: PluginsList, pageObjectList: PluginsList.plugins, async setup() { - server.createList('csi-plugin', PluginsList.pageSize); + server.createList('csi-plugin', PluginsList.pageSize, { shallow: true }); await PluginsList.visit(); }, }); diff --git a/ui/tests/acceptance/volumes-list-test.js b/ui/tests/acceptance/volumes-list-test.js index 4aff4b92f0f..4c15ff44ac4 100644 --- a/ui/tests/acceptance/volumes-list-test.js +++ b/ui/tests/acceptance/volumes-list-test.js @@ -101,6 +101,30 @@ module('Acceptance | volumes list', function(hooks) { assert.equal(VolumesList.emptyState.headline, 'No Volumes'); }); + test('when there are volumes, but no matches for a search, there is an empty message', async function(assert) { + server.create('csi-volume', { id: 'cat 1' }); + server.create('csi-volume', { id: 'cat 2' }); + + await VolumesList.visit(); + + await VolumesList.search('dog'); + assert.ok(VolumesList.isEmpty); + assert.equal(VolumesList.emptyState.headline, 'No Matches'); + }); + + test('searching resets the current page', async function(assert) { + server.createList('csi-volume', VolumesList.pageSize + 1); + + await VolumesList.visit(); + await VolumesList.nextPage(); + + assert.equal(currentURL(), '/csi/volumes?page=2'); + + await VolumesList.search('foobar'); + + assert.equal(currentURL(), '/csi/volumes?search=foobar'); + }); + test('when the namespace query param is set, only matching volumes are shown and the namespace value is forwarded to app state', async function(assert) { server.createList('namespace', 2); const volume1 = server.create('csi-volume', { namespaceId: server.db.namespaces[0].id }); diff --git a/ui/tests/pages/storage/plugins/list.js b/ui/tests/pages/storage/plugins/list.js index e46afa44820..f294baadc3f 100644 --- a/ui/tests/pages/storage/plugins/list.js +++ b/ui/tests/pages/storage/plugins/list.js @@ -1,4 +1,12 @@ -import { clickable, collection, create, isPresent, text, visitable } from 'ember-cli-page-object'; +import { + clickable, + collection, + create, + fillable, + isPresent, + text, + visitable, +} from 'ember-cli-page-object'; import error from 'nomad-ui/tests/pages/components/error'; import pageSizeSelect from 'nomad-ui/tests/pages/components/page-size-select'; @@ -8,6 +16,8 @@ export default create({ visit: visitable('/csi/plugins'), + search: fillable('[data-test-plugins-search] input'), + plugins: collection('[data-test-plugin-row]', { id: text('[data-test-plugin-id]'), controllerHealth: text('[data-test-plugin-controller-health]'), diff --git a/ui/tests/pages/storage/volumes/list.js b/ui/tests/pages/storage/volumes/list.js index 85687f7b00a..36b17a23933 100644 --- a/ui/tests/pages/storage/volumes/list.js +++ b/ui/tests/pages/storage/volumes/list.js @@ -1,4 +1,12 @@ -import { clickable, collection, create, isPresent, text, visitable } from 'ember-cli-page-object'; +import { + clickable, + collection, + create, + fillable, + isPresent, + text, + visitable, +} from 'ember-cli-page-object'; import error from 'nomad-ui/tests/pages/components/error'; import pageSizeSelect from 'nomad-ui/tests/pages/components/page-size-select'; @@ -8,6 +16,8 @@ export default create({ visit: visitable('/csi/volumes'), + search: fillable('[data-test-volumes-search] input'), + volumes: collection('[data-test-volume-row]', { name: text('[data-test-volume-name]'), schedulable: text('[data-test-volume-schedulable]'),