mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
[ui] Custom watchQuery equivalent on the storage index (#25374)
* Custom watchQuery equivalent on the storage index * Tests for live updates to the storage page * Deconditionalizing the pagination on storage, and fixing a bug where I was looking at filtered but not paginated DHV * Test for pagination with live-updates
This commit is contained in:
@@ -8,6 +8,10 @@ import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import Controller from '@ember/controller';
|
||||
import { scheduleOnce } from '@ember/runloop';
|
||||
import { restartableTask, timeout } from 'ember-concurrency';
|
||||
import Ember from 'ember';
|
||||
|
||||
const TASK_THROTTLE = 1000;
|
||||
|
||||
export default class IndexController extends Controller {
|
||||
@service router;
|
||||
@@ -144,11 +148,12 @@ export default class IndexController extends Controller {
|
||||
// Filter, then Sort, then Paginate
|
||||
// all handled client-side
|
||||
|
||||
@tracked csiVolumes = this.model.csiVolumes;
|
||||
get filteredCSIVolumes() {
|
||||
if (!this.csiFilter) {
|
||||
return this.model.csiVolumes;
|
||||
return this.csiVolumes;
|
||||
} else {
|
||||
return this.model.csiVolumes.filter((volume) => {
|
||||
return this.csiVolumes.filter((volume) => {
|
||||
return (
|
||||
volume.plainId.toLowerCase().includes(this.csiFilter.toLowerCase()) ||
|
||||
volume.name.toLowerCase().includes(this.csiFilter.toLowerCase())
|
||||
@@ -172,11 +177,12 @@ export default class IndexController extends Controller {
|
||||
);
|
||||
}
|
||||
|
||||
@tracked dynamicHostVolumes = this.model.dynamicHostVolumes;
|
||||
get filteredDynamicHostVolumes() {
|
||||
if (!this.dhvFilter) {
|
||||
return this.model.dynamicHostVolumes;
|
||||
return this.dynamicHostVolumes;
|
||||
} else {
|
||||
return this.model.dynamicHostVolumes.filter((volume) => {
|
||||
return this.dynamicHostVolumes.filter((volume) => {
|
||||
return (
|
||||
volume.plainId.toLowerCase().includes(this.dhvFilter.toLowerCase()) ||
|
||||
volume.name.toLowerCase().includes(this.dhvFilter.toLowerCase())
|
||||
@@ -238,4 +244,79 @@ export default class IndexController extends Controller {
|
||||
dhv.idWithNamespace
|
||||
);
|
||||
}
|
||||
|
||||
@restartableTask *watchDHV(
|
||||
params,
|
||||
throttle = Ember.testing ? 0 : TASK_THROTTLE
|
||||
) {
|
||||
while (true) {
|
||||
const abortController = new AbortController();
|
||||
try {
|
||||
const result = yield this.store.query('dynamic-host-volume', params, {
|
||||
reload: true,
|
||||
adapterOptions: {
|
||||
watch: true,
|
||||
abortController: abortController,
|
||||
},
|
||||
});
|
||||
|
||||
this.dynamicHostVolumes = result;
|
||||
} catch (e) {
|
||||
console.error('Error fetching dynamic host volumes:', e);
|
||||
yield timeout(throttle);
|
||||
} finally {
|
||||
abortController.abort();
|
||||
}
|
||||
|
||||
yield timeout(throttle);
|
||||
|
||||
if (Ember.testing) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@restartableTask *watchCSI(
|
||||
params,
|
||||
throttle = Ember.testing ? 0 : TASK_THROTTLE
|
||||
) {
|
||||
while (true) {
|
||||
const abortController = new AbortController();
|
||||
try {
|
||||
const result = yield this.store.query('volume', params, {
|
||||
reload: true,
|
||||
adapterOptions: {
|
||||
watch: true,
|
||||
abortController: abortController,
|
||||
},
|
||||
});
|
||||
|
||||
this.csiVolumes = result;
|
||||
} catch (e) {
|
||||
console.error('Error fetching CSI volumes:', e);
|
||||
yield timeout(throttle);
|
||||
} finally {
|
||||
abortController.abort();
|
||||
}
|
||||
|
||||
yield timeout(throttle);
|
||||
|
||||
if (Ember.testing) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
cancelQueryWatch() {
|
||||
this.watchDHV.cancelAll();
|
||||
this.watchCSI.cancelAll();
|
||||
}
|
||||
|
||||
// (called from route)
|
||||
@action
|
||||
startQueryWatch(dhvQuery, csiQuery) {
|
||||
this.watchDHV.perform(dhvQuery.queryParams);
|
||||
this.watchCSI.perform(csiQuery.queryParams);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ import { inject as service } from '@ember/service';
|
||||
import RSVP from 'rsvp';
|
||||
import Route from '@ember/routing/route';
|
||||
import { collect } from '@ember/object/computed';
|
||||
import { watchQuery, watchAll } from 'nomad-ui/utils/properties/watch';
|
||||
import { watchAll } from 'nomad-ui/utils/properties/watch';
|
||||
import WithWatchers from 'nomad-ui/mixins/with-watchers';
|
||||
import notifyForbidden from 'nomad-ui/utils/notify-forbidden';
|
||||
import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class IndexRoute extends Route.extend(
|
||||
WithWatchers,
|
||||
@@ -43,25 +44,32 @@ export default class IndexRoute extends Route.extend(
|
||||
|
||||
startWatchers(controller) {
|
||||
controller.set('namespacesWatch', this.watchNamespaces.perform());
|
||||
controller.set(
|
||||
'modelWatch',
|
||||
this.watchVolumes.perform({
|
||||
type: 'csi',
|
||||
namespace: controller.qpNamespace,
|
||||
})
|
||||
);
|
||||
controller.set(
|
||||
'modelWatch',
|
||||
this.watchDynamicHostVolumes.perform({
|
||||
type: 'host',
|
||||
namespace: controller.qpNamespace,
|
||||
})
|
||||
controller.startQueryWatch(
|
||||
{
|
||||
queryType: 'dynamic-host-volume',
|
||||
queryParams: {
|
||||
type: 'host',
|
||||
namespace: controller.qpNamespace,
|
||||
},
|
||||
},
|
||||
{
|
||||
queryType: 'volume',
|
||||
queryParams: {
|
||||
type: 'csi',
|
||||
namespace: controller.qpNamespace,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@watchQuery('volume') watchVolumes;
|
||||
@watchQuery('dynamic-host-volume') watchDynamicHostVolumes;
|
||||
@action
|
||||
willTransition() {
|
||||
// eslint-disable-next-line
|
||||
this.controller.cancelQueryWatch();
|
||||
this.cancelAllWatchers();
|
||||
}
|
||||
|
||||
@watchAll('namespace') watchNamespaces;
|
||||
@collect('watchVolumes', 'watchNamespaces', 'watchDynamicHostVolumes')
|
||||
@collect('watchNamespaces')
|
||||
watchers;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
{{#if this.isForbidden}}
|
||||
<ForbiddenMessage />
|
||||
{{else}}
|
||||
<Hds::Card::Container @level="base" @hasBorder={{false}} class="storage-index-table-card">
|
||||
<Hds::Card::Container @level="base" @hasBorder={{false}} class="storage-index-table-card" data-test-csi-volumes-card>
|
||||
<header aria-label="CSI Volumes">
|
||||
<h3>CSI Volumes</h3>
|
||||
<p class="intro">
|
||||
@@ -121,16 +121,14 @@
|
||||
</B.Tr>
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
{{#if (gt this.sortedCSIVolumes.length this.userSettings.pageSize)}}
|
||||
<Hds::Pagination::Numbered
|
||||
@totalItems={{this.filteredCSIVolumes.length}}
|
||||
@currentPage={{this.csiPage}}
|
||||
@pageSizes={{this.pageSizes}}
|
||||
@currentPageSize={{this.userSettings.pageSize}}
|
||||
@onPageChange={{action this.handlePageChange "csi"}}
|
||||
@onPageSizeChange={{action (mut this.userSettings.pageSize)}}
|
||||
/>
|
||||
{{/if}}
|
||||
<Hds::Pagination::Numbered
|
||||
@totalItems={{this.filteredCSIVolumes.length}}
|
||||
@currentPage={{this.csiPage}}
|
||||
@pageSizes={{this.pageSizes}}
|
||||
@currentPageSize={{this.userSettings.pageSize}}
|
||||
@onPageChange={{action this.handlePageChange "csi"}}
|
||||
@onPageSizeChange={{action (mut this.userSettings.pageSize)}}
|
||||
/>
|
||||
{{else}}
|
||||
<div class="empty-message" data-test-empty-csi-volumes-list-headline>
|
||||
{{#if this.csiFilter}}
|
||||
@@ -143,7 +141,7 @@
|
||||
{{/if}}
|
||||
</Hds::Card::Container>
|
||||
|
||||
<Hds::Card::Container @level="base" @hasBorder={{false}} class="storage-index-table-card">
|
||||
<Hds::Card::Container @level="base" @hasBorder={{false}} class="storage-index-table-card" data-test-dynamic-host-volumes-card>
|
||||
<header aria-label="Dynamic Host Volumes">
|
||||
<h3>Dynamic Host Volumes</h3>
|
||||
<p class="intro">
|
||||
@@ -162,7 +160,7 @@
|
||||
</header>
|
||||
{{#if this.sortedDynamicHostVolumes.length}}
|
||||
<Hds::Table @caption="Dynamic Host Volumes"
|
||||
@model={{this.sortedDynamicHostVolumes}}
|
||||
@model={{this.paginatedDynamicHostVolumes}}
|
||||
@columns={{this.dhvColumns}}
|
||||
@sortBy={{this.dhvSortProperty}}
|
||||
@sortOrder={{if this.dhvSortDescending "desc" "asc"}}
|
||||
@@ -202,18 +200,16 @@
|
||||
</B.Tr>
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
{{#if (gt this.sortedDynamicHostVolumes.length this.userSettings.pageSize)}}
|
||||
<Hds::Pagination::Numbered
|
||||
@totalItems={{this.filteredDynamicHostVolumes.length}}
|
||||
@currentPage={{this.dhvPage}}
|
||||
@pageSizes={{this.pageSizes}}
|
||||
@currentPageSize={{this.userSettings.pageSize}}
|
||||
@onPageChange={{action this.handlePageChange "dhv"}}
|
||||
@onPageSizeChange={{action (mut this.userSettings.pageSize)}}
|
||||
/>
|
||||
{{/if}}
|
||||
<Hds::Pagination::Numbered
|
||||
@totalItems={{this.filteredDynamicHostVolumes.length}}
|
||||
@currentPage={{this.dhvPage}}
|
||||
@pageSizes={{this.pageSizes}}
|
||||
@currentPageSize={{this.userSettings.pageSize}}
|
||||
@onPageChange={{action this.handlePageChange "dhv"}}
|
||||
@onPageSizeChange={{action (mut this.userSettings.pageSize)}}
|
||||
/>
|
||||
{{else}}
|
||||
<div class="empty-message">
|
||||
<div class="empty-message" data-test-empty-dhv-list-headline>
|
||||
{{#if this.dhvFilter}}
|
||||
<p>No dynamic host volumes match your search for "{{this.dhvFilter}}"</p>
|
||||
<Hds::Button @text="Clear search" @color="secondary" {{on "click" (queue (action (mut this.dhvFilter) "") (action this.handlePageChange "dhv" 1))}} />
|
||||
|
||||
@@ -277,5 +277,226 @@ module('Acceptance | storage list', function (hooks) {
|
||||
'URL has the correct query param key and value'
|
||||
);
|
||||
});
|
||||
|
||||
module('Live updates are reflected in the list', function () {
|
||||
test('When you visit the storage list page, the watch process is kicked off', async function (assert) {
|
||||
await StorageList.visit();
|
||||
const requests = server.pretender.handledRequests;
|
||||
const dhvRequests = requests.filter((request) =>
|
||||
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
||||
);
|
||||
const csiRequests = requests.filter((request) =>
|
||||
request.url.startsWith('/v1/volumes?namespace=%2A&type=csi')
|
||||
);
|
||||
assert.equal(dhvRequests.length, 2, '2 DHV requests were made');
|
||||
assert.equal(csiRequests.length, 2, '2 CSI requests were made');
|
||||
});
|
||||
|
||||
test('When a new dynamic host volume is created, the page should reflect the changes', async function (assert) {
|
||||
server.create('dynamic-host-volume', {
|
||||
name: 'initial-volume',
|
||||
});
|
||||
const controller = this.owner.lookup('controller:storage.index');
|
||||
await visit('/storage');
|
||||
|
||||
// Check pretender to see 2 requests related to DHV: the initial one, and another one with an index on it
|
||||
const requests = server.pretender.handledRequests;
|
||||
|
||||
// Should be 2 DHV requests made: the initial one, and the watcher
|
||||
let dhvRequests = requests.filter((request) =>
|
||||
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
||||
);
|
||||
assert.equal(dhvRequests.length, 2, '2 DHV requests were made');
|
||||
|
||||
assert.dom('[data-test-dhv-row]').exists({ count: 1 });
|
||||
assert.dom('[data-test-dhv-row]').containsText('initial-volume');
|
||||
|
||||
server.create('dynamic-host-volume', {
|
||||
name: 'new-volume',
|
||||
});
|
||||
|
||||
await controller.watchDHV.perform({
|
||||
type: 'host',
|
||||
namespace: controller.qpNamespace,
|
||||
});
|
||||
|
||||
// Now there should be a third DHV request
|
||||
dhvRequests = requests.filter((request) =>
|
||||
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
||||
);
|
||||
assert.equal(dhvRequests.length, 3, '3 DHV requests were made');
|
||||
|
||||
// and a second row
|
||||
assert.dom('[data-test-dhv-row]').exists({ count: 2 });
|
||||
});
|
||||
|
||||
test('When a new csi volume is created, the page should reflect the changes', async function (assert) {
|
||||
server.create('csi-volume', {
|
||||
id: 'initial-volume',
|
||||
});
|
||||
const controller = this.owner.lookup('controller:storage.index');
|
||||
await visit('/storage');
|
||||
|
||||
// Check pretender to see 2 requests related to DHV: the initial one, and another one with an index on it
|
||||
const requests = server.pretender.handledRequests;
|
||||
|
||||
// Should be 2 DHV requests made: the initial one, and the watcher
|
||||
let csiRequests = requests.filter((request) =>
|
||||
request.url.startsWith('/v1/volumes?namespace=%2A&type=csi')
|
||||
);
|
||||
assert.equal(csiRequests.length, 2, '2 CSI requests were made');
|
||||
assert.dom('[data-test-csi-volume-row]').exists({ count: 1 });
|
||||
assert.dom('[data-test-csi-volume-row]').containsText('initial-volume');
|
||||
|
||||
server.create('csi-volume', {
|
||||
id: 'new-volume',
|
||||
});
|
||||
|
||||
await controller.watchCSI.perform({
|
||||
type: 'csi',
|
||||
namespace: controller.qpNamespace,
|
||||
});
|
||||
|
||||
// Now there should be a third DHV request
|
||||
csiRequests = requests.filter((request) =>
|
||||
request.url.startsWith('/v1/volumes?namespace=%2A&type=csi')
|
||||
);
|
||||
assert.equal(csiRequests.length, 3, '3 CSI requests were made');
|
||||
|
||||
// and a second row
|
||||
assert.dom('[data-test-csi-volume-row]').exists({ count: 2 });
|
||||
});
|
||||
|
||||
test('When a dynamic host volume is updated, the page should reflect the changes', async function (assert) {
|
||||
const dhv = server.create('dynamic-host-volume', {
|
||||
name: 'initial-volume',
|
||||
});
|
||||
const controller = this.owner.lookup('controller:storage.index');
|
||||
await visit('/storage');
|
||||
|
||||
// Check pretender to see 2 requests related to DHV: the initial one, and another one with an index on it
|
||||
const requests = server.pretender.handledRequests;
|
||||
|
||||
// Should be 2 DHV requests made: the initial one, and the watcher
|
||||
let dhvRequests = requests.filter((request) =>
|
||||
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
||||
);
|
||||
assert.equal(dhvRequests.length, 2, '2 DHV requests were made');
|
||||
|
||||
assert.dom('[data-test-dhv-row]').exists({ count: 1 });
|
||||
assert.dom('[data-test-dhv-row]').containsText('initial-volume');
|
||||
|
||||
dhv.update('name', 'updated-volume');
|
||||
|
||||
await controller.watchDHV.perform({
|
||||
type: 'host',
|
||||
namespace: controller.qpNamespace,
|
||||
});
|
||||
|
||||
dhvRequests = requests.filter((request) =>
|
||||
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
||||
);
|
||||
assert.equal(dhvRequests.length, 3, '3 DHV requests were made');
|
||||
|
||||
// Still just one row
|
||||
assert.dom('[data-test-dhv-row]').exists({ count: 1 });
|
||||
assert.dom('[data-test-dhv-row]').containsText('updated-volume');
|
||||
});
|
||||
|
||||
test('When a dynamic host volume is deleted, the page should reflect the changes', async function (assert) {
|
||||
const dhv = server.create('dynamic-host-volume', {
|
||||
name: 'initial-volume',
|
||||
});
|
||||
const controller = this.owner.lookup('controller:storage.index');
|
||||
await visit('/storage');
|
||||
|
||||
// Check pretender to see 2 requests related to DHV: the initial one, and another one with an index on it
|
||||
const requests = server.pretender.handledRequests;
|
||||
|
||||
// Should be 2 DHV requests made: the initial one, and the watcher
|
||||
let dhvRequests = requests.filter((request) =>
|
||||
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
||||
);
|
||||
assert.equal(dhvRequests.length, 2, '2 DHV requests were made');
|
||||
|
||||
assert.dom('[data-test-dhv-row]').exists({ count: 1 });
|
||||
assert.dom('[data-test-dhv-row]').containsText('initial-volume');
|
||||
|
||||
dhv.destroy();
|
||||
|
||||
await controller.watchDHV.perform({
|
||||
type: 'host',
|
||||
namespace: controller.qpNamespace,
|
||||
});
|
||||
|
||||
dhvRequests = requests.filter((request) =>
|
||||
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
||||
);
|
||||
assert.equal(dhvRequests.length, 3, '3 DHV requests were made');
|
||||
|
||||
assert.dom('[data-test-dhv-row]').exists({ count: 0 });
|
||||
assert.ok(StorageList.dhvIsEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
test('Pagination is adhered to when live updates happen', async function (assert) {
|
||||
localStorage.setItem('nomadPageSize', 10);
|
||||
server.createList('dynamic-host-volume', 9);
|
||||
const controller = this.owner.lookup('controller:storage.index');
|
||||
|
||||
await StorageList.visit();
|
||||
|
||||
// 9 rows should be present
|
||||
assert.dom('[data-test-dhv-row]').exists({ count: 9 });
|
||||
|
||||
server.create('dynamic-host-volume', { name: 'tenth-volume' });
|
||||
|
||||
await controller.watchDHV.perform({
|
||||
type: 'host',
|
||||
namespace: controller.qpNamespace,
|
||||
});
|
||||
|
||||
// 10 rows should be present
|
||||
assert.dom('[data-test-dhv-row]').exists({ count: 10 });
|
||||
|
||||
// Newest (sorted by modified date by default) should show up first
|
||||
assert
|
||||
.dom('[data-test-dhv-row]:first-child')
|
||||
.containsText('tenth-volume');
|
||||
|
||||
// There should still only be 1 page of pagination
|
||||
assert.dom('.hds-pagination-nav__number').exists({ count: 1 });
|
||||
// 10 rows should be present
|
||||
assert.dom('[data-test-dhv-row]').exists({ count: 10 });
|
||||
|
||||
// Create one more
|
||||
server.create('dynamic-host-volume', { name: 'eleventh-volume' });
|
||||
|
||||
await controller.watchDHV.perform({
|
||||
type: 'host',
|
||||
namespace: controller.qpNamespace,
|
||||
});
|
||||
|
||||
// 10 rows still present
|
||||
assert.dom('[data-test-dhv-row]').exists({ count: 10 });
|
||||
|
||||
// Newest should show up first
|
||||
assert
|
||||
.dom('[data-test-dhv-row]:first-child')
|
||||
.containsText('eleventh-volume');
|
||||
|
||||
// There should now be 2 pages of pagination
|
||||
assert.dom('.hds-pagination-nav__number').exists({ count: 2 });
|
||||
|
||||
// Clicking through to the second page changes the URL and only shows 1 row
|
||||
await StorageList.dhvNextPage();
|
||||
assert.equal(currentURL(), '/storage?dhvPage=2');
|
||||
|
||||
// 1 row should be present
|
||||
assert.dom('[data-test-dhv-row]').exists({ count: 1 });
|
||||
|
||||
// cleanup
|
||||
localStorage.removeItem('nomadPageSize');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,8 +48,22 @@ export default create({
|
||||
csiIsEmpty: isPresent('[data-test-empty-csi-volumes-list-headline]'),
|
||||
csiEmptyState: text('[data-test-empty-csi-volumes-list-headline]'),
|
||||
|
||||
csiNextPage: clickable('.hds-pagination-nav__arrow--direction-next'),
|
||||
csiPrevPage: clickable('.hds-pagination-nav__arrow--direction-prev'),
|
||||
dhvIsEmpty: isPresent('[data-test-empty-dhv-list-headline]'),
|
||||
dhvEmptyState: text('[data-test-empty-dhv-list-headline]'),
|
||||
|
||||
csiNextPage: clickable(
|
||||
'[data-test-csi-volumes-card] .hds-pagination-nav__arrow--direction-next'
|
||||
),
|
||||
csiPrevPage: clickable(
|
||||
'[data-test-csi-volumes-card] .hds-pagination-nav__arrow--direction-prev'
|
||||
),
|
||||
|
||||
dhvNextPage: clickable(
|
||||
'[data-test-dynamic-host-volumes-card] .hds-pagination-nav__arrow--direction-next'
|
||||
),
|
||||
dhvPrevPage: clickable(
|
||||
'[data-test-dynamic-host-volumes-card] .hds-pagination-nav__arrow--direction-prev'
|
||||
),
|
||||
|
||||
error: error(),
|
||||
pageSizeSelect: pageSizeSelect(),
|
||||
|
||||
Reference in New Issue
Block a user