Merge pull request #7895 from hashicorp/f-ui/csi-search-and-filter

UI: CSI Searchable volumes and plugins lists
This commit is contained in:
Michael Lange
2020-05-08 17:34:24 -07:00
committed by GitHub
11 changed files with 196 additions and 57 deletions

View File

@@ -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'),
export default Controller.extend(
SortableFactory([
'plainId',
'controllersHealthyProportion',
'nodesHealthyProportion',
'provider',
]),
Searchable,
{
userSettings: service(),
pluginsController: controller('csi/plugins'),
isForbidden: alias('pluginsController.isForbidden'),
isForbidden: alias('pluginsController.isForbidden'),
queryParams: {
currentPage: 'page',
sortProperty: 'sort',
sortDescending: 'desc',
},
currentPage: 1,
pageSize: readOnly('userSettings.pageSize'),
sortProperty: 'id',
sortDescending: false,
listToSort: alias('model'),
sortedPlugins: alias('listSorted'),
// TODO: Remove once this page gets search capability
resetPagination() {
if (this.currentPage != null) {
this.set('currentPage', 1);
}
},
actions: {
gotoPlugin(plugin, event) {
lazyClick([() => this.transitionToRoute('csi.plugins.plugin', plugin.plainId), event]);
queryParams: {
currentPage: 'page',
searchTerm: 'search',
sortProperty: 'sort',
sortDescending: 'desc',
},
},
});
currentPage: 1,
pageSize: readOnly('userSettings.pageSize'),
searchProps: computed(() => ['id']),
fuzzySearchProps: computed(() => ['id']),
sortProperty: 'id',
sortDescending: false,
listToSort: alias('model'),
listToSearch: alias('listSorted'),
sortedPlugins: alias('listSearched'),
actions: {
gotoPlugin(plugin, event) {
lazyClick([() => this.transitionToRoute('csi.plugins.plugin', plugin.plainId), event]);
},
},
}
);

View File

@@ -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) {

View File

@@ -9,6 +9,17 @@
{{#if isForbidden}}
{{partial "partials/forbidden-message"}}
{{else}}
<div class="toolbar">
<div class="toolbar-item">
{{#if model.length}}
{{search-box
data-test-plugins-search
searchTerm=(mut searchTerm)
onChange=(action resetPagination)
placeholder="Search plugins..."}}
{{/if}}
</div>
</div>
{{#if sortedPlugins}}
{{#list-pagination
source=sortedPlugins
@@ -56,10 +67,17 @@
{{/list-pagination}}
{{else}}
<div data-test-empty-plugins-list class="empty-message">
<h3 data-test-empty-plugins-list-headline class="empty-message-headline">No Plugins</h3>
<p class="empty-message-body">
The cluster currently has no registered CSI Plugins.
</p>
{{#if (eq model.length 0)}}
<h3 data-test-empty-plugins-list-headline class="empty-message-headline">No Plugins</h3>
<p class="empty-message-body">
The cluster currently has no registered CSI Plugins.
</p>
{{else if searchTerm}}
<h3 data-test-empty-plugins-list-headline class="empty-message-headline">No Matches</h3>
<p class="empty-message-body">
No plugins match the term <strong>{{searchTerm}}</strong>
</p>
{{/if}}
</div>
{{/if}}
{{/if}}

View File

@@ -9,6 +9,17 @@
{{#if isForbidden}}
{{partial "partials/forbidden-message"}}
{{else}}
<div class="toolbar">
<div class="toolbar-item">
{{#if model.length}}
{{search-box
data-test-volumes-search
searchTerm=(mut searchTerm)
onChange=(action resetPagination)
placeholder="Search volumes..."}}
{{/if}}
</div>
</div>
{{#if sortedVolumes}}
{{#list-pagination
source=sortedVolumes
@@ -60,10 +71,17 @@
{{/list-pagination}}
{{else}}
<div data-test-empty-volumes-list class="empty-message">
<h3 data-test-empty-volumes-list-headline class="empty-message-headline">No Volumes</h3>
<p class="empty-message-body">
The cluster currently has no CSI Volumes.
</p>
{{#if (eq model.length 0)}}
<h3 data-test-empty-volumes-list-headline class="empty-message-headline">No Volumes</h3>
<p class="empty-message-body">
The cluster currently has no CSI Volumes.
</p>
{{else if searchTerm}}
<h3 data-test-empty-volumes-list-headline class="empty-message-headline">No Matches</h3>
<p class="empty-message-body">
No volumes match the term <strong>{{searchTerm}}</strong>
</p>
{{/if}}
</div>
{{/if}}
{{/if}}

View File

@@ -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,
});
}

View File

@@ -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({

View File

@@ -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({

View File

@@ -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();
},
});

View File

@@ -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 });

View File

@@ -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]'),

View File

@@ -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]'),