UI: glimmerize masked input (#20431)
* Glimmerize masked-input * Update secret-create-or-update to change masked-input value * Use maskedInput for ssh configure privateKey * Add download button to masked input and v2 secrets. Resolves #6364 * Add changelog
This commit is contained in:
parent
3ca73ad07e
commit
59ff9c2eea
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: Add download button for each secret value in KV v2
|
||||
```
|
|
@ -253,22 +253,16 @@ export default class SecretCreateOrUpdate extends Component {
|
|||
this.codemirrorString = this.args.secretData.toJSONString(true);
|
||||
}
|
||||
@action
|
||||
handleMaskedInputChange(secret, index, value) {
|
||||
const row = { ...secret, value };
|
||||
set(this.args.secretData, index, row);
|
||||
this.handleChange();
|
||||
}
|
||||
@action
|
||||
handleChange() {
|
||||
this.codemirrorString = this.args.secretData.toJSONString(true);
|
||||
set(this.args.modelForData, 'secretData', this.args.secretData.toJSON());
|
||||
}
|
||||
//submit on shift + enter
|
||||
@action
|
||||
handleKeyDown(e) {
|
||||
e.stopPropagation();
|
||||
if (!(e.keyCode === keys.ENTER && e.metaKey)) {
|
||||
return;
|
||||
}
|
||||
const $form = this.element.querySelector('form');
|
||||
if ($form.length) {
|
||||
$form.submit();
|
||||
}
|
||||
}
|
||||
@action
|
||||
updateValidationErrorCount(errorCount) {
|
||||
this.validationErrorCount = errorCount;
|
||||
|
|
|
@ -49,14 +49,16 @@
|
|||
}
|
||||
|
||||
.button.masked-input-toggle,
|
||||
.button.copy-button {
|
||||
.button.copy-button,
|
||||
.button.download-button {
|
||||
min-width: $spacing-xl;
|
||||
border-left: 0;
|
||||
color: $grey;
|
||||
box-shadow: 0 3px 1px 0px rgba(10, 10, 10, 0.12);
|
||||
}
|
||||
|
||||
.button.copy-button {
|
||||
.button.copy-button,
|
||||
.button.download-button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
|
@ -66,7 +68,8 @@
|
|||
|
||||
.display-only {
|
||||
.button.masked-input-toggle,
|
||||
.button.copy-button {
|
||||
.button.copy-button,
|
||||
.button.download-button {
|
||||
background: transparent;
|
||||
height: auto;
|
||||
line-height: 1rem;
|
||||
|
|
|
@ -46,7 +46,13 @@
|
|||
Private key
|
||||
</label>
|
||||
<div class="control">
|
||||
<Textarea name="privateKey" id="privateKey" class="input" @value={{@model.privateKey}} />
|
||||
<MaskedInput
|
||||
@name="privateKey"
|
||||
id="privateKey"
|
||||
class="input"
|
||||
@value={{@model.privateKey}}
|
||||
@onChange={{mut @model.privateKey}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
|
|
@ -75,8 +75,7 @@
|
|||
<div class="column info-table-row-edit">
|
||||
<MaskedInput
|
||||
@name={{secret.name}}
|
||||
@onKeyDown={{action "handleKeyDown"}}
|
||||
@onChange={{action "handleChange"}}
|
||||
@onChange={{fn this.handleMaskedInputChange secret index}}
|
||||
@value={{secret.value}}
|
||||
data-test-secret-value="true"
|
||||
/>
|
||||
|
@ -212,8 +211,7 @@
|
|||
<div class="column">
|
||||
<MaskedInput
|
||||
@name={{secret.name}}
|
||||
@onKeyDown={{action "handleKeyDown"}}
|
||||
@onChange={{action "handleChange"}}
|
||||
@onChange={{fn this.handleMaskedInputChange secret index}}
|
||||
@value={{secret.value}}
|
||||
data-test-secret-value="true"
|
||||
/>
|
||||
|
|
|
@ -71,8 +71,6 @@
|
|||
@displayOnly={{true}}
|
||||
@allowCopy={{true}}
|
||||
@value={{this.wrappedData}}
|
||||
@success={{action "handleCopySuccess"}}
|
||||
@error={{action "handleCopyError"}}
|
||||
/>
|
||||
{{/if}}
|
||||
</li>
|
||||
|
|
|
@ -66,7 +66,13 @@
|
|||
{{#each @modelForData.secretKeyAndValue as |secret|}}
|
||||
<InfoTableRow @label={{secret.key}} @value={{secret.value}} @alwaysRender={{true}}>
|
||||
{{#if secret.value}}
|
||||
<MaskedInput @value={{secret.value}} @displayOnly={{true}} @allowCopy={{true}} />
|
||||
<MaskedInput
|
||||
@name={{secret.key}}
|
||||
@value={{secret.value}}
|
||||
@displayOnly={{true}}
|
||||
@allowCopy={{true}}
|
||||
@allowDownload={{@isV2}}
|
||||
/>
|
||||
{{else}}
|
||||
<Icon @name="minus" />
|
||||
{{/if}}
|
||||
|
|
|
@ -216,12 +216,11 @@
|
|||
@subText="{{@attr.options.subText}} Add one item per row."
|
||||
/>
|
||||
{{else if (eq @attr.options.sensitive true)}}
|
||||
{{! Masked Input }}
|
||||
<MaskedInput
|
||||
@name={{@attr.name}}
|
||||
@value={{or (get @model this.valuePath) @attr.options.defaultValue}}
|
||||
@allowCopy="true"
|
||||
@onChange={{this.setAndBroadcast}}
|
||||
@name={{@attr.name}}
|
||||
@onKeyUp={{@onKeyUp}}
|
||||
/>
|
||||
{{#if this.validationError}}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<div
|
||||
class="masked-input {{if @displayOnly 'display-only'}} {{if @allowCopy 'allow-copy'}}"
|
||||
data-test-masked-input
|
||||
data-test-field
|
||||
...attributes
|
||||
>
|
||||
{{#if @displayOnly}}
|
||||
{{#if this.showValue}}
|
||||
<pre class="masked-value display-only is-word-break">{{@value}}</pre>
|
||||
{{else}}
|
||||
<pre class="masked-value display-only masked-font">***********</pre>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<Textarea
|
||||
id={{this.textareaId}}
|
||||
name={{@name}}
|
||||
@value={{@value}}
|
||||
class="input masked-value {{unless this.showValue 'masked-font'}}"
|
||||
rows={{1}}
|
||||
wrap="off"
|
||||
spellcheck="false"
|
||||
{{on "change" this.onChange}}
|
||||
{{on "keyup" (fn this.handleKeyUp @name @value)}}
|
||||
data-test-textarea
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @allowCopy}}
|
||||
<CopyButton
|
||||
@clipboardText={{@value}}
|
||||
@success={{action (set-flash-message "Data copied!")}}
|
||||
class="copy-button button {{if @displayOnly 'is-compact'}}"
|
||||
data-test-copy-button
|
||||
>
|
||||
<Icon @name="clipboard-copy" aria-hidden="Copy value" />
|
||||
</CopyButton>
|
||||
{{/if}}
|
||||
{{#if @allowDownload}}
|
||||
<DownloadButton
|
||||
class="button download-button"
|
||||
@filename={{or @name "secret-value"}}
|
||||
@data={{@value}}
|
||||
@stringify={{true}}
|
||||
aria-label="Download secret value"
|
||||
>
|
||||
<Icon @name="download" />
|
||||
</DownloadButton>
|
||||
{{/if}}
|
||||
<button
|
||||
onclick={{this.toggleMask}}
|
||||
type="button"
|
||||
aria-label={{if this.showValue "mask value" "show value"}}
|
||||
title={{if this.showValue "mask value" "show value"}}
|
||||
class="{{if (eq @value '') 'has-text-grey'}} masked-input-toggle button"
|
||||
data-test-button="toggle-masked"
|
||||
>
|
||||
<Icon @name={{if this.showValue "eye" "eye-off"}} />
|
||||
</button>
|
||||
</div>
|
|
@ -3,9 +3,12 @@
|
|||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@ember/component';
|
||||
import { debug } from '@ember/debug';
|
||||
import { action } from '@ember/object';
|
||||
import { guidFor } from '@ember/object/internals';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import autosize from 'autosize';
|
||||
import layout from '../templates/components/masked-input';
|
||||
|
||||
/**
|
||||
* @module MaskedInput
|
||||
|
@ -13,53 +16,50 @@ import layout from '../templates/components/masked-input';
|
|||
*
|
||||
* @example
|
||||
* <MaskedInput
|
||||
* @value={{attr.options.defaultValue}}
|
||||
* @value={{get @model attr.name}}
|
||||
* @allowCopy={{true}}
|
||||
* @onChange={{action "someAction"}}
|
||||
* @onKeyUp={{action "onKeyUp"}}
|
||||
* @allowDownload={{true}}
|
||||
* @onChange={{this.handleChange}}
|
||||
* @onKeyUp={{this.handleKeyUp}}
|
||||
* />
|
||||
*
|
||||
* @param [value] {String} - The value to display in the input.
|
||||
* @param [allowCopy=null] {bool} - Whether or not the input should render with a copy button.
|
||||
* @param value {String} - The value to display in the input.
|
||||
* @param name {String} - The key correlated to the value. Used for the download file name.
|
||||
* @param [onChange=Callback] {Function|action} - Callback triggered on change, sends new value. Must set the value of @value
|
||||
* @param [allowCopy=false] {bool} - Whether or not the input should render with a copy button.
|
||||
* @param [displayOnly=false] {bool} - Whether or not to display the value as a display only `pre` element or as an input.
|
||||
* @param [onChange=Function.prototype] {Function|action} - A function to call when the value of the input changes.
|
||||
* @param [onKeyUp=Function.prototype] {Function|action} - A function to call whenever on the dom event onkeyup. Generally passed down from higher level parent.
|
||||
* @param [isCertificate=false] {bool} - If certificate display the label and icons differently.
|
||||
*
|
||||
*/
|
||||
export default Component.extend({
|
||||
layout,
|
||||
value: null,
|
||||
showValue: false,
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
autosize(this.element.querySelector('textarea'));
|
||||
},
|
||||
didUpdate() {
|
||||
this._super(...arguments);
|
||||
autosize.update(this.element.querySelector('textarea'));
|
||||
},
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
autosize.destroy(this.element.querySelector('textarea'));
|
||||
},
|
||||
displayOnly: false,
|
||||
onKeyDown() {},
|
||||
onKeyUp() {},
|
||||
onChange() {},
|
||||
actions: {
|
||||
toggleMask() {
|
||||
this.toggleProperty('showValue');
|
||||
},
|
||||
updateValue(e) {
|
||||
const value = e.target.value;
|
||||
this.set('value', value);
|
||||
this.onChange(value);
|
||||
},
|
||||
handleKeyUp(name, value) {
|
||||
if (this.onKeyUp) {
|
||||
this.onKeyUp(name, value);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
export default class MaskedInputComponent extends Component {
|
||||
textareaId = 'textarea-' + guidFor(this);
|
||||
@tracked showValue = false;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
if (!this.args.onChange && !this.args.displayOnly) {
|
||||
debug('onChange is required for editable Masked Input!');
|
||||
}
|
||||
this.updateSize();
|
||||
}
|
||||
|
||||
updateSize() {
|
||||
autosize(document.getElementById(this.textareaId));
|
||||
}
|
||||
|
||||
@action onChange(evt) {
|
||||
const value = evt.target.value;
|
||||
if (this.args.onChange) {
|
||||
this.args.onChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
@action handleKeyUp(name, value) {
|
||||
this.updateSize();
|
||||
if (this.onKeyUp) {
|
||||
this.onKeyUp(name, value);
|
||||
}
|
||||
}
|
||||
@action toggleMask() {
|
||||
this.showValue = !this.showValue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
<div
|
||||
class="masked-input {{if this.displayOnly 'display-only'}} {{if this.allowCopy 'allow-copy'}}"
|
||||
data-test-masked-input
|
||||
data-test-field
|
||||
>
|
||||
{{#if this.displayOnly}}
|
||||
{{#if this.showValue}}
|
||||
<pre class="masked-value display-only is-word-break">{{this.value}}</pre>
|
||||
{{else}}
|
||||
<pre class="masked-value display-only masked-font">***********</pre>
|
||||
{{/if}}
|
||||
{{else if this.inputField}}
|
||||
<input
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
value={{this.value}}
|
||||
class="input {{unless this.showValue 'masked-font'}}"
|
||||
onchange={{action "updateValue"}}
|
||||
data-test-input
|
||||
/>
|
||||
{{else}}
|
||||
{{! template-lint-disable no-down-event-binding }}
|
||||
<textarea
|
||||
class="input masked-value {{unless this.showValue 'masked-font'}}"
|
||||
rows={{1}}
|
||||
wrap="off"
|
||||
onkeydown={{action this.onKeyDown}}
|
||||
onchange={{action "updateValue"}}
|
||||
onkeyup={{action (action "handleKeyUp" this.name) value="target.value"}}
|
||||
value={{this.value}}
|
||||
spellcheck="false"
|
||||
data-test-textarea
|
||||
></textarea>
|
||||
{{! template-lint-enable no-down-event-binding }}
|
||||
{{/if}}
|
||||
{{#if this.allowCopy}}
|
||||
<CopyButton
|
||||
@clipboardText={{this.value}}
|
||||
@success={{action (set-flash-message "Data copied!")}}
|
||||
class="copy-button button {{if this.displayOnly 'is-compact'}}"
|
||||
data-test-copy-button
|
||||
>
|
||||
<Icon @name="clipboard-copy" aria-hidden="Copy value" />
|
||||
</CopyButton>
|
||||
{{/if}}
|
||||
<button
|
||||
onclick={{action "toggleMask"}}
|
||||
type="button"
|
||||
aria-label={{if this.showValue "mask value" "show value"}}
|
||||
title={{if this.showValue "mask value" "show value"}}
|
||||
class="{{if (eq this.value '') 'has-text-grey'}} masked-input-toggle button"
|
||||
data-test-button="toggle-masked"
|
||||
>
|
||||
<Icon @name={{if this.showValue "eye" "eye-off"}} />
|
||||
</button>
|
||||
</div>
|
|
@ -16,68 +16,40 @@ module('Integration | Component | masked input', function (hooks) {
|
|||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`{{masked-input}}`);
|
||||
assert.dom('[data-test-masked-input]').exists('shows expiration beacon');
|
||||
});
|
||||
|
||||
test('it renders a textarea', async function (assert) {
|
||||
await render(hbs`{{masked-input}}`);
|
||||
|
||||
await render(hbs`<MaskedInput />`);
|
||||
assert.dom('[data-test-masked-input]').exists('shows masked input');
|
||||
assert.ok(component.textareaIsPresent);
|
||||
assert.dom('[data-test-textarea]').hasClass('masked-font', 'it renders an input with obscure font');
|
||||
assert.notOk(component.copyButtonIsPresent, 'does not render copy button by default');
|
||||
assert.notOk(component.downloadButtonIsPresent, 'does not render download button by default');
|
||||
|
||||
await component.toggleMasked();
|
||||
assert.dom('.masked-value').doesNotHaveClass('masked-font', 'it unmasks when show button is clicked');
|
||||
await component.toggleMasked();
|
||||
assert.dom('.masked-value').hasClass('masked-font', 'it remasks text when button is clicked');
|
||||
});
|
||||
|
||||
test('it renders an input with obscure font', async function (assert) {
|
||||
await render(hbs`{{masked-input}}`);
|
||||
|
||||
assert.dom('[data-test-textarea]').hasClass('masked-font', 'loading class with correct font');
|
||||
});
|
||||
|
||||
test('it renders obscure font when displayOnly', async function (assert) {
|
||||
test('it renders correctly when displayOnly', async function (assert) {
|
||||
this.set('value', 'value');
|
||||
await render(hbs`{{masked-input displayOnly=true value=this.value}}`);
|
||||
await render(hbs`<MaskedInput @displayOnly={{true}} @value={{this.value}} />`);
|
||||
|
||||
assert.dom('.masked-value').hasClass('masked-font', 'loading class with correct font');
|
||||
});
|
||||
|
||||
test('it does not render a textarea when displayOnly is true', async function (assert) {
|
||||
await render(hbs`{{masked-input displayOnly=true}}`);
|
||||
|
||||
assert.notOk(component.textareaIsPresent);
|
||||
assert.dom('.masked-value').hasClass('masked-font', 'value has obscured font');
|
||||
assert.notOk(component.textareaIsPresent, 'it does not render a textarea when displayOnly is true');
|
||||
});
|
||||
|
||||
test('it renders a copy button when allowCopy is true', async function (assert) {
|
||||
await render(hbs`{{masked-input allowCopy=true}}`);
|
||||
|
||||
await render(hbs`<MaskedInput @allowCopy={{true}} />`);
|
||||
assert.ok(component.copyButtonIsPresent);
|
||||
});
|
||||
|
||||
test('it does not render a copy button when allowCopy is false', async function (assert) {
|
||||
await render(hbs`{{masked-input allowCopy=false}}`);
|
||||
|
||||
assert.notOk(component.copyButtonIsPresent);
|
||||
});
|
||||
|
||||
test('it unmasks text when button is clicked', async function (assert) {
|
||||
this.set('value', 'value');
|
||||
await render(hbs`{{masked-input value=this.value}}`);
|
||||
await component.toggleMasked();
|
||||
|
||||
assert.dom('.masked-value').doesNotHaveClass('masked-font');
|
||||
});
|
||||
|
||||
test('it remasks text when button is clicked', async function (assert) {
|
||||
this.set('value', 'value');
|
||||
await render(hbs`{{masked-input value=this.value}}`);
|
||||
|
||||
await component.toggleMasked();
|
||||
await component.toggleMasked();
|
||||
|
||||
assert.dom('.masked-value').hasClass('masked-font');
|
||||
test('it renders a download button when allowDownload is true', async function (assert) {
|
||||
await render(hbs`<MaskedInput @allowDownload={{true}} />`);
|
||||
assert.ok(component.downloadButtonIsPresent);
|
||||
});
|
||||
|
||||
test('it shortens all outputs when displayOnly and masked', async function (assert) {
|
||||
this.set('value', '123456789-123456789-123456789');
|
||||
await render(hbs`{{masked-input value=this.value displayOnly=true}}`);
|
||||
await render(hbs`<MaskedInput @value={{this.value}} @displayOnly={{true}} />`);
|
||||
const maskedValue = document.querySelector('.masked-value').innerText;
|
||||
assert.strictEqual(maskedValue.length, 11);
|
||||
|
||||
|
@ -88,7 +60,7 @@ module('Integration | Component | masked input', function (hooks) {
|
|||
|
||||
test('it does not unmask text on focus', async function (assert) {
|
||||
this.set('value', '123456789-123456789-123456789');
|
||||
await render(hbs`{{masked-input value=this.value}}`);
|
||||
await render(hbs`<MaskedInput @value={{this.value}} />`);
|
||||
assert.dom('.masked-value').hasClass('masked-font');
|
||||
await focus('.masked-value');
|
||||
assert.dom('.masked-value').hasClass('masked-font');
|
||||
|
@ -96,7 +68,7 @@ module('Integration | Component | masked input', function (hooks) {
|
|||
|
||||
test('it does not remove value on tab', async function (assert) {
|
||||
this.set('value', 'hello');
|
||||
await render(hbs`{{masked-input value=this.value}}`);
|
||||
await render(hbs`<MaskedInput @value={{this.value}} />`);
|
||||
await triggerKeyEvent('[data-test-textarea]', 'keydown', 9);
|
||||
await component.toggleMasked();
|
||||
const unMaskedValue = document.querySelector('.masked-value').value;
|
||||
|
|
|
@ -8,5 +8,6 @@ import { clickable, isPresent } from 'ember-cli-page-object';
|
|||
export default {
|
||||
textareaIsPresent: isPresent('[data-test-textarea]'),
|
||||
copyButtonIsPresent: isPresent('[data-test-copy-button]'),
|
||||
downloadButtonIsPresent: isPresent('[data-test-download-button]'),
|
||||
toggleMasked: clickable('[data-test-button="toggle-masked"]'),
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue