chore: 🤖 migrate host catalog and host set list wrapper fields to key value (#3276)
This commit is contained in:
1 parent
4e2b779d96
commit
0e223ded8f
26 files changed
+507
-383
No files matched your search
@@ -11,8 +11,5 @@
|
||||
@removeItemByIndex={{@removeItemByIndex}}
|
||||
@addStringItem={{@addStringItem}}
|
||||
@addAccountMapItem={{@addAccountMapItem}}
|
||||
@addRow={{this.addRow}}
|
||||
@removeRow={{this.removeRow}}
|
||||
@updateRow={{this.updateRow}}
|
||||
/>
|
||||
{{/if}}
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { assert } from '@ember/debug';
|
||||
import { action } from '@ember/object';
|
||||
import oidc from './oidc';
|
||||
import ldap from './ldap';
|
||||
import password from './password';
|
||||
@@ -25,47 +24,4 @@ export default class FormAuthMethodIndex extends Component {
|
||||
);
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new empty row to the specified field
|
||||
* @param {string} field - The field name to add a row to
|
||||
* @param {Array<string>} properties - Array of property names for the empty row (e.g., ['value'] or ['key', 'value'])
|
||||
*/
|
||||
@action
|
||||
addRow(field, properties) {
|
||||
const emptyRow = Object.fromEntries(properties.map((prop) => [prop, '']));
|
||||
this.args.model[field] = [...this.args.model[field], emptyRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a row from the specified field
|
||||
* @param {string} field - The field name to remove a row from
|
||||
* @param {object} rowData - The row data to remove
|
||||
*/
|
||||
@action
|
||||
removeRow(field, rowData) {
|
||||
let rows = this.args.model[field].filter((item) => item !== rowData);
|
||||
// Ensure at least one empty row exists for editing
|
||||
if (rows.length === 0) {
|
||||
const emptyRow = Object.fromEntries(
|
||||
Object.keys(rowData).map((key) => [key, '']),
|
||||
);
|
||||
rows = [emptyRow];
|
||||
}
|
||||
|
||||
this.args.model[field] = rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a specific property in a row
|
||||
* @param {string} field - The field name containing the row
|
||||
* @param {object} rowData - The row object to update
|
||||
* @param {string} property - The property name to update
|
||||
* @param {object} event - The DOM event containing the new value
|
||||
*/
|
||||
@action
|
||||
updateRow(field, rowData, property, { target: { value } }) {
|
||||
rowData[property] = value;
|
||||
this.args.model[field] = [...this.args.model[field]];
|
||||
}
|
||||
}
|
||||
@@ -84,10 +84,9 @@
|
||||
|
||||
<Form::Field::KeyValue
|
||||
@name='certificates'
|
||||
@model={{@model}}
|
||||
@properties={{array 'value'}}
|
||||
@data={{@model.certificates}}
|
||||
@onAdd={{fn @addRow 'certificates' (array 'value')}}
|
||||
@onRemove={{fn @removeRow 'certificates'}}
|
||||
@onUpdate={{fn @updateRow 'certificates'}}
|
||||
@legend={{t 'resources.auth-method.form.certificates.label'}}
|
||||
@helperText={{t 'resources.auth-method.form.certificates.help'}}
|
||||
@errors={{@model.errors.certificates}}
|
||||
@@ -402,10 +401,9 @@
|
||||
|
||||
<Form::field::KeyValue
|
||||
@name='account_attribute_maps'
|
||||
@model={{@model}}
|
||||
@properties={{array 'key' 'value'}}
|
||||
@data={{@model.account_attribute_maps}}
|
||||
@onAdd={{fn @addRow 'account_attribute_maps' (array 'key' 'value')}}
|
||||
@onRemove={{fn @removeRow 'account_attribute_maps'}}
|
||||
@onUpdate={{fn @updateRow 'account_attribute_maps'}}
|
||||
@legend={{t 'resources.auth-method.form.account_attribute_maps.label'}}
|
||||
@helperText={{t 'resources.auth-method.form.account_attribute_maps.help'}}
|
||||
@errors={{@model.errors.account_attribute_maps}}
|
||||
|
||||
@@ -149,10 +149,9 @@
|
||||
{{! Signing algorithms }}
|
||||
<Form::Field::KeyValue
|
||||
@name='signing_algorithms'
|
||||
@model={{@model}}
|
||||
@properties={{array 'value'}}
|
||||
@data={{@model.signing_algorithms}}
|
||||
@onAdd={{fn @addRow 'signing_algorithms' (array 'value')}}
|
||||
@onRemove={{fn @removeRow 'signing_algorithms'}}
|
||||
@onUpdate={{fn @updateRow 'signing_algorithms'}}
|
||||
@legend={{t 'form.signing_algorithms.label'}}
|
||||
@helperText={{t 'form.signing_algorithms.help'}}
|
||||
@errors={{@model.errors.signing_algorithms}}
|
||||
@@ -187,10 +186,9 @@
|
||||
{{! Allowed audiences }}
|
||||
<Form::Field::KeyValue
|
||||
@name='allowed_audiences'
|
||||
@model={{@model}}
|
||||
@properties={{array 'value'}}
|
||||
@data={{@model.allowed_audiences}}
|
||||
@onAdd={{fn @addRow 'allowed_audiences' (array 'value')}}
|
||||
@onRemove={{fn @removeRow 'allowed_audiences'}}
|
||||
@onUpdate={{fn @updateRow 'allowed_audiences'}}
|
||||
@legend={{t 'form.allowed_audiences.label'}}
|
||||
@helperText={{t 'form.allowed_audiences.help'}}
|
||||
@errors={{@model.errors.allowed_audiences}}
|
||||
@@ -214,10 +212,9 @@
|
||||
{{! Claims Scopes }}
|
||||
<Form::Field::KeyValue
|
||||
@name='claims_scopes'
|
||||
@model={{@model}}
|
||||
@properties={{array 'value'}}
|
||||
@data={{@model.claims_scopes}}
|
||||
@onAdd={{fn @addRow 'claims_scopes' (array 'value')}}
|
||||
@onRemove={{fn @removeRow 'claims_scopes'}}
|
||||
@onUpdate={{fn @updateRow 'claims_scopes'}}
|
||||
@legend={{t 'form.claims_scopes.label'}}
|
||||
@helperText={{t 'form.claims_scopes.help'}}
|
||||
@errors={{@model.errors.claims_scopes}}
|
||||
@@ -241,10 +238,9 @@
|
||||
{{! Account Claim Maps }}
|
||||
<Form::Field::KeyValue
|
||||
@name='account_claim_maps'
|
||||
@model={{@model}}
|
||||
@properties={{array 'key' 'value'}}
|
||||
@data={{@model.account_claim_maps}}
|
||||
@onAdd={{fn @addRow 'account_claim_maps' (array 'key' 'value')}}
|
||||
@onRemove={{fn @removeRow 'account_claim_maps'}}
|
||||
@onUpdate={{fn @updateRow 'account_claim_maps'}}
|
||||
@legend={{t 'form.account_claim_maps.label'}}
|
||||
@helperText={{t 'form.account_claim_maps.help'}}
|
||||
@errors={{@model.errors.account_claim_maps}}
|
||||
@@ -287,10 +283,9 @@
|
||||
{{! Certificates }}
|
||||
<Form::Field::KeyValue
|
||||
@name='idp_ca_certs'
|
||||
@model={{@model}}
|
||||
@properties={{array 'value'}}
|
||||
@data={{@model.idp_ca_certs}}
|
||||
@onAdd={{fn @addRow 'idp_ca_certs' (array 'value')}}
|
||||
@onRemove={{fn @removeRow 'idp_ca_certs'}}
|
||||
@onUpdate={{fn @updateRow 'idp_ca_certs'}}
|
||||
@legend={{t 'form.idp_ca_certs.label'}}
|
||||
@helperText={{t 'form.idp_ca_certs.help'}}
|
||||
@errors={{@model.errors.idp_ca_certs}}
|
||||
|
||||
@@ -227,10 +227,9 @@
|
||||
|
||||
<Form::Field::KeyValue
|
||||
@name='critical_options'
|
||||
@model={{@model}}
|
||||
@properties={{array 'key' 'value'}}
|
||||
@data={{@model.critical_options}}
|
||||
@onAdd={{this.addCriticalOption}}
|
||||
@onRemove={{this.removeCriticalOption}}
|
||||
@onUpdate={{this.updateCriticalOptions}}
|
||||
@legend={{t 'resources.credential-library.form.critical_options.label'}}
|
||||
@helperText={{t 'resources.credential-library.form.critical_options.help'}}
|
||||
@errors={{@model.errors.critical_options}}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
import { action } from '@ember/object';
|
||||
import { options } from 'api/models/credential-library';
|
||||
|
||||
export default class FormCredentialLibraryVaultSshCertComponent extends Component {
|
||||
@@ -24,41 +22,4 @@ export default class FormCredentialLibraryVaultSshCertComponent extends Componen
|
||||
const keyType = this.args.model.key_type;
|
||||
return keyType === 'rsa' || keyType === 'ecdsa';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new empty row to critical options
|
||||
*/
|
||||
@action
|
||||
addCriticalOption() {
|
||||
this.args.model.critical_options = [
|
||||
...this.args.model.critical_options,
|
||||
{ key: '', value: '' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates critical options data
|
||||
*/
|
||||
@action
|
||||
updateCriticalOptions(rowData, property, { target: { value } }) {
|
||||
rowData[property] = value;
|
||||
this.args.model.critical_options = [...this.args.model.critical_options];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a row from critical options
|
||||
* @param {Object} rowData - The row to remove
|
||||
*/
|
||||
@action
|
||||
removeCriticalOption(rowData) {
|
||||
let rows = this.args.model.critical_options.filter(
|
||||
(item) => item !== rowData,
|
||||
);
|
||||
|
||||
// Ensure at least one empty row exists for editing
|
||||
if (rows.length === 0) {
|
||||
rows = [{ key: '', value: '' }];
|
||||
}
|
||||
this.args.model.critical_options = rows;
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@
|
||||
Field=R.Field
|
||||
DeleteRowButton=R.DeleteRowButton
|
||||
rowData=R.rowData
|
||||
removeRow=(fn @onRemove R.rowData)
|
||||
updateRow=(fn @onUpdate R.rowData)
|
||||
removeRow=(fn this.handleRemove R.rowData)
|
||||
updateRow=(fn this.handleUpdate R.rowData)
|
||||
canDelete=this.canDelete
|
||||
)
|
||||
to='row'
|
||||
@@ -32,7 +32,7 @@
|
||||
<F.AddRowButton
|
||||
@text={{t 'actions.add'}}
|
||||
data-test-add-button
|
||||
@onClick={{@onAdd}}
|
||||
@onClick={{this.handleAdd}}
|
||||
/>
|
||||
{{#if @errors}}
|
||||
<F.Error as |E|>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { assert } from '@ember/debug';
|
||||
|
||||
export default class FormFieldKeyValueComponent extends Component {
|
||||
/**
|
||||
@@ -25,4 +27,78 @@ export default class FormFieldKeyValueComponent extends Component {
|
||||
// Return false if there are no rows
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default handler for adding a row.
|
||||
* Uses @model, @name, and @properties if provided, otherwise calls @onAdd.
|
||||
*/
|
||||
@action
|
||||
handleAdd() {
|
||||
// If an onAdd handler is provided, use it
|
||||
if (this.args.onAdd) {
|
||||
return this.args.onAdd();
|
||||
}
|
||||
|
||||
const { model, name, properties } = this.args;
|
||||
|
||||
assert(
|
||||
'FormFieldKeyValueComponent: model, name, and properties are required if no custom onAdd handler is provided.',
|
||||
model && name && properties,
|
||||
);
|
||||
|
||||
const emptyRow = Object.fromEntries(properties.map((prop) => [prop, '']));
|
||||
model[name] = [...(model[name] ?? []), emptyRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Default handler for removing a row.
|
||||
* Uses @model and @name if provided, otherwise calls @onRemove.
|
||||
*/
|
||||
@action
|
||||
handleRemove(rowData) {
|
||||
// If an onRemove handler is provided, use it
|
||||
if (this.args.onRemove) {
|
||||
return this.args.onRemove(rowData);
|
||||
}
|
||||
|
||||
const { model, name, properties } = this.args;
|
||||
|
||||
assert(
|
||||
'FormFieldKeyValueComponent: model, name, and properties are required if no custom onRemove handler is provided.',
|
||||
model && name && properties,
|
||||
);
|
||||
|
||||
const rows = (model[name] ?? []).filter((item) => item !== rowData);
|
||||
|
||||
// If removing the last row, add an empty row back
|
||||
if (rows.length === 0) {
|
||||
const emptyRow = Object.fromEntries(properties.map((prop) => [prop, '']));
|
||||
model[name] = [emptyRow];
|
||||
} else {
|
||||
model[name] = rows;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default handler for updating a row.
|
||||
* Uses @model and @name if provided, otherwise calls @onUpdate.
|
||||
*/
|
||||
@action
|
||||
handleUpdate(rowData, property, event) {
|
||||
// If an onUpdate handler is provided, use it
|
||||
if (this.args.onUpdate) {
|
||||
return this.args.onUpdate(rowData, property, event);
|
||||
}
|
||||
|
||||
const { model, name } = this.args;
|
||||
|
||||
assert(
|
||||
'FormFieldKeyValueComponent: model and name are required if no custom onUpdate handler is provided.',
|
||||
model && name,
|
||||
);
|
||||
|
||||
const value = event?.target?.value ?? event;
|
||||
rowData[property] = value;
|
||||
model[name] = [...(model[name] ?? [])];
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
}}
|
||||
|
||||
<Rose::Form
|
||||
@edit={{@edit}}
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@@ -271,44 +272,37 @@
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
{{! role_tags }}
|
||||
<Form::Field::ListWrapper
|
||||
@layout='horizontal'
|
||||
@isOptional={{true}}
|
||||
@disabled={{form.disabled}}
|
||||
>
|
||||
<:fieldset as |F|>
|
||||
<F.Legend>
|
||||
{{t 'resources.storage-bucket.form.role_tags.label'}}
|
||||
</F.Legend>
|
||||
<F.HelperText>
|
||||
{{t 'resources.storage-bucket.form.role_tags.help'}}
|
||||
</F.HelperText>
|
||||
|
||||
{{#if @model.errors.role_tags}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.role_tags as |error|}}
|
||||
<E.Message>
|
||||
{{error.message}}
|
||||
</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</:fieldset>
|
||||
<:field as |F|>
|
||||
<F.KeyValue
|
||||
<Form::Field::KeyValue
|
||||
@name='role_tags'
|
||||
@options={{@model.role_tags}}
|
||||
@model={{@model}}
|
||||
@properties={{array 'key' 'value'}}
|
||||
@data={{@model.role_tags}}
|
||||
@legend={{t 'resources.storage-bucket.form.role_tags.label'}}
|
||||
@helperText={{t 'resources.storage-bucket.form.role_tags.help'}}
|
||||
@errors={{@model.errors.role_tags}}
|
||||
@disabled={{form.disabled}}
|
||||
@isOptional={{true}}
|
||||
>
|
||||
<:key as |K|>
|
||||
<K.text />
|
||||
</:key>
|
||||
<:value as |V|>
|
||||
<V.text />
|
||||
</:value>
|
||||
</F.KeyValue>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput
|
||||
data-test-key-input
|
||||
@value={{R.rowData.key}}
|
||||
{{on 'input' (fn R.updateRow 'key')}}
|
||||
/>
|
||||
</R.Field>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput
|
||||
data-test-value-input
|
||||
@value={{R.rowData.value}}
|
||||
{{on 'input' (fn R.updateRow 'value')}}
|
||||
/>
|
||||
</R.Field>
|
||||
{{#if R.canDelete}}
|
||||
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
|
||||
{{/if}}
|
||||
</:row>
|
||||
</Form::Field::KeyValue>
|
||||
|
||||
{{else}}
|
||||
<Hds::Form::TextInput::Field
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
}}
|
||||
|
||||
<Rose::Form
|
||||
@edit={{@edit}}
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
}}
|
||||
|
||||
<Rose::Form
|
||||
@edit={{@edit}}
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
<this.hostCatalogFormComponent
|
||||
@model={{@model}}
|
||||
@edit={{@edit}}
|
||||
@submit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@changeType={{@changeType}}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
}}
|
||||
|
||||
<Rose::Form
|
||||
@edit={{@edit}}
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
|
||||
@@ -62,65 +62,55 @@
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
|
||||
<Form::Field::ListWrapper
|
||||
@layout='horizontal'
|
||||
@isOptional={{true}}
|
||||
@disabled={{form.disabled}}
|
||||
>
|
||||
<:fieldset as |F|>
|
||||
<F.Legend>
|
||||
{{t 'resources.host-set.form.preferred_endpoints.label'}}
|
||||
</F.Legend>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-set.form.preferred_endpoints.help'}}
|
||||
</F.HelperText>
|
||||
|
||||
{{#if @model.errors.preferred_endpoints}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.preferred_endpoints as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</:fieldset>
|
||||
<:field as |F|>
|
||||
<F.TextInput
|
||||
<Form::Field::KeyValue
|
||||
@name='preferred_endpoints'
|
||||
@options={{@model.preferred_endpoints}}
|
||||
@model={{@model}}
|
||||
/>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
|
||||
<Form::Field::ListWrapper
|
||||
@layout='horizontal'
|
||||
@isOptional={{true}}
|
||||
@properties={{array 'value'}}
|
||||
@data={{@model.preferred_endpoints}}
|
||||
@legend={{t 'resources.host-set.form.preferred_endpoints.label'}}
|
||||
@helperText={{t 'resources.host-set.form.preferred_endpoints.help'}}
|
||||
@errors={{@model.errors.preferred_endpoints}}
|
||||
@disabled={{form.disabled}}
|
||||
@isOptional={{true}}
|
||||
>
|
||||
<:fieldset as |F|>
|
||||
<F.Legend>
|
||||
{{t 'resources.host-set.form.filter.label'}}
|
||||
</F.Legend>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-set.form.filter.aws.help'}}
|
||||
</F.HelperText>
|
||||
|
||||
{{#if @model.errors.filters}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.filters as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</:fieldset>
|
||||
<:field as |F|>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput
|
||||
@name='filters'
|
||||
@options={{@model.filters}}
|
||||
@model={{@model}}
|
||||
data-test-value-input
|
||||
@value={{R.rowData.value}}
|
||||
{{on 'input' (fn R.updateRow 'value')}}
|
||||
/>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
</R.Field>
|
||||
{{#if R.canDelete}}
|
||||
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
|
||||
{{/if}}
|
||||
</:row>
|
||||
</Form::Field::KeyValue>
|
||||
|
||||
<Form::Field::KeyValue
|
||||
@name='filters'
|
||||
@model={{@model}}
|
||||
@properties={{array 'value'}}
|
||||
@data={{@model.filters}}
|
||||
@legend={{t 'resources.host-set.form.filter.label'}}
|
||||
@helperText={{t 'resources.host-set.form.filter.aws.help'}}
|
||||
@errors={{@model.errors.filters}}
|
||||
@disabled={{form.disabled}}
|
||||
@isOptional={{true}}
|
||||
>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput
|
||||
data-test-value-input
|
||||
@value={{R.rowData.value}}
|
||||
{{on 'input' (fn R.updateRow 'value')}}
|
||||
/>
|
||||
</R.Field>
|
||||
{{#if R.canDelete}}
|
||||
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
|
||||
{{/if}}
|
||||
</:row>
|
||||
</Form::Field::KeyValue>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
name='sync_interval_seconds'
|
||||
|
||||
@@ -62,36 +62,30 @@
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
|
||||
<Form::Field::ListWrapper
|
||||
@layout='horizontal'
|
||||
@isOptional={{true}}
|
||||
@disabled={{form.disabled}}
|
||||
>
|
||||
<:fieldset as |F|>
|
||||
<F.Legend>
|
||||
{{t 'resources.host-set.form.preferred_endpoints.label'}}
|
||||
</F.Legend>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-set.form.preferred_endpoints.help'}}
|
||||
</F.HelperText>
|
||||
|
||||
{{#if @model.errors.preferred_endpoints}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.preferred_endpoints as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</:fieldset>
|
||||
|
||||
<:field as |F|>
|
||||
<F.TextInput
|
||||
<Form::Field::KeyValue
|
||||
@name='preferred_endpoints'
|
||||
@options={{@model.preferred_endpoints}}
|
||||
@model={{@model}}
|
||||
@properties={{array 'value'}}
|
||||
@data={{@model.preferred_endpoints}}
|
||||
@legend={{t 'resources.host-set.form.preferred_endpoints.label'}}
|
||||
@helperText={{t 'resources.host-set.form.preferred_endpoints.help'}}
|
||||
@errors={{@model.errors.preferred_endpoints}}
|
||||
@disabled={{form.disabled}}
|
||||
@isOptional={{true}}
|
||||
>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput
|
||||
data-test-value-input
|
||||
@value={{R.rowData.value}}
|
||||
{{on 'input' (fn R.updateRow 'value')}}
|
||||
/>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
</R.Field>
|
||||
{{#if R.canDelete}}
|
||||
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
|
||||
{{/if}}
|
||||
</:row>
|
||||
</Form::Field::KeyValue>
|
||||
|
||||
<Hds::Form::Textarea::Field
|
||||
name='filter'
|
||||
|
||||
@@ -61,65 +61,55 @@
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
|
||||
<Form::Field::ListWrapper
|
||||
@layout='horizontal'
|
||||
@isOptional={{true}}
|
||||
@disabled={{form.disabled}}
|
||||
>
|
||||
<:fieldset as |F|>
|
||||
<F.Legend>
|
||||
{{t 'resources.host-set.form.preferred_endpoints.label'}}
|
||||
</F.Legend>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-set.form.preferred_endpoints.help'}}
|
||||
</F.HelperText>
|
||||
|
||||
{{#if @model.errors.preferred_endpoints}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.preferred_endpoints as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</:fieldset>
|
||||
<:field as |F|>
|
||||
<F.TextInput
|
||||
<Form::Field::KeyValue
|
||||
@name='preferred_endpoints'
|
||||
@options={{@model.preferred_endpoints}}
|
||||
@model={{@model}}
|
||||
/>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
|
||||
<Form::Field::ListWrapper
|
||||
@layout='horizontal'
|
||||
@isOptional={{true}}
|
||||
@properties={{array 'value'}}
|
||||
@data={{@model.preferred_endpoints}}
|
||||
@legend={{t 'resources.host-set.form.preferred_endpoints.label'}}
|
||||
@helperText={{t 'resources.host-set.form.preferred_endpoints.help'}}
|
||||
@errors={{@model.errors.preferred_endpoints}}
|
||||
@disabled={{form.disabled}}
|
||||
@isOptional={{true}}
|
||||
>
|
||||
<:fieldset as |F|>
|
||||
<F.Legend>
|
||||
{{t 'resources.host-set.form.filter.label'}}
|
||||
</F.Legend>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-set.form.filter.gcp.help'}}
|
||||
</F.HelperText>
|
||||
|
||||
{{#if @model.errors.filters}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.filters as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</:fieldset>
|
||||
<:field as |F|>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput
|
||||
@name='filters'
|
||||
@options={{@model.filters}}
|
||||
@model={{@model}}
|
||||
data-test-value-input
|
||||
@value={{R.rowData.value}}
|
||||
{{on 'input' (fn R.updateRow 'value')}}
|
||||
/>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
</R.Field>
|
||||
{{#if R.canDelete}}
|
||||
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
|
||||
{{/if}}
|
||||
</:row>
|
||||
</Form::Field::KeyValue>
|
||||
|
||||
<Form::Field::KeyValue
|
||||
@name='filters'
|
||||
@model={{@model}}
|
||||
@properties={{array 'value'}}
|
||||
@data={{@model.filters}}
|
||||
@legend={{t 'resources.host-set.form.filter.label'}}
|
||||
@helperText={{t 'resources.host-set.form.filter.gcp.help'}}
|
||||
@errors={{@model.errors.filters}}
|
||||
@disabled={{form.disabled}}
|
||||
@isOptional={{true}}
|
||||
>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput
|
||||
data-test-value-input
|
||||
@value={{R.rowData.value}}
|
||||
{{on 'input' (fn R.updateRow 'value')}}
|
||||
/>
|
||||
</R.Field>
|
||||
{{#if R.canDelete}}
|
||||
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
|
||||
{{/if}}
|
||||
</:row>
|
||||
</Form::Field::KeyValue>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
name='sync_interval_seconds'
|
||||
|
||||
@@ -76,6 +76,16 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsIndexController e
|
||||
isNew ? 'notifications.create-success' : 'notifications.save-success',
|
||||
)
|
||||
async save(hostSet) {
|
||||
if (hostSet.preferred_endpoints) {
|
||||
hostSet.preferred_endpoints = hostSet.preferred_endpoints.filter((item) =>
|
||||
item.value?.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
if (hostSet.filters) {
|
||||
hostSet.filters = hostSet.filters.filter((item) => item.value?.trim());
|
||||
}
|
||||
|
||||
// Fetch newest host set as updates to host set attributes cause an async db update which
|
||||
// updates the version again and can cause a version mismatch if the host set is updated
|
||||
// again and we haven't fetched the newest version.
|
||||
@@ -138,9 +148,15 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsIndexController e
|
||||
hostSet.preferred_endpoints = structuredClone(
|
||||
hostSet.preferred_endpoints,
|
||||
);
|
||||
if (hostSet.preferred_endpoints.length === 0) {
|
||||
hostSet.preferred_endpoints = [{ value: '' }];
|
||||
}
|
||||
}
|
||||
if (hostSet.filters) {
|
||||
hostSet.filters = structuredClone(hostSet.filters);
|
||||
if (hostSet.filters.length === 0) {
|
||||
hostSet.filters = [{ value: '' }];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
import { TYPE_CREDENTIAL_STATIC } from 'api/models/host-catalog';
|
||||
import {
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
TYPE_CREDENTIAL_STATIC,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
} from 'api/models/host-catalog';
|
||||
|
||||
export default class ScopesScopeHostCatalogsIndexController extends Controller {
|
||||
// =services
|
||||
@@ -60,6 +64,19 @@ export default class ScopesScopeHostCatalogsIndexController extends Controller {
|
||||
return this.intl.t(description, { resource });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates one empty role_tags row for AWS host catalogs when the field is empty.
|
||||
* @param {HostCatalogModel} hostCatalog
|
||||
*/
|
||||
generateAwsRoleTagsRowIfEmpty(hostCatalog) {
|
||||
if (
|
||||
hostCatalog.compositeType === TYPE_HOST_CATALOG_PLUGIN_AWS &&
|
||||
(!hostCatalog.role_tags || hostCatalog.role_tags.length === 0)
|
||||
) {
|
||||
hostCatalog.role_tags = [{ key: '', value: '' }];
|
||||
}
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
@@ -96,6 +113,12 @@ export default class ScopesScopeHostCatalogsIndexController extends Controller {
|
||||
isNew ? 'notifications.create-success' : 'notifications.save-success',
|
||||
)
|
||||
async save(hostCatalog) {
|
||||
if (hostCatalog.role_tags) {
|
||||
hostCatalog.role_tags = hostCatalog.role_tags.filter((item) =>
|
||||
item.key?.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
// If the role_arn is empty, then the credential type should be static
|
||||
if (!hostCatalog.role_arn) {
|
||||
hostCatalog.credentialType = TYPE_CREDENTIAL_STATIC;
|
||||
@@ -145,6 +168,25 @@ export default class ScopesScopeHostCatalogsIndexController extends Controller {
|
||||
@action
|
||||
changeCredentialType(hostCatalog, credentialType) {
|
||||
hostCatalog.credentialType = credentialType;
|
||||
|
||||
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
|
||||
this.generateAwsRoleTagsRowIfEmpty(hostCatalog);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares key-value array fields for editing.
|
||||
* @param {HostCatalogModel} hostCatalog
|
||||
*/
|
||||
@action
|
||||
edit(hostCatalog) {
|
||||
if (hostCatalog.role_tags) {
|
||||
hostCatalog.role_tags = structuredClone(hostCatalog.role_tags);
|
||||
}
|
||||
|
||||
if (hostCatalog.credentialType === TYPE_CREDENTIAL_DYNAMIC) {
|
||||
this.generateAwsRoleTagsRowIfEmpty(hostCatalog);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import {
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
} from 'api/models/host-catalog';
|
||||
|
||||
export default class ScopesScopeHostCatalogsHostCatalogHostSetsNewRoute extends Route {
|
||||
// =services
|
||||
@@ -41,9 +46,21 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsNewRoute extends
|
||||
const { id: host_catalog_id, compositeType } = this.modelFor(
|
||||
'scopes.scope.host-catalogs.host-catalog',
|
||||
);
|
||||
return this.store.createRecord('host-set', {
|
||||
const record = this.store.createRecord('host-set', {
|
||||
compositeType,
|
||||
host_catalog_id,
|
||||
});
|
||||
|
||||
if (
|
||||
compositeType === TYPE_HOST_CATALOG_PLUGIN_AWS ||
|
||||
compositeType === TYPE_HOST_CATALOG_PLUGIN_GCP
|
||||
) {
|
||||
record.preferred_endpoints = [{ value: '' }];
|
||||
record.filters = [{ value: '' }];
|
||||
} else if (compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE) {
|
||||
record.preferred_endpoints = [{ value: '' }];
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { TYPE_HOST_CATALOG_PLUGIN_AWS } from 'api/models/host-catalog';
|
||||
|
||||
export default class ScopesScopeHostCatalogsNewRoute extends Route {
|
||||
// =services
|
||||
@@ -59,6 +60,12 @@ export default class ScopesScopeHostCatalogsNewRoute extends Route {
|
||||
description,
|
||||
disable_credential_rotation,
|
||||
});
|
||||
|
||||
if (compositeType === TYPE_HOST_CATALOG_PLUGIN_AWS) {
|
||||
// Ensure one editable role_tags row exists for dynamic credentials.
|
||||
record.role_tags = [{ key: '', value: '' }];
|
||||
}
|
||||
|
||||
record.scopeModel = scopeModel;
|
||||
return record;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<page.body>
|
||||
<Form::HostCatalog
|
||||
@model={{@model}}
|
||||
@edit={{fn this.hostCatalogs.edit @model}}
|
||||
@submit={{fn this.hostCatalogs.save @model}}
|
||||
@cancel={{fn this.hostCatalogs.cancel @model}}
|
||||
@changeCredentialType={{fn this.hostCatalogs.changeCredentialType @model}}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<page.body>
|
||||
<Form::HostCatalog
|
||||
@model={{@model}}
|
||||
@edit={{fn this.hostCatalogs.edit @model}}
|
||||
@submit={{fn this.hostCatalogs.save @model}}
|
||||
@cancel={{fn this.hostCatalogs.cancel @model}}
|
||||
@changeType={{this.hostCatalogs.changeType}}
|
||||
|
||||
@@ -97,9 +97,7 @@ module('Acceptance | host-catalogs | host sets | create', function (hooks) {
|
||||
const name = 'aws host set';
|
||||
await fillIn(commonSelectors.FIELD_NAME, name);
|
||||
await fillIn(selectors.FIELD_PREFERRED_ENDPOINT, 'endpoint');
|
||||
await click(selectors.FIELD_PREFERRED_ENDPOINT_ADD_BTN);
|
||||
await fillIn(selectors.FIELD_FILTERS, 'filter_test');
|
||||
await click(selectors.FIELD_FILTERS_ADD_BTN);
|
||||
|
||||
await fillIn(selectors.FIELD_SYNC_INTERVAL, 10);
|
||||
await click(commonSelectors.SAVE_BTN);
|
||||
@@ -128,7 +126,6 @@ module('Acceptance | host-catalogs | host sets | create', function (hooks) {
|
||||
const name = 'azure host set';
|
||||
await fillIn(commonSelectors.FIELD_NAME, name);
|
||||
await fillIn(selectors.FIELD_PREFERRED_ENDPOINT, 'endpoint');
|
||||
await click(selectors.FIELD_PREFERRED_ENDPOINT_ADD_BTN);
|
||||
await fillIn(selectors.FIELD_AZURE_FILTER, 'filter');
|
||||
await fillIn(selectors.FIELD_SYNC_INTERVAL, 10);
|
||||
await click(commonSelectors.SAVE_BTN);
|
||||
@@ -157,9 +154,7 @@ module('Acceptance | host-catalogs | host sets | create', function (hooks) {
|
||||
const name = 'gcp host set';
|
||||
await fillIn(commonSelectors.FIELD_NAME, name);
|
||||
await fillIn(selectors.FIELD_PREFERRED_ENDPOINT, 'endpoint');
|
||||
await click(selectors.FIELD_PREFERRED_ENDPOINT_ADD_BTN);
|
||||
await fillIn(selectors.FIELD_FILTERS, 'filter_test');
|
||||
await click(selectors.FIELD_FILTERS_ADD_BTN);
|
||||
|
||||
await fillIn(selectors.FIELD_SYNC_INTERVAL, 10);
|
||||
await click(commonSelectors.SAVE_BTN);
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export const FIELD_PREFERRED_ENDPOINT = '[name=preferred_endpoints] input';
|
||||
export const FIELD_PREFERRED_ENDPOINT =
|
||||
'[name=preferred_endpoints] [data-test-value-input]';
|
||||
export const FIELD_PREFERRED_ENDPOINT_ADD_BTN =
|
||||
'[name=preferred_endpoints] button';
|
||||
'[name=preferred_endpoints] [data-test-add-button]';
|
||||
export const FIELD_PREFERRED_ENDPOINT_DELETE_BTN =
|
||||
'[name=preferred_endpoints] [data-test-remove-button]';
|
||||
export const FIELD_FILTERS = '[name=filters] input';
|
||||
export const FIELD_FILTERS_ADD_BTN = '[name=filters] button';
|
||||
'[name=preferred_endpoints] [data-test-delete-button]';
|
||||
export const FIELD_FILTERS = '[name=filters] [data-test-value-input]';
|
||||
export const FIELD_FILTERS_ADD_BTN = '[name=filters] [data-test-add-button]';
|
||||
export const FIELD_SYNC_INTERVAL = '[name=sync_interval_seconds]';
|
||||
export const FIELD_AZURE_FILTER = '[name=filter]';
|
||||
export const FIELD_FILTERS_REMOVE_BTN =
|
||||
'[name=filters] [data-test-remove-button]';
|
||||
'[name=filters] [data-test-delete-button]';
|
||||
|
||||
export const MANAGE_DROPDOWN_HOST_CATALOG =
|
||||
'[data-test-manage-host-catalogs-dropdown] button';
|
||||
|
||||
@@ -172,7 +172,6 @@ module('Acceptance | host-catalogs | host sets | update', function (hooks) {
|
||||
);
|
||||
|
||||
await fillIn(selectors.FIELD_PREFERRED_ENDPOINT, mockEndpoint);
|
||||
await click(selectors.FIELD_PREFERRED_ENDPOINT_ADD_BTN);
|
||||
|
||||
const filterList = await Promise.all(
|
||||
findAll(selectors.FIELD_FILTERS_REMOVE_BTN),
|
||||
@@ -184,7 +183,6 @@ module('Acceptance | host-catalogs | host sets | update', function (hooks) {
|
||||
assert.strictEqual(findAll(selectors.FIELD_FILTERS_REMOVE_BTN).length, 0);
|
||||
|
||||
await fillIn(selectors.FIELD_FILTERS, mockFilter);
|
||||
await click(selectors.FIELD_FILTERS_ADD_BTN);
|
||||
await fillIn(selectors.FIELD_SYNC_INTERVAL, 10);
|
||||
await click(commonSelectors.SAVE_BTN);
|
||||
|
||||
@@ -216,7 +214,6 @@ module('Acceptance | host-catalogs | host sets | update', function (hooks) {
|
||||
);
|
||||
|
||||
await fillIn(selectors.FIELD_PREFERRED_ENDPOINT, mockEndpoint);
|
||||
await click(selectors.FIELD_PREFERRED_ENDPOINT_ADD_BTN);
|
||||
await fillIn(selectors.FIELD_AZURE_FILTER, 'filter');
|
||||
await fillIn(selectors.FIELD_SYNC_INTERVAL, 10);
|
||||
await click(commonSelectors.SAVE_BTN);
|
||||
@@ -247,7 +244,6 @@ module('Acceptance | host-catalogs | host sets | update', function (hooks) {
|
||||
);
|
||||
|
||||
await fillIn(selectors.FIELD_PREFERRED_ENDPOINT, 'sample endpoint');
|
||||
await click(selectors.FIELD_PREFERRED_ENDPOINT_ADD_BTN);
|
||||
|
||||
// Remove all the filters
|
||||
const filterList = await Promise.all(
|
||||
@@ -260,7 +256,6 @@ module('Acceptance | host-catalogs | host sets | update', function (hooks) {
|
||||
assert.strictEqual(findAll(selectors.FIELD_FILTERS_REMOVE_BTN).length, 0);
|
||||
|
||||
await fillIn(selectors.FIELD_FILTERS, 'sample filters');
|
||||
await click(selectors.FIELD_FILTERS_ADD_BTN);
|
||||
await fillIn(selectors.FIELD_SYNC_INTERVAL, 10);
|
||||
await click(commonSelectors.SAVE_BTN);
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ module('Integration | Component | form/field/key-value', function (hooks) {
|
||||
const getValueInputs = () => findAll('[data-test-value-input]');
|
||||
const getDeleteButtons = () => findAll('[data-test-delete-button]');
|
||||
|
||||
test('it renders with default empty row', async function (assert) {
|
||||
test('it renders with default and provided data', async function (assert) {
|
||||
// Default empty row
|
||||
this.set('data', [{ key: '', value: '' }]);
|
||||
|
||||
await render(hbs`
|
||||
<Form::Field::KeyValue @data={{this.data}}>
|
||||
<:row as |R|>
|
||||
@@ -43,14 +43,12 @@ module('Integration | Component | form/field/key-value', function (hooks) {
|
||||
assert.dom('[data-test-value-input]').exists();
|
||||
assert.dom('[data-test-add-button]').exists();
|
||||
assert.dom('[data-test-delete-button]').doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders with provided data', async function (assert) {
|
||||
// Provided data
|
||||
this.set('data', [
|
||||
{ key: 'env', value: 'production' },
|
||||
{ key: 'region', value: 'us-east-1' },
|
||||
]);
|
||||
|
||||
await render(hbs`
|
||||
<Form::Field::KeyValue
|
||||
@data={{this.data}}
|
||||
@@ -72,13 +70,10 @@ module('Integration | Component | form/field/key-value', function (hooks) {
|
||||
</:footer>
|
||||
</Form::Field::KeyValue>
|
||||
`);
|
||||
|
||||
const keyInputs = getKeyInputs();
|
||||
const valueInputs = getValueInputs();
|
||||
|
||||
assert.strictEqual(keyInputs.length, 2);
|
||||
assert.strictEqual(valueInputs.length, 2);
|
||||
|
||||
assert.dom(keyInputs[0]).hasValue('env');
|
||||
assert.dom(keyInputs[1]).hasValue('region');
|
||||
assert.dom(valueInputs[0]).hasValue('production');
|
||||
@@ -87,7 +82,7 @@ module('Integration | Component | form/field/key-value', function (hooks) {
|
||||
assert.dom('.hds-form-helper-text ').hasText('Test helper text');
|
||||
});
|
||||
|
||||
test('it adds new rows', async function (assert) {
|
||||
test('it adds new rows using custom handlers', async function (assert) {
|
||||
this.set('data', [{ key: '', value: '' }]);
|
||||
this.set('onAdd', () => {
|
||||
this.data.push({ key: '', value: '' });
|
||||
@@ -117,89 +112,22 @@ module('Integration | Component | form/field/key-value', function (hooks) {
|
||||
`);
|
||||
|
||||
assert.strictEqual(getKeyInputs().length, 1, 'Initially has one row');
|
||||
|
||||
await click('[data-test-add-button]');
|
||||
|
||||
assert.strictEqual(
|
||||
getKeyInputs().length,
|
||||
2,
|
||||
'After clicking add, has two rows',
|
||||
);
|
||||
assert.strictEqual(getValueInputs().length, 2);
|
||||
|
||||
// Fill in the second row
|
||||
await fillIn(getKeyInputs()[1], 'test-key');
|
||||
await fillIn(getValueInputs()[1], 'test-value');
|
||||
|
||||
// Check that the data was updated
|
||||
assert.strictEqual(this.data.length, 2);
|
||||
assert.strictEqual(this.data[1].key, 'test-key');
|
||||
assert.strictEqual(this.data[1].value, 'test-value');
|
||||
});
|
||||
|
||||
test('it removes rows', async function (assert) {
|
||||
this.set('data', [
|
||||
{ key: 'key1', value: 'value1' },
|
||||
{ key: 'key2', value: 'value2' },
|
||||
]);
|
||||
this.set('onRemove', (rowData) => {
|
||||
// Remove the row from the data array
|
||||
let newData = this.data.filter((item) => item !== rowData);
|
||||
// Ensure at least one empty row remains
|
||||
if (newData.length === 0) {
|
||||
newData = [{ key: '', value: '' }];
|
||||
}
|
||||
this.set('data', newData);
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<Form::Field::KeyValue @data={{this.data}} @onRemove={{this.onRemove}}>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput data-test-key-input @value={{R.rowData.key}} {{on 'input' (fn R.updateRow 'key')}} />
|
||||
</R.Field>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput data-test-value-input @value={{R.rowData.value}} {{on 'input' (fn R.updateRow 'value')}} />
|
||||
</R.Field>
|
||||
{{#if R.canDelete}}
|
||||
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
|
||||
{{/if}}
|
||||
</:row>
|
||||
<:footer as |F|>
|
||||
<F.AddRowButton />
|
||||
</:footer>
|
||||
</Form::Field::KeyValue>
|
||||
`);
|
||||
|
||||
assert.strictEqual(getKeyInputs().length, 2, 'Initially has two rows');
|
||||
|
||||
// Remove first row
|
||||
await click(getDeleteButtons()[0]);
|
||||
|
||||
assert.strictEqual(
|
||||
getKeyInputs().length,
|
||||
1,
|
||||
'After removing first row, has one row',
|
||||
);
|
||||
assert.dom('[data-test-key-input]').hasValue('key2');
|
||||
assert.dom('[data-test-value-input]').hasValue('value2');
|
||||
|
||||
// Remove second row - should ensure at least one empty row remains
|
||||
await click(getDeleteButtons()[0]);
|
||||
|
||||
assert.strictEqual(
|
||||
getKeyInputs().length,
|
||||
1,
|
||||
'After removing all rows, still has one empty row',
|
||||
);
|
||||
assert
|
||||
.dom('[data-test-key-input]')
|
||||
.hasValue('', 'The remaining row has empty key');
|
||||
assert
|
||||
.dom('[data-test-value-input]')
|
||||
.hasValue('', 'The remaining row has empty value');
|
||||
});
|
||||
|
||||
test('it handles different field types', async function (assert) {
|
||||
this.set('data', [{ key: 'k', value: 'v', id: 1 }]);
|
||||
|
||||
@@ -282,6 +210,7 @@ module('Integration | Component | form/field/key-value', function (hooks) {
|
||||
{ message: 'Value must be unique' },
|
||||
]);
|
||||
|
||||
// With errors
|
||||
await render(hbs`
|
||||
<Form::Field::KeyValue @data={{this.data}} @errors={{this.errors}}>
|
||||
<:row as |R|>
|
||||
@@ -300,18 +229,15 @@ module('Integration | Component | form/field/key-value', function (hooks) {
|
||||
</:footer>
|
||||
</Form::Field::KeyValue>
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-error-message]').exists().hasText('Key is required');
|
||||
assert
|
||||
.dom('[data-test-error-message]:nth-child(2)')
|
||||
.hasText('Value must be unique');
|
||||
});
|
||||
|
||||
test('it does not show error messages when no errors are provided', async function (assert) {
|
||||
this.set('data', [{ key: '', value: '' }]);
|
||||
|
||||
// Without errors
|
||||
this.set('errors', undefined);
|
||||
await render(hbs`
|
||||
<Form::Field::KeyValue @data={{this.data}}>
|
||||
<Form::Field::KeyValue @data={{this.data}} @errors={{this.errors}}>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput data-test-key-input @value={{R.rowData.key}} />
|
||||
@@ -328,11 +254,10 @@ module('Integration | Component | form/field/key-value', function (hooks) {
|
||||
</:footer>
|
||||
</Form::Field::KeyValue>
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-error-message]').doesNotExist();
|
||||
});
|
||||
|
||||
test('it updates both key and value fields', async function (assert) {
|
||||
test('it updates both key and value fields using custom handlers', async function (assert) {
|
||||
this.set('data', [{ key: '', value: '' }]);
|
||||
this.set('onUpdate', (rowData, property, { target: { value } }) => {
|
||||
rowData[property] = value;
|
||||
@@ -393,4 +318,174 @@ module('Integration | Component | form/field/key-value', function (hooks) {
|
||||
|
||||
assert.strictEqual(getDeleteButtons().length, 3);
|
||||
});
|
||||
|
||||
test('custom handlers take precedence over default handlers', async function (assert) {
|
||||
const model = {
|
||||
tags: [{ key: '', value: '' }],
|
||||
};
|
||||
this.set('model', model);
|
||||
|
||||
this.set('onAdd', () => {
|
||||
model.tags.push({ key: 'custom', value: 'custom' });
|
||||
this.set('model', { ...model, tags: [...model.tags] });
|
||||
});
|
||||
|
||||
this.set('onRemove', (rowData) => {
|
||||
model.tags = model.tags.filter((item) => item !== rowData);
|
||||
this.set('model', { ...model, tags: [...model.tags] });
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<Form::Field::KeyValue
|
||||
@model={{this.model}}
|
||||
@name="tags"
|
||||
@properties={{array 'key' 'value'}}
|
||||
@data={{this.model.tags}}
|
||||
@onAdd={{this.onAdd}}
|
||||
@onRemove={{this.onRemove}}
|
||||
@onUpdate={{this.onUpdate}}>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput data-test-key-input @value={{R.rowData.key}} {{on 'input' (fn R.updateRow 'key')}} />
|
||||
</R.Field>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput data-test-value-input @value={{R.rowData.value}} {{on 'input' (fn R.updateRow 'value')}} />
|
||||
</R.Field>
|
||||
{{#if R.canDelete}}
|
||||
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
|
||||
{{/if}}
|
||||
</:row>
|
||||
</Form::Field::KeyValue>
|
||||
`);
|
||||
|
||||
// Test custom add handler takes precedence
|
||||
await click('[data-test-add-button]');
|
||||
|
||||
assert.strictEqual(model.tags.length, 2);
|
||||
assert.strictEqual(model.tags[1].key, 'custom');
|
||||
assert.strictEqual(model.tags[1].value, 'custom');
|
||||
|
||||
// Test custom update handler takes precedence
|
||||
await fillIn(getKeyInputs()[0], 'test');
|
||||
|
||||
// Test custom remove handler takes precedence
|
||||
await click(getDeleteButtons()[0]);
|
||||
|
||||
assert.strictEqual(model.tags.length, 1);
|
||||
assert.strictEqual(model.tags[0].key, 'custom');
|
||||
});
|
||||
|
||||
test('it adds row using default handler (model/name/properties)', async function (assert) {
|
||||
const model = { tags: [{ key: 'a', value: 'b' }] };
|
||||
this.set('model', model);
|
||||
this.set('properties', ['key', 'value']);
|
||||
|
||||
await render(hbs`
|
||||
<Form::Field::KeyValue
|
||||
@model={{this.model}}
|
||||
@name="tags"
|
||||
@properties={{this.properties}}
|
||||
@data={{this.model.tags}}>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput data-test-key-input @value={{R.rowData.key}} />
|
||||
</R.Field>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput data-test-value-input @value={{R.rowData.value}} />
|
||||
</R.Field>
|
||||
{{#if R.canDelete}}
|
||||
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
|
||||
{{/if}}
|
||||
</:row>
|
||||
<:footer as |F|>
|
||||
<F.AddRowButton />
|
||||
</:footer>
|
||||
</Form::Field::KeyValue>
|
||||
`);
|
||||
|
||||
assert.strictEqual(model.tags.length, 1);
|
||||
|
||||
await click('[data-test-add-button]');
|
||||
|
||||
assert.strictEqual(model.tags.length, 2);
|
||||
|
||||
assert.deepEqual(model.tags[1], { key: '', value: '' });
|
||||
});
|
||||
|
||||
test('it removes row using default handler (model/name)', async function (assert) {
|
||||
const model = {
|
||||
tags: [
|
||||
{ key: 'a', value: 'b' },
|
||||
{ key: 'c', value: 'd' },
|
||||
],
|
||||
};
|
||||
this.set('model', model);
|
||||
this.set('properties', ['key', 'value']);
|
||||
|
||||
await render(hbs`
|
||||
<Form::Field::KeyValue
|
||||
@model={{this.model}}
|
||||
@name="tags"
|
||||
@properties={{this.properties}}
|
||||
@data={{this.model.tags}}>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput data-test-key-input @value={{R.rowData.key}} />
|
||||
</R.Field>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput data-test-value-input @value={{R.rowData.value}} />
|
||||
</R.Field>
|
||||
{{#if R.canDelete}}
|
||||
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
|
||||
{{/if}}
|
||||
</:row>
|
||||
<:footer as |F|>
|
||||
<F.AddRowButton />
|
||||
</:footer>
|
||||
</Form::Field::KeyValue>
|
||||
`);
|
||||
|
||||
assert.strictEqual(model.tags.length, 2);
|
||||
|
||||
await click(getDeleteButtons()[0]);
|
||||
|
||||
assert.strictEqual(model.tags.length, 1);
|
||||
|
||||
assert.deepEqual(model.tags[0], { key: 'c', value: 'd' });
|
||||
});
|
||||
|
||||
test('it updates row using default handler (model/name)', async function (assert) {
|
||||
const model = { tags: [{ key: '', value: '' }] };
|
||||
this.set('model', model);
|
||||
this.set('properties', ['key', 'value']);
|
||||
|
||||
await render(hbs`
|
||||
<Form::Field::KeyValue
|
||||
@model={{this.model}}
|
||||
@name="tags"
|
||||
@properties={{this.properties}}
|
||||
@data={{this.model.tags}}>
|
||||
<:row as |R|>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput data-test-key-input @value={{R.rowData.key}} {{on 'input' (fn R.updateRow 'key')}} />
|
||||
</R.Field>
|
||||
<R.Field as |F|>
|
||||
<F.TextInput data-test-value-input @value={{R.rowData.value}} {{on 'input' (fn R.updateRow 'value')}} />
|
||||
</R.Field>
|
||||
{{#if R.canDelete}}
|
||||
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
|
||||
{{/if}}
|
||||
</:row>
|
||||
<:footer as |F|>
|
||||
<F.AddRowButton />
|
||||
</:footer>
|
||||
</Form::Field::KeyValue>
|
||||
`);
|
||||
|
||||
await fillIn('[data-test-key-input]', 'updated-key');
|
||||
await fillIn('[data-test-value-input]', 'updated-value');
|
||||
|
||||
assert.strictEqual(model.tags[0].key, 'updated-key');
|
||||
assert.strictEqual(model.tags[0].value, 'updated-value');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user