Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync UI: Add granularity to sync destinations #25500

Merged
merged 11 commits into from
Feb 20, 2024
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">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used Hds here because InfoTooltip styling was too low and didn't have the patience to CSS

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2024-02-16 at 8 17 23 PM

<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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced destinations popup menu so the views were consistent

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}}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hasMenu is false so that we can use the Hds::Dropdown istead of PopupMenu

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