diff --git a/ui/app/app.js b/ui/app/app.js index f603f64f2f..2a4cdd68d1 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -21,6 +21,7 @@ export default class App extends Application { 'namespace', { 'app-router': 'router' }, 'store', + 'pagination', 'version', 'custom-messages', ], @@ -60,6 +61,7 @@ export default class App extends Application { 'path-help', { 'app-router': 'router' }, 'store', + 'pagination', 'version', 'secret-mount-path', ], @@ -78,7 +80,14 @@ export default class App extends Application { }, ldap: { dependencies: { - services: [{ 'app-router': 'router' }, 'store', 'secret-mount-path', 'flash-messages', 'auth'], + services: [ + { 'app-router': 'router' }, + 'store', + 'pagination', + 'secret-mount-path', + 'flash-messages', + 'auth', + ], externalRoutes: { secrets: 'vault.cluster.secrets.backends', }, @@ -95,6 +104,7 @@ export default class App extends Application { { 'app-router': 'router' }, 'secret-mount-path', 'store', + 'pagination', 'version', ], externalRoutes: { @@ -114,6 +124,7 @@ export default class App extends Application { { 'app-router': 'router' }, 'secret-mount-path', 'store', + 'pagination', 'version', ], externalRoutes: { @@ -125,7 +136,7 @@ export default class App extends Application { }, sync: { dependencies: { - services: ['flash-messages', 'flags', { 'app-router': 'router' }, 'store', 'version'], + services: ['flash-messages', 'flags', { 'app-router': 'router' }, 'store', 'pagination', 'version'], externalRoutes: { kvSecretOverview: 'vault.cluster.secrets.backend.kv.secret.index', clientCountOverview: 'vault.cluster.clients', diff --git a/ui/app/components/console/ui-panel.js b/ui/app/components/console/ui-panel.js index ed3524d977..91f7285426 100644 --- a/ui/app/components/console/ui-panel.js +++ b/ui/app/components/console/ui-panel.js @@ -28,7 +28,7 @@ export default Component.extend({ console: service(), router: service(), controlGroup: service(), - store: service(), + pagination: service(), 'data-test-component': 'console/ui-panel', attributeBindings: ['data-test-component'], @@ -108,7 +108,7 @@ export default Component.extend({ const currentRoute = owner.lookup(`router:main`).currentRouteName; try { - this.store.clearDataset(); + this.pagination.clearDataset(); yield this.router.transitionTo(currentRoute); this.logAndOutput(null, { type: 'success', content: 'The current screen has been refreshed!' }); } catch (error) { diff --git a/ui/app/components/generated-item-list.js b/ui/app/components/generated-item-list.js index 09aa959681..9703c66520 100644 --- a/ui/app/components/generated-item-list.js +++ b/ui/app/components/generated-item-list.js @@ -26,13 +26,13 @@ import { tracked } from '@glimmer/tracking'; export default class GeneratedItemList extends Component { @service router; - @service store; + @service pagination; @tracked itemToDelete = null; @action refreshItemList() { const route = getOwner(this).lookup(`route:${this.router.currentRouteName}`); - this.store.clearDataset(); + this.pagination.clearDataset(); route.refresh(); } } diff --git a/ui/app/components/keymgmt/distribute.js b/ui/app/components/keymgmt/distribute.js index 3c2cd84739..915f57a018 100644 --- a/ui/app/components/keymgmt/distribute.js +++ b/ui/app/components/keymgmt/distribute.js @@ -37,6 +37,7 @@ const VALID_TYPES_BY_PROVIDER = { azurekeyvault: ['rsa-2048', 'rsa-3072', 'rsa-4096'], }; export default class KeymgmtDistribute extends Component { + @service pagination; @service store; @service flashMessages; @service router; @@ -191,7 +192,7 @@ export default class KeymgmtDistribute extends Component { .then(() => { this.flashMessages.success(`Successfully distributed key ${key} to ${provider}`); // update keys on provider model - this.store.clearDataset('keymgmt/key'); + this.pagination.clearDataset('keymgmt/key'); const providerModel = this.store.peekRecord('keymgmt/provider', provider); providerModel.fetchKeys(providerModel.keys?.meta?.currentPage || 1); this.args.onClose(); diff --git a/ui/app/mixins/unload-model-route.js b/ui/app/mixins/unload-model-route.js index 7d9f886583..dc86745318 100644 --- a/ui/app/mixins/unload-model-route.js +++ b/ui/app/mixins/unload-model-route.js @@ -5,10 +5,12 @@ import Mixin from '@ember/object/mixin'; import removeRecord from 'vault/utils/remove-record'; +import { service } from '@ember/service'; // removes Ember Data records from the cache when the model // changes or you move away from the current route export default Mixin.create({ + store: service(), modelPath: 'model', unloadModel() { const { modelPath } = this; diff --git a/ui/app/models/keymgmt/provider.js b/ui/app/models/keymgmt/provider.js index d1e2e11962..3c9ff6b2c9 100644 --- a/ui/app/models/keymgmt/provider.js +++ b/ui/app/models/keymgmt/provider.js @@ -44,7 +44,7 @@ const validations = { @withModelValidations(validations) export default class KeymgmtProviderModel extends Model { - @service store; + @service pagination; @attr('string') backend; @attr('string', { label: 'Provider name', @@ -128,7 +128,7 @@ export default class KeymgmtProviderModel extends Model { } else { // try unless capabilities returns false try { - this.keys = await this.store.lazyPaginatedQuery('keymgmt/key', { + this.keys = await this.pagination.lazyPaginatedQuery('keymgmt/key', { backend: this.backend, provider: this.name, responsePath: 'data.keys', diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/index.js b/ui/app/routes/vault/cluster/access/identity/aliases/index.js index 9bdd92d28a..700835dc1d 100644 --- a/ui/app/routes/vault/cluster/access/identity/aliases/index.js +++ b/ui/app/routes/vault/cluster/access/identity/aliases/index.js @@ -8,12 +8,12 @@ import ListRoute from 'core/mixins/list-route'; import { service } from '@ember/service'; export default Route.extend(ListRoute, { - store: service(), + pagination: service(), model(params) { const itemType = this.modelFor('vault.cluster.access.identity'); const modelType = `identity/${itemType}-alias`; - return this.store + return this.pagination .lazyPaginatedQuery(modelType, { responsePath: 'data.keys', page: params.page, @@ -38,12 +38,12 @@ export default Route.extend(ListRoute, { willTransition(transition) { window.scrollTo(0, 0); if (!transition || transition.targetName !== this.routeName) { - this.store.clearDataset(); + this.pagination.clearDataset(); } return true; }, reload() { - this.store.clearDataset(); + this.pagination.clearDataset(); this.refresh(); }, }, diff --git a/ui/app/routes/vault/cluster/access/identity/index.js b/ui/app/routes/vault/cluster/access/identity/index.js index 76a6332522..7333bfef77 100644 --- a/ui/app/routes/vault/cluster/access/identity/index.js +++ b/ui/app/routes/vault/cluster/access/identity/index.js @@ -8,12 +8,12 @@ import ListRoute from 'core/mixins/list-route'; import { service } from '@ember/service'; export default Route.extend(ListRoute, { - store: service(), + pagination: service(), model(params) { const itemType = this.modelFor('vault.cluster.access.identity'); const modelType = `identity/${itemType}`; - return this.store + return this.pagination .lazyPaginatedQuery(modelType, { responsePath: 'data.keys', page: params.page, @@ -38,12 +38,12 @@ export default Route.extend(ListRoute, { willTransition(transition) { window.scrollTo(0, 0); if (transition.targetName !== this.routeName) { - this.store.clearDataset(); + this.pagination.clearDataset(); } return true; }, reload() { - this.store.clearDataset(); + this.pagination.clearDataset(); this.refresh(); }, }, diff --git a/ui/app/routes/vault/cluster/access/leases/list.js b/ui/app/routes/vault/cluster/access/leases/list.js index 1a58a9ec4c..5cdd5c1c4f 100644 --- a/ui/app/routes/vault/cluster/access/leases/list.js +++ b/ui/app/routes/vault/cluster/access/leases/list.js @@ -9,6 +9,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; export default Route.extend({ + pagination: service(), store: service(), queryParams: { @@ -26,7 +27,7 @@ export default Route.extend({ const prefix = params.prefix || ''; if (this.modelFor('vault.cluster.access.leases').canList) { return hash({ - leases: this.store + leases: this.pagination .lazyPaginatedQuery('lease', { prefix, responsePath: 'data.keys', @@ -104,7 +105,7 @@ export default Route.extend({ willTransition(transition) { window.scrollTo(0, 0); if (transition.targetName !== this.routeName) { - this.store.clearDataset(); + this.pagination.clearDataset(); } return true; }, diff --git a/ui/app/routes/vault/cluster/access/method/item/list.js b/ui/app/routes/vault/cluster/access/method/item/list.js index 1c41c1b859..1daa9c7646 100644 --- a/ui/app/routes/vault/cluster/access/method/item/list.js +++ b/ui/app/routes/vault/cluster/access/method/item/list.js @@ -9,7 +9,7 @@ import { singularize } from 'ember-inflector'; import ListRoute from 'vault/mixins/list-route'; export default Route.extend(ListRoute, { - store: service(), + pagination: service(), pathHelp: service('path-help'), getMethodAndModelInfo() { @@ -25,7 +25,7 @@ export default Route.extend(ListRoute, { const { page, pageFilter } = this.paramsFor(this.routeName); const modelType = `generated-${singularize(itemType)}-${type}`; - return this.store + return this.pagination .lazyPaginatedQuery(modelType, { responsePath: 'data.keys', page: page, @@ -46,12 +46,12 @@ export default Route.extend(ListRoute, { willTransition(transition) { window.scrollTo(0, 0); if (transition.targetName !== this.routeName) { - this.store.clearDataset(); + this.pagination.clearDataset(); } return true; }, reload() { - this.store.clearDataset(); + this.pagination.clearDataset(); this.refresh(); }, }, diff --git a/ui/app/routes/vault/cluster/access/namespaces/index.js b/ui/app/routes/vault/cluster/access/namespaces/index.js index 55265487cf..5ff4ee85bc 100644 --- a/ui/app/routes/vault/cluster/access/namespaces/index.js +++ b/ui/app/routes/vault/cluster/access/namespaces/index.js @@ -8,6 +8,7 @@ import Route from '@ember/routing/route'; import UnloadModel from 'vault/mixins/unload-model-route'; export default Route.extend(UnloadModel, { + pagination: service(), store: service(), queryParams: { @@ -27,7 +28,7 @@ export default Route.extend(UnloadModel, { model(params) { if (this.version.hasNamespaces) { - return this.store + return this.pagination .lazyPaginatedQuery('namespace', { responsePath: 'data.keys', page: Number(params?.page) || 1, @@ -74,12 +75,12 @@ export default Route.extend(UnloadModel, { willTransition(transition) { window.scrollTo(0, 0); if (!transition || transition.targetName !== this.routeName) { - this.store.clearDataset(); + this.pagination.clearDataset(); } return true; }, reload() { - this.store.clearDataset(); + this.pagination.clearDataset(); this.refresh(); }, }, diff --git a/ui/app/routes/vault/cluster/policies/index.js b/ui/app/routes/vault/cluster/policies/index.js index 34c9abf77e..48375aaa84 100644 --- a/ui/app/routes/vault/cluster/policies/index.js +++ b/ui/app/routes/vault/cluster/policies/index.js @@ -9,7 +9,7 @@ import ClusterRoute from 'vault/mixins/cluster-route'; import ListRoute from 'core/mixins/list-route'; export default Route.extend(ClusterRoute, ListRoute, { - store: service(), + pagination: service(), version: service(), shouldReturnEmptyModel(policyType, version) { @@ -21,7 +21,7 @@ export default Route.extend(ClusterRoute, ListRoute, { if (this.shouldReturnEmptyModel(policyType, this.version)) { return; } - return this.store + return this.pagination .lazyPaginatedQuery(`policy/${policyType}`, { page: params.page, pageFilter: params.pageFilter, @@ -65,12 +65,12 @@ export default Route.extend(ClusterRoute, ListRoute, { willTransition(transition) { window.scrollTo(0, 0); if (!transition || transition.targetName !== this.routeName) { - this.store.clearDataset(); + this.pagination.clearDataset(); } return true; }, reload() { - this.store.clearDataset(); + this.pagination.clearDataset(); this.refresh(); }, }, diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index 254119da4e..b3e5cdb099 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -30,6 +30,7 @@ function getValidPage(pageParam) { } export default Route.extend({ + pagination: service(), store: service(), templateName: 'vault/cluster/secrets/backend/list', pathHelp: service('path-help'), @@ -131,7 +132,7 @@ export default Route.extend({ return hash({ secret, - secrets: this.store + secrets: this.pagination .lazyPaginatedQuery(modelType, { id: secret, backend, @@ -163,7 +164,7 @@ export default Route.extend({ const has404 = this.has404; // only clear store cache if this is a new model if (secret !== controller?.baseKey?.id) { - this.store.clearDataset(); + this.pagination.clearDataset(); } controller.set('hasModel', true); controller.setProperties({ @@ -220,12 +221,12 @@ export default Route.extend({ willTransition(transition) { window.scrollTo(0, 0); if (transition.targetName !== this.routeName) { - this.store.clearDataset(); + this.pagination.clearDataset(); } return true; }, reload() { - this.store.clearDataset(); + this.pagination.clearDataset(); this.refresh(); }, }, diff --git a/ui/app/services/store.js b/ui/app/services/pagination.js similarity index 81% rename from ui/app/services/store.js rename to ui/app/services/pagination.js index 41ba746913..3486161e3c 100644 --- a/ui/app/services/store.js +++ b/ui/app/services/pagination.js @@ -3,9 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import Store, { CacheHandler } from '@ember-data/store'; -import RequestManager from '@ember-data/request'; -import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; +import Service, { service } from '@ember/service'; import { schedule } from '@ember/runloop'; import { resolve, Promise } from 'rsvp'; import { dasherize } from '@ember/string'; @@ -17,10 +15,6 @@ import sortObjects from 'vault/utils/sort-objects'; const { DEFAULT_PAGE_SIZE } = config.APP; -export function normalizeModelName(modelName) { - return dasherize(modelName); -} - export function keyForCache(query) { /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ // we want to ignore size, page, responsePath, and pageFilter in the cacheKey @@ -34,16 +28,8 @@ export function keyForCache(query) { return JSON.stringify(cacheKeyObject); } -export default class StoreService extends Store { - requestManager = new RequestManager(); - - constructor(args) { - super(args); - // If at some point we no longer need an extended store, we can remove the @ember-data/legacy-compat dep - // See: https://api.emberjs.com/ember-data/4.12/modules/@ember-data%2Frequest - this.requestManager.use([LegacyNetworkHandler]); - this.requestManager.useCache(CacheHandler); - } +export default class Pagination extends Service { + @service store; lazyCaches = new Map(); @@ -51,7 +37,7 @@ export default class StoreService extends Store { const cacheKey = keyForCache(key); const cache = this.lazyCacheForModel(modelName) || new Map(); cache.set(cacheKey, value); - const modelKey = normalizeModelName(modelName); + const modelKey = dasherize(modelName); this.lazyCaches.set(modelKey, cache); } @@ -64,7 +50,7 @@ export default class StoreService extends Store { } lazyCacheForModel(modelName) { - return this.lazyCaches.get(normalizeModelName(modelName)); + return this.lazyCaches.get(dasherize(modelName)); } // This is the public interface for the store extension - to be used just @@ -83,8 +69,8 @@ export default class StoreService extends Store { const skipCache = query.skipCache; // We don't want skipCache to be part of the actual query key, so remove it delete query.skipCache; - const adapter = this.adapterFor(modelType); - const modelName = normalizeModelName(modelType); + const adapter = this.store.adapterFor(modelType); + const modelName = dasherize(modelType); const dataCache = skipCache ? this.clearDataset(modelName) : this.getDataset(modelName, query); const responsePath = query.responsePath; assert('responsePath is required', responsePath); @@ -97,9 +83,9 @@ export default class StoreService extends Store { return resolve(this.fetchPage(modelName, query)); } return adapter - .query(this, { modelName }, query, null, adapterOptions) + .query(this.store, { modelName }, query, null, adapterOptions) .then((response) => { - const serializer = this.serializerFor(modelName); + const serializer = this.store.serializerFor(modelName); const datasetHelper = serializer.extractLazyPaginatedData; const dataset = datasetHelper ? datasetHelper.call(serializer, response) @@ -162,20 +148,16 @@ export default class StoreService extends Store { // pushes records into the store and returns the result fetchPage(modelName, query) { const response = this.constructResponse(modelName, query); - this.unloadAll(modelName); + this.store.unloadAll(modelName); return new Promise((resolve) => { // push subset of records into the store schedule('destroy', () => { - this.push( - this.serializerFor(modelName).normalizeResponse( - this, - this.modelFor(modelName), - response, - null, - 'query' - ) + this.store.push( + this.store + .serializerFor(modelName) + .normalizeResponse(this.store, this.store.modelFor(modelName), response, null, 'query') ); - const model = this.peekAll(modelName).slice(); + const model = this.store.peekAll(modelName).slice(); model.set('meta', response.meta); resolve(model); }); diff --git a/ui/docs/client-pagination.md b/ui/docs/client-pagination.md index 7fd0197c6c..44d425f593 100644 --- a/ui/docs/client-pagination.md +++ b/ui/docs/client-pagination.md @@ -1,10 +1,10 @@ # 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. +Our custom `pagination` 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. It was originally a custom method in our `store` service that extended the ember-data `store` but now is it's own `pagination` service. ## 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` +Rather than use `store.query`, use `pagination.lazyPaginatedQuery`. It generally uses the same inputs, but accepts additional keys in the query object `size`, `page`, `responsePath`, `pageFilter` ### Before @@ -23,12 +23,12 @@ export default class ExampleRoute extends Route { ```js export default class ExampleRoute extends Route { - @service store; + @service pagination; model(params) { const { page, pageFilter, secret } = params; const { backend } = this.paramsFor('vault.cluster.secrets.backend'); - return this.store.lazyPaginatedQuery('secret', { + return this.pagination.lazyPaginatedQuery('secret', { backend, id: secret, size, @@ -47,11 +47,11 @@ In order to interrupt the regular serialization when using `lazyPaginatedData`, ## 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. +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 `pagination.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: +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. `pagination.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 `pagination.lazyCaches` looks like this: ``` lazyCaches = new Map({ @@ -61,7 +61,7 @@ lazyCaches = new 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: +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 pagination.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({ diff --git a/ui/lib/config-ui/addon/components/messages/page/create-and-edit.js b/ui/lib/config-ui/addon/components/messages/page/create-and-edit.js index 17a476ad60..8ba4c827a7 100644 --- a/ui/lib/config-ui/addon/components/messages/page/create-and-edit.js +++ b/ui/lib/config-ui/addon/components/messages/page/create-and-edit.js @@ -25,6 +25,7 @@ import { isAfter } from 'date-fns'; export default class MessagesList extends Component { @service('app-router') router; @service store; + @service pagination; @service flashMessages; @service customMessages; @service namespace; @@ -75,7 +76,7 @@ export default class MessagesList extends Component { const { isNew } = this.args.message; const { id, title } = yield this.args.message.save(); this.flashMessages.success(`Successfully ${isNew ? 'created' : 'updated'} ${title} message.`); - this.store.clearDataset('config-ui/message'); + this.pagination.clearDataset('config-ui/message'); this.customMessages.fetchMessages(this.namespace.path); this.router.transitionTo('vault.cluster.config-ui.messages.message.details', id); } diff --git a/ui/lib/config-ui/addon/components/messages/page/details.js b/ui/lib/config-ui/addon/components/messages/page/details.js index 83e4a53f64..b09ad182dc 100644 --- a/ui/lib/config-ui/addon/components/messages/page/details.js +++ b/ui/lib/config-ui/addon/components/messages/page/details.js @@ -19,17 +19,17 @@ import errorMessage from 'vault/utils/error-message'; */ export default class MessageDetails extends Component { - @service store; @service('app-router') router; @service flashMessages; @service customMessages; @service namespace; + @service pagination; @action async deleteMessage() { try { - this.store.clearDataset('config-ui/message'); await this.args.message.destroyRecord(this.args.message.id); + this.pagination.clearDataset('config-ui/message'); this.router.transitionTo('vault.cluster.config-ui.messages'); this.customMessages.fetchMessages(this.namespace.path); this.flashMessages.success(`Successfully deleted ${this.args.message.title}.`); diff --git a/ui/lib/config-ui/addon/components/messages/page/list.js b/ui/lib/config-ui/addon/components/messages/page/list.js index a8a7a99181..5361ee909e 100644 --- a/ui/lib/config-ui/addon/components/messages/page/list.js +++ b/ui/lib/config-ui/addon/components/messages/page/list.js @@ -23,11 +23,11 @@ import errorMessage from 'vault/utils/error-message'; */ export default class MessagesList extends Component { - @service store; - @service('app-router') router; + @service customMessages; @service flashMessages; @service namespace; - @service customMessages; + @service pagination; + @service('app-router') router; @tracked showMaxMessageModal = false; @tracked messageToDelete = null; @@ -90,8 +90,8 @@ export default class MessagesList extends Component { @task *deleteMessage(message) { try { - this.store.clearDataset('config-ui/message'); yield message.destroyRecord(message.id); + this.pagination.clearDataset('config-ui/message'); this.router.transitionTo('vault.cluster.config-ui.messages'); this.customMessages.fetchMessages(this.namespace.path); this.flashMessages.success(`Successfully deleted ${message.title}.`); diff --git a/ui/lib/config-ui/addon/engine.js b/ui/lib/config-ui/addon/engine.js index 32fc297b05..f8b1a0dbcc 100644 --- a/ui/lib/config-ui/addon/engine.js +++ b/ui/lib/config-ui/addon/engine.js @@ -16,7 +16,16 @@ export default class ConfigUiEngine extends Engine { modulePrefix = modulePrefix; Resolver = Resolver; dependencies = { - services: ['auth', 'store', 'flash-messages', 'namespace', 'app-router', 'version', 'custom-messages'], + services: [ + 'auth', + 'store', + 'pagination', + 'flash-messages', + 'namespace', + 'app-router', + 'version', + 'custom-messages', + ], }; } diff --git a/ui/lib/config-ui/addon/routes/messages/index.js b/ui/lib/config-ui/addon/routes/messages/index.js index ee882b6c3d..de3a6e4be8 100644 --- a/ui/lib/config-ui/addon/routes/messages/index.js +++ b/ui/lib/config-ui/addon/routes/messages/index.js @@ -8,7 +8,7 @@ import { service } from '@ember/service'; import { hash } from 'rsvp'; export default class MessagesRoute extends Route { - @service store; + @service pagination; queryParams = { page: { @@ -38,7 +38,7 @@ export default class MessagesRoute extends Route { if (status === 'active') active = true; if (status === 'inactive') active = false; - const messages = this.store + const messages = this.pagination .lazyPaginatedQuery('config-ui/message', { authenticated, pageFilter: filter, diff --git a/ui/lib/core/addon/mixins/list-route.js b/ui/lib/core/addon/mixins/list-route.js index 1a78345b78..ed097d8b9a 100644 --- a/ui/lib/core/addon/mixins/list-route.js +++ b/ui/lib/core/addon/mixins/list-route.js @@ -35,12 +35,12 @@ export default Mixin.create({ willTransition(transition) { window.scrollTo(0, 0); if (transition.targetName !== this.routeName) { - this.store.clearDataset(); + this.pagination.clearDataset(); } return true; }, reload() { - this.store.clearDataset(); + this.pagination.clearDataset(); this.refresh(); }, }, diff --git a/ui/lib/kmip/addon/engine.js b/ui/lib/kmip/addon/engine.js index 16a3468d89..6c9a0511d6 100644 --- a/ui/lib/kmip/addon/engine.js +++ b/ui/lib/kmip/addon/engine.js @@ -5,15 +5,14 @@ import Engine from 'ember-engines/engine'; import loadInitializers from 'ember-load-initializers'; -import Resolver from './resolver'; +import Resolver from 'ember-resolver'; import config from './config/environment'; const { modulePrefix } = config; -/* eslint-disable ember/avoid-leaking-state-in-ember-objects */ -const Eng = Engine.extend({ - modulePrefix, - Resolver, - dependencies: { +export default class KmipEngine extends Engine { + modulePrefix = modulePrefix; + Resolver = Resolver; + dependencies = { services: [ 'auth', 'download', @@ -22,13 +21,12 @@ const Eng = Engine.extend({ 'path-help', 'app-router', 'store', + 'pagination', 'version', 'secret-mount-path', ], externalRoutes: ['secrets'], - }, -}); + }; +} -loadInitializers(Eng, modulePrefix); - -export default Eng; +loadInitializers(KmipEngine, modulePrefix); diff --git a/ui/lib/kmip/addon/resolver.js b/ui/lib/kmip/addon/resolver.js deleted file mode 100644 index deb3e8ad3b..0000000000 --- a/ui/lib/kmip/addon/resolver.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Resolver from 'ember-resolver'; - -export default Resolver; diff --git a/ui/lib/kmip/addon/routes/credentials/index.js b/ui/lib/kmip/addon/routes/credentials/index.js index f7bd93b497..f886226087 100644 --- a/ui/lib/kmip/addon/routes/credentials/index.js +++ b/ui/lib/kmip/addon/routes/credentials/index.js @@ -8,7 +8,7 @@ import ListRoute from 'core/mixins/list-route'; import { service } from '@ember/service'; export default Route.extend(ListRoute, { - store: service(), + pagination: service(), secretMountPath: service(), credParams() { const { role_name: role, scope_name: scope } = this.paramsFor('credentials'); @@ -19,7 +19,7 @@ export default Route.extend(ListRoute, { }, model(params) { const { role, scope } = this.credParams(); - return this.store + return this.pagination .lazyPaginatedQuery('kmip/credential', { role, scope, diff --git a/ui/lib/kmip/addon/routes/scope/roles.js b/ui/lib/kmip/addon/routes/scope/roles.js index 4976475d97..c9e7edb069 100644 --- a/ui/lib/kmip/addon/routes/scope/roles.js +++ b/ui/lib/kmip/addon/routes/scope/roles.js @@ -8,7 +8,7 @@ import ListRoute from 'core/mixins/list-route'; import { service } from '@ember/service'; export default Route.extend(ListRoute, { - store: service(), + pagination: service(), secretMountPath: service(), pathHelp: service(), scope() { @@ -18,7 +18,7 @@ export default Route.extend(ListRoute, { return this.pathHelp.hydrateModel('kmip/role', this.secretMountPath.currentPath); }, model(params) { - return this.store + return this.pagination .lazyPaginatedQuery('kmip/role', { backend: this.secretMountPath.currentPath, scope: this.scope(), diff --git a/ui/lib/kmip/addon/routes/scopes/index.js b/ui/lib/kmip/addon/routes/scopes/index.js index 3816f4cd9a..f25c0c1635 100644 --- a/ui/lib/kmip/addon/routes/scopes/index.js +++ b/ui/lib/kmip/addon/routes/scopes/index.js @@ -8,10 +8,10 @@ import { service } from '@ember/service'; import ListRoute from 'core/mixins/list-route'; export default Route.extend(ListRoute, { - store: service(), + pagination: service(), secretMountPath: service(), model(params) { - return this.store + return this.pagination .lazyPaginatedQuery('kmip/scope', { backend: this.secretMountPath.currentPath, responsePath: 'data.keys', @@ -31,12 +31,12 @@ export default Route.extend(ListRoute, { willTransition(transition) { window.scrollTo(0, 0); if (transition.targetName !== this.routeName) { - this.store.clearDataset(); + this.pagination.clearDataset(); } return true; }, reload() { - this.store.clearDataset(); + this.pagination.clearDataset(); this.refresh(); }, }, diff --git a/ui/lib/kv/addon/components/page/list.js b/ui/lib/kv/addon/components/page/list.js index cb50ba3229..327ab1f64a 100644 --- a/ui/lib/kv/addon/components/page/list.js +++ b/ui/lib/kv/addon/components/page/list.js @@ -27,7 +27,7 @@ import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs'; export default class KvListPageComponent extends Component { @service flashMessages; @service('app-router') router; - @service store; + @service pagination; @tracked secretPath; @tracked metadataToDelete = null; // set to the metadata intended to delete @@ -57,7 +57,7 @@ export default class KvListPageComponent extends Component { try { // The model passed in is a kv/metadata model await model.destroyRecord(); - this.store.clearDataset('kv/metadata'); // Clear out the store cache so that the metadata/list view is updated. + this.pagination.clearDataset('kv/metadata'); // Clear out the pagination cache so that the metadata/list view is updated. const message = `Successfully deleted the metadata and all version data of the secret ${model.fullSecretPath}.`; this.flashMessages.success(message); // if you've deleted a secret from within a directory, transition to its parent directory. diff --git a/ui/lib/kv/addon/components/page/secret/metadata/details.js b/ui/lib/kv/addon/components/page/secret/metadata/details.js index 55b26493c4..48a16b7253 100644 --- a/ui/lib/kv/addon/components/page/secret/metadata/details.js +++ b/ui/lib/kv/addon/components/page/secret/metadata/details.js @@ -39,6 +39,7 @@ export default class KvSecretMetadataDetails extends Component { @service flashMessages; @service('app-router') router; @service store; + @service pagination; @tracked error = null; @tracked customMetadataFromData = null; @@ -54,7 +55,7 @@ export default class KvSecretMetadataDetails extends Component { const adapter = this.store.adapterFor('kv/metadata'); try { await adapter.deleteMetadata(backend, path); - this.store.clearDataset('kv/metadata'); // Clear out the store cache so that the metadata/list view is updated. + this.pagination.clearDataset('kv/metadata'); // Clear out the store cache so that the metadata/list view is updated. this.flashMessages.success( `Successfully deleted the metadata and all version data for the secret ${path}.` ); diff --git a/ui/lib/kv/addon/components/page/secrets/create.js b/ui/lib/kv/addon/components/page/secrets/create.js index 961ba1befa..33e35cae40 100644 --- a/ui/lib/kv/addon/components/page/secrets/create.js +++ b/ui/lib/kv/addon/components/page/secrets/create.js @@ -29,7 +29,7 @@ export default class KvSecretCreate extends Component { @service controlGroup; @service flashMessages; @service('app-router') router; - @service store; + @service pagination; @tracked showJsonView = false; @tracked errorMessage; @@ -60,7 +60,7 @@ export default class KvSecretCreate extends Component { try { // try saving secret data first yield secret.save(); - this.store.clearDataset('kv/metadata'); // Clear out the store cache so that the metadata/list view is updated. + this.pagination.clearDataset('kv/metadata'); // Clear out the pagination cache so that the metadata/list view is updated. this.flashMessages.success(`Successfully saved secret data for: ${secret.path}.`); } catch (error) { let message = errorMessage(error); diff --git a/ui/lib/kv/addon/engine.js b/ui/lib/kv/addon/engine.js index 3bb5fcab88..3e2457d0d6 100644 --- a/ui/lib/kv/addon/engine.js +++ b/ui/lib/kv/addon/engine.js @@ -25,6 +25,7 @@ export default class KvEngine extends Engine { 'app-router', 'secret-mount-path', 'store', + 'pagination', 'version', ], externalRoutes: ['secrets', 'syncDestination'], diff --git a/ui/lib/kv/addon/routes/list-directory.js b/ui/lib/kv/addon/routes/list-directory.js index 3ead4324d8..0b6e787f6f 100644 --- a/ui/lib/kv/addon/routes/list-directory.js +++ b/ui/lib/kv/addon/routes/list-directory.js @@ -9,7 +9,7 @@ import { hash } from 'rsvp'; import { pathIsDirectory, breadcrumbsForSecret } from 'kv/utils/kv-breadcrumbs'; export default class KvSecretsListRoute extends Route { - @service store; + @service pagination; @service('app-router') router; @service secretMountPath; @@ -23,7 +23,7 @@ export default class KvSecretsListRoute extends Route { }; async fetchMetadata(backend, pathToSecret, params) { - return await this.store + return await this.pagination .lazyPaginatedQuery('kv/metadata', { backend, responsePath: 'data.keys', diff --git a/ui/lib/ldap/addon/components/page/role/create-and-edit.ts b/ui/lib/ldap/addon/components/page/role/create-and-edit.ts index 582f2d8f27..c2a5aa139e 100644 --- a/ui/lib/ldap/addon/components/page/role/create-and-edit.ts +++ b/ui/lib/ldap/addon/components/page/role/create-and-edit.ts @@ -15,7 +15,7 @@ import type LdapRoleModel from 'vault/models/ldap/role'; import { Breadcrumb, ValidationMap } from 'vault/vault/app-types'; import type FlashMessageService from 'vault/services/flash-messages'; import type RouterService from '@ember/routing/router-service'; -import type StoreService from 'vault/services/store'; +import type PaginationService from 'vault/services/pagination'; interface Args { model: LdapRoleModel; @@ -31,7 +31,7 @@ interface RoleTypeOption { export default class LdapCreateAndEditRolePageComponent extends Component { @service declare readonly flashMessages: FlashMessageService; @service('app-router') declare readonly router: RouterService; - @service declare readonly store: StoreService; + @service declare readonly pagination: PaginationService; @tracked modelValidations: ValidationMap | null = null; @tracked invalidFormMessage = ''; @@ -71,7 +71,7 @@ export default class LdapCreateAndEditRolePageComponent extends Component yield model.save(); this.flashMessages.success(`Successfully ${action} the role ${model.name}`); if (action === 'created') { - this.store.clearDataset('ldap/role'); + this.pagination.clearDataset('ldap/role'); } this.router.transitionTo( 'vault.cluster.secrets.backend.ldap.roles.role.details', diff --git a/ui/lib/ldap/addon/components/page/role/details.ts b/ui/lib/ldap/addon/components/page/role/details.ts index 75444e182b..be574b16b1 100644 --- a/ui/lib/ldap/addon/components/page/role/details.ts +++ b/ui/lib/ldap/addon/components/page/role/details.ts @@ -14,7 +14,7 @@ import type LdapRoleModel from 'vault/models/ldap/role'; import { Breadcrumb } from 'vault/vault/app-types'; import type FlashMessageService from 'vault/services/flash-messages'; import type RouterService from '@ember/routing/router-service'; -import type StoreService from 'vault/services/store'; +import type PaginationService from 'vault/services/pagination'; interface Args { model: LdapRoleModel; @@ -24,14 +24,14 @@ interface Args { export default class LdapRoleDetailsPageComponent extends Component { @service declare readonly flashMessages: FlashMessageService; @service('app-router') declare readonly router: RouterService; - @service declare readonly store: StoreService; + @service declare readonly pagination: PaginationService; @action async delete() { try { await this.args.model.destroyRecord(); this.flashMessages.success('Role deleted successfully.'); - this.store.clearDataset('ldap/role'); + this.pagination.clearDataset('ldap/role'); this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles'); } catch (error) { const message = errorMessage(error, 'Unable to delete role. Please try again or contact support.'); diff --git a/ui/lib/ldap/addon/components/page/roles.ts b/ui/lib/ldap/addon/components/page/roles.ts index 9489e39eaa..cc31803d3c 100644 --- a/ui/lib/ldap/addon/components/page/roles.ts +++ b/ui/lib/ldap/addon/components/page/roles.ts @@ -14,7 +14,7 @@ import type SecretEngineModel from 'vault/models/secret-engine'; import type FlashMessageService from 'vault/services/flash-messages'; import type { Breadcrumb, EngineOwner } from 'vault/vault/app-types'; import type RouterService from '@ember/routing/router-service'; -import type StoreService from 'vault/services/store'; +import type PaginationService from 'vault/services/pagination'; import { tracked } from '@glimmer/tracking'; interface Args { @@ -28,7 +28,7 @@ interface Args { export default class LdapRolesPageComponent extends Component { @service declare readonly flashMessages: FlashMessageService; @service('app-router') declare readonly router: RouterService; - @service declare readonly store: StoreService; + @service declare readonly pagination: PaginationService; @tracked credsToRotate: LdapRoleModel | null = null; @tracked roleToDelete: LdapRoleModel | null = null; @@ -64,7 +64,7 @@ export default class LdapRolesPageComponent extends Component { try { const message = `Successfully deleted role ${model.name}.`; await model.destroyRecord(); - this.store.clearDataset('ldap/role'); + this.pagination.clearDataset('ldap/role'); this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles'); this.flashMessages.success(message); } catch (error) { diff --git a/ui/lib/ldap/addon/engine.js b/ui/lib/ldap/addon/engine.js index c2abaaf766..daf761acb2 100644 --- a/ui/lib/ldap/addon/engine.js +++ b/ui/lib/ldap/addon/engine.js @@ -14,7 +14,7 @@ export default class LdapEngine extends Engine { modulePrefix = modulePrefix; Resolver = Resolver; dependencies = { - services: ['app-router', 'store', 'secret-mount-path', 'flash-messages', 'auth'], + services: ['app-router', 'store', 'pagination', 'secret-mount-path', 'flash-messages', 'auth'], externalRoutes: ['secrets'], }; } diff --git a/ui/lib/ldap/addon/routes/roles/index.ts b/ui/lib/ldap/addon/routes/roles/index.ts index 962c48cf9b..ce6d530930 100644 --- a/ui/lib/ldap/addon/routes/roles/index.ts +++ b/ui/lib/ldap/addon/routes/roles/index.ts @@ -9,6 +9,7 @@ import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; import { hash } from 'rsvp'; import type StoreService from 'vault/services/store'; +import type PaginationService from 'vault/services/pagination'; import type SecretMountPath from 'vault/services/secret-mount-path'; import type Transition from '@ember/routing/transition'; import type LdapRoleModel from 'vault/models/ldap/role'; @@ -36,6 +37,7 @@ interface LdapRolesRouteParams { @withConfig('ldap/config') export default class LdapRolesRoute extends Route { @service declare readonly store: StoreService; + @service declare readonly pagination: PaginationService; @service declare readonly secretMountPath: SecretMountPath; declare promptConfig: boolean; @@ -54,7 +56,7 @@ export default class LdapRolesRoute extends Route { return hash({ backendModel, promptConfig: this.promptConfig, - roles: this.store.lazyPaginatedQuery( + roles: this.pagination.lazyPaginatedQuery( 'ldap/role', { backend: backendModel.id, diff --git a/ui/lib/pki/addon/engine.js b/ui/lib/pki/addon/engine.js index 178a88c2a9..7c0bc3bb55 100644 --- a/ui/lib/pki/addon/engine.js +++ b/ui/lib/pki/addon/engine.js @@ -25,6 +25,7 @@ export default class PkiEngine extends Engine { 'app-router', 'secret-mount-path', 'store', + 'pagination', 'version', ], externalRoutes: ['secrets', 'secretsListRootConfiguration', 'externalMountIssuer'], diff --git a/ui/lib/pki/addon/routes/certificates/index.js b/ui/lib/pki/addon/routes/certificates/index.js index 0d21c887e4..a0d621687d 100644 --- a/ui/lib/pki/addon/routes/certificates/index.js +++ b/ui/lib/pki/addon/routes/certificates/index.js @@ -11,8 +11,9 @@ import { getCliMessage } from 'pki/routes/overview'; @withConfig() export default class PkiCertificatesIndexRoute extends Route { - @service store; + @service pagination; @service secretMountPath; + @service store; // used by @withConfig decorator queryParams = { page: { @@ -23,7 +24,7 @@ export default class PkiCertificatesIndexRoute extends Route { async fetchCertificates(params) { try { const page = Number(params.page) || 1; - return await this.store.lazyPaginatedQuery('pki/certificate/base', { + return await this.pagination.lazyPaginatedQuery('pki/certificate/base', { backend: this.secretMountPath.currentPath, responsePath: 'data.keys', page, diff --git a/ui/lib/pki/addon/routes/issuers/index.js b/ui/lib/pki/addon/routes/issuers/index.js index c638048850..77cdd1f7c4 100644 --- a/ui/lib/pki/addon/routes/issuers/index.js +++ b/ui/lib/pki/addon/routes/issuers/index.js @@ -7,12 +7,12 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; export default class PkiIssuersListRoute extends Route { - @service store; + @service pagination; @service secretMountPath; model(params) { const page = Number(params.page) || 1; - return this.store + return this.pagination .lazyPaginatedQuery('pki/issuer', { backend: this.secretMountPath.currentPath, responsePath: 'data.keys', diff --git a/ui/lib/pki/addon/routes/keys/index.js b/ui/lib/pki/addon/routes/keys/index.js index 5c669d63f8..432438223a 100644 --- a/ui/lib/pki/addon/routes/keys/index.js +++ b/ui/lib/pki/addon/routes/keys/index.js @@ -11,8 +11,9 @@ import { PKI_DEFAULT_EMPTY_STATE_MSG } from 'pki/routes/overview'; @withConfig() export default class PkiKeysIndexRoute extends Route { + @service pagination; @service secretMountPath; - @service store; + @service store; // used by @withConfig decorator queryParams = { page: { @@ -25,7 +26,7 @@ export default class PkiKeysIndexRoute extends Route { return hash({ hasConfig: this.pkiMountHasConfig, parentModel: this.modelFor('keys'), - keyModels: this.store + keyModels: this.pagination .lazyPaginatedQuery('pki/key', { backend: this.secretMountPath.currentPath, responsePath: 'data.keys', diff --git a/ui/lib/pki/addon/routes/roles/index.js b/ui/lib/pki/addon/routes/roles/index.js index 3dae1186f8..57f4a0bd7d 100644 --- a/ui/lib/pki/addon/routes/roles/index.js +++ b/ui/lib/pki/addon/routes/roles/index.js @@ -10,8 +10,9 @@ import { hash } from 'rsvp'; import { getCliMessage } from 'pki/routes/overview'; @withConfig() export default class PkiRolesIndexRoute extends Route { - @service store; + @service store; // used by @withConfig decorator @service secretMountPath; + @service pagination; queryParams = { page: { @@ -22,7 +23,7 @@ export default class PkiRolesIndexRoute extends Route { async fetchRoles(params) { try { const page = Number(params.page) || 1; - return await this.store.lazyPaginatedQuery('pki/role', { + return await this.pagination.lazyPaginatedQuery('pki/role', { backend: this.secretMountPath.currentPath, responsePath: 'data.keys', page, diff --git a/ui/lib/sync/addon/components/secrets/destination-header.ts b/ui/lib/sync/addon/components/secrets/destination-header.ts index bd17cd907c..e3293f968e 100644 --- a/ui/lib/sync/addon/components/secrets/destination-header.ts +++ b/ui/lib/sync/addon/components/secrets/destination-header.ts @@ -10,7 +10,7 @@ import errorMessage from 'vault/utils/error-message'; import type SyncDestinationModel from 'vault/models/sync/destination'; import type RouterService from '@ember/routing/router-service'; -import type StoreService from 'vault/services/store'; +import type PaginationService from 'vault/services/pagination'; import type FlashMessageService from 'vault/services/flash-messages'; interface Args { @@ -19,7 +19,7 @@ interface Args { export default class DestinationsTabsToolbar extends Component { @service('app-router') declare readonly router: RouterService; - @service declare readonly store: StoreService; + @service declare readonly pagination: PaginationService; @service declare readonly flashMessages: FlashMessageService; @action @@ -28,7 +28,7 @@ export default class DestinationsTabsToolbar extends Component { const { destination } = this.args; const message = `Destination ${destination.name} has been queued for deletion.`; await destination.destroyRecord(); - this.store.clearDataset('sync/destination'); + this.pagination.clearDataset('sync/destination'); this.router.transitionTo('vault.cluster.sync.secrets.overview'); this.flashMessages.success(message); } catch (error) { diff --git a/ui/lib/sync/addon/components/secrets/page/destinations.ts b/ui/lib/sync/addon/components/secrets/page/destinations.ts index 7f7ea756a4..d85cc1577f 100644 --- a/ui/lib/sync/addon/components/secrets/page/destinations.ts +++ b/ui/lib/sync/addon/components/secrets/page/destinations.ts @@ -14,7 +14,7 @@ import { next } from '@ember/runloop'; import type SyncDestinationModel from 'vault/vault/models/sync/destination'; import type RouterService from '@ember/routing/router-service'; -import type StoreService from 'vault/services/store'; +import type PaginationService from 'vault/services/pagination'; import type FlashMessageService from 'vault/services/flash-messages'; import type { EngineOwner } from 'vault/vault/app-types'; import type { SyncDestinationName, SyncDestinationType } from 'vault/vault/helpers/sync-destinations'; @@ -28,7 +28,7 @@ interface Args { export default class SyncSecretsDestinationsPageComponent extends Component { @service('app-router') declare readonly router: RouterService; - @service declare readonly store: StoreService; + @service declare readonly pagination: PaginationService; @service declare readonly flashMessages: FlashMessageService; @tracked destinationToDelete: SyncDestinationModel | null = null; @@ -98,7 +98,7 @@ export default class SyncSecretsDestinationsPageComponent extends Component { @service declare readonly flashMessages: FlashMessageService; @service('app-router') declare readonly router: RouterService; - @service declare readonly store: StoreService; + @service declare readonly pagination: PaginationService; @tracked modelValidations: ValidationMap | null = null; @tracked invalidFormMessage = ''; @@ -95,7 +95,7 @@ export default class DestinationsCreateForm extends Component { // if the user then attempts to update the record the credential will get overwritten with the masked placeholder value // since the record will be fetched from the details route we can safely unload it to avoid the aforementioned issue destination.unloadRecord(); - this.store.clearDataset('sync/destination'); + this.pagination.clearDataset('sync/destination'); } this.router.transitionTo( 'vault.cluster.sync.secrets.destinations.destination.details', diff --git a/ui/lib/sync/addon/components/secrets/page/destinations/destination/secrets.ts b/ui/lib/sync/addon/components/secrets/page/destinations/destination/secrets.ts index 3f798ded99..abc369095f 100644 --- a/ui/lib/sync/addon/components/secrets/page/destinations/destination/secrets.ts +++ b/ui/lib/sync/addon/components/secrets/page/destinations/destination/secrets.ts @@ -13,7 +13,7 @@ import errorMessage from 'vault/utils/error-message'; import SyncDestinationModel from 'vault/vault/models/sync/destination'; import type SyncAssociationModel from 'vault/vault/models/sync/association'; import type RouterService from '@ember/routing/router-service'; -import type StoreService from 'vault/services/store'; +import type PaginationService from 'vault/services/pagination'; import type FlashMessageService from 'vault/services/flash-messages'; import type { EngineOwner } from 'vault/vault/app-types'; @@ -24,7 +24,7 @@ interface Args { export default class SyncSecretsDestinationsPageComponent extends Component { @service('app-router') declare readonly router: RouterService; - @service declare readonly store: StoreService; + @service declare readonly pagination: PaginationService; @service declare readonly flashMessages: FlashMessageService; @tracked secretToUnsync: SyncAssociationModel | null = null; @@ -41,7 +41,7 @@ export default class SyncSecretsDestinationsPageComponent extends Component { @service('app-router') declare readonly router: RouterService; @service declare readonly store: StoreService; @service declare readonly flashMessages: FlashMessageService; + @service declare readonly pagination: PaginationService; constructor(owner: unknown, args: Args) { super(owner, args); @@ -46,7 +48,7 @@ export default class DestinationSyncPageComponent extends Component { } willDestroy(): void { - this.store.clearDataset('sync/association'); + this.pagination.clearDataset('sync/association'); super.willDestroy(); } diff --git a/ui/lib/sync/addon/engine.js b/ui/lib/sync/addon/engine.js index fc8b344872..20b3a17362 100644 --- a/ui/lib/sync/addon/engine.js +++ b/ui/lib/sync/addon/engine.js @@ -14,7 +14,7 @@ export default class SyncEngine extends Engine { modulePrefix = modulePrefix; Resolver = Resolver; dependencies = { - services: ['flash-messages', 'flags', 'app-router', 'store', 'version'], + services: ['flash-messages', 'flags', 'app-router', 'store', 'pagination', 'version'], externalRoutes: ['kvSecretOverview', 'clientCountOverview'], }; } diff --git a/ui/lib/sync/addon/routes/secrets/destinations/destination/secrets.ts b/ui/lib/sync/addon/routes/secrets/destinations/destination/secrets.ts index 6fcdda8d99..f2ba3a91bd 100644 --- a/ui/lib/sync/addon/routes/secrets/destinations/destination/secrets.ts +++ b/ui/lib/sync/addon/routes/secrets/destinations/destination/secrets.ts @@ -7,7 +7,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { hash } from 'rsvp'; -import type StoreService from 'vault/services/store'; +import type PaginationService from 'vault/services/pagination'; import type SyncDestinationModel from 'vault/vault/models/sync/destination'; import type SyncAssociationModel from 'vault/vault/models/sync/association'; import type Controller from '@ember/controller'; @@ -27,7 +27,7 @@ interface SyncDestinationSecretsController extends Controller { } export default class SyncDestinationSecretsRoute extends Route { - @service declare readonly store: StoreService; + @service declare readonly pagination: PaginationService; queryParams = { page: { @@ -39,7 +39,7 @@ export default class SyncDestinationSecretsRoute extends Route { const destination = this.modelFor('secrets.destinations.destination') as SyncDestinationModel; return hash({ destination, - associations: this.store.lazyPaginatedQuery('sync/association', { + associations: this.pagination.lazyPaginatedQuery('sync/association', { responsePath: 'data.keys', page: Number(params.page) || 1, destinationType: destination.type, diff --git a/ui/lib/sync/addon/routes/secrets/destinations/index.ts b/ui/lib/sync/addon/routes/secrets/destinations/index.ts index 52b1140ed0..5508bd849d 100644 --- a/ui/lib/sync/addon/routes/secrets/destinations/index.ts +++ b/ui/lib/sync/addon/routes/secrets/destinations/index.ts @@ -7,7 +7,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { hash } from 'rsvp'; -import type StoreService from 'vault/services/store'; +import type PaginationService from 'vault/services/pagination'; import type RouterService from '@ember/routing/router-service'; import type { ModelFrom } from 'vault/vault/route'; import type SyncDestinationModel from 'vault/vault/models/sync/destination'; @@ -33,7 +33,7 @@ interface SyncSecretsDestinationsController extends Controller { } export default class SyncSecretsDestinationsIndexRoute extends Route { - @service declare readonly store: StoreService; + @service declare readonly pagination: PaginationService; @service('app-router') declare readonly router: RouterService; queryParams = { @@ -73,7 +73,7 @@ export default class SyncSecretsDestinationsIndexRoute extends Route { async model(params: SyncSecretsDestinationsIndexRouteParams) { const { name, type, page } = params; return hash({ - destinations: this.store.lazyPaginatedQuery('sync/destination', { + destinations: this.pagination.lazyPaginatedQuery('sync/destination', { page: Number(page) || 1, pageFilter: (dataset: Array) => this.filterData(dataset, name, type), responsePath: 'data.keys', diff --git a/ui/tests/integration/components/sync/secrets/destination-header-test.js b/ui/tests/integration/components/sync/secrets/destination-header-test.js index 11c3c11ba7..07e7ac0b15 100644 --- a/ui/tests/integration/components/sync/secrets/destination-header-test.js +++ b/ui/tests/integration/components/sync/secrets/destination-header-test.js @@ -47,7 +47,7 @@ module('Integration | Component | sync | Secrets::DestinationHeader', function ( assert.expect(3); const transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo'); - const clearDatasetStub = sinon.stub(this.store, 'clearDataset'); + const clearDatasetStub = sinon.stub(this.owner.lookup('service:pagination'), 'clearDataset'); this.server.delete('/sys/sync/destinations/aws-sm/us-west-1', () => { assert.ok(true, 'Request made to delete destination'); diff --git a/ui/tests/integration/components/sync/secrets/page/destinations-test.js b/ui/tests/integration/components/sync/secrets/page/destinations-test.js index 57daee409f..e070c2f1f5 100644 --- a/ui/tests/integration/components/sync/secrets/page/destinations-test.js +++ b/ui/tests/integration/components/sync/secrets/page/destinations-test.js @@ -56,7 +56,7 @@ module('Integration | Component | sync | Page::Destinations', function (hooks) { }; this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo'); - this.clearDatasetStub = sinon.stub(store, 'clearDataset'); + this.clearDatasetStub = sinon.stub(this.owner.lookup('service:pagination'), 'clearDataset'); }); test('it should render header and tabs', async function (assert) { diff --git a/ui/tests/integration/components/sync/secrets/page/destinations/create-and-edit-test.js b/ui/tests/integration/components/sync/secrets/page/destinations/create-and-edit-test.js index 3f4a32e5e4..6a62bc9737 100644 --- a/ui/tests/integration/components/sync/secrets/page/destinations/create-and-edit-test.js +++ b/ui/tests/integration/components/sync/secrets/page/destinations/create-and-edit-test.js @@ -24,7 +24,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE hooks.beforeEach(function () { this.store = this.owner.lookup('service:store'); this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo'); - this.clearDatasetStub = sinon.stub(this.store, 'clearDataset'); + this.clearDatasetStub = sinon.stub(this.owner.lookup('service:pagination'), 'clearDataset'); this.renderFormComponent = () => { return render(hbs` `, { diff --git a/ui/tests/integration/components/sync/secrets/page/destinations/destination/sync-test.js b/ui/tests/integration/components/sync/secrets/page/destinations/destination/sync-test.js index fdb4127f44..88bab1e4bb 100644 --- a/ui/tests/integration/components/sync/secrets/page/destinations/destination/sync-test.js +++ b/ui/tests/integration/components/sync/secrets/page/destinations/destination/sync-test.js @@ -140,7 +140,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::Destinatio }); test('it should clear sync associations from store in willDestroy hook', async function (assert) { - const clearDatasetStub = sinon.stub(this.store, 'clearDataset'); + const clearDatasetStub = sinon.stub(this.owner.lookup('service:pagination'), 'clearDataset'); this.renderComponent = true; await render( diff --git a/ui/tests/unit/adapters/sync/associations-test.js b/ui/tests/unit/adapters/sync/associations-test.js index 61f4bd47c5..ea7f385a49 100644 --- a/ui/tests/unit/adapters/sync/associations-test.js +++ b/ui/tests/unit/adapters/sync/associations-test.js @@ -15,6 +15,7 @@ module('Unit | Adapter | sync | association', function (hooks) { hooks.beforeEach(function () { this.store = this.owner.lookup('service:store'); + this.pagination = this.owner.lookup('service:pagination'); this.params = [ { type: 'aws-sm', name: 'us-west-1' }, @@ -50,7 +51,7 @@ module('Unit | Adapter | sync | association', function (hooks) { return associationsResponse(schema, req); }); - await this.store.lazyPaginatedQuery('sync/association', { + await this.pagination.lazyPaginatedQuery('sync/association', { responsePath: 'data.keys', page: 1, destinationType: 'aws-sm', diff --git a/ui/tests/unit/services/pagination-test.js b/ui/tests/unit/services/pagination-test.js new file mode 100644 index 0000000000..9763c8d3a0 --- /dev/null +++ b/ui/tests/unit/services/pagination-test.js @@ -0,0 +1,288 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { keyForCache } from 'vault/services/pagination'; +import { dasherize } from '@ember/string'; +import clamp from 'vault/utils/clamp'; +import config from 'vault/config/environment'; +import Sinon from 'sinon'; + +const { DEFAULT_PAGE_SIZE } = config.APP; + +module('Unit | Service | pagination', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.pagination = this.owner.lookup('service:pagination'); + this.store = this.owner.lookup('service:store'); + }); + + test('pagination.setLazyCacheForModel', function (assert) { + const modelName = 'someModel'; + const key = { + id: '', + backend: 'database', + responsePath: 'data.keys', + page: 1, + pageFilter: null, + size: 15, + }; + const value = { + response: { + request_id: '1eb6473c-8df0-924e-1c8d-e016a6420aee', + lease_id: '', + renewable: false, + lease_duration: 0, + data: { + keys: null, + }, + wrap_info: null, + warnings: null, + auth: null, + mount_type: 'database', + backend: 'database', + }, + dataset: ['connection', 'connection2'], + }; + this.pagination.setLazyCacheForModel(modelName, key, value); + const cacheEntry = this.pagination.lazyCaches.get(dasherize(modelName)); + const actual = Object.fromEntries(cacheEntry); // convert from Map to Object for assertion + const expected = { '{"backend":"database","id":""}': value }; + assert.propEqual(actual, expected, 'model name is dasherized and can be retrieved from lazyCache'); + }); + + test('keyForCache', function (assert) { + const query = { id: 1 }; + const queryWithSize = { id: 1, size: 1 }; + assert.deepEqual(keyForCache(query), JSON.stringify(query), 'generated the correct cache key'); + assert.deepEqual(keyForCache(queryWithSize), JSON.stringify(query), 'excludes size from query cache'); + }); + + test('clamp', function (assert) { + assert.strictEqual(clamp('foo', 0, 100), 0, 'returns the min if passed a non-number'); + assert.strictEqual(clamp(0, 1, 100), 1, 'returns the min when passed number is less than the min'); + assert.strictEqual(clamp(200, 1, 100), 100, 'returns the max passed number is greater than the max'); + assert.strictEqual(clamp(50, 1, 100), 50, 'returns the passed number when it is in range'); + }); + + test('pagination.storeDataset', function (assert) { + const arr = ['one', 'two']; + const query = { id: 1 }; + this.pagination.storeDataset('data', query, {}, arr); + + assert.deepEqual( + this.pagination.getDataset('data', query).dataset, + arr, + 'it stores the array as .dataset' + ); + assert.deepEqual( + this.pagination.getDataset('data', query).response, + {}, + 'it stores the response as .response' + ); + assert.ok(this.pagination.get('lazyCaches').has('data'), 'it stores model map'); + assert.ok( + this.pagination.get('lazyCaches').get('data').has(keyForCache(query)), + 'it stores data on the model map' + ); + }); + + test('pagination.clearDataset with a prefix', function (assert) { + const arr = ['one', 'two']; + const arr2 = ['one', 'two', 'three', 'four']; + this.pagination.storeDataset('data', { id: 1 }, {}, arr); + this.pagination.storeDataset('transit-key', { id: 2 }, {}, arr2); + assert.strictEqual(this.pagination.get('lazyCaches').size, 2, 'it stores both keys'); + + this.pagination.clearDataset('transit-key'); + assert.strictEqual(this.pagination.get('lazyCaches').size, 1, 'deletes one key'); + assert.notOk(this.pagination.get('lazyCaches').has('transit-key'), 'cache is no longer stored'); + }); + + test('pagination.clearDataset with no args clears entire cache', function (assert) { + const arr = ['one', 'two']; + const arr2 = ['one', 'two', 'three', 'four']; + this.pagination.storeDataset('data', { id: 1 }, {}, arr); + this.pagination.storeDataset('transit-key', { id: 2 }, {}, arr2); + assert.strictEqual(this.pagination.get('lazyCaches').size, 2, 'it stores both keys'); + + this.pagination.clearDataset(); + assert.strictEqual(this.pagination.get('lazyCaches').size, 0, 'deletes all of the keys'); + assert.notOk(this.pagination.get('lazyCaches').has('transit-key'), 'first cache key is no longer stored'); + assert.notOk(this.pagination.get('lazyCaches').has('data'), 'second cache key is no longer stored'); + }); + + test('pagination.getDataset', function (assert) { + const arr = ['one', 'two']; + this.pagination.storeDataset('data', { id: 1 }, {}, arr); + + assert.deepEqual(this.pagination.getDataset('data', { id: 1 }), { response: {}, dataset: arr }); + }); + + test('pagination.constructResponse', function (assert) { + const arr = ['one', 'two', 'three', 'fifteen', 'twelve']; + this.pagination.storeDataset('data', { id: 1 }, {}, arr); + + assert.deepEqual( + this.pagination.constructResponse('data', { + id: 1, + pageFilter: 't', + page: 1, + size: 3, + responsePath: 'data', + }), + { + data: ['two', 'three', 'fifteen'], + meta: { + currentPage: 1, + lastPage: 2, + nextPage: 2, + prevPage: 1, + total: 5, + filteredTotal: 4, + pageSize: 3, + }, + }, + 'it returns filtered results' + ); + }); + + test('pagination.fetchPage', async function (assert) { + const keys = ['zero', 'one', 'two', 'three', 'four', 'five', 'six']; + const data = { + data: { + keys, + }, + }; + const pageSize = 2; + const query = { + size: pageSize, + page: 1, + responsePath: 'data.keys', + }; + this.pagination.storeDataset('transit-key', query, data, keys); + + let result; + result = await this.pagination.fetchPage('transit-key', query); + assert.strictEqual(result.get('length'), pageSize, 'returns the correct number of items'); + assert.deepEqual( + result.map((r) => r.id), + keys.slice(0, pageSize), + 'returns the first page of items' + ); + assert.deepEqual( + result.get('meta'), + { + nextPage: 2, + prevPage: 1, + currentPage: 1, + lastPage: 4, + total: 7, + filteredTotal: 7, + pageSize: 2, + }, + 'returns correct meta values' + ); + + result = await this.pagination.fetchPage('transit-key', { + size: pageSize, + page: 3, + responsePath: 'data.keys', + }); + const pageThreeEnd = 3 * pageSize; + const pageThreeStart = pageThreeEnd - pageSize; + assert.deepEqual( + result.map((r) => r.id), + keys.slice(pageThreeStart, pageThreeEnd), + 'returns the third page of items' + ); + + result = await this.pagination.fetchPage('transit-key', { + size: pageSize, + page: 99, + responsePath: 'data.keys', + }); + + assert.deepEqual( + result.map((r) => r.id), + keys.slice(keys.length - 1), + 'returns the last page when the page value is beyond the of bounds' + ); + + result = await this.pagination.fetchPage('transit-key', { + size: pageSize, + page: 0, + responsePath: 'data.keys', + }); + assert.deepEqual( + result.map((r) => r.id), + keys.slice(0, pageSize), + 'returns the first page when page value is under the bounds' + ); + }); + + test('pagination.lazyPaginatedQuery', async function (assert) { + const response = { + data: ['foo'], + }; + let queryArgs; + const adapterForStub = () => { + return { + query(store, modelName, query) { + queryArgs = query; + return Promise.resolve(response); + }, + }; + }; + Sinon.stub(this.store, 'adapterFor').callsFake(adapterForStub); + // stub fetchPage because we test it separately + Sinon.stub(this.pagination, 'fetchPage').callsFake(() => {}); + const query = { page: 1, size: 1, responsePath: 'data' }; + + await this.pagination.lazyPaginatedQuery('transit-key', query); + assert.deepEqual( + this.pagination.getDataset('transit-key', query), + { response: { data: null }, dataset: ['foo'] }, + 'stores returned dataset' + ); + + await this.pagination.lazyPaginatedQuery('secret', { page: 1, responsePath: 'data' }); + assert.strictEqual(queryArgs.size, DEFAULT_PAGE_SIZE, 'calls query with DEFAULT_PAGE_SIZE'); + + assert.throws( + () => { + this.pagination.lazyPaginatedQuery('transit-key', {}); + }, + /responsePath is required/, + 'requires responsePath' + ); + assert.throws( + () => { + this.pagination.lazyPaginatedQuery('transit-key', { responsePath: 'foo' }); + }, + /page is required/, + 'requires page' + ); + }); + + test('pagination.filterData', async function (assert) { + const dataset = [ + { id: 'foo', name: 'Foo', type: 'test' }, + { id: 'bar', name: 'Bar', type: 'test' }, + { id: 'bar-2', name: 'Bar', type: null }, + ]; + + const defaultFiltering = this.pagination.filterData('foo', dataset); + assert.deepEqual(defaultFiltering, [{ id: 'foo', name: 'Foo', type: 'test' }]); + + const filter = (data) => { + return data.filter((d) => d.name === 'Bar' && d.type === 'test'); + }; + const customFiltering = this.pagination.filterData(filter, dataset); + assert.deepEqual(customFiltering, [{ id: 'bar', name: 'Bar', type: 'test' }]); + }); +}); diff --git a/ui/tests/unit/services/store-test.js b/ui/tests/unit/services/store-test.js deleted file mode 100644 index ba349ffe3b..0000000000 --- a/ui/tests/unit/services/store-test.js +++ /dev/null @@ -1,257 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { resolve } from 'rsvp'; -import { run } from '@ember/runloop'; -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; -import { normalizeModelName, keyForCache } from 'vault/services/store'; -import clamp from 'vault/utils/clamp'; -import config from 'vault/config/environment'; - -const { DEFAULT_PAGE_SIZE } = config.APP; - -module('Unit | Service | store', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - }); - - test('normalizeModelName', function (assert) { - assert.strictEqual(normalizeModelName('oneThing'), 'one-thing', 'dasherizes modelName'); - }); - - test('keyForCache', function (assert) { - const query = { id: 1 }; - const queryWithSize = { id: 1, size: 1 }; - assert.deepEqual(keyForCache(query), JSON.stringify(query), 'generated the correct cache key'); - assert.deepEqual(keyForCache(queryWithSize), JSON.stringify(query), 'excludes size from query cache'); - }); - - test('clamp', function (assert) { - assert.strictEqual(clamp('foo', 0, 100), 0, 'returns the min if passed a non-number'); - assert.strictEqual(clamp(0, 1, 100), 1, 'returns the min when passed number is less than the min'); - assert.strictEqual(clamp(200, 1, 100), 100, 'returns the max passed number is greater than the max'); - assert.strictEqual(clamp(50, 1, 100), 50, 'returns the passed number when it is in range'); - }); - - test('store.storeDataset', function (assert) { - const arr = ['one', 'two']; - const query = { id: 1 }; - this.store.storeDataset('data', query, {}, arr); - - assert.deepEqual(this.store.getDataset('data', query).dataset, arr, 'it stores the array as .dataset'); - assert.deepEqual( - this.store.getDataset('data', query).response, - {}, - 'it stores the response as .response' - ); - assert.ok(this.store.get('lazyCaches').has('data'), 'it stores model map'); - assert.ok( - this.store.get('lazyCaches').get('data').has(keyForCache(query)), - 'it stores data on the model map' - ); - }); - - test('store.clearDataset with a prefix', function (assert) { - const arr = ['one', 'two']; - const arr2 = ['one', 'two', 'three', 'four']; - this.store.storeDataset('data', { id: 1 }, {}, arr); - this.store.storeDataset('transit-key', { id: 2 }, {}, arr2); - assert.strictEqual(this.store.get('lazyCaches').size, 2, 'it stores both keys'); - - this.store.clearDataset('transit-key'); - assert.strictEqual(this.store.get('lazyCaches').size, 1, 'deletes one key'); - assert.notOk(this.store.get('lazyCaches').has('transit-key'), 'cache is no longer stored'); - }); - - test('store.clearDataset with no args clears entire cache', function (assert) { - const arr = ['one', 'two']; - const arr2 = ['one', 'two', 'three', 'four']; - this.store.storeDataset('data', { id: 1 }, {}, arr); - this.store.storeDataset('transit-key', { id: 2 }, {}, arr2); - assert.strictEqual(this.store.get('lazyCaches').size, 2, 'it stores both keys'); - - this.store.clearDataset(); - assert.strictEqual(this.store.get('lazyCaches').size, 0, 'deletes all of the keys'); - assert.notOk(this.store.get('lazyCaches').has('transit-key'), 'first cache key is no longer stored'); - assert.notOk(this.store.get('lazyCaches').has('data'), 'second cache key is no longer stored'); - }); - - test('store.getDataset', function (assert) { - const arr = ['one', 'two']; - this.store.storeDataset('data', { id: 1 }, {}, arr); - - assert.deepEqual(this.store.getDataset('data', { id: 1 }), { response: {}, dataset: arr }); - }); - - test('store.constructResponse', function (assert) { - const arr = ['one', 'two', 'three', 'fifteen', 'twelve']; - this.store.storeDataset('data', { id: 1 }, {}, arr); - - assert.deepEqual( - this.store.constructResponse('data', { - id: 1, - pageFilter: 't', - page: 1, - size: 3, - responsePath: 'data', - }), - { - data: ['two', 'three', 'fifteen'], - meta: { - currentPage: 1, - lastPage: 2, - nextPage: 2, - prevPage: 1, - total: 5, - filteredTotal: 4, - pageSize: 3, - }, - }, - 'it returns filtered results' - ); - }); - - test('store.fetchPage', async function (assert) { - const keys = ['zero', 'one', 'two', 'three', 'four', 'five', 'six']; - const data = { - data: { - keys, - }, - }; - const pageSize = 2; - const query = { - size: pageSize, - page: 1, - responsePath: 'data.keys', - }; - this.store.storeDataset('transit-key', query, data, keys); - - let result; - result = await this.store.fetchPage('transit-key', query); - assert.strictEqual(result.get('length'), pageSize, 'returns the correct number of items'); - assert.deepEqual( - result.map((r) => r.id), - keys.slice(0, pageSize), - 'returns the first page of items' - ); - assert.deepEqual( - result.get('meta'), - { - nextPage: 2, - prevPage: 1, - currentPage: 1, - lastPage: 4, - total: 7, - filteredTotal: 7, - pageSize: 2, - }, - 'returns correct meta values' - ); - - result = await this.store.fetchPage('transit-key', { - size: pageSize, - page: 3, - responsePath: 'data.keys', - }); - const pageThreeEnd = 3 * pageSize; - const pageThreeStart = pageThreeEnd - pageSize; - assert.deepEqual( - result.map((r) => r.id), - keys.slice(pageThreeStart, pageThreeEnd), - 'returns the third page of items' - ); - - result = await this.store.fetchPage('transit-key', { - size: pageSize, - page: 99, - responsePath: 'data.keys', - }); - - assert.deepEqual( - result.map((r) => r.id), - keys.slice(keys.length - 1), - 'returns the last page when the page value is beyond the of bounds' - ); - - result = await this.store.fetchPage('transit-key', { - size: pageSize, - page: 0, - responsePath: 'data.keys', - }); - assert.deepEqual( - result.map((r) => r.id), - keys.slice(0, pageSize), - 'returns the first page when page value is under the bounds' - ); - }); - - test('store.lazyPaginatedQuery', function (assert) { - const response = { - data: ['foo'], - }; - let queryArgs; - const store = this.owner.factoryFor('service:store').create({ - adapterFor() { - return { - query(store, modelName, query) { - queryArgs = query; - return resolve(response); - }, - }; - }, - fetchPage() {}, - }); - - const query = { page: 1, size: 1, responsePath: 'data' }; - run(function () { - store.lazyPaginatedQuery('transit-key', query); - }); - assert.deepEqual( - store.getDataset('transit-key', query), - { response: { data: null }, dataset: ['foo'] }, - 'stores returned dataset' - ); - - run(function () { - store.lazyPaginatedQuery('secret', { page: 1, responsePath: 'data' }); - }); - assert.strictEqual(queryArgs.size, DEFAULT_PAGE_SIZE, 'calls query with DEFAULT_PAGE_SIZE'); - - assert.throws( - () => { - store.lazyPaginatedQuery('transit-key', {}); - }, - /responsePath is required/, - 'requires responsePath' - ); - assert.throws( - () => { - store.lazyPaginatedQuery('transit-key', { responsePath: 'foo' }); - }, - /page is required/, - 'requires page' - ); - }); - - test('store.filterData', async function (assert) { - const dataset = [ - { id: 'foo', name: 'Foo', type: 'test' }, - { id: 'bar', name: 'Bar', type: 'test' }, - { id: 'bar-2', name: 'Bar', type: null }, - ]; - - const defaultFiltering = this.store.filterData('foo', dataset); - assert.deepEqual(defaultFiltering, [{ id: 'foo', name: 'Foo', type: 'test' }]); - - const filter = (data) => { - return data.filter((d) => d.name === 'Bar' && d.type === 'test'); - }; - const customFiltering = this.store.filterData(filter, dataset); - assert.deepEqual(customFiltering, [{ id: 'bar', name: 'Bar', type: 'test' }]); - }); -}); diff --git a/ui/types/vault/services/pagination.d.ts b/ui/types/vault/services/pagination.d.ts new file mode 100644 index 0000000000..cc06d35a0c --- /dev/null +++ b/ui/types/vault/services/pagination.d.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Service from '@ember/service'; +import { RecordArray } from '@ember-data/store'; + +export default class PaginationService extends Service { + lazyPaginatedQuery( + modelName: string, + query: object, + options?: { adapterOptions: object } + ): Promise; + clearDataset(modelName: string); +} diff --git a/ui/types/vault/services/store.d.ts b/ui/types/vault/services/store.d.ts index 9abeb08ee7..970990b691 100644 --- a/ui/types/vault/services/store.d.ts +++ b/ui/types/vault/services/store.d.ts @@ -3,16 +3,11 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import Store, { RecordArray } from '@ember-data/store'; +import Store from '@ember-data/store'; export default class StoreService extends Store { - lazyPaginatedQuery( - modelName: string, - query: object, - options?: { adapterOptions: object } - ): Promise; - - clearDataset(modelName: string); + adapterFor(modelName: string); + createRecord(modelName: string, object); findRecord(modelName: string, path: string); peekRecord(modelName: string, path: string); query(modelName: string, query: object);