mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 10:12:35 +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 {
|
export default class SyncDestinationModel extends Model {
|
||||||
@attr('string', { subText: 'Specifies the name for this destination.', editDisabled: true }) name;
|
@attr('string', { subText: 'Specifies the name for this destination.', editDisabled: true }) name;
|
||||||
@attr type;
|
@attr type;
|
||||||
|
// only present if delete action has been initiated
|
||||||
|
@attr('string') purgeInitiatedAt;
|
||||||
|
@attr('string') purgeError;
|
||||||
|
|
||||||
// findDestination returns static attributes for each destination type
|
// findDestination returns static attributes for each destination type
|
||||||
get icon() {
|
get icon() {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export default class SyncDestinationSerializer extends ApplicationSerializer {
|
|||||||
attrs = {
|
attrs = {
|
||||||
name: { serialize: false },
|
name: { serialize: false },
|
||||||
type: { serialize: false },
|
type: { serialize: false },
|
||||||
|
purgeInitiatedAt: { serialize: false },
|
||||||
|
purgeError: { serialize: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
serialize(snapshot) {
|
serialize(snapshot) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}}>
|
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}}>
|
||||||
<:syncDetails>
|
<:syncDetails>
|
||||||
{{#if this.syncStatus}}
|
{{#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>
|
<A.Title>
|
||||||
This secret has been synced from Vault to
|
This secret has been synced from Vault to
|
||||||
{{pluralize this.syncStatus.length "destination"}}. Updates to this secret will automatically sync to its
|
{{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">
|
<div class="tabs-container box is-bottomless is-marginless is-paddingless">
|
||||||
<nav class="tabs" aria-label="destination tabs">
|
<nav class="tabs" aria-label="destination tabs">
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -26,10 +26,14 @@ export default class DestinationsTabsToolbar extends Component<Args> {
|
|||||||
async deleteDestination() {
|
async deleteDestination() {
|
||||||
try {
|
try {
|
||||||
const { destination } = this.args;
|
const { destination } = this.args;
|
||||||
const message = `Successfully deleted destination ${destination.name}.`;
|
const message = `Destination ${destination.name} has been queued for deletion.`;
|
||||||
await destination.destroyRecord();
|
await destination.destroyRecord();
|
||||||
this.store.clearDataset('sync/destination');
|
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);
|
this.flashMessages.success(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.flashMessages.danger(`Error deleting destination \n ${errorMessage(error)}`);
|
this.flashMessages.danger(`Error deleting destination \n ${errorMessage(error)}`);
|
||||||
|
|||||||
@@ -93,11 +93,11 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
|
|||||||
@action
|
@action
|
||||||
async onDelete(destination: SyncDestinationModel) {
|
async onDelete(destination: SyncDestinationModel) {
|
||||||
try {
|
try {
|
||||||
const { name } = destination;
|
const { name, type } = destination;
|
||||||
const message = `Successfully deleted destination ${name}.`;
|
const message = `Destination ${name} has been queued for deletion.`;
|
||||||
await destination.destroyRecord();
|
await destination.destroyRecord();
|
||||||
this.store.clearDataset('sync/destination');
|
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);
|
this.flashMessages.success(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.flashMessages.danger(`Error deleting destination \n ${errorMessage(error)}`);
|
this.flashMessages.danger(`Error deleting destination \n ${errorMessage(error)}`);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default class DestinationsCreateForm extends Component<Args> {
|
|||||||
title: `Edit ${name}`,
|
title: `Edit ${name}`,
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{ label: 'Secrets Sync', route: 'secrets.overview' },
|
{ label: 'Secrets Sync', route: 'secrets.overview' },
|
||||||
|
{ label: 'Destinations', route: 'secrets.destinations' },
|
||||||
{
|
{
|
||||||
label: 'Destination',
|
label: 'Destination',
|
||||||
route: 'secrets.destinations.destination.secrets',
|
route: 'secrets.destinations.destination.secrets',
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export default Factory.extend({
|
|||||||
type: 'gcp-sm',
|
type: 'gcp-sm',
|
||||||
name: 'destination-gcp',
|
name: 'destination-gcp',
|
||||||
credentials: '*****',
|
credentials: '*****',
|
||||||
project_id: 'gcp-project-id', // TODO backend will add, doesn't exist yet
|
|
||||||
}),
|
}),
|
||||||
gh: trait({
|
gh: trait({
|
||||||
type: 'gh',
|
type: 'gh',
|
||||||
|
|||||||
@@ -133,8 +133,22 @@ export default function (server) {
|
|||||||
});
|
});
|
||||||
server.delete(uri, (schema, req) => {
|
server.delete(uri, (schema, req) => {
|
||||||
const { type, name } = req.params;
|
const { type, name } = req.params;
|
||||||
schema.db.syncDestinations.remove({ type, name });
|
schema.db.syncDestinations.update(
|
||||||
return new Response(204);
|
{ 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
|
// associations
|
||||||
server.get('/sys/sync/associations', (schema) => {
|
server.get('/sys/sync/associations', (schema) => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const PAGE = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
destinations: {
|
destinations: {
|
||||||
|
deleteBanner: '[data-test-delete-status-banner]',
|
||||||
sync: {
|
sync: {
|
||||||
mountSelect: '[data-test-sync-mount-select]',
|
mountSelect: '[data-test-sync-mount-select]',
|
||||||
mountInput: '[data-test-sync-mount-input]',
|
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 { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import { setupModels } from 'vault/tests/helpers/sync/setup-models';
|
import { setupModels } from 'vault/tests/helpers/sync/setup-models';
|
||||||
import hbs from 'htmlbars-inline-precompile';
|
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 { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||||
import { PAGE } from 'vault/tests/helpers/sync/sync-selectors';
|
import { PAGE } from 'vault/tests/helpers/sync/sync-selectors';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
@@ -60,7 +60,7 @@ module('Integration | Component | sync | Secrets::DestinationHeader', function (
|
|||||||
|
|
||||||
assert.propEqual(
|
assert.propEqual(
|
||||||
transitionStub.lastCall.args,
|
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'
|
'Transition is triggered on delete success'
|
||||||
);
|
);
|
||||||
assert.propEqual(
|
assert.propEqual(
|
||||||
@@ -69,4 +69,34 @@ module('Integration | Component | sync | Secrets::DestinationHeader', function (
|
|||||||
'Store dataset is cleared on delete success'
|
'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(
|
assert.propEqual(
|
||||||
this.transitionStub.lastCall.args,
|
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'
|
'Transition is triggered on delete success'
|
||||||
);
|
);
|
||||||
assert.propEqual(
|
assert.propEqual(
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE
|
|||||||
this.model = this.store.peekRecord(`sync/destinations/${type}`, id);
|
this.model = this.store.peekRecord(`sync/destinations/${type}`, id);
|
||||||
|
|
||||||
await this.renderFormComponent();
|
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('h2').hasText('Credentials', 'renders credentials section on edit');
|
||||||
assert
|
assert
|
||||||
.dom('p.hds-foreground-faint')
|
.dom('p.hds-foreground-faint')
|
||||||
|
|||||||
Reference in New Issue
Block a user