diff --git a/changelog/25321.txt b/changelog/25321.txt new file mode 100644 index 000000000000..247861c69caa --- /dev/null +++ b/changelog/25321.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Use Hds::Dropdown component to replace list view popup menus +``` \ No newline at end of file diff --git a/ui/app/components/identity/_popup-base.js b/ui/app/components/identity/_popup-base.js deleted file mode 100644 index f7ef118a9541..000000000000 --- a/ui/app/components/identity/_popup-base.js +++ /dev/null @@ -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)); - }); - }, - }, -}); diff --git a/ui/app/components/identity/popup-alias.js b/ui/app/components/identity/popup-alias.js index 3579039738da..919cb74ed42e 100644 --- a/ui/app/components/identity/popup-alias.js +++ b/ui/app/components/identity/popup-alias.js @@ -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); + } + } +} diff --git a/ui/app/components/identity/popup-members.js b/ui/app/components/identity/popup-members.js index cd6bffb917f2..fdafd79dba9b 100644 --- a/ui/app/components/identity/popup-members.js +++ b/ui/app/components/identity/popup-members.js @@ -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'; - -export default Base.extend({ - model: alias('params.firstObject'), - - groupArray: computed('params', function () { - return this.params.objectAt(1); - }), - - memberId: computed('params', function () { - return this.params.objectAt(2); - }), - - 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(); - }, -}); +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 class IdentityPopupMembers extends Component { + @service flashMessages; + @tracked showConfirmModal = false; + + 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}`); + } + + 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(); + } + + @action + async removeGroup() { + const memberId = this.args.memberId; + try { + await this.transaction(); + this.onSuccess(memberId); + } catch (e) { + this.onError(e, memberId); + } + } +} diff --git a/ui/app/components/identity/popup-metadata.js b/ui/app/components/identity/popup-metadata.js index c9097f2e596b..2089f4a90e9c 100644 --- a/ui/app/components/identity/popup-metadata.js +++ b/ui/app/components/identity/popup-metadata.js @@ -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); + } + } +} diff --git a/ui/app/components/identity/popup-policy.js b/ui/app/components/identity/popup-policy.js index 94eb5d9244ee..591e3d17e80a 100644 --- a/ui/app/components/identity/popup-policy.js +++ b/ui/app/components/identity/popup-policy.js @@ -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); + } + } +} diff --git a/ui/app/components/secret-list/aws-role-item.js b/ui/app/components/secret-list/aws-role-item.js new file mode 100644 index 000000000000..cd95580a36b7 --- /dev/null +++ b/ui/app/components/secret-list/aws-role-item.js @@ -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; +} diff --git a/ui/app/components/secret-list/database-list-item.js b/ui/app/components/secret-list/database-list-item.js index 46adb4c0e303..84a9cb3e850a 100644 --- a/ui/app/components/secret-list/database-list-item.js +++ b/ui/app/components/secret-list/database-list-item.js @@ -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)); } } diff --git a/ui/app/components/secret-list/item.js b/ui/app/components/secret-list/item.js new file mode 100644 index 000000000000..db17c97fda82 --- /dev/null +++ b/ui/app/components/secret-list/item.js @@ -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; +} diff --git a/ui/app/components/secret-list/ssh-role-item.js b/ui/app/components/secret-list/ssh-role-item.js new file mode 100644 index 000000000000..d753751d71cf --- /dev/null +++ b/ui/app/components/secret-list/ssh-role-item.js @@ -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; +} diff --git a/ui/app/controllers/vault/cluster/access/identity/index.js b/ui/app/controllers/vault/cluster/access/identity/index.js index db9da7a80abd..bb8eb9843ada 100644 --- a/ui/app/controllers/vault/cluster/access/identity/index.js +++ b/ui/app/controllers/vault/cluster/access/identity/index.js @@ -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(); diff --git a/ui/app/controllers/vault/cluster/policies/index.js b/ui/app/controllers/vault/cluster/policies/index.js index 4d41580889af..355f6cd4f543 100644 --- a/ui/app/controllers/vault/cluster/policies/index.js +++ b/ui/app/controllers/vault/cluster/policies/index.js @@ -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)); }, }, }); diff --git a/ui/app/controllers/vault/cluster/secrets/backends.js b/ui/app/controllers/vault/cluster/secrets/backends.js index a24396f7ba57..8c8d7f10c6ad 100644 --- a/ui/app/controllers/vault/cluster/secrets/backends.js +++ b/ui/app/controllers/vault/cluster/secrets/backends.js @@ -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; } } } diff --git a/ui/app/models/identity/group.js b/ui/app/models/identity/group.js index 79e5f3efe783..2f25e6ec7547 100644 --- a/ui/app/models/identity/group.js +++ b/ui/app/models/identity/group.js @@ -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'), }); diff --git a/ui/app/models/oidc/client.js b/ui/app/models/oidc/client.js index 2829d29b50d7..be4761ed68b3 100644 --- a/ui/app/models/oidc/client.js +++ b/ui/app/models/oidc/client.js @@ -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; } } diff --git a/ui/app/models/oidc/provider.js b/ui/app/models/oidc/provider.js index c2d72dff3158..36ed9d9334c9 100644 --- a/ui/app/models/oidc/provider.js +++ b/ui/app/models/oidc/provider.js @@ -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; } } diff --git a/ui/app/templates/components/identity/item-aliases.hbs b/ui/app/templates/components/identity/item-aliases.hbs index ccc6a0fee457..f0fffc814c1d 100644 --- a/ui/app/templates/components/identity/item-aliases.hbs +++ b/ui/app/templates/components/identity/item-aliases.hbs @@ -24,7 +24,7 @@
- +
diff --git a/ui/app/templates/components/identity/item-members.hbs b/ui/app/templates/components/identity/item-members.hbs index e153e9a068a6..3b149199f04e 100644 --- a/ui/app/templates/components/identity/item-members.hbs +++ b/ui/app/templates/components/identity/item-members.hbs @@ -18,7 +18,7 @@
{{#if @model.canEdit}} - + {{/if}}
@@ -38,7 +38,7 @@
{{#if @model.canEdit}} - + {{/if}}
diff --git a/ui/app/templates/components/identity/item-metadata.hbs b/ui/app/templates/components/identity/item-metadata.hbs index 6fe217e45968..a9012398ef93 100644 --- a/ui/app/templates/components/identity/item-metadata.hbs +++ b/ui/app/templates/components/identity/item-metadata.hbs @@ -16,7 +16,7 @@
{{#if @model.canEdit}} - + {{/if}}
diff --git a/ui/app/templates/components/identity/item-policies.hbs b/ui/app/templates/components/identity/item-policies.hbs index e94df943a7ee..987859059900 100644 --- a/ui/app/templates/components/identity/item-policies.hbs +++ b/ui/app/templates/components/identity/item-policies.hbs @@ -17,7 +17,7 @@
{{#if @model.canEdit}} - + {{/if}}
diff --git a/ui/app/templates/components/identity/popup-alias.hbs b/ui/app/templates/components/identity/popup-alias.hbs index ffeb2d9572eb..b73c9a728db0 100644 --- a/ui/app/templates/components/identity/popup-alias.hbs +++ b/ui/app/templates/components/identity/popup-alias.hbs @@ -3,43 +3,38 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - - {{#let (get this.params "0") as |item|}} - - {{/let}} - \ No newline at end of file +
+ + + + {{#if @item.updatePath.isPending}} + + + + {{else}} + {{#if @item.canEdit}} + + {{/if}} + {{#if @item.canDelete}} + + {{/if}} + {{/if}} + +
+ +{{#if this.showConfirmModal}} + +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/identity/popup-members.hbs b/ui/app/templates/components/identity/popup-members.hbs index 0fd07c556127..ff2b30441421 100644 --- a/ui/app/templates/components/identity/popup-members.hbs +++ b/ui/app/templates/components/identity/popup-members.hbs @@ -3,18 +3,24 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - - - \ No newline at end of file +
+ + + + +
+ +{{#if this.showConfirmModal}} + +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/identity/popup-metadata.hbs b/ui/app/templates/components/identity/popup-metadata.hbs index c40109151ea0..9954f1c78534 100644 --- a/ui/app/templates/components/identity/popup-metadata.hbs +++ b/ui/app/templates/components/identity/popup-metadata.hbs @@ -3,18 +3,19 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - - - \ No newline at end of file +
+ + + + +
+ +{{#if this.showConfirmModal}} + +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/identity/popup-policy.hbs b/ui/app/templates/components/identity/popup-policy.hbs index fc77051cc602..eef6d33d5829 100644 --- a/ui/app/templates/components/identity/popup-policy.hbs +++ b/ui/app/templates/components/identity/popup-policy.hbs @@ -3,28 +3,30 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - - - \ No newline at end of file +
+ + + + + + +
+ +{{#if this.showConfirmModal}} + +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/mfa/login-enforcement-list-item.hbs b/ui/app/templates/components/mfa/login-enforcement-list-item.hbs index 544e0980d6aa..1827e60c27b5 100644 --- a/ui/app/templates/components/mfa/login-enforcement-list-item.hbs +++ b/ui/app/templates/components/mfa/login-enforcement-list-item.hbs @@ -19,30 +19,26 @@
- - - + + + + +
diff --git a/ui/app/templates/components/mfa/method-list-item.hbs b/ui/app/templates/components/mfa/method-list-item.hbs index baff7ab01e66..75a719a5a3e6 100644 --- a/ui/app/templates/components/mfa/method-list-item.hbs +++ b/ui/app/templates/components/mfa/method-list-item.hbs @@ -30,30 +30,26 @@
- - - + + + + +
diff --git a/ui/app/templates/components/oidc/client-list.hbs b/ui/app/templates/components/oidc/client-list.hbs index 3473e135f3ae..27f00eeea09c 100644 --- a/ui/app/templates/components/oidc/client-list.hbs +++ b/ui/app/templates/components/oidc/client-list.hbs @@ -24,32 +24,32 @@
- - - + {{#if (or client.canRead client.canEdit)}} + + + {{#if client.canRead}} + + {{/if}} + {{#if client.canEdit}} + + {{/if}} + + {{/if}}
diff --git a/ui/app/templates/components/oidc/provider-list.hbs b/ui/app/templates/components/oidc/provider-list.hbs index 401f69039306..d684399fb062 100644 --- a/ui/app/templates/components/oidc/provider-list.hbs +++ b/ui/app/templates/components/oidc/provider-list.hbs @@ -24,32 +24,32 @@
- - - + {{#if (or provider.canRead provider.canEdit)}} + + + {{#if provider.canRead}} + + {{/if}} + {{#if provider.canEdit}} + + {{/if}} + + {{/if}}
diff --git a/ui/app/templates/components/secret-list/aws-role-item.hbs b/ui/app/templates/components/secret-list/aws-role-item.hbs index 10a67616b30b..a995f16e4f15 100644 --- a/ui/app/templates/components/secret-list/aws-role-item.hbs +++ b/ui/app/templates/components/secret-list/aws-role-item.hbs @@ -23,58 +23,60 @@
- - - + + + {{#if @item.generatePath.isPending}} + + + + {{else if @item.canGenerate}} + + {{/if}} + {{#if @item.updatePath.isPending}} + + + + {{else}} + {{#if @item.canRead}} + + {{/if}} + {{#if @item.canEdit}} + + {{/if}} + {{#if @item.canDelete}} + + {{/if}} + {{/if}} +
- \ No newline at end of file + + +{{#if this.showConfirmModal}} + +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/secret-list/database-list-item.hbs b/ui/app/templates/components/secret-list/database-list-item.hbs index 8a65fc9297e9..409429dfd3aa 100644 --- a/ui/app/templates/components/secret-list/database-list-item.hbs +++ b/ui/app/templates/components/secret-list/database-list-item.hbs @@ -26,77 +26,56 @@
- - - + + + {{#if @item.canEdit}} + + {{/if}} + {{#if @item.canEditRole}} + + {{/if}} + {{#if @item.canReset}} + + {{/if}} + {{#if (and (eq @item.type "dynamic") @item.canGenerateCredentials)}} + + {{else if (and (eq @item.type "static") @item.canGetCredentials)}} + + {{/if}} + {{#if (and @item.canRotateRoleCredentials (eq this.keyTypeValue "static"))}} + + {{/if}} + {{#if @item.canRotateRoot}} + + {{/if}} +
\ No newline at end of file diff --git a/ui/app/templates/components/secret-list/item.hbs b/ui/app/templates/components/secret-list/item.hbs index 652d96444100..26f7c704fc46 100644 --- a/ui/app/templates/components/secret-list/item.hbs +++ b/ui/app/templates/components/secret-list/item.hbs @@ -30,56 +30,57 @@
- - - + {{/if}} + {{/if}} +
- \ No newline at end of file + + +{{#if this.showConfirmModal}} + +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/secret-list/ssh-role-item.hbs b/ui/app/templates/components/secret-list/ssh-role-item.hbs index 26294b6683f2..aa6c295704e8 100644 --- a/ui/app/templates/components/secret-list/ssh-role-item.hbs +++ b/ui/app/templates/components/secret-list/ssh-role-item.hbs @@ -36,89 +36,86 @@
{{#if (eq @backendType "ssh")}} - - - + + + {{#if (eq @item.keyType "otp")}} + {{#if @item.generatePath.isPending}} + + + + {{else if @item.canGenerate}} + + {{/if}} + {{else if (eq @item.keyType "ca")}} + {{#if @item.signPath.isPending}} + + + + {{else if @item.canGenerate}} + + {{/if}} + {{/if}} + {{#if @loadingToggleZeroAddress}} + + + + {{else if @item.canEditZeroAddress}} + + {{/if}} + {{#if @item.updatePath.isPending}} + + + + {{else}} + {{#if @item.canRead}} + + {{/if}} + {{#if @item.canEdit}} + + {{/if}} + {{#if @item.canDelete}} + + {{/if}} + {{/if}} + {{/if}}
- \ No newline at end of file + + +{{#if this.showConfirmModal}} + +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/secret-list/transform-list-item.hbs b/ui/app/templates/components/secret-list/transform-list-item.hbs index 2b7bf38718f8..18c176ac0db7 100644 --- a/ui/app/templates/components/secret-list/transform-list-item.hbs +++ b/ui/app/templates/components/secret-list/transform-list-item.hbs @@ -25,26 +25,20 @@
{{#if (or @item.updatePath.canRead @item.updatePath.canUpdate)}} - - - + + + {{#if @item.updatePath.canRead}} + + {{/if}} + {{#if @item.updatePath.canUpdate}} + + {{/if}} + {{/if}}
@@ -55,17 +49,13 @@
{{#if this.isBuiltin}} - - - {{@item.id}} - - -
- This is a built-in HashiCorp - {{@itemType}}. It can't be viewed or edited. -
-
-
+ + {{@item.id}} + {{else}} {{@item.id}} {{/if}} diff --git a/ui/app/templates/components/secret-list/transform-transformation-item.hbs b/ui/app/templates/components/secret-list/transform-transformation-item.hbs index 798902453083..30095e1cb0e1 100644 --- a/ui/app/templates/components/secret-list/transform-transformation-item.hbs +++ b/ui/app/templates/components/secret-list/transform-transformation-item.hbs @@ -3,66 +3,42 @@ SPDX-License-Identifier: BUSL-1.1 ~}} -{{! CBS TODO do not let click if !canRead }} -{{#if (eq @options.item "transformation")}} - -
-
- - - {{if (eq @item.id " ") "(self)" (or @item.keyWithoutParent @item.id)}} - -
-
- {{#if (or @item.updatePath.canRead @item.updatePath.canUpdate)}} - - - - {{/if}} -
-
-
-{{else}} -
-
-
+ +
+
+ {{if (eq @item.id " ") "(self)" (or @item.keyWithoutParent @item.id)}} -
+ +
+
+ {{#if (or @item.updatePath.canRead @item.updatePath.canUpdate)}} + + + {{#if @item.updatePath.canRead}} + + {{/if}} + {{#if @item.updatePath.canUpdate}} + + {{/if}} + + {{/if}}
-{{/if}} \ No newline at end of file + \ No newline at end of file diff --git a/ui/app/templates/components/transit-form-show.hbs b/ui/app/templates/components/transit-form-show.hbs index f08210e264f9..559be499fc2a 100644 --- a/ui/app/templates/components/transit-form-show.hbs +++ b/ui/app/templates/components/transit-form-show.hbs @@ -151,21 +151,15 @@
- - - + + + +
diff --git a/ui/app/templates/components/wizard-content.hbs b/ui/app/templates/components/wizard-content.hbs index e189376fea78..61a50dd5c4bd 100644 --- a/ui/app/templates/components/wizard-content.hbs +++ b/ui/app/templates/components/wizard-content.hbs @@ -5,15 +5,10 @@
{{#unless this.hidePopup}} - - - + + + + {{/unless}}

diff --git a/ui/app/templates/vault/cluster/access/identity/aliases/index.hbs b/ui/app/templates/vault/cluster/access/identity/aliases/index.hbs index d3d2183d6821..6dca7b8eafd5 100644 --- a/ui/app/templates/vault/cluster/access/identity/aliases/index.hbs +++ b/ui/app/templates/vault/cluster/access/identity/aliases/index.hbs @@ -31,7 +31,7 @@

- +
diff --git a/ui/app/templates/vault/cluster/access/identity/index.hbs b/ui/app/templates/vault/cluster/access/identity/index.hbs index 88cd07db0461..de21aa26f668 100644 --- a/ui/app/templates/vault/cluster/access/identity/index.hbs +++ b/ui/app/templates/vault/cluster/access/identity/index.hbs @@ -9,7 +9,7 @@
@@ -32,63 +32,53 @@ {{/if}}
- - - + {{/if}} + {{#if item.canEdit}} + + {{#if item.disabled}} + + {{else if (eq this.identityType "entity")}} + + {{/if}} + {{/if}} + {{#if item.canDelete}} + + {{/if}} + {{/if}} +
@@ -117,4 +107,22 @@ @iconPosition="trailing" /> +{{/if}} + +{{#if this.entityToDisable}} + +{{/if}} + +{{#if this.itemToDelete}} + {{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/access/mfa/enforcements/enforcement/index.hbs b/ui/app/templates/vault/cluster/access/mfa/enforcements/enforcement/index.hbs index a93bce4935de..f84bd9eb9860 100644 --- a/ui/app/templates/vault/cluster/access/mfa/enforcements/enforcement/index.hbs +++ b/ui/app/templates/vault/cluster/access/mfa/enforcements/enforcement/index.hbs @@ -83,17 +83,20 @@ {{#if target.link}}
- - - + + + +
{{/if}} diff --git a/ui/app/templates/vault/cluster/access/oidc/assignments/index.hbs b/ui/app/templates/vault/cluster/access/oidc/assignments/index.hbs index a767d31f9fd7..25f46ddbb020 100644 --- a/ui/app/templates/vault/cluster/access/oidc/assignments/index.hbs +++ b/ui/app/templates/vault/cluster/access/oidc/assignments/index.hbs @@ -38,32 +38,28 @@ {{#if (not-eq model.name "allow_all")}}
- - - + + + + +
{{/if}} diff --git a/ui/app/templates/vault/cluster/access/oidc/keys/index.hbs b/ui/app/templates/vault/cluster/access/oidc/keys/index.hbs index 2c81e37029f4..2995b2cb5347 100644 --- a/ui/app/templates/vault/cluster/access/oidc/keys/index.hbs +++ b/ui/app/templates/vault/cluster/access/oidc/keys/index.hbs @@ -28,32 +28,28 @@
- - - + + + + +
diff --git a/ui/app/templates/vault/cluster/access/oidc/scopes/index.hbs b/ui/app/templates/vault/cluster/access/oidc/scopes/index.hbs index 5642457be179..0747bde83971 100644 --- a/ui/app/templates/vault/cluster/access/oidc/scopes/index.hbs +++ b/ui/app/templates/vault/cluster/access/oidc/scopes/index.hbs @@ -29,32 +29,28 @@
- - - + + + + +
diff --git a/ui/app/templates/vault/cluster/policies/index.hbs b/ui/app/templates/vault/cluster/policies/index.hbs index 7f477cb6bdf4..df42c3bd8735 100644 --- a/ui/app/templates/vault/cluster/policies/index.hbs +++ b/ui/app/templates/vault/cluster/policies/index.hbs @@ -91,52 +91,44 @@
- - - + + + {{#if item.updatePath.isPending}} + + + + {{else}} + {{#if item.canRead}} + + {{/if}} + {{#if item.canEdit}} + + {{/if}} + {{#if (and item.canDelete (not-eq item.name "default"))}} + + {{/if}} + {{/if}} +
@@ -180,4 +172,14 @@ {{/if}} {{else}} +{{/if}} + +{{#if this.policyToDelete}} + {{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/secrets/backends.hbs b/ui/app/templates/vault/cluster/secrets/backends.hbs index 352bc4e5d9bf..5a86dd7e446c 100644 --- a/ui/app/templates/vault/cluster/secrets/backends.hbs +++ b/ui/app/templates/vault/cluster/secrets/backends.hbs @@ -86,28 +86,39 @@ {{/if}} - {{! meatball sandwich menu }}
- - - + + + + {{#if (not-eq backend.type "cubbyhole")}} + + {{/if}} +
-{{/each}} \ No newline at end of file +{{/each}} + +{{#if this.engineToDisable}} + +{{/if}} \ No newline at end of file diff --git a/ui/lib/config-ui/addon/components/messages/page/list.hbs b/ui/lib/config-ui/addon/components/messages/page/list.hbs index f105c25ab24e..851bd4ab9b52 100644 --- a/ui/lib/config-ui/addon/components/messages/page/list.hbs +++ b/ui/lib/config-ui/addon/components/messages/page/list.hbs @@ -56,28 +56,22 @@
- - - + {{#if (or message.canEditCustomMessages message.canDeleteCustomMessages)}} + + + {{#if message.canEditCustomMessages}} + + {{/if}} + {{#if message.canDeleteCustomMessages}} + + {{/if}} + + {{/if}}
@@ -117,4 +111,13 @@ +{{/if}} + +{{#if this.messageToDelete}} + {{/if}} \ No newline at end of file diff --git a/ui/lib/config-ui/addon/components/messages/page/list.js b/ui/lib/config-ui/addon/components/messages/page/list.js index f97e477555b5..6ed34a1e4b03 100644 --- a/ui/lib/config-ui/addon/components/messages/page/list.js +++ b/ui/lib/config-ui/addon/components/messages/page/list.js @@ -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; } } diff --git a/ui/lib/core/addon/components/confirm-action.hbs b/ui/lib/core/addon/components/confirm-action.hbs index 2c22eedd8ea8..019d1246fce7 100644 --- a/ui/lib/core/addon/components/confirm-action.hbs +++ b/ui/lib/core/addon/components/confirm-action.hbs @@ -25,44 +25,13 @@ {{/if}} {{#if this.showConfirmModal}} - - {{#if @disabledMessage}} - - Not allowed - - - {{@disabledMessage}} - - - - - {{else}} - - {{or @confirmTitle "Are you sure?"}} - - - {{this.confirmMessage}} - - - - - - - - {{/if}} - + @onConfirm={{this.onConfirm}} + @confirmTitle={{@confirmTitle}} + @confirmMessage={{this.confirmMessage}} + @disabledMessage={{@disabledMessage}} + @isRunning={{@isRunning}} + /> {{/if}} \ No newline at end of file diff --git a/ui/lib/core/addon/components/confirm-modal.hbs b/ui/lib/core/addon/components/confirm-modal.hbs new file mode 100644 index 000000000000..c6021b0bfd34 --- /dev/null +++ b/ui/lib/core/addon/components/confirm-modal.hbs @@ -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 }} + + + {{#if @disabledMessage}} + + Not allowed + + + {{@disabledMessage}} + + + + + {{else}} + + {{or @confirmTitle "Are you sure?"}} + + + {{or @confirmMessage "You will not be able to recover it later."}} + + + + + + + + {{/if}} + \ No newline at end of file diff --git a/ui/lib/core/addon/components/confirmation-modal.js b/ui/lib/core/addon/components/confirmation-modal.js index 06878dfefd35..6a5a4bd6b6b6 100644 --- a/ui/lib/core/addon/components/confirmation-modal.js +++ b/ui/lib/core/addon/components/confirmation-modal.js @@ -5,7 +5,8 @@ import Component from '@glimmer/component'; /** * @module ConfirmationModal - * ConfirmationModal components wrap the component to present a critical (red) type-to-confirm modal. + * ConfirmationModal components wrap the 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 diff --git a/ui/lib/core/app/components/confirm-modal.js b/ui/lib/core/app/components/confirm-modal.js new file mode 100644 index 000000000000..e4ceff2d8c1d --- /dev/null +++ b/ui/lib/core/app/components/confirm-modal.js @@ -0,0 +1,6 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +export { default } from 'core/components/confirm-modal'; diff --git a/ui/lib/kv/addon/components/page/list.hbs b/ui/lib/kv/addon/components/page/list.hbs index ff9436c2f03a..f503ef29d153 100644 --- a/ui/lib/kv/addon/components/page/list.hbs +++ b/ui/lib/kv/addon/components/page/list.hbs @@ -71,57 +71,55 @@
- - - + + + {{#if metadata.pathIsDirectory}} + + {{else}} + + {{#if metadata.canReadMetadata}} + + {{/if}} + {{#if metadata.canCreateVersionData}} + + {{/if}} + {{#if metadata.canDeleteMetadata}} + + {{/if}} + {{/if}} +
{{/each}} + {{#if this.metadataToDelete}} + + {{/if}} {{! Pagination }}
- - - + + + + {{#if (and @metadata.canCreateVersionData (not versionData.destroyed) (not versionData.isSecretDeleted))}} + + {{/if}} +
diff --git a/ui/lib/pki/addon/components/page/pki-issuer-list.hbs b/ui/lib/pki/addon/components/page/pki-issuer-list.hbs index 95ee744127df..b6d5ab63f5b8 100644 --- a/ui/lib/pki/addon/components/page/pki-issuer-list.hbs +++ b/ui/lib/pki/addon/components/page/pki-issuer-list.hbs @@ -84,22 +84,21 @@
- - - + + + + +
diff --git a/ui/lib/pki/addon/components/page/pki-key-list.hbs b/ui/lib/pki/addon/components/page/pki-key-list.hbs index 08131d0247ce..a8d13458f422 100644 --- a/ui/lib/pki/addon/components/page/pki-key-list.hbs +++ b/ui/lib/pki/addon/components/page/pki-key-list.hbs @@ -40,32 +40,32 @@
- - - + {{#if (or @canRead @canEdit)}} + + + {{#if @canRead}} + + {{/if}} + {{#if @canEdit}} + + {{/if}} + + {{/if}}
diff --git a/ui/lib/pki/addon/templates/certificates/index.hbs b/ui/lib/pki/addon/templates/certificates/index.hbs index f14b4bb12560..96e2d394af36 100644 --- a/ui/lib/pki/addon/templates/certificates/index.hbs +++ b/ui/lib/pki/addon/templates/certificates/index.hbs @@ -26,17 +26,15 @@
- - - + + + +
diff --git a/ui/lib/pki/addon/templates/roles/index.hbs b/ui/lib/pki/addon/templates/roles/index.hbs index 265a2c192f0d..956466968c4a 100644 --- a/ui/lib/pki/addon/templates/roles/index.hbs +++ b/ui/lib/pki/addon/templates/roles/index.hbs @@ -27,22 +27,16 @@
- - - + + + + +
diff --git a/ui/lib/replication/addon/controllers/application.js b/ui/lib/replication/addon/controllers/application.js index ed72429dd1cb..a4674791d903 100644 --- a/ui/lib/replication/addon/controllers/application.js +++ b/ui/lib/replication/addon/controllers/application.js @@ -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: { diff --git a/ui/lib/replication/addon/templates/mode/secondaries/index.hbs b/ui/lib/replication/addon/templates/mode/secondaries/index.hbs index d41be803a22e..5f5b63cf30e8 100644 --- a/ui/lib/replication/addon/templates/mode/secondaries/index.hbs +++ b/ui/lib/replication/addon/templates/mode/secondaries/index.hbs @@ -29,34 +29,29 @@
{{#if (or (eq this.replicationMode "performance") this.model.canRevokeSecondary)}} - - - + + + {{#if (eq this.replicationMode "performance")}} + + {{/if}} + {{#if this.model.canRevokeSecondary}} + + {{/if}} + {{/if}}
@@ -76,4 +71,14 @@ /> {{/if}} +{{/if}} + +{{#if this.secondaryToRevoke}} + {{/if}} \ No newline at end of file diff --git a/ui/lib/sync/addon/components/secrets/page/overview.hbs b/ui/lib/sync/addon/components/secrets/page/overview.hbs index 1273e241a21e..bc422385dd1b 100644 --- a/ui/lib/sync/addon/components/secrets/page/overview.hbs +++ b/ui/lib/sync/addon/components/secrets/page/overview.hbs @@ -90,7 +90,7 @@ {{/if}} - + `[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( diff --git a/ui/tests/acceptance/access/identity/entities/index-test.js b/ui/tests/acceptance/access/identity/entities/index-test.js index fa9bcc5de414..8bb80fb28fab 100644 --- a/ui/tests/acceptance/access/identity/entities/index-test.js +++ b/ui/tests/acceptance/access/identity/entities/index-test.js @@ -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); + }); }); diff --git a/ui/tests/acceptance/mfa-method-test.js b/ui/tests/acceptance/mfa-method-test.js index 107c0648f01b..afc5616ff016 100644 --- a/ui/tests/acceptance/mfa-method-test.js +++ b/ui/tests/acceptance/mfa-method-test.js @@ -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']; diff --git a/ui/tests/acceptance/pki/pki-engine-workflow-test.js b/ui/tests/acceptance/pki/pki-engine-workflow-test.js index de3a2bf81150..395bf41c19a4 100644 --- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js +++ b/ui/tests/acceptance/pki/pki-engine-workflow-test.js @@ -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'); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js index 3ea21ed28e78..bf94b8018838 100644 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js @@ -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) { diff --git a/ui/tests/acceptance/secrets/backend/ssh/role-test.js b/ui/tests/acceptance/secrets/backend/ssh/role-test.js index fee830da1bbe..f4d3ab8d932c 100644 --- a/ui/tests/acceptance/secrets/backend/ssh/role-test.js +++ b/ui/tests/acceptance/secrets/backend/ssh/role-test.js @@ -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) { diff --git a/ui/tests/acceptance/transit-test.js b/ui/tests/acceptance/transit-test.js index 82d89de6e01e..4587150a2523 100644 --- a/ui/tests/acceptance/transit-test.js +++ b/ui/tests/acceptance/transit-test.js @@ -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); diff --git a/ui/tests/helpers/pki/workflow.js b/ui/tests/helpers/pki/workflow.js index d0c1676898f4..c962265dd630 100644 --- a/ui/tests/helpers/pki/workflow.js +++ b/ui/tests/helpers/pki/workflow.js @@ -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, diff --git a/ui/tests/integration/components/auth-config-form/options-test.js b/ui/tests/integration/components/auth-config-form/options-test.js index edad9d815341..f267dad0f195 100644 --- a/ui/tests/integration/components/auth-config-form/options-test.js +++ b/ui/tests/integration/components/auth-config-form/options-test.js @@ -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``); component.save(); return settled().then(() => { - assert.ok(model.config.serialize.calledOnce); + assert.strictEqual(model.config.serialize.callCount, 1, 'config serialize was called once'); }); }); }); diff --git a/ui/tests/integration/components/confirm-modal-test.js b/ui/tests/integration/components/confirm-modal-test.js new file mode 100644 index 000000000000..464db30283c4 --- /dev/null +++ b/ui/tests/integration/components/confirm-modal-test.js @@ -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``); + 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'); + }); +}); diff --git a/ui/tests/integration/components/kv/page/kv-page-list-test.js b/ui/tests/integration/components/kv/page/kv-page-list-test.js index 001a6cfad324..12c8de3abd04 100644 --- a/ui/tests/integration/components/kv/page/kv-page-list-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-list-test.js @@ -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'); }); diff --git a/ui/tests/integration/components/kv/page/kv-page-version-history-test.js b/ui/tests/integration/components/kv/page/kv-page-version-history-test.js index 012dadbded24..6461d6d0a64b 100644 --- a/ui/tests/integration/components/kv/page/kv-page-version-history-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-version-history-test.js @@ -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.'); }); }); diff --git a/ui/tests/integration/components/oidc/client-list-test.js b/ui/tests/integration/components/oidc/client-list-test.js new file mode 100644 index 000000000000..e65063164892 --- /dev/null +++ b/ui/tests/integration/components/oidc/client-list-test.js @@ -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``); + + 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``); + + 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'); + }); +}); diff --git a/ui/tests/integration/components/oidc/provider-list-test.js b/ui/tests/integration/components/oidc/provider-list-test.js new file mode 100644 index 000000000000..6bb194954cb5 --- /dev/null +++ b/ui/tests/integration/components/oidc/provider-list-test.js @@ -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``); + + 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``); + 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'); + }); +}); diff --git a/ui/tests/integration/components/pki/page/pki-key-list-test.js b/ui/tests/integration/components/pki/page/pki-key-list-test.js index 2562d28c4eda..a9f56c301531 100644 --- a/ui/tests/integration/components/pki/page/pki-key-list-test.js +++ b/ui/tests/integration/components/pki/page/pki-key-list-test.js @@ -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` `); 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) { diff --git a/ui/tests/pages/access/identity/aliases/index.js b/ui/tests/pages/access/identity/aliases/index.js index e43fd116d526..8fd07de9ceb1 100644 --- a/ui/tests/pages/access/identity/aliases/index.js +++ b/ui/tests/pages/access/identity/aliases/index.js @@ -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]'), diff --git a/ui/tests/pages/access/identity/index.js b/ui/tests/pages/access/identity/index.js index 73367d0db1a0..a034edb77707 100644 --- a/ui/tests/pages/access/identity/index.js +++ b/ui/tests/pages/access/identity/index.js @@ -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]'),