Skip to content

Commit 8170eff

Browse files
committed
Fix storage table page-size persistence
1 parent a9e1205 commit 8170eff

10 files changed

Lines changed: 190 additions & 50 deletions

File tree

app/services/prefs.js

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
import Ember from 'ember';
22
import C from 'ui/utils/constants';
33

4+
function pageSizePreference(preference, options, fallback) {
5+
function normalize(value) {
6+
let parsed = parseInt(value, 10);
7+
8+
return options.indexOf(parsed) === -1 ? fallback : parsed;
9+
}
10+
11+
return Ember.computed(preference, {
12+
get() {
13+
return normalize(this.get(preference));
14+
},
15+
16+
set(key, value) {
17+
let normalized = normalize(value);
18+
19+
this.set(preference, normalized);
20+
21+
return normalized;
22+
},
23+
});
24+
}
25+
426
export default Ember.Service.extend({
527
userStore: Ember.inject.service('user-store'),
628

@@ -79,32 +101,21 @@ export default Ember.Service.extend({
79101
this.endPropertyChanges();
80102
},
81103

82-
tablePerPage: Ember.computed(`${C.PREFS.TABLE_COUNT}`, function() {
83-
let out = this.get(`${C.PREFS.TABLE_COUNT}`);
84-
if ( C.TABLES.PAGE_SIZES.indexOf(out) === -1 ) {
85-
out = C.TABLES.DEFAULT_COUNT;
86-
}
87-
88-
return out;
89-
}),
90-
91-
statsTablePerPage: Ember.computed(`${C.PREFS.STATS_TABLE_COUNT}`, function() {
92-
let out = this.get(`${C.PREFS.STATS_TABLE_COUNT}`);
93-
94-
if ( C.TABLES.STATS_PAGE_SIZES.indexOf(out) === -1 ) {
95-
out = C.TABLES.DEFAULT_STATS_COUNT;
96-
}
97-
98-
return out;
99-
}),
100-
101-
storageTablePerPage: Ember.computed(`${C.PREFS.STORAGE_TABLE_COUNT}`, function() {
102-
let out = this.get(`${C.PREFS.STORAGE_TABLE_COUNT}`);
103-
104-
if ( C.TABLES.STORAGE_PAGE_SIZES.indexOf(out) === -1 ) {
105-
out = C.TABLES.DEFAULT_STORAGE_COUNT;
106-
}
107-
108-
return out;
109-
}),
104+
tablePerPage: pageSizePreference(
105+
C.PREFS.TABLE_COUNT,
106+
C.TABLES.PAGE_SIZES,
107+
C.TABLES.DEFAULT_COUNT
108+
),
109+
110+
statsTablePerPage: pageSizePreference(
111+
C.PREFS.STATS_TABLE_COUNT,
112+
C.TABLES.STATS_PAGE_SIZES,
113+
C.TABLES.DEFAULT_STATS_COUNT
114+
),
115+
116+
storageTablePerPage: pageSizePreference(
117+
C.PREFS.STORAGE_TABLE_COUNT,
118+
C.TABLES.STORAGE_PAGE_SIZES,
119+
C.TABLES.DEFAULT_STORAGE_COUNT
120+
),
110121
});

docs/storage-bulk-remove.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,17 @@ sent. Removal uses the public resource action with at most four concurrent
2626
requests and never mutates a database directly. Progress and per-item failures
2727
remain visible until the operator closes the result.
2828

29-
The page-size selector treats its invocation value as read-only. The table
30-
keeps a separate internal effective page size, so choosing All stores the
31-
semantic preference value `0` without attempting to write through the
32-
caller's computed preference. This prevents an Ember property-setter failure
33-
and preserves the same 10, 25, 50, and All behavior for every shared table.
29+
The page-size selector keeps a separate internal effective page size and the
30+
preference service exposes normalized read/write computed properties. Choosing
31+
All stores the semantic preference value `0`; a legacy two-way component
32+
binding can safely write that value back without reaching an undefined Ember
33+
setter. Unsupported values normalize to the documented default. This preserves
34+
the same 10, 25, 50, and All behavior for every shared table.
3435

3536
After each successful remove response, the corresponding row is removed from
3637
the current model, filtered result, and selected-item list immediately. A
3738
revision-backed recomputation keeps the visible table and selected count in
38-
sync while the remaining requests continue. Failed rows stay visible and
39-
selected so the operator can inspect or retry them.
39+
sync while the remaining requests continue. If removal empties the current
40+
page, the table moves to the last valid page before it renders the remaining
41+
rows. Failed rows stay visible and selected so the operator can inspect or
42+
retry them.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pasturestack/web-console",
3-
"version": "1.6.56-pasturestack.54",
3+
"version": "1.6.56-pasturestack.55",
44
"private": true,
55
"description": "PastureStack browser console for the compatible control platform.",
66
"repository": {

scripts/check-modernization-blockers

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f:
4141
print(json.load(f).get('version', ''))
4242
PY
4343
)
44-
if [[ "$version" != "1.6.56-pasturestack.54" ]]; then
45-
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.56-pasturestack.54"
44+
if [[ "$version" != "1.6.56-pasturestack.55" ]]; then
45+
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.56-pasturestack.55"
4646
failures=$((failures + 1))
4747
fi
4848

scripts/check-ui-console-workspace

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,4 @@ if [[ -n ${PASTURESTACK_PRIVATE_MARKER:-} ]] && grep -RInF -- "$PASTURESTACK_PRI
141141
fi
142142

143143
printf 'UI_CONSOLE_WORKSPACE_OK version=%s persistence=%s cross_tab=%s\n' \
144-
1.6.56-pasturestack.54 browser-session broker-broadcast
144+
1.6.56-pasturestack.55 browser-session broker-broadcast

scripts/check-ui-sortable-table-refresh.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ const componentPath = 'vendor/lacsso/addon/components/sortable-table.js';
77
const testPath = 'tests/unit/components/sortable-table-test.js';
88
const hostRoutePath = 'app/host/containers/route.js';
99
const hostRouteTestPath = 'tests/unit/host/containers/route-test.js';
10+
const prefsPath = 'app/services/prefs.js';
11+
const prefsTestPath = 'tests/unit/services/prefs-test.js';
1012
const ciPath = 'scripts/ci';
1113
const component = fs.readFileSync(componentPath, 'utf8');
1214
const test = fs.readFileSync(testPath, 'utf8');
1315
const hostRoute = fs.readFileSync(hostRoutePath, 'utf8');
1416
const hostRouteTest = fs.readFileSync(hostRouteTestPath, 'utf8');
17+
const prefs = fs.readFileSync(prefsPath, 'utf8');
18+
const prefsTest = fs.readFileSync(prefsTestPath, 'utf8');
1519
const ci = fs.readFileSync(ciPath, 'utf8');
1620
const failures = [];
1721

@@ -31,6 +35,8 @@ for (const marker of [
3135
"paged.set('page', page);",
3236
"paged.set('perPage', perPage);",
3337
'this._syncPagedContent(out);',
38+
'this.clampPageToContentLength(out.length);',
39+
'clampPageToContentLength(length) {',
3440
'Ember.run.throttle(this, this._updateFiltered, 100, false)',
3541
'Ember.run.debounce(this, this._updateFiltered, 100, false)',
3642
]) {
@@ -39,6 +45,29 @@ for (const marker of [
3945
}
4046
}
4147

48+
for (const marker of [
49+
'function pageSizePreference(preference, options, fallback)',
50+
'return Ember.computed(preference, {',
51+
'set(key, value) {',
52+
'this.set(preference, normalized);',
53+
'storageTablePerPage: pageSizePreference(',
54+
]) {
55+
if (!prefs.includes(marker)) {
56+
failures.push('PAGE_SIZE_PREFERENCE_CONTRACT_MISSING=' + marker);
57+
}
58+
}
59+
60+
for (const marker of [
61+
'page-size computed properties accept legacy two-way binding writes',
62+
"service.set('storageTablePerPage', 0)",
63+
"assert.equal(service.get('storageTablePerPage'), 0",
64+
"records.findBy('name', 'storageTableCount').get('value'), '0'",
65+
]) {
66+
if (!prefsTest.includes(marker)) {
67+
failures.push('PAGE_SIZE_PREFERENCE_TEST_MISSING=' + marker);
68+
}
69+
}
70+
4271
for (const marker of [
4372
"host.followLink('instances')",
4473
'Ember.Object.create({',
@@ -93,6 +122,7 @@ for (const marker of [
93122
"component.set('page', 2)",
94123
'component.setProperties({page: 1, perPage: 2})',
95124
'keeps the invocation page size read-only while selecting all rows',
125+
'clamps the current page immediately when live rows are removed',
96126
"component.send('changePerPage', '0')",
97127
"assert.equal(component.get('perPage'), 1, 'does not write through the caller-owned input')",
98128
"assert.equal(prefs.get('storageTableCount'), 0, 'persists the semantic All preference')",

tests/unit/components/sortable-table-test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,43 @@ test('it keeps the invocation page size read-only while selecting all rows', fun
204204

205205
destroyOwned(component);
206206
});
207+
208+
test('it clamps the current page immediately when live rows are removed', function(assert) {
209+
assert.expect(4);
210+
211+
let body = Ember.A([
212+
Ember.Object.create({id: '1', name: 'Volume 1'}),
213+
Ember.Object.create({id: '2', name: 'Volume 2'}),
214+
Ember.Object.create({id: '3', name: 'Volume 3'}),
215+
]);
216+
let component;
217+
218+
Ember.run(() => {
219+
component = createOwned(SortableTableComponent, {
220+
renderer: inertRenderer(),
221+
prefs: Ember.Object.create(),
222+
body,
223+
headers: Ember.A([
224+
Ember.Object.create({name: 'name', searchField: 'name'}),
225+
]),
226+
sortBy: 'name',
227+
perPage: 1,
228+
paging: true,
229+
}, 'component');
230+
component.didReceiveAttrs();
231+
component.set('page', 3);
232+
});
233+
234+
assert.equal(component.get('page'), 3, 'starts on the last page');
235+
assert.deepEqual(component.get('pagedContent').mapBy('name'), ['Volume 3'], 'renders the last row');
236+
237+
Ember.run(() => {
238+
body.removeObjects(body.slice(1));
239+
component.didReceiveAttrs();
240+
});
241+
242+
assert.equal(component.get('page'), 1, 'moves to the last valid page synchronously');
243+
assert.deepEqual(component.get('pagedContent').mapBy('name'), ['Volume 1'], 'renders the remaining row immediately');
244+
245+
destroyOwned(component);
246+
});

tests/unit/services/prefs-test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Ember from 'ember';
2+
import { module, test } from 'qunit';
3+
4+
import PrefsService from 'ui/services/prefs';
5+
6+
module('Unit | Service | prefs');
7+
8+
function preferenceRecord(name, value) {
9+
return Ember.Object.create({
10+
id: `1up-${name}`,
11+
name,
12+
value: JSON.stringify(value),
13+
save() {
14+
return Ember.RSVP.resolve(this);
15+
},
16+
});
17+
}
18+
19+
test('page-size computed properties accept legacy two-way binding writes', function(assert) {
20+
assert.expect(7);
21+
22+
let records = Ember.A([
23+
preferenceRecord('tableCount', 50),
24+
preferenceRecord('statsTableCount', 10),
25+
preferenceRecord('storageTableCount', 25),
26+
]);
27+
let service = PrefsService.create({
28+
userStore: Ember.Object.create({
29+
all() {
30+
return records;
31+
},
32+
}),
33+
});
34+
35+
assert.equal(service.get('storageTablePerPage'), 25, 'reads the stored storage page size');
36+
37+
Ember.run(() => service.set('storageTablePerPage', 0));
38+
39+
assert.equal(service.get('storageTablePerPage'), 0, 'accepts the semantic All value');
40+
assert.equal(records.findBy('name', 'storageTableCount').get('value'), '0', 'persists All through the base preference');
41+
42+
Ember.run(() => service.set('storageTablePerPage', 999));
43+
44+
assert.equal(service.get('storageTablePerPage'), 25, 'normalizes unsupported storage sizes');
45+
assert.equal(records.findBy('name', 'storageTableCount').get('value'), '25', 'persists the normalized storage size');
46+
47+
Ember.run(() => service.set('tablePerPage', 0));
48+
49+
assert.equal(service.get('tablePerPage'), 50, 'does not allow All on ordinary tables');
50+
assert.equal(records.findBy('name', 'tableCount').get('value'), '50', 'persists the ordinary-table fallback');
51+
52+
Ember.run(() => service.destroy());
53+
});

vendor/lacsso/addon/components/sortable-table.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -391,10 +391,20 @@ export default Ember.Component.extend(Sortable, StickyHeader, {
391391
}
392392
}
393393

394+
this.clampPageToContentLength(out.length);
394395
this.set('filtered', out);
395396
this._syncPagedContent(out);
396397
},
397398

399+
clampPageToContentLength(length) {
400+
let perPage = this.get('effectivePerPage') || 1;
401+
let lastPage = Math.max(1, Math.ceil(length / perPage));
402+
403+
if ( this.get('page') > lastPage ) {
404+
this.set('page', lastPage);
405+
}
406+
},
407+
398408
pagedContentChanged: Ember.observer('pagedContent.[]', function() {
399409
// Remove selected items not in the current content
400410
let content = this.get('pagedContent');
@@ -519,15 +529,8 @@ export default Ember.Component.extend(Sortable, StickyHeader, {
519529
}),
520530

521531
pageCountChanged: Ember.observer('indexFrom', 'filtered.length', function() {
522-
// Go to the last page if we end up past the last page
523-
let from = this.get('indexFrom');
524-
let last = this.get('filtered.length');
525-
var perPage = this.get('effectivePerPage');
526-
527-
if ( this.get('page') > 1 && from > last) {
528-
let page = Math.ceil(last/perPage);
529-
this.set('page', page);
530-
}
532+
// Keep the current page valid when live rows are removed.
533+
this.clampPageToContentLength(this.get('filtered.length') || 0);
531534
}),
532535

533536
sortKeyChanged: Ember.observer('sortBy', function() {

0 commit comments

Comments
 (0)