mirror of
https://github.com/kemko/nomad.git
synced 2026-01-06 02:15:43 +03:00
Merge pull request #7895 from hashicorp/f-ui/csi-search-and-filter
UI: CSI Searchable volumes and plugins lists
This commit is contained in:
@@ -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]);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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]'),
|
||||
|
||||
@@ -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]'),
|
||||
|
||||
Reference in New Issue
Block a user