mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 20:36:26 +02:00
UI: octanify lazy paginated query (#21602)
This commit is contained in:
parent
8bb9cbbeba
commit
15b5dd0a4e
@ -9,7 +9,7 @@ import { copy } from 'ember-copy';
|
||||
import { resolve, Promise } from 'rsvp';
|
||||
import { dasherize } from '@ember/string';
|
||||
import { assert } from '@ember/debug';
|
||||
import { set, get, computed } from '@ember/object';
|
||||
import { set, get } from '@ember/object';
|
||||
import clamp from 'vault/utils/clamp';
|
||||
import config from 'vault/config/environment';
|
||||
|
||||
@ -32,26 +32,16 @@ export function keyForCache(query) {
|
||||
return JSON.stringify(cacheKeyObject);
|
||||
}
|
||||
|
||||
export default Store.extend({
|
||||
// this is a map of map that stores the caches
|
||||
// eslint-disable-next-line
|
||||
lazyCaches: computed({
|
||||
get() {
|
||||
return this._lazyCaches || new Map();
|
||||
},
|
||||
set(key, value) {
|
||||
return (this._lazyCaches = value);
|
||||
},
|
||||
}),
|
||||
export default class StoreService extends Store {
|
||||
lazyCaches = new Map();
|
||||
|
||||
setLazyCacheForModel(modelName, key, value) {
|
||||
const cacheKey = keyForCache(key);
|
||||
const cache = this.lazyCacheForModel(modelName) || new Map();
|
||||
cache.set(cacheKey, value);
|
||||
const lazyCaches = this.lazyCaches;
|
||||
const modelKey = normalizeModelName(modelName);
|
||||
lazyCaches.set(modelKey, cache);
|
||||
},
|
||||
this.lazyCaches.set(modelKey, cache);
|
||||
}
|
||||
|
||||
getLazyCacheForModel(modelName, key) {
|
||||
const cacheKey = keyForCache(key);
|
||||
@ -59,11 +49,11 @@ export default Store.extend({
|
||||
if (modelCache) {
|
||||
return modelCache.get(cacheKey);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
lazyCacheForModel(modelName) {
|
||||
return this.lazyCaches.get(normalizeModelName(modelName));
|
||||
},
|
||||
}
|
||||
|
||||
// This is the public interface for the store extension - to be used just
|
||||
// like `Store.query`. Special handling of the response is controlled by
|
||||
@ -105,7 +95,7 @@ export default Store.extend({
|
||||
.catch(function (e) {
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
filterData(filter, dataset) {
|
||||
let newData = dataset || [];
|
||||
@ -116,7 +106,7 @@ export default Store.extend({
|
||||
});
|
||||
}
|
||||
return newData;
|
||||
},
|
||||
}
|
||||
|
||||
// reconstructs the original form of the response from the server
|
||||
// with an additional `meta` block
|
||||
@ -147,14 +137,12 @@ export default Store.extend({
|
||||
};
|
||||
|
||||
return response;
|
||||
},
|
||||
}
|
||||
|
||||
// pushes records into the store and returns the result
|
||||
fetchPage(modelName, query) {
|
||||
const response = this.constructResponse(modelName, query);
|
||||
this.peekAll(modelName).forEach((record) => {
|
||||
record.unloadRecord();
|
||||
});
|
||||
this.unloadAll(modelName);
|
||||
return new Promise((resolve) => {
|
||||
schedule('destroy', () => {
|
||||
this.push(
|
||||
@ -171,12 +159,12 @@ export default Store.extend({
|
||||
resolve(model);
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
// get cached data
|
||||
getDataset(modelName, query) {
|
||||
return this.getLazyCacheForModel(modelName, query);
|
||||
},
|
||||
}
|
||||
|
||||
// store data cache as { response, dataset}
|
||||
// also populated `lazyCaches` attribute
|
||||
@ -186,20 +174,18 @@ export default Store.extend({
|
||||
dataset: array,
|
||||
};
|
||||
this.setLazyCacheForModel(modelName, query, dataSet);
|
||||
},
|
||||
}
|
||||
|
||||
clearDataset(modelName) {
|
||||
const cacheList = this.lazyCaches;
|
||||
if (!cacheList.size) return;
|
||||
if (modelName && cacheList.has(modelName)) {
|
||||
cacheList.delete(modelName);
|
||||
if (!this.lazyCaches.size) return;
|
||||
if (modelName && this.lazyCaches.has(modelName)) {
|
||||
this.lazyCaches.delete(modelName);
|
||||
return;
|
||||
}
|
||||
cacheList.clear();
|
||||
this.set('lazyCaches', cacheList);
|
||||
},
|
||||
this.lazyCaches.clear();
|
||||
}
|
||||
|
||||
clearAllDatasets() {
|
||||
this.clearDataset();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
93
ui/docs/client-pagination.md
Normal file
93
ui/docs/client-pagination.md
Normal file
@ -0,0 +1,93 @@
|
||||
# Client-side pagination
|
||||
|
||||
Our custom extended `store` service allows us to paginate LIST responses while maintaining good performance, particularly when the LIST response includes tens of thousands of keys in the data response. It does this by caching the entire response, and then filtering the full response into the datastore for the client.
|
||||
|
||||
## Using pagination
|
||||
|
||||
Rather than use `store.query`, use `store.lazyPaginatedQuery`. It generally uses the same inputs, but accepts additional keys in the query object `size`, `page`, `responsePath`, `pageFilter`
|
||||
|
||||
### Before
|
||||
|
||||
```js
|
||||
export default class ExampleRoute extends Route {
|
||||
@service store;
|
||||
|
||||
model(params) {
|
||||
const { secret } = params;
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
return this.store.query('pki/role', { backend, id })
|
||||
}
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```js
|
||||
export default class ExampleRoute extends Route {
|
||||
@service store;
|
||||
|
||||
model(params) {
|
||||
const { page, pageFilter, secret } = params;
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
return this.store.lazyPaginatedQuery('secret-v2', {
|
||||
backend,
|
||||
id: secret,
|
||||
size,
|
||||
page,
|
||||
responsePath,
|
||||
pageFilter
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The `size` param defaults to the default page size set in [the app config](../config/environment.js). `responsePath` and `page` are required, and typically `responsePath` is going to be `data.keys` since that is where the LIST responses typically return their array data.
|
||||
|
||||
### Serializing
|
||||
|
||||
In order to interrupt the regular serialization when using `lazyPaginatedData`, define `extractLazyPaginatedData` on the modelType's serializer. This will be called with the raw response before being cached on the store.
|
||||
|
||||
## Gotchas
|
||||
|
||||
The data is cached from whenever the original API call is made, which means that if a user views a list and then creates or deletes an item, viewing the list page again will show outdated information unless the cache for the item is cleared first. For this reason, it is best practice to clear the dataset with `store.clearDataset(modelName)` after successfully deleting or creating an item.
|
||||
|
||||
## How it works
|
||||
|
||||
When using the `lazyPaginatedQuery` method, the full response is cached in a [tracked Map](https://github.com/tracked-tools/tracked-built-ins/tree/master) within the service. `store.lazyCaches` is actually a Map of [Maps](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), keyed first on the normalized modelType and then on a stringified version of the base query (all keys except ones related to pagination). So, at the top level `store.lazyCaches` looks like this:
|
||||
|
||||
```
|
||||
lazyCaches = new Map({
|
||||
"secret-v2": <Map>,
|
||||
"kmip": <Map>,
|
||||
"namespace": <Map>,
|
||||
})
|
||||
```
|
||||
|
||||
Within each top-level modelType, we need to separate cached responses based on the details of the query. Typically (but not always) this includes the backend name. In list items that can be nested (see KV V2 secrets or namespaces for example) `id` is also provided, so that the keys nested under the given ID is returned. The store.lazyCaches may look something like the following after a user navigates to a couple different KV v2 lists, and clicks into the `app/` item:
|
||||
|
||||
```
|
||||
lazyCaches = new Map({
|
||||
"secret-v2": {
|
||||
"{ backend: 'secret', id: '' }: <CachedData>,
|
||||
"{ backend: 'secret', id: 'app/' }: <CachedData>,
|
||||
"{ backend: 'kv2', id: '' }: <CachedData>,
|
||||
},
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
The cached data at the given key is an object with `response` and `dataset` keys. The response is the full response from the original API call, with the `responsePath` nulled out (it is repopulated before "sending" the data back to the store). `dataset` is the full, original value at `responsePath`, usually an array of strings. An example of what the data might look like:
|
||||
|
||||
```
|
||||
lazyCaches = new Map({
|
||||
"secret-v2": {
|
||||
"{ backend: 'secret', id: 'app/' }: {
|
||||
dataset: ['some', 'nested', 'secrets'],
|
||||
response: {
|
||||
request_id: 'foobar',
|
||||
data: {},
|
||||
...
|
||||
}
|
||||
},
|
||||
},
|
||||
...
|
||||
})
|
||||
```
|
||||
@ -56,7 +56,7 @@ module('Unit | Service | store', function (hooks) {
|
||||
|
||||
store.clearDataset('transit-key');
|
||||
assert.strictEqual(store.get('lazyCaches').size, 1, 'deletes one key');
|
||||
assert.notOk(store.get('lazyCaches').has(), 'cache is no longer stored');
|
||||
assert.notOk(store.get('lazyCaches').has('transit-key'), 'cache is no longer stored');
|
||||
});
|
||||
|
||||
test('store.clearAllDatasets', function (assert) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user