From 5ebb6dfb0eaa32738ad41af93b8e5211d23bc030 Mon Sep 17 00:00:00 2001 From: claire bontempo <68122737+hellobontempo@users.noreply.github.com> Date: Mon, 19 Dec 2022 15:54:59 -0800 Subject: [PATCH] ui: refactor text file component (#18458) * wip tests * Move text-file to addon * rename fileName to filename, initial cleanup of text-fil * rename args, rename test selector * fix eye-con, remove enterAsText from file object * add tests * move files back to original location * rename files via git for git diff * adjsut test * Revert "wip tests" This reverts commit 63716a1e647a0b01236d34322837456ef3e9db43. * fix policy form input * cleanup conditional * add bottom margin * add element id * change arg name * add text area input test * add upload test to policy form Co-authored-by: Chelsea Shaw --- ui/app/components/file-to-array-buffer.js | 4 +- ui/app/components/pgp-file.js | 2 +- ui/app/components/policy-form.hbs | 2 +- ui/app/components/policy-form.js | 7 +- ui/app/components/text-file.js | 83 ------------- .../components/file-to-array-buffer.hbs | 6 +- ui/app/templates/components/pgp-file.hbs | 6 +- ui/app/templates/components/text-file.hbs | 100 ---------------- ui/lib/core/addon/components/form-field.hbs | 16 +-- ui/lib/core/addon/components/form-field.js | 4 +- ui/lib/core/addon/components/text-file.hbs | 95 +++++++++++++++ ui/lib/core/addon/components/text-file.js | 69 +++++++++++ .../core/addon/templates/components/icon.hbs | 2 +- .../templates/components/masked-input.hbs | 2 +- .../templates/components/shamir-flow.hbs | 2 +- .../components/shamir-modal-flow.hbs | 2 +- ui/lib/core/app/components/text-file.js | 1 + .../integration/components/form-field-test.js | 2 +- .../components/policy-form-test.js | 49 ++++---- .../integration/components/text-file-test.js | 113 ++++++++++++++++++ ui/tests/pages/components/masked-input.js | 2 +- 21 files changed, 330 insertions(+), 239 deletions(-) delete mode 100644 ui/app/components/text-file.js delete mode 100644 ui/app/templates/components/text-file.hbs create mode 100644 ui/lib/core/addon/components/text-file.hbs create mode 100644 ui/lib/core/addon/components/text-file.js create mode 100644 ui/lib/core/app/components/text-file.js create mode 100644 ui/tests/integration/components/text-file-test.js diff --git a/ui/app/components/file-to-array-buffer.js b/ui/app/components/file-to-array-buffer.js index e9056f357a..c27180fedc 100644 --- a/ui/app/components/file-to-array-buffer.js +++ b/ui/app/components/file-to-array-buffer.js @@ -23,7 +23,7 @@ export default Component.extend({ fileHelpText: null, file: null, - fileName: null, + filename: null, fileSize: null, fileLastModified: null, @@ -54,7 +54,7 @@ export default Component.extend({ const { name, size, lastModifiedDate } = fileMeta || {}; const fileSize = size ? filesize(size) : null; this.set('file', fileAsBytes); - this.set('fileName', name); + this.set('filename', name); this.set('fileSize', fileSize); this.set('fileLastModified', lastModifiedDate); this.onChange(fileAsBytes, name); diff --git a/ui/app/components/pgp-file.js b/ui/app/components/pgp-file.js index fd1a7d1fbb..e3f51cf471 100644 --- a/ui/app/components/pgp-file.js +++ b/ui/app/components/pgp-file.js @@ -54,7 +54,7 @@ export default Component.extend({ // If after decoding it's not b64, we want // the original as it was only encoded when we used `readAsDataURL`. const fileData = decoded.match(BASE_64_REGEX) ? decoded : b64File; - yield this.onChange(this.index, { value: fileData, fileName: filename }); + yield this.onChange(this.index, { value: fileData, filename: filename }); }) ), diff --git a/ui/app/components/policy-form.hbs b/ui/app/components/policy-form.hbs index 35f7e112fc..1119d6f1b4 100644 --- a/ui/app/components/policy-form.hbs +++ b/ui/app/components/policy-form.hbs @@ -38,7 +38,7 @@ {{#if this.showFileUpload}} - + {{else}} - * - * @param [inputOnly] {bool} - When true, only the file input will be rendered - * @param [helpText] {string} - Text underneath label. - * @param file {object} - * Object in the shape of: - * { - * value: 'file contents here', - * fileName: 'nameOfFile.txt', - * enterAsText: boolean ability to enter as text - * } - * @param [onChange=Function.prototype] {Function|action} - A function to call when the value of the input changes. - * @param [label=null] {string} - Text to use as the label for the file input. If null, a default will be rendered. - */ - -export default class TextFile extends Component { - fileHelpText = 'Select a file from your computer'; - textareaHelpText = 'Enter the value as text'; - elementId = guidFor(this); - index = ''; - - @tracked file = null; - @tracked showValue = false; - - get inputOnly() { - return this.args.inputOnly || false; - } - get label() { - return this.args.label || null; - } - - readFile(file) { - const reader = new FileReader(); - reader.onload = () => this.setFile(reader.result, file.name); - reader.readAsText(file); - } - - setFile(contents, filename) { - this.args.onChange(this.index, { value: contents, fileName: filename }); - } - - @action - pickedFile(e) { - e.preventDefault(); - const { files } = e.target; - if (!files.length) { - return; - } - for (let i = 0, len = files.length; i < len; i++) { - this.readFile(files[i]); - } - } - @action - updateData(e) { - e.preventDefault(); - const file = this.args.file; - set(file, 'value', e.target.value); - this.args.onChange(this.index, file); - } - @action - clearFile() { - this.args.onChange(this.index, { value: '' }); - } - @action - toggleMask() { - this.showValue = !this.showValue; - } -} diff --git a/ui/app/templates/components/file-to-array-buffer.hbs b/ui/app/templates/components/file-to-array-buffer.hbs index e3d32244f7..118aecb77a 100644 --- a/ui/app/templates/components/file-to-array-buffer.hbs +++ b/ui/app/templates/components/file-to-array-buffer.hbs @@ -13,9 +13,9 @@ Choose a file… - {{or this.fileName "No file chosen"}} + {{or this.filename "No file chosen"}} - {{#if this.fileName}} + {{#if this.filename}} diff --git a/ui/app/templates/components/text-file.hbs b/ui/app/templates/components/text-file.hbs deleted file mode 100644 index a3e392e6ab..0000000000 --- a/ui/app/templates/components/text-file.hbs +++ /dev/null @@ -1,100 +0,0 @@ -{{#unless this.inputOnly}} -
-
- -
-
-
- - -
-
-
-{{/unless}} -
- {{#if @file.enterAsText}} -
- - -
-

- {{this.textareaHelpText}} -

- {{else}} -
-
-
- - - - {{#if @file.fileName}} - {{@file.fileName}} - {{else}} - No file chosen - {{/if}} - - {{#if @file.fileName}} - - {{/if}} -
-
-
-

- {{this.fileHelpText}} -

- {{/if}} -
\ No newline at end of file diff --git a/ui/lib/core/addon/components/form-field.hbs b/ui/lib/core/addon/components/form-field.hbs index fcd6d78518..5d9ca3b91b 100644 --- a/ui/lib/core/addon/components/form-field.hbs +++ b/ui/lib/core/addon/components/form-field.hbs @@ -125,12 +125,14 @@ /> {{else if (eq @attr.options.editType "file")}} {{! File Input }} - +
+ +
{{else if (eq @attr.options.editType "ttl")}} {{! TTL Picker }}
@@ -302,7 +304,7 @@ value={{or (get @model this.valuePath) @attr.options.defaultValue}} onchange={{this.onChangeWithEvent}} onkeyup={{this.handleKeyUp}} - class="input {{if this.validationError 'has-error-border'}}" + class="input {{if this.validationError 'has-error-border'}} has-bottom-margin-m" maxLength={{@attr.options.characterLimit}} /> {{#if @attr.options.validationAttr}} diff --git a/ui/lib/core/addon/components/form-field.js b/ui/lib/core/addon/components/form-field.js index 5c2b5d47ee..9c6d5c1600 100644 --- a/ui/lib/core/addon/components/form-field.js +++ b/ui/lib/core/addon/components/form-field.js @@ -56,7 +56,6 @@ export default class FormFieldComponent extends Component { 'ttl', ]; @tracked showInput = false; - @tracked file = { value: '' }; // used by the pgp-file component when an attr is editType of 'file' constructor() { super(...arguments); @@ -116,12 +115,11 @@ export default class FormFieldComponent extends Component { } @action - setFile(_, keyFile) { + setFile(keyFile) { const path = this.valuePath; const { value } = keyFile; this.args.model.set(path, value); this.onChange(path, value); - this.file = keyFile; } @action setAndBroadcast(value) { diff --git a/ui/lib/core/addon/components/text-file.hbs b/ui/lib/core/addon/components/text-file.hbs new file mode 100644 index 0000000000..9da7942bed --- /dev/null +++ b/ui/lib/core/addon/components/text-file.hbs @@ -0,0 +1,95 @@ +{{#unless @uploadOnly}} +
+
+ +
+
+
+ + +
+
+
+{{/unless}} +
+ {{#if this.showTextArea}} +
+ + +
+

Enter the value as text

+ {{else}} +
+
+
+ + + + {{or this.filename "No file chosen"}} + + {{#if this.filename}} + + {{/if}} +
+
+
+

Select a file from your computer

+ {{#if (or @validationError this.uploadError)}} + + {{/if}} + {{/if}} +
\ No newline at end of file diff --git a/ui/lib/core/addon/components/text-file.js b/ui/lib/core/addon/components/text-file.js new file mode 100644 index 0000000000..9b06b31fc4 --- /dev/null +++ b/ui/lib/core/addon/components/text-file.js @@ -0,0 +1,69 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { guidFor } from '@ember/object/internals'; +/** + * @module TextFile + * `TextFile` components render a file upload input with the option to toggle a "Enter as text" button + * that changes the input into a textarea + * + * @example + * + * + * @param {function} onChange - Callback function to call when the value of the input changes, returns an object in the shape of { value: fileContents, filename: 'some-file.txt' } + * @param {bool} [uploadOnly=false] - When true, renders a static file upload input and removes the option to toggle and input plain text + * @param {string} [helpText] - Text underneath label. + * @param {string} [label='File'] - Text to use as the label for the file input. If none, default of 'File' is rendered + */ + +export default class TextFileComponent extends Component { + @tracked content = ''; + @tracked filename = ''; + @tracked uploadError = ''; + @tracked showValue = false; + @tracked showTextArea = false; + elementId = guidFor(this); + + async readFile(file) { + try { + this.content = await file.text(); + this.filename = file.name; + this.handleChange(); + } catch (error) { + this.clearFile(); + this.uploadError = 'There was a problem uploading. Please try again.'; + } + } + + @action + handleFileUpload(e) { + e.preventDefault(); + const { files } = e.target; + if (!files.length) return; + this.readFile(files[0]); + } + + @action + handleTextInput(e) { + e.preventDefault(); + this.content = e.target.value; + this.handleChange(); + } + + @action + clearFile() { + this.content = ''; + this.filename = ''; + this.handleChange(); + } + + handleChange() { + this.args.onChange({ value: this.content, filename: this.filename }); + this.uploadError = ''; + } +} diff --git a/ui/lib/core/addon/templates/components/icon.hbs b/ui/lib/core/addon/templates/components/icon.hbs index f61cc50d25..b8e3aea80e 100644 --- a/ui/lib/core/addon/templates/components/icon.hbs +++ b/ui/lib/core/addon/templates/components/icon.hbs @@ -1,5 +1,5 @@ {{#if this.isFlightIcon}} - + {{else}} {{svg-jar @name}} diff --git a/ui/lib/core/addon/templates/components/masked-input.hbs b/ui/lib/core/addon/templates/components/masked-input.hbs index 2d9a2ed253..0c546753a9 100644 --- a/ui/lib/core/addon/templates/components/masked-input.hbs +++ b/ui/lib/core/addon/templates/components/masked-input.hbs @@ -47,7 +47,7 @@ onclick={{action "toggleMask"}} type="button" class="{{if (eq this.value '') 'has-text-grey'}} masked-input-toggle button" - data-test-button + data-test-button="toggle-masked" > diff --git a/ui/lib/core/addon/templates/components/shamir-flow.hbs b/ui/lib/core/addon/templates/components/shamir-flow.hbs index 9ce99744a6..84632a4afd 100644 --- a/ui/lib/core/addon/templates/components/shamir-flow.hbs +++ b/ui/lib/core/addon/templates/components/shamir-flow.hbs @@ -107,7 +107,7 @@

PGP Key - {{this.pgpKeyFile.fileName}} + {{this.pgpKeyFile.filename}}

{{this.pgp_key}}
diff --git a/ui/lib/core/addon/templates/components/shamir-modal-flow.hbs b/ui/lib/core/addon/templates/components/shamir-modal-flow.hbs index a2f7ff0e1e..0690de15c0 100644 --- a/ui/lib/core/addon/templates/components/shamir-modal-flow.hbs +++ b/ui/lib/core/addon/templates/components/shamir-modal-flow.hbs @@ -129,7 +129,7 @@

PGP Key - {{this.pgpKeyFile.fileName}} + {{this.pgpKeyFile.filename}}

diff --git a/ui/lib/core/app/components/text-file.js b/ui/lib/core/app/components/text-file.js new file mode 100644 index 0000000000..c7237a7739 --- /dev/null +++ b/ui/lib/core/app/components/text-file.js @@ -0,0 +1 @@ +export { default } from 'core/components/text-file'; diff --git a/ui/tests/integration/components/form-field-test.js b/ui/tests/integration/components/form-field-test.js index d2a10f6289..af367d6091 100644 --- a/ui/tests/integration/components/form-field-test.js +++ b/ui/tests/integration/components/form-field-test.js @@ -105,7 +105,7 @@ module('Integration | Component | form field', function (hooks) { await click('[data-test-text-toggle]'); await fillIn('[data-test-text-file-textarea]', 'hello world'); assert.dom('[data-test-text-file-textarea]').hasClass('masked-font'); - await click('[data-test-button]'); + await click('[data-test-button="toggle-masked"]'); assert.dom('[data-test-text-file-textarea]').doesNotHaveClass('masked-font'); }); diff --git a/ui/tests/integration/components/policy-form-test.js b/ui/tests/integration/components/policy-form-test.js index d413064bd3..eec5368739 100644 --- a/ui/tests/integration/components/policy-form-test.js +++ b/ui/tests/integration/components/policy-form-test.js @@ -1,8 +1,8 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { click, fillIn, render } from '@ember/test-helpers'; +import { click, fillIn, render, triggerEvent } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; -import Sinon from 'sinon'; +import sinon from 'sinon'; import Pretender from 'pretender'; const SELECTORS = { @@ -21,8 +21,8 @@ module('Integration | Component | policy-form', function (hooks) { hooks.beforeEach(function () { this.store = this.owner.lookup('service:store'); this.model = this.store.createRecord('policy/acl'); - this.onSave = Sinon.spy(); - this.onCancel = Sinon.spy(); + this.onSave = sinon.spy(); + this.onCancel = sinon.spy(); this.server = new Pretender(function () { this.put('/v1/sys/policies/acl/bad-policy', () => { return [ @@ -44,15 +44,11 @@ module('Integration | Component | policy-form', function (hooks) { }); test('it renders the form for new ACL policy', async function (assert) { - const saveSpy = Sinon.spy(); - const model = this.store.createRecord('policy/acl'); const policy = ` path "secret/*" { capabilities = [ "create", "read", "update", "list" ] } `; - this.set('model', model); - this.set('onSave', saveSpy); await render(hbs` `); await click(SELECTORS.saveButton); - assert.ok(saveSpy.notCalled); + assert.ok(this.onSave.notCalled); assert.dom(SELECTORS.error).includesText('An error occurred'); }); }); diff --git a/ui/tests/integration/components/text-file-test.js b/ui/tests/integration/components/text-file-test.js new file mode 100644 index 0000000000..81616eb350 --- /dev/null +++ b/ui/tests/integration/components/text-file-test.js @@ -0,0 +1,113 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; +import { click, fillIn, render, triggerEvent } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import sinon from 'sinon'; + +const SELECTORS = { + label: '[data-test-text-file-label]', + toggle: '[data-test-text-toggle]', + textarea: '[data-test-text-file-textarea]', + fileUpload: '[data-test-text-file-input]', +}; +module('Integration | Component | text-file', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.label = 'Some label'; + this.onChange = sinon.spy(); + this.owner.lookup('service:flash-messages').registerTypes(['danger']); + }); + + test('it renders with label and toggle by default', async function (assert) { + await render(hbs``); + + assert.dom(SELECTORS.label).hasText('File', 'renders default label'); + assert.dom(SELECTORS.toggle).exists({ count: 1 }, 'toggle exists'); + assert.dom(SELECTORS.fileUpload).exists({ count: 1 }, 'File input shown'); + }); + + test('it renders without toggle and option for text input when uploadOnly=true', async function (assert) { + await render(hbs``); + + assert.dom(SELECTORS.label).doesNotExist('Label no longer rendered'); + assert.dom(SELECTORS.toggle).doesNotExist('toggle no longer rendered'); + assert.dom(SELECTORS.fileUpload).exists({ count: 1 }, 'File input shown'); + }); + + test('it toggles between upload and textarea', async function (assert) { + await render(hbs``); + + assert.dom(SELECTORS.fileUpload).exists({ count: 1 }, 'File input shown'); + assert.dom(SELECTORS.textarea).doesNotExist('Texarea hidden'); + await click(SELECTORS.toggle); + assert.dom(SELECTORS.textarea).exists({ count: 1 }, 'Textarea shown'); + assert.dom(SELECTORS.fileUpload).doesNotExist('File upload hidden'); + }); + + test('it correctly parses uploaded files', async function (assert) { + this.file = new File(['some content for a file'], 'filename.txt'); + await render(hbs``); + await triggerEvent(SELECTORS.fileUpload, 'change', { files: [this.file] }); + assert.propEqual( + this.onChange.lastCall.args[0], + { + filename: 'filename.txt', + value: 'some content for a file', + }, + 'parent callback function is called with correct arguments' + ); + }); + + test('it correctly submits text input', async function (assert) { + const PEM_BUNDLE = `-----BEGIN CERTIFICATE----- +MIIDGjCCAgKgAwIBAgIUFvnhb2nQ8+KNS3SzjlfYDMHGIRgwDQYJKoZIhvcNAQEL +BQAwDTELMAkGA1UEAxMCZmEwHhcNMTgwMTEwMTg1NDI5WhcNMTgwMjExMTg1NDU5 +WjANMQswCQYDVQQDEwJmYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN2VtBn6EMlA4aYre/xoKHxlgNDxJnfSQWfs6yF/K201qPnt4QF9AXChatbmcKVn +OaURq+XEJrGVgF/u2lSos3NRZdhWVe8o3/sOetsGxcrd0gXAieOSmkqJjp27bYdl +uY3WsxhyiPvdfS6xz39OehsK/YCB6qCzwB4eEfSKqbkvfDL9sLlAiOlaoHC9pczf +6/FANKp35UDwInSwmq5vxGbnWk9zMkh5Jq6hjOWHZnVc2J8J49PYvkIM8uiHDgOE +w71T2xM5plz6crmZnxPCOcTKIdF7NTEP2lUfiqc9lONV9X1Pi4UclLPHJf5bwTmn +JaWgbKeY+IlF61/mgxzhC7cCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFLDtc6+HZN2lv60JSDAZq3+IHoq7MB8GA1Ud +IwQYMBaAFLDtc6+HZN2lv60JSDAZq3+IHoq7MA0GA1UdEQQGMASCAmZhMA0GCSqG +SIb3DQEBCwUAA4IBAQDVt6OddTV1MB0UvF5v4zL1bEB9bgXvWx35v/FdS+VGn/QP +cC2c4ZNukndyHhysUEPdqVg4+up1aXm4eKXzNmGMY/ottN2pEhVEWQyoIIA1tH0e +8Kv/bysYpHZKZuoGg5+mdlHS2p2Dh2bmYFyBLJ8vaeP83NpTs2cNHcmEvWh/D4UN +UmYDODRN4qh9xYruKJ8i89iMGQfbdcq78dCC4JwBIx3bysC8oF4lqbTYoYNVTnAi +LVqvLdHycEOMlqV0ecq8uMLhPVBalCmIlKdWNQFpXB0TQCsn95rCCdi7ZTsYk5zv +Q4raFvQrZth3Cz/X5yPTtQL78oBYrmHzoQKDFJ2z +-----END CERTIFICATE-----`; + + await render(hbs``); + await click(SELECTORS.toggle); + await fillIn(SELECTORS.textarea, PEM_BUNDLE); + assert.propEqual( + this.onChange.lastCall.args[0], + { + filename: '', + value: PEM_BUNDLE, + }, + 'parent callback function is called with correct text area input' + ); + }); + + test('it throws an error when it cannot read the file', async function (assert) { + this.file = { foo: 'bar' }; + await render(hbs``); + + await triggerEvent(SELECTORS.fileUpload, 'change', { files: [this.file] }); + assert + .dom('[data-test-field-validation="text-file"]') + .hasText('There was a problem uploading. Please try again.'); + assert.propEqual( + this.onChange.lastCall.args[0], + { + filename: '', + value: '', + }, + 'parent callback function is called with cleared out values' + ); + }); +}); diff --git a/ui/tests/pages/components/masked-input.js b/ui/tests/pages/components/masked-input.js index 611073269a..8fca606684 100644 --- a/ui/tests/pages/components/masked-input.js +++ b/ui/tests/pages/components/masked-input.js @@ -3,5 +3,5 @@ import { clickable, isPresent } from 'ember-cli-page-object'; export default { textareaIsPresent: isPresent('[data-test-textarea]'), copyButtonIsPresent: isPresent('[data-test-copy-button]'), - toggleMasked: clickable('[data-test-button]'), + toggleMasked: clickable('[data-test-button="toggle-masked"]'), };