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
*/
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({
messageArgs(model) {
const type = model.get('identityType');
const id = model.id;
return [type, id];
},
export default class IdentityPopupAlias extends Component {
@service flashMessages;
@tracked showConfirmModal = false;
successMessage(type, id) {
return `Successfully deleted ${type}: ${id}`;
},
onSuccess(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) {
const error = e.errors ? e.errors.join(' ') : e.message;
return `There was a problem deleting ${type}: ${id} - ${error}`;
},
transaction(model) {
return model.destroyRecord();
},
});
@action
async deleteAlias() {
const { identityType, id } = this.args.item;
try {
await this.args.item.destroyRecord();
this.onSuccess(identityType, id);
} catch (e) {
this.onError(e, identityType, id);
}
}
}

View File

@@ -3,37 +3,44 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { alias } from '@ember/object/computed';
import { computed } from '@ember/object';
import Base from './_popup-base';
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import errorMessage from 'vault/utils/error-message';
export default Base.extend({
model: alias('params.firstObject'),
export default class IdentityPopupMembers extends Component {
@service flashMessages;
@tracked showConfirmModal = false;
groupArray: computed('params', function () {
return this.params.objectAt(1);
}),
onSuccess(memberId) {
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 () {
return this.params.objectAt(2);
}),
transaction() {
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*/) {
return [...arguments];
},
successMessage(model, groupArray, memberId) {
return `Successfully removed '${memberId}' from the group`;
},
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();
},
});
@action
async removeGroup() {
const memberId = this.args.memberId;
try {
await this.transaction();
this.onSuccess(memberId);
} catch (e) {
this.onError(e, memberId);
}
}
}

View File

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

View File

