mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
Secrets Sync UI: Add purge delete progress and error banner to destination header (#24761)
* add deletion in progress banner * update kv details banner to inline alert * add logic for purge error * add params to mirage * comment in purge_initiated_at for mirage * update flash message for deleting * add test for banner * transition to destination associations after delete * redirect to details after delete instead of list * remove attrs from serializer * update mirage handler to mock purge_initiated_at
This commit is contained in:
@@ -20,6 +20,9 @@ const validations = {
|
||||
export default class SyncDestinationModel extends Model {
|
||||
@attr('string', { subText: 'Specifies the name for this destination.', editDisabled: true }) name;
|
||||
@attr type;
|
||||
// only present if delete action has been initiated
|
||||
@attr('string') purgeInitiatedAt;
|
||||
@attr('string') purgeError;
|
||||
|
||||
// findDestination returns static attributes for each destination type
|
||||
get icon() {
|
||||
|
||||
@@ -9,6 +9,8 @@ export default class SyncDestinationSerializer extends ApplicationSerializer {
|
||||
attrs = {
|
||||
name: { serialize: false },
|
||||
type: { serialize: false },
|
||||
purgeInitiatedAt: { serialize: false },
|
||||
purgeError: { serialize: false },
|
||||
};
|
||||
|
||||
serialize(snapshot) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}}>
|
||||
<:syncDetails>
|
||||
{{#if this.syncStatus}}
|
||||
<Hds::Alert data-test-sync-alert @type="page" @color="neutral" as |A|>
|
||||
<Hds::Alert data-test-sync-alert @type="inline" class="has-top-margin-s has-bottom-margin-m" @color="neutral" as |A|>
|
||||
<A.Title>
|
||||
This secret has been synced from Vault to
|
||||
{{pluralize this.syncStatus.length "destination"}}. Updates to this secret will automatically sync to its
|
||||
|
||||
@@ -13,6 +13,35 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
{{#if @destination.purgeInitiatedAt}}
|
||||
<Hds::Alert
|
||||
data-test-delete-status-banner
|
||||
@type="inline"
|
||||
class="has-bottom-margin-m"
|
||||
@color={{if @destination.purgeError "critical" "neutral"}}
|
||||
@icon={{unless @destination.purgeError "loading-static"}}
|
||||
as |A|
|
||||
>
|
||||
{{#if @destination.purgeError}}
|
||||
<A.Title>Deletion failed</A.Title>
|
||||
<A.Description>
|
||||
There was a problem with the delete purge initiated at
|
||||
{{date-format @destination.purgeInitiatedAt "MMM dd, yyyy 'at' hh:mm:ss aaa"}}.
|
||||
</A.Description>
|
||||
<A.Description>
|
||||
{{@destination.purgeError}}
|
||||
</A.Description>
|
||||
{{else}}
|
||||
<A.Title>Deletion in progress</A.Title>
|
||||
<A.Description>
|
||||
Purge initiated on
|
||||
{{date-format @destination.purgeInitiatedAt "MMM dd, yyyy 'at' hh:mm:ss aaa"}}. This process may take some time
|
||||
depending on how many secrets must be un-synced from this destination.
|
||||
</A.Description>
|
||||
{{/if}}
|
||||
</Hds::Alert>
|
||||
{{/if}}
|
||||
|
||||
<div class="tabs-container box is-bottomless is-marginless is-paddingless">
|
||||
<nav class="tabs" aria-label="destination tabs">
|
||||
<ul>
|
||||
|
||||
@@ -26,10 +26,14 @@ export default class DestinationsTabsToolbar extends Component<Args> {
|
||||
async deleteDestination() {
|
||||
try {
|
||||
const { destination } = this.args;
|
||||
const message = `Successfully deleted destination ${destination.name}.`;
|
||||
const message = `Destination ${destination.name} has been queued for deletion.`;
|
||||
await destination.destroyRecord();
|
||||
this.store.clearDataset('sync/destination');
|
||||
this.router.transitionTo('vault.cluster.sync.secrets.destinations');
|
||||
this.router.transitionTo(
|
||||
'vault.cluster.sync.secrets.destinations.destination.secrets',
|
||||
destination.type,
|
||||
destination.name
|
||||
);
|
||||
this.flashMessages.success(message);
|
||||
} catch (error) {
|
||||
this.flashMessages.danger(`Error deleting destination \n ${errorMessage(error)}`);
|
||||
|
||||
@@ -93,11 +93,11 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
|
||||
@action
|
||||
async onDelete(destination: SyncDestinationModel) {
|
||||
try {
|
||||
const { name } = destination;
|
||||
const message = `Successfully deleted destination ${name}.`;
|
||||
const { name, type } = destination;
|
||||
const message = `Destination ${name} has been queued for deletion.`;
|
||||
await destination.destroyRecord();
|
||||
this.store.clearDataset('sync/destination');
|
||||
this.router.transitionTo('vault.cluster.sync.secrets.destinations');
|
||||
this.router.transitionTo('vault.cluster.sync.secrets.destinations.destination.secrets', type, name);
|
||||
this.flashMessages.success(message);
|
||||
} catch (error) {
|
||||
this.flashMessages.danger(`Error deleting destination \n ${errorMessage(error)}`);
|
||||
|
||||
@@ -45,6 +45,7 @@ export default class DestinationsCreateForm extends Component<Args> {
|
||||
title: `Edit ${name}`,
|
||||
breadcrumbs: [
|
||||
{ label: 'Secrets Sync', route: 'secrets.overview' },
|
||||
{ label: 'Destinations', route: 'secrets.destinations' },
|
||||
{
|
||||
label: 'Destination',
|
||||
route: 'secrets.destinations.destination.secrets',
|
||||
|
||||
@@ -27,7 +27,6 @@ export default Factory.extend({
|
||||
type: 'gcp-sm',
|
||||
name: 'destination-gcp',
|
||||
credentials: '*****',
|
||||
project_id: 'gcp-project-id', // TODO backend will add, doesn't exist yet
|
||||
}),
|
||||
gh: trait({
|
||||
type: 'gh',
|
||||
|
||||
@@ -133,8 +133,22 @@ export default function (server) {
|
||||
});
|
||||
server.delete(uri, (schema, req) => {
|
||||
const { type, name } = req.params;
|
||||
schema.db.syncDestinations.remove({ type, name });
|
||||
return new Response(204);
|
||||
schema.db.syncDestinations.update(
|
||||
{ type, name },
|
||||
// these parameters are added after a purge delete is initiated
|
||||
// if only `purge_initiated_at` exists the delete progress banner renders
|
||||
// if `purge_error` also has a value then delete failed banner renders
|
||||
{
|
||||
purge_initiated_at: '2024-01-09T16:54:28.463879-07:00',
|
||||
// WIP (backend hasn't added yet) update when we have a realistic error message)
|
||||
// purge_error: '1 error occurred: association could for some confusing reason not be un-synced!',
|
||||
}
|
||||
);
|
||||
const record = schema.db.syncDestinations.findBy({ type, name });
|
||||
return destinationResponse(record);
|
||||
// return the following instead to test immediate deletion
|
||||
// schema.db.syncDestinations.remove({ type, name });
|
||||
// return new Response(204);
|
||||
});
|
||||
// associations
|
||||
server.get('/sys/sync/associations', (schema) => {
|
||||
|
||||
@@ -26,6 +26,7 @@ export const PAGE = {
|
||||
},
|
||||
},
|
||||
destinations: {
|
||||
deleteBanner: '[data-test-delete-status-banner]',
|
||||
sync: {
|
||||
mountSelect: '[data-test-sync-mount-select]',
|
||||
mountInput: '[data-test-sync-mount-input]',
|
||||
|
||||
@@ -9,7 +9,7 @@ import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { setupModels } from 'vault/tests/helpers/sync/setup-models';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { click, fillIn, render } from '@ember/test-helpers';
|
||||
import { click, fillIn, render, settled } from '@ember/test-helpers';
|
||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import { PAGE } from 'vault/tests/helpers/sync/sync-selectors';
|
||||
import sinon from 'sinon';
|
||||
@@ -60,7 +60,7 @@ module('Integration | Component | sync | Secrets::DestinationHeader', function (
|
||||
|
||||
assert.propEqual(
|
||||
transitionStub.lastCall.args,
|
||||
['vault.cluster.sync.secrets.destinations'],
|
||||
['vault.cluster.sync.secrets.destinations.destination.secrets', 'aws-sm', 'us-west-1'],
|
||||
'Transition is triggered on delete success'
|
||||
);
|
||||
assert.propEqual(
|
||||
@@ -69,4 +69,34 @@ module('Integration | Component | sync | Secrets::DestinationHeader', function (
|
||||
'Store dataset is cleared on delete success'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should render delete progress banner', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.destination.set('purgeInitiatedAt', '2024-01-09T16:54:28.463879');
|
||||
await settled();
|
||||
assert
|
||||
.dom(PAGE.destinations.deleteBanner)
|
||||
.hasText(
|
||||
'Deletion in progress Purge initiated on Jan 09, 2024 at 04:54:28 pm. This process may take some time depending on how many secrets must be un-synced from this destination.'
|
||||
);
|
||||
assert
|
||||
.dom(`${PAGE.destinations.deleteBanner} ${PAGE.icon('loading-static')}`)
|
||||
.exists('banner renders loading icon');
|
||||
});
|
||||
|
||||
test('it should render delete error banner', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.destination.set('purgeInitiatedAt', '2024-01-09T16:54:28.463879');
|
||||
this.destination.set('purgeError', 'oh no! a problem occurred!');
|
||||
await settled();
|
||||
assert
|
||||
.dom(PAGE.destinations.deleteBanner)
|
||||
.hasText(
|
||||
'Deletion failed There was a problem with the delete purge initiated at Jan 09, 2024 at 04:54:28 pm. oh no! a problem occurred!',
|
||||
'banner renders error message'
|
||||
);
|
||||
assert
|
||||
.dom(`${PAGE.destinations.deleteBanner} ${PAGE.icon('alert-diamond')}`)
|
||||
.exists('banner renders critical icon');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -154,7 +154,7 @@ module('Integration | Component | sync | Page::Destinations', function (hooks) {
|
||||
|
||||
assert.propEqual(
|
||||
this.transitionStub.lastCall.args,
|
||||
['vault.cluster.sync.secrets.destinations'],
|
||||
['vault.cluster.sync.secrets.destinations.destination.secrets', 'aws-sm', 'destination-aws'],
|
||||
'Transition is triggered on delete success'
|
||||
);
|
||||
assert.propEqual(
|
||||
|
||||
@@ -58,7 +58,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE
|
||||
this.model = this.store.peekRecord(`sync/destinations/${type}`, id);
|
||||
|
||||
await this.renderFormComponent();
|
||||
assert.dom(PAGE.breadcrumbs).hasText('Secrets Sync Destination Edit Destination');
|
||||
assert.dom(PAGE.breadcrumbs).hasText('Secrets Sync Destinations Destination Edit Destination');
|
||||
assert.dom('h2').hasText('Credentials', 'renders credentials section on edit');
|
||||
assert
|
||||
.dom('p.hds-foreground-faint')
|
||||
|
||||
Reference in New Issue
Block a user