diff --git a/changelog/11530.txt b/changelog/11530.txt
new file mode 100644
index 000000000000..95bb8be7c20b
--- /dev/null
+++ b/changelog/11530.txt
@@ -0,0 +1,3 @@
+```release-note:improvement
+ui: Redesign of KV 2 Delete toolbar.
+```
\ No newline at end of file
diff --git a/ui/app/adapters/secret-v2-version.js b/ui/app/adapters/secret-v2-version.js
index 5aac49d731d9..5bdd02129566 100644
--- a/ui/app/adapters/secret-v2-version.js
+++ b/ui/app/adapters/secret-v2-version.js
@@ -70,14 +70,27 @@ export default ApplicationAdapter.extend({
v2DeleteOperation(store, id, deleteType = 'delete') {
let [backend, path, version] = JSON.parse(id);
-
- // deleteType should be 'delete', 'destroy', 'undelete'
- return this.ajax(this._url(backend, path, deleteType), 'POST', { data: { versions: [version] } }).then(
- () => {
- let model = store.peekRecord('secret-v2-version', id);
- return model && model.rollbackAttributes() && model.reload();
- }
- );
+ // deleteType should be 'delete', 'destroy', 'undelete', 'delete-latest-version', 'destroy-version'
+ if ((!version && deleteType === 'delete') || deleteType === 'delete-latest-version') {
+ return this.ajax(this._url(backend, path, 'data'), 'DELETE')
+ .then(() => {
+ let model = store.peekRecord('secret-v2-version', id);
+ return model && model.rollbackAttributes() && model.reload();
+ })
+ .catch(e => {
+ return e;
+ });
+ } else {
+ return this.ajax(this._url(backend, path, deleteType), 'POST', { data: { versions: [version] } })
+ .then(() => {
+ let model = store.peekRecord('secret-v2-version', id);
+ // potential that model.reload() is never called.
+ return model && model.rollbackAttributes() && model.reload();
+ })
+ .catch(e => {
+ return e;
+ });
+ }
},
handleResponse(status, headers, payload, requestData) {
diff --git a/ui/app/components/secret-delete-menu.js b/ui/app/components/secret-delete-menu.js
new file mode 100644
index 000000000000..e5feb32e7634
--- /dev/null
+++ b/ui/app/components/secret-delete-menu.js
@@ -0,0 +1,148 @@
+import Ember from 'ember';
+import { inject as service } from '@ember/service';
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { alias } from '@ember/object/computed';
+import { maybeQueryRecord } from 'vault/macros/maybe-query-record';
+
+const getErrorMessage = errors => {
+ let errorMessage = errors?.join('. ') || 'Something went wrong. Check the Vault logs for more information.';
+ return errorMessage;
+};
+export default class SecretDeleteMenu extends Component {
+ @service store;
+ @service router;
+ @service flashMessages;
+
+ @tracked showDeleteModal = false;
+
+ @maybeQueryRecord(
+ 'capabilities',
+ context => {
+ if (!context.args.model) {
+ return;
+ }
+ let backend = context.args.model.backend;
+ let id = context.args.model.id;
+ let path = context.args.isV2
+ ? `${encodeURIComponent(backend)}/data/${encodeURIComponent(id)}`
+ : `${encodeURIComponent(backend)}/${encodeURIComponent(id)}`;
+ return {
+ id: path,
+ };
+ },
+ 'isV2',
+ 'model',
+ 'model.id',
+ 'mode'
+ )
+ updatePath;
+ @alias('updatePath.canDelete') canDelete;
+ @alias('updatePath.canUpdate') canUpdate;
+
+ @maybeQueryRecord(
+ 'capabilities',
+ context => {
+ if (!context.args || !context.args.modelForData || !context.args.modelForData.id) return;
+ let [backend, id] = JSON.parse(context.args.modelForData.id);
+ return {
+ id: `${encodeURIComponent(backend)}/delete/${encodeURIComponent(id)}`,
+ };
+ },
+ 'model.id'
+ )
+ deleteVersionPath;
+ @alias('deleteVersionPath.canUpdate') canDeleteAnyVersion;
+
+ @maybeQueryRecord(
+ 'capabilities',
+ context => {
+ if (!context.args || !context.args.modelForData || !context.args.modelForData.id) return;
+ let [backend, id] = JSON.parse(context.args.modelForData.id);
+ return {
+ id: `${encodeURIComponent(backend)}/undelete/${encodeURIComponent(id)}`,
+ };
+ },
+ 'model.id'
+ )
+ undeleteVersionPath;
+ @alias('undeleteVersionPath.canUpdate') canUndeleteVersion;
+
+ @maybeQueryRecord(
+ 'capabilities',
+ context => {
+ if (!context.args || !context.args.modelForData || !context.args.modelForData.id) return;
+ let [backend, id] = JSON.parse(context.args.modelForData.id);
+ return {
+ id: `${encodeURIComponent(backend)}/destroy/${encodeURIComponent(id)}`,
+ };
+ },
+ 'model.id'
+ )
+ destroyVersionPath;
+ @alias('destroyVersionPath.canUpdate') canDestroyVersion;
+
+ @maybeQueryRecord(
+ 'capabilities',
+ context => {
+ if (!context.args.model || !context.args.model.engine || !context.args.model.id) return;
+ let backend = context.args.model.engine.id;
+ let id = context.args.model.id;
+ return {
+ id: `${encodeURIComponent(backend)}/metadata/${encodeURIComponent(id)}`,
+ };
+ },
+ 'model',
+ 'model.id',
+ 'mode'
+ )
+ v2UpdatePath;
+ @alias('v2UpdatePath.canDelete') canDestroyAllVersions;
+
+ get isLatestVersion() {
+ let { model } = this.args;
+ if (!model) return false;
+ let latestVersion = model.currentVersion;
+ let selectedVersion = model.selectedVersion.version;
+ if (latestVersion !== selectedVersion) {
+ return false;
+ }
+ return true;
+ }
+
+ @action
+ handleDelete(deleteType) {
+ // deleteType should be 'delete', 'destroy', 'undelete', 'delete-latest-version', 'destroy-all-versions'
+ if (!deleteType) {
+ return;
+ }
+ if (deleteType === 'destroy-all-versions' || deleteType === 'v1') {
+ let { id } = this.args.model;
+ this.args.model.destroyRecord().then(() => {
+ this.args.navToNearestAncestor.perform(id);
+ });
+ } else {
+ return this.store
+ .adapterFor('secret-v2-version')
+ .v2DeleteOperation(this.store, this.args.modelForData.id, deleteType)
+ .then(resp => {
+ if (Ember.testing) {
+ return;
+ }
+ if (!resp) {
+ this.showDeleteModal = false;
+ this.args.refresh();
+ return;
+ }
+ if (resp.isAdapterError) {
+ const errorMessage = getErrorMessage(resp.errors);
+ this.flashMessages.danger(errorMessage);
+ } else {
+ // not likely to ever get to this situation, but adding just in case.
+ location.reload();
+ }
+ });
+ }
+ }
+}
diff --git a/ui/app/components/secret-edit.js b/ui/app/components/secret-edit.js
index 4adbc148bbc0..3c049b610e5b 100644
--- a/ui/app/components/secret-edit.js
+++ b/ui/app/components/secret-edit.js
@@ -112,7 +112,7 @@ export default Component.extend(FocusOnInsertMixin, WithNavToNearestAncestor, {
'model.id',
'mode'
),
- canDelete: alias('model.canDelete'),
+ canDelete: alias('updatePath.canDelete'),
canEdit: alias('updatePath.canUpdate'),
v2UpdatePath: maybeQueryRecord(
diff --git a/ui/app/components/secret-version-menu.js b/ui/app/components/secret-version-menu.js
index f698870d05d1..b4aa0db982a0 100644
--- a/ui/app/components/secret-version-menu.js
+++ b/ui/app/components/secret-version-menu.js
@@ -1,60 +1,5 @@
-import { maybeQueryRecord } from 'vault/macros/maybe-query-record';
-import Component from '@ember/component';
-import { inject as service } from '@ember/service';
-import { alias, or } from '@ember/object/computed';
+import Component from '@glimmer/component';
-export default Component.extend({
- tagName: '',
- store: service(),
- version: null,
- useDefaultTrigger: false,
- onRefresh() {},
-
- deleteVersionPath: maybeQueryRecord(
- 'capabilities',
- context => {
- let [backend, id] = JSON.parse(context.version.id);
- return {
- id: `${backend}/delete/${id}`,
- };
- },
- 'version.id'
- ),
- canDeleteVersion: alias('deleteVersionPath.canUpdate'),
- destroyVersionPath: maybeQueryRecord(
- 'capabilities',
- context => {
- let [backend, id] = JSON.parse(context.version.id);
- return {
- id: `${backend}/destroy/${id}`,
- };
- },
- 'version.id'
- ),
- canDestroyVersion: alias('destroyVersionPath.canUpdate'),
- undeleteVersionPath: maybeQueryRecord(
- 'capabilities',
- context => {
- let [backend, id] = JSON.parse(context.version.id);
- return {
- id: `${backend}/undelete/${id}`,
- };
- },
- 'version.id'
- ),
- canUndeleteVersion: alias('undeleteVersionPath.canUpdate'),
-
- isFetchingVersionCapabilities: or(
- 'deleteVersionPath.isPending',
- 'destroyVersionPath.isPending',
- 'undeleteVersionPath.isPending'
- ),
- actions: {
- deleteVersion(deleteType = 'destroy') {
- return this.store
- .adapterFor('secret-v2-version')
- .v2DeleteOperation(this.store, this.version.id, deleteType)
- .then(this.onRefresh);
- },
- },
-});
+export default class SecretVersionMenu extends Component {
+ onRefresh() {}
+}
diff --git a/ui/app/styles/components/masked-input.scss b/ui/app/styles/components/masked-input.scss
index 65f03e0cba32..32f6c5aac90b 100644
--- a/ui/app/styles/components/masked-input.scss
+++ b/ui/app/styles/components/masked-input.scss
@@ -66,7 +66,6 @@
height: auto;
line-height: 1rem;
min-width: $spacing-l;
- z-index: 100;
border: 0;
box-shadow: none;
color: $grey-light;
diff --git a/ui/app/styles/components/modal.scss b/ui/app/styles/components/modal.scss
index fe629265737a..9378ced2f1df 100644
--- a/ui/app/styles/components/modal.scss
+++ b/ui/app/styles/components/modal.scss
@@ -76,3 +76,17 @@ pre {
background: #f7f8fa;
border-top: 1px solid #bac1cc;
}
+
+.modal-radio-button {
+ display: flex;
+ align-items: baseline;
+ margin-bottom: $spacing-xs;
+
+ input {
+ top: 2px;
+ }
+
+ .helper-text {
+ margin-left: 10px;
+ }
+}
diff --git a/ui/app/templates/components/secret-delete-menu.hbs b/ui/app/templates/components/secret-delete-menu.hbs
new file mode 100644
index 000000000000..614f50e2e403
--- /dev/null
+++ b/ui/app/templates/components/secret-delete-menu.hbs
@@ -0,0 +1,131 @@
+{{#unless @isV2}}
+ {{#if this.canDelete}}
+
There are three ways to delete or destroy the {{@model.id}} secret. Each is different, so be sure to read the below carefully.
+How would you like to proceed?
+ {{#unless @modelForData.destroyed}} + {{#unless @modelForData.deleted}} + {{#if this.canDelete}} + + {{/if}} + {{/unless}} + {{#if this.canDestroyVersion}} + + {{/if}} + {{/unless}} + {{#if this.canDestroyAllVersions}} + + {{/if}} +