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:
claire bontempo
2024-01-10 21:07:10 -07:00
committed by GitHub
parent aed43fe80e
commit 2cabfe0143
13 changed files with 96 additions and 13 deletions

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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>

View File

@@ -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)}`);

View File

@@ -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)}`);

View File

@@ -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',

View File

@@ -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',

View File

@@ -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) => {

View File

@@ -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]',

View File

@@ -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');
});
});

View File

@@ -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(

View File

@@ -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')