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:
parent
57e2657c3e
commit
00e06301f1
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
ui: Add filtering by engine type and engine name to the Secret Engine list view.
|
||||||
|
```
|
|
@ -2,36 +2,76 @@
|
||||||
* Copyright (c) HashiCorp, Inc.
|
* Copyright (c) HashiCorp, Inc.
|
||||||
* SPDX-License-Identifier: MPL-2.0
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
*/
|
*/
|
||||||
|
/* eslint ember/no-computed-properties-in-native-classes: 'warn' */
|
||||||
import { filterBy } from '@ember/object/computed';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
import Controller from '@ember/controller';
|
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';
|
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({
|
export default class VaultClusterSecretsBackendController extends Controller {
|
||||||
flashMessages: service(),
|
@service flashMessages;
|
||||||
displayableBackends: filterBy('model', 'shouldIncludeInList'),
|
@filterBy('model', 'shouldIncludeInList') displayableBackends;
|
||||||
|
|
||||||
supportedBackends: computed('displayableBackends', 'displayableBackends.[]', function () {
|
@tracked secretEngineOptions = [];
|
||||||
return (this.displayableBackends || [])
|
@tracked selectedEngineType = null;
|
||||||
.filter((backend) => LINKED_BACKENDS.includes(backend.get('engineType')))
|
@tracked selectedEngineName = null;
|
||||||
.sortBy('id');
|
|
||||||
}),
|
|
||||||
|
|
||||||
unsupportedBackends: computed(
|
get sortedDisplayableBackends() {
|
||||||
'displayableBackends',
|
// show supported secret engines first and then organize those by id.
|
||||||
'displayableBackends.[]',
|
const sortedBackends = this.displayableBackends.sort(
|
||||||
'supportedBackends',
|
(a, b) => b.isSupportedBackend - a.isSupportedBackend || a.id - b.id
|
||||||
'supportedBackends.[]',
|
);
|
||||||
function () {
|
|
||||||
return (this.displayableBackends || []).slice().removeObjects(this.supportedBackends).sortBy('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;
|
const { engineType, path } = engine;
|
||||||
try {
|
try {
|
||||||
yield engine.destroyRecord();
|
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(' ')}.`
|
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${err.errors.join(' ')}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}).drop(),
|
}
|
||||||
});
|
}
|
||||||
|
|
|
@ -8,6 +8,9 @@ import { computed } from '@ember/object'; // eslint-disable-line
|
||||||
import { equal } from '@ember/object/computed'; // eslint-disable-line
|
import { equal } from '@ember/object/computed'; // eslint-disable-line
|
||||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||||
import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes';
|
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
|
// identity will be managed separately and the inclusion
|
||||||
// of the system backend is an implementation detail
|
// 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);
|
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() {
|
get localDisplay() {
|
||||||
return this.local ? 'local' : 'replicated';
|
return this.local ? 'local' : 'replicated';
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,106 +7,115 @@
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<Toolbar>
|
<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>
|
<ToolbarActions>
|
||||||
<ToolbarLink @route="vault.cluster.settings.mount-secret-backend" @type="add" data-test-enable-engine>
|
<ToolbarLink @route="vault.cluster.settings.mount-secret-backend" @type="add" data-test-enable-engine>
|
||||||
Enable new engine
|
Enable new engine
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
</ToolbarActions>
|
</ToolbarActions>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
{{#each this.sortedDisplayableBackends as |backend|}}
|
||||||
{{#each this.supportedBackends as |backend|}}
|
<LinkedBlock
|
||||||
{{#let
|
@params={{array backend.backendLink backend.id}}
|
||||||
(if
|
class="list-item-row linkable-item is-no-underline"
|
||||||
(eq backend.engineType "kmip")
|
data-test-auth-backend-link={{backend.id}}
|
||||||
"vault.cluster.secrets.backend.kmip.scopes"
|
@disabled={{if backend.isSupportedBackend false true}}
|
||||||
(if
|
>
|
||||||
(eq backend.engineType "database") "vault.cluster.secrets.backend.overview" "vault.cluster.secrets.backend.list-root"
|
<div class="linkable-item-content" data-test-linkable-item-content>
|
||||||
)
|
<div class="has-text-grey">
|
||||||
)
|
{{#if backend.icon}}
|
||||||
as |backendLink|
|
<ToolTip @horizontalPosition="left" as |T|>
|
||||||
}}
|
<T.Trigger>
|
||||||
<LinkableItem data-test-secret-backend-row={{backend.id}} @link={{hash route=backendLink model=backend.id}} as |Li|>
|
<Icon @name={{backend.icon}} class="has-text-grey-light" data-test-linkable-item-glyph />
|
||||||
<Li.content
|
</T.Trigger>
|
||||||
@accessor={{if (eq backend.version 2) (concat "v2 " backend.accessor) backend.accessor}}
|
<T.Content @defaultClass="tool-tip">
|
||||||
@description={{backend.description}}
|
<div class="box">
|
||||||
@glyphText={{backend.engineType}}
|
{{or backend.engineType backend.path}}
|
||||||
@glyph={{backend.icon}}
|
</div>
|
||||||
@link={{hash route=backendLink model=backend.id}}
|
</T.Content>
|
||||||
@title={{backend.path}}
|
</ToolTip>
|
||||||
/>
|
{{/if}}
|
||||||
<Li.menu>
|
{{#if backend.path}}
|
||||||
<PopupMenu @name="engine-menu">
|
{{#if backend.isSupportedBackend}}
|
||||||
<Confirm as |c|>
|
<LinkTo
|
||||||
<nav class="menu" aria-label="supported secrets engine menu">
|
@route={{backend.backendLink}}
|
||||||
<ul class="menu-list">
|
@model={{backend.id}}
|
||||||
<li class="action">
|
class="has-text-black has-text-weight-semibold"
|
||||||
<LinkTo @route="vault.cluster.secrets.backend.configuration" @model={{backend.id}}>
|
data-test-secret-path
|
||||||
View configuration
|
>
|
||||||
</LinkTo>
|
{{backend.path}}
|
||||||
</li>
|
</LinkTo>
|
||||||
{{#if (not-eq backend.type "cubbyhole")}}
|
{{else}}
|
||||||
<li class="action">
|
<span data-test-secret-path>{{backend.path}}</span>
|
||||||
<c.Message
|
{{/if}}
|
||||||
@id={{backend.id}}
|
{{/if}}
|
||||||
@triggerText="Disable"
|
</div>
|
||||||
@message="Any data in this engine will be permanently deleted."
|
{{#if backend.accessor}}
|
||||||
@title="Disable engine?"
|
<code class="has-text-grey is-size-8" data-test-linkable-item-accessor>
|
||||||
@confirmButtonText="Disable"
|
{{backend.accessor}}
|
||||||
@onConfirm={{perform this.disableEngine backend}}
|
</code>
|
||||||
data-test-engine-disable="true"
|
{{/if}}
|
||||||
/>
|
{{#if backend.description}}
|
||||||
</li>
|
<ReadMore data-test-linkable-item-description>
|
||||||
{{/if}}
|
{{backend.description}}
|
||||||
{{#if this.item.updatePath.isPending}}
|
</ReadMore>
|
||||||
<li class="action">
|
{{/if}}
|
||||||
<button disabled type="button" class="link button is-loading is-transparent">
|
{{yield}}
|
||||||
loading
|
</div>
|
||||||
</button>
|
{{! meatball sandwich menu }}
|
||||||
</li>
|
<div class="linkable-item-menu" data-test-linkable-item-menu={{backend.path}}>
|
||||||
{{/if}}
|
<PopupMenu @name="engine-menu">
|
||||||
</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">
|
|
||||||
<Confirm as |c|>
|
<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">
|
<ul class="menu-list">
|
||||||
<li class="action">
|
<li class="action">
|
||||||
<LinkTo @route="vault.cluster.secrets.backend.configuration" @model={{backend.id}} data-test-engine-config>
|
<LinkTo @route="vault.cluster.secrets.backend.configuration" @model={{backend.id}} data-test-engine-config>
|
||||||
View configuration
|
View configuration
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
{{#if (not-eq backend.type "cubbyhole")}}
|
||||||
<c.Message
|
<li class="action">
|
||||||
@id={{backend.id}}
|
<c.Message
|
||||||
@triggerText="Disable"
|
@id={{backend.id}}
|
||||||
@message="Any data in this engine will be permanently deleted."
|
@triggerText="Disable"
|
||||||
@title="Disable engine?"
|
@message="Any data in this engine will be permanently deleted."
|
||||||
@confirmButtonText="Disable"
|
@title="Disable engine?"
|
||||||
@onConfirm={{perform this.disableEngine backend}}
|
@confirmButtonText="Disable"
|
||||||
data-test-engine-disable="true"
|
@onConfirm={{perform this.disableEngine backend}}
|
||||||
/>
|
data-test-engine-disable="true"
|
||||||
</li>
|
/>
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</Confirm>
|
</Confirm>
|
||||||
</PopupMenu>
|
</PopupMenu>
|
||||||
</Li.menu>
|
</div>
|
||||||
</LinkableItem>
|
</LinkedBlock>
|
||||||
{{/each}}
|
{{/each}}
|
|
@ -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);
|
|
|
@ -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);
|
|
|
@ -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);
|
|
|
@ -39,6 +39,7 @@
|
||||||
@onChange={{this.selectOrCreate}}
|
@onChange={{this.selectOrCreate}}
|
||||||
@placeholderComponent={{component "search-select-placeholder"}}
|
@placeholderComponent={{component "search-select-placeholder"}}
|
||||||
@verticalPosition="below"
|
@verticalPosition="below"
|
||||||
|
@disabled={{@disabled}}
|
||||||
as |option|
|
as |option|
|
||||||
>
|
>
|
||||||
{{#if this.shouldRenderName}}
|
{{#if this.shouldRenderName}}
|
||||||
|
|
|
@ -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 {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 {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 {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
|
// * 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
|
* @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
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<div class="linkable-item-menu" data-test-linkable-item-menu ...attributes>
|
|
||||||
{{yield}}
|
|
||||||
</div>
|
|
|
@ -1,6 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) HashiCorp, Inc.
|
|
||||||
* SPDX-License-Identifier: MPL-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { default } from 'core/components/linkable-item';
|
|
|
@ -4,15 +4,21 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { currentRouteName, settled } from '@ember/test-helpers';
|
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 { module, test } from 'qunit';
|
||||||
|
import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
|
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
|
||||||
import backendsPage from 'vault/tests/pages/secrets/backends';
|
import backendsPage from 'vault/tests/pages/secrets/backends';
|
||||||
import authPage from 'vault/tests/pages/auth';
|
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);
|
setupApplicationTest(hooks);
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
hooks.beforeEach(function () {
|
||||||
|
@ -20,7 +26,7 @@ module('Acceptance | engine/disable', function (hooks) {
|
||||||
return authPage.login();
|
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.
|
// first mount an engine so we can disable it.
|
||||||
const enginePath = `alicloud-disable-${this.uid}`;
|
const enginePath = `alicloud-disable-${this.uid}`;
|
||||||
await mountSecrets.enable('alicloud', enginePath);
|
await mountSecrets.enable('alicloud', enginePath);
|
||||||
|
@ -41,11 +47,68 @@ module('Acceptance | engine/disable', function (hooks) {
|
||||||
'vault.cluster.secrets.backends',
|
'vault.cluster.secrets.backends',
|
||||||
'redirects to the backends page'
|
'redirects to the backends page'
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
backendsPage.rows.filterBy('path', `${enginePath}/`).length,
|
backendsPage.rows.filterBy('path', `${enginePath}/`).length,
|
||||||
0,
|
0,
|
||||||
'does not show the disabled engine'
|
'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}`]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -126,7 +126,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
|
||||||
await page.secretList();
|
await page.secretList();
|
||||||
await settled();
|
await settled();
|
||||||
assert
|
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');
|
.exists({ count: 1 }, 'renders only one instance of the engine');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -9,7 +9,7 @@ import uiPanel from 'vault/tests/pages/components/console/ui-panel';
|
||||||
export default create({
|
export default create({
|
||||||
consoleToggle: clickable('[data-test-console-toggle]'),
|
consoleToggle: clickable('[data-test-console-toggle]'),
|
||||||
visit: visitable('/vault/secrets'),
|
visit: visitable('/vault/secrets'),
|
||||||
rows: collection('[data-test-secret-backend-row]', {
|
rows: collection('[data-test-auth-backend-link]', {
|
||||||
path: text('[data-test-secret-path]'),
|
path: text('[data-test-secret-path]'),
|
||||||
menu: clickable('[data-test-popup-menu-trigger]'),
|
menu: clickable('[data-test-popup-menu-trigger]'),
|
||||||
}),
|
}),
|
||||||
|
|
Loading…
Reference in New Issue