Refactor key value comp (#2180)

* refactor: 💡 keyvalue comp
This commit is contained in:
Dharini Jeeva authored and GitHub committed 2024-06-03 13:38:42 -07:00
1 parent 9b403b3159
commit 6397cd80a6
22 files changed
+531 -405

No files matched your search

@@ -81,7 +81,7 @@ export default class GeneratedCredentialLibraryModel extends BaseModel {
})
credential_type;
@attr('object', {
@attr('object-as-array', {
for: 'vault-generic',
description: 'It indicates the credential mapping overrides.',
})
@@ -13,15 +13,12 @@ export const options = {
key_type: ['ed25519', 'ecdsa', 'rsa'],
credential_types: ['ssh_private_key', 'username_password'],
mapping_overrides: {
username_password: {
username_attribute: 'Username',
password_attribute: 'Password',
},
ssh_private_key: {
private_key_attribute: 'Private Key',
private_key_passphrase_attribute: 'Private Key Passphrase',
username_attribute: 'Username',
},
username_password: ['username_attribute', 'password_attribute'],
ssh_private_key: [
'private_key_attribute',
'private_key_passphrase_attribute',
'username_attribute',
],
},
};
@@ -32,32 +32,9 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
return super.serialize(...arguments);
}
}
serializeAttribute(snapshot, json) {
const value = super.serializeAttribute(...arguments);
const { credential_type, type, credential_mapping_overrides, isNew } =
snapshot.record;
if (
type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC &&
credential_mapping_overrides &&
credential_type &&
!isNew
) {
// API expects to send null to fields if it is undefined or deleted
json.credential_mapping_overrides = Object.keys(
options.mapping_overrides[credential_type],
).reduce((obj, key) => {
if (credential_mapping_overrides[key]) {
obj[key] = credential_mapping_overrides[key];
} else {
obj[key] = null;
}
return obj;
}, {});
return value;
}
}
serializeVaultGeneric() {
serializeVaultGeneric(snapshot) {
const { isNew } = snapshot;
const serialized = super.serialize(...arguments);
if (serialized.attributes) {
@@ -66,7 +43,22 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
if (!serialized.attributes?.http_method?.match(/post/i)) {
serialized.attributes.http_request_body = null;
}
const { credential_type, credential_mapping_overrides } = serialized;
// API expects to send null to fields if it is undefined or deleted
if (credential_mapping_overrides && !isNew) {
serialized.credential_mapping_overrides = options.mapping_overrides[
credential_type
].reduce((obj, key) => {
if (credential_mapping_overrides[key]) {
obj[key] = credential_mapping_overrides[key];
} else {
obj[key] = null;
}
return obj;
}, {});
}
return serialized;
}
return serialized;
}
}
@@ -9,13 +9,13 @@ export default class AccountValueMapArrayTransform extends Transform {
deserialize(serialized) {
const accountValues = serialized || [];
return accountValues.map((accountValueString) => ({
from: accountValueString.split('=')[0],
to: accountValueString.split('=')[1],
key: accountValueString.split('=')[0],
value: accountValueString.split('=')[1],
}));
}
serialize(deserialized) {
const accountValues = deserialized || [];
return accountValues.map(({ from, to }) => `${from}=${to}`);
return accountValues.map(({ key, value }) => `${key}=${value}`);
}
}
@@ -139,7 +139,7 @@ module('Unit | Serializer | application', function (hooks) {
type: TYPE_AUTH_METHOD_OIDC, //has both string and array `for` option
name: 'OIDC Auth Method',
state: 'foo',
account_claim_maps: [{ from: 'foo', to: 'bar' }],
account_claim_maps: [{ key: 'foo', value: 'bar' }],
claims_scopes: [{ value: 'profile' }, { value: 'email' }],
signing_algorithms: [{ value: 'RS256' }, { value: 'RS384' }],
allowed_audiences: [
@@ -45,7 +45,7 @@ module('Unit | Serializer | auth method', function (hooks) {
type: TYPE_AUTH_METHOD_OIDC,
name: 'OIDC Auth Method',
state: 'foo',
account_claim_maps: [{ from: 'foo', to: 'bar' }],
account_claim_maps: [{ key: 'foo', value: 'bar' }],
claims_scopes: [{ value: 'profile' }, { value: 'email' }],
signing_algorithms: [{ value: 'RS256' }, { value: 'RS384' }],
allowed_audiences: [
@@ -96,7 +96,7 @@ module('Unit | Serializer | auth method', function (hooks) {
type: TYPE_AUTH_METHOD_LDAP,
name: 'LDAP Auth Method',
state: 'baz',
account_attribute_maps: [{ from: 'foo', to: 'bar' }],
account_attribute_maps: [{ key: 'foo', value: 'bar' }],
urls: [{ value: 'ldap://ldap.example.com' }],
use_token_groups: false,
start_tls: true,
@@ -160,7 +160,7 @@ module('Unit | Serializer | auth method', function (hooks) {
type: TYPE_AUTH_METHOD_OIDC,
name: 'OIDC Auth Method',
state: 'foo',
account_claim_maps: [{ from: 'foo', to: 'bar' }],
account_claim_maps: [{ key: 'foo', value: 'bar' }],
claims_scopes: [{ value: 'profile' }, { value: 'email' }],
signing_algorithms: [{ value: 'RS256' }, { value: 'RS384' }],
allowed_audiences: [
@@ -210,7 +210,7 @@ module('Unit | Serializer | auth method', function (hooks) {
type: TYPE_AUTH_METHOD_LDAP,
name: 'LDAP Auth Method',
state: 'baz',
account_attribute_maps: [{ from: 'foo', to: 'bar' }],
account_attribute_maps: [{ key: 'foo', value: 'bar' }],
urls: [{ value: 'ldap://ldap.example.com' }],
use_token_groups: false,
start_tls: true,
@@ -410,8 +410,8 @@ module('Unit | Serializer | auth method', function (hooks) {
prompts,
} = record;
assert.deepEqual(account_claim_maps, [
{ from: 'from', to: 'to' },
{ from: 'foo', to: 'bar' },
{ key: 'from', value: 'to' },
{ key: 'foo', value: 'bar' },
]);
assert.deepEqual(claims_scopes, [{ value: 'profile' }, { value: 'email' }]);
assert.deepEqual(allowed_audiences, [
@@ -481,8 +481,8 @@ module('Unit | Serializer | auth method', function (hooks) {
certificates: recordCertificates,
} = record;
assert.deepEqual(account_attribute_maps, [
{ from: 'from', to: 'to' },
{ from: 'foo', to: 'bar' },
{ key: 'from', value: 'to' },
{ key: 'foo', value: 'bar' },
]);
assert.deepEqual(urls, [{ value: ldapUrl }]);
assert.deepEqual(recordCertificates, [
@@ -24,7 +24,9 @@ module('Unit | Serializer | credential library', function (hooks) {
http_method: 'GET',
version: 1,
credential_type: 'ssh_private_key',
credential_mapping_overrides: { username_attribute: 'user' },
credential_mapping_overrides: [
{ key: 'username_attribute', value: 'user' },
],
});
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
@@ -61,7 +63,9 @@ module('Unit | Serializer | credential library', function (hooks) {
http_method: 'GET',
version: 1,
credential_type: 'ssh_private_key',
credential_mapping_overrides: {},
credential_mapping_overrides: [
{ key: 'private_key_attribute', value: 'test' },
],
},
},
});
@@ -75,7 +79,7 @@ module('Unit | Serializer | credential library', function (hooks) {
description: 'Description',
credential_type: 'ssh_private_key',
credential_mapping_overrides: {
private_key_attribute: null,
private_key_attribute: 'test',
private_key_passphrase_attribute: null,
username_attribute: null,
},
@@ -116,7 +120,7 @@ module('Unit | Serializer | credential library', function (hooks) {
name: null,
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
credential_type: null,
credential_mapping_overrides: {},
credential_mapping_overrides: null,
});
assert.deepEqual(serializedVaultSSHCertificateRecord, {
attributes: {
@@ -157,7 +161,7 @@ module('Unit | Serializer | credential library', function (hooks) {
name: null,
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
credential_type: null,
credential_mapping_overrides: {},
credential_mapping_overrides: null,
},
'http_request_body attribute is not expected',
);
@@ -177,7 +181,7 @@ module('Unit | Serializer | credential library', function (hooks) {
name: null,
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
credential_type: null,
credential_mapping_overrides: {},
credential_mapping_overrides: null,
},
'http_request_body attribute is expected',
);
@@ -13,16 +13,16 @@ module('Unit | Transform | account value map array', function (hooks) {
const transform = this.owner.lookup('transform:account-value-map-array');
const deserialized = transform.deserialize(['oid=sub', 'full_name=name']);
assert.deepEqual(deserialized, [
{ from: 'oid', to: 'sub' },
{ from: 'full_name', to: 'name' },
{ key: 'oid', value: 'sub' },
{ key: 'full_name', value: 'name' },
]);
});
test('it serializes an array of account claims', function (assert) {
const transform = this.owner.lookup('transform:account-value-map-array');
const serialized = transform.serialize([
{ from: 'oid', to: 'sub' },
{ from: 'full_name', to: 'name' },
{ key: 'oid', value: 'sub' },
{ key: 'full_name', value: 'name' },
]);
assert.deepEqual(serialized, ['oid=sub', 'full_name=name']);
});
@@ -382,124 +382,45 @@
}}</F.HelperText>
</Hds::Form::TextInput::Field>
<Hds::Form::Fieldset @isOptional={{true}} as |F|>
<F.Legend>{{t
'resources.auth-method.form.account_attribute_maps.label'
}}</F.Legend>
<F.HelperText>{{t
'resources.auth-method.form.account_attribute_maps.help'
}}</F.HelperText>
{{#let (unique-id) (unique-id) as |fromHeaderID toHeaderID|}}
<F.Control>
<Hds::Table class='list-wrapper-field' name='account_attribute_maps'>
<:head as |H|>
<H.Tr>
<H.Th id={{fromHeaderID}}>{{t
'resources.auth-method.titles.from-attr'
}}</H.Th>
<H.Th id={{toHeaderID}}>{{t
'resources.auth-method.titles.to-attr'
}}</H.Th>
<H.Th>{{t 'titles.actions'}}</H.Th>
</H.Tr>
</:head>
<:body as |B|>
{{#each @model.account_attribute_maps as |attrMap i|}}
<B.Tr>
<B.Td>
<Hds::Form::TextInput::Field
@value={{attrMap.from}}
@type='text'
disabled={{form.disabled}}
aria-labelledby={{fromHeaderID}}
{{on 'input' (set-from-event attrMap 'from')}}
/>
</B.Td>
<B.Td>
<Hds::Form::Select::Field
@value={{attrMap.to}}
@width='100%'
disabled={{form.disabled}}
aria-labelledby={{toHeaderID}}
{{on 'change' (set-from-event attrMap 'to')}}
as |F|
>
<F.Options>
{{#each this.toAttributes as |toAttrOption|}}
<option
value={{toAttrOption}}
selected={{eq toAttrOption attrMap.to}}
>{{toAttrOption}}</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Field>
</B.Td>
<B.Td>
<Hds::Button
@text={{t 'actions.remove'}}
@color='critical'
@icon='trash'
@isIconOnly={{true}}
disabled={{form.disabled}}
{{on
'click'
(fn @removeItemByIndex 'account_attribute_maps' i)
}}
/>
</B.Td>
</B.Tr>
{{/each}}
<B.Tr>
<B.Td>
<Hds::Form::TextInput::Field
@value={{this.newFromAttribute}}
@type='text'
@isOptional={{true}}
disabled={{form.disabled}}
aria-labelledby={{fromHeaderID}}
{{on 'input' (set-from-event this 'newFromAttribute')}}
/>
</B.Td>
<B.Td>
<Hds::Form::Select::Field
@value={{this.newToAttribute}}
@width='100%'
disabled={{form.disabled}}
aria-labelledby={{toHeaderID}}
{{on 'change' (set-from-event this 'newToAttribute')}}
as |F|
>
<F.Options>
{{#each this.toAttributes as |toAttrOption|}}
<option
value={{toAttrOption}}
selected={{eq toAttrOption this.newToAttribute}}
>{{toAttrOption}}</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Field>
</B.Td>
<B.Td>
<Hds::Button
@text={{t 'actions.add'}}
@color='secondary'
disabled={{or form.disabled (not this.newFromAttribute)}}
{{on 'click' this.addAttributeMap}}
/>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
</F.Control>
{{/let}}
{{#if @model.errors.account_attribute_maps}}
<F.Error as |E|>
{{#each @model.errors.account_attribute_maps as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Fieldset>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.auth-method.form.account_attribute_maps.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.auth-method.form.account_attribute_maps.help'}}
</F.HelperText>
{{#if @model.errors.account_attribute_maps}}
<F.Error as |E|>
{{#each @model.errors.account_attribute_maps as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</:fieldset>
<:field as |F|>
<F.KeyValue
@name='account_attribute_maps'
@options={{@model.account_attribute_maps}}
@model={{@model}}
@keyLabel={{t 'form.from_claim.label'}}
@valueLabel={{t 'form.to_claim.label'}}
@width='100%'
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.select @selectOptions={{this.toAttributes}} />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
<div class='ldap-section'>
<p class='hds-typography-display-300 hds-font-weight-bold'>{{t
@@ -247,138 +247,45 @@
</Form::Field::ListWrapper>
{{! Account Claim Maps }}
<Rose::Form::Fieldset>
<:title>{{t 'form.account_claim_maps.label'}}</:title>
<:description>{{t 'form.account_claim_maps.help'}}</:description>
<:body>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'form.account_claim_maps.label'}}
</F.Legend>
<F.HelperText>
{{t 'form.account_claim_maps.help'}}
</F.HelperText>
{{#if @model.errors.account_claim_maps}}
<Rose::Form::Errors as |errors|>
<F.Error as |E|>
{{#each @model.errors.account_claim_maps as |error|}}
<errors.message>{{error.message}}</errors.message>
<E.Message>{{error.message}}</E.Message>
{{/each}}
</Rose::Form::Errors>
</F.Error>
{{/if}}
<Rose::Table @style='condensed' as |table|>
<table.header as |header|>
<header.row as |row|>
<row.headerCell>{{t 'form.from_claim.label'}}</row.headerCell>
<row.headerCell>{{t 'form.to_claim.label'}}</row.headerCell>
<row.headerCell>{{t 'titles.actions'}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
{{#each @model.account_claim_maps as |claimMap i|}}
<body.row as |row|>
<row.cell>
<form.input
@type='text'
@name='from_claim'
@value={{claimMap.from}}
@error={{@model.errors.account_claim_maps}}
@contextual={{true}}
as |field|
>
<field.label>{{t 'form.from_claim.label'}}</field.label>
<field.field />
</form.input>
</row.cell>
<row.cell>
<Hds::Form::Select::Field
@value={{claimMap.to}}
@isInvalid={{@model.errors.account_claim_maps}}
disabled={{form.disabled}}
{{on 'change' (set-from-event claimMap 'to')}}
as |F|
>
<F.Label class='sr-only'>{{t 'form.to_claim.label'}}</F.Label>
<F.Options>
{{#each this.toClaims as |toClaimOption|}}
<option
value={{toClaimOption}}
selected={{eq toClaimOption claimMap.to}}
>{{toClaimOption}}</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Field>
</row.cell>
<row.cell @shrink={{true}}>
<Rose::Button
@style='warning'
@iconOnly='flight-icons/svg/trash-16'
title={{t 'actions.remove'}}
@disabled={{if
@model.isNew
false
(if form.isEditable false true)
}}
{{on 'click' (fn @removeItemByIndex 'account_claim_maps' i)}}
>
{{t 'actions.remove'}}
</Rose::Button>
</row.cell>
</body.row>
{{/each}}
<body.row as |row|>
<row.cell>
<form.input
@type='text'
@name='from_claim'
@value={{this.newFromClaim}}
@label={{t 'resources.auth-method.titles.new-from-claim'}}
/>
</row.cell>
<row.cell>
<Hds::Form::Select::Field
@value={{this.newToClaim}}
disabled={{form.disabled}}
{{on 'change' (set-from-event this 'newToClaim')}}
as |F|
>
<F.Label class='sr-only'>{{t
'resources.auth-method.titles.new-to-claim'
}}</F.Label>
<F.Options>
{{#each this.toClaims as |toClaimOption|}}
<option
value={{toClaimOption}}
selected={{eq toClaimOption this.newToClaim}}
>{{toClaimOption}}</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Field>
</row.cell>
<row.cell @shrink={{true}}>
<Rose::Button
@style='secondary'
title={{t 'actions.add'}}
@disabled={{if
@model.isNew
false
(if form.isEditable false true)
}}
{{on
'click'
(fn
@addAccountMapItem
'account_claim_maps'
this.newFromClaim
this.newToClaim
)
}}
>
{{t 'actions.add'}}
</Rose::Button>
</row.cell>
</body.row>
</table.body>
</Rose::Table>
</:body>
</Rose::Form::Fieldset>
</:fieldset>
<:field as |F|>
<F.KeyValue
@name='account_claim_maps'
@options={{@model.account_claim_maps}}
@model={{@model}}
@keyLabel={{t 'form.from_claim.label'}}
@valueLabel={{t 'form.to_claim.label'}}
@width='100%'
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.select @selectOptions={{this.toClaims}} />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
{{! Certificates }}
<Form::Field::ListWrapper
@@ -169,14 +169,23 @@
</:fieldset>
<:field as |F|>
<F.SelectText
<F.KeyValue
@name='credential_mapping_overrides'
@selectOptions={{this.mappingOverrides}}
@model={{@model}}
@disabled={{form.disabled}}
@removeDuplicates={{true}}
@width='400px'
/>
@width='100%'
@showNewRow={{this.showNewRow}}
>
<:newKey as |N|>
<N.select @selectOptions={{this.selectOptions}} />
</:newKey>
<:key as |K|>
<K.select @selectOptions={{this.mappingOverrides}} />
</:key>
<:value as |V|>
<V.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
{{/if}}
@@ -17,6 +17,7 @@ export default class FormCredentialLibraryVaultGenericComponent extends Componen
httpMethodOptions = options.http_method;
credentialTypes = options.credential_types;
/**
*
* @type {Array.<string>}
@@ -35,12 +36,46 @@ export default class FormCredentialLibraryVaultGenericComponent extends Componen
return options.mapping_overrides[this.args.model.credential_type];
}
get allowedEntries() {
return this.mappingOverrides.length;
}
/**
* Clear the previously selected key value pair when toggling between credential types on a new form
*/
@action
selectCredentialType({ target: { value } }) {
this.args.model.credential_mapping_overrides = {};
this.args.model.credential_mapping_overrides = [];
this.args.model.credential_type = value;
}
/**
* Prevents users from selecting duplicate keys from the select list if the arg is set to true
* @type {array}
*/
get selectOptions() {
const previouslySelectedKeys =
this.args.model.credential_mapping_overrides || [];
if (previouslySelectedKeys.length) {
return this.mappingOverrides.filter((key) =>
previouslySelectedKeys.every((obj) => obj.key !== key),
);
} else {
return this.mappingOverrides;
}
}
/**
* Determines if we need to show an empty row to the users to enter more key/value pairs based on removeDuplicates arg,
* by default it is true
* @type {object}
*/
@action
showNewRow() {
return (
this.args.model.credential_mapping_overrides?.length !==
this.allowedEntries
);
}
}
@@ -266,7 +266,14 @@
@options={{@model.critical_options}}
@model={{@model}}
@disabled={{form.disabled}}
/>
>
<:key as |K|>
<K.text />
</:key>
<:value as |K|>
<K.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
@@ -297,7 +304,14 @@
@name='extensions'
@options={{@model.extensions}}
@model={{@model}}
/>
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
@@ -25,9 +25,6 @@
'form/field/list-wrapper/textarea' disabled=@disabled
)
Select=(component 'form/field/list-wrapper/select' disabled=@disabled)
SelectText=(component
'form/field/list-wrapper/select-text' disabled=@disabled
)
)
to='field'
}}
@@ -2,16 +2,26 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
{{#let (unique-id) (unique-id) as |keyHeaderID valueHeaderID|}}
{{#let (unique-id) (unique-id) as |keyHeaderID valueHeaderID|}}
<Hds::Table class='list-wrapper-field' name={{@name}}>
<:head as |H|>
<H.Tr>
<H.Th id={{keyHeaderID}}>{{t 'form.key.label'}}</H.Th>
<H.Th id={{valueHeaderID}}>{{t 'form.value.label'}}</H.Th>
<H.Th id={{keyHeaderID}}>
{{#if @keyLabel}}
{{@keyLabel}}
{{else}}
{{t 'form.key.label'}}
{{/if}}</H.Th>
<H.Th id={{valueHeaderID}}>
{{#if @valueLabel}}
{{@valueLabel}}
{{else}}
{{t 'form.value.label'}}
{{/if}}
</H.Th>
<H.Th>
{{t 'titles.actions'}}
</H.Th>
</H.Tr>
</:head>
@@ -19,23 +29,49 @@
{{#each this.options as |option index|}}
<B.Tr>
<B.Td>
<Hds::Form::TextInput::Field
@value={{option.key}}
@type='text'
disabled={{@disabled}}
aria-labelledby={{keyHeaderID}}
{{on 'input' (set-from-event option 'key')}}
/>
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=option.key
ariaLabel=keyHeaderID
setContext=(set-from-event option 'key')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=option.key
ariaLabel=keyHeaderID
setContext=(set-from-event option 'key')
width=@width
)
)
to='key'
}}
</B.Td>
<B.Td>
<Hds::Form::TextInput::Field
@value={{option.value}}
@type='text'
disabled={{@disabled}}
aria-labelledby={{valueHeaderID}}
{{on 'input' (set-from-event option 'value')}}
/>
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=option.value
ariaLabel=valueHeaderID
setContext=(set-from-event option 'value')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=option.value
ariaLabel=valueHeaderID
width=@width
setContext=(set-from-event option 'value')
)
)
to='value'
}}
</B.Td>
<B.Td>
@@ -53,40 +89,114 @@
</B.Td>
</B.Tr>
{{/each}}
{{#if this.showNewRow}}
<B.Tr>
<B.Td>
{{#if (has-block 'newKey')}}
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=this.newOptionKey
ariaLabel=keyHeaderID
setContext=(set-from-event this 'newOptionKey')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=this.newOptionKey
ariaLabel=keyHeaderID
width=@width
setContext=(set-from-event this 'newOptionKey')
)
)
to='newKey'
}}
<B.Tr>
<B.Td>
<Hds::Form::TextInput::Field
@value={{this.newOptionKey}}
@type='text'
disabled={{@disabled}}
aria-labelledby={{keyHeaderID}}
{{on 'input' (set-from-event this 'newOptionKey')}}
/>
</B.Td>
{{else}}
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=this.newOptionKey
ariaLabel=keyHeaderID
setContext=(set-from-event this 'newOptionKey')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=this.newOptionKey
ariaLabel=keyHeaderID
width=@width
setContext=(set-from-event this 'newOptionKey')
)
)
to='key'
}}
{{/if}}
</B.Td>
<B.Td>
<Hds::Form::TextInput::Field
@value={{this.newOptionValue}}
@type='text'
disabled={{@disabled}}
aria-labelledby={{valueHeaderID}}
{{on 'input' (set-from-event this 'newOptionValue')}}
/>
</B.Td>
<B.Td>
{{#if (has-block 'newValue')}}
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=this.newOptionValue
ariaLabel=valueHeaderID
setContext=(set-from-event this 'newOptionValue')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=this.newOptionValue
ariaLabel=valueHeaderID
width=@width
setContext=(set-from-event this 'newOptionValue')
)
)
to='newValue'
}}
<B.Td>
<Hds::Button
@text={{t 'actions.add'}}
@color='secondary'
type='button'
disabled={{or @disabled (not this.newOptionKey)}}
{{on 'click' this.addOption}}
/>
</B.Td>
</B.Tr>
{{else}}
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=this.newOptionValue
ariaLabel=valueHeaderID
setContext=(set-from-event this 'newOptionValue')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=this.newOptionValue
ariaLabel=valueHeaderID
width=@width
setContext=(set-from-event this 'newOptionValue')
)
)
to='value'
}}
{{/if}}
</B.Td>
<B.Td>
<Hds::Button
@text={{t 'actions.add'}}
@color='secondary'
type='button'
disabled={{or @disabled (not this.newOptionKey)}}
{{on 'click' this.addOption}}
/>
</B.Td>
</B.Tr>
{{/if}}
</:body>
</Hds::Table>
{{/let}}
@@ -29,6 +29,20 @@ export default class MappingListComponent extends Component {
get options() {
return this.args?.options || this.args?.model?.[this.args.name];
}
/**
* Determines if we need to show an empty row to the users to enter more key/value pairs based on removeDuplicates arg,
* by default it is true
* @type {object}
*/
get showNewRow() {
if (this.args.showNewRow) {
return this.args.showNewRow();
} else {
return true;
}
}
// =actions
/**
@@ -49,6 +63,7 @@ export default class MappingListComponent extends Component {
} else {
const field = this.args.name;
const existingArray = this.args.model[field] ?? [];
const newArray = [
...existingArray,
{ key: this.newOptionKey, value: this.newOptionValue },
@@ -0,0 +1,19 @@
<Hds::Form::Select::Base
@value={{@value}}
@width={{@width}}
disabled={{@disabled}}
aria-labelledby={{@ariaLabel}}
{{on 'change' @setContext}}
as |F|
>
<F.Options>
<option hidden selected value=''>
{{t 'titles.choose-an-option'}}
</option>
{{#each @selectOptions as |selectOption|}}
<option value={{selectOption}} selected={{eq selectOption @value}}>
{{selectOption}}
</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Base>
@@ -0,0 +1,6 @@
<Hds::Form::TextInput::Field
@value={{@value}}
disabled={{@disabled}}
aria-labelledby={{@ariaLabel}}
{{on 'input' @setContext}}
/>
@@ -95,7 +95,14 @@
@disabled={{form.disabled}}
@addOption={{this.addWorkerTag}}
@removeOptionByIndex={{this.removeWorkerTagByIndex}}
/>
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
@@ -128,10 +128,16 @@ module('Acceptance | auth-methods | create', function (hooks) {
await fillIn(CLAIMS_SCOPES_INPUT_SELECTOR, 'claims_scopes');
await click(CLAIMS_SCOPES_BTN_SELECTOR, 'claims_scopes');
await fillIn('[name="from_claim"]', 'from_claim');
await select('form fieldset:nth-of-type(4) select', 'email');
await fillIn(
'[name="account_claim_maps"] tbody td:nth-of-type(1) input',
'from_claim',
);
await select(
'[name="account_claim_maps"] tbody td:nth-of-type(2) select',
'email',
);
await click('form fieldset:nth-of-type(4) [title="Add"]');
await click('[name="account_claim_maps"] button');
await fillIn(IDP_CERTS_INPUT_SELECTOR, 'certificates');
await click(IDP_CERTS_BTN_SELECTOR);
@@ -164,14 +164,23 @@ module('Acceptance | auth-methods | update', function (hooks) {
// Remove all claim maps
const claimMaps = await Promise.all(
findAll('form fieldset:nth-of-type(4) [title="Remove"]'),
findAll('[name="account_claim_maps"] [data-test-remove-button]'),
);
for (const element of claimMaps) {
await click(element);
}
await fillIn('[name="from_claim"]', 'from_claim');
await select('form fieldset:nth-of-type(4) select', 'email');
await click('form fieldset:nth-of-type(4) [title="Add"]');
await fillIn(
'[name="account_claim_maps"] tbody td:nth-of-type(1) input',
'from_claim',
);
await select(
'[name="account_claim_maps"] tbody td:nth-of-type(2) select',
'email',
);
await click('[name="account_claim_maps"] button');
// Remove all certificates
const certificatesList = findAll(IDP_CERTS_REMOVE_BTN_SELECTOR);
@@ -179,15 +188,15 @@ module('Acceptance | auth-methods | update', function (hooks) {
for (const element of certificatesList) {
await click(element);
}
await fillIn(IDP_CERTS_INPUT_SELECTOR, 'certificates');
await click(IDP_CERTS_BTN_SELECTOR);
await fillIn('[name="max_age"]', '5');
await fillIn('[name="api_url_prefix"]', 'api_url_prefix');
await click(TOGGLE_SELECTOR);
await click('form [type="submit"]:not(:disabled)');
await click('.rose-form-actions [type="submit"]');
const authMethod = this.server.schema.authMethods.findBy({ name });
assert.strictEqual(authMethod.name, name);
assert.strictEqual(authMethod.description, 'description');
assert.strictEqual(authMethod.attributes.issuer, 'issuer');
@@ -5,7 +5,7 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { click, render, select } from '@ember/test-helpers';
import { click, findAll, render, select } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupIntl } from 'ember-intl/test-support';
@@ -55,7 +55,14 @@ module('Integration | Component | list-wrapper', function (hooks) {
await render(hbs`
<Form::Field::ListWrapper>
<:field as |F|>
<F.KeyValue @options={{this.options}}></F.KeyValue>
<F.KeyValue @options={{this.options}}>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
`);
@@ -165,42 +172,56 @@ module('Integration | Component | list-wrapper', function (hooks) {
});
test('it renders multiple options with select and text input as key value pair', async function (assert) {
this.options = {
username_attribute: 'user',
password_attribute: 'pass',
};
this.options = [
{
key: 'username_attribute',
value: 'user',
},
{
key: 'password_attribute',
value: 'pass',
},
];
this.selectOptions = ['username_attribute', 'password_attribute'];
this.selectOptions = {
username_attribute: 'User Key',
password_attribute: 'Pass Key',
};
await render(hbs`
<Form::Field::ListWrapper>
<:field as |F|>
<F.SelectText @name="credential_mapping_overrides" @options={{this.options}} @removeDuplicates='true' @selectOptions={{this.selectOptions}}></F.SelectText>
<F.KeyValue @name="credential_mapping_overrides">
<:key as |K|>
<K.select @selectOptions={{this.selectOptions}}/>
</:key>
<:value as |V|>
<V.text/>
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
`);
assert.dom('tbody tr').exists({ count: 2 });
assert
.dom('.list-wrapper-field tbody tr:last-child input')
.hasValue('pass');
assert
.dom('.list-wrapper-field tbody tr:last-child select')
.hasValue('password_attribute');
assert.dom('tbody tr').exists({ count: 1 });
});
test('it does not render new rows when the select option limit is reached by passing in @removeDuplicates', async function (assert) {
this.options = { username_attribute: 'user', password_attribute: 'pass' };
this.selectOptions = {
username_attribute: 'User Key',
password_attribute: 'Pass Key',
};
test('it does not render new rows when the select option limit is reached when showNewRow is passed', async function (assert) {
this.options = [
{ key: 'username_attribute', value: 'user' },
{ key: 'password_attribute', value: 'pass' },
];
this.selectOptions = ['username_attribute', 'password_attribute'];
this.showNewRow = () => false;
await render(hbs`
<Form::Field::ListWrapper>
<:field as |F|>
<F.SelectText @name="credential_mapping_overrides" @options={{this.options}} @selectOptions={{this.selectOptions}} @removeDuplicates='true'></F.SelectText>
<F.KeyValue @name="credential_mapping_overrides" @options={{this.options}} @showNewRow={{this.showNewRow}}>
<:key as |K|>
<K.select @selectOptions={{this.selectOptions}}/>
</:key>
<:value as |V|>
<V.text/>
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
`);
@@ -208,20 +229,77 @@ module('Integration | Component | list-wrapper', function (hooks) {
assert.dom('tbody tr').exists({ count: 2 });
});
test('it does render unlimited new rows when @removeDuplicates is not passed', async function (assert) {
this.options = { username_attribute: 'user', password_attribute: 'pass' };
this.selectOptions = {
username_attribute: 'User Key',
password_attribute: 'Pass Key',
};
test('it does render unlimited new rows when @showNewRow is not passed', async function (assert) {
this.options = [
{
key: 'username_attribute',
value: 'user',
},
{
key: 'password_attribute',
value: 'pass',
},
];
this.selectOptions = ['username_attribute', 'password_attribute'];
await render(hbs`
<Form::Field::ListWrapper>
<:field as |F|>
<F.SelectText @name="credential_mapping_overrides" @options={{this.options}} @selectOptions={{this.selectOptions}}></F.SelectText>
<F.KeyValue @name="credential_mapping_overrides" @options={{this.options}} >
<:key as |K|>
<K.text/>
</:key>
<:value as |V|>
<V.text/>
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
`);
assert.dom('tbody tr').exists({ count: 3 });
});
test('it renders `newKey` and `newValue` named blocks when passed instead of key and value blocks', async function (assert) {
this.options = ['option1', 'option2', 'option3'];
this.selectOptionsNewValue = [
'username_attribute_new_val',
'password_attribute_new_val',
];
this.selectOptionsNewKey = [
'username_attribute_new_key',
'password_attribute_new_key',
];
await render(hbs`
<Form::Field::ListWrapper>
<:field as |F|>
<F.KeyValue @name="credential_mapping_overrides">
<:key as |K|>
<K.select @selectOptions = {{this.options}}/>
</:key>
<:newKey as |N|>
<N.select @selectOptions= {{this.selectOptionsNewKey}}/>
</:newKey>
<:newValue as |S|>
<S.select @selectOptions= {{this.selectOptionsNewValue}}/>
</:newValue>
<:value as |V|>
<V.select @selectOptions = {{this.options}}/>
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
`);
assert.strictEqual(
findAll('.list-wrapper-field tbody td:nth-of-type(1) select option')
.length,
3,
);
assert.strictEqual(
findAll('.list-wrapper-field tbody td:nth-of-type(2) select option')
.length,
3,
);
});
});