diff --git a/ui/app/app.js b/ui/app/app.js
index 863b96d6b6..6ae15248eb 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -97,6 +97,7 @@ export default class App extends Application {
kv: {
dependencies: {
services: [
+ 'api',
'capabilities',
'control-group',
'download',
diff --git a/ui/app/utils/constants/capabilities.ts b/ui/app/utils/constants/capabilities.ts
index c4cfe46069..aadea31619 100644
--- a/ui/app/utils/constants/capabilities.ts
+++ b/ui/app/utils/constants/capabilities.ts
@@ -23,6 +23,7 @@ export const PATH_MAP = {
syncSetAssociation: apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/set`,
syncRemoveAssociation: apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/remove`,
kvConfig: apiPath`${'path'}/config`,
+ kvMetadata: apiPath`${'backend'}/metadata/${'path'}`,
authMethodConfig: apiPath`auth/${'path'}/config`,
authMethodConfigAws: apiPath`auth/${'path'}/config/client`,
authMethodDelete: apiPath`sys/auth/${'path'}`,
diff --git a/ui/lib/kv/addon/components/kv-list-filter.js b/ui/lib/kv/addon/components/kv-list-filter.js
index 49bad80571..59106dbe37 100644
--- a/ui/lib/kv/addon/components/kv-list-filter.js
+++ b/ui/lib/kv/addon/components/kv-list-filter.js
@@ -21,11 +21,9 @@ import { task, timeout } from 'ember-concurrency';
* route will reload the model and completely refresh the page.
* *
*
- * @param {array} secrets - An array of secret models.
* @param {string} mountPoint - Where in the router files we're located. For this component it will always be vault.cluster.secrets.backend.kv
* @param {string} filterValue - Full initial search value. A concatenation between the list-directory's dynamic path "path-to-secret" and the queryParam "pageFilter". For example, if we're inside the beep/ directory searching for any secret that starts with "my-" this value will equal "beep/my-".
*/
diff --git a/ui/lib/kv/addon/components/page/configuration.hbs b/ui/lib/kv/addon/components/page/configuration.hbs
index 782f157ddb..71f0f5de6f 100644
--- a/ui/lib/kv/addon/components/page/configuration.hbs
+++ b/ui/lib/kv/addon/components/page/configuration.hbs
@@ -3,35 +3,20 @@
SPDX-License-Identifier: BUSL-1.1
}}
-
+
<:tabLinks>
Secrets
Configuration
-{{! engine configuration }}
-{{#if @engineConfig.canRead}}
-
- {{#each @engineConfig.formFields as |attr|}}
-
- {{/each}}
-
-{{/if}}
-
-{{! mount configuration }}
- {{#each @mountConfig.attrs as |attr|}}
- {{#if (not (includes attr.name @engineConfig.displayFields))}}
-
- {{/if}}
- {{/each}}
+ {{#each-in @config as |key value|}}
+
+ {{/each-in}}
\ No newline at end of file
diff --git a/ui/lib/kv/addon/components/page/configuration.js b/ui/lib/kv/addon/components/page/configuration.js
new file mode 100644
index 0000000000..0565d8524f
--- /dev/null
+++ b/ui/lib/kv/addon/components/page/configuration.js
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { toLabel } from 'core/helpers/to-label';
+import { duration } from 'core/helpers/format-duration';
+
+/**
+ * @module KvConfigPageComponent
+ * KvConfigPageComponent is a component to show secrets mount and engine configuration data
+ *
+ * @param {object} config - config data for mount and engine
+ * @param {string} backend - The name of the kv secret engine.
+ * @param {array} breadcrumbs - Breadcrumbs as an array of objects that contain label, route, and modelId. They are updated via the util kv-breadcrumbs to handle dynamic *pathToSecret on the list-directory route.
+ */
+
+export default class KvConfigPageComponent extends Component {
+ label = (key) => {
+ const label = toLabel([key]);
+ // map specific fields to custom labels
+ return (
+ {
+ cas_required: 'Require check and set',
+ delete_version_after: 'Automate secret deletion',
+ max_versions: 'Maximum number of versions',
+ default_lease_ttl: 'Default Lease TTL',
+ max_lease_ttl: 'Max Lease TTL',
+ }[key] || label
+ );
+ };
+
+ value = (key, value) => {
+ if (key === 'delete_version_after') {
+ return value === '0s' ? 'Never delete' : duration([value]);
+ }
+ return value;
+ };
+}
diff --git a/ui/lib/kv/addon/components/page/list.hbs b/ui/lib/kv/addon/components/page/list.hbs
index c60c47bbe0..457c7a51f3 100644
--- a/ui/lib/kv/addon/components/page/list.hbs
+++ b/ui/lib/kv/addon/components/page/list.hbs
@@ -19,9 +19,8 @@
<:toolbarFilters>
-
{{#if (and (not-eq @secrets 403) (or @secrets @filterValue))}}
-
+
{{/if}}
@@ -37,6 +36,7 @@
+
{{#if (eq @secrets 403)}}
@@ -73,59 +73,80 @@
{{else}}
{{#if @secrets}}
- {{#each @secrets as |metadata|}}
-
-
-
-
-
-
- {{metadata.path}}
-
+ {{#each @secrets as |secretPath|}}
+ {{#let (this.isDirectory secretPath) (this.fullSecretPath secretPath) as |isDir fullPath|}}
+
+
+
+
+
+
+ {{secretPath}}
+
+
-
-
-
-
-
- {{#if metadata.pathIsDirectory}}
- Content
- {{else}}
- Overview
- Secret data
- {{#if metadata.canReadMetadata}}
-
- View version history
- {{/if}}
- {{#if metadata.canDeleteMetadata}}
+
+
+
+
+ {{#if isDir}}
Permanently delete
+ @route="list-directory"
+ @models={{array @backend fullPath}}
+ data-test-list-menu-item="Content"
+ >
+ Content
+
+ {{else}}
+
+ Overview
+
+
+ Secret data
+
+ {{#if @capabilities.canRead}}
+
+ View version history
+
+ {{/if}}
+ {{#if @capabilities.canDelete}}
+
+ Permanently delete
+
+ {{/if}}
{{/if}}
- {{/if}}
-
+
+
-
-
+
+ {{/let}}
{{/each}}
{{#if this.metadataToDelete}}
pathIsDirectory(path);
+ fullSecretPath = (secret) => `${this.args.pathToSecret}${secret}`;
+
get mountPoint() {
// mountPoint tells transition where to start. In this case, mountPoint will always be vault.cluster.secrets.backend.kv.
return getOwner(this).mountPoint;
@@ -53,16 +59,15 @@ export default class KvListPageComponent extends Component {
}
@action
- async onDelete(model) {
+ async onDelete(secretPath) {
try {
- // The model passed in is a kv/metadata model
- await model.destroyRecord();
- 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}.`;
+ const fullSecretPath = this.fullSecretPath(secretPath);
+ await this.api.secrets.kvV2DeleteMetadataAndAllVersions(fullSecretPath, this.args.backend);
+ const message = `Successfully deleted the metadata and all version data of the secret ${fullSecretPath}.`;
this.flashMessages.success(message);
// if you've deleted a secret from within a directory, transition to its parent directory.
if (this.router.currentRoute.localName === 'list-directory') {
- const ancestors = ancestorKeysForKey(model.fullSecretPath);
+ const ancestors = ancestorKeysForKey(fullSecretPath);
const nearest = ancestors.pop();
this.router.transitionTo(`${this.mountPoint}.list-directory`, nearest);
} else {
@@ -70,7 +75,10 @@ export default class KvListPageComponent extends Component {
this.router.transitionTo(`${this.mountPoint}.list`);
}
} catch (error) {
- const message = errorMessage(error, 'Error deleting secret. Please try again or contact support.');
+ const { message } = await this.api.parseError(
+ error,
+ 'Error deleting secret. Please try again or contact support.'
+ );
this.flashMessages.danger(message);
} finally {
this.metadataToDelete = null;
diff --git a/ui/lib/kv/addon/engine.js b/ui/lib/kv/addon/engine.js
index 3e2457d0d6..c5e27da7bd 100644
--- a/ui/lib/kv/addon/engine.js
+++ b/ui/lib/kv/addon/engine.js
@@ -17,6 +17,7 @@ export default class KvEngine extends Engine {
Resolver = Resolver;
dependencies = {
services: [
+ 'api',
'capabilities',
'control-group',
'download',
diff --git a/ui/lib/kv/addon/routes/configuration.js b/ui/lib/kv/addon/routes/configuration.js
index 63e897ab61..76a7dd882c 100644
--- a/ui/lib/kv/addon/routes/configuration.js
+++ b/ui/lib/kv/addon/routes/configuration.js
@@ -5,27 +5,46 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
-import { hash } from 'rsvp';
export default class KvConfigurationRoute extends Route {
- @service store;
+ @service api;
- model() {
+ async model() {
const backend = this.modelFor('application');
- return hash({
- mountConfig: this.store.query('secret-engine', { path: backend.id }).then((models = []) => models[0]),
- engineConfig: this.store.findRecord('kv/config', backend.id).catch(() => {
- // return an empty record so we have access to model capabilities
- return this.store.createRecord('kv/config', { backend: backend.id });
- }),
- });
+ const {
+ type,
+ path,
+ accessor,
+ running_plugin_version,
+ local,
+ seal_wrap,
+ config: { default_lease_ttl, max_lease_ttl },
+ options: { version },
+ } = await this.api.sys.internalUiReadMountInformation(backend.id);
+ // display mount config if engine config request fails
+ const engineConfig = await this.api.secrets.kvV2ReadConfiguration(backend.id).catch(() => {});
+
+ return {
+ ...engineConfig,
+ type,
+ path,
+ accessor,
+ running_plugin_version,
+ local,
+ seal_wrap,
+ default_lease_ttl,
+ max_lease_ttl,
+ version,
+ };
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
+ const { id } = this.modelFor('application');
+ controller.backend = id;
controller.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
- { label: resolvedModel.mountConfig.id, route: 'list', model: resolvedModel.engineConfig.backend },
+ { label: id, route: 'list', model: id },
{ label: 'Configuration' },
];
}
diff --git a/ui/lib/kv/addon/routes/list-directory.js b/ui/lib/kv/addon/routes/list-directory.js
index 0b6e787f6f..b35ab3e2fa 100644
--- a/ui/lib/kv/addon/routes/list-directory.js
+++ b/ui/lib/kv/addon/routes/list-directory.js
@@ -5,13 +5,15 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
-import { hash } from 'rsvp';
import { pathIsDirectory, breadcrumbsForSecret } from 'kv/utils/kv-breadcrumbs';
+import { paginate } from 'core/utils/paginate-list';
export default class KvSecretsListRoute extends Route {
@service pagination;
@service('app-router') router;
@service secretMountPath;
+ @service api;
+ @service capabilities;
queryParams = {
pageFilter: {
@@ -23,24 +25,19 @@ export default class KvSecretsListRoute extends Route {
};
async fetchMetadata(backend, pathToSecret, params) {
- return await this.pagination
- .lazyPaginatedQuery('kv/metadata', {
- backend,
- responsePath: 'data.keys',
- page: Number(params.page) || 1,
- pageFilter: params.pageFilter,
- pathToSecret,
- })
- .catch((err) => {
- if (err.httpStatus === 403) {
- return 403;
- }
- if (err.httpStatus === 404) {
- return [];
- } else {
- throw err;
- }
- });
+ try {
+ const { keys } = await this.api.secrets.kvV2List(pathToSecret, backend, true);
+ return paginate(keys, { page: Number(params.page) || 1, filter: params.pageFilter });
+ } catch (error) {
+ const { status, response } = await this.api.parseError(error);
+ if (status === 403 && !response.isControlGroupError) {
+ return 403;
+ }
+ if (status === 404) {
+ return [];
+ }
+ throw error;
+ }
}
getPathToSecret(pathParam) {
@@ -51,18 +48,22 @@ export default class KvSecretsListRoute extends Route {
return pathIsDirectory(pathParam) ? pathParam : `${pathParam}/`;
}
- model(params) {
+ async model(params) {
const { pageFilter, path_to_secret } = params;
const pathToSecret = this.getPathToSecret(path_to_secret);
const backend = this.secretMountPath.currentPath;
const filterValue = pathToSecret ? (pageFilter ? pathToSecret + pageFilter : pathToSecret) : pageFilter;
- return hash({
- secrets: this.fetchMetadata(backend, pathToSecret, params),
+ const secrets = await this.fetchMetadata(backend, pathToSecret, params);
+ const capabilities = await this.capabilities.for('kvMetadata', { backend, path: path_to_secret });
+
+ return {
+ secrets,
backend,
pathToSecret,
filterValue,
pageFilter,
- });
+ capabilities,
+ };
}
setupController(controller, resolvedModel) {
diff --git a/ui/lib/kv/addon/templates/configuration.hbs b/ui/lib/kv/addon/templates/configuration.hbs
index f2daac229f..05b67cd2ed 100644
--- a/ui/lib/kv/addon/templates/configuration.hbs
+++ b/ui/lib/kv/addon/templates/configuration.hbs
@@ -3,8 +3,4 @@
SPDX-License-Identifier: BUSL-1.1
}}
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/ui/lib/kv/addon/templates/list-directory.hbs b/ui/lib/kv/addon/templates/list-directory.hbs
index c4ed2bad7c..289dcee75a 100644
--- a/ui/lib/kv/addon/templates/list-directory.hbs
+++ b/ui/lib/kv/addon/templates/list-directory.hbs
@@ -7,9 +7,9 @@
@secrets={{this.model.secrets}}
@backend={{this.model.backend}}
@pathToSecret={{this.model.pathToSecret}}
- @pageFilter={{this.model.pageFilter}}
@filterValue={{this.model.filterValue}}
@failedDirectoryQuery={{this.model.failedDirectoryQuery}}
@breadcrumbs={{this.breadcrumbs}}
@currentRouteParams={{array this.model.backend this.model.pathToSecret}}
+ @capabilities={{this.model.capabilities}}
/>
\ No newline at end of file
diff --git a/ui/lib/kv/addon/templates/list.hbs b/ui/lib/kv/addon/templates/list.hbs
index ba561ccc33..d8836980f9 100644
--- a/ui/lib/kv/addon/templates/list.hbs
+++ b/ui/lib/kv/addon/templates/list.hbs
@@ -11,4 +11,5 @@
@failedDirectoryQuery={{this.model.failedDirectoryQuery}}
@breadcrumbs={{this.breadcrumbs}}
@currentRouteParams={{array this.model.backend}}
+ @capabilities={{this.model.capabilities}}
/>
\ No newline at end of file
diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js
index 03fb793e71..2e665347b8 100644
--- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js
+++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js
@@ -420,7 +420,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook
// correct popup menu items appear on list view
const popupSelector = `${PAGE.list.item('bad-secret')} ${PAGE.popup}`;
await click(popupSelector);
- assert.dom(PAGE.list.listMenuDelete).exists('shows the option to permanently delete');
+ assert.dom(PAGE.list.menuItem('Permanently delete')).exists('shows the option to permanently delete');
});
test('can not delete all secret versions from root list view (snc)', async function (assert) {
assert.expect(1);
diff --git a/ui/tests/helpers/kv/kv-selectors.js b/ui/tests/helpers/kv/kv-selectors.js
index c4299aa6d1..f11b3664b1 100644
--- a/ui/tests/helpers/kv/kv-selectors.js
+++ b/ui/tests/helpers/kv/kv-selectors.js
@@ -58,8 +58,8 @@ export const PAGE = {
list: {
createSecret: '[data-test-toolbar-create-secret]',
item: (secret) => (!secret ? '[data-test-list-item]' : `[data-test-list-item="${secret}"]`),
+ menuItem: (label) => `[data-test-list-menu-item="${label}"]`,
filter: `[data-test-kv-list-filter]`,
- listMenuDelete: `[data-test-popup-metadata-delete]`,
overviewCard: '[data-test-overview-card-container="View secret"]',
overviewInput: '[data-test-view-secret] input',
},
diff --git a/ui/tests/integration/components/kv/page/kv-page-configuration-test.js b/ui/tests/integration/components/kv/page/kv-page-configuration-test.js
index 392be73b69..dca2789981 100644
--- a/ui/tests/integration/components/kv/page/kv-page-configuration-test.js
+++ b/ui/tests/integration/components/kv/page/kv-page-configuration-test.js
@@ -15,94 +15,57 @@ module('Integration | Component | kv-v2 | Page::Configuration', function (hooks)
setupEngine(hooks, 'kv');
hooks.beforeEach(async function () {
- this.store = this.owner.lookup('service:store');
- this.mountData = {
- id: 'my-kv',
- accessor: 'kv_80616825',
- config: this.store.createRecord('mount-config', {
- defaultLeaseTtl: '72h',
- forceNoCache: false,
- maxLeaseTtl: '123h',
- }),
- options: {
- version: '2',
- },
- description: '',
- path: 'my-kv',
- sealWrap: false,
+ this.config = {
+ cas_required: true,
+ max_versions: 0,
+ delete_version_after: '0s',
type: 'kv',
- uuid: 'f1739f9d-dfc0-83c8-011f-ec17103a06a1',
- // TODO: remove when attrs aren't duplicated across models
- // these kv specific attrs exist on the secret-engine model (for POST request when mounting the engine)
- // we want to make sure we're rendering values from kv/config while duplicates exist
- maxVersions: 'this should never render',
- casRequired: 'test is failing if this shows',
- deleteVersionAfter: `definitely shouldn't render this`,
+ path: 'my-kv',
+ accessor: 'kv_80616825',
+ running_plugin_version: '2.7.0',
+ local: false,
+ seal_wrap: false,
+ default_lease_ttl: '72h',
+ max_lease_ttl: '123h',
+ version: '2',
};
- this.store.pushPayload('kv/config', {
- modelName: 'kv/config',
- id: 'my-config',
- data: { max_versions: 0, cas_required: false, delete_version_after: '0s' },
- });
-
- // this is the route model, not an ember data model
- this.model = {
- engineConfig: this.store.peekRecord('kv/config', 'my-config'),
- mountConfig: this.store.createRecord('secret-engine', this.mountData),
- };
-
+ this.backend = 'my-kv';
this.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
- { label: this.model.mountConfig.path, route: 'list' },
+ { label: 'my-kv', route: 'list' },
{ label: 'Configuration' },
];
});
test('it renders kv configuration details', async function (assert) {
- assert.expect(11);
+ assert.expect(15);
await render(
hbs`
`,
{ owner: this.engine }
);
- assert.dom(PAGE.title).includesText(this.mountData.path, 'renders engine path as page title');
- assert.dom(PAGE.infoRowValue('Require check and set')).hasText('No');
+ assert.dom(PAGE.title).includesText('my-kv', 'renders engine path as page title');
+ assert.dom(PAGE.secretTab('Secrets')).exists('renders Secrets tab');
+ assert.dom(PAGE.secretTab('Configuration')).exists('renders Configuration tab');
+
+ assert.dom(PAGE.infoRowValue('Require check and set')).hasText('Yes');
assert.dom(PAGE.infoRowValue('Automate secret deletion')).hasText('Never delete');
assert.dom(PAGE.infoRowValue('Maximum number of versions')).hasText('0');
- assert.dom(PAGE.infoRowValue('Accessor')).hasText(this.mountData.accessor);
- assert.dom(PAGE.infoRowValue('Path')).hasText(this.mountData.path);
- assert.dom(PAGE.infoRowValue('Type')).hasText(this.mountData.type);
- assert.dom(PAGE.infoRowValue('Description')).doesNotExist();
+ assert.dom(PAGE.infoRowValue('Type')).hasText('kv');
+ assert.dom(PAGE.infoRowValue('Path')).hasText('my-kv');
+ assert.dom(PAGE.infoRowValue('Accessor')).hasText('kv_80616825');
+ assert.dom(PAGE.infoRowValue('Running plugin version')).hasText('2.7.0');
+ assert.dom(PAGE.infoRowValue('Local')).hasText('No');
assert.dom(PAGE.infoRowValue('Seal wrap')).hasText('No');
assert.dom(PAGE.infoRowValue('Default Lease TTL')).hasText('3 days');
assert.dom(PAGE.infoRowValue('Max Lease TTL')).hasText('5 days 3 hours');
- });
-
- test('it renders non default kv engine config data', async function (assert) {
- assert.expect(3);
- this.model.engineConfig.maxVersions = 10;
- this.model.engineConfig.casRequired = true;
- this.model.engineConfig.deleteVersionAfter = '10d';
-
- await render(
- hbs`
-
- `,
- { owner: this.engine }
- );
- assert.dom(PAGE.infoRowValue('Require check and set')).hasText('Yes');
- assert.dom(PAGE.infoRowValue('Automate secret deletion')).hasText('10 days');
- assert.dom(PAGE.infoRowValue('Maximum number of versions')).hasText('10');
+ assert.dom(PAGE.infoRowValue('Version')).hasText('2');
});
});
diff --git a/ui/tests/integration/components/kv/page/kv-page-list-test.js b/ui/tests/integration/components/kv/page/kv-page-list-test.js
index bd4b5864f3..665ae0242f 100644
--- a/ui/tests/integration/components/kv/page/kv-page-list-test.js
+++ b/ui/tests/integration/components/kv/page/kv-page-list-test.js
@@ -6,45 +6,57 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { setupEngine } from 'ember-engines/test-support';
-import { setupMirage } from 'ember-cli-mirage/test-support';
-import { render, click } from '@ember/test-helpers';
+import { render, click, fillIn, typeIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
-import { kvMetadataPath } from 'vault/utils/kv-path';
-import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
+import sinon from 'sinon';
-const CREATE_RECORDS = (number, store, server) => {
- const mirageList = server.createList('kv-metadatum', number, 'withCustomPath');
- mirageList.forEach((record) => {
- record.data.path = record.path;
- record.id = kvMetadataPath(record.data.backend, record.data.path);
- store.pushPayload('kv/metadata', {
- modelName: 'kv/metadata',
- ...record,
- });
- });
-};
-
-const META = {
- currentPage: 1,
- lastPage: 2,
- nextPage: 2,
- prevPage: 1,
- total: 16,
- filteredTotal: 16,
- pageSize: 15,
-};
-
-module('Integration | Component | kv | Page::List', function (hooks) {
+module('Integration | Component | kv-v2 | Page::List', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'kv');
- setupMirage(hooks);
hooks.beforeEach(async function () {
- this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
- this.store = this.owner.lookup('service:store');
+ const paginationMeta = {
+ currentPage: 1,
+ lastPage: 2,
+ nextPage: 2,
+ prevPage: 1,
+ total: 5,
+ filteredTotal: 5,
+ pageSize: 3,
+ };
+ this.secrets = ['secret-1', 'my-path/', 'secret-2'];
+ this.secrets.meta = paginationMeta;
+ this.pathToSecret = 'my-kv/';
+ this.backend = 'kv-engine';
+ this.filterValue = '';
+ this.failedDirectoryQuery = false;
+ this.breadcrumbs = [
+ { label: 'Secrets', route: 'secrets', linkExternal: true },
+ { label: this.backend, route: 'list' },
+ ];
+ this.capabilities = { canRead: true, canDelete: true };
+
+ this.renderComponent = () =>
+ render(
+ hbs`
+ `,
+ { owner: this.engine }
+ );
+
+ this.transitionTo = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
+
setRunOptions({
rules: {
// TODO: ConfirmAction renders modal within list when @isInDropdown
@@ -53,47 +65,95 @@ module('Integration | Component | kv | Page::List', function (hooks) {
});
});
- test('it renders Pagination and allows you to delete a kv/metadata record', async function (assert) {
- assert.expect(20);
- CREATE_RECORDS(15, this.store, this.server);
- this.model = await this.store.peekAll('kv/metadata');
- this.model.meta = META;
- this.backend = 'kv-engine';
- this.breadcrumbs = [
- { label: 'Secrets', route: 'secrets', linkExternal: true },
- { label: this.backend, route: 'list' },
- ];
- this.failedDirectoryQuery = false;
- await render(
- hbs``,
- {
- owner: this.engine,
- }
+ test('it should render page title and toolbar elements', async function (assert) {
+ await this.renderComponent();
+
+ assert.dom(PAGE.title).includesText(this.backend, 'renders mount path as page title');
+ assert.dom(PAGE.secretTab('Secrets')).exists('renders Secrets tab');
+ assert.dom(PAGE.secretTab('Configuration')).exists('renders Configuration tab');
+ assert.dom(PAGE.list.filter).exists('renders filter input');
+ assert.dom(PAGE.list.createSecret).exists('renders create secret action');
+ });
+
+ test('it should render 403 state', async function (assert) {
+ this.secrets = 403;
+ this.failedDirectoryQuery = true;
+ await this.renderComponent();
+
+ assert.dom(PAGE.list.filter).doesNotExist('filter input is hidden');
+ assert.dom(PAGE.list.overviewCard).exists('renders overview card');
+ assert.dom(PAGE.list.overviewInput).hasValue('my-kv/', 'shows correct path in overview card input');
+
+ await typeIn(PAGE.list.overviewInput, 'my-dir/');
+ await click(GENERAL.submitButton);
+ assert.true(
+ this.transitionTo.calledWith('vault.cluster.secrets.backend.kv.list-directory', 'my-kv/my-dir/'),
+ 'transitions to correct route if path is directory'
);
+ assert
+ .dom(GENERAL.inlineAlert)
+ .hasText(
+ 'You do not have the required permissions or the directory does not exist.',
+ 'alert renders for failed directory query'
+ );
- assert.dom(GENERAL.pagination).exists('shows hds pagination component');
- assert.dom(GENERAL.paginationInfo).hasText('1–15 of 16', 'shows correct page of pages');
- assert.dom(PAGE.title).includesText(this.backend, 'shows backend as title');
+ await fillIn(PAGE.list.overviewInput, '');
+ await typeIn(PAGE.list.overviewInput, 'secret');
+ await click(GENERAL.submitButton);
+ assert.true(
+ this.transitionTo.calledWith('vault.cluster.secrets.backend.kv.secret.index', 'secret'),
+ 'transitions to correct route if path is not a directory'
+ );
+ });
- this.model.forEach((record) => {
- assert.dom(PAGE.list.item(record.path)).exists('lists all records from 0-14 on the first page');
- });
+ test('it should render empty states', async function (assert) {
+ this.secrets = [];
+ await this.renderComponent();
+ assert.dom(GENERAL.emptyStateTitle).hasText('No secrets yet', 'empty state renders for no secrets');
- this.server.delete(kvMetadataPath('kv-engine', 'my-secret-0'), () => {
- assert.ok(true, 'request made to correct endpoint on delete metadata.');
- });
+ this.filterValue = 'foo';
+ await this.renderComponent();
+ assert
+ .dom(GENERAL.emptyStateTitle)
+ .hasText('There are no secrets matching "foo".', 'empty state renders for no filter results');
+ });
- const popupSelector = `${PAGE.list.item('my-secret-0')} ${PAGE.popup}`;
- await click(popupSelector);
- await click('[data-test-popup-metadata-delete]');
+ test('it should render paginated secrets', async function (assert) {
+ await this.renderComponent();
+
+ assert.dom(PAGE.list.item()).exists({ count: 3 }, 'renders 3 secrets for first page');
+ assert.dom(PAGE.list.item('secret-1')).hasText('secret-1', 'secret path renders');
+ assert.dom(GENERAL.pagination).exists('renders hds pagination component');
+ assert.dom(GENERAL.paginationInfo).hasText('1–3 of 5', 'renders correct page information');
+ });
+
+ test('it should render list item menu', async function (assert) {
+ await this.renderComponent();
+
+ await click(`${PAGE.list.item('my-path/')} ${PAGE.popup}`);
+ assert.dom(PAGE.list.menuItem('Content')).exists('renders content menu item for directory');
+
+ await click(`${PAGE.list.item('secret-1')} ${PAGE.popup}`);
+ assert.dom(PAGE.list.menuItem('Overview')).exists('renders overview menu item');
+ assert.dom(PAGE.list.menuItem('Secret data')).exists('renders secret data menu item');
+ assert.dom(PAGE.list.menuItem('View version history')).exists('renders version history menu item');
+ assert.dom(PAGE.list.menuItem('Permanently delete')).exists('renders delete menu item');
+
+ await click(PAGE.list.menuItem('Permanently delete'));
+ assert
+ .dom(GENERAL.confirmMessage)
+ .hasText(
+ 'This will permanently delete this secret and all its versions.',
+ 'renders confirm modal on delete click'
+ );
+
+ this.deleteStub = sinon
+ .stub(this.owner.lookup('service:api').secrets, 'kvV2DeleteMetadataAndAllVersions')
+ .resolves();
await click(GENERAL.confirmButton);
- assert.dom(PAGE.list.item('my-secret-0')).doesNotExist('deleted the first record from the list');
+ assert.true(
+ this.deleteStub.calledWith('my-kv/secret-1', this.backend),
+ 'makes request to delete secret on confirm'
+ );
});
});