Filter Secret Engine List view by engineType and/or name (#20481)

* initial WIP glimmerize the controller

* wip got the filter engine type by supported backends working

* got filter by engine type working

* wip need to refactor but working ish for name

* wip working state with both filters, does not work if both fiters are set

* fixed when you have two selected filters, but broken for multiples of the same type with different names

* remove repeated engineTypes in filter list

* add disabled to power select

* fix bug of glimmer for the concurrency task.

* wording fix

* remove linkableItem and the nested contextual compnents to help with loading speed.

* add changelog

* fix some tests

* add test coverage

* Update 20481.txt

update changelog text

* test fixes 🤞

* test fix?

* address a pr comment and save

* address pr comment
This commit is contained in:
Angel Garbarino 2023-05-15 10:57:27 -06:00 committed by GitHub
parent 57e2657c3e
commit 00e06301f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 256 additions and 364 deletions

3
changelog/20481.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Add filtering by engine type and engine name to the Secret Engine list view.
```

View File

@ -2,36 +2,76 @@
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { filterBy } from '@ember/object/computed';
import { computed } from '@ember/object';
/* eslint ember/no-computed-properties-in-native-classes: 'warn' */
import Controller from '@ember/controller';
import { task } from 'ember-concurrency';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { inject as service } from '@ember/service';
const LINKED_BACKENDS = supportedSecretBackends();
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { filterBy } from '@ember/object/computed';
import { dropTask } from 'ember-concurrency';
export default Controller.extend({
flashMessages: service(),
displayableBackends: filterBy('model', 'shouldIncludeInList'),
export default class VaultClusterSecretsBackendController extends Controller {
@service flashMessages;
@filterBy('model', 'shouldIncludeInList') displayableBackends;
supportedBackends: computed('displayableBackends', 'displayableBackends.[]', function () {
return (this.displayableBackends || [])
.filter((backend) => LINKED_BACKENDS.includes(backend.get('engineType')))
.sortBy('id');
}),
@tracked secretEngineOptions = [];
@tracked selectedEngineType = null;
@tracked selectedEngineName = null;
unsupportedBackends: computed(
'displayableBackends',
'displayableBackends.[]',
'supportedBackends',
'supportedBackends.[]',
function () {
return (this.displayableBackends || []).slice().removeObjects(this.supportedBackends).sortBy('id');
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);
}
),
disableEngine: task(function* (engine) {
// 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;
}
@dropTask
*disableEngine(engine) {
const { engineType, path } = engine;
try {
yield engine.destroyRecord();
@ -41,5 +81,5 @@ export default Controller.extend({
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${err.errors.join(' ')}.`
);
}
}).drop(),
});
}
}

View File

@ -8,6 +8,9 @@ import { computed } from '@ember/object'; // eslint-disable-line
import { equal } from '@ember/object/computed'; // eslint-disable-line
import { withModelValidations } from 'vault/decorators/model-validations';
import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
const LINKED_BACKENDS = supportedSecretBackends();
// identity will be managed separately and the inclusion
// of the system backend is an implementation detail
@ -143,6 +146,27 @@ export default class SecretEngineModel extends Model {
return !LIST_EXCLUDED_BACKENDS.includes(this.engineType);
}
get isSupportedBackend() {
return LINKED_BACKENDS.includes(this.engineType);
}
get backendLink() {
if (this.engineType === 'kmip') {
return 'vault.cluster.secrets.backend.kmip.scopes';
}
if (this.engineType === 'database') {
return 'vault.cluster.secrets.backend.overview';
}
return 'vault.cluster.secrets.backend.list-root';
}
get accessor() {
if (this.version === 2) {
return `v2 ${this.accessor}`;
}
return this.accessor;
}
get localDisplay() {
return this.local ? 'local' : 'replicated';
}

View File

