mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 01:32:33 +00:00
Sync UI: Add granularity to sync destinations (#25500)
* 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
This commit is contained in:
3
changelog/25500.txt
Normal file
3
changelog/25500.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
ui: add granularity param to sync destinations
|
||||
```
|
||||
@@ -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`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'] },
|
||||
];
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -12,6 +12,7 @@ export default class SyncAssociationSerializer extends ApplicationSerializer {
|
||||
destinationType: { serialize: false },
|
||||
syncStatus: { serialize: false },
|
||||
updatedAt: { serialize: false },
|
||||
subKey: { serialize: false },
|
||||
};
|
||||
|
||||
extractLazyPaginatedData(payload) {
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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';
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,18 @@
|
||||
<ListItem as |Item|>
|
||||
<Item.content>
|
||||
<div>
|
||||
<Hds::Badge @text="{{association.mount}}/" />
|
||||
<Hds::TooltipButton @text="KV v2 engine mount path">
|
||||
<Hds::Badge @text="{{association.mount}}/" />
|
||||
</Hds::TooltipButton>
|
||||
<LinkToExternal
|
||||
data-test-association-name={{index}}
|
||||
class="has-text-black has-text-weight-semibold"
|
||||
@route="kvSecretDetails"
|
||||
@models={{array association.mount association.secretName}}
|
||||
>
|
||||
{{association.secretName}}
|
||||
</LinkToExternal>
|
||||
>{{association.secretName}}</LinkToExternal>
|
||||
{{#if association.subKey}}
|
||||
<Hds::Badge @text="secret key: {{association.subKey}}/" />
|
||||
{{/if}}
|
||||
<div>
|
||||
<SyncStatusBadge @status={{association.syncStatus}} data-test-association-status={{index}} />
|
||||
<code class="has-text-grey is-size-8" data-test-association-updated={{index}}>
|
||||
@@ -30,42 +33,49 @@
|
||||
</div>
|
||||
</Item.content>
|
||||
|
||||
<Item.menu>
|
||||
{{#if (or association.setAssociationPath.isLoading association.removeAssociationPath.isLoading)}}
|
||||
<li class="action">
|
||||
<LoadingDropdownOption />
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="action">
|
||||
<Hds::Button
|
||||
@text="Sync now"
|
||||
class="link is-flex-start"
|
||||
@isFullWidth={{true}}
|
||||
disabled={{not association.canSync}}
|
||||
data-test-association-action="sync"
|
||||
{{on "click" (fn this.update association "set")}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<LinkToExternal
|
||||
class="has-text-black has-text-weight-semibold"
|
||||
<Item.menu @hasMenu={{false}}>
|
||||
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" @width="210px" as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="Synced secret popup menu"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
{{#if (or association.setAssociationPath.isLoading association.removeAssociationPath.isLoading)}}
|
||||
<dd.Generic class="has-text-center">
|
||||
<LoadingDropdownOption />
|
||||
</dd.Generic>
|
||||
{{else}}
|
||||
{{#if (eq @destination.granularity "secret-key")}}
|
||||
<dd.Description
|
||||
@text='Sync or unsync actions will apply to the secret "{{association.secretName}}" and not this individual key.'
|
||||
/>
|
||||
<dd.Separator />
|
||||
{{/if}}
|
||||
{{#if association.canSync}}
|
||||
<dd.Interactive
|
||||
@text="Sync now"
|
||||
data-test-association-action="sync"
|
||||
{{on "click" (fn this.update association "set")}}
|
||||
/>
|
||||
{{/if}}
|
||||
<dd.Interactive
|
||||
@text="View secret"
|
||||
data-test-association-action="view"
|
||||
@route="kvSecretDetails"
|
||||
@isRouteExternal={{true}}
|
||||
@models={{array association.mount association.secretName}}
|
||||
>
|
||||
View secret
|
||||
</LinkToExternal>
|
||||
</li>
|
||||
{{#if association.canUnsync}}
|
||||
<ConfirmAction
|
||||
data-test-association-action="unsync"
|
||||
@isInDropdown={{true}}
|
||||
@buttonText="Unsync"
|
||||
@confirmMessage="This secret will be unsynced from this destination."
|
||||
@onConfirmAction={{fn this.update association "remove"}}
|
||||
/>
|
||||
{{#if association.canUnsync}}
|
||||
<dd.Interactive
|
||||
data-test-association-action="unsync"
|
||||
@color="critical"
|
||||
@text="Unsync"
|
||||
{{on "click" (fn (mut this.secretToUnsync) association)}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
</Item.menu>
|
||||
</ListItem>
|
||||
{{/each}}
|
||||
@@ -92,4 +102,13 @@
|
||||
@route="secrets.destinations.destination.sync"
|
||||
/>
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.secretToUnsync}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage='The secret "{{this.secretToUnsync.secretName}}" will be unsynced from this destination.'
|
||||
@onClose={{fn (mut this.secretToUnsync) null}}
|
||||
@onConfirm={{fn this.update this.secretToUnsync "remove"}}
|
||||
/>
|
||||
{{/if}}
|
||||
@@ -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';
|
||||
|
||||
@@ -26,6 +27,8 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked secretToUnsync = null;
|
||||
|
||||
get mountPoint(): string {
|
||||
const owner = getOwner(this) as EngineOwner;
|
||||
return owner.mountPoint;
|
||||
@@ -55,6 +58,7 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
|
||||
} catch (error) {
|
||||
this.flashMessages.danger(`Sync operation error: \n ${errorMessage(error)}`);
|
||||
} finally {
|
||||
this.secretToUnsync = null;
|
||||
this.refreshRoute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
Select a KV engine mount and path to sync a secret to the
|
||||
{{@destination.typeDisplayName}}
|
||||
destination. Selecting a previously synced secret will re-sync that secret.
|
||||
{{#if (eq @destination.granularity "secret-key")}}
|
||||
This destination is configured to sync with
|
||||
<strong>secret key</strong>
|
||||
granularity. Each key-value pair of the selected secret will sync as a distinct entry at the destination.
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
<div class="has-top-margin-l">
|
||||
|
||||
@@ -14,6 +14,7 @@ export default Factory.extend({
|
||||
secret_access_key: '*****',
|
||||
region: 'us-west-1',
|
||||
// options
|
||||
granularity: 'secret-path', // default option (same for all destinations) so edit test can update to 'secret-key'
|
||||
secret_name_template: 'vault-{{ .MountAccessor | replace "_" "-" }}-{{ .SecretPath }}',
|
||||
custom_tags: { foo: 'bar' },
|
||||
}),
|
||||
@@ -28,6 +29,7 @@ export default Factory.extend({
|
||||
client_secret: '*****',
|
||||
cloud: 'Azure Public Cloud',
|
||||
// options
|
||||
granularity: 'secret-path',
|
||||
secret_name_template: 'vault-{{ .MountAccessor | replace "_" "-" }}-{{ .SecretPath }}',
|
||||
custom_tags: { foo: 'bar' },
|
||||
}),
|
||||
@@ -37,6 +39,7 @@ export default Factory.extend({
|
||||
// connection_details
|
||||
credentials: '*****',
|
||||
// options
|
||||
granularity: 'secret-path',
|
||||
secret_name_template: 'vault-{{ .MountAccessor | replace "_" "-" }}-{{ .SecretPath }}',
|
||||
custom_tags: { foo: 'bar' },
|
||||
}),
|
||||
@@ -48,6 +51,7 @@ export default Factory.extend({
|
||||
repository_owner: 'my-organization-or-username',
|
||||
repository_name: 'my-repository',
|
||||
// options
|
||||
granularity: 'secret-path',
|
||||
secret_name_template: 'vault-{{ .MountAccessor | replace "_" "-" }}-{{ .SecretPath }}',
|
||||
}),
|
||||
['vercel-project']: trait({
|
||||
|
||||
@@ -10,19 +10,48 @@ import clientsHandler from './clients';
|
||||
|
||||
export const associationsResponse = (schema, req) => {
|
||||
const { type, name } = req.params;
|
||||
const [destination] = schema.db.syncDestinations.where({ type, name });
|
||||
const records = schema.db.syncAssociations.where({ type, name });
|
||||
const associations = records.length
|
||||
? records.reduce((associations, association) => {
|
||||
const key = `${association.mount}/${association.secret_name}`;
|
||||
delete association.type;
|
||||
delete association.name;
|
||||
associations[key] = association;
|
||||
return associations;
|
||||
}, {})
|
||||
: {};
|
||||
|
||||
// if a destination has granularity: 'secret-key' keys of the secret
|
||||
// are added to the association response but they are not individual associations
|
||||
// the secret itself is still a single association
|
||||
const subKeys = {
|
||||
'my-kv/my-granular-secret/foo': {
|
||||
mount: 'my-kv',
|
||||
secret_name: 'my-granular-secret',
|
||||
sync_status: 'SYNCED',
|
||||
updated_at: '2023-09-20T10:51:53.961861096-04:00',
|
||||
sub_key: 'foo',
|
||||
},
|
||||
'my-kv/my-granular-secret/bar': {
|
||||
mount: 'my-kv',
|
||||
secret_name: 'my-granular-secret',
|
||||
sync_status: 'SYNCED',
|
||||
updated_at: '2023-09-20T10:51:53.961861096-04:00',
|
||||
sub_key: 'bar',
|
||||
},
|
||||
'my-kv/my-granular-secret/baz': {
|
||||
mount: 'my-kv',
|
||||
secret_name: 'my-granular-secret',
|
||||
sync_status: 'SYNCED',
|
||||
updated_at: '2023-09-20T10:51:53.961861096-04:00',
|
||||
sub_key: 'baz',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
data: {
|
||||
associated_secrets: records.length
|
||||
? records.reduce((associations, association) => {
|
||||
const key = `${association.mount}/${association.secret_name}`;
|
||||
delete association.type;
|
||||
delete association.name;
|
||||
associations[key] = association;
|
||||
return associations;
|
||||
}, {})
|
||||
: {},
|
||||
associated_secrets: destination.granularity === 'secret-path' ? associations : subKeys,
|
||||
store_name: name,
|
||||
store_type: type,
|
||||
},
|
||||
|
||||
@@ -76,6 +76,8 @@ export const PAGE = {
|
||||
fillInByAttr: async (attr, value) => {
|
||||
// for handling more complex form input elements by attr name
|
||||
switch (attr) {
|
||||
case 'granularity':
|
||||
return await click(`[data-test-radio="secret-key"]`);
|
||||
case 'credentials':
|
||||
await click('[data-test-text-toggle]');
|
||||
return fillIn('[data-test-text-file-textarea]', value);
|
||||
|
||||
@@ -188,9 +188,9 @@ module('Integration | Component | form field', function (hooks) {
|
||||
assert.ok(component.hasRadio, 'renders radio buttons');
|
||||
const selectedValue = 'SHA256';
|
||||
await component.selectRadioInput(selectedValue);
|
||||
assert.dom('[data-test-radio-label="Label 1"] span').hasText('Label 1');
|
||||
assert.dom('[data-test-radio-label="Label 2"] span').hasText('Label 2');
|
||||
assert.dom('[data-test-radio-label="SHA256"] span').hasText('SHA256');
|
||||
assert.dom('[data-test-radio-label="Label 1"]').hasTextContaining('Label 1');
|
||||
assert.dom('[data-test-radio-label="Label 2"]').hasTextContaining('Label 2');
|
||||
assert.dom('[data-test-radio-label="SHA256"]').hasTextContaining('SHA256');
|
||||
assert.dom('[data-test-radio-subText="Some subtext 1"]').hasText('Some subtext 1');
|
||||
assert.dom('[data-test-radio-subText="Some subtext 2"]').hasText('Some subtext 2');
|
||||
assert.dom('[data-test-radio-subText="Some subtext 3"]').hasText('Some subtext 3');
|
||||
|
||||
@@ -266,19 +266,30 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE
|
||||
}
|
||||
|
||||
// EDIT FORM ASSERTIONS FOR EACH DESTINATION TYPE
|
||||
// * test updates: if editable, add param here
|
||||
// if it is not a string type, add case to EXPECTED_VALUE and update
|
||||
// fillInByAttr() (in sync-selectors) to interact with the form
|
||||
const EDITABLE_FIELDS = {
|
||||
'aws-sm': ['accessKeyId', 'secretAccessKey', 'secretNameTemplate', 'customTags'],
|
||||
'azure-kv': ['clientId', 'clientSecret', 'secretNameTemplate', 'customTags'],
|
||||
'gcp-sm': ['credentials', 'secretNameTemplate', 'customTags'],
|
||||
gh: ['accessToken', 'secretNameTemplate'],
|
||||
'vercel-project': ['accessToken', 'teamId', 'deploymentEnvironments', 'secretNameTemplate'],
|
||||
'aws-sm': ['accessKeyId', 'secretAccessKey', 'granularity', 'secretNameTemplate', 'customTags'],
|
||||
'azure-kv': ['clientId', 'clientSecret', 'granularity', 'secretNameTemplate', 'customTags'],
|
||||
'gcp-sm': ['credentials', 'granularity', 'secretNameTemplate', 'customTags'],
|
||||
gh: ['accessToken', 'granularity', 'secretNameTemplate'],
|
||||
'vercel-project': [
|
||||
'accessToken',
|
||||
'teamId',
|
||||
'deploymentEnvironments',
|
||||
'granularity',
|
||||
'secretNameTemplate',
|
||||
],
|
||||
};
|
||||
const EXPECTED_VALUE = (key) => {
|
||||
switch (key) {
|
||||
case 'deployment_environments':
|
||||
return ['production'];
|
||||
case 'custom_tags':
|
||||
return { foo: `new-${key}-value` };
|
||||
case 'deployment_environments':
|
||||
return ['production'];
|
||||
case 'granularity':
|
||||
return 'secret-key';
|
||||
default:
|
||||
// for all string type parameters
|
||||
return `new-${key}-value`;
|
||||
|
||||
Reference in New Issue
Block a user