mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
UI: Create engine list view into separate component (#30393)
* pulling engine list view into separate component * add header * add header x2 * rename and add test * copyright * removing the unnecessary, updating tracked * tests * adding tests * fix tests * wrap in enterprise? * fix line * add import * fix tests pls * move tests around * cleanup * some fixes
This commit is contained in:
parent
8bee09280a
commit
ca778930d0
9 changed files with 403 additions and 270 deletions
116
ui/app/components/secret-engine/list.hbs
Normal file
116
ui/app/components/secret-engine/list.hbs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarFilters>
|
||||
<SearchSelect
|
||||
@id="filter-by-engine-type"
|
||||
@options={{this.secretEngineArrayByType}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.filterEngineType}}
|
||||
@placeholder="Filter by engine type"
|
||||
@displayInherit={{true}}
|
||||
@inputValue={{if this.selectedEngineType (array this.selectedEngineType)}}
|
||||
@disabled={{if this.selectedEngineName true false}}
|
||||
class="is-marginless"
|
||||
/>
|
||||
<SearchSelect
|
||||
@id="filter-by-engine-name"
|
||||
@options={{this.secretEngineArrayByName}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.filterEngineName}}
|
||||
@placeholder="Filter by engine name"
|
||||
@displayInherit={{true}}
|
||||
@inputValue={{if this.selectedEngineName (array this.selectedEngineName)}}
|
||||
class="is-marginless has-left-padding-s"
|
||||
/>
|
||||
</ToolbarFilters>
|
||||
<ToolbarActions>
|
||||
<ToolbarLink @route="vault.cluster.settings.mount-secret-backend" @type="add" data-test-enable-engine>
|
||||
Enable new engine
|
||||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
{{#each this.sortedDisplayableBackends as |backend|}}
|
||||
<LinkedBlock
|
||||
@params={{array backend.backendLink backend.id}}
|
||||
class="list-item-row linked-block-item is-no-underline"
|
||||
data-test-secrets-backend-link={{backend.id}}
|
||||
@disabled={{not backend.isSupportedBackend}}
|
||||
>
|
||||
<div>
|
||||
<div class="has-text-grey is-grid align-items-center linked-block-title">
|
||||
{{#if backend.icon}}
|
||||
<Hds::TooltipButton @text={{or backend.engineType backend.path}} aria-label="Type of backend">
|
||||
<Icon @name={{backend.icon}} class="has-text-grey-light" />
|
||||
</Hds::TooltipButton>
|
||||
{{/if}}
|
||||
{{#if backend.path}}
|
||||
{{#if backend.isSupportedBackend}}
|
||||
<LinkTo
|
||||
@route={{backend.backendLink}}
|
||||
@model={{backend.id}}
|
||||
class="has-text-black has-text-weight-semibold overflow-wrap"
|
||||
data-test-secret-path={{backend.path}}
|
||||
>
|
||||
{{backend.path}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<span data-test-secret-path={{backend.path}}>{{backend.path}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if backend.accessor}}
|
||||
<code class="has-text-grey is-size-8">
|
||||
{{if (eq backend.version 2) (concat "v2 " backend.accessor) backend.accessor}}
|
||||
</code>
|
||||
{{/if}}
|
||||
{{#if backend.description}}
|
||||
<ReadMore>
|
||||
{{backend.description}}
|
||||
</ReadMore>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="linked-block-popup-menu">
|
||||
<Hds::Dropdown @isInline={{true}} as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="{{if backend.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
<dd.Interactive
|
||||
@route={{backend.backendConfigurationLink}}
|
||||
@model={{backend.id}}
|
||||
data-test-popup-menu="view-configuration"
|
||||
>
|
||||
View configuration
|
||||
</dd.Interactive>
|
||||
{{#if (not-eq backend.type "cubbyhole")}}
|
||||
<dd.Interactive
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.engineToDisable) backend)}}
|
||||
data-test-popup-menu="disable-engine"
|
||||
>Disable</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
</div>
|
||||
</LinkedBlock>
|
||||
{{/each}}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{perform this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
112
ui/app/components/secret-engine/list.ts
Normal file
112
ui/app/components/secret-engine/list.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { dropTask } from 'ember-concurrency';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import SecretEngineModel from 'vault/models/secret-engine';
|
||||
|
||||
/**
|
||||
* @module SecretEngineList handles the display of the list of secret engines, including the filtering.
|
||||
*
|
||||
* @example
|
||||
* <SecretEngine::List
|
||||
@secretEngineModels={{this.model}}
|
||||
/>
|
||||
*
|
||||
* @param {array} secretEngineModels - An array of Secret Engine models returned from query on the parent route.
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
secretEngineModels: Array<SecretEngineModel>;
|
||||
}
|
||||
|
||||
export default class SecretListItem extends Component<Args> {
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked secretEngineOptions: Array<string> | [] = [];
|
||||
@tracked selectedEngineType = '';
|
||||
@tracked selectedEngineName = '';
|
||||
@tracked engineToDisable: SecretEngineModel | undefined = undefined;
|
||||
|
||||
get displayableBackends() {
|
||||
return this.args.secretEngineModels.filter((backend) => backend.shouldIncludeInList);
|
||||
}
|
||||
|
||||
get sortedDisplayableBackends() {
|
||||
// show supported secret engines first and then organize those by id.
|
||||
const sortedBackends = this.displayableBackends.sort(
|
||||
(a, b) => Number(b.isSupportedBackend) - Number(a.isSupportedBackend) || a.id.localeCompare(b.id)
|
||||
);
|
||||
|
||||
// return an options list to filter by engine type, ex: 'kv'
|
||||
if (this.selectedEngineType) {
|
||||
// check first if the user has also filtered by name.
|
||||
if (this.selectedEngineName) {
|
||||
return sortedBackends.filter((backend) => this.selectedEngineName === backend.id);
|
||||
}
|
||||
// otherwise filter by engine type
|
||||
return sortedBackends.filter((backend) => this.selectedEngineType === backend.engineType);
|
||||
}
|
||||
|
||||
// return an options list to filter by engine name, ex: 'secret'
|
||||
if (this.selectedEngineName) {
|
||||
return sortedBackends.filter((backend) => this.selectedEngineName === backend.id);
|
||||
}
|
||||
// no filters, return full sorted list.
|
||||
return sortedBackends;
|
||||
}
|
||||
|
||||
// Filtering & searching
|
||||
get secretEngineArrayByType() {
|
||||
const arrayOfAllEngineTypes = this.sortedDisplayableBackends.map((modelObject) => modelObject.engineType);
|
||||
// filter out repeated engineTypes (e.g. [kv, kv] => [kv])
|
||||
const arrayOfUniqueEngineTypes = [...new Set(arrayOfAllEngineTypes)];
|
||||
|
||||
return arrayOfUniqueEngineTypes.map((engineType) => ({
|
||||
name: engineType,
|
||||
id: engineType,
|
||||
}));
|
||||
}
|
||||
|
||||
get secretEngineArrayByName() {
|
||||
return this.sortedDisplayableBackends.map((modelObject) => ({
|
||||
name: modelObject.id,
|
||||
id: modelObject.id,
|
||||
}));
|
||||
}
|
||||
|
||||
@action
|
||||
filterEngineType(type: string[]) {
|
||||
const [selectedType] = type;
|
||||
this.selectedEngineType = selectedType || '';
|
||||
}
|
||||
|
||||
@action
|
||||
filterEngineName(name: string[]) {
|
||||
const [selectedName] = name;
|
||||
this.selectedEngineName = selectedName || '';
|
||||
}
|
||||
|
||||
@dropTask
|
||||
*disableEngine(engine: SecretEngineModel) {
|
||||
const { engineType, path } = engine;
|
||||
try {
|
||||
yield engine.destroyRecord();
|
||||
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
|
||||
} catch (err) {
|
||||
this.flashMessages.danger(
|
||||
`There was an error disabling the ${engineType} Secrets Engines at ${path}: ${errorMessage(err)}.`
|
||||
);
|
||||
} finally {
|
||||
this.engineToDisable = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,88 +2,6 @@
|
|||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
/* eslint ember/no-computed-properties-in-native-classes: 'warn' */
|
||||
import Controller from '@ember/controller';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class VaultClusterSecretsBackendController extends Controller {
|
||||
@service flashMessages;
|
||||
|
||||
@tracked secretEngineOptions = [];
|
||||
@tracked selectedEngineType = null;
|
||||
@tracked selectedEngineName = null;
|
||||
@tracked engineToDisable = null;
|
||||
|
||||
get displayableBackends() {
|
||||
return this.model.filter((backend) => backend.shouldIncludeInList);
|
||||
}
|
||||
|
||||
get sortedDisplayableBackends() {
|
||||
// show supported secret engines first and then organize those by id.
|
||||
const sortedBackends = this.displayableBackends.sort(
|
||||
(a, b) => b.isSupportedBackend - a.isSupportedBackend || a.id - b.id
|
||||
);
|
||||
|
||||
// return an options list to filter by engine type, ex: 'kv'
|
||||
if (this.selectedEngineType) {
|
||||
// check first if the user has also filtered by name.
|
||||
if (this.selectedEngineName) {
|
||||
return sortedBackends.filter((backend) => this.selectedEngineName === backend.id);
|
||||
}
|
||||
// otherwise filter by engine type
|
||||
return sortedBackends.filter((backend) => this.selectedEngineType === backend.engineType);
|
||||
}
|
||||
|
||||
// return an options list to filter by engine name, ex: 'secret'
|
||||
if (this.selectedEngineName) {
|
||||
return sortedBackends.filter((backend) => this.selectedEngineName === backend.id);
|
||||
}
|
||||
// no filters, return full sorted list.
|
||||
return sortedBackends;
|
||||
}
|
||||
|
||||
get secretEngineArrayByType() {
|
||||
const arrayOfAllEngineTypes = this.sortedDisplayableBackends.map((modelObject) => modelObject.engineType);
|
||||
// filter out repeated engineTypes (e.g. [kv, kv] => [kv])
|
||||
const arrayOfUniqueEngineTypes = [...new Set(arrayOfAllEngineTypes)];
|
||||
|
||||
return arrayOfUniqueEngineTypes.map((engineType) => ({
|
||||
name: engineType,
|
||||
id: engineType,
|
||||
}));
|
||||
}
|
||||
|
||||
get secretEngineArrayByName() {
|
||||
return this.sortedDisplayableBackends.map((modelObject) => ({
|
||||
name: modelObject.id,
|
||||
id: modelObject.id,
|
||||
}));
|
||||
}
|
||||
|
||||
@action
|
||||
filterEngineType([type]) {
|
||||
this.selectedEngineType = type;
|
||||
}
|
||||
|
||||
@action
|
||||
filterEngineName([name]) {
|
||||
this.selectedEngineName = name;
|
||||
}
|
||||
|
||||
@action
|
||||
async disableEngine(engine) {
|
||||
const { engineType, path } = engine;
|
||||
try {
|
||||
await engine.destroyRecord();
|
||||
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
|
||||
} catch (err) {
|
||||
this.flashMessages.danger(
|
||||
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${err.errors.join(' ')}.`
|
||||
);
|
||||
} finally {
|
||||
this.engineToDisable = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
export default class VaultClusterSecretsBackendController extends Controller {}
|
||||
|
|
|
|||
|
|
@ -11,114 +11,4 @@
|
|||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarFilters>
|
||||
<SearchSelect
|
||||
@id="filter-by-engine-type"
|
||||
@options={{this.secretEngineArrayByType}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.filterEngineType}}
|
||||
@placeholder={{"Filter by engine type"}}
|
||||
@displayInherit={{true}}
|
||||
@inputValue={{if this.selectedEngineType (array this.selectedEngineType)}}
|
||||
@disabled={{if this.selectedEngineName true false}}
|
||||
class="is-marginless"
|
||||
/>
|
||||
<SearchSelect
|
||||
@id="filter-by-engine-name"
|
||||
@options={{this.secretEngineArrayByName}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.filterEngineName}}
|
||||
@placeholder={{"Filter by engine name"}}
|
||||
@displayInherit={{true}}
|
||||
@inputValue={{if this.selectedEngineName (array this.selectedEngineName)}}
|
||||
class="is-marginless has-left-padding-s"
|
||||
/>
|
||||
</ToolbarFilters>
|
||||
<ToolbarActions>
|
||||
<ToolbarLink @route="vault.cluster.settings.mount-secret-backend" @type="add" data-test-enable-engine>
|
||||
Enable new engine
|
||||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
{{#each this.sortedDisplayableBackends as |backend|}}
|
||||
<LinkedBlock
|
||||
@params={{array backend.backendLink backend.id}}
|
||||
class="list-item-row linked-block-item is-no-underline"
|
||||
data-test-secrets-backend-link={{backend.id}}
|
||||
@disabled={{not backend.isSupportedBackend}}
|
||||
>
|
||||
<div>
|
||||
<div class="has-text-grey is-grid align-items-center linked-block-title">
|
||||
{{#if backend.icon}}
|
||||
<Hds::TooltipButton @text={{or backend.engineType backend.path}} aria-label="Type of backend">
|
||||
<Icon @name={{backend.icon}} class="has-text-grey-light" />
|
||||
</Hds::TooltipButton>
|
||||
{{/if}}
|
||||
{{#if backend.path}}
|
||||
{{#if backend.isSupportedBackend}}
|
||||
<LinkTo
|
||||
@route={{backend.backendLink}}
|
||||
@model={{backend.id}}
|
||||
class="has-text-black has-text-weight-semibold overflow-wrap"
|
||||
data-test-secret-path={{backend.path}}
|
||||
>
|
||||
{{backend.path}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<span data-test-secret-path={{backend.path}}>{{backend.path}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if backend.accessor}}
|
||||
<code class="has-text-grey is-size-8">
|
||||
{{if (eq backend.version 2) (concat "v2 " backend.accessor) backend.accessor}}
|
||||
</code>
|
||||
{{/if}}
|
||||
{{#if backend.description}}
|
||||
<ReadMore>
|
||||
{{backend.description}}
|
||||
</ReadMore>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="linked-block-popup-menu">
|
||||
<Hds::Dropdown @isInline={{true}} as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="{{if backend.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
<dd.Interactive
|
||||
@route={{backend.backendConfigurationLink}}
|
||||
@model={{backend.id}}
|
||||
data-test-popup-menu="view-configuration"
|
||||
>
|
||||
View configuration
|
||||
</dd.Interactive>
|
||||
{{#if (not-eq backend.type "cubbyhole")}}
|
||||
<dd.Interactive
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.engineToDisable) backend)}}
|
||||
data-test-popup-menu="disable-engine"
|
||||
>Disable</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
</div>
|
||||
</LinkedBlock>
|
||||
{{/each}}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{fn this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
<SecretEngine::List @secretEngineModels={{this.model}} />
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Hds::Breadcrumb>
|
||||
<Hds::Breadcrumb::Item @text="Secrets" @route="vault.cluster.secrets" />
|
||||
<Hds::Breadcrumb::Item @text="Secrets" @route="vault.cluster.secrets" data-test-breadcrumb="Secrets" />
|
||||
<Hds::Breadcrumb::Item
|
||||
@text={{@model.id}}
|
||||
@current={{not @isConfigure}}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { click, fillIn, find, findAll, currentRouteName, visit, currentURL } from '@ember/test-helpers';
|
||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import { click, fillIn, currentRouteName, visit, currentURL } from '@ember/test-helpers';
|
||||
import { selectChoose } from 'ember-power-select/test-support';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
|
|
@ -13,8 +12,9 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { UNSUPPORTED_ENGINES, mountableEngines } from 'vault/helpers/mountable-secret-engines';
|
||||
import { login, loginNs, logout } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { MOUNT_BACKEND_FORM } from '../helpers/components/mount-backend-form-selectors';
|
||||
import page from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
|
||||
module('Acceptance | secret-engine list view', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
|
@ -22,7 +22,6 @@ module('Acceptance | secret-engine list view', function (hooks) {
|
|||
const createSecret = async (path, key, value, enginePath) => {
|
||||
await click(SES.createSecretLink);
|
||||
await fillIn(SES.secretPath('create'), path);
|
||||
|
||||
await fillIn(SES.secretKey('create'), key);
|
||||
await fillIn(GENERAL.inputByAttr(key), value);
|
||||
await click(GENERAL.saveButton);
|
||||
|
|
@ -34,7 +33,60 @@ module('Acceptance | secret-engine list view', function (hooks) {
|
|||
return login();
|
||||
});
|
||||
|
||||
test('it allows you to disable an engine', async function (assert) {
|
||||
test('after enabling an unsupported engine it takes you to list page', async function (assert) {
|
||||
await visit('/vault/secrets');
|
||||
await page.enableEngine();
|
||||
await click(MOUNT_BACKEND_FORM.mountType('nomad'));
|
||||
await click(GENERAL.saveButton);
|
||||
|
||||
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backends', 'navigates to the list page');
|
||||
// cleanup
|
||||
await runCmd(deleteEngineCmd('nomad'));
|
||||
});
|
||||
|
||||
test('after enabling a supported engine it takes you to mount page, can see configure and clicking breadcrumb takes you back to list page', async function (assert) {
|
||||
await visit('/vault/secrets');
|
||||
await page.enableEngine();
|
||||
await click(MOUNT_BACKEND_FORM.mountType('aws'));
|
||||
await click(GENERAL.saveButton);
|
||||
|
||||
assert.dom(SES.configTab).exists();
|
||||
|
||||
await click(GENERAL.breadcrumbLink('Secrets'));
|
||||
assert.strictEqual(
|
||||
currentRouteName(),
|
||||
'vault.cluster.secrets.backends',
|
||||
'breadcrumb navigates to the list page'
|
||||
);
|
||||
// cleanup
|
||||
await runCmd(deleteEngineCmd('aws'));
|
||||
});
|
||||
|
||||
test('enterprise: cannot view list without permissions inside namespace', async function (assert) {
|
||||
this.version = 'enterprise';
|
||||
this.backend = `bk-${this.uid}`;
|
||||
this.namespace = `ns-${this.uid}`;
|
||||
await runCmd([`write sys/namespaces/${this.namespace} -force`]);
|
||||
await loginNs(this.namespace, ' ');
|
||||
|
||||
await visit('/vault/secrets');
|
||||
assert.dom(SES.secretsBackendLink('cubbyhole')).doesNotExist();
|
||||
|
||||
await logout();
|
||||
});
|
||||
|
||||
test('enterprise: can view list with permissions inside namespace', async function (assert) {
|
||||
this.version = 'enterprise';
|
||||
this.backend = `bk-${this.uid}`;
|
||||
this.namespace = `ns-${this.uid}`;
|
||||
await runCmd([`write sys/namespaces/${this.namespace} -force`]);
|
||||
await loginNs(this.namespace);
|
||||
await visit('/vault/secrets');
|
||||
|
||||
assert.dom(SES.secretsBackendLink('cubbyhole')).exists();
|
||||
});
|
||||
|
||||
test('after disabling it stays on the list view', async function (assert) {
|
||||
// first mount an engine so we can disable it.
|
||||
const enginePath = `alicloud-disable-${this.uid}`;
|
||||
await runCmd(mountEngineCmd('alicloud', enginePath));
|
||||
|
|
@ -51,74 +103,6 @@ module('Acceptance | secret-engine list view', function (hooks) {
|
|||
'vault.cluster.secrets.backends',
|
||||
'redirects to the backends list page'
|
||||
);
|
||||
assert.dom(SES.secretsBackendLink(enginePath)).doesNotExist('does not show the disabled engine');
|
||||
// remove the filter as it may cause issues in the next tests
|
||||
await click(GENERAL.searchSelect.removeSelected);
|
||||
});
|
||||
|
||||
test('it adds disabled css styling to unsupported secret engines', async function (assert) {
|
||||
assert.expect(16);
|
||||
const allEnginesArray = mountableEngines();
|
||||
for (const engineObject of allEnginesArray) {
|
||||
const engine = engineObject.type;
|
||||
const enginePath = `${engine}-${this.uid}`;
|
||||
await runCmd(mountEngineCmd(engine, enginePath));
|
||||
await visit('/vault/cluster/dashboard');
|
||||
await visit('/vault/secrets');
|
||||
if (UNSUPPORTED_ENGINES.includes(engine)) {
|
||||
assert
|
||||
.dom(SES.secretsBackendLink(enginePath))
|
||||
.doesNotHaveClass(
|
||||
'linked-block',
|
||||
`the linked-block class is not added to the unsupported ${engine}, which effectively disables it.`
|
||||
);
|
||||
} else {
|
||||
assert
|
||||
.dom(SES.secretsBackendLink(enginePath))
|
||||
.hasClass('linked-block', `linked-block class is added to supported ${engine} engines.`);
|
||||
}
|
||||
// cleanup
|
||||
await runCmd(deleteEngineCmd(enginePath));
|
||||
}
|
||||
});
|
||||
|
||||
test('it filters by name and engine type', async function (assert) {
|
||||
const enginePath1 = `aws-1-${this.uid}`;
|
||||
const enginePath2 = `aws-2-${this.uid}`;
|
||||
|
||||
await await runCmd(mountEngineCmd('aws', enginePath1));
|
||||
await await runCmd(mountEngineCmd('aws', enginePath2));
|
||||
await visit('/vault/secrets');
|
||||
// filter by type
|
||||
await clickTrigger('#filter-by-engine-type');
|
||||
await click(GENERAL.searchSelect.option());
|
||||
|
||||
const rows = findAll(SES.secretsBackendLink());
|
||||
const rowsAws = Array.from(rows).filter((row) => row.innerText.includes('aws'));
|
||||
|
||||
assert.strictEqual(rows.length, rowsAws.length, 'all rows returned are aws');
|
||||
// filter by name
|
||||
await clickTrigger('#filter-by-engine-name');
|
||||
const firstItemToSelect = find(GENERAL.searchSelect.option()).innerText;
|
||||
await click(GENERAL.searchSelect.option());
|
||||
const singleRow = document.querySelectorAll(SES.secretsBackendLink());
|
||||
assert.strictEqual(singleRow.length, 1, 'returns only one row');
|
||||
assert.dom(singleRow[0]).includesText(firstItemToSelect, 'shows the filtered by name engine');
|
||||
// clear filter by engine name
|
||||
await click(`#filter-by-engine-name ${GENERAL.searchSelect.removeSelected}`);
|
||||
const rowsAgain = document.querySelectorAll(SES.secretsBackendLink());
|
||||
assert.ok(rowsAgain.length > 1, 'filter has been removed');
|
||||
|
||||
// cleanup
|
||||
await runCmd(deleteEngineCmd(enginePath1));
|
||||
await runCmd(deleteEngineCmd(enginePath2));
|
||||
});
|
||||
|
||||
test('it applies overflow styling', async function (assert) {
|
||||
await visit('/vault/secrets');
|
||||
// not using the secret-engine-selector "secretPath" because I want to return the first node of a querySelectorAll
|
||||
const firstSecretEngine = document.querySelectorAll('[data-test-secret-path]')[0];
|
||||
assert.dom(firstSecretEngine).hasClass('overflow-wrap', 'secret engine name has overflow class ');
|
||||
});
|
||||
|
||||
test('it allows navigation to a non-nested secret with pagination', async function (assert) {
|
||||
111
ui/tests/integration/components/list-test.js
Normal file
111
ui/tests/integration/components/list-test.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render, click, find, findAll } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import sinon from 'sinon';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
|
||||
import { createSecretsEngine } from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
module('Integration | Component | secret-engine/list', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.server.post('/sys/capabilities-self', () => ({
|
||||
data: {
|
||||
capabilities: ['root'],
|
||||
},
|
||||
}));
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.flashMessages = this.owner.lookup('service:flash-messages');
|
||||
this.flashMessages.registerTypes(['success', 'danger']);
|
||||
this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success');
|
||||
this.flashDangerSpy = sinon.spy(this.flashMessages, 'danger');
|
||||
this.uid = uuidv4();
|
||||
// generate a model of cubbyhole, kv2, and nomad
|
||||
this.secretEngineModels = [
|
||||
createSecretsEngine(this.store, 'cubbyhole', 'cubbyhole-test'),
|
||||
createSecretsEngine(this.store, 'kv', 'kv2-test'),
|
||||
createSecretsEngine(this.store, 'aws', 'aws-1'),
|
||||
createSecretsEngine(this.store, 'aws', 'aws-2'),
|
||||
createSecretsEngine(this.store, 'nomad', 'nomad-test'),
|
||||
];
|
||||
});
|
||||
|
||||
test('it allows you to disable an engine', async function (assert) {
|
||||
const enginePath = 'kv2-test';
|
||||
this.server.delete(`sys/mounts/${enginePath}`, () => {
|
||||
assert.true(true, 'Destroy record is called and deletes the engine');
|
||||
return overrideResponse(204);
|
||||
});
|
||||
await render(hbs`<SecretEngine::List @secretEngineModels={{this.secretEngineModels}} />`);
|
||||
|
||||
assert.dom(SES.secretsBackendLink(enginePath)).exists('shows the link for the kvv2 secrets engine');
|
||||
const row = SES.secretsBackendLink(enginePath);
|
||||
await click(`${row} ${GENERAL.menuTrigger}`);
|
||||
await click(GENERAL.menuItem('disable-engine'));
|
||||
await click(GENERAL.confirmButton);
|
||||
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`The kv Secrets Engine at ${enginePath}/ has been disabled.`),
|
||||
'Flash message shows that engine was disabled.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it adds disabled css styling to unsupported secret engines', async function (assert) {
|
||||
await render(hbs`<SecretEngine::List @secretEngineModels={{this.secretEngineModels}} />`);
|
||||
assert
|
||||
.dom(SES.secretsBackendLink('nomad-test'))
|
||||
.doesNotHaveClass(
|
||||
'linked-block',
|
||||
`the linked-block class is not added to the unsupported nomad engine, which effectively disables it.`
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(SES.secretsBackendLink('aws-1'))
|
||||
.hasClass('linked-block', `linked-block class is added to supported aws engines.`);
|
||||
});
|
||||
|
||||
test('it filters by name and engine type', async function (assert) {
|
||||
await render(hbs`<SecretEngine::List @secretEngineModels={{this.secretEngineModels}} />`);
|
||||
// filter by type
|
||||
await clickTrigger('#filter-by-engine-type');
|
||||
await click(GENERAL.searchSelect.option());
|
||||
|
||||
const rows = findAll(SES.secretsBackendLink());
|
||||
const rowsAws = Array.from(rows).filter((row) => row.innerText.includes('aws'));
|
||||
|
||||
assert.strictEqual(rows.length, rowsAws.length, 'all rows returned are aws');
|
||||
// filter by name
|
||||
await clickTrigger('#filter-by-engine-name');
|
||||
const firstItemToSelect = find(GENERAL.searchSelect.option()).innerText;
|
||||
await click(GENERAL.searchSelect.option());
|
||||
const singleRow = document.querySelectorAll(SES.secretsBackendLink());
|
||||
assert.strictEqual(singleRow.length, 1, 'returns only one row');
|
||||
assert.dom(singleRow[0]).includesText(firstItemToSelect, 'shows the filtered by name engine');
|
||||
|
||||
// clear filter by engine name
|
||||
await click(`#filter-by-engine-name ${GENERAL.searchSelect.removeSelected}`);
|
||||
const rowsAgain = document.querySelectorAll(SES.secretsBackendLink());
|
||||
assert.true(rowsAgain.length > 1, 'filter has been removed');
|
||||
});
|
||||
|
||||
test('it applies overflow styling', async function (assert) {
|
||||
await render(hbs`<SecretEngine::List @secretEngineModels={{this.secretEngineModels}} />`);
|
||||
// not using the secret-engine-selector "secretPath" because I want to return the first node of a querySelectorAll
|
||||
const firstSecretEngine = document.querySelectorAll('[data-test-secret-path]')[0];
|
||||
assert.dom(firstSecretEngine).hasClass('overflow-wrap', 'secret engine name has overflow class ');
|
||||
});
|
||||
});
|
||||
|
|
@ -18,7 +18,8 @@ export default create({
|
|||
url: fillable('[data-test-input="url"]'),
|
||||
username: fillable('[data-test-input="username"]'),
|
||||
password: fillable('[data-test-input="password"]'),
|
||||
addRole: clickable('[data-test-add-role]'), // only from connection show
|
||||
save: clickable('[data-test-secret-save]'),
|
||||
addRole: clickable('[data-test-add-role]'),
|
||||
enable: clickable('[data-test-enable-connection]'),
|
||||
edit: clickable('[data-test-edit-link]'),
|
||||
delete: clickable('[data-test-database-connection-delete]'),
|
||||
|
|
|
|||
1
ui/types/vault/models/secret-engine.d.ts
vendored
1
ui/types/vault/models/secret-engine.d.ts
vendored
|
|
@ -40,6 +40,7 @@ export default class SecretEngineModel extends Model {
|
|||
get localDisplay(): string;
|
||||
get formFields(): Array<FormField>;
|
||||
get formFieldGroups(): FormFieldGroups;
|
||||
destroyRecord(): void;
|
||||
saveZeroAddressConfig(): Promise;
|
||||
validate(): ModelValidations;
|
||||
// need to override isNew which is a computed prop and ts will complain since it sees it as a function
|
||||
|
|
|
|||
Loading…
Reference in a new issue