UI: Hds::Dropdown replace PopupMenu (#25321)

This commit is contained in:
claire bontempo
2024-02-09 10:38:14 -08:00
committed by GitHub
parent 28d81ed832
commit fe56069f67
78 changed files with 1446 additions and 1215 deletions

3
changelog/25321.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
ui: Use Hds::Dropdown component to replace list view popup menus
```

View File

@@ -1,46 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { inject as service } from '@ember/service';
import { assert } from '@ember/debug';
import Component from '@ember/component';
export default Component.extend({
tagName: '',
flashMessages: service(),
params: null,
successMessage() {
return 'Save was successful';
},
errorMessage() {
return 'There was an error saving';
},
onError(model) {
if (model && model.rollbackAttributes) {
model.rollbackAttributes();
}
},
onSuccess() {},
// override and return a promise
transaction() {
assert('override transaction call in an extension of popup-base', false);
},
actions: {
performTransaction() {
const args = [...arguments];
const messageArgs = this.messageArgs(...args);
return this.transaction(...args)
.then(() => {
this.onSuccess();
this.flashMessages.success(this.successMessage(...messageArgs));
})
.catch((e) => {
this.onError(...messageArgs);
this.flashMessages.success(this.errorMessage(e, ...messageArgs));
});
},
},
});

View File

@@ -3,25 +3,38 @@
* SPDX-License-Identifier: BUSL-1.1 * SPDX-License-Identifier: BUSL-1.1
*/ */
import Base from './_popup-base'; import Component from '@glimmer/component';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import errorMessage from 'vault/utils/error-message';
export default Base.extend({ export default class IdentityPopupAlias extends Component {
messageArgs(model) { @service flashMessages;
const type = model.get('identityType'); @tracked showConfirmModal = false;
const id = model.id;
return [type, id];
},
successMessage(type, id) { onSuccess(type, id) {
return `Successfully deleted ${type}: ${id}`; if (this.args.onSuccess) {
}, this.args.onSuccess();
}
this.flashMessages.success(`Successfully deleted ${type}: ${id}`);
}
onError(err, type, id) {
if (this.args.onError) {
this.args.onError();
}
const error = errorMessage(err);
this.flashMessages.danger(`There was a problem deleting ${type}: ${id} - ${error}`);
}
errorMessage(e, type, id) { @action
const error = e.errors ? e.errors.join(' ') : e.message; async deleteAlias() {
return `There was a problem deleting ${type}: ${id} - ${error}`; const { identityType, id } = this.args.item;
}, try {
await this.args.item.destroyRecord();
transaction(model) { this.onSuccess(identityType, id);
return model.destroyRecord(); } catch (e) {
}, this.onError(e, identityType, id);
}); }
}
}

View File

@@ -3,37 +3,44 @@
* SPDX-License-Identifier: BUSL-1.1 * SPDX-License-Identifier: BUSL-1.1
*/ */
import { alias } from '@ember/object/computed'; import Component from '@glimmer/component';
import { computed } from '@ember/object'; import { action } from '@ember/object';
import Base from './_popup-base'; import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import errorMessage from 'vault/utils/error-message';
export default Base.extend({ export default class IdentityPopupMembers extends Component {
model: alias('params.firstObject'), @service flashMessages;
@tracked showConfirmModal = false;
groupArray: computed('params', function () { onSuccess(memberId) {
return this.params.objectAt(1); if (this.args.onSuccess) {
}), this.args.onSuccess();
}
this.flashMessages.success(`Successfully removed '${memberId}' from the group`);
}
onError(err, memberId) {
if (this.args.onError) {
this.args.onError();
}
const error = errorMessage(err);
this.flashMessages.danger(`There was a problem removing '${memberId}' from the group - ${error}`);
}
memberId: computed('params', function () { transaction() {
return this.params.objectAt(2); const members = this.args.model[this.args.groupArray];
}), this.args.model[this.args.groupArray] = members.without(this.args.memberId);
return this.args.model.save();
}
messageArgs(/*model, groupArray, memberId*/) { @action
return [...arguments]; async removeGroup() {
}, const memberId = this.args.memberId;
try {
successMessage(model, groupArray, memberId) { await this.transaction();
return `Successfully removed '${memberId}' from the group`; this.onSuccess(memberId);
}, } catch (e) {
this.onError(e, memberId);
errorMessage(e, model, groupArray, memberId) { }
const error = e.errors ? e.errors.join(' ') : e.message; }
return `There was a problem removing '${memberId}' from the group - ${error}`; }
},
transaction(model, groupArray, memberId) {
const members = model.get(groupArray);
model.set(groupArray, members.without(memberId));
return model.save();
},
});

View File

@@ -3,32 +3,45 @@
* SPDX-License-Identifier: BUSL-1.1 * SPDX-License-Identifier: BUSL-1.1
*/ */
import Base from './_popup-base'; import { action } from '@ember/object';
import { computed } from '@ember/object'; import { service } from '@ember/service';
import { alias } from '@ember/object/computed'; import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import errorMessage from 'vault/utils/error-message';
export default Base.extend({ export default class IdentityPopupMetadata extends Component {
model: alias('params.firstObject'), @service flashMessages;
key: computed('params', function () { @tracked showConfirmModal = false;
return this.params.objectAt(1);
}),
messageArgs(model, key) { onSuccess(key) {
return [model, key]; if (this.args.onSuccess) {
}, this.args.onSuccess();
}
this.flashMessages.success(`Successfully removed '${key}' from metadata`);
}
onError(err, key) {
if (this.args.onError) {
this.args.onError();
}
const error = errorMessage(err);
this.flashMessages.danger(`There was a problem removing '${key}' from the metadata - ${error}`);
}
successMessage(model, key) { transaction() {
return `Successfully removed '${key}' from metadata`; const metadata = this.args.model.metadata;
}, delete metadata[this.args.key];
errorMessage(e, model, key) { this.args.model.metadata = { ...metadata };
const error = e.errors ? e.errors.join(' ') : e.message; return this.args.model.save();
return `There was a problem removing '${key}' from the metadata - ${error}`; }
},
transaction(model, key) { @action
const metadata = model.metadata; async removeMetadata() {
delete metadata[key]; const key = this.args.key;
model.set('metadata', { ...metadata }); try {
return model.save(); await this.transaction();
}, this.onSuccess(key);
}); } catch (e) {
this.onError(e, key);
}
}
}

View File

@@ -3,32 +3,47 @@
* SPDX-License-Identifier: BUSL-1.1 * SPDX-License-Identifier: BUSL-1.1
*/ */
import { alias } from '@ember/object/computed'; import Component from '@glimmer/component';
import { computed } from '@ember/object'; import { action } from '@ember/object';
import Base from './_popup-base'; import { service } from '@ember/service';
import errorMessage from 'vault/utils/error-message';
import { tracked } from '@glimmer/tracking';
export default Base.extend({ export default class IdentityPopupPolicy extends Component {
model: alias('params.firstObject'), @service flashMessages;
policyName: computed('params', function () { @tracked showConfirmModal = false;
return this.params.objectAt(1);
}),
messageArgs(model, policyName) { onSuccess(policyName, modelId) {
return [model, policyName]; if (this.args.onSuccess) {
}, this.args.onSuccess();
}
this.flashMessages.success(`Successfully removed '${policyName}' policy from ${modelId}`);
}
onError(err, policyName) {
if (this.args.onError) {
this.args.onError();
}
const error = errorMessage(err);
this.flashMessages.danger(`There was a problem removing '${policyName}' policy - ${error}`);
}
successMessage(model, policyName) { transaction() {
return `Successfully removed '${policyName}' policy from ${model.id} `; const policies = this.args.model.policies;
}, this.args.model.policies = policies.without(this.args.policyName);
return this.args.model.save();
}
errorMessage(e, model, policyName) { @action
const error = e.errors ? e.errors.join(' ') : e.message; async removePolicy() {
return `There was a problem removing '${policyName}' policy - ${error}`; const {
}, policyName,
model: { id },
transaction(model, policyName) { } = this.args;
const policies = model.get('policies'); try {
model.set('policies', policies.without(policyName)); await this.transaction();
return model.save(); this.onSuccess(policyName, id);
}, } catch (e) {
}); this.onError(e, policyName);
}
}
}

View File

@@ -0,0 +1,11 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class SecretListAwsRoleItemComponent extends Component {
@tracked showConfirmModal = false;
}

View File

@@ -22,6 +22,7 @@ import { action } from '@ember/object';
export default class DatabaseListItem extends Component { export default class DatabaseListItem extends Component {
@tracked roleType = ''; @tracked roleType = '';
@tracked actionRunning = null;
@service store; @service store;
@service flashMessages; @service flashMessages;
@@ -41,6 +42,7 @@ export default class DatabaseListItem extends Component {
resetConnection(id) { resetConnection(id) {
const { backend } = this.args.item; const { backend } = this.args.item;
const adapter = this.store.adapterFor('database/connection'); const adapter = this.store.adapterFor('database/connection');
this.actionRunning = 'reset';
adapter adapter
.resetConnection(backend, id) .resetConnection(backend, id)
.then(() => { .then(() => {
@@ -48,12 +50,14 @@ export default class DatabaseListItem extends Component {
}) })
.catch((e) => { .catch((e) => {
this.flashMessages.danger(e.errors); this.flashMessages.danger(e.errors);
}); })
.finally(() => (this.actionRunning = null));
} }
@action @action
rotateRootCred(id) { rotateRootCred(id) {
const { backend } = this.args.item; const { backend } = this.args.item;
const adapter = this.store.adapterFor('database/connection'); const adapter = this.store.adapterFor('database/connection');
this.actionRunning = 'rotateRoot';
adapter adapter
.rotateRootCredentials(backend, id) .rotateRootCredentials(backend, id)
.then(() => { .then(() => {
@@ -61,12 +65,14 @@ export default class DatabaseListItem extends Component {
}) })
.catch((e) => { .catch((e) => {
this.flashMessages.danger(e.errors); this.flashMessages.danger(e.errors);
}); })
.finally(() => (this.actionRunning = null));
} }
@action @action
rotateRoleCred(id) { rotateRoleCred(id) {
const { backend } = this.args.item; const { backend } = this.args.item;
const adapter = this.store.adapterFor('database/credential'); const adapter = this.store.adapterFor('database/credential');
this.actionRunning = 'rotateRole';
adapter adapter
.rotateRoleCredentials(backend, id) .rotateRoleCredentials(backend, id)
.then(() => { .then(() => {
@@ -74,6 +80,7 @@ export default class DatabaseListItem extends Component {
}) })
.catch((e) => { .catch((e) => {
this.flashMessages.danger(e.errors); this.flashMessages.danger(e.errors);
}); })
.finally(() => (this.actionRunning = null));
} }
} }

View File

@@ -0,0 +1,11 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class SecretListItemComponent extends Component {
@tracked showConfirmModal = false;
}

View File

@@ -0,0 +1,11 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class SecretListSshRoleItemComponent extends Component {
@tracked showConfirmModal = false;
}

View File

@@ -10,6 +10,9 @@ import ListController from 'core/mixins/list-controller';
export default Controller.extend(ListController, { export default Controller.extend(ListController, {
flashMessages: service(), flashMessages: service(),
entityToDisable: null,
itemToDelete: null,
// callback from HDS pagination to set the queryParams page // callback from HDS pagination to set the queryParams page
get paginationQueryParams() { get paginationQueryParams() {
return (page) => { return (page) => {
@@ -33,7 +36,8 @@ export default Controller.extend(ListController, {
this.flashMessages.success( this.flashMessages.success(
`There was a problem deleting ${type}: ${id} - ${e.errors.join(' ') || e.message}` `There was a problem deleting ${type}: ${id} - ${e.errors.join(' ') || e.message}`
); );
}); })
.finally(() => this.set('itemToDelete', null));
}, },
toggleDisabled(model) { toggleDisabled(model) {
@@ -51,7 +55,8 @@ export default Controller.extend(ListController, {
this.flashMessages.success( this.flashMessages.success(
`There was a problem ${action[1]} ${type}: ${id} - ${e.errors.join(' ') || e.message}` `There was a problem ${action[1]} ${type}: ${id} - ${e.errors.join(' ') || e.message}`
); );
}); })
.finally(() => this.set('entityToDisable', null));
}, },
reloadRecord(model) { reloadRecord(model) {
model.reload(); model.reload();

View File

@@ -21,8 +21,8 @@ export default Controller.extend({
filterFocused: false, filterFocused: false,
// set via the route `loading` action isLoading: false, // set via the route `loading` action
isLoading: false, policyToDelete: null, // set when clicking 'Delete' from popup menu
// callback from HDS pagination to set the queryParams page // callback from HDS pagination to set the queryParams page
get paginationQueryParams() { get paginationQueryParams() {
@@ -77,7 +77,8 @@ export default Controller.extend({
flash.danger( flash.danger(
`There was an error deleting the ${policyType.toUpperCase()} policy "${name}": ${errors}.` `There was an error deleting the ${policyType.toUpperCase()} policy "${name}": ${errors}.`
); );
}); })
.finally(() => this.set('policyToDelete', null));
}, },
}, },
}); });

View File

@@ -17,6 +17,7 @@ export default class VaultClusterSecretsBackendController extends Controller {
@tracked secretEngineOptions = []; @tracked secretEngineOptions = [];
@tracked selectedEngineType = null; @tracked selectedEngineType = null;
@tracked selectedEngineName = null; @tracked selectedEngineName = null;
@tracked engineToDisable = null;
get sortedDisplayableBackends() { get sortedDisplayableBackends() {
// show supported secret engines first and then organize those by id. // show supported secret engines first and then organize those by id.
@@ -80,6 +81,8 @@ export default class VaultClusterSecretsBackendController extends Controller {
this.flashMessages.danger( this.flashMessages.danger(
`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(' ')}.`
); );
} finally {
this.engineToDisable = null;
} }
} }
} }

View File

@@ -84,13 +84,5 @@ export default IdentityModel.extend({
canEdit: alias('updatePath.canUpdate'), canEdit: alias('updatePath.canUpdate'),
aliasPath: lazyCapabilities(apiPath`identity/group-alias`), aliasPath: lazyCapabilities(apiPath`identity/group-alias`),
canAddAlias: computed('aliasPath.canCreate', 'type', 'alias', function () { canAddAlias: alias('aliasPath.canCreate'),
const type = this.type;
const alias = this.alias;
// internal groups can't have aliases, and external groups can only have one
if (type === 'internal' || alias) {
return false;
}
return this.aliasPath.canCreate;
}),
}); });

View File

@@ -101,12 +101,12 @@ export default class OidcClientModel extends Model {
// CAPABILITIES // // CAPABILITIES //
@lazyCapabilities(apiPath`identity/oidc/client/${'name'}`, 'name') clientPath; @lazyCapabilities(apiPath`identity/oidc/client/${'name'}`, 'name') clientPath;
get canRead() { get canRead() {
return this.clientPath.get('canRead'); return this.clientPath.get('canRead') !== false;
} }
get canEdit() { get canEdit() {
return this.clientPath.get('canUpdate'); return this.clientPath.get('canUpdate') !== false;
} }
get canDelete() { get canDelete() {
return this.clientPath.get('canDelete'); return this.clientPath.get('canDelete') !== false;
} }
} }

View File

@@ -53,12 +53,12 @@ export default class OidcProviderModel extends Model {
@lazyCapabilities(apiPath`identity/oidc/provider/${'name'}`, 'name') providerPath; @lazyCapabilities(apiPath`identity/oidc/provider/${'name'}`, 'name') providerPath;
get canRead() { get canRead() {
return this.providerPath.get('canRead'); return this.providerPath.get('canRead') !== false;
} }
get canEdit() { get canEdit() {
return this.providerPath.get('canUpdate'); return this.providerPath.get('canUpdate') !== false;
} }
get canDelete() { get canDelete() {
return this.providerPath.get('canDelete'); return this.providerPath.get('canDelete') !== false;
} }
} }

View File

@@ -24,7 +24,7 @@
</span> </span>
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
<Identity::PopupAlias @params={{array item}} /> <Identity::PopupAlias @item={{item}} />
</div> </div>
</div> </div>
</LinkedBlock> </LinkedBlock>

View File

@@ -18,7 +18,7 @@
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
{{#if @model.canEdit}} {{#if @model.canEdit}}
<Identity::PopupMembers @params={{array @model "memberGroupIds" gid}} /> <Identity::PopupMembers @model={{@model}} @groupArray="memberGroupIds" @memberId={{gid}} />
{{/if}} {{/if}}
</div> </div>
</div> </div>
@@ -38,7 +38,7 @@
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
{{#if @model.canEdit}} {{#if @model.canEdit}}
<Identity::PopupMembers @params={{array @model "memberEntityIds" gid}} /> <Identity::PopupMembers @model={{@model}} @groupArray="memberEntityIds" @memberId={{gid}} />
{{/if}} {{/if}}
</div> </div>
</div> </div>

View File

@@ -16,7 +16,7 @@
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
{{#if @model.canEdit}} {{#if @model.canEdit}}
<Identity::PopupMetadata @params={{array @model key}} /> <Identity::PopupMetadata @key={{key}} @model={{@model}} />
{{/if}} {{/if}}
</div> </div>
</div> </div>

View File

@@ -17,7 +17,7 @@
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
{{#if @model.canEdit}} {{#if @model.canEdit}}
<Identity::PopupPolicy @params={{array @model policyName}} /> <Identity::PopupPolicy @model={{@model}} @policyName={{policyName}} />
{{/if}} {{/if}}
</div> </div>
</div> </div>

View File

@@ -3,43 +3,38 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<PopupMenu @name="alias-menu"> <div class="has-text-right">
{{#let (get this.params "0") as |item|}} <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="navigation for managing aliases"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li class="action"> @text="Alias management options"
<LinkTo @hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive
@text="Details"
@route="vault.cluster.access.identity.aliases.show" @route="vault.cluster.access.identity.aliases.show"
@models={{array (pluralize item.parentType) item.id "details"}} @models={{array (pluralize @item.parentType) @item.id "details"}}
> />
Details {{#if @item.updatePath.isPending}}
</LinkTo> <dd.Generic class="has-text-center">
</li>
{{#if item.updatePath.isPending}}
<li class="action">
<LoadingDropdownOption /> <LoadingDropdownOption />
</li> </dd.Generic>
{{else}} {{else}}
{{#if item.canEdit}} {{#if @item.canEdit}}
<li class="action"> <dd.Interactive
<LinkTo @text="Edit"
@route="vault.cluster.access.identity.aliases.edit" @route="vault.cluster.access.identity.aliases.edit"
@models={{array (pluralize item.parentType) item.id}} @models={{array (pluralize @item.parentType) @item.id}}
>
Edit
</LinkTo>
</li>
{{/if}}
{{#if item.canDelete}}
<ConfirmAction
@buttonText="Delete"
@isInDropdown={{true}}
@onConfirmAction={{action "performTransaction" item}}
data-test-item-delete
/> />
{{/if}} {{/if}}
{{#if @item.canDelete}}
<dd.Interactive @text="Remove" @color="critical" {{on "click" (fn (mut this.showConfirmModal) true)}} />
{{/if}} {{/if}}
</ul> {{/if}}
</nav> </Hds::Dropdown>
{{/let}} </div>
</PopupMenu>
{{#if this.showConfirmModal}}
<ConfirmModal @color="critical" @onClose={{fn (mut this.showConfirmModal) false}} @onConfirm={{this.deleteAlias}} />
{{/if}}

View File

@@ -3,18 +3,24 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<PopupMenu @name="member-edit-menu"> <div class="has-text-right">
<nav class="menu" aria-label="navigation for managing identity members"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<ul class="menu-list"> <dd.ToggleIcon
<li class="action"> @icon="more-horizontal"
<ConfirmAction @text="Identity member options"
@buttonText="Remove" @hasChevron={{false}}
@confirmTitle="Remove this group?" data-test-popup-menu-trigger
@isInDropdown={{true}}
@confirmMessage="This may affect permissions for this group."
@onConfirmAction={{action "performTransaction" this.model this.groupArray this.memberId}}
/> />
</li> <dd.Interactive @text="Remove" @color="critical" {{on "click" (fn (mut this.showConfirmModal) true)}} />
</ul> </Hds::Dropdown>
</nav> </div>
</PopupMenu>
{{#if this.showConfirmModal}}
<ConfirmModal
@color="critical"
@onClose={{fn (mut this.showConfirmModal) false}}
@onConfirm={{this.removeGroup}}
@confirmTitle="Remove this group?"
@confirmMessage="This may affect permissions for this group."
/>
{{/if}}

View File

@@ -3,18 +3,19 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<PopupMenu @name="metadata-edit-menu"> <div class="has-text-right">
<nav class="menu" aria-label="navigation for managing identity metadata"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<ul class="menu-list"> <dd.ToggleIcon @icon="more-horizontal" @text="Metadata options" @hasChevron={{false}} data-test-popup-menu-trigger />
<li class="action"> <dd.Interactive @text="Remove" @color="critical" {{on "click" (fn (mut this.showConfirmModal) true)}} />
<ConfirmAction </Hds::Dropdown>
@buttonText="Remove" </div>
{{#if this.showConfirmModal}}
<ConfirmModal
@color="critical"
@onClose={{fn (mut this.showConfirmModal) false}}
@onConfirm={{this.removeMetadata}}
@confirmTitle="Remove metadata?" @confirmTitle="Remove metadata?"
@isInDropdown={{true}}
@confirmMessage="This data may be used outside of Vault." @confirmMessage="This data may be used outside of Vault."
@onConfirmAction={{action "performTransaction" this.model this.key}}
/> />
</li> {{/if}}
</ul>
</nav>
</PopupMenu>

View File

@@ -3,28 +3,30 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<PopupMenu @name="policy-menu"> <div class="has-text-right">
<nav class="menu" aria-label="navigation for managing identity policies"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<ul class="menu-list"> <dd.ToggleIcon
<li class="action"> @icon="more-horizontal"
<LinkTo @route="vault.cluster.policy.show" @models={{array "acl" this.policyName}}> @text="Identity policy management options"
View Policy @hasChevron={{false}}
</LinkTo> data-test-popup-menu-trigger
</li>
<li class="action">
<LinkTo @route="vault.cluster.policy.edit" @models={{array "acl" this.policyName}}>
Edit Policy
</LinkTo>
</li>
<li class="action">
<ConfirmAction
@buttonText="Remove from {{this.model.identityType}}"
@confirmTitle="Remove this policy?"
@isInDropdown={{true}}
@confirmMessage="This policy may affect permissions to access Vault data."
@onConfirmAction={{action "performTransaction" this.model this.policyName}}
/> />
</li> <dd.Interactive @text="View policy" @route="vault.cluster.policy.show" @models={{array "acl" @policyName}} />
</ul> <dd.Interactive @text="Edit policy" @route="vault.cluster.policy.edit" @models={{array "acl" @policyName}} />
</nav> <dd.Interactive
</PopupMenu> @text="Remove from {{@model.identityType}}"
@color="critical"
{{on "click" (fn (mut this.showConfirmModal) true)}}
/>
</Hds::Dropdown>
</div>
{{#if this.showConfirmModal}}
<ConfirmModal
@color="critical"
@onClose={{fn (mut this.showConfirmModal) false}}
@onConfirm={{this.removePolicy}}
@confirmTitle="Remove this policy?"
@confirmMessage="This policy may affect permissions to access Vault data."
/>
{{/if}}

View File

@@ -19,30 +19,26 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="navigation for managing login enforcements"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li> @text="Manage M-F-A enforcement"
<LinkTo @hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive
@text="Details"
@route="vault.cluster.access.mfa.enforcements.enforcement" @route="vault.cluster.access.mfa.enforcements.enforcement"
@model={{@model.name}} @model={{@model.name}}
data-test-list-item-link="details" data-test-list-item-link="details"
> />
Details <dd.Interactive
</LinkTo> @text="Edit"
</li>
<li>
<LinkTo
@route="vault.cluster.access.mfa.enforcements.enforcement.edit" @route="vault.cluster.access.mfa.enforcements.enforcement.edit"
@model={{@model.name}} @model={{@model.name}}
data-test-list-item-link="edit" data-test-list-item-link="edit"
> />
Edit </Hds::Dropdown>
</LinkTo>
</li>
</ul>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -30,30 +30,26 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="navigation for managing MFA methods"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li> @text="Manage M-F-A method"
<LinkTo @hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive
@text="Details"
@route="vault.cluster.access.mfa.methods.method" @route="vault.cluster.access.mfa.methods.method"
@model={{@model.id}} @model={{@model.id}}
data-test-mfa-method-menu-link="details" data-test-mfa-method-menu-link="details"
> />
Details <dd.Interactive
</LinkTo> @text="Edit"
</li>
<li>
<LinkTo
@route="vault.cluster.access.mfa.methods.method.edit" @route="vault.cluster.access.mfa.methods.method.edit"
@model={{@model.id}} @model={{@model.id}}
data-test-mfa-method-menu-link="edit" data-test-mfa-method-menu-link="edit"
> />
Edit </Hds::Dropdown>
</LinkTo>
</li>
</ul>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -24,32 +24,32 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> {{#if (or client.canRead client.canEdit)}}
<nav class="menu" aria-label="navigation for managing OIDC client {{client.name}}"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<ul class="menu-list"> <dd.ToggleIcon
<li> @icon="more-horizontal"
<LinkTo @text="Application nav options"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if client.canRead}}
<dd.Interactive
@text="Details"
@route="vault.cluster.access.oidc.clients.client.details" @route="vault.cluster.access.oidc.clients.client.details"
@model={{client.name}} @model={{client.name}}
@disabled={{eq client.canRead false}}
data-test-oidc-client-menu-link="details" data-test-oidc-client-menu-link="details"
> />
Details {{/if}}
</LinkTo> {{#if client.canEdit}}
</li> <dd.Interactive
<li> @text="Edit"
<LinkTo
@route="vault.cluster.access.oidc.clients.client.edit" @route="vault.cluster.access.oidc.clients.client.edit"
@model={{client.name}} @model={{client.name}}
@disabled={{eq client.canEdit false}}
data-test-oidc-client-menu-link="edit" data-test-oidc-client-menu-link="edit"
> />
Edit {{/if}}
</LinkTo> </Hds::Dropdown>
</li> {{/if}}
</ul>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -24,32 +24,32 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> {{#if (or provider.canRead provider.canEdit)}}
<nav class="menu" aria-label="navigation for managing provider {{provider.name}}"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<ul class="menu-list"> <dd.ToggleIcon
<li> @icon="more-horizontal"
<LinkTo @text="Provider nav options"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if provider.canRead}}
<dd.Interactive
@text="Details"
@route="vault.cluster.access.oidc.providers.provider.details" @route="vault.cluster.access.oidc.providers.provider.details"
@model={{provider.name}} @model={{provider.name}}
@disabled={{eq provider.canRead false}}
data-test-oidc-provider-menu-link="details" data-test-oidc-provider-menu-link="details"
> />
Details {{/if}}
</LinkTo> {{#if provider.canEdit}}
</li> <dd.Interactive
<li> @text="Edit"
<LinkTo
@route="vault.cluster.access.oidc.providers.provider.edit" @route="vault.cluster.access.oidc.providers.provider.edit"
@model={{provider.name}} @model={{provider.name}}
@disabled={{not provider.canEdit}}
data-test-oidc-provider-menu-link="edit" data-test-oidc-provider-menu-link="edit"
> />
Edit {{/if}}
</LinkTo> </Hds::Dropdown>
</li> {{/if}}
</ul>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -23,58 +23,60 @@
</LinkTo> </LinkTo>
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
<PopupMenu @name="role-aws-nav" @contentClass="is-wide"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="navigation for managing A-W-S role {{@item.id}}"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
@text="manage A-W-S role {{@item.id}}"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if @item.generatePath.isPending}} {{#if @item.generatePath.isPending}}
<li class="action"> <dd.Generic class="has-text-center">
<LoadingDropdownOption /> <LoadingDropdownOption />
</li> </dd.Generic>
{{else if @item.canGenerate}} {{else if @item.canGenerate}}
<li class="action"> <dd.Interactive
<LinkTo @text="Generate credentials"
@route="vault.cluster.secrets.backend.credentials" @route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}} @model={{@item.id}}
data-test-role-aws-link="generate" data-test-role-aws-link="generate"
> />
Generate credentials
</LinkTo>
</li>
{{/if}} {{/if}}
{{#if @item.updatePath.isPending}} {{#if @item.updatePath.isPending}}
<li class="action"> <dd.Generic class="has-text-center">
<LoadingDropdownOption /> <LoadingDropdownOption />
</li> </dd.Generic>
<li class="action">
<LoadingDropdownOption />
</li>
{{else}} {{else}}
{{#if @item.canRead}} {{#if @item.canRead}}
<li class="action"> <dd.Interactive
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@item.id}} data-test-role-ssh-link="show"> @text="Details"
Details @route="vault.cluster.secrets.backend.show"
</LinkTo> @model={{@item.id}}
</li> data-test-role-ssh-link="show"
/>
{{/if}} {{/if}}
{{#if @item.canEdit}} {{#if @item.canEdit}}
<li class="action"> <dd.Interactive
<LinkTo @route="vault.cluster.secrets.backend.edit" @model={{@item.id}} data-test-role-ssh-link="edit"> @text="Edit"
Edit @route="vault.cluster.secrets.backend.edit"
</LinkTo> @model={{@item.id}}
</li> data-test-role-ssh-link="edit"
/>
{{/if}} {{/if}}
{{#if @item.canDelete}} {{#if @item.canDelete}}
<ConfirmAction <dd.Interactive
@buttonText="Delete" @text="Delete"
@isInDropdown={{true}} @color="critical"
@onConfirmAction={{@delete}} {{on "click" (fn (mut this.showConfirmModal) true)}}
data-test-aws-role-delete={{@item.id}} data-test-aws-role-delete={{@item.id}}
/> />
{{/if}} {{/if}}
{{/if}} {{/if}}
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</LinkedBlock> </LinkedBlock>
{{#if this.showConfirmModal}}
<ConfirmModal @color="critical" @onClose={{fn (mut this.showConfirmModal) false}} @onConfirm={{@delete}} />
{{/if}}

View File

@@ -26,77 +26,56 @@
</LinkTo> </LinkTo>
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
<PopupMenu name="secret-menu"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="navigation for managing database {{@item.id}}"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
@text="Manage database {{@item.id}}"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if @item.canEdit}} {{#if @item.canEdit}}
<li class="action"> <dd.Interactive @text="Edit connection" @route="vault.cluster.secrets.backend.edit" @model={{@item.id}} />
<SecretLink @mode="edit" @secret={{@item.id}} class="has-text-black has-text-weight-semibold">
Edit connection
</SecretLink>
</li>
{{/if}} {{/if}}
{{#if @item.canEditRole}} {{#if @item.canEditRole}}
<li class="action"> <dd.Interactive @text="Edit Role" @route="vault.cluster.secrets.backend.edit" @model={{concat "role/" @item.id}} />
<SecretLink @mode="edit" @secret={{concat "role/" @item.id}} class="has-text-black has-text-weight-semibold">
Edit Role
</SecretLink>
</li>
{{/if}} {{/if}}
{{#if @item.canReset}} {{#if @item.canReset}}
<li class="action"> <dd.Interactive
<Hds::Button
@text="Reset connection" @text="Reset connection"
@color="secondary" @icon={{if (eq this.actionRunning "reset") "loading"}}
class="link"
{{on "click" (fn this.resetConnection @item.id)}} {{on "click" (fn this.resetConnection @item.id)}}
/> />
</li>
{{/if}} {{/if}}
{{#if (and (eq @item.type "dynamic") @item.canGenerateCredentials)}} {{#if (and (eq @item.type "dynamic") @item.canGenerateCredentials)}}
<li class="action"> <dd.Interactive
<LinkTo @text="Generate credentials"
@route="vault.cluster.secrets.backend.credentials" @route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}} @model={{@item.id}}
@query={{hash roleType=this.keyTypeValue}} @query={{hash roleType=this.keyTypeValue}}
> />
Generate credentials
</LinkTo>
</li>
{{else if (and (eq @item.type "static") @item.canGetCredentials)}} {{else if (and (eq @item.type "static") @item.canGetCredentials)}}
<li class="action"> <dd.Interactive
<LinkTo @text="Get credentials"
@route="vault.cluster.secrets.backend.credentials" @route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}} @model={{@item.id}}
@query={{hash roleType=this.keyTypeValue}} @query={{hash roleType=this.keyTypeValue}}
> />
Get credentials
</LinkTo>
</li>
{{/if}} {{/if}}
{{#if (and @item.canRotateRoleCredentials (eq this.keyTypeValue "static"))}} {{#if (and @item.canRotateRoleCredentials (eq this.keyTypeValue "static"))}}
<li class="action"> <dd.Interactive
<Hds::Button
@text="Rotate credentials" @text="Rotate credentials"
@color="secondary" @icon={{if (eq this.actionRunning "rotateRole") "loading"}}
class="link"
{{on "click" (fn this.rotateRoleCred @item.id)}} {{on "click" (fn this.rotateRoleCred @item.id)}}
/> />
</li>
{{/if}} {{/if}}
{{#if @item.canRotateRoot}} {{#if @item.canRotateRoot}}
<li class="action"> <dd.Interactive
<Hds::Button
@text="Rotate root credentials" @text="Rotate root credentials"
@color="secondary" @icon={{if (eq this.actionRunning "rotateRoot") "loading"}}
class="link"
{{on "click" (fn this.rotateRootCred @item.id)}} {{on "click" (fn this.rotateRootCred @item.id)}}
/> />
</li>
{{/if}} {{/if}}
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</LinkedBlock> </LinkedBlock>

View File

@@ -30,56 +30,57 @@
</SecretLink> </SecretLink>
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
<PopupMenu name="secret-menu"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="navigation for managing {{@item.id}}"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
@text="Manage database {{@item.id}}"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if @item.isFolder}} {{#if @item.isFolder}}
<SecretLink @mode="list" @secret={{@item.id}} class="has-text-black has-text-weight-semibold"> <dd.Interactive @text="Contents" @route="vault.cluster.secrets.backend.list" @model={{@item.id}} />
Contents
</SecretLink>
{{else}} {{else}}
{{#if (or @item.versionPath.isLoading @item.secretPath.isLoading)}} {{#if (or @item.versionPath.isLoading @item.secretPath.isLoading)}}
<li class="action"> <dd.Generic class="has-text-center">
<LoadingDropdownOption /> <LoadingDropdownOption />
</li> </dd.Generic>
{{else}} {{else}}
{{#if @item.canRead}} {{#if @item.canRead}}
<li class="action"> <dd.Interactive
<SecretLink @text="Details"
@mode="show" @route="vault.cluster.secrets.backend.show"
@secret={{@item.id}} @model={{@item.id}}
@queryParams={{secret-query-params @backendModel.type @item.type asQueryParams=true}} @query={{secret-query-params @backendModel.type @item.type asQueryParams=true}}
class="has-text-black has-text-weight-semibold" />
>
Details
</SecretLink>
</li>
{{/if}} {{/if}}
{{#if @item.canEdit}} {{#if @item.canEdit}}
<li class="action"> <dd.Interactive
<SecretLink @text="Edit"
@mode="edit" @route="vault.cluster.secrets.backend.edit"
@secret={{@item.id}} @model={{@item.id}}
@queryParams={{secret-query-params @backendModel.type @item.type asQueryParams=true}} @query={{secret-query-params @backendModel.type @item.type asQueryParams=true}}
class="has-text-black has-text-weight-semibold" />
>
Edit
</SecretLink>
</li>
{{/if}} {{/if}}
{{#if @item.canDelete}} {{#if @item.canDelete}}
<ConfirmAction <dd.Interactive
@isInDropdown={{true}} @text="Delete"
@buttonText="Delete" @color="critical"
@confirmMessage="This will permanently delete this secret." data-test-confirm-action-trigger
@onConfirmAction={{@delete}} {{on "click" (fn (mut this.showConfirmModal) true)}}
/> />
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</LinkedBlock> </LinkedBlock>
{{#if this.showConfirmModal}}
<ConfirmModal
@color="critical"
@onClose={{fn (mut this.showConfirmModal) false}}
@confirmMessage="This will permanently delete this secret."
@onConfirm={{@delete}}
/>
{{/if}}

View File

@@ -36,89 +36,86 @@
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
{{#if (eq @backendType "ssh")}} {{#if (eq @backendType "ssh")}}
<PopupMenu @name="role-ssh-nav"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="navigation for managing SSH role {{@item.id}}"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
@text="Manage SSH role {{@item.id}}"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if (eq @item.keyType "otp")}} {{#if (eq @item.keyType "otp")}}
{{#if @item.generatePath.isPending}} {{#if @item.generatePath.isPending}}
<li class="action"> <dd.Generic class="has-text-center">
<Hds::Button disabled @color="tertiary" @icon="loading" @text="loading" @isIconOnly={{true}} /> <LoadingDropdownOption />
</li> </dd.Generic>
{{else if @item.canGenerate}} {{else if @item.canGenerate}}
<li class="action"> <dd.Interactive
<LinkTo @text="Generate credentials"
@route="vault.cluster.secrets.backend.credentials" @route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}} @model={{@item.id}}
data-test-role-ssh-link="generate" data-test-role-ssh-link="generate"
> />
Generate Credentials
</LinkTo>
</li>
{{/if}} {{/if}}
{{else if (eq @item.keyType "ca")}} {{else if (eq @item.keyType "ca")}}
{{#if @item.signPath.isPending}} {{#if @item.signPath.isPending}}
<li class="action"> <dd.Generic class="has-text-center">
<li class="action"> <LoadingDropdownOption />
<Hds::Button disabled @color="tertiary" @icon="loading" @text="loading" @isIconOnly={{true}} /> </dd.Generic>
</li>
</li>
{{else if @item.canGenerate}} {{else if @item.canGenerate}}
<li class="action"> <dd.Interactive
<LinkTo @text="Sign Keys"
@route="vault.cluster.secrets.backend.sign" @route="vault.cluster.secrets.backend.sign"
@model={{@item.id}} @model={{@item.id}}
data-test-role-ssh-link="generate" data-test-role-ssh-link="generate"
>
Sign Keys
</LinkTo>
</li>
{{/if}}
{{/if}}
{{#if @item.canEditZeroAddress}}
<li class="action">
<Hds::Button
disabled={{@loadingToggleZeroAddress}}
class="link"
@icon={{if @loadingToggleZeroAddress "loading"}}
@isIconOnly={{@loadingToggleZeroAddress}}
{{on "click" @toggleZeroAddress}}
@text={{if @item.zeroAddress "Disable Zero Address" "Enable Zero Address"}}
/> />
</li> {{/if}}
{{/if}}
{{#if @loadingToggleZeroAddress}}
<dd.Generic class="has-text-center">
<LoadingDropdownOption />
</dd.Generic>
{{else if @item.canEditZeroAddress}}
<dd.Interactive
@text={{if @item.zeroAddress "Disable Zero Address" "Enable Zero Address"}}
{{on "click" @toggleZeroAddress}}
/>
{{/if}} {{/if}}
{{#if @item.updatePath.isPending}} {{#if @item.updatePath.isPending}}
<li class="action"> <dd.Generic class="has-text-center">
<Hds::Button disabled @color="tertiary" @icon="loading" @text="loading" @isIconOnly={{true}} /> <LoadingDropdownOption />
<Hds::Button disabled @color="tertiary" @icon="loading" @text="loading" @isIconOnly={{true}} /> </dd.Generic>
</li>
{{else}} {{else}}
{{#if @item.canRead}} {{#if @item.canRead}}
<li class="action"> <dd.Interactive
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@item.id}} data-test-role-ssh-link="show"> @text="Details"
Details @route="vault.cluster.secrets.backend.show"
</LinkTo> @model={{@item.id}}
</li> data-test-role-ssh-link="show"
/>
{{/if}} {{/if}}
{{#if @item.canEdit}} {{#if @item.canEdit}}
<li class="action"> <dd.Interactive
<LinkTo @route="vault.cluster.secrets.backend.edit" @model={{@item.id}} data-test-role-ssh-link="edit"> @text="Edit"
Edit @route="vault.cluster.secrets.backend.edit"
</LinkTo> @model={{@item.id}}
</li> data-test-role-ssh-link="edit"
/>
{{/if}} {{/if}}
{{#if @item.canDelete}} {{#if @item.canDelete}}
<ConfirmAction <dd.Interactive
@buttonText="Delete" @text="Delete"
@isInDropdown={{true}} @color="critical"
@onConfirmAction={{@delete}} {{on "click" (fn (mut this.showConfirmModal) true)}}
data-test-ssh-role-delete data-test-ssh-role-delete
/> />
{{/if}} {{/if}}
{{/if}} {{/if}}
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
{{/if}} {{/if}}
</div> </div>
</div> </div>
</LinkedBlock> </LinkedBlock>
{{#if this.showConfirmModal}}
<ConfirmModal @color="critical" @onClose={{fn (mut this.showConfirmModal) false}} @onConfirm={{@delete}} />
{{/if}}

View File

@@ -25,26 +25,20 @@
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
{{#if (or @item.updatePath.canRead @item.updatePath.canUpdate)}} {{#if (or @item.updatePath.canRead @item.updatePath.canUpdate)}}
<PopupMenu name="secret-menu"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="navigation for managing transformation item {{@itemPath}}"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
@text="Manage transform {{@itemType}}"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if @item.updatePath.canRead}} {{#if @item.updatePath.canRead}}
<li class="action"> <dd.Interactive @text="Details" @route="vault.cluster.secrets.backend.show" @model={{@itemPath}} />
<SecretLink @mode="show" @secret={{@itemPath}} class="has-text-black has-text-weight-semibold">
Details
</SecretLink>
</li>
{{/if}} {{/if}}
{{#if @item.updatePath.canUpdate}} {{#if @item.updatePath.canUpdate}}
<li class="action"> <dd.Interactive @text="Edit" @route="vault.cluster.secrets.backend.edit" @model={{@itemPath}} />
<SecretLink @mode="edit" @secret={{@itemPath}} class="has-text-black has-text-weight-semibold">
Edit
</SecretLink>
</li>
{{/if}} {{/if}}
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
{{/if}} {{/if}}
</div> </div>
</div> </div>
@@ -55,17 +49,13 @@
<div class="column is-12 has-text-grey has-text-weight-semibold"> <div class="column is-12 has-text-grey has-text-weight-semibold">
<Icon @name="file" class="has-text-grey-light" /> <Icon @name="file" class="has-text-grey-light" />
{{#if this.isBuiltin}} {{#if this.isBuiltin}}
<ToolTip @verticalPosition="above" @horizontalPosition="left" as |T|> <Hds::TooltipButton
<T.Trigger @tabindex={{false}}> @text="This is a built-in HashiCorp {{@itemType}}. It can't be viewed or edited."
@placement="top-start"
aria-label="Why this item cannot be viewed or edited"
>
{{@item.id}} {{@item.id}}
</T.Trigger> </Hds::TooltipButton>
<T.Content @defaultClass="tool-tip">
<div class="box">
This is a built-in HashiCorp
{{@itemType}}. It can't be viewed or edited.
</div>
</T.Content>
</ToolTip>
{{else}} {{else}}
{{@item.id}} {{@item.id}}
{{/if}} {{/if}}

View File

@@ -3,15 +3,13 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
{{! CBS TODO do not let click if !canRead }} <LinkedBlock
{{#if (eq @options.item "transformation")}}
<LinkedBlock
@params={{array "vault.cluster.secrets.backend.show" @item.id}} @params={{array "vault.cluster.secrets.backend.show" @item.id}}
class="list-item-row" class="list-item-row"
data-test-secret-link={{@item.id}} data-test-secret-link={{@item.id}}
@encode={{true}} @encode={{true}}
@queryParams={{secret-query-params @backendModel.type}} @queryParams={{secret-query-params @backendModel.type}}
> >
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column is-10"> <div class="column is-10">
<SecretLink <SecretLink
@@ -26,43 +24,21 @@
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
{{#if (or @item.updatePath.canRead @item.updatePath.canUpdate)}} {{#if (or @item.updatePath.canRead @item.updatePath.canUpdate)}}
<PopupMenu name="secret-menu" aria-label={{concat "navigation for managing transformation " @item.id}}> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
{{#if (or @item.versionPath.isLoading @item.secretPath.isLoading)}} @text="Manage transformation"
<li class="action"> @hasChevron={{false}}
<LoadingDropdownOption /> data-test-popup-menu-trigger
</li> />
{{else}}
{{#if @item.updatePath.canRead}} {{#if @item.updatePath.canRead}}
<li class="action"> <dd.Interactive @text="Details" @route="vault.cluster.secrets.backend.show" @model={{@item.id}} />
<SecretLink @mode="show" @secret={{@item.id}} class="has-text-black has-text-weight-semibold">
Details
</SecretLink>
</li>
{{/if}} {{/if}}
{{#if @item.updatePath.canUpdate}} {{#if @item.updatePath.canUpdate}}
<li class="action"> <dd.Interactive @text="Edit" @route="vault.cluster.secrets.backend.edit" @model={{@item.id}} />
<SecretLink @mode="edit" @secret={{@item.id}} class="has-text-black has-text-weight-semibold">
Edit
</SecretLink>
</li>
{{/if}} {{/if}}
{{/if}} </Hds::Dropdown>
</ul>
</nav>
</PopupMenu>
{{/if}} {{/if}}
</div> </div>
</div> </div>
</LinkedBlock> </LinkedBlock>
{{else}}
<div class="list-item-row">
<div class="columns is-mobile">
<div class="column is-12 has-text-grey has-text-weight-semibold">
<Icon @name="file" class="has-text-grey-light" />
{{if (eq @item.id " ") "(self)" (or @item.keyWithoutParent @item.id)}}
</div>
</div>
</div>
{{/if}}

View File

@@ -151,21 +151,15 @@
</div> </div>
</div> </div>
<div class="column is-1 is-flex-end"> <div class="column is-1 is-flex-end">
<PopupMenu name="secret-menu"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="copy public key"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li class="action"> @text="Public key options"
<Hds::Copy::Button @hasChevron={{false}}
@text="Copy Public Key" data-test-popup-menu-trigger
@textToCopy={{meta.public_key}}
@isFullWidth={{true}}
class="in-dropdown link is-flex-start"
{{on "click" (action (set-flash-message "Public key copied!"))}}
/> />
</li> <dd.CopyItem @text={{meta.public_key}} @copyItemTitle="Copy Public Key" />
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,15 +5,10 @@
<div class="wizard-header"> <div class="wizard-header">
{{#unless this.hidePopup}} {{#unless this.hidePopup}}
<PopupMenu @class="wizard-dismiss-menu"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="navigation for wizard content"> <dd.ToggleIcon @icon="more-horizontal" @text="Wizard dismiss menu" @hasChevron={{false}} class="wizard-dismiss-menu" />
<ul class="menu-list"> <dd.Interactive @text="Dismiss" {{on "click" (action "dismissWizard")}} />
<li class="action"> </Hds::Dropdown>
<Hds::Button @text="Dismiss" @color="secondary" class="link" {{on "click" (action "dismissWizard")}} />
</li>
</ul>
</nav>
</PopupMenu>
{{/unless}} {{/unless}}
<h1 class="title is-5"> <h1 class="title is-5">
<Icon @name={{this.glyph}} /> <Icon @name={{this.glyph}} />

View File

@@ -31,7 +31,7 @@
</span> </span>
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
<Identity::PopupAlias @params={{array item}} @onSuccess={{action "onDelete"}} /> <Identity::PopupAlias @item={{item}} @onSuccess={{action "onDelete"}} />
</div> </div>
</div> </div>
</LinkedBlock> </LinkedBlock>

View File

@@ -9,7 +9,7 @@
<LinkedBlock <LinkedBlock
@params={{array "vault.cluster.access.identity.show" item.id "details"}} @params={{array "vault.cluster.access.identity.show" item.id "details"}}
class="list-item-row" class="list-item-row"
data-test-identity-row data-test-identity-row={{item.name}}
> >
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column is-7-tablet is-10-mobile"> <div class="column is-7-tablet is-10-mobile">
@@ -32,63 +32,53 @@
{{/if}} {{/if}}
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
<PopupMenu @name="identity-item" @onOpen={{action "reloadRecord" item}}> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="navigation for managing identity"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li class="action"> @text="Identity management options"
<LinkTo @route="vault.cluster.access.identity.show" @models={{array item.id "details"}}> @hasChevron={{false}}
Details {{on "click" (action "reloadRecord" item)}}
</LinkTo> data-test-popup-menu-trigger
</li> />
<dd.Interactive
@text="Details"
@route="vault.cluster.access.identity.show"
@models={{array item.id "details"}}
/>
{{#if (or item.isReloading item.updatePath.isPending item.aliasPath.isPending)}} {{#if (or item.isReloading item.updatePath.isPending item.aliasPath.isPending)}}
<li class="action"> <dd.Generic class="has-text-center">
<LoadingDropdownOption /> <LoadingDropdownOption />
</li> </dd.Generic>
{{else}} {{else}}
{{#if item.canAddAlias}} {{#if item.canAddAlias}}
<li class="action"> {{! entities can always add aliases, internal groups cannot have any and external groups can only have one }}
<LinkTo {{#if (or (eq this.identityType "entity") (and (eq item.type "external") (not item.alias)))}}
<dd.Interactive
data-test-popup-menu="create alias"
@text="Create alias"
@route="vault.cluster.access.identity.aliases.add" @route="vault.cluster.access.identity.aliases.add"
@models={{array (pluralize this.identityType) item.id}} @models={{array (pluralize this.identityType) item.id}}
> />
Create alias {{/if}}
</LinkTo>
</li>
{{/if}} {{/if}}
{{#if item.canEdit}} {{#if item.canEdit}}
<li class="action"> <dd.Interactive @text="Edit" @route="vault.cluster.access.identity.edit" @model={{item.id}} />
<LinkTo @route="vault.cluster.access.identity.edit" @model={{item.id}}>
Edit
</LinkTo>
</li>
<li class="action">
{{#if item.disabled}} {{#if item.disabled}}
<Hds::Button @text="Enable" {{on "click" (action "toggleDisabled" item)}} class="link" /> <dd.Interactive @text="Enable" {{on "click" (action "toggleDisabled" item)}} />
{{else if (eq this.identityType "entity")}} {{else if (eq this.identityType "entity")}}
<ConfirmAction <dd.Interactive @text="Disable" @color="critical" {{on "click" (fn (mut this.entityToDisable) item)}} />
@isInDropdown={{true}}
@buttonText="Disable"
@confirmMessage="Associated tokens will not be revoked, but cannot be used"
@confirmTitle="Disable this entity?"
@onConfirmAction={{action "toggleDisabled" item}}
@modalColor="warning"
/>
{{/if}} {{/if}}
</li>
{{/if}} {{/if}}
{{#if item.canDelete}} {{#if item.canDelete}}
<ConfirmAction <dd.Interactive
@isInDropdown={{true}} @text="Delete"
@buttonText="Delete" @color="critical"
@onConfirmAction={{action "delete" item}} {{on "click" (fn (mut this.itemToDelete) item)}}
@confirmTitle="Delete this {{this.identityType}}?" data-test-popup-menu="delete"
data-test-item-delete
/> />
{{/if}} {{/if}}
{{/if}} {{/if}}
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</LinkedBlock> </LinkedBlock>
@@ -118,3 +108,21 @@
/> />
</EmptyState> </EmptyState>
{{/if}} {{/if}}
{{#if this.entityToDisable}}
<ConfirmModal
@confirmMessage="Associated tokens will not be revoked, but cannot be used."
@confirmTitle="Disable this entity?"
@onConfirm={{action "toggleDisabled" this.entityToDisable}}
@onClose={{fn (mut this.entityToDisable) null}}
/>
{{/if}}
{{#if this.itemToDelete}}
<ConfirmModal
@color="critical"
@confirmTitle="Delete this {{this.identityType}}?"
@onConfirm={{action "delete" this.itemToDelete}}
@onClose={{fn (mut this.itemToDelete) null}}
/>
{{/if}}

View File

@@ -83,17 +83,20 @@
{{#if target.link}} {{#if target.link}}
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="Enforcement target more menu"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li> @text="Manage enforcement target"
<LinkTo @route={{target.link}} @models={{target.linkModels}} data-test-target-link={{target.title}}> @hasChevron={{false}}
Details data-test-popup-menu-trigger
</LinkTo> />
</li> <dd.Interactive
</ul> @text="Details"
</nav> @route={{target.link}}
</PopupMenu> @models={{target.linkModels}}
data-test-target-link={{target.title}}
/>
</Hds::Dropdown>
</div> </div>
</div> </div>
{{/if}} {{/if}}

View File

@@ -38,32 +38,28 @@
{{#if (not-eq model.name "allow_all")}} {{#if (not-eq model.name "allow_all")}}
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li> @text="Assignment nav options"
<LinkTo @hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive
@text="Details"
@route="vault.cluster.access.oidc.assignments.assignment.details" @route="vault.cluster.access.oidc.assignments.assignment.details"
@model={{model.name}} @model={{model.name}}
@disabled={{eq model.canRead false}} @disabled={{eq model.canRead false}}
data-test-oidc-assignment-menu-link="details" data-test-oidc-assignment-menu-link="details"
> />
Details <dd.Interactive
</LinkTo> @text="Edit"
</li>
<li>
<LinkTo
@route="vault.cluster.access.oidc.assignments.assignment.edit" @route="vault.cluster.access.oidc.assignments.assignment.edit"
@model={{model.name}} @model={{model.name}}
@disabled={{eq model.canEdit false}} @disabled={{eq model.canEdit false}}
data-test-oidc-assignment-menu-link="edit" data-test-oidc-assignment-menu-link="edit"
> />
Edit </Hds::Dropdown>
</LinkTo>
</li>
</ul>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
{{/if}} {{/if}}

View File

@@ -28,32 +28,28 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li> @text="Key nav options"
<LinkTo @hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive
@text="Details"
@route="vault.cluster.access.oidc.keys.key.details" @route="vault.cluster.access.oidc.keys.key.details"
@model={{model.name}} @model={{model.name}}
@disabled={{eq model.canRead false}} @disabled={{eq model.canRead false}}
data-test-oidc-key-menu-link="details" data-test-oidc-key-menu-link="details"
> />
Details <dd.Interactive
</LinkTo> @text="Edit"
</li>
<li>
<LinkTo
@route="vault.cluster.access.oidc.keys.key.edit" @route="vault.cluster.access.oidc.keys.key.edit"
@model={{model.name}} @model={{model.name}}
@disabled={{eq model.canEdit false}} @disabled={{eq model.canEdit false}}
data-test-oidc-key-menu-link="edit" data-test-oidc-key-menu-link="edit"
> />
Edit </Hds::Dropdown>
</LinkTo>
</li>
</ul>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -29,32 +29,28 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li> @text="Assignment nav options"
<LinkTo @hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive
@text="Details"
@route="vault.cluster.access.oidc.scopes.scope.details" @route="vault.cluster.access.oidc.scopes.scope.details"
@model={{model.name}} @model={{model.name}}
@disabled={{eq model.canRead false}} @disabled={{eq model.canRead false}}
data-test-oidc-scope-menu-link="details" data-test-oidc-scope-menu-link="details"
> />
Details <dd.Interactive
</LinkTo> @text="Edit"
</li>
<li>
<LinkTo
@route="vault.cluster.access.oidc.scopes.scope.edit" @route="vault.cluster.access.oidc.scopes.scope.edit"
@model={{model.name}} @model={{model.name}}
@disabled={{eq model.canEdit false}} @disabled={{eq model.canEdit false}}
data-test-oidc-scope-menu-link="edit" data-test-oidc-scope-menu-link="edit"
> />
Edit </Hds::Dropdown>
</LinkTo>
</li>
</ul>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -91,52 +91,44 @@
</LinkTo> </LinkTo>
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
<PopupMenu name="policy-nav"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
@text="Policy nav menu"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if item.updatePath.isPending}} {{#if item.updatePath.isPending}}
<li class="action"> <dd.Generic class="has-text-center">
<LoadingDropdownOption /> <LoadingDropdownOption />
</li> </dd.Generic>
<li class="action">
<LoadingDropdownOption />
</li>
{{else}} {{else}}
{{#if item.canRead}} {{#if item.canRead}}
<li class="action"> <dd.Interactive
<LinkTo @text="Details"
@route="vault.cluster.policy.show" @route="vault.cluster.policy.show"
@models={{array this.policyType item.id}} @models={{array this.policyType item.id}}
data-test-policy-link="show" data-test-policy-link="show"
> />
Details
</LinkTo>
</li>
{{/if}} {{/if}}
{{#if item.canEdit}} {{#if item.canEdit}}
<li class="action"> <dd.Interactive
<LinkTo @text="Edit"
@route="vault.cluster.policy.edit" @route="vault.cluster.policy.edit"
@models={{array this.policyType item.id}} @models={{array this.policyType item.id}}
data-test-policy-link="edit" data-test-policy-link="edit"
> />
Edit
</LinkTo>
</li>
{{/if}} {{/if}}
{{#if item.canDelete}} {{#if (and item.canDelete (not-eq item.name "default"))}}
<ConfirmAction <dd.Interactive
@isInDropdown={{true}} @text="Delete"
@buttonText="Delete" @color="critical"
@confirmTitle="Delete this policy?" data-test-confirm-action-trigger
@confirmMessage="This will permanently delete this policy and may affect access to some data" {{on "click" (fn (mut this.policyToDelete) item)}}
@onConfirmAction={{action "deletePolicy" item}}
/> />
{{/if}} {{/if}}
{{/if}} {{/if}}
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</LinkedBlock> </LinkedBlock>
@@ -181,3 +173,13 @@
{{else}} {{else}}
<UpgradePage @title="Sentinel" @minimumEdition="Vault Enterprise Premium" /> <UpgradePage @title="Sentinel" @minimumEdition="Vault Enterprise Premium" />
{{/if}} {{/if}}
{{#if this.policyToDelete}}
<ConfirmModal
@color="critical"
@confirmTitle="Delete this policy?"
@confirmMessage="This will permanently delete this policy and may affect access to some data."
@onClose={{fn (mut this.policyToDelete) null}}
@onConfirm={{action "deletePolicy" this.policyToDelete}}
/>
{{/if}}

View File

@@ -86,28 +86,39 @@
</ReadMore> </ReadMore>
{{/if}} {{/if}}
</div> </div>
{{! meatball sandwich menu }}
<div class="linked-block-popup-menu"> <div class="linked-block-popup-menu">
<PopupMenu @name="engine-menu"> <Hds::Dropdown @isInline={{true}} as |dd|>
<nav class="menu" aria-label="{{if backend.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li class="action"> @text="{{if backend.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
<LinkTo @route="vault.cluster.secrets.backend.configuration" @model={{backend.id}} data-test-engine-config> @hasChevron={{false}}
View configuration data-test-popup-menu-trigger
</LinkTo> />
</li> <dd.Interactive
@text="View configuration"
@route="vault.cluster.secrets.backend.configuration"
@model={{backend.id}}
data-test-engine-config
/>
{{#if (not-eq backend.type "cubbyhole")}} {{#if (not-eq backend.type "cubbyhole")}}
<ConfirmAction <dd.Interactive
@isInDropdown={{true}} @text="Disable"
@confirmMessage="Any data in this engine will be permanently deleted." @color="critical"
@confirmTitle="Disable engine?" {{on "click" (fn (mut this.engineToDisable) backend)}}
@buttonText="Disable" data-test-confirm-action-trigger
@onConfirmAction={{perform this.disableEngine backend}}
/> />
{{/if}} {{/if}}
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
</div> </div>
</LinkedBlock> </LinkedBlock>
{{/each}} {{/each}}
{{#if this.engineToDisable}}
<ConfirmModal
@color="critical"
@confirmMessage="Any data in this engine will be permanently deleted."
@confirmTitle="Disable engine?"
@onClose={{fn (mut this.engineToDisable) null}}
@onConfirm={{perform this.disableEngine this.engineToDisable}}
/>
{{/if}}

View File

@@ -56,28 +56,22 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu @name="engine-menu"> {{#if (or message.canEditCustomMessages message.canDeleteCustomMessages)}}
<nav class="menu"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<ul class="menu-list"> <dd.ToggleIcon
@icon="more-horizontal"
@text="Message popup menu"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if message.canEditCustomMessages}} {{#if message.canEditCustomMessages}}
<li class="action"> <dd.Interactive @text="Edit" @route="messages.message.edit" @model={{message.id}} />
<LinkTo @route="messages.message.edit" @model={{message.id}}>
Edit
</LinkTo>
</li>
{{/if}} {{/if}}
{{#if message.canDeleteCustomMessages}} {{#if message.canDeleteCustomMessages}}
<ConfirmAction <dd.Interactive @text="Disable" @color="critical" {{on "click" (fn (mut this.messageToDelete) message)}} />
@isInDropdown={{true}} {{/if}}
@buttonText="Delete" </Hds::Dropdown>
@confirmTitle="Are you sure?"
@confirmMessage="This will delete this message permanently. You cannot undo this action."
@onConfirmAction={{perform this.deleteMessage message}}
/>
{{/if}} {{/if}}
</ul>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>
@@ -118,3 +112,12 @@
</M.Footer> </M.Footer>
</Hds::Modal> </Hds::Modal>
{{/if}} {{/if}}
{{#if this.messageToDelete}}
<ConfirmModal
@color="critical"
@confirmMessage="This will delete this message permanently. You cannot undo this action."
@onClose={{fn (mut this.messageToDelete) null}}
@onConfirm={{perform this.deleteMessage this.messageToDelete}}
/>
{{/if}}

View File

@@ -30,6 +30,7 @@ export default class MessagesList extends Component {
@service customMessages; @service customMessages;
@tracked showMaxMessageModal = false; @tracked showMaxMessageModal = false;
@tracked messageToDelete = null;
// This follows the pattern in sync/addon/components/secrets/page/destinations for FilterInput. // This follows the pattern in sync/addon/components/secrets/page/destinations for FilterInput.
// Currently, FilterInput doesn't do a full page refresh causing it to lose focus. // Currently, FilterInput doesn't do a full page refresh causing it to lose focus.
@@ -110,6 +111,8 @@ export default class MessagesList extends Component {
} catch (e) { } catch (e) {
const message = errorMessage(e); const message = errorMessage(e);
this.flashMessages.danger(message); this.flashMessages.danger(message);
} finally {
this.messageToDelete = null;
} }
} }

View File

@@ -25,44 +25,13 @@
{{/if}} {{/if}}
{{#if this.showConfirmModal}} {{#if this.showConfirmModal}}
<Hds::Modal <ConfirmModal
id="confirm-action-modal"
class="has-text-left"
@color={{this.modalColor}} @color={{this.modalColor}}
@size="small"
@onClose={{fn (mut this.showConfirmModal) false}} @onClose={{fn (mut this.showConfirmModal) false}}
as |M| @onConfirm={{this.onConfirm}}
> @confirmTitle={{@confirmTitle}}
{{#if @disabledMessage}} @confirmMessage={{this.confirmMessage}}
<M.Header data-test-confirm-action-title @icon="x-circle"> @disabledMessage={{@disabledMessage}}
Not allowed @isRunning={{@isRunning}}
</M.Header>
<M.Body data-test-confirm-action-message>
{{@disabledMessage}}
</M.Body>
<M.Footer as |F|>
<Hds::Button data-test-confirm-cancel-button @text="Close" {{on "click" F.close}} />
</M.Footer>
{{else}}
<M.Header data-test-confirm-action-title @icon="alert-circle">
{{or @confirmTitle "Are you sure?"}}
</M.Header>
<M.Body data-test-confirm-action-message>
{{this.confirmMessage}}
</M.Body>
<M.Footer as |F|>
<Hds::ButtonSet>
<Hds::Button
data-test-confirm-button
disabled={{@isRunning}}
@icon={{if @isRunning "loading"}}
@color={{if (eq this.modalColor "critical") "critical" "primary"}}
@text="Confirm"
{{on "click" this.onConfirm}}
/> />
<Hds::Button data-test-confirm-cancel-button @color="secondary" @text="Cancel" {{on "click" F.close}} />
</Hds::ButtonSet>
</M.Footer>
{{/if}}
</Hds::Modal>
{{/if}} {{/if}}

View File

@@ -0,0 +1,51 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{! Replaces ConfirmAction in dropdowns, instead use dd.Interactive + this modal }}
{{! Destructive action confirmation modal that asks "Are you sure?" or similar @confirmTitle }}
{{! If a tracked property is used to pass the list item to the destructive action, }}
{{! remember to reset item to null via the @onClose action }}
<Hds::Modal
id="confirm-action-modal"
class="has-text-left"
@color={{or @color "warning"}}
@size="small"
@onClose={{@onClose}}
data-test-confirm-modal
as |M|
>
{{#if @disabledMessage}}
<M.Header data-test-confirm-action-title @icon="x-circle">
Not allowed
</M.Header>
<M.Body data-test-confirm-action-message>
{{@disabledMessage}}
</M.Body>
<M.Footer as |F|>
<Hds::Button data-test-confirm-cancel-button @text="Close" {{on "click" F.close}} />
</M.Footer>
{{else}}
<M.Header data-test-confirm-action-title @icon="alert-circle">
{{or @confirmTitle "Are you sure?"}}
</M.Header>
<M.Body data-test-confirm-action-message>
{{or @confirmMessage "You will not be able to recover it later."}}
</M.Body>
<M.Footer as |F|>
<Hds::ButtonSet>
<Hds::Button
data-test-confirm-button
disabled={{@isRunning}}
@icon={{if @isRunning "loading"}}
@color={{if (eq @color "critical") "critical" "primary"}}
@text="Confirm"
{{on "click" @onConfirm}}
/>
<Hds::Button data-test-confirm-cancel-button @color="secondary" @text="Cancel" {{on "click" F.close}} />
</Hds::ButtonSet>
</M.Footer>
{{/if}}
</Hds::Modal>

View File

@@ -5,7 +5,8 @@
import Component from '@glimmer/component'; import Component from '@glimmer/component';
/** /**
* @module ConfirmationModal * @module ConfirmationModal
* ConfirmationModal components wrap the <Hds::Modal> component to present a critical (red) type-to-confirm modal. * ConfirmationModal components wrap the <Hds::Modal> component to present a critical (red) type-to-confirm modal
* which require the user to type something to confirm the action.
* They are used for extremely destructive actions that require extra consideration before confirming. * They are used for extremely destructive actions that require extra consideration before confirming.
* *
* @example * @example

View File

@@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'core/components/confirm-modal';

View File

@@ -71,57 +71,55 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="menu items for managing {{metadata.path}}"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
@text="Manage secret"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if metadata.pathIsDirectory}} {{#if metadata.pathIsDirectory}}
<li> <dd.Interactive @text="Content" @route="list-directory" @model={{metadata.fullSecretPath}} />
<LinkTo @route="list-directory" @model={{metadata.fullSecretPath}}>
Content
</LinkTo>
</li>
{{else}} {{else}}
<li> <dd.Interactive @text="Details" @route="secret.details" @model={{metadata.fullSecretPath}} />
<LinkTo @route="secret.details" @model={{metadata.fullSecretPath}}>
Details
</LinkTo>
</li>
{{#if metadata.canReadMetadata}} {{#if metadata.canReadMetadata}}
<li> <dd.Interactive
<LinkTo @route="secret.metadata.versions" @model={{metadata.fullSecretPath}}> @text="View version history"
View version history @route="secret.metadata.versions"
</LinkTo> @model={{metadata.fullSecretPath}}
</li> />
{{/if}} {{/if}}
{{#if metadata.canCreateVersionData}} {{#if metadata.canCreateVersionData}}
<li> <dd.Interactive
<LinkTo @text="Create new version"
@route="secret.details.edit" @route="secret.details.edit"
@model={{metadata.fullSecretPath}} @model={{metadata.fullSecretPath}}
data-test-popup-create-new-version data-test-popup-create-new-version
> />
Create new version
</LinkTo>
</li>
{{/if}} {{/if}}
{{#if metadata.canDeleteMetadata}} {{#if metadata.canDeleteMetadata}}
<ConfirmAction <dd.Interactive
@buttonText="Permanently delete" @text="Permanently delete"
@isInDropdown={{true}} @color="critical"
@onConfirmAction={{fn this.onDelete metadata}} {{on "click" (fn (mut this.metadataToDelete) metadata)}}
@confirmMessage="This will permanently delete this secret and all its versions."
data-test-popup-metadata-delete data-test-popup-metadata-delete
/> />
{{/if}} {{/if}}
{{/if}} {{/if}}
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>
</LinkedBlock> </LinkedBlock>
{{/each}} {{/each}}
{{#if this.metadataToDelete}}
<ConfirmModal
@color="critical"
@onClose={{fn (mut this.metadataToDelete) null}}
@onConfirm={{fn this.onDelete this.metadataToDelete}}
@confirmMessage="This will permanently delete this secret and all its versions."
/>
{{/if}}
{{! Pagination }} {{! Pagination }}
<Hds::Pagination::Numbered <Hds::Pagination::Numbered
@currentPage={{@secrets.meta.currentPage}} @currentPage={{@secrets.meta.currentPage}}

View File

@@ -30,6 +30,7 @@ export default class KvListPageComponent extends Component {
@service store; @service store;
@tracked secretPath; @tracked secretPath;
@tracked metadataToDelete = null; // set to the metadata intended to delete
get mountPoint() { get mountPoint() {
// mountPoint tells transition where to start. In this case, mountPoint will always be vault.cluster.secrets.backend.kv. // mountPoint tells transition where to start. In this case, mountPoint will always be vault.cluster.secrets.backend.kv.
@@ -71,6 +72,8 @@ export default class KvListPageComponent extends Component {
} catch (error) { } catch (error) {
const message = errorMessage(error, 'Error deleting secret. Please try again or contact support.'); const message = errorMessage(error, 'Error deleting secret. Please try again or contact support.');
this.flashMessages.danger(message); this.flashMessages.danger(message);
} finally {
this.metadataToDelete = null;
} }
} }

View File

@@ -70,31 +70,27 @@
<div class="level-right"> <div class="level-right">
<div class="level-item"> <div class="level-item">
<PopupMenu @name="version-{{versionData.version}}"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li> @text="Manage version"
<LinkTo @route="secret.details" @query={{hash version=versionData.version}}> @hasChevron={{false}}
View version data-test-popup-menu-trigger
{{versionData.version}} />
</LinkTo> <dd.Interactive
</li> @text="View version {{versionData.version}}"
{{#if @metadata.canCreateVersionData}} @route="secret.details"
<li> @query={{hash version=versionData.version}}
<LinkTo />
{{#if (and @metadata.canCreateVersionData (not versionData.destroyed) (not versionData.isSecretDeleted))}}
<dd.Interactive
@text="Create new version from {{versionData.version}}"
@route="secret.details.edit" @route="secret.details.edit"
@query={{hash version=versionData.version}} @query={{hash version=versionData.version}}
data-test-create-new-version-from={{versionData.version}} data-test-create-new-version-from={{versionData.version}}
@disabled={{or versionData.destroyed versionData.isSecretDeleted}} />
>
Create new version from
{{versionData.version}}
</LinkTo>
</li>
{{/if}} {{/if}}
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -84,22 +84,21 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu" aria-label="issuer config options"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li data-test-popup-menu-details> @text="Manage issuer"
<LinkTo @route="issuers.issuer.details" @model={{pkiIssuer.id}}> @hasChevron={{false}}
Details data-test-popup-menu-trigger
</LinkTo> />
</li> <dd.Interactive
<li> @text="Details"
<LinkTo @route="issuers.issuer.edit" @model={{pkiIssuer.id}}> @route="issuers.issuer.details"
Edit @model={{pkiIssuer.id}}
</LinkTo> data-test-popup-menu-details
</li> />
</ul> <dd.Interactive @text="Edit" @route="issuers.issuer.edit" @model={{pkiIssuer.id}} />
</nav> </Hds::Dropdown>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -40,32 +40,32 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> {{#if (or @canRead @canEdit)}}
<nav class="menu"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<ul class="menu-list"> <dd.ToggleIcon
<li> @icon="more-horizontal"
<LinkTo @text="Manage key"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if @canRead}}
<dd.Interactive
@text="Details"
@route="keys.key.details" @route="keys.key.details"
@model={{pkiKey.keyId}} @model={{pkiKey.keyId}}
@disabled={{not @canRead}}
data-test-key-menu-link="details" data-test-key-menu-link="details"
> />
Details {{/if}}
</LinkTo> {{#if @canEdit}}
</li> <dd.Interactive
<li> @text="Edit"
<LinkTo
@route="keys.key.edit" @route="keys.key.edit"
@model={{pkiKey.keyId}} @model={{pkiKey.keyId}}
@disabled={{not @canEdit}}
data-test-key-menu-link="edit" data-test-key-menu-link="edit"
> />
Edit {{/if}}
</LinkTo> </Hds::Dropdown>
</li> {{/if}}
</ul>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -26,17 +26,15 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li> @text="Manage certificate"
<LinkTo @route="certificates.certificate.details" @model={{pkiCertificate.id}}> @hasChevron={{false}}
Details data-test-popup-menu-trigger
</LinkTo> />
</li> <dd.Interactive @text="Details" @route="certificates.certificate.details" @model={{pkiCertificate.id}} />
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -27,22 +27,16 @@
</div> </div>
<div class="level-right is-flex is-paddingless is-marginless"> <div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item"> <div class="level-item">
<PopupMenu> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
<li> @text="Manage role"
<LinkTo @route="roles.role.details" @model={{pkiRole.id}}> @hasChevron={{false}}
Details data-test-popup-menu-trigger
</LinkTo> />
</li> <dd.Interactive @text="Details" @route="roles.role.details" @model={{pkiRole.id}} />
<li> <dd.Interactive @text="Edit" @route="roles.role.edit" @model={{pkiRole.id}} />
<LinkTo @route="roles.role.edit" @model={{pkiRole.id}}> </Hds::Dropdown>
Edit
</LinkTo>
</li>
</ul>
</nav>
</PopupMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -31,6 +31,7 @@ export default Controller.extend(copy(DEFAULTS, true), {
store: service(), store: service(),
rm: service('replication-mode'), rm: service('replication-mode'),
replicationMode: alias('rm.mode'), replicationMode: alias('rm.mode'),
secondaryToRevoke: null,
submitError(e) { submitError(e) {
if (e.errors) { if (e.errors) {
@@ -114,7 +115,8 @@ export default Controller.extend(copy(DEFAULTS, true), {
}); });
}, },
(...args) => this.submitError(...args) (...args) => this.submitError(...args)
); )
.finally(() => this.set('secondaryToRevoke', null));
}, },
actions: { actions: {

View File

@@ -29,34 +29,29 @@
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
{{#if (or (eq this.replicationMode "performance") this.model.canRevokeSecondary)}} {{#if (or (eq this.replicationMode "performance") this.model.canRevokeSecondary)}}
<PopupMenu @name="secondary-details"> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<nav class="menu"> <dd.ToggleIcon
<ul class="menu-list"> @icon="more-horizontal"
@text="Secondary popup nav menu"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if (eq this.replicationMode "performance")}} {{#if (eq this.replicationMode "performance")}}
<li class="action"> <dd.Interactive
<LinkTo @text="Path filter config"
@route="mode.secondaries.config-show" @route="mode.secondaries.config-show"
@models={{array this.replicationMode secondary}} @models={{array this.replicationMode secondary}}
data-test-replication-path-filter-link={{true}} data-test-replication-path-filter-link={{true}}
> />
Path filter config
</LinkTo>
</li>
{{/if}} {{/if}}
{{#if this.model.canRevokeSecondary}} {{#if this.model.canRevokeSecondary}}
<li class="action"> <dd.Interactive
<ConfirmAction @text="Revoke"
@buttonText="Revoke" @color="critical"
@isInDropdown={{true}} {{on "click" (fn (mut this.secondaryToRevoke) secondary)}}
@confirmTitle="Revoke token?"
@confirmMessage="This will revoke this secondary token."
@onConfirmAction={{action "onSubmit" "revoke-secondary" "primary" (hash id=secondary)}}
/> />
</li>
{{/if}} {{/if}}
</ul> </Hds::Dropdown>
</nav>
</PopupMenu>
{{/if}} {{/if}}
</div> </div>
</div> </div>
@@ -77,3 +72,13 @@
</EmptyState> </EmptyState>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{#if this.secondaryToRevoke}}
<ConfirmModal
@color="critical"
@confirmTitle="Revoke token?"
@confirmMessage="This will revoke this secondary token."
@onClose={{fn (mut this.secondaryToRevoke) null}}
@onConfirm={{action "onSubmit" "revoke-secondary" "primary" (hash id=this.secondaryToRevoke)}}
/>
{{/if}}

View File

@@ -90,7 +90,7 @@
{{/if}} {{/if}}
</B.Td> </B.Td>
<B.Td @align="right"> <B.Td @align="right">
<Hds::Dropdown @isInline={{true}} as |dd|> <Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon <dd.ToggleIcon
@icon="more-horizontal" @icon="more-horizontal"
@text="Actions" @text="Actions"

View File

@@ -8,7 +8,11 @@ import { selectChoose, clickTrigger } from 'ember-power-select/test-support/help
import page from 'vault/tests/pages/access/identity/create'; import page from 'vault/tests/pages/access/identity/create';
import showPage from 'vault/tests/pages/access/identity/show'; import showPage from 'vault/tests/pages/access/identity/show';
import indexPage from 'vault/tests/pages/access/identity/index'; import indexPage from 'vault/tests/pages/access/identity/index';
const SELECTORS = {
identityRow: (name) => `[data-test-identity-row="${name}"]`,
popupMenu: '[data-test-popup-menu-trigger]',
menuDelete: '[data-test-popup-menu="delete"]',
};
export const testCRUD = async (name, itemType, assert) => { export const testCRUD = async (name, itemType, assert) => {
await page.visit({ item_type: itemType }); await page.visit({ item_type: itemType });
await settled(); await settled();
@@ -24,7 +28,6 @@ export const testCRUD = async (name, itemType, assert) => {
`${itemType}: navigates to show on create` `${itemType}: navigates to show on create`
); );
assert.ok(showPage.nameContains(name), `${itemType}: renders the name on the show page`); assert.ok(showPage.nameContains(name), `${itemType}: renders the name on the show page`);
await indexPage.visit({ item_type: itemType }); await indexPage.visit({ item_type: itemType });
await settled(); await settled();
assert.strictEqual( assert.strictEqual(
@@ -32,10 +35,10 @@ export const testCRUD = async (name, itemType, assert) => {
1, 1,
`${itemType}: lists the entity in the entity list` `${itemType}: lists the entity in the entity list`
); );
await indexPage.items.filterBy('name', name)[0].menu();
await waitUntil(() => find('[data-test-item-delete]')); await click(`${SELECTORS.identityRow(name)} ${SELECTORS.popupMenu}`);
await indexPage.delete(); await waitUntil(() => find(SELECTORS.menuDelete));
await settled(); await click(SELECTORS.menuDelete);
await indexPage.confirmDelete(); await indexPage.confirmDelete();
await settled(); await settled();
assert.ok( assert.ok(

View File

@@ -3,12 +3,22 @@
* SPDX-License-Identifier: BUSL-1.1 * SPDX-License-Identifier: BUSL-1.1
*/ */
import { currentRouteName } from '@ember/test-helpers'; import { fillIn, click, currentRouteName, currentURL, visit } from '@ember/test-helpers';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit'; import { setupApplicationTest } from 'ember-qunit';
import page from 'vault/tests/pages/access/identity/index'; import page from 'vault/tests/pages/access/identity/index';
import authPage from 'vault/tests/pages/auth'; import authPage from 'vault/tests/pages/auth';
import { runCmd } from 'vault/tests/helpers/commands';
import { SELECTORS as GENERAL } from 'vault/tests/helpers/general-selectors';
import { v4 as uuidv4 } from 'uuid';
const SELECTORS = {
listItem: (name) => `[data-test-identity-row="${name}"]`,
menu: `[data-test-popup-menu-trigger]`,
menuItem: (element) => `[data-test-popup-menu="${element}"]`,
submit: '[data-test-identity-submit]',
confirm: '[data-test-confirm-button]',
};
module('Acceptance | /access/identity/entities', function (hooks) { module('Acceptance | /access/identity/entities', function (hooks) {
setupApplicationTest(hooks); setupApplicationTest(hooks);
@@ -33,4 +43,62 @@ module('Acceptance | /access/identity/entities', function (hooks) {
'navigates to the correct route' 'navigates to the correct route'
); );
}); });
test('it renders popup menu for entities', async function (assert) {
const name = `entity-${uuidv4()}`;
await runCmd(`vault write identity/entity name="${name}" policies="default"`);
await visit('/vault/access/identity/entities');
assert.strictEqual(currentURL(), '/vault/access/identity/entities', 'navigates to entities tab');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`);
assert
.dom('.hds-dropdown ul')
.hasText('Details Create alias Edit Disable Delete', 'all actions render for entities');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`);
await click(SELECTORS.confirm);
});
test('it renders popup menu for external groups', async function (assert) {
const name = `external-${uuidv4()}`;
await runCmd(`vault write identity/group name="${name}" policies="default" type="external"`);
await visit('/vault/access/identity/groups');
assert.strictEqual(currentURL(), '/vault/access/identity/groups', 'navigates to the groups tab');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`);
assert
.dom('.hds-dropdown ul')
.hasText('Details Create alias Edit Delete', 'all actions render for external groups');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`);
await click(SELECTORS.confirm);
});
test('it renders popup menu for external groups with alias', async function (assert) {
const name = `external-hasalias-${uuidv4()}`;
await runCmd(`vault write identity/group name="${name}" policies="default" type="external"`);
await visit('/vault/access/identity/groups');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`);
await click(SELECTORS.menuItem('create alias'));
await fillIn(GENERAL.inputByAttr('name'), 'alias-test');
await click(SELECTORS.submit);
await visit('/vault/access/identity/groups');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`);
assert
.dom('.hds-dropdown ul')
.hasText('Details Edit Delete', 'no "Create alias" option for external groups with an alias');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`);
await click(SELECTORS.confirm);
});
test('it renders popup menu for internal groups', async function (assert) {
const name = `internal-${uuidv4()}`;
await runCmd(`vault write identity/group name="${name}" policies="default" type="internal"`);
await visit('/vault/access/identity/groups');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`);
assert
.dom('.hds-dropdown ul')
.hasText('Details Edit Delete', 'no "Create alias" option for internal groups');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`);
await click(SELECTORS.confirm);
});
}); });

