-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
UI: [VAULT-17035 VAULT-17038 VAULT-17039] Dashboard Quick Actions card (
#21929) Co-authored-by: [email protected] <[email protected]>
- Loading branch information
1 parent
cf558ab
commit a9603f6
Showing
5 changed files
with
343 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
/** | ||
* Copyright (c) HashiCorp, Inc. | ||
* SPDX-License-Identifier: MPL-2.0 | ||
*/ | ||
|
||
import Component from '@glimmer/component'; | ||
import { action } from '@ember/object'; | ||
import { tracked } from '@glimmer/tracking'; | ||
import { inject as service } from '@ember/service'; | ||
|
||
/** | ||
* @module DashboardQuickActionsCard | ||
* DashboardQuickActionsCard component allows users to see a list of secrets engines filtered by | ||
* kv, pki and database and perform certain actions based on the type of secret engine selected | ||
* | ||
* @example | ||
* ```js | ||
* <Dashboard::QuickActionsCard @secretsEngines={{@model.secretsEngines}} /> | ||
* ``` | ||
*/ | ||
|
||
const QUICK_ACTION_ENGINES = ['pki', 'kv', 'database']; | ||
|
||
export default class DashboardQuickActionsCard extends Component { | ||
@service router; | ||
|
||
@tracked selectedEngine; | ||
@tracked selectedAction; | ||
@tracked mountPath; | ||
@tracked paramValue; | ||
|
||
get actionOptions() { | ||
switch (this.selectedEngine) { | ||
case 'kv': | ||
return ['Find KV secrets']; | ||
case 'database': | ||
return ['Generate credentials for database']; | ||
case 'pki': | ||
return ['Issue certificate', 'View certificate', 'View issuer']; | ||
default: | ||
return []; | ||
} | ||
} | ||
|
||
get searchSelectParams() { | ||
switch (this.selectedAction) { | ||
case 'Find KV secrets': | ||
return { | ||
title: 'Secret Path', | ||
subText: 'Path of the secret you want to read, including the mount. E.g., secret/data/foo.', | ||
buttonText: 'Read secrets', | ||
model: 'secret-v2', | ||
route: 'vault.cluster.secrets.backends.show', | ||
}; | ||
case 'Generate credentials for database': | ||
return { | ||
title: 'Role to use', | ||
buttonText: 'Generate credentials', | ||
model: 'database/role', | ||
route: 'vault.cluster.secrets.backend.credentials', | ||
}; | ||
case 'Issue certificate': | ||
return { | ||
title: 'Role to use', | ||
placeholder: 'Type to find a role', | ||
buttonText: 'Issue leaf certificate', | ||
model: 'pki/role', | ||
route: 'vault.cluster.secrets.backend.pki.roles.role.generate', | ||
}; | ||
case 'View certificate': | ||
return { | ||
title: 'Certificate serial number', | ||
placeholder: '33:a3:...', | ||
buttonText: 'View certificate', | ||
model: 'pki/certificate/base', | ||
route: 'vault.cluster.secrets.backend.pki.certificates.certificate.details', | ||
}; | ||
case 'View issuer': | ||
return { | ||
title: 'Issuer', | ||
placeholder: 'Type issuer name or ID', | ||
buttonText: 'View issuer', | ||
model: 'pki/issuer', | ||
nameKey: 'issuerName', | ||
route: 'vault.cluster.secrets.backend.pki.issuers.issuer.details', | ||
}; | ||
default: | ||
return { | ||
placeholder: 'Please select an action above', | ||
buttonText: 'Select an action', | ||
model: '', | ||
}; | ||
} | ||
} | ||
|
||
get filteredSecretEngines() { | ||
return this.args.secretsEngines.filter((engine) => QUICK_ACTION_ENGINES.includes(engine.type)); | ||
} | ||
|
||
get mountOptions() { | ||
return this.filteredSecretEngines.map((engine) => { | ||
const { id, type } = engine; | ||
return { name: id, type, id }; | ||
}); | ||
} | ||
|
||
@action | ||
handleSearchEngineSelect([selection]) { | ||
this.selectedEngine = selection?.type; | ||
this.mountPath = selection?.id; | ||
// reset tracked properties | ||
this.selectedAction = null; | ||
this.paramValue = null; | ||
} | ||
|
||
@action | ||
setSelectedAction(selectedAction) { | ||
this.selectedAction = selectedAction; | ||
this.paramValue = null; | ||
} | ||
|
||
@action | ||
handleActionSelect(val) { | ||
if (Array.isArray(val)) { | ||
this.paramValue = val[0]; | ||
} else { | ||
this.paramValue = val; | ||
} | ||
} | ||
|
||
@action | ||
navigateToPage() { | ||
let searchSelectParamRoute = this.searchSelectParams.route; | ||
|
||
// kv has a special use case where if the paramValue ends in a '/' you should | ||
// link to different route | ||
if (this.selectedEngine === 'kv') { | ||
searchSelectParamRoute = | ||
this.paramValue && this.paramValue[this.paramValue?.length - 1] === '/' | ||
? 'vault.cluster.secrets.backend.list' | ||
: 'vault.cluster.secrets.backend.show'; | ||
} | ||
|
||
this.router.transitionTo(searchSelectParamRoute, this.mountPath, this.paramValue); | ||
} | ||
} |
63 changes: 63 additions & 0 deletions
63
ui/app/templates/components/dashboard/quick-actions-card.hbs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
<h3 class="title is-4">Quick actions</h3> | ||
|
||
<div class="has-bottom-margin-m"> | ||
<h4 class="title is-6">Secrets engines</h4> | ||
<SearchSelect | ||
@id="secrets-engines-select" | ||
@options={{this.mountOptions}} | ||
@selectLimit="1" | ||
@disallowNewItems={{true}} | ||
@fallbackComponent="input-search" | ||
@onChange={{this.handleSearchEngineSelect}} | ||
@placeholder="Type to select a mount" | ||
@displayInherit={{true}} | ||
@shouldRenderName={{true}} | ||
@passObject={{true}} | ||
@objectKeys={{array "type"}} | ||
class="is-marginless" | ||
data-test-secrets-engines-select | ||
/> | ||
</div> | ||
|
||
{{#if this.selectedEngine}} | ||
<h4 class="title is-6">Action</h4> | ||
<Select | ||
@name="action-select" | ||
@options={{this.actionOptions}} | ||
@isFullwidth={{true}} | ||
@selectedValue={{this.selectedAction}} | ||
@onChange={{this.setSelectedAction}} | ||
@noDefault={{true}} | ||
/> | ||
|
||
{{#if this.searchSelectParams.model}} | ||
<h4 class="title is-6" data-test-search-select-params-title>{{this.searchSelectParams.title}}</h4> | ||
|
||
<SearchSelect | ||
class="is-flex-1" | ||
@selectLimit="1" | ||
@models={{array this.searchSelectParams.model}} | ||
@backend={{this.mountPath}} | ||
@placeholder={{this.searchSelectParams.placeholder}} | ||
@disallowNewItems={{true}} | ||
@onChange={{this.handleActionSelect}} | ||
@fallbackComponent="input-search" | ||
@nameKey={{this.searchSelectParams.nameKey}} | ||
@disabled={{not this.searchSelectParams.model}} | ||
/> | ||
|
||
<div> | ||
<button | ||
type="button" | ||
class="button is-primary has-top-margin-m" | ||
disabled={{(not (and this.selectedAction this.selectedEngine this.paramValue))}} | ||
{{on "click" this.navigateToPage}} | ||
data-test-button={{this.searchSelectParams.buttonText}} | ||
> | ||
{{this.searchSelectParams.buttonText}} | ||
</button> | ||
</div> | ||
{{/if}} | ||
{{else}} | ||
<EmptyState @title="No mount selected" @message="Select a mount above to get started." /> | ||
{{/if}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
ui/tests/integration/components/dashboard/quick-actions-card-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/** | ||
* Copyright (c) HashiCorp, Inc. | ||
* SPDX-License-Identifier: MPL-2.0 | ||
*/ | ||
|
||
import { module, test } from 'qunit'; | ||
import { setupRenderingTest } from 'vault/tests/helpers'; | ||
import { render } from '@ember/test-helpers'; | ||
import { hbs } from 'ember-cli-htmlbars'; | ||
import { fillIn } from '@ember/test-helpers'; | ||
|
||
import { selectChoose } from 'ember-power-select/test-support/helpers'; | ||
|
||
// // TODO LANDING PAGE: create SELECTORS for the data-test attributes | ||
|
||
module('Integration | Component | dashboard/quick-actions-card', function (hooks) { | ||
setupRenderingTest(hooks); | ||
|
||
hooks.beforeEach(function () { | ||
this.store = this.owner.lookup('service:store'); | ||
this.store.pushPayload('secret-engine', { | ||
modelName: 'secret-engine', | ||
data: { | ||
accessor: 'kubernetes_f3400dee', | ||
path: 'kubernetes-test/', | ||
type: 'kubernetes', | ||
}, | ||
}); | ||
this.store.pushPayload('secret-engine', { | ||
modelName: 'secret-engine', | ||
data: { | ||
accessor: 'database_f3400dee', | ||
path: 'database-test/', | ||
type: 'database', | ||
}, | ||
}); | ||
this.store.pushPayload('secret-engine', { | ||
modelName: 'secret-engine', | ||
data: { | ||
accessor: 'pki_i1234dd', | ||
path: 'apki-test/', | ||
type: 'pki', | ||
}, | ||
}); | ||
this.store.pushPayload('secret-engine', { | ||
modelName: 'secret-engine', | ||
data: { | ||
accessor: 'secrets_j2350ii', | ||
path: 'secrets-test/', | ||
type: 'kv', | ||
}, | ||
}); | ||
this.store.pushPayload('secret-engine', { | ||
modelName: 'secret-engine', | ||
data: { | ||
accessor: 'nomad_123hh', | ||
path: 'nomad/', | ||
type: 'nomad', | ||
}, | ||
}); | ||
this.store.pushPayload('secret-engine', { | ||
modelName: 'secret-engine', | ||
data: { | ||
accessor: 'pki_f3400dee', | ||
path: 'pki-0-test/', | ||
type: 'pki', | ||
}, | ||
}); | ||
this.store.pushPayload('secret-engine', { | ||
modelName: 'secret-engine', | ||
data: { | ||
accessor: 'pki_i1234dd', | ||
path: 'pki-1-test/', | ||
description: 'pki-1-path-description', | ||
type: 'pki', | ||
}, | ||
}); | ||
this.store.pushPayload('secret-engine', { | ||
modelName: 'secret-engine', | ||
data: { | ||
accessor: 'secrets_j2350ii', | ||
path: 'secrets-1-test/', | ||
type: 'kv', | ||
}, | ||
}); | ||
|
||
this.secretsEngines = this.store.peekAll('secret-engine', {}); | ||
|
||
this.renderComponent = () => { | ||
return render(hbs`<Dashboard::QuickActionsCard @secretsEngines={{this.secretsEngines}} />`); | ||
}; | ||
}); | ||
|
||
test('it should show quick action empty state if no engine is selected', async function (assert) { | ||
await this.renderComponent(); | ||
assert.dom('.title').hasText('Quick actions'); | ||
assert.dom('[data-test-secrets-engines-select]').exists({ count: 1 }); | ||
assert.dom('[data-test-component="empty-state"]').exists({ count: 1 }); | ||
}); | ||
|
||
test('it should show correct actions for pki', async function (assert) { | ||
await this.renderComponent(); | ||
await selectChoose('.search-select', 'pki-0-test'); | ||
await fillIn('[data-test-select="action-select"]', 'Issue certificate'); | ||
assert.dom('[data-test-component="empty-state"]').doesNotExist(); | ||
await fillIn('[data-test-select="action-select"]', 'Issue certificate'); | ||
assert.dom('[data-test-button="Issue leaf certificate"]').exists({ count: 1 }); | ||
assert.dom('[data-test-search-select-params-title]').hasText('Role to use'); | ||
await fillIn('[data-test-select="action-select"]', 'View certificate'); | ||
assert.dom('[data-test-search-select-params-title]').hasText('Certificate serial number'); | ||
assert.dom('[data-test-button="View certificate"]').exists({ count: 1 }); | ||
await fillIn('[data-test-select="action-select"]', 'View issuer'); | ||
assert.dom('[data-test-search-select-params-title]').hasText('Issuer'); | ||
assert.dom('[data-test-button="View issuer"]').exists({ count: 1 }); | ||
}); | ||
test('it should show correct actions for database', async function (assert) { | ||
await this.renderComponent(); | ||
await selectChoose('.search-select', 'database-test'); | ||
assert.dom('[data-test-component="empty-state"]').doesNotExist(); | ||
await fillIn('[data-test-select="action-select"]', 'Generate credentials for database'); | ||
assert.dom('[data-test-search-select-params-title]').hasText('Role to use'); | ||
assert.dom('[data-test-button="Generate credentials"]').exists({ count: 1 }); | ||
}); | ||
test('it should show correct actions for kv', async function (assert) { | ||
await this.renderComponent(); | ||
await selectChoose('.search-select', 'secrets-1-test'); | ||
assert.dom('[data-test-component="empty-state"]').doesNotExist(); | ||
await fillIn('[data-test-select="action-select"]', 'Find KV secrets'); | ||
assert.dom('[data-test-search-select-params-title]').hasText('Secret Path'); | ||
assert.dom('[data-test-button="Read secrets"]').exists({ count: 1 }); | ||
}); | ||
}); |