Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 22 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7f39915
initial WIP glimmerize the controller
Monkeychip May 1, 2023
152aac3
wip got the filter engine type by supported backends working
Monkeychip May 2, 2023
252c396
got filter by engine type working
Monkeychip May 2, 2023
4e9682e
wip need to refactor but working ish for name
Monkeychip May 2, 2023
e54db03
wip working state with both filters, does not work if both fiters are…
Monkeychip May 3, 2023
c143cf9
fixed when you have two selected filters, but broken for multiples of…
Monkeychip May 3, 2023
97b80df
remove repeated engineTypes in filter list
Monkeychip May 3, 2023
b5c2e93
add disabled to power select
Monkeychip May 3, 2023
577886c
fix bug of glimmer for the concurrency task.
Monkeychip May 3, 2023
4a1c09f
wording fix
Monkeychip May 3, 2023
34f71f9
remove linkableItem and the nested contextual compnents to help with …
Monkeychip May 4, 2023
586d8e5
add changelog
Monkeychip May 4, 2023
994ccde
fix some tests
Monkeychip May 5, 2023
f11e204
add test coverage
Monkeychip May 5, 2023
7d1c095
Update 20481.txt
Monkeychip May 5, 2023
a66e319
Merge branch 'main' into ui/VAULT-16024/filter-secret-engines-type-name
Monkeychip May 5, 2023
eed1407
Merge branch 'ui/VAULT-16024/filter-secret-engines-type-name' of gith…
Monkeychip May 5, 2023
3faa498
test fixes 🤞
Monkeychip May 5, 2023
ed8ec45
test fix?
Monkeychip May 15, 2023
92f8aa9
address a pr comment and save
Monkeychip May 15, 2023
d90698a
Merge branch 'main' into ui/VAULT-16024/filter-secret-engines-type-name
Monkeychip May 15, 2023
5ea6e66
address pr comment
Monkeychip May 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/20481.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: Add filtering by engine type and engine name to the Secret Engine list view.
```
96 changes: 68 additions & 28 deletions ui/app/controllers/vault/cluster/secrets/backends.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

export default Controller.extend({
flashMessages: service(),
displayableBackends: filterBy('model', 'shouldIncludeInList'),

supportedBackends: computed('displayableBackends', 'displayableBackends.[]', function () {
return (this.displayableBackends || [])
.filter((backend) => LINKED_BACKENDS.includes(backend.get('engineType')))
.sortBy('id');
}),

unsupportedBackends: computed(
'displayableBackends',
'displayableBackends.[]',
'supportedBackends',
'supportedBackends.[]',
function () {
return (this.displayableBackends || []).slice().removeObjects(this.supportedBackends).sortBy('id');
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { filterBy } from '@ember/object/computed';
import { dropTask } from 'ember-concurrency';

export default class VaultClusterSecretsBackendController extends Controller {
@service flashMessages;
@filterBy('model', 'shouldIncludeInList') displayableBackends;

@tracked secretEngineOptions = [];
@tracked selectedEngineType = null;
@tracked selectedEngineName = null;

Copy link
Contributor Author

@Monkeychip Monkeychip May 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open to suggestions on ways to make this cleaner, but for context here is the flow:

  1. On init, this getter returns the full list of secret engines to display on the template.
  2. When someone filters, either by name or engine type, this list is updated. This update is triggered because of the tracked properties that change inside the getter when someone filters by name or engine type.
  3. This getter is used to create the options param passed into the SearchSelect components.

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.get('id'));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to use .get anymore, we could instead do:

Suggested change
return sortedBackends.filter((backend) => this.selectedEngineName === backend.get('id'));
return sortedBackends.filter((backend) => this.selectedEngineName === backend.id);

But if that doesn't work for some reason I'd like to understand why!

}
// otherwise filter by engine type
return sortedBackends.filter((backend) => this.selectedEngineType === backend.get('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.get('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();
Expand All @@ -41,5 +81,5 @@ export default Controller.extend({
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${err.errors.join(' ')}.`
);
}
}).drop(),
});
}
}
24 changes: 24 additions & 0 deletions ui/app/models/secret-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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';
}
Expand Down
179 changes: 94 additions & 85 deletions ui/app/templates/vault/cluster/secrets/backends.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
28 changes: 0 additions & 28 deletions ui/lib/core/addon/components/linkable-item.js

This file was deleted.

Loading
Loading