UI/fix kmip role form (#13585)

* Fix info-table-row not rendering if alwaysRender=false and only block content present

* use defaultFields for form and nonOperationFields for adapter

* WIP: Move info table row template to addon component dir

* Refactor InfoTableRow to glimmer component

* Add changelog

* passthrough attributes, change @data-test-x to data-test-x on InfoTableRow invocations
This commit is contained in:
Chelsea Shaw 2022-01-07 09:16:40 -06:00 committed by GitHub
parent a09a20e758
commit 5301934368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 252 additions and 155 deletions

3
changelog/13585.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: Fixes issue saving KMIP role correctly
```

View File

@ -38,7 +38,7 @@ export default BaseAdapter.extend({
serialize(snapshot) {
// the endpoint here won't allow sending `operation_all` and `operation_none` at the same time or with
// other values, so we manually check for them and send an abbreviated object
// other operation_ values, so we manually check for them and send an abbreviated object
let json = snapshot.serialize();
let keys = snapshot.record.nonOperationFields.map(decamelize);
let nonOperationFields = getProperties(json, keys);

View File

@ -17,10 +17,16 @@ export const COMPUTEDS = {
return ['tlsClientKeyBits', 'tlsClientKeyType', 'tlsClientTtl'];
}),
nonOperationFields: computed('newFields', 'operationFields', 'tlsFields', function () {
// For rendering on the create/edit pages
defaultFields: computed('newFields', 'operationFields', 'tlsFields', function () {
let excludeFields = ['role'].concat(this.operationFields, this.tlsFields);
return this.newFields.slice().removeObjects(excludeFields);
}),
// For adapter/serializer
nonOperationFields: computed('newFields', 'operationFields', function () {
return this.newFields.slice().removeObjects(this.operationFields);
}),
};
const ModelExport = Model.extend(COMPUTEDS, {
@ -31,10 +37,10 @@ const ModelExport = Model.extend(COMPUTEDS, {
getHelpUrl(path) {
return `/v1/${path}/scope/example/role/example?help=1`;
},
fieldGroups: computed('fields', 'nonOperationFields.length', 'tlsFields', function () {
fieldGroups: computed('fields', 'defaultFields.length', 'tlsFields', function () {
const groups = [{ TLS: this.tlsFields }];
if (this.nonOperationFields.length) {
groups.unshift({ default: this.nonOperationFields });
if (this.defaultFields.length) {
groups.unshift({ default: this.defaultFields });
}
let ret = fieldToAttrs(this, groups);
return ret;
@ -61,7 +67,7 @@ const ModelExport = Model.extend(COMPUTEDS, {
];
if (others.length) {
groups.push({
'': others,
Other: others,
});
}
return fieldToAttrs(this, groups);
@ -69,8 +75,8 @@ const ModelExport = Model.extend(COMPUTEDS, {
tlsFormFields: computed('tlsFields', function () {
return expandAttributeMeta(this, this.tlsFields);
}),
fields: computed('nonOperationFields', function () {
return expandAttributeMeta(this, this.nonOperationFields);
fields: computed('defaultFields', function () {
return expandAttributeMeta(this, this.defaultFields);
}),
});

View File

@ -56,12 +56,19 @@
attr.options.masked
)
}}
<InfoTableRow
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get this.model attr.name}}
>
<MaskedInput @value={{get this.model attr.name}} @name={{attr.name}} @displayOnly={{true}} @allowCopy={{true}} />
</InfoTableRow>
{{#if (get this.model attr.name)}}
<InfoTableRow
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get this.model attr.name}}
>
<MaskedInput
@value={{get this.model attr.name}}
@name={{attr.name}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
{{/if}}
{{else if (and (get this.model attr.name) (or (eq attr.name "issueDate") (eq attr.name "expiryDate")))}}
<InfoTableRow
data-test-table-row

View File

@ -1,4 +1,4 @@
<InfoTableRow @label="Name" @value={{@model.name}} @data-test-alias-name={{true}} />
<InfoTableRow @label="Name" @value={{@model.name}} data-test-alias-name={{true}} />
<InfoTableRow @label="ID" @value={{@model.id}} />
<InfoTableRow @label={{if (eq @model.identityType "entity-alias") "Entity ID" "Group ID"}} @value={{this.model.canonicalId}}>
<LinkTo

View File

@ -13,7 +13,7 @@
{{/if}}
</AlertBanner>
{{/if}}
<InfoTableRow @label="Name" @value={{@model.name}} @data-test-identity-item-name={{true}} />
<InfoTableRow @label="Name" @value={{@model.name}} data-test-identity-item-name={{true}} />
<InfoTableRow @label="Type" @value={{@model.type}} />
<InfoTableRow @label="ID" @value={{@model.id}} />
<InfoTableRow @label="Merged Ids" @value={{@model.mergedEntityIds}}>

View File

@ -7,13 +7,13 @@
<section class="box is-sideless is-marginless is-shadowless is-fullwidth">
<span class="title is-5">Details</span>
<div class="field box is-fullwidth is-shadowless is-paddingless is-marginless">
<InfoTableRow @label="License ID" @value={{@licenseId}} @data-test-detail-row={{true}} />
<InfoTableRow @label="Valid from" @value={{@startTime}} @data-test-detail-row={{true}}>
<InfoTableRow @label="License ID" @value={{@licenseId}} data-test-detail-row />
<InfoTableRow @label="Valid from" @value={{@startTime}} data-test-detail-row>
{{date-format @startTime "MMM dd, yyyy hh:mm:ss a"}}
to
{{date-format @expirationTime "MMM dd, yyyy hh:mm:ss a"}}
</InfoTableRow>
<InfoTableRow @label="License state" @value={{if @autoloaded "Autoloaded" "Stored"}} @data-test-detail-row={{true}}>
<InfoTableRow @label="License state" @value={{if @autoloaded "Autoloaded" "Stored"}} data-test-detail-row>
{{#if @autoloaded}}
Autoloaded
{{else}}
@ -32,11 +32,7 @@
<span class="title is-5">Features</span>
<div class="field box is-fullwidth is-shadowless is-paddingless is-marginless">
{{#each this.featuresInfo as |info|}}
<InfoTableRow
@label={{info.name}}
@value={{if info.active "Active" "Not Active"}}
@data-test-feature-row="data-test-feature-row"
>
<InfoTableRow @label={{info.name}} @value={{if info.active "Active" "Not Active"}} data-test-feature-row>
{{#if info.active}}
<Icon @name="check-circle" class="icon-true" />
<span data-test-feature-status>

View File

@ -8,12 +8,12 @@
{{#if (or @creation_time @creation_ttl)}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
<InfoTableRow @label="Creation path" @value={{@creation_path}} @data-test-tools="token-lookup-row" />
<InfoTableRow @label="Creation time" @value={{@creation_time}} @data-test-tools="token-lookup-row" />
<InfoTableRow @label="Creation TTL" @value={{@creation_ttl}} @data-test-tools="token-lookup-row" />
<InfoTableRow @label="Creation path" @value={{@creation_path}} data-test-tools="token-lookup-row" />
<InfoTableRow @label="Creation time" @value={{@creation_time}} data-test-tools="token-lookup-row" />
<InfoTableRow @label="Creation TTL" @value={{@creation_ttl}} data-test-tools="token-lookup-row" />
{{#if @expirationDate}}
<InfoTableRow @label="Expiration date" @value={{@expirationDate}} @data-test-tools="token-lookup-row" />
<InfoTableRow @label="Expires in" @value={{date-from-now @expirationDate}} @data-test-tools="token-lookup-row" />
<InfoTableRow @label="Expiration date" @value={{@expirationDate}} data-test-tools="token-lookup-row" />
<InfoTableRow @label="Expires in" @value={{date-from-now @expirationDate}} data-test-tools="token-lookup-row" />
{{/if}}
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">

View File

@ -0,0 +1,90 @@
{{#if (or (has-block) this.isVisible)}}
<div class="info-table-row" data-test-component="info-table-row" ...attributes>
<div
class="column is-one-quarter {{if this.hasLabelOverflow "label-overflow"}}"
data-test-label-div
{{did-insert this.calculateLabelOverflow}}
>
{{#if @label}}
{{#if this.hasLabelOverflow}}
<ToolTip @verticalPosition="below" @horizontalPosition="left" as |T|>
<T.Trigger @tabindex={{false}}>
<span class="is-label has-text-grey-dark" data-test-row-label={{@label}}>{{@label}}</span>
</T.Trigger>
<T.Content class="tool-tip">
<div class="box fit-content" data-test-label-tooltip>
{{@label}}
</div>
</T.Content>
</ToolTip>
{{else}}
<span class="is-label has-text-grey-dark" data-test-row-label={{@label}}>{{@label}}</span>
{{/if}}
{{#if @helperText}}
<div>
<span class="is-label helper-text has-text-grey">{{@helperText}}</span>
</div>
{{/if}}
{{else}}
<Icon @name="minus" />
{{/if}}
</div>
<div class="column is-flex foobar" data-test-value-div={{@label}}>
{{#if (has-block)}}
{{yield}}
{{else if this.valueIsBoolean}}
{{#if @value}}
<Icon class="icon-true" @name="check-circle" data-test-boolean-true />
Yes
{{else}}
<Icon @name="x-square" class="icon-false" data-test-boolean-false />
No
{{/if}}
{{! alwaysRender is still true }}
{{else if this.valueIsEmpty}}
{{#if @defaultShown}}
<span data-test-row-value={{@label}}>{{@defaultShown}}</span>
{{else}}
<Icon @name="minus" />
{{/if}}
{{else}}
{{#if (eq @type "array")}}
<InfoTableItemArray
@backend={{@backend}}
@displayArray={{@value}}
@isLink={{@isLink}}
@label={{@label}}
@modelType={{@modelType}}
@queryParam={{@queryParam}}
@viewAll={{@viewAll}}
@wildcardLabel={{@wildcardLabel}}
/>
{{else}}
{{#if @tooltipText}}
<ToolTip @verticalPosition="above" @horizontalPosition="left" as |T|>
<T.Trigger @tabindex={{false}}>
<span class="is-word-break has-text-black" data-test-row-value={{this.label}}>{{this.value}}</span>
</T.Trigger>
<T.Content class="tool-tip">
<CopyButton
@clipboardText={{@tooltipText}}
@success={{action (set-flash-message "Data copied!")}}
@tagName="div"
@disabled={{not @isTooltipCopyable}}
class={{if @isTooltipCopyable "has-pointer"}}
data-test-tooltip-copy
>
<div class="box">
{{@tooltipText}}
</div>
</CopyButton>
</T.Content>
</ToolTip>
{{else}}
<span class="is-word-break has-text-black" data-test-row-value={{@label}}>{{@value}}</span>
{{/if}}
{{/if}}
{{/if}}
</div>
</div>
{{/if}}

View File

@ -1,8 +1,8 @@
import { typeOf } from '@ember/utils';
import { computed } from '@ember/object';
import { or } from '@ember/object/computed';
import Component from '@ember/component';
import layout from '../templates/components/info-table-row';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
// import layout from '../templates/components/info-table-row';
/**
* @module InfoTableRow
@ -14,42 +14,35 @@ import layout from '../templates/components/info-table-row';
* <InfoTableRow @value={{5}} @label="TTL" @helperText="Some description"/>
* ```
*
* @param value=null {any} - The the data to be displayed - by default the content of the component will only show if there is a value. Also note that special handling is given to boolean values - they will render `Yes` for true and `No` for false.
* @param label=null {string} - The display name for the value.
* @param helperText=null {string} - Text to describe the value displayed beneath the label.
* @param alwaysRender=false {Boolean} - Indicates if the component content should be always be rendered. When false, the value of `value` will be used to determine if the component should render.
* @param value=null {any} - The the data to be displayed - by default the content of the component will only show if there is a value. Also note that special handling is given to boolean values - they will render `Yes` for true and `No` for false. Overridden by block if exists
* @param [alwaysRender=false] {Boolean} - Indicates if the component content should be always be rendered. When false, the value of `value` will be used to determine if the component should render.
* @param [defaultShown] {String} - Text that renders as value if alwaysRender=true. Eg. "Vault default"
* @param [tooltipText] {String} - Text if a tooltip should display over the value.
* @param [isTooltipCopyable] {Boolean} - Allows tooltip click to copy
* @param [type=array] {string} - The type of value being passed in. This is used for when you want to trim an array. For example, if you have an array value that can equal length 15+ this will trim to show 5 and count how many more are there
* @param [isLink=true] {Boolean} - Passed through to InfoTableItemArray. Indicates if the item should contain a link-to component. Only setup for arrays, but this could be changed if needed.
* @param [modelType=null] {string} - Passed through to InfoTableItemArray. Tells what model you want data for the allOptions to be returned from. Used in conjunction with the the isLink.
* @param [queryParam] {String} - Passed through to InfoTableItemArray. If you want to specific a tab for the View All XX to display to. Ex: role
* @param [backend] {String} - Passed through to InfoTableItemArray. To specify secrets backend to point link to Ex: transformation
* @param [queryParam] {String} - Passed through to InfoTableItemArray. If you want to specific a tab for the View All XX to display to. Ex= role
* @param [backend] {String} - Passed through to InfoTableItemArray. To specify secrets backend to point link to Ex= transformation
* @param [viewAll] {String} - Passed through to InfoTableItemArray. Specify the word at the end of the link View all.
* @param [tooltipText] {String} - Text if a tooltip should display over the value.
* @param [isTooltipCopyable] {Boolean} - Allows tooltip click to copy
* @param [defaultShown] {String} - Text that renders as value if alwaysRender=true. Eg. "Vault default"
*/
export default Component.extend({
layout,
'data-test-component': 'info-table-row',
classNames: ['info-table-row'],
isVisible: or('alwaysRender', 'value'),
export default class InfoTableRowComponent extends Component {
@tracked
hasLabelOverflow = false; // is calculated and set in didInsertElement
alwaysRender: false,
label: null,
helperText: null,
value: null,
tooltipText: '',
isTooltipCopyable: false,
defaultShown: '',
hasLabelOverflow: false, // is calculated and set in didInsertElement
get isVisible() {
return this.args.alwaysRender || !this.valueIsEmpty;
}
valueIsBoolean: computed('value', function () {
return typeOf(this.value) === 'boolean';
}),
get valueIsBoolean() {
return typeOf(this.args.value) === 'boolean';
}
valueIsEmpty: computed('value', function () {
let { value } = this;
get valueIsEmpty() {
let { value } = this.args;
if (typeOf(value) === 'array' && value.length === 0) {
return true;
}
@ -63,17 +56,16 @@ export default Component.extend({
default:
return false;
}
}),
}
didInsertElement() {
this._super(...arguments);
const labelDiv = this.element.querySelector('div');
const labelText = this.element.querySelector('.is-label');
@action
calculateLabelOverflow(el) {
const labelDiv = el;
const labelText = el.querySelector('.is-label');
if (labelDiv && labelText) {
if (labelText.offsetWidth > labelDiv.offsetWidth) {
labelDiv.classList.add('label-overflow');
this.set('hasLabelOverflow', true);
this.hasLabelOverflow = true;
}
}
},
});
}
}

View File

@ -1,82 +0,0 @@
{{#if (or this.alwaysRender this.value)}}
<div class="column is-one-quarter" data-test-label-div>
{{#if this.label}}
{{#if this.hasLabelOverflow}}
<ToolTip @verticalPosition="below" @horizontalPosition="left" as |T|>
<T.Trigger @tabindex={{false}}>
<span class="is-label has-text-grey-dark" data-test-row-label={{this.label}}>{{this.label}}</span>
</T.Trigger>
<T.Content class="tool-tip">
<div class="box fit-content">
{{this.label}}
</div>
</T.Content>
</ToolTip>
{{else}}
<span class="is-label has-text-grey-dark" data-test-row-label={{this.label}}>{{this.label}}</span>
{{/if}}
{{#if this.helperText}}
<div>
<span class="is-label helper-text has-text-grey">{{this.helperText}}</span>
</div>
{{/if}}
{{else}}
<Icon @name="minus" />
{{/if}}
</div>
<div class="column is-flex" data-test-value-div={{this.label}}>
{{#if (has-block)}}
{{yield}}
{{else if this.valueIsBoolean}}
{{#if this.value}}
<Icon class="icon-true" @name="check-circle" data-test-boolean-true />
Yes
{{else}}
<Icon @name="x-square" class="icon-false" />
No
{{/if}}
{{! alwaysRender is still true }}
{{else if (and (not this.value) this.defaultShown)}}
<span data-test-row-value={{this.label}}>{{this.defaultShown}}</span>
{{else if this.valueIsEmpty}}
<Icon @name="minus" />
{{else}}
{{#if (eq this.type "array")}}
<InfoTableItemArray
@backend={{this.backend}}
@displayArray={{this.value}}
@isLink={{this.isLink}}
@label={{this.label}}
@modelType={{this.modelType}}
@queryParam={{this.queryParam}}
@viewAll={{this.viewAll}}
@wildcardLabel={{this.wildcardLabel}}
/>
{{else}}
{{#if this.tooltipText}}
<ToolTip @verticalPosition="above" @horizontalPosition="left" as |T|>
<T.Trigger @tabindex={{false}}>
<span class="is-word-break has-text-black" data-test-row-value={{this.label}}>{{this.value}}</span>
</T.Trigger>
<T.Content class="tool-tip">
<CopyButton
@clipboardText={{this.tooltipText}}
@success={{action (set-flash-message "Data copied!")}}
@tagName="div"
@disabled={{not this.isTooltipCopyable}}
class={{if this.isTooltipCopyable "has-pointer"}}
data-test-tooltip-copy
>
<div class="box">
{{this.tooltipText}}
</div>
</CopyButton>
</T.Content>
</ToolTip>
{{else}}
<span class="is-word-break has-text-black" data-test-row-value={{this.label}}>{{this.value}}</span>
{{/if}}
{{/if}}
{{/if}}
</div>
{{/if}}

View File

@ -25,7 +25,7 @@
</div>
{{#if this.model.config.mode}}
<div class="box is-fullwidth is-shadowless">
<InfoTableRow @label="Mode" @value={{this.model.config.mode}} @data-test-mount-config-mode={{true}} />
<InfoTableRow @label="Mode" @value={{this.model.config.mode}} data-test-mount-config-mode={{true}} />
<InfoTableRow @label="Paths" @value={{this.model.config.paths}}>
<ul class="list--comma" data-test-mount-config-paths={{true}}>
{{#each this.model.config.paths as |path|}}

View File

@ -55,8 +55,7 @@ elRplAzrMF4=
await settled();
await generatePage.issueCert('foo');
await settled();
let countMaskedFonts = document.querySelectorAll('.masked-font').length;
assert.equal(countMaskedFonts, 3); // certificate, issuing ca, and private key
assert.dom('.masked-font').exists({ count: 3 }, 'renders 3 masked rows');
let firstUnMaskButton = document.querySelectorAll('.masked-input-toggle')[0];
await click(firstUnMaskButton);
assert.dom('.masked-value').hasTextContaining('-----BEGIN CERTIFICATE-----');

View File

@ -76,9 +76,9 @@ module('Integration | Component | InfoTableRow', function (hooks) {
this.set('isCopyable', false);
await render(hbs`
<InfoTableRow
<InfoTableRow
@label={{this.label}}
@value={{this.value}}
@value={{this.value}}
@tooltipText="Foo bar"
@isTooltipCopyable={{this.isCopyable}}
/>
@ -156,10 +156,96 @@ module('Integration | Component | InfoTableRow', function (hooks) {
@value={{this.value}}
@label={{this.label}}
@alwaysRender={{true}}>
Block content is here
Block content is here
</InfoTableRow>`);
let block = document.querySelector('[data-test-value-div]').textContent.trim();
assert.equal(block, 'Block content is here', 'renders block passed through');
});
test('Row renders when block content even if alwaysRender = false', async function (assert) {
await render(hbs`<InfoTableRow
@label={{this.label}}
@alwaysRender={{false}}>
Block content
</InfoTableRow>`);
assert.dom('[data-test-value-div]').exists('renders block');
assert.dom('[data-test-value-div]').hasText('Block content', 'renders block');
});
test('Row does not render empty block content when alwaysRender = false', async function (assert) {
await render(hbs`<InfoTableRow
@label={{this.label}}
@alwaysRender={{false}} />`);
assert.dom('[data-test-component="info-table-row"]').doesNotExist();
});
test('Has dashed label if none provided', async function (assert) {
await render(hbs`<InfoTableRow
@value={{this.value}}
/>`);
assert.dom('[data-test-component="info-table-row"]').exists();
assert.dom('[data-test-icon="minus"]').exists('renders dash when no label');
});
test('Truncates the label if too long', async function (assert) {
this.set('label', 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz');
await render(hbs`<InfoTableRow
@label={{this.label}}
@value={{this.value}}
/>`);
assert.dom('[data-test-component="info-table-row"]').exists('Row renders');
assert.dom('[data-test-label-div].label-overflow').exists('Label has class label-overflow');
await triggerEvent('[data-test-row-label]', 'mouseenter');
assert.dom('[data-test-label-tooltip]').exists('Label tooltip exists on hover');
});
test('Renders if block value and alwaysrender=false', async function (assert) {
await render(hbs`<InfoTableRow @alwaysRender={{false}}>{{this.value}}</InfoTableRow>`);
assert.dom('[data-test-component="info-table-row"]').exists();
});
test('Does not render if value is empty and alwaysrender=false', async function (assert) {
await render(hbs`<InfoTableRow @alwaysRender={{false}} @value="" />`);
assert.dom('[data-test-component="info-table-row"]').doesNotExist();
});
test('Renders dash for value if value empty and alwaysRender=true', async function (assert) {
await render(hbs`<InfoTableRow
@label={{this.label}}
@alwaysRender={{true}}
/>`);
assert.dom('[data-test-component="info-table-row"]').exists();
assert.dom('[data-test-value-div] [data-test-icon="minus"]').exists('renders dash for value');
});
test('Renders block over @value or @defaultShown', async function (assert) {
await render(hbs`<InfoTableRow
@label={{this.label}}
@value="bar"
@defaultShown="baz"
>
foo
</InfoTableRow>`);
assert.dom('[data-test-component="info-table-row"]').exists();
assert.dom('[data-test-value-div]').hasText('foo', 'renders block value');
});
test('Renders icons if value is boolean', async function (assert) {
this.set('value', true);
await render(hbs`<InfoTableRow
@label={{this.label}}
@value={{this.value}}
/>`);
assert.dom('[data-test-boolean-true]').exists('check icon exists');
assert.dom('[data-test-value-div]').hasText('Yes', 'Renders yes text');
this.set('value', false);
assert.dom('[data-test-boolean-false]').exists('x icon exists');
assert.dom('[data-test-value-div]').hasText('No', 'renders no text');
});
test('Renders data-test attrs passed from parent', async function (assert) {
this.set('value', true);
await render(hbs`<InfoTableRow
@label={{this.label}}
@value={{this.value}}
data-test-foo-bar
/>`);
assert.dom('[data-test-foo-bar]').exists();
});
});