KV 2 Toolbar delete redesign (#11530)
* initial setup, modify toolbar header * footer buttons setup * setup first delete version delete method * clean up * handle destory all versions * handle undelete * conditional for modal and undelete * remove delete from version area * modelForData in permissions * setup for soft delete and modify adpater to allow DELETE in additon to POST * dropdown for soft delete * stuck * handle all soft deletes * conditional for destroy all versions * remove old functionality from secret-version-menu * glimmerize secret-version-menu * Updated secret version menu and version history * Updated icons and columns in version history * create new component * clean up * glimmerize secret delete menu * fix undelete * Fixed radio labels in version delete menu * handle v1 delete * refining * handle errors with flash messages * add changelog * fix test * add to test * amend test * address PR comments * whoopies * add urlEncoding Co-authored-by: Arnav Palnitkar <arnav@hashicorp.com>
This commit is contained in:
parent
6b8d7fe2e6
commit
8f5d62139c
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: Redesign of KV 2 Delete toolbar.
|
||||
```
|
|
@ -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(
|
||||
() => {
|
||||
// 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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,6 @@
|
|||
height: auto;
|
||||
line-height: 1rem;
|
||||
min-width: $spacing-l;
|
||||
z-index: 100;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
color: $grey-light;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
{{#unless @isV2}}
|
||||
{{#if this.canDelete}}
|
||||
<ConfirmAction
|
||||
@buttonClasses="toolbar-link"
|
||||
@confirmTitle="Delete secret?"
|
||||
@confirmMessage="You will not be able to recover this secret data later."
|
||||
@onConfirmAction={{action "handleDelete" "v1"}}
|
||||
data-test-secret-v1-delete="true"
|
||||
>
|
||||
Delete
|
||||
</ConfirmAction>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (and this.canUndeleteVersion @modelForData.deleted)}}
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-link"
|
||||
onclick={{action this.handleDelete "undelete"}}
|
||||
data-test-secret-undelete
|
||||
>
|
||||
Undelete
|
||||
</button>
|
||||
{{/if}}
|
||||
{{#if (and (not @modelForData.deleted) (not @modelForData.destroyed)) }}
|
||||
{{#if (or this.canDestroyVersion this.canDestroyAllVersions)}}
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-link"
|
||||
{{on "click" (fn (mut this.showDeleteModal false))}}
|
||||
data-test-delete-open-modal
|
||||
>
|
||||
{{if (and (not @modelForData.deleted) (not @modelForData.destroyed)) "Delete" "Destroy"}}
|
||||
</button>
|
||||
<div class="toolbar-separator"/>
|
||||
{{else}}
|
||||
{{#if (or this.canDeleteAnyVersion (and this.isLatestVersion this.canDelete))}}
|
||||
<ConfirmAction
|
||||
@buttonClasses="toolbar-link"
|
||||
@confirmTitle="Delete"
|
||||
@confirmMessage="Deleting this secret removes read access, but the underlying data will not be removed and can be undeleted later."
|
||||
@onConfirmAction={{action "handleDelete" (if canDeleteAnyVersion "delete" "delete-latest-version") }}
|
||||
data-test-secret-v2-delete="true"
|
||||
>
|
||||
Delete
|
||||
</ConfirmAction>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<Modal
|
||||
@title="Delete Secret?"
|
||||
@onClose={{action (mut this.showDeleteModal) false}}
|
||||
@isActive={{this.showDeleteModal}}
|
||||
@type="warning"
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
<p class="has-bottom-margin-s">There are three ways to delete or destroy the <strong>{{@model.id}}</strong> secret. Each is different, so be sure to read the below carefully.</p>
|
||||
<p class="has-bottom-margin-s"><strong>How would you like to proceed?</strong></p>
|
||||
{{#unless @modelForData.destroyed}}
|
||||
{{#unless @modelForData.deleted}}
|
||||
{{#if this.canDelete}}
|
||||
<div class="modal-radio-button" data-test-delete-modal="delete-version">
|
||||
<RadioButton
|
||||
@value="delete"
|
||||
@radioClass="radio"
|
||||
@groupValue={{this.deleteType}}
|
||||
@changed={{action (mut this.deleteType)}}
|
||||
@name="setup-deleteType"
|
||||
@radioId="delete-version"
|
||||
/>
|
||||
<div class="helper-text">
|
||||
<label for="delete-version" data-test-replication-type-select="delete-version"><strong>Delete this version</strong></label>
|
||||
<p>This deletes Version {{@modelForData.version}} of the secret. It can be un-deleted later.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
{{#if this.canDestroyVersion}}
|
||||
<div class="modal-radio-button" data-test-delete-modal="destroy-version">
|
||||
<RadioButton
|
||||
@value="destroy"
|
||||
@radioClass="radio"
|
||||
@groupValue={{this.deleteType}}
|
||||
@changed={{action (mut this.deleteType)}}
|
||||
@name="setup-deleteType"
|
||||
@radioId="destroy-version"
|
||||
/>
|
||||
<div class="helper-text">
|
||||
<label for="destroy-version" data-test-replication-type-select="destroy-version"><strong>Destroy this version</strong></label>
|
||||
<p>Version {{@modelForData.version}} is permanently destroyed and cannot be read or recovered later.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
{{#if this.canDestroyAllVersions}}
|
||||
<div class="modal-radio-button" data-test-delete-modal="destroy-all-versions">
|
||||
<RadioButton
|
||||
@value="destroy-all-versions"
|
||||
@radioClass="radio"
|
||||
@groupValue={{this.deleteType}}
|
||||
@changed={{action (mut this.deleteType)}}
|
||||
@name="setup-deleteType"
|
||||
@radioId="destroy-all-versions"
|
||||
/>
|
||||
<div class="helper-text">
|
||||
<label for="destroy-all-versions" data-test-replication-type-select="destroy-all-versions"><strong>Destroy all versions</strong></label>
|
||||
<p>All secret versions and metadata are permanently destroyed and cannot be read or recovered later.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
type="button"
|
||||
class="button has-text-danger"
|
||||
{{on "click" (fn this.handleDelete this.deleteType (action (mut this.showDeleteModal) false))}}
|
||||
disabled={{if this.deleteType false true}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-secondary"
|
||||
{{on "click" (action (mut this.showDeleteModal) false)}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
{{/unless}}
|
|
@ -33,74 +33,15 @@
|
|||
</ToolbarFilters>
|
||||
{{/unless}}
|
||||
<ToolbarActions>
|
||||
{{#if (and (eq @mode "show") this.isV2 (not @model.failedServerRead))}}
|
||||
<SecretVersionMenu
|
||||
@version={{this.modelForData}}
|
||||
@onRefresh={{action 'refresh'}}
|
||||
{{#if (eq mode 'show')}}
|
||||
<SecretDeleteMenu
|
||||
@modelForData={{this.modelForData}}
|
||||
@model={{this.model}}
|
||||
@navToNearestAncestor={{this.navToNearestAncestor}}
|
||||
@isV2={{isV2}}
|
||||
@refresh={{action 'refresh'}}
|
||||
/>
|
||||
<BasicDropdown
|
||||
@class="popup-menu"
|
||||
@horizontalPosition="auto-right"
|
||||
@verticalPosition="below"
|
||||
as |D|
|
||||
>
|
||||
<D.trigger
|
||||
data-test-popup-menu-trigger="history"
|
||||
@class={{concat "popup-menu-trigger toolbar-link" (if D.isOpen " is-active")}}
|
||||
@tagName="button"
|
||||
>
|
||||
History <Chevron @direction="down" @isButton={{true}} />
|
||||
</D.trigger>
|
||||
<D.content @class="popup-menu-content ">
|
||||
<nav class="box menu">
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@data-test-version-history
|
||||
@mode="versions"
|
||||
@secret={{this.model.id}}
|
||||
@class="has-text-black has-text-weight-semibold has-bottom-shadow"
|
||||
@onLinkClick={{action D.actions.close}}
|
||||
>
|
||||
View version history
|
||||
</SecretLink>
|
||||
</li>
|
||||
</ul>
|
||||
<h5 class="list-header">Versions</h5>
|
||||
<ul class="menu-list">
|
||||
{{#each (reverse this.model.versions) as |secretVersion|}}
|
||||
<li class="action">
|
||||
<LinkTo class="link" @params={{array (query-params version=secretVersion.version)}} @invokeAction={{action D.actions.close}} >
|
||||
Version {{secretVersion.version}}
|
||||
{{#if (and (eq secretVersion.version this.model.currentVersion) (not secretVersion.deleted))}}
|
||||
<Icon @glyph="check-circle-outline" class="has-text-success is-pulled-right" />
|
||||
{{else if secretVersion.deleted}}
|
||||
<Icon @glyph="cancel-square-outline" class="has-text-grey is-pulled-right" />
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</nav>
|
||||
</D.content>
|
||||
</BasicDropdown>
|
||||
<div class="toolbar-separator"/>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and (eq mode 'show') canDelete)}}
|
||||
<ConfirmAction
|
||||
@buttonClasses="toolbar-link"
|
||||
@onConfirmAction={{action "deleteKey"}}
|
||||
@confirmMessage={{if isV2
|
||||
(concat "This will permanently delete all versions of this secret.")
|
||||
(concat "You will not be able to recover this secret data later.")
|
||||
}}
|
||||
data-test-secret-delete="true"
|
||||
>
|
||||
Delete secret
|
||||
</ConfirmAction>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and (eq mode 'show') (or canEditV2Secret canEdit))}}
|
||||
{{#let (concat 'vault.cluster.secrets.backend.' (if (eq mode 'show') 'edit' 'show')) as |targetRoute|}}
|
||||
{{#unless (and isV2 (or isWriteWithoutRead modelForData.destroyed modelForData.deleted))}}
|
||||
|
@ -116,7 +57,7 @@
|
|||
@class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@tagName="button"
|
||||
>
|
||||
Copy secret
|
||||
Copy
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
</D.trigger>
|
||||
<D.content @class="popup-menu-content is-wide">
|
||||
|
@ -159,7 +100,19 @@
|
|||
</D.content>
|
||||
</BasicDropdown>
|
||||
{{/unless}}
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (and (eq @mode "show") this.isV2 (not @model.failedServerRead))}}
|
||||
<SecretVersionMenu
|
||||
@version={{this.modelForData}}
|
||||
@onRefresh={{action 'refresh'}}
|
||||
@model={{this.model}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and (eq mode 'show') (or canEditV2Secret canEdit))}}
|
||||
{{#let (concat 'vault.cluster.secrets.backend.' (if (eq mode 'show') 'edit' 'show')) as |targetRoute|}}
|
||||
{{#if isV2}}
|
||||
<ToolbarLink
|
||||
@params={{array targetRoute model.id (query-params version=this.modelForData.version)}}
|
||||
|
|
|
@ -5,16 +5,12 @@
|
|||
as |D|
|
||||
>
|
||||
<D.trigger
|
||||
data-test-popup-menu-trigger="true"
|
||||
data-test-popup-menu-trigger="version"
|
||||
@class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@tagName="button"
|
||||
>
|
||||
{{#if useDefaultTrigger}}
|
||||
<Icon aria-label="More options" @glyph="more-horizontal" class="has-text-black auto-width" />
|
||||
{{else}}
|
||||
Version {{this.version.version}}
|
||||
Version {{@version.version}}
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
{{/if}}
|
||||
</D.trigger>
|
||||
<D.content @class="popup-menu-content ">
|
||||
<nav class="box menu">
|
||||
|
@ -22,66 +18,31 @@
|
|||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
{{#if this.version.destroyed}}
|
||||
<li class="action has-text-grey">
|
||||
<button type="button" class="link" disabled >
|
||||
The data for {{this.version.path}} version {{this.version.version}} has been destroyed.
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
{{#if isFetchingVersionCapabilities}}
|
||||
{{#each (reverse @model.versions) as |secretVersion|}}
|
||||
<li class="action">
|
||||
<button disabled type="button" class="link button is-loading is-transparent">
|
||||
loading
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="action">
|
||||
{{#if this.version.deleted}}
|
||||
{{#if canUndeleteVersion}}
|
||||
<button type="button" class="link" {{action (queue (action D.actions.close) (action "deleteVersion" "undelete"))}}>
|
||||
Undelete version
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="button" class="link" disabled >
|
||||
The data for {{this.version.path}} version {{this.version.version}} has been deleted. You do not have the permisssion to undelete it.
|
||||
</button>
|
||||
<LinkTo class="link" @params={{array (query-params version=secretVersion.version)}} @invokeAction={{action D.actions.close}} >
|
||||
Version {{secretVersion.version}}
|
||||
{{#if (and (eq secretVersion.version @model.currentVersion) (not secretVersion.destroyed) (not secretVersion.deleted))}}
|
||||
<Icon @glyph="check-circle-outline" class="has-text-success is-pulled-right" />
|
||||
{{else if secretVersion.destroyed}}
|
||||
<Icon @glyph="cancel-square-fill" class="has-text-danger is-pulled-right" />
|
||||
{{else if secretVersion.deleted}}
|
||||
<Icon @glyph="cancel-square-fill" class="has-text-grey is-pulled-right" />
|
||||
{{/if}}
|
||||
{{else if canDeleteVersion}}
|
||||
<ConfirmAction
|
||||
@buttonClasses="link is-destroy"
|
||||
@confirmTitle="Delete version?"
|
||||
@confirmMessage="This version will no longer be able to be read, but the underlying data will not be removed and can still be undeleted."
|
||||
@onConfirmAction={{action (queue (action D.actions.close) (action "deleteVersion" "delete"))}}
|
||||
data-test-secret-v2-delete="true"
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/each}}
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@data-test-version-history
|
||||
@mode="versions"
|
||||
@secret={{@model.id}}
|
||||
@class="has-text-black has-text-weight-semibold has-bottom-shadow"
|
||||
@onLinkClick={{action D.actions.close}}
|
||||
>
|
||||
Delete version
|
||||
</ConfirmAction>
|
||||
{{else}}
|
||||
<button type="button" class="link" disabled >
|
||||
You do not have the permissions to delete the data for this secret.
|
||||
</button>
|
||||
{{/if}}
|
||||
View version history
|
||||
</SecretLink>
|
||||
</li>
|
||||
{{#if canDestroyVersion}}
|
||||
<li class="action">
|
||||
<ConfirmAction
|
||||
@buttonClasses="link is-destroy"
|
||||
@confirmTitle="Permanently delete?"
|
||||
@confirmMessage="This version will no longer be able to be read, and cannot be undeleted."
|
||||
@onConfirmAction={{action (queue (action D.actions.close) (action "deleteVersion" "destroy"))}}
|
||||
data-test-secret-v2-destroy="true"
|
||||
>
|
||||
Permanently destroy version
|
||||
</ConfirmAction>
|
||||
</li>
|
||||
{{else}}
|
||||
<button type="button" class="link" disabled >
|
||||
You do not have the permissions to destroy the data for this secret.
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
</D.content>
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<KeyValueHeader @baseKey={{hash id=model.id}} @path="vault.cluster.secrets.backend.list" @mode="show" @root={{hash
|
||||
<KeyValueHeader
|
||||
@baseKey={{hash id=model.id}}
|
||||
@path="vault.cluster.secrets.backend.list"
|
||||
@mode="show"
|
||||
@root={{hash
|
||||
label=model.engineId
|
||||
text=model.engineId
|
||||
path="vault.cluster.secrets.backend.list-root"
|
||||
model=model.engineId
|
||||
}} @showCurrent={{true}} />
|
||||
model=model.engineId }}
|
||||
@showCurrent={{true}}
|
||||
/>
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
|
@ -17,30 +22,50 @@
|
|||
<ListItem @hasMenu={{false}} @linkParams={{array 'vault.cluster.secrets.backend.show' model.id (query-params version=list.item.version) }} as |Item|>
|
||||
<Item.content>
|
||||
<div class="columns is-flex-1">
|
||||
<div class="column is-one-third">
|
||||
<Icon @glyph="file-outline" class="has-text-grey" />Version {{list.item.version}}
|
||||
{{#if (eq list.item.version model.currentVersion)}}
|
||||
<span class="has-text-success is-size-9">
|
||||
<Icon @glyph="check-circle-outline" />Current
|
||||
</span>
|
||||
{{/if}}
|
||||
<div>
|
||||
<Icon @glyph="file-outline" class="has-text-grey" />
|
||||
Version {{list.item.version}}
|
||||
</div>
|
||||
<div class="column">
|
||||
{{#if list.item.deleted}}
|
||||
<span class="has-text-grey is-size-8">
|
||||
<Icon @glyph="cancel-square-outline" />Deleted
|
||||
{{#if (eq list.item.version model.currentVersion)}}
|
||||
<div class="column is-1">
|
||||
<span class="has-text-success is-size-9">
|
||||
<Icon @glyph="check-circle-fill" />Current
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if list.item.deleted}}
|
||||
<div class="column is-1">
|
||||
<span class="has-text-grey is-size-8">
|
||||
<Icon @glyph="cancel-square-fill" />Deleted
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if list.item.destroyed}}
|
||||
<div class="column is-1">
|
||||
<span class="has-text-danger is-size-8">
|
||||
<Icon @glyph="cancel-square-outline" />Destroyed
|
||||
<Icon @glyph="cancel-square-fill" />Destroyed
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</Item.content>
|
||||
<Item.menu>
|
||||
<SecretVersionMenu @version={{list.item}} @useDefaultTrigger={{true}}>
|
||||
<BasicDropdown
|
||||
@class="popup-menu"
|
||||
@horizontalPosition="auto-right"
|
||||
@verticalPosition="below"
|
||||
as |D|
|
||||
>
|
||||
<D.trigger
|
||||
data-test-popup-menu-trigger="true"
|
||||
@class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@tagName="button"
|
||||
>
|
||||
<Icon aria-label="More options" @glyph="more-horizontal" class="has-text-black auto-width" />
|
||||
</D.trigger>
|
||||
<D.content @class="popup-menu-content ">
|
||||
<nav class="box menu">
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@data-test-version
|
||||
|
@ -62,7 +87,10 @@
|
|||
Create new version from {{list.item.version}}
|
||||
</SecretLink>
|
||||
</li>
|
||||
</SecretVersionMenu>
|
||||
</ul>
|
||||
</nav>
|
||||
</D.content>
|
||||
</BasicDropdown>
|
||||
</Item.menu>
|
||||
</ListItem>
|
||||
</ListView>
|
||||
|
|
|
@ -144,7 +144,7 @@ module('Acceptance | secrets/secret/create', function(hooks) {
|
|||
.submit();
|
||||
await listPage.create();
|
||||
await editPage.createSecret(secretPath, 'foo', 'bar');
|
||||
await showPage.deleteSecret();
|
||||
await showPage.deleteSecretV1();
|
||||
assert.equal(
|
||||
currentRouteName(),
|
||||
'vault.cluster.secrets.backend.list-root',
|
||||
|
@ -251,6 +251,78 @@ module('Acceptance | secrets/secret/create', function(hooks) {
|
|||
assert.ok(showPage.editIsPresent, 'shows the edit button');
|
||||
});
|
||||
|
||||
test('version 2 with policy with destroy capabilities shows modal', async function(assert) {
|
||||
let backend = 'kv-v2';
|
||||
const V2_POLICY = `
|
||||
path "kv-v2/destroy/*" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
path "kv-v2/metadata/*" {
|
||||
capabilities = ["list", "update", "delete"]
|
||||
}
|
||||
path "kv-v2/data/secret" {
|
||||
capabilities = ["create", "read", "update"]
|
||||
}
|
||||
`;
|
||||
await consoleComponent.runCommands([
|
||||
`write sys/mounts/${backend} type=kv options=version=2`,
|
||||
`write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`,
|
||||
// delete any kv previously written here so that tests can be re-run
|
||||
'delete kv-v2/metadata/secret',
|
||||
'write -field=client_token auth/token/create policies=kv-v2-degrade',
|
||||
]);
|
||||
|
||||
let userToken = consoleComponent.lastLogOutput;
|
||||
await logout.visit();
|
||||
await authPage.login(userToken);
|
||||
|
||||
await writeSecret(backend, 'secret', 'foo', 'bar');
|
||||
await click('[data-test-delete-open-modal]');
|
||||
await settled();
|
||||
assert.dom('[data-test-delete-modal="destroy-version"]').exists('destroy this version option shows');
|
||||
assert.dom('[data-test-delete-modal="destroy-all-versions"]').exists('destroy all versions option shows');
|
||||
assert.dom('[data-test-delete-modal="delete-version"]').doesNotExist('delete version does not show');
|
||||
});
|
||||
|
||||
test('version 2 with policy with only delete option does not show modal and undelete is an option', async function(assert) {
|
||||
let backend = 'kv-v2';
|
||||
const V2_POLICY = `
|
||||
path "kv-v2/delete/*" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
path "kv-v2/undelete/*" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
path "kv-v2/metadata/*" {
|
||||
capabilities = ["list","read","create","update"]
|
||||
}
|
||||
path "kv-v2/data/secret" {
|
||||
capabilities = ["create", "read"]
|
||||
}
|
||||
`;
|
||||
await consoleComponent.runCommands([
|
||||
`write sys/mounts/${backend} type=kv options=version=2`,
|
||||
`write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`,
|
||||
// delete any kv previously written here so that tests can be re-run
|
||||
'delete kv-v2/metadata/secret',
|
||||
'write -field=client_token auth/token/create policies=kv-v2-degrade',
|
||||
]);
|
||||
|
||||
let userToken = consoleComponent.lastLogOutput;
|
||||
await logout.visit();
|
||||
await authPage.login(userToken);
|
||||
await writeSecret(backend, 'secret', 'foo', 'bar');
|
||||
assert.dom('[data-test-delete-open-modal]').doesNotExist('delete version does not show');
|
||||
assert.dom('[data-test-secret-v2-delete="true"]').exists('drop down delete shows');
|
||||
await showPage.deleteSecretV2();
|
||||
// unable to reload page in test scenario so going to list and back to secret to confirm deletion
|
||||
let url = `/vault/secrets/${backend}/list`;
|
||||
await visit(url);
|
||||
await click('[data-test-secret-link="secret"]');
|
||||
assert.dom('[data-test-component="empty-state"]').exists('secret has been deleted');
|
||||
assert.dom('[data-test-secret-undelete]').exists('undelete button shows');
|
||||
});
|
||||
|
||||
test('paths are properly encoded', async function(assert) {
|
||||
let backend = 'kv';
|
||||
let paths = [
|
||||
|
@ -305,7 +377,7 @@ module('Acceptance | secrets/secret/create', function(hooks) {
|
|||
await listPage.create();
|
||||
await editPage.createSecret(secretPath, 'foo', 'bar');
|
||||
await settled();
|
||||
await click('[data-test-popup-menu-trigger="history"]');
|
||||
await click('[data-test-popup-menu-trigger="version"]');
|
||||
await settled();
|
||||
await click('[data-test-version-history]');
|
||||
await settled();
|
||||
|
|
|
@ -8,6 +8,8 @@ export default create({
|
|||
text: text(),
|
||||
}),
|
||||
deleteBtn: clickable('[data-test-secret-delete] button'),
|
||||
deleteBtnV1: clickable('[data-test-secret-v1-delete="true"] button'),
|
||||
deleteBtnV2: clickable('[data-test-secret-v2-delete="true"] button'),
|
||||
confirmBtn: clickable('[data-test-confirm-button]'),
|
||||
rows: collection('data-test-row-label'),
|
||||
toggleJSON: clickable('[data-test-secret-json-toggle]'),
|
||||
|
@ -22,4 +24,10 @@ export default create({
|
|||
deleteSecret() {
|
||||
return this.deleteBtn().confirmBtn();
|
||||
},
|
||||
deleteSecretV1() {
|
||||
return this.deleteBtnV1().confirmBtn();
|
||||
},
|
||||
deleteSecretV2() {
|
||||
return this.deleteBtnV2().confirmBtn();
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue