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

UI: [VAULT-17035 VAULT-17038 VAULT-17039] Dashboard Quick Actions card #21929

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3548b9d
VAULT-17038 VAULT-17039 wip dropdown and filtered list
kiannaquach Jul 18, 2023
49c8444
Merge branch 'ui/landing-page-dashboard' into ui/dashboard-quick-acti…
kiannaquach Jul 20, 2023
79815f6
Merge branch 'ui/landing-page-dashboard' into ui/dashboard-quick-acti…
kiannaquach Jul 20, 2023
70ac4df
Merge branch 'ui/landing-page-dashboard' into ui/dashboard-quick-acti…
kiannaquach Jul 24, 2023
7ae1a21
Semi working quick actions
kiannaquach Jul 24, 2023
7f24e84
Merge branch 'ui/landing-page-dashboard' into ui/dashboard-quick-acti…
kiannaquach Jul 25, 2023
1e2670b
Fixes some bugs
kiannaquach Jul 25, 2023
8044dd3
Fix button routes.. still needs more work to transition correctly
kiannaquach Jul 26, 2023
74ab401
Pair with Claire on getting transition to work
kiannaquach Jul 26, 2023
e37fd7b
Merge branch 'ui/landing-page-dashboard' into ui/dashboard-quick-acti…
kiannaquach Aug 1, 2023
c6c8538
Add default state for quick actions
kiannaquach Aug 1, 2023
fea5a6f
Address feedback
kiannaquach Aug 1, 2023
ea759c1
Merge branch 'ui/landing-page-dashboard' into ui/dashboard-quick-acti…
kiannaquach Aug 1, 2023
b26989b
Add path for kv
kiannaquach Aug 2, 2023
04d06f9
return object from search select option with engine id and type
hellobontempo Aug 3, 2023
2c6cbf2
Merge branch 'ui/landing-page-dashboard' into ui/dashboard-quick-acti…
kiannaquach Aug 3, 2023
2fd9e1d
Refactor quick actions component with Claire based on her feedback
kiannaquach Aug 3, 2023
1f704e1
Address more feedback!
kiannaquach Aug 3, 2023
f3ceb8e
Remove shouldRenderName
kiannaquach Aug 4, 2023
b650599
Fix failing test!
kiannaquach Aug 4, 2023
3a51f82
Add quick actions acceptance test
kiannaquach Aug 7, 2023
e93c243
Merge branch 'ui/landing-page-dashboard' into ui/dashboard-quick-acti…
kiannaquach Aug 7, 2023
2f0a734
Merge branch 'ui/landing-page-dashboard' into ui/dashboard-quick-acti…
kiannaquach Aug 7, 2023
c666969
Add consol test
kiannaquach Aug 7, 2023
eb88387
More tests
kiannaquach Aug 7, 2023
2c5bf06
Revert acceptance test to why its failing
kiannaquach Aug 7, 2023
49154fb
Put things back
kiannaquach Aug 8, 2023
426176c
Add TODO for selectors
kiannaquach Aug 8, 2023
842deea
Remove extra spaces
kiannaquach Aug 8, 2023
159e917
Remove ellipsis and update default
kiannaquach Aug 8, 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
146 changes: 146 additions & 0 deletions ui/app/components/dashboard/quick-actions-card.js
kiannaquach marked this conversation as resolved.
Show resolved Hide resolved
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 ui/app/templates/components/dashboard/quick-actions-card.hbs
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}}
Copy link
Contributor

Choose a reason for hiding this comment

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

nice and clean on this. Looks great!

/>

<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}}
2 changes: 1 addition & 1 deletion ui/app/templates/vault/cluster/dashboard.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</Hds::Card::Container>

<Hds::Card::Container @hasBorder={{true}} class="has-padding-l is-flex-column is-flex-half">
<h3 class="title is-4">Quick Actions</h3>
<Dashboard::QuickActionsCard @secretsEngines={{@model.secretsEngines}} />
</Hds::Card::Container>

<Hds::Card::Container @hasBorder={{true}} class="has-padding-l is-flex-column is-flex-half">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ module('Acceptance | secrets/database/*', function (hooks) {
await authPage.logout();
// Check with restricted permissions
await authPage.login(token);
await click('[data-test-sidebar-nav-link="Secrets engines"]');
assert.dom(`[data-test-auth-backend-link="${backend}"]`).exists('Shows backend on secret list page');
await navToConnection(backend, connection);
assert.strictEqual(
Expand Down
132 changes: 132 additions & 0 deletions ui/tests/integration/components/dashboard/quick-actions-card-test.js
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 });
});
});