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:
Dan Rivera 2025-05-19 18:15:07 -04:00 committed by GitHub
parent 8bee09280a
commit ca778930d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 403 additions and 270 deletions

View 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}}

View 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;
}
}
}

View file

@ -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 {}

View file

@ -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}} />

View file

@ -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}}

View file

@ -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) {

View 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 ');
});
});

View file

@ -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]'),

View file

@ -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