Change tooltip for token_bound_certs and glimmerize string-list component (#15852)

* wip

* wip

* glimmerization done?

* fix tests

* tooltip and test

* changelog

* clean up

* cleanup

* cleanup
This commit is contained in:
Angel Garbarino 2022-06-07 12:15:25 -07:00 committed by GitHub
parent 426e3a5583
commit a86644968b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 169 additions and 171 deletions

3
changelog/15852.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Changed the tokenBoundCidrs tooltip content to clarify that comma separated values are not accepted in this field.
```

View File

@ -196,12 +196,13 @@
</Toggle> </Toggle>
{{else if (eq @attr.options.editType "stringArray")}} {{else if (eq @attr.options.editType "stringArray")}}
<StringList <StringList
@data-test-input={{@attr.name}} data-test-input={{@attr.name}}
@label={{this.labelString}} @label={{this.labelString}}
@warning={{@attr.options.warning}} @warning={{@attr.options.warning}}
@helpText={{@attr.options.helpText}} @helpText={{@attr.options.helpText}}
@inputValue={{get @model this.valuePath}} @inputValue={{get @model this.valuePath}}
@onChange={{this.setAndBroadcast}} @onChange={{this.setAndBroadcast}}
@attrName={{@attr.name}}
/> />
{{else if (eq @attr.options.sensitive true)}} {{else if (eq @attr.options.sensitive true)}}
{{! Masked Input }} {{! Masked Input }}

View File

@ -0,0 +1,56 @@
<div
class="field string-list form-section"
data-test-component="string-list"
{{did-insert this.autoSize}}
{{did-update this.autoSizeUpdate}}
data-test-input
...attributes
>
{{#if @label}}
<label class="title is-label" data-test-string-list-label="true">
{{@label}}
{{#if this.helpText}}
<InfoTooltip>{{this.helpText}}</InfoTooltip>
{{/if}}
</label>
{{/if}}
{{#if @warning}}
<AlertBanner @type="warning" @message={{@warning}} />
{{/if}}
{{#each this.inputList as |data index|}}
<div class="field is-grouped" data-test-string-list-row={{index}}>
<div class="control is-expanded">
<Textarea
data-test-string-list-input={{index}}
class="input"
@value={{data.value}}
name={{concat this.elementId "-" index}}
id={{concat this.elementId "-" index}}
{{on "keyup" (action "inputChanged" index)}}
{{on "change" (action "inputChanged" index)}}
/>
</div>
<div class="control">
{{#if (eq (inc index) this.inputList.length)}}
<button
type="button"
class="button is-outlined is-primary"
data-test-string-list-button="add"
{{on "click" this.addInput}}
>
Add
</button>
{{else}}
<button
type="button"
class="button is-expanded is-icon"
data-test-string-list-button="delete"
{{on "click" (fn this.removeInput index)}}
>
<Icon @name="trash" />
</button>
{{/if}}
</div>
</div>
{{/each}}
</div>

View File

@ -1,75 +1,32 @@
import ArrayProxy from '@ember/array/proxy'; import ArrayProxy from '@ember/array/proxy';
import Component from '@ember/component'; import Component from '@glimmer/component';
import { set, computed } from '@ember/object';
import autosize from 'autosize'; import autosize from 'autosize';
import layout from '../templates/components/string-list'; import { action } from '@ember/object';
import { set } from '@ember/object';
export default Component.extend({ /**
layout, * @module StringList
'data-test-component': 'string-list', *
attributeBindings: ['data-test-component', 'data-test-input'], * @example
classNames: ['field', 'string-list', 'form-section'], * ```js
* <StringList @label={label} @onChange={{this.setAndBroadcast}} @inputValue={{this.valuePath}}/>
* ```
* @param {string} label - Text displayed in the header above all the inputs.
* @param {function} onChange - Function called when any of the inputs change.
* @param {string} inputValue - A string or an array of strings.
* @param {string} warning - Text displayed as a warning.
* @param {string} helpText - Text displayed as a tooltip.
* @param {string} type=array - Optional type for inputValue.
* @param {string} attrName - We use this to check the type so we can modify the tooltip content.
*/
/* export default class StringList extends Component {
* @public constructor() {
* @param String super(...arguments);
*
* Optional - Text displayed in the header above all of the inputs
*
*/
label: null,
/* this.inputList = ArrayProxy.create({
* @public
* @param Function
*
* Function called when any of the inputs change
* accepts a single param `value` that is the
* result of calling `toVal()`.
*
*/
onChange: () => {},
/*
* @public
* @param String | Array
* A comma-separated string or an array of strings.
* Defaults to an empty array.
*
*/
inputValue: computed(function () {
return [];
}),
/*
*
* @public
* @param String - ['array'|'string]
*
* Optional type for `inputValue` - defaults to `'array'`
* Needs to match type of `inputValue` because it is set by the component on init.
*
*/
type: 'array',
/*
*
* @private
* @param Ember.ArrayProxy
*
* mutable array that contains objects in the form of
* {
* value: 'somestring',
* }
*
* used to track the state of values bound to the various inputs
*
*/
/* eslint-disable ember/no-side-effects */
inputList: computed('content', function () {
return ArrayProxy.create({
content: [],
// trim the `value` when accessing objects // trim the `value` when accessing objects
content: [],
objectAtContent: function (idx) { objectAtContent: function (idx) {
const obj = this.content.objectAt(idx); const obj = this.content.objectAt(idx);
if (obj && obj.value) { if (obj && obj.value) {
@ -78,32 +35,28 @@ export default Component.extend({
return obj; return obj;
}, },
}); });
}), this.type = this.args.type || 'array';
init() {
this._super(...arguments);
this.setType(); this.setType();
this.toList(); this.toList();
this.send('addInput'); this.addInput();
}, }
didInsertElement() {
this._super(...arguments);
autosize(this.element.querySelector('textarea'));
},
didUpdate() {
this._super(...arguments);
autosize.update(this.element.querySelector('textarea'));
},
setType() { setType() {
const list = this.inputList; const list = this.inputList;
if (!list) { if (!list) {
return; return;
} }
this.set('type', typeof list); this.type = typeof list;
}, }
toList() {
let input = this.args.inputValue || [];
const inputList = this.inputList;
if (typeof input === 'string') {
input = input.split(',');
}
inputList.addObjects(input.map((value) => ({ value })));
}
toVal() { toVal() {
const inputs = this.inputList.filter((x) => x.value).mapBy('value'); const inputs = this.inputList.filter((x) => x.value).mapBy('value');
@ -111,37 +64,47 @@ export default Component.extend({
return inputs.join(','); return inputs.join(',');
} }
return inputs; return inputs;
}, }
toList() { get helpText() {
let input = this.inputValue || []; if (this.args.attrName === 'tokenBoundCidrs') {
const inputList = this.inputList; return 'Specifies the blocks of IP addresses which are allowed to use the generated token. One entry per row.';
if (typeof input === 'string') { } else {
input = input.split(','); return this.args.helpText;
} }
inputList.addObjects(input.map((value) => ({ value }))); }
},
actions: { @action
inputChanged(idx, event) { autoSize(element) {
const inputObj = this.inputList.objectAt(idx); autosize(element.querySelector('textarea'));
const onChange = this.onChange; }
set(inputObj, 'value', event.target.value);
onChange(this.toVal());
},
addInput() { @action
const inputList = this.inputList; autoSizeUpdate(element) {
if (inputList.get('lastObject.value') !== '') { autosize.update(element.querySelector('textarea'));
inputList.pushObject({ value: '' }); }
}
},
removeInput(idx) { @action
const onChange = this.onChange; inputChanged(idx, event) {
const inputs = this.inputList; const inputObj = this.inputList.objectAt(idx);
inputs.removeObject(inputs.objectAt(idx)); const onChange = this.args.onChange;
onChange(this.toVal()); set(inputObj, 'value', event.target.value);
}, onChange(this.toVal());
}, }
});
@action
addInput() {
const inputList = this.inputList;
if (inputList.get('lastObject.value') !== '') {
inputList.pushObject({ value: '' });
}
}
@action
removeInput(idx) {
const onChange = this.args.onChange;
const inputs = this.inputList;
inputs.removeObject(inputs.objectAt(idx));
onChange(this.toVal());
}
}

View File

@ -1,42 +0,0 @@
{{#if this.label}}
<label class="title is-label" data-test-string-list-label="true">
{{this.label}}
{{#if this.helpText}}
<InfoTooltip>{{this.helpText}}</InfoTooltip>
{{/if}}
</label>
{{/if}}
{{#if this.warning}}
<AlertBanner @type="warning" @message={{this.warning}} />
{{/if}}
{{#each this.inputList as |data index|}}
<div class="field is-grouped" data-test-string-list-row={{index}}>
<div class="control is-expanded">
<Textarea
data-test-string-list-input={{index}}
class="input"
@value={{data.value}}
name={{concat this.elementId "-" index}}
id={{concat this.elementId "-" index}}
{{on "keyup" (action "inputChanged" index)}}
{{on "change" (action "inputChanged" index)}}
/>
</div>
<div class="control">
{{#if (eq (inc index) this.inputList.length)}}
<button type="button" class="button is-outlined is-primary" data-test-string-list-button="add" {{action "addInput"}}>
Add
</button>
{{else}}
<button
type="button"
class="button is-expanded is-icon"
data-test-string-list-button="delete"
{{action "removeInput" index}}
>
<Icon @name="trash" />
</button>
{{/if}}
</div>
</div>
{{/each}}

View File

@ -17,6 +17,7 @@
"ember-composable-helpers": "*", "ember-composable-helpers": "*",
"ember-concurrency": "*", "ember-concurrency": "*",
"ember-maybe-in-element": "*", "ember-maybe-in-element": "*",
"ember/render-modifiers": "*",
"ember-power-select": "*", "ember-power-select": "*",
"ember-router-helpers": "*", "ember-router-helpers": "*",
"ember-svg-jar": "*", "ember-svg-jar": "*",

View File

@ -1,11 +1,16 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit'; import { setupRenderingTest } from 'ember-qunit';
import { render, click, fillIn, triggerKeyEvent } from '@ember/test-helpers'; import { render, click, fillIn, triggerKeyEvent, triggerEvent } from '@ember/test-helpers';
import sinon from 'sinon';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | string list', function (hooks) { module('Integration | Component | string list', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.spy = sinon.spy();
});
const assertBlank = function (assert) { const assertBlank = function (assert) {
assert.dom('[data-test-string-list-input]').exists({ count: 1 }, 'renders 1 input'); assert.dom('[data-test-string-list-input]').exists({ count: 1 }, 'renders 1 input');
assert.dom('[data-test-string-list-input]').hasValue('', 'the input is blank'); assert.dom('[data-test-string-list-input]').hasValue('', 'the input is blank');
@ -26,56 +31,55 @@ module('Integration | Component | string list', function (hooks) {
test('it renders the label', async function (assert) { test('it renders the label', async function (assert) {
assert.expect(4); assert.expect(4);
await render(hbs`{{string-list label="foo"}}`); await render(hbs`<StringList @label="foo" @onChange={{this.spy}} />`);
assert.dom('[data-test-string-list-label]').hasText('foo', 'renders the label when provided'); assert.dom('[data-test-string-list-label]').hasText('foo', 'renders the label when provided');
await render(hbs`<StringList />`);
await render(hbs`{{string-list}}`);
assert.dom('[data-test-string-list-label]').doesNotExist('does not render the label'); assert.dom('[data-test-string-list-label]').doesNotExist('does not render the label');
assertBlank(assert); assertBlank(assert);
}); });
test('it renders inputValue from empty string', async function (assert) { test('it renders inputValue from empty string', async function (assert) {
assert.expect(2); assert.expect(2);
await render(hbs`{{string-list inputValue=""}}`); await render(hbs`<StringList @inputValue="" />`);
assertBlank(assert); assertBlank(assert);
}); });
test('it renders inputValue from string with one value', async function (assert) { test('it renders inputValue from string with one value', async function (assert) {
assert.expect(3); assert.expect(3);
await render(hbs`{{string-list inputValue="foo"}}`); await render(hbs`<StringList @inputValue="foo" />`);
assertFoo(assert); assertFoo(assert);
}); });
test('it renders inputValue from comma-separated string', async function (assert) { test('it renders inputValue from comma-separated string', async function (assert) {
assert.expect(4); assert.expect(4);
await render(hbs`{{string-list inputValue="foo,bar"}}`); await render(hbs`<StringList @inputValue="foo,bar" />`);
assertFooBar(assert); assertFooBar(assert);
}); });
test('it renders inputValue from a blank array', async function (assert) { test('it renders inputValue from a blank array', async function (assert) {
assert.expect(2); assert.expect(2);
this.set('inputValue', []); this.set('inputValue', []);
await render(hbs`{{string-list inputValue=inputValue}}`); await render(hbs`<StringList @inputValue={{inputValue}} />`);
assertBlank(assert); assertBlank(assert);
}); });
test('it renders inputValue array with a single item', async function (assert) { test('it renders inputValue array with a single item', async function (assert) {
assert.expect(3); assert.expect(3);
this.set('inputValue', ['foo']); this.set('inputValue', ['foo']);
await render(hbs`{{string-list inputValue=inputValue}}`); await render(hbs`<StringList @inputValue={{inputValue}} />`);
assertFoo(assert); assertFoo(assert);
}); });
test('it renders inputValue array with a multiple items', async function (assert) { test('it renders inputValue array with a multiple items', async function (assert) {
assert.expect(4); assert.expect(4);
this.set('inputValue', ['foo', 'bar']); this.set('inputValue', ['foo', 'bar']);
await render(hbs`{{string-list inputValue=inputValue}}`); await render(hbs`<StringList @inputValue={{inputValue}} />`);
assertFooBar(assert); assertFooBar(assert);
}); });
test('it adds a new row only when the last row is not blank', async function (assert) { test('it adds a new row only when the last row is not blank', async function (assert) {
assert.expect(5); assert.expect(5);
await render(hbs`{{string-list inputValue=""}}`); await render(hbs`<StringList @inputValue="" @onChange={{this.spy}}/>`);
await click('[data-test-string-list-button="add"]'); await click('[data-test-string-list-button="add"]');
assertBlank(assert); assertBlank(assert);
await fillIn('[data-test-string-list-input="0"]', 'foo'); await fillIn('[data-test-string-list-input="0"]', 'foo');
@ -85,7 +89,7 @@ module('Integration | Component | string list', function (hooks) {
}); });
test('it trims input values', async function (assert) { test('it trims input values', async function (assert) {
await render(hbs`{{string-list inputValue=""}}`); await render(hbs`<StringList @inputValue="" @onChange={{this.spy}}/>`);
await fillIn('[data-test-string-list-input="0"]', 'foo'); await fillIn('[data-test-string-list-input="0"]', 'foo');
await triggerKeyEvent('[data-test-string-list-input="0"]', 'keyup', 14); await triggerKeyEvent('[data-test-string-list-input="0"]', 'keyup', 14);
assert.dom('[data-test-string-list-input="0"]').hasValue('foo'); assert.dom('[data-test-string-list-input="0"]').hasValue('foo');
@ -97,7 +101,7 @@ module('Integration | Component | string list', function (hooks) {
this.set('onChange', function (val) { this.set('onChange', function (val) {
assert.deepEqual(val, ['foo', 'bar'], 'calls onChange with expected value'); assert.deepEqual(val, ['foo', 'bar'], 'calls onChange with expected value');
}); });
await render(hbs`{{string-list inputValue=inputValue onChange=(action onChange)}}`); await render(hbs`<StringList @inputValue={{inputValue}} @onChange={{onChange}} />`);
await fillIn('[data-test-string-list-input="1"]', 'bar'); await fillIn('[data-test-string-list-input="1"]', 'bar');
await triggerKeyEvent('[data-test-string-list-input="1"]', 'keyup', 14); await triggerKeyEvent('[data-test-string-list-input="1"]', 'keyup', 14);
}); });
@ -108,7 +112,7 @@ module('Integration | Component | string list', function (hooks) {
this.set('onChange', function (val) { this.set('onChange', function (val) {
assert.equal(val, 'foo,bar', 'calls onChange with expected value'); assert.equal(val, 'foo,bar', 'calls onChange with expected value');
}); });
await render(hbs`{{string-list inputValue=inputValue onChange=(action onChange)}}`); await render(hbs`<StringList @inputValue={{inputValue}} @onChange={{onChange}}/>`);
await fillIn('[data-test-string-list-input="1"]', 'bar'); await fillIn('[data-test-string-list-input="1"]', 'bar');
await triggerKeyEvent('[data-test-string-list-input="1"]', 'keyup', 14); await triggerKeyEvent('[data-test-string-list-input="1"]', 'keyup', 14);
}); });
@ -119,11 +123,23 @@ module('Integration | Component | string list', function (hooks) {
this.set('onChange', function (val) { this.set('onChange', function (val) {
assert.equal(val, 'bar', 'calls onChange with expected value'); assert.equal(val, 'bar', 'calls onChange with expected value');
}); });
await render(hbs`{{string-list inputValue=inputValue onChange=(action onChange)}}`); await render(hbs`<StringList @inputValue={{inputValue}} @onChange={{onChange}} />`);
await click('[data-test-string-list-row="0"] [data-test-string-list-button="delete"]'); await click('[data-test-string-list-row="0"] [data-test-string-list-button="delete"]');
assert.dom('[data-test-string-list-input]').exists({ count: 2 }, 'renders 2 inputs'); assert.dom('[data-test-string-list-input]').exists({ count: 2 }, 'renders 2 inputs');
assert.dom('[data-test-string-list-input="0"]').hasValue('bar'); assert.dom('[data-test-string-list-input="0"]').hasValue('bar');
assert.dom('[data-test-string-list-input="1"]').hasValue(''); assert.dom('[data-test-string-list-input="1"]').hasValue('');
}); });
test('it replaces helpText if name is tokenBoundCidrs', async function (assert) {
assert.expect(1);
await render(hbs`<StringList @label={{'blah'}} @helpText={{'blah'}} @attrName={{'tokenBoundCidrs'}} />`);
let tooltipTrigger = document.querySelector('[data-test-tool-tip-trigger]');
await triggerEvent(tooltipTrigger, 'mouseenter');
assert
.dom('[data-test-info-tooltip-content]')
.hasText(
'Specifies the blocks of IP addresses which are allowed to use the generated token. One entry per row.'
);
});
}); });