@ -7,106 +7,115 @@
</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.supportedBackends as |backend|}}
{{#let
(if
(eq backend.engineType "kmip")
"vault.cluster.secrets.backend.kmip.scopes"
(if
(eq backend.engineType "database") "vault.cluster.secrets.backend.overview" "vault.cluster.secrets.backend.list-root"
)
)
as |backendLink|
}}
<LinkableItem data-test-secret-backend-row={{backend.id}} @link={{hash route=backendLink model=backend.id}} as |Li|>
<Li.content
@accessor={{if (eq backend.version 2) (concat "v2 " backend.accessor) backend.accessor}}
@description={{backend.description}}
@glyphText={{backend.engineType}}
@glyph={{backend.icon}}
@link={{hash route=backendLink model=backend.id}}
@title={{backend.path}}
/>
<Li.menu>
<PopupMenu @name="engine-menu">
<Confirm as |c|>
<nav class="menu" aria-label="supported secrets engine menu">
<ul class="menu-list">
<li class="action">
<LinkTo @route="vault.cluster.secrets.backend.configuration" @model={{backend.id}}>
View configuration
</LinkTo>
</li>
{{#if (not-eq backend.type "cubbyhole")}}
<li class="action">
<c.Message
@id={{backend.id}}
@triggerText="Disable"
@message="Any data in this engine will be permanently deleted."
@title="Disable engine?"
@confirmButtonText="Disable"
@onConfirm={{perform this.disableEngine backend}}
data-test-engine-disable="true"
/>
</li>
{{/if}}
{{#if this.item.updatePath.isPending}}
<li class="action">
<button disabled type="button" class="link button is-loading is-transparent">
loading
</button>
</li>
{{/if}}
</ul>
</nav>
</Confirm>
</PopupMenu>
</Li.menu>
</LinkableItem>
{{/let}}
{{/each}}
{{#each this.unsupportedBackends as |backend|}}
<LinkableItem data-test-secret-backend-row={{backend.id}} @disabled={{true}} as |Li|>
<Li.content
@accessor={{if (eq backend.version 2) (concat "v2 " backend.accessor) backend.accessor}}
@description={{backend.description}}
@glyphText={{backend.engineType}}
@glyph={{or (if (eq backend.engineType "kmip") "secrets" backend.engineType) "secrets"}}
@title={{backend.path}}
/>
<Li.menu>
<PopupMenu name="engine-menu">
{{#each this.sortedDisplayableBackends as |backend|}}
<LinkedBlock
@params={{array backend.backendLink backend.id}}
class="list-item-row linkable-item is-no-underline"
data-test-auth-backend-link={{backend.id}}
@disabled={{if backend.isSupportedBackend false true}}
>
<div class="linkable-item-content" data-test-linkable-item-content>
<div class="has-text-grey">
{{#if backend.icon}}
<ToolTip @horizontalPosition="left" as |T|>
<T.Trigger>
<Icon @name={{backend.icon}} class="has-text-grey-light" data-test-linkable-item-glyph />
</T.Trigger>
<T.Content @defaultClass="tool-tip">
<div class="box">
{{or backend.engineType backend.path}}
</div>
</T.Content>
</ToolTip>
{{/if}}
{{#if backend.path}}
{{#if backend.isSupportedBackend}}
<LinkTo
@route={{backend.backendLink}}
@model={{backend.id}}
class="has-text-black has-text-weight-semibold"
data-test-secret-path
>
{{backend.path}}
</LinkTo>
{{else}}
<span data-test-secret-path>{{backend.path}}</span>
{{/if}}
{{/if}}
</div>
{{#if backend.accessor}}
<code class="has-text-grey is-size-8" data-test-linkable-item-accessor>
{{backend.accessor}}
</code>
{{/if}}
{{#if backend.description}}
<ReadMore data-test-linkable-item-description>
{{backend.description}}
</ReadMore>
{{/if}}
{{yield}}
</div>
{{! meatball sandwich menu }}
<div class="linkable-item-menu" data-test-linkable-item-menu={{backend.path}}>
<PopupMenu @name="engine-menu">
<Confirm as |c|>
<nav class="menu" aria-label="unsupported secrets engine menu">
<nav class="menu" aria-label="{{if backend.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu">
<ul class="menu-list">
<li class="action">
<LinkTo @route="vault.cluster.secrets.backend.configuration" @model={{backend.id}} data-test-engine-config>
View configuration
</LinkTo>
</li>
<li>
<c.Message
@id={{backend.id}}
@triggerText="Disable"
@message="Any data in this engine will be permanently deleted."
@title="Disable engine?"
@confirmButtonText="Disable"
@onConfirm={{perform this.disableEngine backend}}
data-test-engine-disable="true"
/>
</li>
{{#if (not-eq backend.type "cubbyhole")}}
<li class="action">
<c.Message
@id={{backend.id}}
@triggerText="Disable"
@message="Any data in this engine will be permanently deleted."
@title="Disable engine?"
@confirmButtonText="Disable"
@onConfirm={{perform this.disableEngine backend}}
data-test-engine-disable="true"
/>
</li>
{{/if}}
</ul>
</nav>
</Confirm>
</PopupMenu>
</Li.menu>
</LinkableItem>
</div>
</LinkedBlock>
{{/each}}

View File

@ -1,28 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
import layout from '../templates/components/linkable-item';
import { setComponentTemplate } from '@ember/component';
/**
* @module LinkableItem
* LinkableItem components have two contextual components, a Content component used to show information on the left with a Menu component on the right, all aligned vertically centered. If passed a link, the block will be clickable.
*
* @example
* ```js
* <LinkableItem @link={{hash route='vault.backends' model='my-backend-path'}} data-test-row="my-backend-path">
* // Use <LinkableItem.content> and <LinkableItem.menu> here
* </LinkableItem>
* ```
*
* @param {object} [link=null] - Link should have route and model
* @param {boolean} [disabled=false] - If no link then should be given a disabled attribute equal to true
*/
/* eslint ember/no-empty-glimmer-component-classes: 'warn' */
class LinkableItemComponent extends Component {}
export default setComponentTemplate(layout, LinkableItemComponent);

View File

@ -1,36 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
import layout from '../../templates/components/linkable-item/content';
import { setComponentTemplate } from '@ember/component';
/**
* @module Content
* Content components are contextual components of LinkableItem, used to display content on the left side of a LinkableItem component.
*
* @example
* ```js
* <LinkableItem as |Li|>
* <Li.content
* @accessor="cubbyhole_e21f8ee6"
* @description="per-token private secret storage"
* @glyphText="tooltip text"
* @glyph=glyph
* @title="title"
* />
* </LinkableItem>
* ```
* @param {string} accessor=null - formatted as HTML <code> tag
* @param {string} description=null - will truncate if wider than parent div
* @param {string} glyphText=null - tooltip for glyph
* @param {string} glyph=null - will display as icon beside the title
* @param {string} title=null - if @link object is passed in then title will link to @link.route
*/
/* eslint ember/no-empty-glimmer-component-classes: 'warn' */
class ContentComponent extends Component {}
export default setComponentTemplate(layout, ContentComponent);

View File

@ -1,27 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
import layout from '../../templates/components/linkable-item/menu';
import { setComponentTemplate } from '@ember/component';
/**
* @module Menu
* Menu components are contextual components of LinkableItem, used to display a menu on the right side of a LinkableItem component.
*
* @example
* ```js
* <LinkableItem as |Li|>
* <Li.menu>
* Some menu here
* </Li.menu>
* </LinkableItem>
* ```
*/
/* eslint ember/no-empty-glimmer-component-classes: 'warn' */
class MenuComponent extends Component {}
export default setComponentTemplate(layout, MenuComponent);

View File

@ -39,6 +39,7 @@
@onChange={{this.selectOrCreate}}
@placeholderComponent={{component "search-select-placeholder"}}
@verticalPosition="below"
@disabled={{@disabled}}
as |option|
>
{{#if this.shouldRenderName}}

View File

@ -53,6 +53,7 @@ import { filterOptions, defaultMatcher } from 'ember-power-select/utils/group-ut
* @param {string} [placeholder] - text you wish to replace the default "search" with
* @param {boolean} [displayInherit=false] - if you need the search select component to display inherit instead of box.
* @param {function} [renderInfoTooltip] - receives each inputValue string and list of dropdownOptions as args, so parent can determine when to render a tooltip beside a selectedOption and the tooltip text. see 'oidc/provider-form.js'
* @param {boolean} [disabled] - if true sets the disabled property on the ember-power-select component and makes it unusable.
*
// * advanced customization
* @param {Array} options - array of objects passed directly to the power-select component. If doing this, `models` should not also be passed as that will overwrite the

View File

@ -1,17 +0,0 @@
<div ...attributes>
{{#if @disabled}}
<div class="list-item-row linkable-item is-no-underline">
{{yield (hash content=(component "linkable-item/content"))}}
{{yield (hash menu=(component "linkable-item/menu"))}}
</div>
{{else}}
<LinkedBlock
@params={{array @link.route @link.model}}
class="list-item-row linkable-item is-no-underline"
data-test-auth-backend-link={{@link.model}}
>
{{yield (hash content=(component "linkable-item/content"))}}
{{yield (hash menu=(component "linkable-item/menu"))}}
</LinkedBlock>
{{/if}}
</div>

View File

@ -1,44 +0,0 @@
<div class="linkable-item-content" data-test-linkable-item-content ...attributes>
<div class="has-text-grey">
{{#if @glyph}}
<ToolTip @horizontalPosition="left" as |T|>
<T.Trigger>
<Icon @name={{@glyph}} class="has-text-grey-light" data-test-linkable-item-glyph />
</T.Trigger>
<T.Content @defaultClass="tool-tip">
<div class="box">
{{or @glyphText @title}}
</div>
</T.Content>
</ToolTip>
{{/if}}
{{#if @title}}
{{#if @link}}
<LinkTo
@route={{@link.route}}
@model={{@link.model}}
class="has-text-black has-text-weight-semibold"
data-test-secret-path
>
{{@title}}
</LinkTo>
{{else}}
<span data-test-secret-path>{{@title}}</span>
{{/if}}
{{/if}}
</div>
{{#if @accessor}}
<code class="has-text-grey is-size-8" data-test-linkable-item-accessor>
{{@accessor}}
</code>
{{/if}}
{{#if @description}}
<ReadMore data-test-linkable-item-description>
{{@description}}
</ReadMore>
{{/if}}
{{yield}}
</div>

View File

@ -1,3 +0,0 @@
<div class="linkable-item-menu" data-test-linkable-item-menu ...attributes>
{{yield}}
</div>

View File

@ -1,6 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
export { default } from 'core/components/linkable-item';

View File

@ -4,15 +4,21 @@
*/
import { currentRouteName, settled } from '@ember/test-helpers';
import { clickTrigger } from 'ember-power-select/test-support/helpers';
import { create } from 'ember-cli-page-object';
import { module, test } from 'qunit';
import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
import { setupApplicationTest } from 'ember-qunit';
import { v4 as uuidv4 } from 'uuid';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import backendsPage from 'vault/tests/pages/secrets/backends';
import authPage from 'vault/tests/pages/auth';
import ss from 'vault/tests/pages/components/search-select';
module('Acceptance | engine/disable', function (hooks) {
const searchSelect = create(ss);
module('Acceptance | secret-engine list view', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(function () {
@ -20,7 +26,7 @@ module('Acceptance | engine/disable', function (hooks) {
return authPage.login();
});
test('disable engine', async function (assert) {
test('it allows you to disable an engine', async function (assert) {
// first mount an engine so we can disable it.
const enginePath = `alicloud-disable-${this.uid}`;
await mountSecrets.enable('alicloud', enginePath);
@ -41,11 +47,68 @@ module('Acceptance | engine/disable', function (hooks) {
'vault.cluster.secrets.backends',
'redirects to the backends page'
);
assert.strictEqual(
backendsPage.rows.filterBy('path', `${enginePath}/`).length,
0,
'does not show the disabled engine'
);
});
test('it adds disabled css styling to unsupported secret engines', async function (assert) {
assert.expect(2);
// first mount engine that is not supported
const enginePath = `nomad-${this.uid}`;
await mountSecrets.enable('nomad', enginePath);
await settled();
await backendsPage.visit();
await settled();
const rows = document.querySelectorAll('[data-test-auth-backend-link]');
const rowUnsupported = Array.from(rows).filter((row) => row.innerText.includes('nomad'));
const rowSupported = Array.from(rows).filter((row) => row.innerText.includes('cubbyhole'));
assert
.dom(rowUnsupported[0])
.doesNotHaveClass(
'linked-block',
`the linked-block class is not added to unsupported engines, which effectively disables it.`
);
assert.dom(rowSupported[0]).hasClass('linked-block', `linked-block class is added to supported engines.`);
// cleanup
await runCommands([`delete sys/mounts/${enginePath}`]);
});
test('it filters by name and engine type', async function (assert) {
assert.expect(3);
const enginePath1 = `aws-1-${this.uid}`;
const enginePath2 = `aws-2-${this.uid}`;
await mountSecrets.enable('aws', enginePath1);
await mountSecrets.enable('aws', enginePath2);
await backendsPage.visit();
await settled();
// filter by type
await clickTrigger('#filter-by-engine-type');
await searchSelect.options.objectAt(0).click();
const rows = document.querySelectorAll('[data-test-auth-backend-link]');
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');
await searchSelect.options.objectAt(1).click();
const singleRow = document.querySelectorAll('[data-test-auth-backend-link]');
assert.dom(singleRow[0]).includesText('aws-2', 'shows the filtered by name engine');
// clear filter by engine name
await searchSelect.deleteButtons.objectAt(1).click();
const rowsAgain = document.querySelectorAll('[data-test-auth-backend-link]');
assert.ok(rowsAgain.length > 1, 'filter has been removed');
// cleanup
await runCommands([`delete sys/mounts/${enginePath1}`]);
await runCommands([`delete sys/mounts/${enginePath2}`]);
});
});

View File

@ -126,7 +126,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
await page.secretList();
await settled();
assert
.dom(`[data-test-secret-backend-row=${path}]`)
.dom(`[data-test-auth-backend-link=${path}]`)
.exists({ count: 1 }, 'renders only one instance of the engine');
});

View File

@ -1,88 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | linkable-item', function (hooks) {
setupRenderingTest(hooks);
test('it renders anything passed in', async function (assert) {
await render(hbs`<LinkableItem />`);
assert.dom(this.element).hasText('', 'No content rendered');
await render(hbs`
<LinkableItem as |Li|>
<Li.content>
stuff here
</Li.content>
<Li.menu>
menu
</Li.menu>
</LinkableItem>
`);
assert.dom('[data-test-linkable-item-content]').hasText('stuff here');
assert.dom('[data-test-linkable-item-menu]').hasText('menu');
});
test('it is not wrapped in a linked block if disabled is true', async function (assert) {
await render(hbs`
<LinkableItem @disabled={{true}} as |Li|>
<Li.content>
stuff here
</Li.content>
</LinkableItem>
`);
assert.dom('.list-item-row').exists('List item row exists');
assert.dom('.list-item-row.linked-block').doesNotExist('Does not render linked block');
assert.dom('[data-test-secret-path]').doesNotExist('Title is not rendered');
assert.dom('[data-test-linkable-item-accessor]').doesNotExist('Accessor is not rendered');
assert.dom('[data-test-linkable-item-accessor]').doesNotExist('Accessor is not rendered');
assert.dom('[data-test-linkable-item-glyph]').doesNotExist('Glyph is not rendered');
});
test('it is wrapped in a linked block if a link is passed', async function (assert) {
await render(hbs`
<LinkableItem @link={{hash route="vault" model="modelId"}} as |Li|>
<Li.content
@title="A title"
@link={{hash route="vault" model="modelId"}}
>
stuff here
</Li.content>
</LinkableItem>
`);
assert.dom('.list-item-row.linked-block').exists('Renders linked block');
});
test('it renders standard attributes on content', async function (assert) {
this.set('title', 'A Title');
this.set('accessor', 'my accessor');
this.set('description', 'my description');
this.set('glyph', 'key');
this.set('glyphText', 'Here is some extra info');
// Template block usage:
await render(hbs`
<LinkableItem data-test-example as |Li|>
<Li.content
@accessor={{this.accessor}}
@description={{this.description}}
@glyph={{this.glyph}}
@glyphText={{this.glyphText}}
@title={{this.title}}
/>
</LinkableItem>
`);
assert.dom('.list-item-row').exists('List item row exists');
assert.dom('[data-test-secret-path]').hasText(this.title, 'Title is rendered');
assert.dom('[data-test-linkable-item-accessor]').hasText(this.accessor, 'Accessor is rendered');
assert.dom('[data-test-linkable-item-description]').hasText(this.description, 'Description is rendered');
assert.dom('[data-test-linkable-item-glyph]').exists('Glyph is rendered');
});
});

View File

@ -9,7 +9,7 @@ import uiPanel from 'vault/tests/pages/components/console/ui-panel';
export default create({
consoleToggle: clickable('[data-test-console-toggle]'),
visit: visitable('/vault/secrets'),
rows: collection('[data-test-secret-backend-row]', {
rows: collection('[data-test-auth-backend-link]', {
path: text('[data-test-secret-path]'),
menu: clickable('[data-test-popup-menu-trigger]'),
}),