@@ -3,32 +3,47 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { alias } from '@ember/object/computed';
import { computed } from '@ember/object';
import Base from './_popup-base';
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { service } from '@ember/service';
import errorMessage from 'vault/utils/error-message';
import { tracked } from '@glimmer/tracking';
export default Base.extend({
model: alias('params.firstObject'),
policyName: computed('params', function () {
return this.params.objectAt(1);
}),
export default class IdentityPopupPolicy extends Component {
@service flashMessages;
@tracked showConfirmModal = false;
messageArgs(model, policyName) {
return [model, policyName];
},
onSuccess(policyName, modelId) {
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) {
return `Successfully removed '${policyName}' policy from ${model.id} `;
},
transaction() {
const policies = this.args.model.policies;
this.args.model.policies = policies.without(this.args.policyName);
return this.args.model.save();
}
errorMessage(e, model, policyName) {
const error = e.errors ? e.errors.join(' ') : e.message;
return `There was a problem removing '${policyName}' policy - ${error}`;
},
transaction(model, policyName) {
const policies = model.get('policies');
model.set('policies', policies.without(policyName));
return model.save();
},
});
@action
async removePolicy() {
const {
policyName,
model: { id },
} = this.args;
try {
await this.transaction();
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 {
@tracked roleType = '';
@tracked actionRunning = null;
@service store;
@service flashMessages;
@@ -41,6 +42,7 @@ export default class DatabaseListItem extends Component {
resetConnection(id) {
const { backend } = this.args.item;
const adapter = this.store.adapterFor('database/connection');
this.actionRunning = 'reset';
adapter
.resetConnection(backend, id)
.then(() => {
@@ -48,12 +50,14 @@ export default class DatabaseListItem extends Component {
})
.catch((e) => {
this.flashMessages.danger(e.errors);
});
})
.finally(() => (this.actionRunning = null));
}
@action
rotateRootCred(id) {
const { backend } = this.args.item;
const adapter = this.store.adapterFor('database/connection');
this.actionRunning = 'rotateRoot';
adapter
.rotateRootCredentials(backend, id)
.then(() => {
@@ -61,12 +65,14 @@ export default class DatabaseListItem extends Component {
})
.catch((e) => {
this.flashMessages.danger(e.errors);
});
})
.finally(() => (this.actionRunning = null));
}
@action
rotateRoleCred(id) {
const { backend } = this.args.item;
const adapter = this.store.adapterFor('database/credential');
this.actionRunning = 'rotateRole';
adapter
.rotateRoleCredentials(backend, id)
.then(() => {
@@ -74,6 +80,7 @@ export default class DatabaseListItem extends Component {
})
.catch((e) => {
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, {
flashMessages: service(),
entityToDisable: null,
itemToDelete: null,
// callback from HDS pagination to set the queryParams page
get paginationQueryParams() {
return (page) => {
@@ -33,7 +36,8 @@ export default Controller.extend(ListController, {
this.flashMessages.success(
`There was a problem deleting ${type}: ${id} - ${e.errors.join(' ') || e.message}`
);
});
})
.finally(() => this.set('itemToDelete', null));
},
toggleDisabled(model) {
@@ -51,7 +55,8 @@ export default Controller.extend(ListController, {
this.flashMessages.success(
`There was a problem ${action[1]} ${type}: ${id} - ${e.errors.join(' ') || e.message}`
);
});
})
.finally(() => this.set('entityToDisable', null));
},
reloadRecord(model) {
model.reload();

View File

@@ -21,8 +21,8 @@ export default Controller.extend({
filterFocused: false,
// set via the route `loading` action
isLoading: false,
isLoading: false, // set via the route `loading` action
policyToDelete: null, // set when clicking 'Delete' from popup menu
// callback from HDS pagination to set the queryParams page
get paginationQueryParams() {
@@ -77,7 +77,8 @@ export default Controller.extend({
flash.danger(
`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 selectedEngineType = null;
@tracked selectedEngineName = null;
@tracked engineToDisable = null;
get sortedDisplayableBackends() {
// show supported secret engines first and then organize those by id.
@@ -80,6 +81,8 @@ export default class VaultClusterSecretsBackendController extends Controller {
this.flashMessages.danger(
`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'),
aliasPath: lazyCapabilities(apiPath`identity/group-alias`),
canAddAlias: computed('aliasPath.canCreate', 'type', 'alias', function () {
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;
}),
canAddAlias: alias('aliasPath.canCreate'),
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,43 +3,38 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<PopupMenu @name="alias-menu">
{{#let (get this.params "0") as |item|}}
<nav class="menu" aria-label="navigation for managing aliases">
<ul class="menu-list">
<li class="action">
<LinkTo
@route="vault.cluster.access.identity.aliases.show"
@models={{array (pluralize item.parentType) item.id "details"}}
>
Details
</LinkTo>
</li>
{{#if item.updatePath.isPending}}
<li class="action">
<LoadingDropdownOption />
</li>
{{else}}
{{#if item.canEdit}}
<li class="action">
<LinkTo
@route="vault.cluster.access.identity.aliases.edit"
@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}}
</ul>
</nav>
{{/let}}
</PopupMenu>
<div class="has-text-right">
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Alias management options"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive
@text="Details"
@route="vault.cluster.access.identity.aliases.show"
@models={{array (pluralize @item.parentType) @item.id "details"}}
/>
{{#if @item.updatePath.isPending}}
<dd.Generic class="has-text-center">
<LoadingDropdownOption />
</dd.Generic>
{{else}}
{{#if @item.canEdit}}
<dd.Interactive
@text="Edit"
@route="vault.cluster.access.identity.aliases.edit"
@models={{array (pluralize @item.parentType) @item.id}}
/>
{{/if}}
{{#if @item.canDelete}}
<dd.Interactive @text="Remove" @color="critical" {{on "click" (fn (mut this.showConfirmModal) true)}} />
{{/if}}
{{/if}}
</Hds::Dropdown>
</div>
{{#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
~}}
<PopupMenu @name="member-edit-menu">
<nav class="menu" aria-label="navigation for managing identity members">
<ul class="menu-list">
<li class="action">
<ConfirmAction
@buttonText="Remove"
@confirmTitle="Remove this group?"
@isInDropdown={{true}}
@confirmMessage="This may affect permissions for this group."
@onConfirmAction={{action "performTransaction" this.model this.groupArray this.memberId}}
/>
</li>
</ul>
</nav>
</PopupMenu>
<div class="has-text-right">
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Identity member options"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive @text="Remove" @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.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
~}}
<PopupMenu @name="metadata-edit-menu">
<nav class="menu" aria-label="navigation for managing identity metadata">
<ul class="menu-list">
<li class="action">
<ConfirmAction
@buttonText="Remove"
@confirmTitle="Remove metadata?"
@isInDropdown={{true}}
@confirmMessage="This data may be used outside of Vault."
@onConfirmAction={{action "performTransaction" this.model this.key}}
/>
</li>
</ul>
</nav>
</PopupMenu>
<div class="has-text-right">
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon @icon="more-horizontal" @text="Metadata options" @hasChevron={{false}} data-test-popup-menu-trigger />
<dd.Interactive @text="Remove" @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.removeMetadata}}
@confirmTitle="Remove metadata?"
@confirmMessage="This data may be used outside of Vault."
/>
{{/if}}

View File

@@ -3,28 +3,30 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<PopupMenu @name="policy-menu">
<nav class="menu" aria-label="navigation for managing identity policies">
<ul class="menu-list">
<li class="action">
<LinkTo @route="vault.cluster.policy.show" @models={{array "acl" this.policyName}}>
View Policy
</LinkTo>
</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>
</ul>
</nav>
</PopupMenu>
<div class="has-text-right">
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Identity policy management options"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive @text="View policy" @route="vault.cluster.policy.show" @models={{array "acl" @policyName}} />
<dd.Interactive @text="Edit policy" @route="vault.cluster.policy.edit" @models={{array "acl" @policyName}} />
<dd.Interactive
@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 class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
<PopupMenu>
<nav class="menu" aria-label="navigation for managing login enforcements">
<ul class="menu-list">
<li>
<LinkTo
@route="vault.cluster.access.mfa.enforcements.enforcement"
@model={{@model.name}}
data-test-list-item-link="details"
>
Details
</LinkTo>
</li>
<li>
<LinkTo
@route="vault.cluster.access.mfa.enforcements.enforcement.edit"
@model={{@model.name}}
data-test-list-item-link="edit"
>
Edit
</LinkTo>
</li>
</ul>
</nav>
</PopupMenu>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Manage M-F-A enforcement"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive
@text="Details"
@route="vault.cluster.access.mfa.enforcements.enforcement"
@model={{@model.name}}
data-test-list-item-link="details"
/>
<dd.Interactive
@text="Edit"
@route="vault.cluster.access.mfa.enforcements.enforcement.edit"
@model={{@model.name}}
data-test-list-item-link="edit"
/>
</Hds::Dropdown>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

@@ -23,58 +23,60 @@
</LinkTo>
</div>
<div class="column has-text-right">
<PopupMenu @name="role-aws-nav" @contentClass="is-wide">
<nav class="menu" aria-label="navigation for managing A-W-S role {{@item.id}}">
<ul class="menu-list">
{{#if @item.generatePath.isPending}}
<li class="action">
<LoadingDropdownOption />
</li>
{{else if @item.canGenerate}}
<li class="action">
<LinkTo
@route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}}
data-test-role-aws-link="generate"
>
Generate credentials
</LinkTo>
</li>
{{/if}}
{{#if @item.updatePath.isPending}}
<li class="action">
<LoadingDropdownOption />
</li>
<li class="action">
<LoadingDropdownOption />
</li>
{{else}}
{{#if @item.canRead}}
<li class="action">
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@item.id}} data-test-role-ssh-link="show">
Details
</LinkTo>
</li>
{{/if}}
{{#if @item.canEdit}}
<li class="action">
<LinkTo @route="vault.cluster.secrets.backend.edit" @model={{@item.id}} data-test-role-ssh-link="edit">
Edit
</LinkTo>
</li>
{{/if}}
{{#if @item.canDelete}}
<ConfirmAction
@buttonText="Delete"
@isInDropdown={{true}}
@onConfirmAction={{@delete}}
data-test-aws-role-delete={{@item.id}}
/>
{{/if}}
{{/if}}
</ul>
</nav>
</PopupMenu>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="manage A-W-S role {{@item.id}}"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if @item.generatePath.isPending}}
<dd.Generic class="has-text-center">
<LoadingDropdownOption />
</dd.Generic>
{{else if @item.canGenerate}}
<dd.Interactive
@text="Generate credentials"
@route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}}
data-test-role-aws-link="generate"
/>
{{/if}}
{{#if @item.updatePath.isPending}}
<dd.Generic class="has-text-center">
<LoadingDropdownOption />
</dd.Generic>
{{else}}
{{#if @item.canRead}}
<dd.Interactive
@text="Details"
@route="vault.cluster.secrets.backend.show"
@model={{@item.id}}
data-test-role-ssh-link="show"
/>
{{/if}}
{{#if @item.canEdit}}
<dd.Interactive
@text="Edit"
@route="vault.cluster.secrets.backend.edit"
@model={{@item.id}}
data-test-role-ssh-link="edit"
/>
{{/if}}
{{#if @item.canDelete}}
<dd.Interactive
@text="Delete"
@color="critical"
{{on "click" (fn (mut this.showConfirmModal) true)}}
data-test-aws-role-delete={{@item.id}}
/>
{{/if}}
{{/if}}
</Hds::Dropdown>
</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>
</div>
<div class="column has-text-right">
<PopupMenu name="secret-menu">
<nav class="menu" aria-label="navigation for managing database {{@item.id}}">
<ul class="menu-list">
{{#if @item.canEdit}}
<li class="action">
<SecretLink @mode="edit" @secret={{@item.id}} class="has-text-black has-text-weight-semibold">
Edit connection
</SecretLink>
</li>
{{/if}}
{{#if @item.canEditRole}}
<li class="action">
<SecretLink @mode="edit" @secret={{concat "role/" @item.id}} class="has-text-black has-text-weight-semibold">
Edit Role
</SecretLink>
</li>
{{/if}}
{{#if @item.canReset}}
<li class="action">
<Hds::Button
@text="Reset connection"
@color="secondary"
class="link"
{{on "click" (fn this.resetConnection @item.id)}}
/>
</li>
{{/if}}
{{#if (and (eq @item.type "dynamic") @item.canGenerateCredentials)}}
<li class="action">
<LinkTo
@route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}}
@query={{hash roleType=this.keyTypeValue}}
>
Generate credentials
</LinkTo>
</li>
{{else if (and (eq @item.type "static") @item.canGetCredentials)}}
<li class="action">
<LinkTo
@route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}}
@query={{hash roleType=this.keyTypeValue}}
>
Get credentials
</LinkTo>
</li>
{{/if}}
{{#if (and @item.canRotateRoleCredentials (eq this.keyTypeValue "static"))}}
<li class="action">
<Hds::Button
@text="Rotate credentials"
@color="secondary"
class="link"
{{on "click" (fn this.rotateRoleCred @item.id)}}
/>
</li>
{{/if}}
{{#if @item.canRotateRoot}}
<li class="action">
<Hds::Button
@text="Rotate root credentials"
@color="secondary"
class="link"
{{on "click" (fn this.rotateRootCred @item.id)}}
/>
</li>
{{/if}}
</ul>
</nav>
</PopupMenu>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Manage database {{@item.id}}"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if @item.canEdit}}
<dd.Interactive @text="Edit connection" @route="vault.cluster.secrets.backend.edit" @model={{@item.id}} />
{{/if}}
{{#if @item.canEditRole}}
<dd.Interactive @text="Edit Role" @route="vault.cluster.secrets.backend.edit" @model={{concat "role/" @item.id}} />
{{/if}}
{{#if @item.canReset}}
<dd.Interactive
@text="Reset connection"
@icon={{if (eq this.actionRunning "reset") "loading"}}
{{on "click" (fn this.resetConnection @item.id)}}
/>
{{/if}}
{{#if (and (eq @item.type "dynamic") @item.canGenerateCredentials)}}
<dd.Interactive
@text="Generate credentials"
@route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}}
@query={{hash roleType=this.keyTypeValue}}
/>
{{else if (and (eq @item.type "static") @item.canGetCredentials)}}
<dd.Interactive
@text="Get credentials"
@route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}}
@query={{hash roleType=this.keyTypeValue}}
/>
{{/if}}
{{#if (and @item.canRotateRoleCredentials (eq this.keyTypeValue "static"))}}
<dd.Interactive
@text="Rotate credentials"
@icon={{if (eq this.actionRunning "rotateRole") "loading"}}
{{on "click" (fn this.rotateRoleCred @item.id)}}
/>
{{/if}}
{{#if @item.canRotateRoot}}
<dd.Interactive
@text="Rotate root credentials"
@icon={{if (eq this.actionRunning "rotateRoot") "loading"}}
{{on "click" (fn this.rotateRootCred @item.id)}}
/>
{{/if}}
</Hds::Dropdown>
</div>
</div>
</LinkedBlock>

View File

@@ -30,56 +30,57 @@
</SecretLink>
</div>
<div class="column has-text-right">
<PopupMenu name="secret-menu">
<nav class="menu" aria-label="navigation for managing {{@item.id}}">
<ul class="menu-list">
{{#if @item.isFolder}}
<SecretLink @mode="list" @secret={{@item.id}} class="has-text-black has-text-weight-semibold">
Contents
</SecretLink>
{{else}}
{{#if (or @item.versionPath.isLoading @item.secretPath.isLoading)}}
<li class="action">
<LoadingDropdownOption />
</li>
{{else}}
{{#if @item.canRead}}
<li class="action">
<SecretLink
@mode="show"
@secret={{@item.id}}
@queryParams={{secret-query-params @backendModel.type @item.type asQueryParams=true}}
class="has-text-black has-text-weight-semibold"
>
Details
</SecretLink>
</li>
{{/if}}
{{#if @item.canEdit}}
<li class="action">
<SecretLink
@mode="edit"
@secret={{@item.id}}
@queryParams={{secret-query-params @backendModel.type @item.type asQueryParams=true}}
class="has-text-black has-text-weight-semibold"
>
Edit
</SecretLink>
</li>
{{/if}}
{{#if @item.canDelete}}
<ConfirmAction
@isInDropdown={{true}}
@buttonText="Delete"
@confirmMessage="This will permanently delete this secret."
@onConfirmAction={{@delete}}
/>
{{/if}}
{{/if}}
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Manage database {{@item.id}}"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if @item.isFolder}}
<dd.Interactive @text="Contents" @route="vault.cluster.secrets.backend.list" @model={{@item.id}} />
{{else}}
{{#if (or @item.versionPath.isLoading @item.secretPath.isLoading)}}
<dd.Generic class="has-text-center">
<LoadingDropdownOption />
</dd.Generic>
{{else}}
{{#if @item.canRead}}
<dd.Interactive
@text="Details"
@route="vault.cluster.secrets.backend.show"
@model={{@item.id}}
@query={{secret-query-params @backendModel.type @item.type asQueryParams=true}}
/>
{{/if}}
</ul>
</nav>
</PopupMenu>
{{#if @item.canEdit}}
<dd.Interactive
@text="Edit"
@route="vault.cluster.secrets.backend.edit"
@model={{@item.id}}
@query={{secret-query-params @backendModel.type @item.type asQueryParams=true}}
/>
{{/if}}
{{#if @item.canDelete}}
<dd.Interactive
@text="Delete"
@color="critical"
data-test-confirm-action-trigger
{{on "click" (fn (mut this.showConfirmModal) true)}}
/>
{{/if}}
{{/if}}
{{/if}}
</Hds::Dropdown>
</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 class="column has-text-right">
{{#if (eq @backendType "ssh")}}
<PopupMenu @name="role-ssh-nav">
<nav class="menu" aria-label="navigation for managing SSH role {{@item.id}}">
<ul class="menu-list">
{{#if (eq @item.keyType "otp")}}
{{#if @item.generatePath.isPending}}
<li class="action">
<Hds::Button disabled @color="tertiary" @icon="loading" @text="loading" @isIconOnly={{true}} />
</li>
{{else if @item.canGenerate}}
<li class="action">
<LinkTo
@route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}}
data-test-role-ssh-link="generate"
>
Generate Credentials
</LinkTo>
</li>
{{/if}}
{{else if (eq @item.keyType "ca")}}
{{#if @item.signPath.isPending}}
<li class="action">
<li class="action">
<Hds::Button disabled @color="tertiary" @icon="loading" @text="loading" @isIconOnly={{true}} />
</li>
</li>
{{else if @item.canGenerate}}
<li class="action">
<LinkTo
@route="vault.cluster.secrets.backend.sign"
@model={{@item.id}}
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 @item.updatePath.isPending}}
<li class="action">
<Hds::Button disabled @color="tertiary" @icon="loading" @text="loading" @isIconOnly={{true}} />
<Hds::Button disabled @color="tertiary" @icon="loading" @text="loading" @isIconOnly={{true}} />
</li>
{{else}}
{{#if @item.canRead}}
<li class="action">
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@item.id}} data-test-role-ssh-link="show">
Details
</LinkTo>
</li>
{{/if}}
{{#if @item.canEdit}}
<li class="action">
<LinkTo @route="vault.cluster.secrets.backend.edit" @model={{@item.id}} data-test-role-ssh-link="edit">
Edit
</LinkTo>
</li>
{{/if}}
{{#if @item.canDelete}}
<ConfirmAction
@buttonText="Delete"
@isInDropdown={{true}}
@onConfirmAction={{@delete}}
data-test-ssh-role-delete
/>
{{/if}}
{{/if}}
</ul>
</nav>
</PopupMenu>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Manage SSH role {{@item.id}}"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if (eq @item.keyType "otp")}}
{{#if @item.generatePath.isPending}}
<dd.Generic class="has-text-center">
<LoadingDropdownOption />
</dd.Generic>
{{else if @item.canGenerate}}
<dd.Interactive
@text="Generate credentials"
@route="vault.cluster.secrets.backend.credentials"
@model={{@item.id}}
data-test-role-ssh-link="generate"
/>
{{/if}}
{{else if (eq @item.keyType "ca")}}
{{#if @item.signPath.isPending}}
<dd.Generic class="has-text-center">
<LoadingDropdownOption />
</dd.Generic>
{{else if @item.canGenerate}}
<dd.Interactive
@text="Sign Keys"
@route="vault.cluster.secrets.backend.sign"
@model={{@item.id}}
data-test-role-ssh-link="generate"
/>
{{/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 @item.updatePath.isPending}}
<dd.Generic class="has-text-center">
<LoadingDropdownOption />
</dd.Generic>
{{else}}
{{#if @item.canRead}}
<dd.Interactive
@text="Details"
@route="vault.cluster.secrets.backend.show"
@model={{@item.id}}
data-test-role-ssh-link="show"
/>
{{/if}}
{{#if @item.canEdit}}
<dd.Interactive
@text="Edit"
@route="vault.cluster.secrets.backend.edit"
@model={{@item.id}}
data-test-role-ssh-link="edit"
/>
{{/if}}
{{#if @item.canDelete}}
<dd.Interactive
@text="Delete"
@color="critical"
{{on "click" (fn (mut this.showConfirmModal) true)}}
data-test-ssh-role-delete
/>
{{/if}}
{{/if}}
</Hds::Dropdown>
{{/if}}
</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 class="column has-text-right">
{{#if (or @item.updatePath.canRead @item.updatePath.canUpdate)}}
<PopupMenu name="secret-menu">
<nav class="menu" aria-label="navigation for managing transformation item {{@itemPath}}">
<ul class="menu-list">
{{#if @item.updatePath.canRead}}
<li class="action">
<SecretLink @mode="show" @secret={{@itemPath}} class="has-text-black has-text-weight-semibold">
Details
</SecretLink>
</li>
{{/if}}
{{#if @item.updatePath.canUpdate}}
<li class="action">
<SecretLink @mode="edit" @secret={{@itemPath}} class="has-text-black has-text-weight-semibold">
Edit
</SecretLink>
</li>
{{/if}}
</ul>
</nav>
</PopupMenu>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Manage transform {{@itemType}}"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if @item.updatePath.canRead}}
<dd.Interactive @text="Details" @route="vault.cluster.secrets.backend.show" @model={{@itemPath}} />
{{/if}}
{{#if @item.updatePath.canUpdate}}
<dd.Interactive @text="Edit" @route="vault.cluster.secrets.backend.edit" @model={{@itemPath}} />
{{/if}}
</Hds::Dropdown>
{{/if}}
</div>
</div>
@@ -55,17 +49,13 @@
<div class="column is-12 has-text-grey has-text-weight-semibold">
<Icon @name="file" class="has-text-grey-light" />
{{#if this.isBuiltin}}
<ToolTip @verticalPosition="above" @horizontalPosition="left" as |T|>
<T.Trigger @tabindex={{false}}>
{{@item.id}}
</T.Trigger>
<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>
<Hds::TooltipButton
@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}}
</Hds::TooltipButton>
{{else}}
{{@item.id}}
{{/if}}

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@
<LinkedBlock
@params={{array "vault.cluster.access.identity.show" item.id "details"}}
class="list-item-row"
data-test-identity-row
data-test-identity-row={{item.name}}
>
<div class="columns is-mobile">
<div class="column is-7-tablet is-10-mobile">
@@ -32,63 +32,53 @@
{{/if}}
</div>
<div class="column has-text-right">
<PopupMenu @name="identity-item" @onOpen={{action "reloadRecord" item}}>
<nav class="menu" aria-label="navigation for managing identity">
<ul class="menu-list">
<li class="action">
<LinkTo @route="vault.cluster.access.identity.show" @models={{array item.id "details"}}>
Details
</LinkTo>
</li>
{{#if (or item.isReloading item.updatePath.isPending item.aliasPath.isPending)}}
<li class="action">
<LoadingDropdownOption />
</li>
{{else}}
{{#if item.canAddAlias}}
<li class="action">
<LinkTo
@route="vault.cluster.access.identity.aliases.add"
@models={{array (pluralize this.identityType) item.id}}
>
Create alias
</LinkTo>
</li>
{{/if}}
{{#if item.canEdit}}
<li class="action">
<LinkTo @route="vault.cluster.access.identity.edit" @model={{item.id}}>
Edit
</LinkTo>
</li>
<li class="action">
{{#if item.disabled}}
<Hds::Button @text="Enable" {{on "click" (action "toggleDisabled" item)}} class="link" />
{{else if (eq this.identityType "entity")}}
<ConfirmAction
@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}}
</li>
{{/if}}
{{#if item.canDelete}}
<ConfirmAction
@isInDropdown={{true}}
@buttonText="Delete"
@onConfirmAction={{action "delete" item}}
@confirmTitle="Delete this {{this.identityType}}?"
data-test-item-delete
/>
{{/if}}
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Identity management options"
@hasChevron={{false}}
{{on "click" (action "reloadRecord" item)}}
data-test-popup-menu-trigger
/>
<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)}}
<dd.Generic class="has-text-center">
<LoadingDropdownOption />
</dd.Generic>
{{else}}
{{#if item.canAddAlias}}
{{! entities can always add aliases, internal groups cannot have any and external groups can only have one }}
{{#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"
@models={{array (pluralize this.identityType) item.id}}
/>
{{/if}}
</ul>
</nav>
</PopupMenu>
{{/if}}
{{#if item.canEdit}}
<dd.Interactive @text="Edit" @route="vault.cluster.access.identity.edit" @model={{item.id}} />
{{#if item.disabled}}
<dd.Interactive @text="Enable" {{on "click" (action "toggleDisabled" item)}} />
{{else if (eq this.identityType "entity")}}
<dd.Interactive @text="Disable" @color="critical" {{on "click" (fn (mut this.entityToDisable) item)}} />
{{/if}}
{{/if}}
{{#if item.canDelete}}
<dd.Interactive
@text="Delete"
@color="critical"
{{on "click" (fn (mut this.itemToDelete) item)}}
data-test-popup-menu="delete"
/>
{{/if}}
{{/if}}
</Hds::Dropdown>
</div>
</div>
</LinkedBlock>
@@ -117,4 +107,22 @@
@iconPosition="trailing"
/>
</EmptyState>
{{/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}}
<div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
<PopupMenu>
<nav class="menu" aria-label="Enforcement target more menu">
<ul class="menu-list">
<li>
<LinkTo @route={{target.link}} @models={{target.linkModels}} data-test-target-link={{target.title}}>
Details
</LinkTo>
</li>
</ul>
</nav>
</PopupMenu>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Manage enforcement target"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive
@text="Details"
@route={{target.link}}
@models={{target.linkModels}}
data-test-target-link={{target.title}}
/>
</Hds::Dropdown>
</div>
</div>
{{/if}}

View File

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

View File

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

View File

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

View File

@@ -91,52 +91,44 @@
</LinkTo>
</div>
<div class="column has-text-right">
<PopupMenu name="policy-nav">
<nav class="menu">
<ul class="menu-list">
{{#if item.updatePath.isPending}}
<li class="action">
<LoadingDropdownOption />
</li>
<li class="action">
<LoadingDropdownOption />
</li>
{{else}}
{{#if item.canRead}}
<li class="action">
<LinkTo
@route="vault.cluster.policy.show"
@models={{array this.policyType item.id}}
data-test-policy-link="show"
>
Details
</LinkTo>
</li>
{{/if}}
{{#if item.canEdit}}
<li class="action">
<LinkTo
@route="vault.cluster.policy.edit"
@models={{array this.policyType item.id}}
data-test-policy-link="edit"
>
Edit
</LinkTo>
</li>
{{/if}}
{{#if item.canDelete}}
<ConfirmAction
@isInDropdown={{true}}
@buttonText="Delete"
@confirmTitle="Delete this policy?"
@confirmMessage="This will permanently delete this policy and may affect access to some data"
@onConfirmAction={{action "deletePolicy" item}}
/>
{{/if}}
{{/if}}
</ul>
</nav>
</PopupMenu>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Policy nav menu"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if item.updatePath.isPending}}
<dd.Generic class="has-text-center">
<LoadingDropdownOption />
</dd.Generic>
{{else}}
{{#if item.canRead}}
<dd.Interactive
@text="Details"
@route="vault.cluster.policy.show"
@models={{array this.policyType item.id}}
data-test-policy-link="show"
/>
{{/if}}
{{#if item.canEdit}}
<dd.Interactive
@text="Edit"
@route="vault.cluster.policy.edit"
@models={{array this.policyType item.id}}
data-test-policy-link="edit"
/>
{{/if}}
{{#if (and item.canDelete (not-eq item.name "default"))}}
<dd.Interactive
@text="Delete"
@color="critical"
data-test-confirm-action-trigger
{{on "click" (fn (mut this.policyToDelete) item)}}
/>
{{/if}}
{{/if}}
</Hds::Dropdown>
</div>
</div>
</LinkedBlock>
@@ -180,4 +172,14 @@
{{/if}}
{{else}}
<UpgradePage @title="Sentinel" @minimumEdition="Vault Enterprise Premium" />
{{/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>
{{/if}}
</div>
{{! meatball sandwich menu }}
<div class="linked-block-popup-menu">
<PopupMenu @name="engine-menu">
<nav class="menu" aria-label="{{if backend.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu">
<ul class="menu-list">
<li class="action">
<LinkTo @route="vault.cluster.secrets.backend.configuration" @model={{backend.id}} data-test-engine-config>
View configuration
</LinkTo>
</li>
{{#if (not-eq backend.type "cubbyhole")}}
<ConfirmAction
@isInDropdown={{true}}
@confirmMessage="Any data in this engine will be permanently deleted."
@confirmTitle="Disable engine?"
@buttonText="Disable"
@onConfirmAction={{perform this.disableEngine backend}}
/>
{{/if}}
</ul>
</nav>
</PopupMenu>
<Hds::Dropdown @isInline={{true}} as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="{{if backend.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive
@text="View configuration"
@route="vault.cluster.secrets.backend.configuration"
@model={{backend.id}}
data-test-engine-config
/>
{{#if (not-eq backend.type "cubbyhole")}}
<dd.Interactive
@text="Disable"
@color="critical"
{{on "click" (fn (mut this.engineToDisable) backend)}}
data-test-confirm-action-trigger
/>
{{/if}}
</Hds::Dropdown>
</div>
</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 class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
<PopupMenu @name="engine-menu">
<nav class="menu">
<ul class="menu-list">
{{#if message.canEditCustomMessages}}
<li class="action">
<LinkTo @route="messages.message.edit" @model={{message.id}}>
Edit
</LinkTo>
</li>
{{/if}}
{{#if message.canDeleteCustomMessages}}
<ConfirmAction
@isInDropdown={{true}}
@buttonText="Delete"
@confirmTitle="Are you sure?"
@confirmMessage="This will delete this message permanently. You cannot undo this action."
@onConfirmAction={{perform this.deleteMessage message}}
/>
{{/if}}
</ul>
</nav>
</PopupMenu>
{{#if (or message.canEditCustomMessages message.canDeleteCustomMessages)}}
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Message popup menu"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if message.canEditCustomMessages}}
<dd.Interactive @text="Edit" @route="messages.message.edit" @model={{message.id}} />
{{/if}}
{{#if message.canDeleteCustomMessages}}
<dd.Interactive @text="Disable" @color="critical" {{on "click" (fn (mut this.messageToDelete) message)}} />
{{/if}}
</Hds::Dropdown>
{{/if}}
</div>
</div>
</div>
@@ -117,4 +111,13 @@
<Hds::Button @text="Close" {{on "click" F.close}} data-test-modal-button="maximum-message-modal" />
</M.Footer>
</Hds::Modal>
{{/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;
@tracked showMaxMessageModal = false;
@tracked messageToDelete = null;
// 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.
@@ -110,6 +111,8 @@ export default class MessagesList extends Component {
} catch (e) {
const message = errorMessage(e);
this.flashMessages.danger(message);
} finally {
this.messageToDelete = null;
}
}

View File

@@ -25,44 +25,13 @@
{{/if}}
{{#if this.showConfirmModal}}
<Hds::Modal
id="confirm-action-modal"
class="has-text-left"
<ConfirmModal
@color={{this.modalColor}}
@size="small"
@onClose={{fn (mut this.showConfirmModal) false}}
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>
{{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>
@onConfirm={{this.onConfirm}}
@confirmTitle={{@confirmTitle}}
@confirmMessage={{this.confirmMessage}}
@disabledMessage={{@disabledMessage}}
@isRunning={{@isRunning}}
/>
{{/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';
/**
* @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.
*
* @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 class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
<PopupMenu>
<nav class="menu" aria-label="menu items for managing {{metadata.path}}">
<ul class="menu-list">
{{#if metadata.pathIsDirectory}}
<li>
<LinkTo @route="list-directory" @model={{metadata.fullSecretPath}}>
Content
</LinkTo>
</li>
{{else}}
<li>
<LinkTo @route="secret.details" @model={{metadata.fullSecretPath}}>
Details
</LinkTo>
</li>
{{#if metadata.canReadMetadata}}
<li>
<LinkTo @route="secret.metadata.versions" @model={{metadata.fullSecretPath}}>
View version history
</LinkTo>
</li>
{{/if}}
{{#if metadata.canCreateVersionData}}
<li>
<LinkTo
@route="secret.details.edit"
@model={{metadata.fullSecretPath}}
data-test-popup-create-new-version
>
Create new version
</LinkTo>
</li>
{{/if}}
{{#if metadata.canDeleteMetadata}}
<ConfirmAction
@buttonText="Permanently delete"
@isInDropdown={{true}}
@onConfirmAction={{fn this.onDelete metadata}}
@confirmMessage="This will permanently delete this secret and all its versions."
data-test-popup-metadata-delete
/>
{{/if}}
{{/if}}
</ul>
</nav>
</PopupMenu>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Manage secret"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if metadata.pathIsDirectory}}
<dd.Interactive @text="Content" @route="list-directory" @model={{metadata.fullSecretPath}} />
{{else}}
<dd.Interactive @text="Details" @route="secret.details" @model={{metadata.fullSecretPath}} />
{{#if metadata.canReadMetadata}}
<dd.Interactive
@text="View version history"
@route="secret.metadata.versions"
@model={{metadata.fullSecretPath}}
/>
{{/if}}
{{#if metadata.canCreateVersionData}}
<dd.Interactive
@text="Create new version"
@route="secret.details.edit"
@model={{metadata.fullSecretPath}}
data-test-popup-create-new-version
/>
{{/if}}
{{#if metadata.canDeleteMetadata}}
<dd.Interactive
@text="Permanently delete"
@color="critical"
{{on "click" (fn (mut this.metadataToDelete) metadata)}}
data-test-popup-metadata-delete
/>
{{/if}}
{{/if}}
</Hds::Dropdown>
</div>
</div>
</div>
</LinkedBlock>
{{/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 }}
<Hds::Pagination::Numbered
@currentPage={{@secrets.meta.currentPage}}

View File

@@ -30,6 +30,7 @@ export default class KvListPageComponent extends Component {
@service store;
@tracked secretPath;
@tracked metadataToDelete = null; // set to the metadata intended to delete
get mountPoint() {
// 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) {
const message = errorMessage(error, 'Error deleting secret. Please try again or contact support.');
this.flashMessages.danger(message);
} finally {
this.metadataToDelete = null;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,34 +29,29 @@
</div>
<div class="column has-text-right">
{{#if (or (eq this.replicationMode "performance") this.model.canRevokeSecondary)}}
<PopupMenu @name="secondary-details">
<nav class="menu">
<ul class="menu-list">
{{#if (eq this.replicationMode "performance")}}
<li class="action">
<LinkTo
@route="mode.secondaries.config-show"
@models={{array this.replicationMode secondary}}
data-test-replication-path-filter-link={{true}}
>
Path filter config
</LinkTo>
</li>
{{/if}}
{{#if this.model.canRevokeSecondary}}
<li class="action">
<ConfirmAction
@buttonText="Revoke"
@isInDropdown={{true}}
@confirmTitle="Revoke token?"
@confirmMessage="This will revoke this secondary token."
@onConfirmAction={{action "onSubmit" "revoke-secondary" "primary" (hash id=secondary)}}
/>
</li>
{{/if}}
</ul>
</nav>
</PopupMenu>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Secondary popup nav menu"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if (eq this.replicationMode "performance")}}
<dd.Interactive
@text="Path filter config"
@route="mode.secondaries.config-show"
@models={{array this.replicationMode secondary}}
data-test-replication-path-filter-link={{true}}
/>
{{/if}}
{{#if this.model.canRevokeSecondary}}
<dd.Interactive
@text="Revoke"
@color="critical"
{{on "click" (fn (mut this.secondaryToRevoke) secondary)}}
/>
{{/if}}
</Hds::Dropdown>
{{/if}}
</div>
</div>
@@ -76,4 +71,14 @@
/>
</EmptyState>
{{/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}}
</B.Td>
<B.Td @align="right">
<Hds::Dropdown @isInline={{true}} as |dd|>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@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 showPage from 'vault/tests/pages/access/identity/show';
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) => {
await page.visit({ item_type: itemType });
await settled();
@@ -24,7 +28,6 @@ export const testCRUD = async (name, itemType, assert) => {
`${itemType}: navigates to show on create`
);
assert.ok(showPage.nameContains(name), `${itemType}: renders the name on the show page`);
await indexPage.visit({ item_type: itemType });
await settled();
assert.strictEqual(
@@ -32,10 +35,10 @@ export const testCRUD = async (name, itemType, assert) => {
1,
`${itemType}: lists the entity in the entity list`
);
await indexPage.items.filterBy('name', name)[0].menu();
await waitUntil(() => find('[data-test-item-delete]'));
await indexPage.delete();
await settled();
await click(`${SELECTORS.identityRow(name)} ${SELECTORS.popupMenu}`);
await waitUntil(() => find(SELECTORS.menuDelete));
await click(SELECTORS.menuDelete);
await indexPage.confirmDelete();
await settled();
assert.ok(

View File

@@ -3,12 +3,22 @@
* 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 { setupApplicationTest } from 'ember-qunit';
import page from 'vault/tests/pages/access/identity/index';
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) {
setupApplicationTest(hooks);
@@ -33,4 +43,62 @@ module('Acceptance | /access/identity/entities', function (hooks) {
'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');
const id = this.element.querySelector('[data-test-mfa-method-list-item] .tag').textContent.trim();
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"]');
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 visit(`/vault/secrets/${this.mountPath}/pki/overview`);
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');
const keyId = find(SELECTORS.keyPages.keyId).innerText;
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);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys/${keyId}/details`);
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('Version History')).hasText('Version History');
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) {

View File

@@ -59,7 +59,7 @@ module('Acceptance | secrets/ssh', function (hooks) {
assert.strictEqual(listPage.secrets.length, 1, 'shows role in the list');
const secret = listPage.secrets.objectAt(0);
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) {

View File

@@ -244,7 +244,7 @@ module('Acceptance | transit (flaky)', function (hooks) {
assert.dom(SELECTORS.infoRow('Convergent encryption')).hasText('Yes');
await click(SELECTORS.rootCrumb(this.path));
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');
await click(SELECTORS.secretLink);

View File

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

View File

@@ -49,10 +49,10 @@ module('Integration | Component | auth-config-form options', function (hooks) {
});
sinon.spy(model.config, 'serialize');
this.set('model', model);
await render(hbs`{{auth-config-form/options model=this.model}}`);
await render(hbs`<AuthConfigForm::Options @model={{this.model}} />`);
component.save();
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}`;
await click(popupSelector);
await click('[data-test-confirm-action-trigger]');
await click('[data-test-popup-metadata-delete]');
await click('[data-test-confirm-button]');
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 }
);
// 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);
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.');
});
});

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');
});
test('it hides or disables actions when permission denied', async function (assert) {
assert.expect(4);
test('it hides actions when permission denied', async function (assert) {
assert.expect(3);
await render(
hbs`
<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.generateKey).doesNotExist('renders generate action');
await click(SELECTORS.popupMenuTrigger);
assert.dom(SELECTORS.popupMenuDetails).hasClass('disabled', 'details link enabled');
assert.dom(SELECTORS.popupMenuEdit).hasClass('disabled', 'edit link enabled');
assert.dom(SELECTORS.popupMenuTrigger).doesNotExist('does not render popup menu when no permission');
});
});

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');
await click('button.popup-menu-trigger');
assert.dom('.popup-menu-content li').exists({ count: 1 }, 'has one option');
await click('[data-test-popup-menu-trigger]');
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) {
@@ -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');
await click('button.popup-menu-trigger');
assert.dom('.popup-menu-content li').exists({ count: 2 }, 'has both options');
await click('[data-test-popup-menu-trigger]');
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) {

View File

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

View File

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