View File

@@ -255,7 +255,7 @@ module('Acceptance | mfa-method', function (hooks) {
await visit('/vault/access/mfa/methods'); await visit('/vault/access/mfa/methods');
const id = this.element.querySelector('[data-test-mfa-method-list-item] .tag').textContent.trim(); const id = this.element.querySelector('[data-test-mfa-method-list-item] .tag').textContent.trim();
const model = this.store.peekRecord('mfa-method', id); const model = this.store.peekRecord('mfa-method', id);
await click('[data-test-mfa-method-list-item] .ember-basic-dropdown-trigger'); await click('[data-test-mfa-method-list-item] [data-test-popup-menu-trigger]');
await click('[data-test-mfa-method-menu-link="edit"]'); await click('[data-test-mfa-method-menu-link="edit"]');
const keys = ['issuer', 'period', 'key_size', 'qr_size', 'algorithm', 'digits', 'skew']; const keys = ['issuer', 'period', 'key_size', 'qr_size', 'algorithm', 'digits', 'skew'];

View File

@@ -319,7 +319,7 @@ module('Acceptance | pki workflow', function (hooks) {
); );
}); });
test('it hide corrects actions for user with read policy', async function (assert) { test('it hides correct actions for user with read policy', async function (assert) {
await authPage.login(this.pkiKeyReader); await authPage.login(this.pkiKeyReader);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`); await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.keysTab); await click(SELECTORS.keysTab);
@@ -330,7 +330,7 @@ module('Acceptance | pki workflow', function (hooks) {
assert.dom('.linked-block').exists({ count: 1 }, 'One key is in list'); assert.dom('.linked-block').exists({ count: 1 }, 'One key is in list');
const keyId = find(SELECTORS.keyPages.keyId).innerText; const keyId = find(SELECTORS.keyPages.keyId).innerText;
await click(SELECTORS.keyPages.popupMenuTrigger); await click(SELECTORS.keyPages.popupMenuTrigger);
assert.dom(SELECTORS.keyPages.popupMenuEdit).hasClass('disabled', 'popup menu edit link is disabled'); assert.dom(SELECTORS.keyPages.popupMenuEdit).doesNotExist('popup menu edit link is not shown');
await click(SELECTORS.keyPages.popupMenuDetails); await click(SELECTORS.keyPages.popupMenuDetails);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys/${keyId}/details`); assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys/${keyId}/details`);
assert.dom(SELECTORS.keyPages.keyDeleteButton).doesNotExist('Delete key button is not shown'); assert.dom(SELECTORS.keyPages.keyDeleteButton).doesNotExist('Delete key button is not shown');

View File

@@ -117,7 +117,7 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) {
assert.dom(PAGE.secretTab('Metadata')).doesNotHaveClass('active'); assert.dom(PAGE.secretTab('Metadata')).doesNotHaveClass('active');
assert.dom(PAGE.secretTab('Version History')).hasText('Version History'); assert.dom(PAGE.secretTab('Version History')).hasText('Version History');
assert.dom(PAGE.secretTab('Version History')).doesNotHaveClass('active'); assert.dom(PAGE.secretTab('Version History')).doesNotHaveClass('active');
assert.dom(PAGE.toolbarAction).exists({ count: 5 }, 'toolbar renders all actions'); assert.dom(PAGE.toolbarAction).exists({ count: 4 }, 'toolbar renders all actions');
}); });
test('it navigates back to engine index route via breadcrumbs from secret details', async function (assert) { test('it navigates back to engine index route via breadcrumbs from secret details', async function (assert) {

View File

@@ -59,7 +59,7 @@ module('Acceptance | secrets/ssh', function (hooks) {
assert.strictEqual(listPage.secrets.length, 1, 'shows role in the list'); assert.strictEqual(listPage.secrets.length, 1, 'shows role in the list');
const secret = listPage.secrets.objectAt(0); const secret = listPage.secrets.objectAt(0);
await secret.menuToggle(); await secret.menuToggle();
assert.ok(listPage.menuItems.length > 0, 'shows links in the menu'); assert.dom('.hds-dropdown li').exists({ count: 5 }, 'Renders 5 popup menu items');
}); });
test('it deletes a role', async function (assert) { test('it deletes a role', async function (assert) {

View File

@@ -244,7 +244,7 @@ module('Acceptance | transit (flaky)', function (hooks) {
assert.dom(SELECTORS.infoRow('Convergent encryption')).hasText('Yes'); assert.dom(SELECTORS.infoRow('Convergent encryption')).hasText('Yes');
await click(SELECTORS.rootCrumb(this.path)); await click(SELECTORS.rootCrumb(this.path));
await click(SELECTORS.popupMenu); await click(SELECTORS.popupMenu);
const actions = findAll('.ember-basic-dropdown-content li'); const actions = findAll('.hds-dropdown__list li');
assert.strictEqual(actions.length, 2, 'shows 2 items in popup menu'); assert.strictEqual(actions.length, 2, 'shows 2 items in popup menu');
await click(SELECTORS.secretLink); await click(SELECTORS.secretLink);

View File

@@ -57,7 +57,7 @@ export const SELECTORS = {
generateIssuerRoot: '[data-test-generate-issuer="root"]', generateIssuerRoot: '[data-test-generate-issuer="root"]',
generateIssuerIntermediate: '[data-test-generate-issuer="intermediate"]', generateIssuerIntermediate: '[data-test-generate-issuer="intermediate"]',
issuerPopupMenu: '[data-test-popup-menu-trigger]', issuerPopupMenu: '[data-test-popup-menu-trigger]',
issuerPopupDetails: '[data-test-popup-menu-details] a', issuerPopupDetails: '[data-test-popup-menu-details]',
issuerDetails: { issuerDetails: {
title: '[data-test-pki-issuer-page-title]', title: '[data-test-pki-issuer-page-title]',
...ISSUERDETAILS, ...ISSUERDETAILS,

View File

@@ -49,10 +49,10 @@ module('Integration | Component | auth-config-form options', function (hooks) {
}); });
sinon.spy(model.config, 'serialize'); sinon.spy(model.config, 'serialize');
this.set('model', model); this.set('model', model);
await render(hbs`{{auth-config-form/options model=this.model}}`); await render(hbs`<AuthConfigForm::Options @model={{this.model}} />`);
component.save(); component.save();
return settled().then(() => { return settled().then(() => {
assert.ok(model.config.serialize.calledOnce); assert.strictEqual(model.config.serialize.callCount, 1, 'config serialize was called once');
}); });
}); });
}); });

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import sinon from 'sinon';
module('Integration | Component | confirm-modal', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.onConfirm = sinon.spy();
this.onClose = sinon.spy();
});
test('it renders a reasonable default', async function (assert) {
await render(hbs`<ConfirmModal @onConfirm={{this.onConfirm}} @onClose={{this.onClose}} />`);
assert
.dom('[data-test-confirm-modal]')
.hasClass('hds-modal--color-warning', 'renders warning modal color');
assert
.dom('[data-test-confirm-button]')
.hasClass('hds-button--color-primary', 'renders primary confirm button');
assert.dom('[data-test-confirm-action-title]').hasText('Are you sure?', 'renders default title');
assert
.dom('[data-test-confirm-action-message]')
.hasText('You will not be able to recover it later.', 'renders default body text');
await click('[data-test-confirm-cancel-button]');
assert.ok(this.onClose.called, 'calls the onClose action when Cancel is clicked');
await click('[data-test-confirm-button]');
assert.ok(this.onConfirm.called, 'calls the onConfirm action when Confirm is clicked');
});
});

View File

@@ -90,7 +90,7 @@ module('Integration | Component | kv | Page::List', function (hooks) {
const popupSelector = `${PAGE.list.item('my-secret-0')} ${PAGE.popup}`; const popupSelector = `${PAGE.list.item('my-secret-0')} ${PAGE.popup}`;
await click(popupSelector); await click(popupSelector);
await click('[data-test-confirm-action-trigger]'); await click('[data-test-popup-metadata-delete]');
await click('[data-test-confirm-button]'); await click('[data-test-confirm-button]');
assert.dom(PAGE.list.item('my-secret-0')).doesNotExist('deleted the first record from the list'); assert.dom(PAGE.list.item('my-secret-0')).doesNotExist('deleted the first record from the list');
}); });

View File

@@ -87,10 +87,10 @@ module('Integration | Component | kv | Page::Secret::Metadata::Version-History',
{ owner: this.engine } { owner: this.engine }
); );
// because the popup menu is nested in a linked block we must combine the two selectors // because the popup menu is nested in a linked block we must combine the two selectors
const popupSelector = `${PAGE.versions.linkedBlock(2)} ${PAGE.popup}`; const popupSelector = `${PAGE.versions.linkedBlock(1)} ${PAGE.popup}`;
await click(popupSelector); await click(popupSelector);
assert assert
.dom('[data-test-create-new-version-from="2"]') .dom('[data-test-create-new-version-from="1"]')
.exists('Shows the option to create a new version from that secret.'); .exists('Shows the option to create a new version from that secret.');
}); });
}); });

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { overrideCapabilities } from 'vault/tests/helpers/oidc-config';
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
module('Integration | Component | oidc/client-list', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.store.createRecord('oidc/client', { name: 'first-client' });
this.store.createRecord('oidc/client', { name: 'second-client' });
this.model = this.store.peekAll('oidc/client');
});
test('it renders list of clients', async function (assert) {
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub(['read', 'update']));
await render(hbs`<Oidc::ClientList @model={{this.model}} />`);
assert.dom('[data-test-oidc-client-linked-block]').exists({ count: 2 }, 'Two clients are rendered');
assert.dom('[data-test-oidc-client-linked-block="first-client"]').exists('First client is rendered');
assert.dom('[data-test-oidc-client-linked-block="second-client"]').exists('Second client is rendered');
await click('[data-test-oidc-client-linked-block="first-client"] [data-test-popup-menu-trigger]');
assert.dom('[data-test-oidc-client-menu-link="details"]').exists('Details link is rendered');
assert.dom('[data-test-oidc-client-menu-link="edit"]').exists('Edit link is rendered');
});
test('it renders popup menu based on permissions', async function (assert) {
this.server.post('/sys/capabilities-self', (schema, req) => {
const { paths } = JSON.parse(req.requestBody);
if (paths[0] === 'identity/oidc/client/first-client') {
return overrideCapabilities('identity/oidc/client/first-client', ['read']);
} else {
return overrideCapabilities('identity/oidc/client/second-client', ['deny']);
}
});
await render(hbs`<Oidc::ClientList @model={{this.model}} />`);
assert.dom('[data-test-popup-menu-trigger]').exists({ count: 1 }, 'Only one popup menu is rendered');
await click('[data-test-popup-menu-trigger]');
assert.dom('[data-test-oidc-client-menu-link="details"]').exists('Details link is rendered');
assert.dom('[data-test-oidc-client-menu-link="edit"]').doesNotExist('Edit link is not rendered');
});
});

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { overrideCapabilities } from 'vault/tests/helpers/oidc-config';
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
module('Integration | Component | oidc/provider-list', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.store.createRecord('oidc/provider', { name: 'first-provider', issuer: 'foobar' });
this.store.createRecord('oidc/provider', { name: 'second-provider', issuer: 'foobar' });
this.model = this.store.peekAll('oidc/provider');
});
test('it renders list of providers', async function (assert) {
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub(['read', 'update']));
await render(hbs`<Oidc::ProviderList @model={{this.model}} />`);
assert.dom('[data-test-oidc-provider-linked-block]').exists({ count: 2 }, 'Two clients are rendered');
assert.dom('[data-test-oidc-provider-linked-block="first-provider"]').exists('First client is rendered');
assert
.dom('[data-test-oidc-provider-linked-block="second-provider"]')
.exists('Second client is rendered');
await click('[data-test-oidc-provider-linked-block="first-provider"] [data-test-popup-menu-trigger]');
assert.dom('[data-test-oidc-provider-menu-link="details"]').exists('Details link is rendered');
assert.dom('[data-test-oidc-provider-menu-link="edit"]').exists('Edit link is rendered');
});
test('it renders popup menu based on permissions', async function (assert) {
this.server.post('/sys/capabilities-self', (schema, req) => {
const { paths } = JSON.parse(req.requestBody);
if (paths[0] === 'identity/oidc/provider/first-provider') {
return overrideCapabilities('identity/oidc/provider/first-provider', ['read']);
} else {
return overrideCapabilities('identity/oidc/provider/second-provider', ['deny']);
}
});
await render(hbs`<Oidc::ProviderList @model={{this.model}} />`);
assert.dom('[data-test-popup-menu-trigger]').exists({ count: 1 }, 'Only one popup menu is rendered');
await click('[data-test-popup-menu-trigger]');
assert.dom('[data-test-oidc-provider-menu-link="details"]').exists('Details link is rendered');
assert.dom('[data-test-oidc-provider-menu-link="edit"]').doesNotExist('Edit link is not rendered');
});
});

View File

@@ -91,8 +91,8 @@ module('Integration | Component | pki key list page', function (hooks) {
assert.dom(SELECTORS.popupMenuEdit).exists('edit link exists'); assert.dom(SELECTORS.popupMenuEdit).exists('edit link exists');
}); });
test('it hides or disables actions when permission denied', async function (assert) { test('it hides actions when permission denied', async function (assert) {
assert.expect(4); assert.expect(3);
await render( await render(
hbs` hbs`
<Page::PkiKeyList <Page::PkiKeyList
@@ -108,8 +108,6 @@ module('Integration | Component | pki key list page', function (hooks) {
); );
assert.dom(SELECTORS.importKey).doesNotExist('renders import action'); assert.dom(SELECTORS.importKey).doesNotExist('renders import action');
assert.dom(SELECTORS.generateKey).doesNotExist('renders generate action'); assert.dom(SELECTORS.generateKey).doesNotExist('renders generate action');
await click(SELECTORS.popupMenuTrigger); assert.dom(SELECTORS.popupMenuTrigger).doesNotExist('does not render popup menu when no permission');
assert.dom(SELECTORS.popupMenuDetails).hasClass('disabled', 'details link enabled');
assert.dom(SELECTORS.popupMenuEdit).hasClass('disabled', 'edit link enabled');
}); });
}); });

View File

@@ -53,8 +53,8 @@ module('Integration | Component | transform-list-item', function (hooks) {
/>`); />`);
assert.dom('[data-test-secret-link="template/foo"]').exists('shows clickable list item'); assert.dom('[data-test-secret-link="template/foo"]').exists('shows clickable list item');
await click('button.popup-menu-trigger'); await click('[data-test-popup-menu-trigger]');
assert.dom('.popup-menu-content li').exists({ count: 1 }, 'has one option'); assert.dom('.hds-dropdown li').exists({ count: 1 }, 'has one option');
}); });
test('it has details and edit menu item if read & edit capabilities', async function (assert) { test('it has details and edit menu item if read & edit capabilities', async function (assert) {
@@ -76,8 +76,8 @@ module('Integration | Component | transform-list-item', function (hooks) {
/>`); />`);
assert.dom('[data-test-secret-link="alphabet/foo"]').exists('shows clickable list item'); assert.dom('[data-test-secret-link="alphabet/foo"]').exists('shows clickable list item');
await click('button.popup-menu-trigger'); await click('[data-test-popup-menu-trigger]');
assert.dom('.popup-menu-content li').exists({ count: 2 }, 'has both options'); assert.dom('.hds-dropdown li').exists({ count: 2 }, 'has both options');
}); });
test('it is not clickable if built-in template with all capabilities', async function (assert) { test('it is not clickable if built-in template with all capabilities', async function (assert) {

View File

@@ -13,7 +13,7 @@ export default create({
menu: clickable('[data-test-popup-menu-trigger]'), menu: clickable('[data-test-popup-menu-trigger]'),
name: text('[data-test-identity-link]'), name: text('[data-test-identity-link]'),
}), }),
delete: clickable('[data-test-item-delete]', { delete: clickable('[data-test-popup-menu="delete"]', {
testContainer: '#ember-testing', testContainer: '#ember-testing',
}), }),
confirmDelete: clickable('[data-test-confirm-button]'), confirmDelete: clickable('[data-test-confirm-button]'),

View File

@@ -14,7 +14,7 @@ export default create({
name: text('[data-test-identity-link]'), name: text('[data-test-identity-link]'),
}), }),
delete: clickable('[data-test-item-delete]', { delete: clickable('[data-test-popup-menu="delete"]', {
testContainer: '#ember-testing', testContainer: '#ember-testing',
}), }),
confirmDelete: clickable('[data-test-confirm-button]'), confirmDelete: clickable('[data-test-confirm-button]'),