Skip to content

Commit

Permalink
Sync UI: Add granularity to sync destinations (#25500)
Browse files Browse the repository at this point in the history
* add granularity form field to sync destinations

* update mirage, shim in subkey response

* fix comment

* add granular updates to list view

* update mirage;

* update test

* comment for updating test

* use hds::dropdown in destinations for consistency

* move banner to popup menu

* add changelog

* remove spans from test
  • Loading branch information
hellobontempo authored Feb 20, 2024
1 parent 3132592 commit dd62f9f
Show file tree
Hide file tree
Showing 20 changed files with 249 additions and 94 deletions.
3 changes: 3 additions & 0 deletions changelog/25500.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: add granularity param to sync destinations
```
1 change: 1 addition & 0 deletions ui/app/models/sync/association.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default class SyncAssociationModel extends Model {
// destination related properties that are not serialized to payload
@attr destinationName;
@attr destinationType;
@attr subKey; // this property is added if a destination has 'secret-key' granularity

@lazyCapabilities(
apiPath`sys/sync/destinations/${'destinationType'}/${'destinationName'}/associations/set`,
Expand Down
20 changes: 20 additions & 0 deletions ui/app/models/sync/destination.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,26 @@ export default class SyncDestinationModel extends Model {
'Go-template string that indicates how to format the secret name at the destination. The default template varies by destination type but is generally in the form of "vault-<accessor_id>-<secret_path>" e.g. "vault-kv-1234-my-secret-1".',
})
secretNameTemplate;
@attr('string', {
editType: 'radio',
label: 'Secret sync granularity',
possibleValues: [
{
label: 'Secret path',
subText: 'Sync entire secret contents as a single entry at the destination.',
value: 'secret-path',
},
{
label: 'Secret key',
subText: 'Sync each key-value pair of secret data as a distinct entry at the destination.',
helpText:
'Only top-level keys will be synced and any nested or complex values will be encoded as a JSON string.',
value: 'secret-key',
},
],
defaultValue: 'secret-path',
})
granularity;

// only present if delete action has been initiated
@attr('string') purgeInitiatedAt;
Expand Down
5 changes: 4 additions & 1 deletion ui/app/models/sync/destinations/aws-sm.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import { attr } from '@ember-data/model';
import { withFormFields } from 'vault/decorators/model-form-fields';

const displayFields = [
// connection details
'name',
'region',
'accessKeyId',
'secretAccessKey',
// sync config options
'granularity',
'secretNameTemplate',
'customTags',
];
const formFieldGroups = [
{ default: ['name', 'region', 'secretNameTemplate', 'customTags'] },
{ default: ['name', 'region', 'granularity', 'secretNameTemplate', 'customTags'] },
{ Credentials: ['accessKeyId', 'secretAccessKey'] },
];
@withFormFields(displayFields, formFieldGroups)
Expand Down
16 changes: 15 additions & 1 deletion ui/app/models/sync/destinations/azure-kv.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,31 @@ import { attr } from '@ember-data/model';
import { withFormFields } from 'vault/decorators/model-form-fields';

const displayFields = [
// connection details
'name',
'keyVaultUri',
'tenantId',
'cloud',
'clientId',
'clientSecret',
// vault sync config options
'granularity',
'secretNameTemplate',
'customTags',
];
const formFieldGroups = [
{ default: ['name', 'keyVaultUri', 'tenantId', 'cloud', 'clientId', 'secretNameTemplate', 'customTags'] },
{
default: [
'name',
'keyVaultUri',
'tenantId',
'cloud',
'clientId',
'granularity',
'secretNameTemplate',
'customTags',
],
},
{ Credentials: ['clientSecret'] },
];
@withFormFields(displayFields, formFieldGroups)
Expand Down
12 changes: 10 additions & 2 deletions ui/app/models/sync/destinations/gcp-sm.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@ import SyncDestinationModel from '../destination';
import { attr } from '@ember-data/model';
import { withFormFields } from 'vault/decorators/model-form-fields';

const displayFields = ['name', 'credentials', 'secretNameTemplate', 'customTags'];
const displayFields = [
// connection details
'name',
'credentials',
// vault sync config options
'granularity',
'secretNameTemplate',
'customTags',
];
const formFieldGroups = [
{ default: ['name', 'secretNameTemplate', 'customTags'] },
{ default: ['name', 'granularity', 'secretNameTemplate', 'customTags'] },
{ Credentials: ['credentials'] },
];
@withFormFields(displayFields, formFieldGroups)
Expand Down
14 changes: 12 additions & 2 deletions ui/app/models/sync/destinations/gh.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@
import SyncDestinationModel from '../destination';
import { attr } from '@ember-data/model';
import { withFormFields } from 'vault/decorators/model-form-fields';
const displayFields = ['name', 'repositoryOwner', 'repositoryName', 'accessToken', 'secretNameTemplate'];

const displayFields = [
// connection details
'name',
'repositoryOwner',
'repositoryName',
'accessToken',
// vault sync config options
'granularity',
'secretNameTemplate',
];
const formFieldGroups = [
{ default: ['name', 'repositoryOwner', 'repositoryName', 'secretNameTemplate'] },
{ default: ['name', 'repositoryOwner', 'repositoryName', 'granularity', 'secretNameTemplate'] },
{ Credentials: ['accessToken'] },
];

Expand Down
5 changes: 4 additions & 1 deletion ui/app/models/sync/destinations/vercel-project.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ const validations = {
};

const displayFields = [
// connection details
'name',
'accessToken',
'projectId',
'teamId',
'deploymentEnvironments',
// vault sync config options
'granularity',
'secretNameTemplate',
];
const formFieldGroups = [
{ default: ['name', 'projectId', 'teamId', 'deploymentEnvironments', 'secretNameTemplate'] },
{ default: ['name', 'projectId', 'teamId', 'deploymentEnvironments', 'granularity', 'secretNameTemplate'] },
{ Credentials: ['accessToken'] },
];
@withModelValidations(validations)
Expand Down
1 change: 1 addition & 0 deletions ui/app/serializers/sync/association.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default class SyncAssociationSerializer extends ApplicationSerializer {
destinationType: { serialize: false },
syncStatus: { serialize: false },
updatedAt: { serialize: false },
subKey: { serialize: false },
};

extractLazyPaginatedData(payload) {
Expand Down
7 changes: 6 additions & 1 deletion ui/lib/core/addon/components/form-field.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@
class="has-left-margin-xs has-text-black is-size-7"
data-test-radio-label={{or val.label val.value val}}
>
<span>{{or val.label val.value val}}</span>
{{or val.label val.value val}}
{{#if val.helpText}}
<Hds::TooltipButton @text={{val.helpText}} aria-label="More information">
<FlightIcon @name="info" />
</Hds::TooltipButton>
{{/if}}
{{#if this.hasRadioSubText}}
<p class="has-left-margin-xs has-text-grey is-size-8" data-test-radio-subText={{val.subText}}>
{{val.subText}}
Expand Down
73 changes: 41 additions & 32 deletions ui/lib/sync/addon/components/secrets/page/destinations.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -67,44 +67,44 @@
</code>
</Item.content>

<Item.menu>
{{#if destination.destinationPath.isLoading}}
<li class="action">
<LoadingDropdownOption />
</li>
{{else}}
<li>
<LinkTo
class="has-text-black has-text-weight-semibold"
<Item.menu @hasMenu={{false}}>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Destinations popup menu"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if destination.destinationPath.isLoading}}
<dd.Generic class="has-text-center">
<LoadingDropdownOption />
</dd.Generic>
{{else}}
<dd.Interactive
@text="Details"
data-test-details
@route="secrets.destinations.destination.details"
@models={{array destination.type destination.name}}
@disabled={{not destination.canRead}}
>
Details
</LinkTo>
</li>
<li>
<LinkTo
class="has-text-black has-text-weight-semibold"
data-test-edit
@route="secrets.destinations.destination.edit"
@models={{array destination.type destination.name}}
@disabled={{not destination.canEdit}}
>
Edit
</LinkTo>
</li>
{{#if destination.canDelete}}
<ConfirmAction
data-test-delete
@isInDropdown={{true}}
@buttonText="Delete"
@confirmMessage="The destination will be permanently deleted and all the secrets will be unsynced. This cannot be undone."
@onConfirmAction={{fn this.onDelete destination}}
/>
{{#if destination.canEdit}}
<dd.Interactive
@text="Edit"
data-test-edit
@route="secrets.destinations.destination.edit"
@models={{array destination.type destination.name}}
/>
{{/if}}
{{#if destination.canDelete}}
<dd.Interactive
data-test-delete
@text="Delete"
@color="critical"
{{on "click" (fn (mut this.destinationToDelete) destination)}}
/>
{{/if}}
{{/if}}
{{/if}}
</Hds::Dropdown>
</Item.menu>
</ListItem>
{{/each}}
Expand All @@ -120,4 +120,13 @@
</div>
{{else}}
<EmptyState @title={{this.noResultsMessage}} />
{{/if}}

{{#if this.destinationToDelete}}
<ConfirmModal
@color="critical"
@confirmMessage="The destination will be permanently deleted and all the secrets will be unsynced. This cannot be undone."
@onClose={{fn (mut this.destinationToDelete) null}}
@onConfirm={{fn this.onDelete this.destinationToDelete}}
/>
{{/if}}
4 changes: 4 additions & 0 deletions ui/lib/sync/addon/components/secrets/page/destinations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { getOwner } from '@ember/application';
import errorMessage from 'vault/utils/error-message';
import { findDestination, syncDestinations } from 'core/helpers/sync-destinations';
Expand All @@ -30,6 +31,7 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
@service declare readonly store: StoreService;
@service declare readonly flashMessages: FlashMessageService;

@tracked destinationToDelete = null;
// for some reason there isn't a full page refresh happening when transitioning on filter change
// when the transition happens it causes the FilterInput component to lose focus since it can only focus on didInsert
// to work around this, verify that a transition from this route was completed and then focus the input
Expand Down Expand Up @@ -101,6 +103,8 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
this.flashMessages.success(message);
} catch (error) {
this.flashMessages.danger(`Error deleting destination \n ${errorMessage(error)}`);
} finally {
this.destinationToDelete = null;
}
}
}
Loading

0 comments on commit dd62f9f

Please sign in to comment.