mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 11:08:10 +00:00
Secrets Sync UI: Add sync destination fields custom_tags and secret_name_template (#24930)
* leverage isSectionHeader option to change component styling * update destination models to include new params * update form and details template to accommodate new fields * remove extra horizontal line * move is-empty-value to core addon and use in details template * remove leftover or conditional * update mirage and tests * update form tests
This commit is contained in:
@@ -24,6 +24,7 @@ export default IdentityModel.extend({
|
|||||||
}),
|
}),
|
||||||
metadata: attr({
|
metadata: attr({
|
||||||
editType: 'kv',
|
editType: 'kv',
|
||||||
|
isSectionHeader: true,
|
||||||
}),
|
}),
|
||||||
mountPath: attr('string', {
|
mountPath: attr('string', {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default IdentityModel.extend({
|
|||||||
mergedEntityIds: attr(),
|
mergedEntityIds: attr(),
|
||||||
metadata: attr({
|
metadata: attr({
|
||||||
editType: 'kv',
|
editType: 'kv',
|
||||||
|
isSectionHeader: true,
|
||||||
}),
|
}),
|
||||||
policies: attr({
|
policies: attr({
|
||||||
editType: 'yield',
|
editType: 'yield',
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export default IdentityModel.extend({
|
|||||||
}),
|
}),
|
||||||
metadata: attr('object', {
|
metadata: attr('object', {
|
||||||
editType: 'kv',
|
editType: 'kv',
|
||||||
|
isSectionHeader: true,
|
||||||
}),
|
}),
|
||||||
policies: attr({
|
policies: attr({
|
||||||
editType: 'yield',
|
editType: 'yield',
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export default class KvSecretMetadataModel extends Model {
|
|||||||
|
|
||||||
@attr('object', {
|
@attr('object', {
|
||||||
editType: 'kv',
|
editType: 'kv',
|
||||||
|
isSectionHeader: true,
|
||||||
subText: 'An optional set of informational key-value pairs that will be stored with all secret versions.',
|
subText: 'An optional set of informational key-value pairs that will be stored with all secret versions.',
|
||||||
})
|
})
|
||||||
customMetadata;
|
customMetadata;
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ 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;
|
||||||
|
@attr('string', {
|
||||||
|
subText:
|
||||||
|
'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;
|
||||||
|
|
||||||
// only present if delete action has been initiated
|
// only present if delete action has been initiated
|
||||||
@attr('string') purgeInitiatedAt;
|
@attr('string') purgeInitiatedAt;
|
||||||
@attr('string') purgeError;
|
@attr('string') purgeError;
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ import SyncDestinationModel from '../destination';
|
|||||||
import { attr } from '@ember-data/model';
|
import { attr } from '@ember-data/model';
|
||||||
import { withFormFields } from 'vault/decorators/model-form-fields';
|
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||||
|
|
||||||
const displayFields = ['name', 'region', 'accessKeyId', 'secretAccessKey'];
|
const displayFields = [
|
||||||
|
'name',
|
||||||
|
'region',
|
||||||
|
'accessKeyId',
|
||||||
|
'secretAccessKey',
|
||||||
|
'secretNameTemplate',
|
||||||
|
'customTags',
|
||||||
|
];
|
||||||
const formFieldGroups = [
|
const formFieldGroups = [
|
||||||
{ default: ['name', 'region'] },
|
{ default: ['name', 'region', 'secretNameTemplate', 'customTags'] },
|
||||||
{ Credentials: ['accessKeyId', 'secretAccessKey'] },
|
{ Credentials: ['accessKeyId', 'secretAccessKey'] },
|
||||||
];
|
];
|
||||||
@withFormFields(displayFields, formFieldGroups)
|
@withFormFields(displayFields, formFieldGroups)
|
||||||
@@ -34,4 +41,11 @@ export default class SyncDestinationsAwsSecretsManagerModel extends SyncDestinat
|
|||||||
editDisabled: true,
|
editDisabled: true,
|
||||||
})
|
})
|
||||||
region;
|
region;
|
||||||
|
|
||||||
|
@attr('object', {
|
||||||
|
subText:
|
||||||
|
'An optional set of informational key-value pairs added as additional metadata on secrets synced to this destination. Custom tags are merged with built-in tags.',
|
||||||
|
editType: 'kv',
|
||||||
|
})
|
||||||
|
customTags;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,19 @@
|
|||||||
import SyncDestinationModel from '../destination';
|
import SyncDestinationModel from '../destination';
|
||||||
import { attr } from '@ember-data/model';
|
import { attr } from '@ember-data/model';
|
||||||
import { withFormFields } from 'vault/decorators/model-form-fields';
|
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||||
const displayFields = ['name', 'keyVaultUri', 'tenantId', 'cloud', 'clientId', 'clientSecret'];
|
|
||||||
|
const displayFields = [
|
||||||
|
'name',
|
||||||
|
'keyVaultUri',
|
||||||
|
'tenantId',
|
||||||
|
'cloud',
|
||||||
|
'clientId',
|
||||||
|
'clientSecret',
|
||||||
|
'secretNameTemplate',
|
||||||
|
'customTags',
|
||||||
|
];
|
||||||
const formFieldGroups = [
|
const formFieldGroups = [
|
||||||
{ default: ['name', 'keyVaultUri', 'tenantId', 'cloud', 'clientId'] },
|
{ default: ['name', 'keyVaultUri', 'tenantId', 'cloud', 'clientId', 'secretNameTemplate', 'customTags'] },
|
||||||
{ Credentials: ['clientSecret'] },
|
{ Credentials: ['clientSecret'] },
|
||||||
];
|
];
|
||||||
@withFormFields(displayFields, formFieldGroups)
|
@withFormFields(displayFields, formFieldGroups)
|
||||||
@@ -47,4 +57,11 @@ export default class SyncDestinationsAzureKeyVaultModel extends SyncDestinationM
|
|||||||
editDisabled: true,
|
editDisabled: true,
|
||||||
})
|
})
|
||||||
cloud;
|
cloud;
|
||||||
|
|
||||||
|
@attr('object', {
|
||||||
|
subText:
|
||||||
|
'An optional set of informational key-value pairs added as additional metadata on secrets synced to this destination. Custom tags are merged with built-in tags.',
|
||||||
|
editType: 'kv',
|
||||||
|
})
|
||||||
|
customTags;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import SyncDestinationModel from '../destination';
|
|||||||
import { attr } from '@ember-data/model';
|
import { attr } from '@ember-data/model';
|
||||||
import { withFormFields } from 'vault/decorators/model-form-fields';
|
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||||
|
|
||||||
const displayFields = ['name', 'credentials'];
|
const displayFields = ['name', 'credentials', 'secretNameTemplate', 'customTags'];
|
||||||
const formFieldGroups = [{ default: ['name'] }, { Credentials: ['credentials'] }];
|
const formFieldGroups = [
|
||||||
|
{ default: ['name', 'secretNameTemplate', 'customTags'] },
|
||||||
|
{ Credentials: ['credentials'] },
|
||||||
|
];
|
||||||
@withFormFields(displayFields, formFieldGroups)
|
@withFormFields(displayFields, formFieldGroups)
|
||||||
export default class SyncDestinationsGoogleCloudSecretManagerModel extends SyncDestinationModel {
|
export default class SyncDestinationsGoogleCloudSecretManagerModel extends SyncDestinationModel {
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
@@ -20,5 +23,10 @@ export default class SyncDestinationsGoogleCloudSecretManagerModel extends SyncD
|
|||||||
})
|
})
|
||||||
credentials; // obfuscated, never returned by API
|
credentials; // obfuscated, never returned by API
|
||||||
|
|
||||||
// TODO - confirm if project_id is going to be added to READ response (not editable)
|
@attr('object', {
|
||||||
|
subText:
|
||||||
|
'An optional set of informational key-value pairs added as additional metadata on secrets synced to this destination. Custom tags are merged with built-in tags.',
|
||||||
|
editType: 'kv',
|
||||||
|
})
|
||||||
|
customTags;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
import SyncDestinationModel from '../destination';
|
import SyncDestinationModel from '../destination';
|
||||||
import { attr } from '@ember-data/model';
|
import { attr } from '@ember-data/model';
|
||||||
import { withFormFields } from 'vault/decorators/model-form-fields';
|
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||||
const displayFields = ['name', 'repositoryOwner', 'repositoryName', 'accessToken'];
|
const displayFields = ['name', 'repositoryOwner', 'repositoryName', 'accessToken', 'secretNameTemplate'];
|
||||||
const formFieldGroups = [
|
const formFieldGroups = [
|
||||||
{ default: ['name', 'repositoryOwner', 'repositoryName'] },
|
{ default: ['name', 'repositoryOwner', 'repositoryName', 'secretNameTemplate'] },
|
||||||
{ Credentials: ['accessToken'] },
|
{ Credentials: ['accessToken'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,17 @@ const validations = {
|
|||||||
// getter/setter for the deploymentEnvironments model attribute
|
// getter/setter for the deploymentEnvironments model attribute
|
||||||
deploymentEnvironmentsArray: [{ type: 'presence', message: 'At least one environment is required.' }],
|
deploymentEnvironmentsArray: [{ type: 'presence', message: 'At least one environment is required.' }],
|
||||||
};
|
};
|
||||||
const displayFields = ['name', 'accessToken', 'projectId', 'teamId', 'deploymentEnvironments'];
|
|
||||||
|
const displayFields = [
|
||||||
|
'name',
|
||||||
|
'accessToken',
|
||||||
|
'projectId',
|
||||||
|
'teamId',
|
||||||
|
'deploymentEnvironments',
|
||||||
|
'secretNameTemplate',
|
||||||
|
];
|
||||||
const formFieldGroups = [
|
const formFieldGroups = [
|
||||||
{ default: ['name', 'projectId', 'teamId', 'deploymentEnvironments'] },
|
{ default: ['name', 'projectId', 'teamId', 'deploymentEnvironments', 'secretNameTemplate'] },
|
||||||
{ Credentials: ['accessToken'] },
|
{ Credentials: ['accessToken'] },
|
||||||
];
|
];
|
||||||
@withModelValidations(validations)
|
@withModelValidations(validations)
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ export default class SyncDestinationSerializer extends ApplicationSerializer {
|
|||||||
// only send changed parameters for PATCH requests
|
// only send changed parameters for PATCH requests
|
||||||
const changedKeys = Object.keys(snapshot.changedAttributes()).map((key) => decamelize(key));
|
const changedKeys = Object.keys(snapshot.changedAttributes()).map((key) => decamelize(key));
|
||||||
return changedKeys.reduce((payload, key) => {
|
return changedKeys.reduce((payload, key) => {
|
||||||
payload[key] = data[key];
|
if (JSON.stringify(data[key]) === '{}') {
|
||||||
|
// sending an empty object won't clear the previous param, set to null so PATCH removes pre-existing value
|
||||||
|
payload[key] = null;
|
||||||
|
} else {
|
||||||
|
payload[key] = data[key];
|
||||||
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
@@ -55,10 +60,11 @@ export default class SyncDestinationSerializer extends ApplicationSerializer {
|
|||||||
} else if (payload?.data) {
|
} else if (payload?.data) {
|
||||||
// uses name for id and spreads connection_details object into data
|
// uses name for id and spreads connection_details object into data
|
||||||
const { data } = payload;
|
const { data } = payload;
|
||||||
const connection_details = payload.data.connection_details || {};
|
const { connection_details, options } = data;
|
||||||
data.id = data.name;
|
data.id = data.name;
|
||||||
delete data.connection_details;
|
delete data.connection_details;
|
||||||
return { data: { ...data, ...connection_details } };
|
delete data.options;
|
||||||
|
return { data: { ...data, ...connection_details, ...options } };
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,12 +133,12 @@
|
|||||||
@value={{get @model this.valuePath}}
|
@value={{get @model this.valuePath}}
|
||||||
@onChange={{this.setAndBroadcast}}
|
@onChange={{this.setAndBroadcast}}
|
||||||
@label={{this.labelString}}
|
@label={{this.labelString}}
|
||||||
@labelClass="title {{if (eq @mode 'create') 'is-5' 'is-4'}}"
|
@labelClass={{if @attr.options.isSectionHeader "title is-4" "is-label"}}
|
||||||
@helpText={{@attr.options.helpText}}
|
@helpText={{@attr.options.helpText}}
|
||||||
@subText={{@attr.options.subText}}
|
@subText={{@attr.options.subText}}
|
||||||
@onKeyUp={{this.handleKeyUp}}
|
@onKeyUp={{this.handleKeyUp}}
|
||||||
@validationError={{this.validationError}}
|
@validationError={{this.validationError}}
|
||||||
class="form-section"
|
class={{if @attr.options.isSectionHeader "form-section"}}
|
||||||
/>
|
/>
|
||||||
{{else if (eq @attr.options.editType "file")}}
|
{{else if (eq @attr.options.editType "file")}}
|
||||||
{{! File Input }}
|
{{! File Input }}
|
||||||
|
|||||||
6
ui/lib/core/app/helpers/is-empty-value.js
Normal file
6
ui/lib/core/app/helpers/is-empty-value.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default } from 'core/helpers/is-empty-value';
|
||||||
@@ -18,22 +18,21 @@
|
|||||||
<Hds::Text::Body @tag="p" @size="100" @color="faint" class="has-bottom-margin-m">
|
<Hds::Text::Body @tag="p" @size="100" @color="faint" class="has-bottom-margin-m">
|
||||||
Connection credentials are sensitive information and the value cannot be read. Enable the input to update.
|
Connection credentials are sensitive information and the value cannot be read. Enable the input to update.
|
||||||
</Hds::Text::Body>
|
</Hds::Text::Body>
|
||||||
{{/if}}
|
{{#each fields as |attr|}}
|
||||||
|
|
||||||
{{#each fields as |attr|}}
|
|
||||||
{{#if (and (eq group "Credentials") (not @destination.isNew))}}
|
|
||||||
<EnableInput data-test-enable-field={{attr.name}} class="field" @attr={{attr}}>
|
<EnableInput data-test-enable-field={{attr.name}} class="field" @attr={{attr}}>
|
||||||
<FormField @attr={{attr}} @model={{@destination}} @modelValidations={{this.modelValidations}} />
|
<FormField @attr={{attr}} @model={{@destination}} @modelValidations={{this.modelValidations}} />
|
||||||
</EnableInput>
|
</EnableInput>
|
||||||
{{else}}
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
{{#each fields as |attr|}}
|
||||||
<FormField
|
<FormField
|
||||||
@attr={{attr}}
|
@attr={{attr}}
|
||||||
@model={{@destination}}
|
@model={{@destination}}
|
||||||
@modelValidations={{this.modelValidations}}
|
@modelValidations={{this.modelValidations}}
|
||||||
@onKeyUp={{this.updateWarningValidation}}
|
@onKeyUp={{this.updateWarningValidation}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/each}}
|
||||||
{{/each}}
|
{{/if}}
|
||||||
{{/each-in}}
|
{{/each-in}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,22 @@
|
|||||||
<Secrets::DestinationHeader @destination={{@destination}} />
|
<Secrets::DestinationHeader @destination={{@destination}} />
|
||||||
|
|
||||||
{{#each @destination.formFields as |field|}}
|
{{#each @destination.formFields as |field|}}
|
||||||
{{#let (get @destination field.name) as |value|}}
|
{{#let (get @destination field.name) as |fieldValue|}}
|
||||||
{{#if (includes field.name @destination.maskedParams)}}
|
{{#if (includes field.name @destination.maskedParams)}}
|
||||||
<InfoTableRow @label={{or field.options.label (to-label field.name)}}>
|
<InfoTableRow @label={{or field.options.label (to-label field.name)}}>
|
||||||
<Hds::Badge @text={{this.credentialValue value}} @icon="check-circle" @color="success" />
|
<Hds::Badge @text={{this.credentialValue fieldValue}} @icon="check-circle" @color="success" />
|
||||||
</InfoTableRow>
|
</InfoTableRow>
|
||||||
|
{{else if (eq field.name "customTags")}}
|
||||||
|
{{#unless (is-empty-value fieldValue)}}
|
||||||
|
<Hds::Text::Display @tag="h3" @size="300" @weight="semibold" class="has-top-margin-l" data-test-section-header>
|
||||||
|
Custom tags
|
||||||
|
</Hds::Text::Display>
|
||||||
|
{{/unless}}
|
||||||
|
{{#each-in fieldValue as |key value|}}
|
||||||
|
<InfoTableRow @alwaysRender={{false}} @label={{key}} @value={{value}} />
|
||||||
|
{{/each-in}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<InfoTableRow @label={{or field.options.label (to-label field.name)}} @value={{value}} />
|
<InfoTableRow @label={{or field.options.label (to-label field.name)}} @value={{fieldValue}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/let}}
|
{{/let}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
@@ -9,38 +9,56 @@ export default Factory.extend({
|
|||||||
['aws-sm']: trait({
|
['aws-sm']: trait({
|
||||||
type: 'aws-sm',
|
type: 'aws-sm',
|
||||||
name: 'destination-aws',
|
name: 'destination-aws',
|
||||||
|
// connection_details
|
||||||
access_key_id: '*****',
|
access_key_id: '*****',
|
||||||
secret_access_key: '*****',
|
secret_access_key: '*****',
|
||||||
region: 'us-west-1',
|
region: 'us-west-1',
|
||||||
|
// options
|
||||||
|
secret_name_template: 'vault-{{ .MountAccessor | replace "_" "-" }}-{{ .SecretPath }}',
|
||||||
|
custom_tags: { foo: 'bar' },
|
||||||
}),
|
}),
|
||||||
['azure-kv']: trait({
|
['azure-kv']: trait({
|
||||||
type: 'azure-kv',
|
type: 'azure-kv',
|
||||||
name: 'destination-azure',
|
name: 'destination-azure',
|
||||||
|
// connection_details
|
||||||
key_vault_uri: 'https://keyvault-1234abcd.vault.azure.net',
|
key_vault_uri: 'https://keyvault-1234abcd.vault.azure.net',
|
||||||
subscription_id: 'subscription-id',
|
subscription_id: 'subscription-id',
|
||||||
tenant_id: 'tenant-id',
|
tenant_id: 'tenant-id',
|
||||||
client_id: 'azure-client-id',
|
client_id: 'azure-client-id',
|
||||||
client_secret: '*****',
|
client_secret: '*****',
|
||||||
cloud: 'Azure Public Cloud',
|
cloud: 'Azure Public Cloud',
|
||||||
|
// options
|
||||||
|
secret_name_template: 'vault-{{ .MountAccessor | replace "_" "-" }}-{{ .SecretPath }}',
|
||||||
|
custom_tags: { foo: 'bar' },
|
||||||
}),
|
}),
|
||||||
['gcp-sm']: trait({
|
['gcp-sm']: trait({
|
||||||
type: 'gcp-sm',
|
type: 'gcp-sm',
|
||||||
name: 'destination-gcp',
|
name: 'destination-gcp',
|
||||||
|
// connection_details
|
||||||
credentials: '*****',
|
credentials: '*****',
|
||||||
|
// options
|
||||||
|
secret_name_template: 'vault-{{ .MountAccessor | replace "_" "-" }}-{{ .SecretPath }}',
|
||||||
|
custom_tags: { foo: 'bar' },
|
||||||
}),
|
}),
|
||||||
gh: trait({
|
gh: trait({
|
||||||
type: 'gh',
|
type: 'gh',
|
||||||
name: 'destination-gh',
|
name: 'destination-gh',
|
||||||
|
// connection_details
|
||||||
access_token: '*****',
|
access_token: '*****',
|
||||||
repository_owner: 'my-organization-or-username',
|
repository_owner: 'my-organization-or-username',
|
||||||
repository_name: 'my-repository',
|
repository_name: 'my-repository',
|
||||||
|
// options
|
||||||
|
secret_name_template: 'vault-{{ .MountAccessor | replace "_" "-" }}-{{ .SecretPath }}',
|
||||||
}),
|
}),
|
||||||
['vercel-project']: trait({
|
['vercel-project']: trait({
|
||||||
type: 'vercel-project',
|
type: 'vercel-project',
|
||||||
name: 'destination-vercel',
|
name: 'destination-vercel',
|
||||||
|
// connection_details
|
||||||
access_token: '*****',
|
access_token: '*****',
|
||||||
project_id: 'prj_12345',
|
project_id: 'prj_12345',
|
||||||
team_id: 'team_12345',
|
team_id: 'team_12345',
|
||||||
deployment_environments: ['development', 'preview'], // 'production' is also an option, but left out for testing to assert form changes value
|
deployment_environments: ['development', 'preview'], // 'production' is also an option, but left out for testing to assert form changes value
|
||||||
|
// options
|
||||||
|
secret_name_template: 'vault-{{ .MountAccessor | replace "_" "-" }}-{{ .SecretPath }}',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const SELECTORS = {
|
|||||||
// FORMS
|
// FORMS
|
||||||
infoRowValue: (label) => `[data-test-value-div="${label}"]`,
|
infoRowValue: (label) => `[data-test-value-div="${label}"]`,
|
||||||
inputByAttr: (attr) => `[data-test-input="${attr}"]`,
|
inputByAttr: (attr) => `[data-test-input="${attr}"]`,
|
||||||
|
fieldByAttr: (attr) => `[data-test-field="${attr}"]`,
|
||||||
validation: (attr) => `[data-test-field-validation=${attr}]`,
|
validation: (attr) => `[data-test-field-validation=${attr}]`,
|
||||||
validationWarning: (attr) => `[data-test-validation-warning=${attr}]`,
|
validationWarning: (attr) => `[data-test-validation-warning=${attr}]`,
|
||||||
messageError: '[data-test-message-error]',
|
messageError: '[data-test-message-error]',
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ export const PAGE = {
|
|||||||
},
|
},
|
||||||
destinations: {
|
destinations: {
|
||||||
deleteBanner: '[data-test-delete-status-banner]',
|
deleteBanner: '[data-test-delete-status-banner]',
|
||||||
|
details: {
|
||||||
|
sectionHeader: '[data-test-section-header]',
|
||||||
|
},
|
||||||
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]',
|
||||||
@@ -71,6 +74,9 @@ export const PAGE = {
|
|||||||
case 'credentials':
|
case 'credentials':
|
||||||
await click('[data-test-text-toggle]');
|
await click('[data-test-text-toggle]');
|
||||||
return fillIn('[data-test-text-file-textarea]', value);
|
return fillIn('[data-test-text-file-textarea]', value);
|
||||||
|
case 'customTags':
|
||||||
|
await fillIn('[data-test-kv-key="0"]', 'foo');
|
||||||
|
return fillIn('[data-test-kv-value="0"]', value);
|
||||||
case 'deploymentEnvironments':
|
case 'deploymentEnvironments':
|
||||||
await click('[data-test-input="deploymentEnvironments"] input#development');
|
await click('[data-test-input="deploymentEnvironments"] input#development');
|
||||||
await click('[data-test-input="deploymentEnvironments"] input#preview');
|
await click('[data-test-input="deploymentEnvironments"] input#preview');
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE
|
|||||||
|
|
||||||
assert.dom(PAGE.title).hasTextContaining(`Create Destination for ${name}`);
|
assert.dom(PAGE.title).hasTextContaining(`Create Destination for ${name}`);
|
||||||
for (const attr of this.model.formFields) {
|
for (const attr of this.model.formFields) {
|
||||||
assert.dom(PAGE.inputByAttr(attr.name)).exists();
|
assert.dom(PAGE.fieldByAttr(attr.name)).exists();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,16 +206,18 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE
|
|||||||
|
|
||||||
// EDIT FORM ASSERTIONS FOR EACH DESTINATION TYPE
|
// EDIT FORM ASSERTIONS FOR EACH DESTINATION TYPE
|
||||||
const EDITABLE_FIELDS = {
|
const EDITABLE_FIELDS = {
|
||||||
'aws-sm': ['accessKeyId', 'secretAccessKey'],
|
'aws-sm': ['accessKeyId', 'secretAccessKey', 'secretNameTemplate', 'customTags'],
|
||||||
'azure-kv': ['clientId', 'clientSecret'],
|
'azure-kv': ['clientId', 'clientSecret', 'secretNameTemplate', 'customTags'],
|
||||||
'gcp-sm': ['credentials'],
|
'gcp-sm': ['credentials', 'secretNameTemplate', 'customTags'],
|
||||||
gh: ['accessToken'],
|
gh: ['accessToken', 'secretNameTemplate'],
|
||||||
'vercel-project': ['accessToken', 'teamId', 'deploymentEnvironments'],
|
'vercel-project': ['accessToken', 'teamId', 'deploymentEnvironments', 'secretNameTemplate'],
|
||||||
};
|
};
|
||||||
const EXPECTED_VALUE = (key) => {
|
const EXPECTED_VALUE = (key) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'deployment_environments':
|
case 'deployment_environments':
|
||||||
return ['production'];
|
return ['production'];
|
||||||
|
case 'custom_tags':
|
||||||
|
return { foo: `new-${key}-value` };
|
||||||
default:
|
default:
|
||||||
// for all string type parameters
|
// for all string type parameters
|
||||||
return `new-${key}-value`;
|
return `new-${key}-value`;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ module(
|
|||||||
this.unmaskedAttrs = this.model.formFields.filter((attr) => !maskedParams.includes(attr.name));
|
this.unmaskedAttrs = this.model.formFields.filter((attr) => !maskedParams.includes(attr.name));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders destination details with connection_details', async function (assert) {
|
test('it renders destination details with connection_details and options', async function (assert) {
|
||||||
assert.expect(this.model.formFields.length);
|
assert.expect(this.model.formFields.length);
|
||||||
|
|
||||||
await this.renderFormComponent();
|
await this.renderFormComponent();
|
||||||
@@ -86,23 +86,36 @@ module(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// assert the remaining model attributes render
|
// assert the remaining model attributes render
|
||||||
this.unmaskedAttrs.forEach(({ name, options }) => {
|
this.unmaskedAttrs.forEach(({ name, options, type }) => {
|
||||||
const label = options.label || toLabel([name]);
|
let label, value;
|
||||||
const value = Array.isArray(this.model[name]) ? this.model[name].join(',') : this.model[name];
|
if (type === 'object') {
|
||||||
|
[label] = Object.keys(this.model[name]);
|
||||||
|
[value] = Object.values(this.model[name]);
|
||||||
|
} else {
|
||||||
|
label = options.label || toLabel([name]);
|
||||||
|
value = Array.isArray(this.model[name]) ? this.model[name].join(',') : this.model[name];
|
||||||
|
}
|
||||||
assert.dom(PAGE.infoRowValue(label)).hasText(value);
|
assert.dom(PAGE.infoRowValue(label)).hasText(value);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders destination details without connection_details', async function (assert) {
|
test('it renders destination details without connection_details or options', async function (assert) {
|
||||||
assert.expect(this.maskedAttrs.length + 3);
|
assert.expect(this.maskedAttrs.length + 4);
|
||||||
|
|
||||||
this.maskedAttrs.forEach((attr) => {
|
this.maskedAttrs.forEach((attr) => {
|
||||||
// these values are undefined when environment variables are set
|
// these values are undefined when environment variables are set
|
||||||
this.model[attr.name] = undefined;
|
this.model[attr.name] = undefined;
|
||||||
});
|
});
|
||||||
|
// assert custom tags section header does not render
|
||||||
|
if (this.model?.get('customTags')) {
|
||||||
|
this.model['customTags'] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
await this.renderFormComponent();
|
await this.renderFormComponent();
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(PAGE.destinations.details.sectionHeader)
|
||||||
|
.doesNotExist('does not render Custom tags header');
|
||||||
assert.dom(PAGE.title).hasTextContaining(this.model.name);
|
assert.dom(PAGE.title).hasTextContaining(this.model.name);
|
||||||
assert.dom(PAGE.icon(this.model.icon)).exists();
|
assert.dom(PAGE.icon(this.model.icon)).exists();
|
||||||
assert.dom(PAGE.infoRowValue('Name')).hasText(this.model.name);
|
assert.dom(PAGE.infoRowValue('Name')).hasText(this.model.name);
|
||||||
|
|||||||
Reference in New Issue
Block a user