diff --git a/changelog/20431.txt b/changelog/20431.txt
new file mode 100644
index 0000000000..a0083d879e
--- /dev/null
+++ b/changelog/20431.txt
@@ -0,0 +1,3 @@
+```release-note:improvement
+ui: Add download button for each secret value in KV v2
+```
diff --git a/ui/app/components/secret-create-or-update.js b/ui/app/components/secret-create-or-update.js
index 47aedac955..0427127a08 100644
--- a/ui/app/components/secret-create-or-update.js
+++ b/ui/app/components/secret-create-or-update.js
@@ -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;
diff --git a/ui/app/styles/components/masked-input.scss b/ui/app/styles/components/masked-input.scss
index 51bc8392fe..03d7868934 100644
--- a/ui/app/styles/components/masked-input.scss
+++ b/ui/app/styles/components/masked-input.scss
@@ -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;
diff --git a/ui/app/templates/components/configure-ssh-secret.hbs b/ui/app/templates/components/configure-ssh-secret.hbs
index 94136fe9bc..8e61dc38e9 100644
--- a/ui/app/templates/components/configure-ssh-secret.hbs
+++ b/ui/app/templates/components/configure-ssh-secret.hbs
@@ -46,7 +46,13 @@
Private key
-
+
diff --git a/ui/app/templates/components/secret-create-or-update.hbs b/ui/app/templates/components/secret-create-or-update.hbs
index dbae471fb3..28a8a635ed 100644
--- a/ui/app/templates/components/secret-create-or-update.hbs
+++ b/ui/app/templates/components/secret-create-or-update.hbs
@@ -75,8 +75,7 @@
@@ -212,8 +211,7 @@
diff --git a/ui/app/templates/components/secret-edit-toolbar.hbs b/ui/app/templates/components/secret-edit-toolbar.hbs
index b9f4c4fb52..574871772f 100644
--- a/ui/app/templates/components/secret-edit-toolbar.hbs
+++ b/ui/app/templates/components/secret-edit-toolbar.hbs
@@ -71,8 +71,6 @@
@displayOnly={{true}}
@allowCopy={{true}}
@value={{this.wrappedData}}
- @success={{action "handleCopySuccess"}}
- @error={{action "handleCopyError"}}
/>
{{/if}}
diff --git a/ui/app/templates/components/secret-form-show.hbs b/ui/app/templates/components/secret-form-show.hbs
index 6e3c96f355..4616b5dd3a 100644
--- a/ui/app/templates/components/secret-form-show.hbs
+++ b/ui/app/templates/components/secret-form-show.hbs
@@ -66,7 +66,13 @@
{{#each @modelForData.secretKeyAndValue as |secret|}}
{{#if secret.value}}
-
+
{{else}}
{{/if}}
diff --git a/ui/lib/core/addon/components/form-field.hbs b/ui/lib/core/addon/components/form-field.hbs
index 212671a3c0..126ef28f70 100644
--- a/ui/lib/core/addon/components/form-field.hbs
+++ b/ui/lib/core/addon/components/form-field.hbs
@@ -216,12 +216,11 @@
@subText="{{@attr.options.subText}} Add one item per row."
/>
{{else if (eq @attr.options.sensitive true)}}
- {{! Masked Input }}
{{#if this.validationError}}
diff --git a/ui/lib/core/addon/components/masked-input.hbs b/ui/lib/core/addon/components/masked-input.hbs
new file mode 100644
index 0000000000..a8965a22d1
--- /dev/null
+++ b/ui/lib/core/addon/components/masked-input.hbs
@@ -0,0 +1,58 @@
+
\ No newline at end of file
diff --git a/ui/lib/core/addon/components/masked-input.js b/ui/lib/core/addon/components/masked-input.js
index 72ba2dc545..8200640885 100644
--- a/ui/lib/core/addon/components/masked-input.js
+++ b/ui/lib/core/addon/components/masked-input.js
@@ -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
*
*
- * @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;
+ }
+}
diff --git a/ui/lib/core/addon/templates/components/masked-input.hbs b/ui/lib/core/addon/templates/components/masked-input.hbs
deleted file mode 100644
index 88a3739804..0000000000
--- a/ui/lib/core/addon/templates/components/masked-input.hbs
+++ /dev/null
@@ -1,56 +0,0 @@
-
\ No newline at end of file
diff --git a/ui/tests/integration/components/masked-input-test.js b/ui/tests/integration/components/masked-input-test.js
index 933f36c66e..a4ee8e075e 100644
--- a/ui/tests/integration/components/masked-input-test.js
+++ b/ui/tests/integration/components/masked-input-test.js
@@ -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``);
+ 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``);
- 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``);
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``);
+ 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``);
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``);
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``);
await triggerKeyEvent('[data-test-textarea]', 'keydown', 9);
await component.toggleMasked();
const unMaskedValue = document.querySelector('.masked-value').value;
diff --git a/ui/tests/pages/components/masked-input.js b/ui/tests/pages/components/masked-input.js
index c4894b04f6..61ec76b25b 100644
--- a/ui/tests/pages/components/masked-input.js
+++ b/ui/tests/pages/components/masked-input.js
@@ -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"]'),
};