UI/obscure secret on input (#11284)
* new font and add as font-family to be used in masked-input * clean up logic * refactor for displayOnly * start cert masking * work on certificates * upload cert work * fix global styling * fix styling for class no longer used * make mask by default and remove option * glimmerize start and certificate on LDAP a file field * glimmerize actions * first part of glimmerizing text-file still need to do some clean up * not doing awesome over here * getting ready to un-glimmer * unglimmerize * remove placeholder based on conversations with design * clean up text-file * cleanup * fix class bindings * handle class binding * set up for test * fix elementId * track down index * update masked-input test * add more to the masked-input test * test-file test * fix broken test * clear old style * clean up * remove pgp key masked font, this really needs to be refactored to text-file component * changelog * cover other certificate view * add allowCopy * address some pr styling comments * improve test coverage * fix some issues * add attr.options.masked
This commit is contained in:
parent
29d91d09ff
commit
2e35e9578c
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
ui: Obscure secret values on input and displayOnly fields like certificates.
|
||||||
|
```
|
|
@ -1,84 +1,84 @@
|
||||||
import Component from '@ember/component';
|
import Component from '@glimmer/component';
|
||||||
import { set } from '@ember/object';
|
import { set } from '@ember/object';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { guidFor } from '@ember/object/internals';
|
||||||
|
|
||||||
export default Component.extend({
|
/**
|
||||||
'data-test-component': 'text-file',
|
* @module TextFile
|
||||||
classNames: ['box', 'is-fullwidth', 'is-marginless', 'is-shadowless'],
|
* `TextFile` components are file upload components where you can either toggle to upload a file or enter text.
|
||||||
classNameBindings: ['inputOnly:is-paddingless'],
|
*
|
||||||
|
* @example
|
||||||
|
* <TextFile
|
||||||
|
* @inputOnly={{true}}
|
||||||
|
* @helpText="help text"
|
||||||
|
* @file={{object}}
|
||||||
|
* @onChange={{action "someOnChangeFunction"}}
|
||||||
|
* @label={{"string"}}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* @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 {
|
||||||
* @public
|
fileHelpText = 'Select a file from your computer';
|
||||||
* @param Object
|
textareaHelpText = 'Enter the value as text';
|
||||||
* Object in the shape of:
|
elementId = guidFor(this);
|
||||||
* {
|
index = '';
|
||||||
* value: 'file contents here',
|
|
||||||
* fileName: 'nameOfFile.txt',
|
|
||||||
* enterAsText: bool
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
file: null,
|
|
||||||
|
|
||||||
index: null,
|
@tracked file = null;
|
||||||
onChange: () => {},
|
@tracked showValue = false;
|
||||||
|
|
||||||
/*
|
get inputOnly() {
|
||||||
* @public
|
return this.args.inputOnly || false;
|
||||||
* @param Boolean
|
}
|
||||||
* When true, only the file input will be rendered
|
get label() {
|
||||||
*/
|
return this.args.label || null;
|
||||||
inputOnly: false,
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* @public
|
|
||||||
* @param String
|
|
||||||
* Text to use as the label for the file input
|
|
||||||
* If null, a default will be rendered
|
|
||||||
*/
|
|
||||||
label: null,
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @public
|
|
||||||
* @param String
|
|
||||||
* Text to use as help under the file input
|
|
||||||
* If null, a default will be rendered
|
|
||||||
*/
|
|
||||||
fileHelpText: 'Select a file from your computer',
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @public
|
|
||||||
* @param String
|
|
||||||
* Text to use as help under the textarea in text-input mode
|
|
||||||
* If null, a default will be rendered
|
|
||||||
*/
|
|
||||||
textareaHelpText: 'Enter the value as text',
|
|
||||||
|
|
||||||
readFile(file) {
|
readFile(file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => this.setFile(reader.result, file.name);
|
reader.onload = () => this.setFile(reader.result, file.name);
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
},
|
}
|
||||||
|
|
||||||
setFile(contents, filename) {
|
setFile(contents, filename) {
|
||||||
this.onChange(this.index, { value: contents, fileName: filename });
|
this.args.onChange(this.index, { value: contents, fileName: filename });
|
||||||
},
|
}
|
||||||
|
|
||||||
actions: {
|
@action
|
||||||
pickedFile(e) {
|
pickedFile(e) {
|
||||||
const { files } = e.target;
|
e.preventDefault();
|
||||||
if (!files.length) {
|
const { files } = e.target;
|
||||||
return;
|
if (!files.length) {
|
||||||
}
|
return;
|
||||||
for (let i = 0, len = files.length; i < len; i++) {
|
}
|
||||||
this.readFile(files[i]);
|
for (let i = 0, len = files.length; i < len; i++) {
|
||||||
}
|
this.readFile(files[i]);
|
||||||
},
|
}
|
||||||
updateData(e) {
|
}
|
||||||
const file = this.file;
|
@action
|
||||||
set(file, 'value', e.target.value);
|
updateData(e) {
|
||||||
this.onChange(this.index, this.file);
|
e.preventDefault();
|
||||||
},
|
let file = this.args.file;
|
||||||
clearFile() {
|
set(file, 'value', e.target.value);
|
||||||
this.onChange(this.index, { value: '' });
|
this.args.onChange(this.index, file);
|
||||||
},
|
}
|
||||||
},
|
@action
|
||||||
});
|
clearFile() {
|
||||||
|
this.args.onChange(this.index, { value: '' });
|
||||||
|
}
|
||||||
|
@action
|
||||||
|
toggleMask() {
|
||||||
|
this.showValue = !this.showValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default AuthConfig.extend({
|
||||||
useOpenAPI: true,
|
useOpenAPI: true,
|
||||||
certificate: attr({
|
certificate: attr({
|
||||||
label: 'Certificate',
|
label: 'Certificate',
|
||||||
editType: 'textarea',
|
editType: 'file',
|
||||||
}),
|
}),
|
||||||
fieldGroups: computed('newFields', function() {
|
fieldGroups: computed('newFields', function() {
|
||||||
let groups = [
|
let groups = [
|
||||||
|
|
|
@ -143,6 +143,7 @@ export default Certificate.extend({
|
||||||
csr: attr('string', {
|
csr: attr('string', {
|
||||||
editType: 'textarea',
|
editType: 'textarea',
|
||||||
label: 'CSR',
|
label: 'CSR',
|
||||||
|
masked: true,
|
||||||
}),
|
}),
|
||||||
expiration: attr(),
|
expiration: attr(),
|
||||||
|
|
||||||
|
|
|
@ -63,14 +63,20 @@ export default Model.extend({
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
certificate: attr('string'),
|
certificate: attr('string', {
|
||||||
|
masked: true,
|
||||||
|
}),
|
||||||
issuingCa: attr('string', {
|
issuingCa: attr('string', {
|
||||||
label: 'Issuing CA',
|
label: 'Issuing CA',
|
||||||
|
masked: true,
|
||||||
}),
|
}),
|
||||||
caChain: attr('string', {
|
caChain: attr('string', {
|
||||||
label: 'CA chain',
|
label: 'CA chain',
|
||||||
|
masked: true,
|
||||||
|
}),
|
||||||
|
privateKey: attr('string', {
|
||||||
|
masked: true,
|
||||||
}),
|
}),
|
||||||
privateKey: attr('string'),
|
|
||||||
privateKeyType: attr('string'),
|
privateKeyType: attr('string'),
|
||||||
serialNumber: attr('string'),
|
serialNumber: attr('string'),
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
@import 'ember-basic-dropdown';
|
@import 'ember-basic-dropdown';
|
||||||
@import 'ember-power-select';
|
@import 'ember-power-select';
|
||||||
@import './core';
|
@import './core';
|
||||||
|
|
||||||
|
@mixin font-face($name) {
|
||||||
|
@font-face {
|
||||||
|
font-family: $name;
|
||||||
|
src: url('/ui/fonts/#{$name}.woff2') format('woff2'), url('/ui/fonts/#{$name}.woff') format('woff');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include font-face('obscure');
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
|
.masked-font {
|
||||||
|
color: $ui-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
.masked-input {
|
.masked-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.masked-input.masked.display-only,
|
|
||||||
.masked-input:not(.masked) {
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-label .masked-input {
|
.has-label .masked-input {
|
||||||
padding-top: $spacing-s;
|
padding-top: $spacing-s;
|
||||||
}
|
}
|
||||||
|
@ -91,10 +90,6 @@
|
||||||
color: $grey-light;
|
color: $grey-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
.masked-input:not(.masked) .masked-input-toggle {
|
|
||||||
color: $blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.masked-input .input:focus + .masked-input-toggle {
|
.masked-input .input:focus + .masked-input-toggle {
|
||||||
background: rgba($white, 0.95);
|
background: rgba($white, 0.95);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
.text-file {
|
||||||
|
.has-icon-right {
|
||||||
|
display: flex;
|
||||||
|
width: 97%;
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.button.masked-input-toggle,
|
||||||
|
.button.copy-button {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
|
@ -99,6 +99,7 @@
|
||||||
@import './components/splash-page';
|
@import './components/splash-page';
|
||||||
@import './components/status-menu';
|
@import './components/status-menu';
|
||||||
@import './components/tabs';
|
@import './components/tabs';
|
||||||
|
@import './components/text-file';
|
||||||
@import './components/token-expire-warning';
|
@import './components/token-expire-warning';
|
||||||
@import './components/toolbar';
|
@import './components/toolbar';
|
||||||
@import './components/tool-tip';
|
@import './components/tool-tip';
|
||||||
|
|
|
@ -11,6 +11,13 @@ label {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.masked-font {
|
||||||
|
font-family: obscure;
|
||||||
|
font-size: $size-medium;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: $ui-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: $grey-darker;
|
color: $grey-darker;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
|
@ -9,7 +9,27 @@
|
||||||
</h2>
|
</h2>
|
||||||
{{#if (or model.certificate model.csr)}}
|
{{#if (or model.certificate model.csr)}}
|
||||||
{{#each model.attrs as |attr|}}
|
{{#each model.attrs as |attr|}}
|
||||||
{{info-table-row data-test-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
|
{{#if attr.options.masked}}
|
||||||
|
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
|
||||||
|
<MaskedInput
|
||||||
|
@value={{get model attr.name}}
|
||||||
|
@displayOnly={{true}}
|
||||||
|
@allowCopy={{true}}
|
||||||
|
/>
|
||||||
|
</InfoTableRow>
|
||||||
|
{{else if (eq attr.name "expiration")}}
|
||||||
|
{{info-table-row
|
||||||
|
data-test-table-row
|
||||||
|
label=(capitalize (or attr.options.label (humanize (dasherize attr.name))))
|
||||||
|
value=(date-format (get model attr.name) 'MMM dd, yyyy hh:mm:ss a')
|
||||||
|
}}
|
||||||
|
{{else}}
|
||||||
|
{{info-table-row
|
||||||
|
data-test-table-row
|
||||||
|
label=(capitalize (or attr.options.label (humanize (dasherize attr.name))))
|
||||||
|
value=(get model attr.name)
|
||||||
|
}}
|
||||||
|
{{/if}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
@ -70,7 +90,17 @@
|
||||||
data-test-warning
|
data-test-warning
|
||||||
/>
|
/>
|
||||||
{{#each model.attrs as |attr|}}
|
{{#each model.attrs as |attr|}}
|
||||||
{{info-table-row data-test-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
|
{{#if attr.options.masked}}
|
||||||
|
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
|
||||||
|
<MaskedInput
|
||||||
|
@value={{get model attr.name}}
|
||||||
|
@displayOnly={{true}}
|
||||||
|
@allowCopy={{true}}
|
||||||
|
/>
|
||||||
|
</InfoTableRow>
|
||||||
|
{{else}}
|
||||||
|
{{info-table-row data-test-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
|
||||||
|
{{/if}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|
|
@ -49,12 +49,14 @@
|
||||||
(eq attr.name "secretKey")
|
(eq attr.name "secretKey")
|
||||||
(eq attr.name "securityToken")
|
(eq attr.name "securityToken")
|
||||||
(eq attr.name "privateKey")
|
(eq attr.name "privateKey")
|
||||||
|
attr.options.masked
|
||||||
)}}
|
)}}
|
||||||
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
|
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
|
||||||
<MaskedInput
|
<MaskedInput
|
||||||
@value={{get model attr.name}}
|
@value={{get model attr.name}}
|
||||||
@name={{attr.name}}
|
@name={{attr.name}}
|
||||||
@displayOnly={{true}}
|
@displayOnly={{true}}
|
||||||
|
@allowCopy={{true}}
|
||||||
/>
|
/>
|
||||||
</InfoTableRow>
|
</InfoTableRow>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -15,7 +15,17 @@
|
||||||
{{#if (eq attr.type "object")}}
|
{{#if (eq attr.type "object")}}
|
||||||
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{stringify (get model attr.name)}} />
|
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{stringify (get model attr.name)}} />
|
||||||
{{else}}
|
{{else}}
|
||||||
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}} />
|
{{#if attr.options.masked}}
|
||||||
|
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
|
||||||
|
<MaskedInput
|
||||||
|
@value={{get model attr.name}}
|
||||||
|
@displayOnly={{true}}
|
||||||
|
@allowCopy={{true}}
|
||||||
|
/>
|
||||||
|
</InfoTableRow>
|
||||||
|
{{else}}
|
||||||
|
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}} />
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
{{#unless inputOnly}}
|
{{#unless this.inputOnly}}
|
||||||
<div class="level is-mobile">
|
<div class="level is-mobile">
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<label class="is-label" data-test-text-label=true>
|
<label class="is-label" data-test-text-label=true>
|
||||||
{{#if label}}
|
{{#if this.label}}
|
||||||
{{label}}
|
{{this.label}}
|
||||||
{{#if helpText}}
|
{{#if @helpText}}
|
||||||
<InfoTooltip>
|
<InfoTooltip>
|
||||||
<span data-test-help-text>
|
<span data-test-help-text>
|
||||||
{{helpText}}
|
{{@helpText}}
|
||||||
</span>
|
</span>
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -20,50 +20,57 @@
|
||||||
<div class="control is-flex">
|
<div class="control is-flex">
|
||||||
<input
|
<input
|
||||||
data-test-text-toggle=true
|
data-test-text-toggle=true
|
||||||
id={{concat "useText-" elementId}}
|
id={{concat "useText-" this.elementId}}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name={{concat "useText-" elementId}}
|
name={{concat "useText-" this.elementId}}
|
||||||
class="switch is-rounded is-success is-small"
|
class="switch is-rounded is-success is-small"
|
||||||
checked={{file.enterAsText}}
|
checked={{@file.enterAsText}}
|
||||||
onchange={{action (toggle "enterAsText" file)}}
|
{{on 'change' (toggle "enterAsText" @file)}}
|
||||||
/>
|
/>
|
||||||
<label for={{concat "useText-" elementId}}>
|
<label for={{concat "useText-" this.elementId}}>
|
||||||
Enter as text
|
Enter as text
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
<div class="field">
|
<div class="field text-file box is-fullwidth is-marginless is-shadowless {{if this.inputOnly "is-paddingless"}}" data-test-component="text-file">
|
||||||
{{#if file.enterAsText}}
|
{{#if @file.enterAsText}}
|
||||||
<div class="control">
|
<div class="control has-icon-right">
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea"
|
class="textarea {{unless showValue "masked-font"}}"
|
||||||
oninput={{action "updateData"}}
|
{{on 'input' this.updateData}}
|
||||||
data-test-text-file-textarea=true
|
data-test-text-file-textarea=true
|
||||||
>{{file.value}}</textarea>
|
>{{@file.value}}</textarea>
|
||||||
|
<button
|
||||||
|
{{on 'click' this.toggleMask}}
|
||||||
|
type="button"
|
||||||
|
class="{{if (eq value "") "has-text-grey"}} masked-input-toggle button {{if displayOnly "is-compact"}}"
|
||||||
|
data-test-button>
|
||||||
|
<Icon @glyph={{if showValue "visibility-show" "visibility-hide"}} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="help has-text-grey">
|
<p class="help has-text-grey">
|
||||||
{{textareaHelpText}}
|
{{this.textareaHelpText}}
|
||||||
</p>
|
</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
<div class="file has-name is-fullwidth">
|
<div class="file has-name is-fullwidth">
|
||||||
<label class="file-label">
|
<label class="file-label">
|
||||||
<input class="file-input" type="file" onchange={{action "pickedFile"}} data-test-text-file-input=true>
|
<input class="file-input" type="file" {{on 'change' this.pickedFile}} data-test-text-file-input=true>
|
||||||
<span class="file-cta button">
|
<span class="file-cta button">
|
||||||
<Icon @glyph="upload" class="has-light-grey-text" />
|
<Icon @glyph="upload" class="has-light-grey-text" />
|
||||||
Choose a file…
|
Choose a file…
|
||||||
</span>
|
</span>
|
||||||
<span class="file-name has-text-grey-dark" data-test-text-file-input-label=true>
|
<span class="file-name has-text-grey-dark" data-test-text-file-input-label=true>
|
||||||
{{#if file.fileName}}
|
{{#if @file.fileName}}
|
||||||
{{file.fileName}}
|
{{@file.fileName}}
|
||||||
{{else}}
|
{{else}}
|
||||||
No file chosen
|
No file chosen
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</span>
|
</span>
|
||||||
{{#if file.fileName}}
|
{{#if @file.fileName}}
|
||||||
<button type="button" class="file-delete-button" {{action 'clearFile'}} data-test-text-clear=true>
|
<button type="button" class="file-delete-button" {{on 'click' this.clearFile}} data-test-text-clear=true>
|
||||||
<Icon @glyph="cancel-circle-outline" />
|
<Icon @glyph="cancel-circle-outline" />
|
||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -71,7 +78,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help has-text-grey">
|
<p class="help has-text-grey">
|
||||||
{{fileHelpText}}
|
{{this.fileHelpText}}
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,13 +5,20 @@
|
||||||
Public key
|
Public key
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<Textarea @name="publicKey" @id="publicKey" class="textarea" @value={{model.publicKey}} @readonly={{true}} data-test-ssh-input="public-key" />
|
<MaskedInput
|
||||||
|
@name="publickey"
|
||||||
|
@id="publicKey"
|
||||||
|
@value={{model.publicKey}}
|
||||||
|
@displayOnly={{true}}
|
||||||
|
@allowCopy={{true}}
|
||||||
|
data-test-ssh-input="public-key"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<CopyButton @clipboardTarget="#publicKey" @class="button is-primary" @buttonType="button" @success={{action (set-flash-message "Public Key copied!")}}>
|
<CopyButton @clipboardText={{model.publicKey}} @class="button is-primary" @buttonType="button" @success={{action (set-flash-message "Public Key copied!")}}>
|
||||||
Copy
|
Copy
|
||||||
</CopyButton>
|
</CopyButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{#if showFileUpload}}
|
{{#if showFileUpload}}
|
||||||
<TextFile @inputOnly={{true}} @index="" @file={{file}} @onChange={{action "setPolicyFromFile"}} />
|
<TextFile @inputOnly={{true}} @file={{file}} @onChange={{action "setPolicyFromFile"}} />
|
||||||
{{else}}
|
{{else}}
|
||||||
<IvyCodemirror @value={{model.policy}} @id="policy" @valueUpdated={{action (mut model.policy)}} @options={{hash
|
<IvyCodemirror @value={{model.policy}} @id="policy" @valueUpdated={{action (mut model.policy)}} @options={{hash
|
||||||
lineNumbers=true
|
lineNumbers=true
|
||||||
|
|
|
@ -67,6 +67,7 @@ module.exports = function(environment) {
|
||||||
ENV.contentSecurityPolicy = {
|
ENV.contentSecurityPolicy = {
|
||||||
'connect-src': ["'self'"],
|
'connect-src': ["'self'"],
|
||||||
'img-src': ["'self'", 'data:'],
|
'img-src': ["'self'", 'data:'],
|
||||||
|
'font-src': ["'self'"],
|
||||||
'form-action': ["'none'"],
|
'form-action': ["'none'"],
|
||||||
'script-src': ["'self'"],
|
'script-src': ["'self'"],
|
||||||
'style-src': ["'unsafe-inline'", "'self'"],
|
'style-src': ["'unsafe-inline'", "'self'"],
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
import { computed } from '@ember/object';
|
|
||||||
import autosize from 'autosize';
|
import autosize from 'autosize';
|
||||||
import layout from '../templates/components/masked-input';
|
import layout from '../templates/components/masked-input';
|
||||||
|
|
||||||
|
@ -10,24 +9,21 @@ import layout from '../templates/components/masked-input';
|
||||||
* @example
|
* @example
|
||||||
* <MaskedInput
|
* <MaskedInput
|
||||||
* @value={{attr.options.defaultValue}}
|
* @value={{attr.options.defaultValue}}
|
||||||
* @placeholder="secret"
|
|
||||||
* @allowCopy={{true}}
|
* @allowCopy={{true}}
|
||||||
* @onChange={{action "someAction"}}
|
* @onChange={{action "someAction"}}
|
||||||
* />
|
* />
|
||||||
*
|
*
|
||||||
* @param [value] {String} - The value to display in the input.
|
* @param [value] {String} - The value to display in the input.
|
||||||
* @param [placeholder=value] {String} - The placeholder to display before the user has entered any input.
|
|
||||||
* @param [allowCopy=null] {bool} - Whether or not the input should render with a copy button.
|
* @param [allowCopy=null] {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 [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 [onChange=Function.prototype] {Function|action} - A function to call when the value of the input changes.
|
||||||
*
|
* @param [isCertificate=false] {bool} - If certificate display the label and icons differently.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
layout,
|
layout,
|
||||||
value: null,
|
value: null,
|
||||||
placeholder: 'value',
|
showValue: false,
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
autosize(this.element.querySelector('textarea'));
|
autosize(this.element.querySelector('textarea'));
|
||||||
|
@ -40,30 +36,12 @@ export default Component.extend({
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
autosize.destroy(this.element.querySelector('textarea'));
|
autosize.destroy(this.element.querySelector('textarea'));
|
||||||
},
|
},
|
||||||
shouldObscure: computed('isMasked', 'isFocused', 'value', function() {
|
|
||||||
if (this.value === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (this.isFocused === true) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.isMasked;
|
|
||||||
}),
|
|
||||||
displayValue: computed('shouldObscure', 'value', function() {
|
|
||||||
if (this.shouldObscure) {
|
|
||||||
return '■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■';
|
|
||||||
} else {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
isMasked: true,
|
|
||||||
isFocused: false,
|
|
||||||
displayOnly: false,
|
displayOnly: false,
|
||||||
onKeyDown() {},
|
onKeyDown() {},
|
||||||
onChange() {},
|
onChange() {},
|
||||||
actions: {
|
actions: {
|
||||||
toggleMask() {
|
toggleMask() {
|
||||||
this.toggleProperty('isMasked');
|
this.toggleProperty('showValue');
|
||||||
},
|
},
|
||||||
updateValue(e) {
|
updateValue(e) {
|
||||||
let value = e.target.value;
|
let value = e.target.value;
|
||||||
|
|
|
@ -4,6 +4,12 @@ import { format, parseISO } from 'date-fns';
|
||||||
export function dateFormat([date, style]) {
|
export function dateFormat([date, style]) {
|
||||||
// see format breaking in upgrade to date-fns 2.x https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md#changed-5
|
// see format breaking in upgrade to date-fns 2.x https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md#changed-5
|
||||||
let number = typeof date === 'string' ? parseISO(date) : date;
|
let number = typeof date === 'string' ? parseISO(date) : date;
|
||||||
|
if (!number) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (number.toString().length === 10) {
|
||||||
|
number = new Date(number * 1000);
|
||||||
|
}
|
||||||
return format(number, style);
|
return format(number, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -110,12 +110,9 @@
|
||||||
{{else if (eq attr.options.editType "file")}}
|
{{else if (eq attr.options.editType "file")}}
|
||||||
{{!-- File Input --}}
|
{{!-- File Input --}}
|
||||||
{{text-file
|
{{text-file
|
||||||
index=""
|
helpText=attr.options.helpText
|
||||||
fileHelpText=attr.options.helpText
|
|
||||||
textareaHelpText=attr.options.helpText
|
|
||||||
file=file
|
file=file
|
||||||
onChange=(action "setFile")
|
onChange=(action "setFile")
|
||||||
warning=attr.options.warning
|
|
||||||
label=labelString
|
label=labelString
|
||||||
}}
|
}}
|
||||||
{{else if (eq attr.options.editType "ttl")}}
|
{{else if (eq attr.options.editType "ttl")}}
|
||||||
|
@ -175,8 +172,11 @@
|
||||||
}}
|
}}
|
||||||
{{else if (eq attr.options.sensitive true)}}
|
{{else if (eq attr.options.sensitive true)}}
|
||||||
{{!-- Masked Input --}}
|
{{!-- Masked Input --}}
|
||||||
<MaskedInput @value={{or (get model valuePath) attr.options.defaultValue}} @placeholder="" @allowCopy="true"
|
<MaskedInput
|
||||||
@onChange={{action (action "setAndBroadcast" valuePath)}} @maskWhileTyping={{if (eq attr.name "bindpass") true}}/>
|
@value={{or (get model valuePath) attr.options.defaultValue}}
|
||||||
|
@allowCopy="true"
|
||||||
|
@onChange={{action (action "setAndBroadcast" valuePath)}}
|
||||||
|
/>
|
||||||
{{else if (or (eq attr.type "number") (eq attr.type "string"))}}
|
{{else if (or (eq attr.type "number") (eq attr.type "string"))}}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{{#if (eq attr.options.editType "textarea")}}
|
{{#if (eq attr.options.editType "textarea")}}
|
||||||
|
|
|
@ -1,35 +1,41 @@
|
||||||
<div class="masked-input {{if shouldObscure "masked"}} {{if displayOnly "display-only"}} {{if allowCopy "allow-copy"}}"
|
|
||||||
|
<div class="masked-input {{if displayOnly "display-only"}} {{if allowCopy "allow-copy"}}"
|
||||||
data-test-masked-input data-test-field>
|
data-test-masked-input data-test-field>
|
||||||
{{#if displayOnly}}
|
{{#if displayOnly}}
|
||||||
<pre class="masked-value display-only is-word-break">{{displayValue}}</pre>
|
<pre class="masked-value display-only is-word-break {{unless showValue "masked-font"}}">{{unless showValue (truncate value 20) value}}</pre>
|
||||||
{{else if maskWhileTyping}}
|
{{else if inputField}}
|
||||||
<input
|
<input
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
type={{if isMasked "password" "text"}}
|
|
||||||
value={{value}}
|
value={{value}}
|
||||||
|
class="input {{unless showValue "masked-font"}}"
|
||||||
onchange={{action "updateValue"}}
|
onchange={{action "updateValue"}}
|
||||||
class="input"
|
|
||||||
data-test-input
|
data-test-input
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<textarea class="input masked-value" rows=1 wrap="off" placeholder={{placeholder}}
|
<textarea
|
||||||
onfocus={{action (mut isFocused) true}} onblur={{action (mut isFocused) false}} onkeydown={{action onKeyDown}}
|
class="input masked-value {{unless showValue "masked-font"}}"
|
||||||
onchange={{action "updateValue"}} value={{readonly displayValue}} data-test-textarea />
|
rows=1 wrap="off"
|
||||||
|
onkeydown={{action onKeyDown}}
|
||||||
|
onchange={{action "updateValue"}}
|
||||||
|
value={{value}}
|
||||||
|
data-test-textarea
|
||||||
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if allowCopy}}
|
{{#if allowCopy}}
|
||||||
<CopyButton
|
<CopyButton
|
||||||
@clipboardText={{value}}
|
@clipboardText={{value}}
|
||||||
@success={{action (set-flash-message 'Data copied!')}}
|
@success={{action (set-flash-message 'Data copied!')}}
|
||||||
class="copy-button button {{if displayOnly "is-compact"}}"
|
class="copy-button button {{if displayOnly "is-compact"}}"
|
||||||
data-test-copy-button>
|
data-test-copy-button>
|
||||||
<Icon @glyph="copy-action" aria-hidden="Copy value" />
|
<Icon @glyph="copy-action" aria-hidden="Copy value" />
|
||||||
</CopyButton>
|
</CopyButton>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<button {{action "toggleMask"}}
|
<button
|
||||||
|
onclick={{action "toggleMask"}}
|
||||||
type="button"
|
type="button"
|
||||||
class="{{if (eq value "") "has-text-grey"}} masked-input-toggle button {{if displayOnly "is-compact"}}"
|
class="{{if (eq value "") "has-text-grey"}} masked-input-toggle button"
|
||||||
data-test-button>
|
data-test-button>
|
||||||
<Icon @glyph={{if shouldObscure "visibility-hide" "visibility-show"}} aria-hidden="true" />
|
<Icon @glyph={{if showValue "visibility-show" "visibility-hide"}} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,10 +19,13 @@
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{{#if model}}
|
{{#if model}}
|
||||||
<FieldGroupShow @model={{model}} />
|
<FieldGroupShow @model={{model}} />
|
||||||
<InfoTableRow
|
<InfoTableRow @label="CA PEM" @value={{model.ca.caPem}}>
|
||||||
@label="CA PEM"
|
<MaskedInput
|
||||||
@value={{model.ca.caPem}}
|
@value={{model.ca.caPem}}
|
||||||
/>
|
@displayOnly={{true}}
|
||||||
|
@allowCopy={{true}}
|
||||||
|
/>
|
||||||
|
</InfoTableRow>
|
||||||
{{else}}
|
{{else}}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@title="No configuration for this secrets engine"
|
@title="No configuration for this secrets engine"
|
||||||
|
|
|
@ -51,10 +51,13 @@
|
||||||
</ToolbarActions>
|
</ToolbarActions>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<div class="box is-shadowless is-fullwidth is-sideless">
|
<div class="box is-shadowless is-fullwidth is-sideless">
|
||||||
<InfoTableRow
|
<InfoTableRow @label="Serial number" @value={{model.id}}>
|
||||||
@label="Serial number"
|
<MaskedInput
|
||||||
@value={{model.id}}
|
@value={{model.id}}
|
||||||
/>
|
@displayOnly={{true}}
|
||||||
|
@allowCopy={{true}}
|
||||||
|
/>
|
||||||
|
</InfoTableRow>
|
||||||
<InfoTableRow
|
<InfoTableRow
|
||||||
@label="Private key"
|
@label="Private key"
|
||||||
@value={{model.privateKey}}
|
@value={{model.privateKey}}
|
||||||
|
@ -74,17 +77,24 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</InfoTableRow>
|
</InfoTableRow>
|
||||||
<InfoTableRow
|
<InfoTableRow @label="Certificate" @value={{model.certificate}}>
|
||||||
@label="Certificate"
|
<MaskedInput
|
||||||
@value={{model.certificate}}
|
@value={{model.certificate}}
|
||||||
/>
|
@displayOnly={{true}}
|
||||||
|
@allowCopy={{true}}
|
||||||
|
/>
|
||||||
|
</InfoTableRow>
|
||||||
<InfoTableRow
|
<InfoTableRow
|
||||||
@label="CA Chain"
|
@label="CA Chain"
|
||||||
@value={{model.caChain}}
|
@value={{model.caChain}}
|
||||||
>
|
>
|
||||||
<div class="is-block">
|
<div class="is-block">
|
||||||
{{#each model.caChain as |chain|}}
|
{{#each model.caChain as |chain|}}
|
||||||
<code class="is-block is-word-break has-text-black">{{chain}}</code>
|
<MaskedInput
|
||||||
|
@value={{chain}}
|
||||||
|
@displayOnly={{true}}
|
||||||
|
@allowCopy={{true}}
|
||||||
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</InfoTableRow>
|
</InfoTableRow>
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,10 +1,9 @@
|
||||||
import { currentRouteName, settled } from '@ember/test-helpers';
|
import { currentRouteName, settled, click } from '@ember/test-helpers';
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
import editPage from 'vault/tests/pages/secrets/backend/pki/edit-role';
|
import editPage from 'vault/tests/pages/secrets/backend/pki/edit-role';
|
||||||
import listPage from 'vault/tests/pages/secrets/backend/list';
|
import listPage from 'vault/tests/pages/secrets/backend/list';
|
||||||
import generatePage from 'vault/tests/pages/secrets/backend/pki/generate-cert';
|
import generatePage from 'vault/tests/pages/secrets/backend/pki/generate-cert';
|
||||||
import showPage from 'vault/tests/pages/secrets/backend/pki/show';
|
|
||||||
import configPage from 'vault/tests/pages/settings/configure-secret-backends/pki/section-cert';
|
import configPage from 'vault/tests/pages/settings/configure-secret-backends/pki/section-cert';
|
||||||
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
|
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
|
||||||
import authPage from 'vault/tests/pages/auth';
|
import authPage from 'vault/tests/pages/auth';
|
||||||
|
@ -56,7 +55,11 @@ elRplAzrMF4=
|
||||||
await settled();
|
await settled();
|
||||||
await generatePage.issueCert('foo');
|
await generatePage.issueCert('foo');
|
||||||
await settled();
|
await settled();
|
||||||
assert.ok(generatePage.hasCert, 'displays the cert');
|
let countMaskedFonts = document.querySelectorAll('.masked-font').length;
|
||||||
|
assert.equal(countMaskedFonts, 3); // certificate, issuing ca, and private key
|
||||||
|
let firstUnMaskButton = document.querySelectorAll('.masked-input-toggle')[0];
|
||||||
|
await click(firstUnMaskButton);
|
||||||
|
assert.dom('.masked-value').hasTextContaining('-----BEGIN CERTIFICATE-----');
|
||||||
await settled();
|
await settled();
|
||||||
await generatePage.back();
|
await generatePage.back();
|
||||||
await settled();
|
await settled();
|
||||||
|
@ -68,7 +71,9 @@ elRplAzrMF4=
|
||||||
await settled();
|
await settled();
|
||||||
await generatePage.sign('common', CSR);
|
await generatePage.sign('common', CSR);
|
||||||
await settled();
|
await settled();
|
||||||
assert.ok(generatePage.hasCert, 'displays the cert');
|
let firstUnMaskButton = document.querySelectorAll('.masked-input-toggle')[0];
|
||||||
|
await click(firstUnMaskButton);
|
||||||
|
assert.dom('.masked-value').hasTextContaining('-----BEGIN CERTIFICATE-----');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it views a cert', async function(assert) {
|
test('it views a cert', async function(assert) {
|
||||||
|
@ -82,6 +87,8 @@ elRplAzrMF4=
|
||||||
await listPage.secrets.objectAt(0).click();
|
await listPage.secrets.objectAt(0).click();
|
||||||
await settled();
|
await settled();
|
||||||
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'navigates to the show page');
|
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'navigates to the show page');
|
||||||
assert.ok(showPage.hasCert, 'shows the cert');
|
let firstUnMaskButton = document.querySelectorAll('.masked-input-toggle')[0];
|
||||||
|
await click(firstUnMaskButton);
|
||||||
|
assert.dom('.masked-value').hasTextContaining('-----BEGIN CERTIFICATE-----');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { currentRouteName, settled } from '@ember/test-helpers';
|
import { currentRouteName, settled, click } from '@ember/test-helpers';
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
import page from 'vault/tests/pages/settings/configure-secret-backends/pki/section-cert';
|
import page from 'vault/tests/pages/settings/configure-secret-backends/pki/section-cert';
|
||||||
|
@ -112,7 +112,8 @@ BXUV2Uwtxf+QCphnlht9muX2fsLIzDJea0JipWj1uf2H8OZsjE8=
|
||||||
await page.form.generateCA('Intermediate CA', 'intermediate');
|
await page.form.generateCA('Intermediate CA', 'intermediate');
|
||||||
await settled();
|
await settled();
|
||||||
// cache csr
|
// cache csr
|
||||||
csrVal = page.form.csr;
|
await click('.masked-input-toggle');
|
||||||
|
csrVal = document.querySelector('.masked-value').innerText;
|
||||||
await page.form.back();
|
await page.form.back();
|
||||||
await settled();
|
await settled();
|
||||||
await page.visit({ backend: rootPath });
|
await page.visit({ backend: rootPath });
|
||||||
|
@ -121,7 +122,8 @@ BXUV2Uwtxf+QCphnlht9muX2fsLIzDJea0JipWj1uf2H8OZsjE8=
|
||||||
await settled();
|
await settled();
|
||||||
await page.form.csrField(csrVal).submit();
|
await page.form.csrField(csrVal).submit();
|
||||||
await settled();
|
await settled();
|
||||||
intermediateCert = page.form.certificate;
|
await click('.masked-input-toggle');
|
||||||
|
intermediateCert = document.querySelector('[data-test-masked-input]').innerText;
|
||||||
await page.form.back();
|
await page.form.back();
|
||||||
await settled();
|
await settled();
|
||||||
await page.visit({ backend: intermediatePath });
|
await page.visit({ backend: intermediatePath });
|
||||||
|
|
|
@ -28,7 +28,7 @@ const setupWrapping = async () => {
|
||||||
module('Acceptance | wrapped_token query param functionality', function(hooks) {
|
module('Acceptance | wrapped_token query param functionality', function(hooks) {
|
||||||
setupApplicationTest(hooks);
|
setupApplicationTest(hooks);
|
||||||
|
|
||||||
test('it authenticates you if the query param is present meep', async function(assert) {
|
test('it authenticates you if the query param is present', async function(assert) {
|
||||||
let token = await setupWrapping();
|
let token = await setupWrapping();
|
||||||
await auth.visit({ wrapped_token: token });
|
await auth.visit({ wrapped_token: token });
|
||||||
await settled();
|
await settled();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import EmberObject from '@ember/object';
|
import EmberObject from '@ember/object';
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupRenderingTest } from 'ember-qunit';
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
import { render } from '@ember/test-helpers';
|
import { render, click, fillIn } from '@ember/test-helpers';
|
||||||
import hbs from 'htmlbars-inline-precompile';
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
import { create } from 'ember-cli-page-object';
|
import { create } from 'ember-cli-page-object';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
@ -102,6 +102,11 @@ module('Integration | Component | form field', function(hooks) {
|
||||||
test('it renders: editType file', async function(assert) {
|
test('it renders: editType file', async function(assert) {
|
||||||
await setup.call(this, createAttr('foo', 'string', { editType: 'file' }));
|
await setup.call(this, createAttr('foo', 'string', { editType: 'file' }));
|
||||||
assert.ok(component.hasTextFile, 'renders the text-file component');
|
assert.ok(component.hasTextFile, 'renders the text-file component');
|
||||||
|
await click('[data-test-text-toggle="true"]');
|
||||||
|
await fillIn('[data-test-text-file-textarea="true"]', 'hello world');
|
||||||
|
assert.dom('[data-test-text-file-textarea="true"]').hasClass('masked-font');
|
||||||
|
await click('[data-test-button]');
|
||||||
|
assert.dom('[data-test-text-file-textarea="true"]').doesNotHaveClass('masked-font');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders: editType ttl', async function(assert) {
|
test('it renders: editType ttl', async function(assert) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupRenderingTest } from 'ember-qunit';
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
import { render } from '@ember/test-helpers';
|
import { render, focus } from '@ember/test-helpers';
|
||||||
import { create } from 'ember-cli-page-object';
|
import { create } from 'ember-cli-page-object';
|
||||||
import hbs from 'htmlbars-inline-precompile';
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
import maskedInput from 'vault/tests/pages/components/masked-input';
|
import maskedInput from 'vault/tests/pages/components/masked-input';
|
||||||
|
@ -10,14 +10,9 @@ const component = create(maskedInput);
|
||||||
module('Integration | Component | masked input', function(hooks) {
|
module('Integration | Component | masked input', function(hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
const hasClass = (classString = '', classToFind) => {
|
|
||||||
return classString.split(' ').includes(classToFind);
|
|
||||||
};
|
|
||||||
|
|
||||||
test('it renders', async function(assert) {
|
test('it renders', async function(assert) {
|
||||||
await render(hbs`{{masked-input}}`);
|
await render(hbs`{{masked-input}}`);
|
||||||
|
assert.dom('[data-test-masked-input]').exists('shows expiration beacon');
|
||||||
assert.ok(hasClass(component.wrapperClass, 'masked'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders a textarea', async function(assert) {
|
test('it renders a textarea', async function(assert) {
|
||||||
|
@ -26,14 +21,17 @@ module('Integration | Component | masked input', function(hooks) {
|
||||||
assert.ok(component.textareaIsPresent);
|
assert.ok(component.textareaIsPresent);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders an input with type password when maskWhileTyping is true', async function(assert) {
|
test('it renders an input with obscure font', async function(assert) {
|
||||||
await render(hbs`{{masked-input maskWhileTyping=true}}`);
|
await render(hbs`{{masked-input}}`);
|
||||||
assert.ok(component.inputIsPresent);
|
|
||||||
assert.equal(
|
assert.dom('[data-test-textarea]').hasClass('masked-font', 'loading class with correct font');
|
||||||
this.element.querySelector('input').getAttribute('type'),
|
});
|
||||||
'password',
|
|
||||||
'default type equals password'
|
test('it renders obscure font when displayOnly', async function(assert) {
|
||||||
);
|
this.set('value', 'value');
|
||||||
|
await render(hbs`{{masked-input displayOnly=true value=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) {
|
test('it does not render a textarea when displayOnly is true', async function(assert) {
|
||||||
|
@ -54,36 +52,12 @@ module('Integration | Component | masked input', function(hooks) {
|
||||||
assert.notOk(component.copyButtonIsPresent);
|
assert.notOk(component.copyButtonIsPresent);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it unmasks text on focus', async function(assert) {
|
|
||||||
this.set('value', 'value');
|
|
||||||
await render(hbs`{{masked-input value=value}}`);
|
|
||||||
assert.ok(hasClass(component.wrapperClass, 'masked'));
|
|
||||||
|
|
||||||
await component.focusField();
|
|
||||||
assert.notOk(hasClass(component.wrapperClass, 'masked'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it remasks text on blur', async function(assert) {
|
|
||||||
this.set('value', 'value');
|
|
||||||
await render(hbs`{{masked-input value=value}}`);
|
|
||||||
|
|
||||||
assert.ok(hasClass(component.wrapperClass, 'masked'));
|
|
||||||
|
|
||||||
await component.focusField();
|
|
||||||
await component.blurField();
|
|
||||||
|
|
||||||
assert.ok(hasClass(component.wrapperClass, 'masked'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it unmasks text when button is clicked', async function(assert) {
|
test('it unmasks text when button is clicked', async function(assert) {
|
||||||
this.set('value', 'value');
|
this.set('value', 'value');
|
||||||
await render(hbs`{{masked-input value=value}}`);
|
await render(hbs`{{masked-input value=value}}`);
|
||||||
|
|
||||||
assert.ok(hasClass(component.wrapperClass, 'masked'));
|
|
||||||
|
|
||||||
await component.toggleMasked();
|
await component.toggleMasked();
|
||||||
|
|
||||||
assert.notOk(hasClass(component.wrapperClass, 'masked'));
|
assert.dom('.masked-value').doesNotHaveClass('masked-font');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it remasks text when button is clicked', async function(assert) {
|
test('it remasks text when button is clicked', async function(assert) {
|
||||||
|
@ -93,18 +67,25 @@ module('Integration | Component | masked input', function(hooks) {
|
||||||
await component.toggleMasked();
|
await component.toggleMasked();
|
||||||
await component.toggleMasked();
|
await component.toggleMasked();
|
||||||
|
|
||||||
assert.ok(hasClass(component.wrapperClass, 'masked'));
|
assert.dom('.masked-value').hasClass('masked-font');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it changes type to text when unmasked button is clicked', async function(assert) {
|
test('it shortens long outputs when displayOnly and masked', async function(assert) {
|
||||||
this.set('value', 'value');
|
this.set('value', '123456789-123456789-123456789');
|
||||||
await render(hbs`{{masked-input value=value maskWhileTyping=true}}`);
|
await render(hbs`{{masked-input value=value displayOnly=true}}`);
|
||||||
await component.toggleMasked();
|
let maskedValue = document.querySelector('.masked-value').innerText;
|
||||||
|
assert.equal(maskedValue.length, 20);
|
||||||
|
|
||||||
assert.equal(
|
await component.toggleMasked();
|
||||||
this.element.querySelector('input').getAttribute('type'),
|
let unMaskedValue = document.querySelector('.masked-value').innerText;
|
||||||
'text',
|
assert.equal(unMaskedValue.length, this.value.length);
|
||||||
'when unmasked type changes to text'
|
});
|
||||||
);
|
|
||||||
|
test('it does not unmask text on focus', async function(assert) {
|
||||||
|
this.set('value', '123456789-123456789-123456789');
|
||||||
|
await render(hbs`{{masked-input value=value}}`);
|
||||||
|
assert.dom('.masked-value').hasClass('masked-font');
|
||||||
|
await focus('.masked-value');
|
||||||
|
assert.dom('.masked-value').hasClass('masked-font');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,4 +34,12 @@ module('Integration | Helper | date-format', function(hooks) {
|
||||||
.dom('[data-test-date-format]')
|
.dom('[data-test-date-format]')
|
||||||
.includesText(todayString, 'it renders the a date if passed in as a string');
|
.includesText(todayString, 'it renders the a date if passed in as a string');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it supports ten digit dates', async function(assert) {
|
||||||
|
let tenDigitDate = 1621785298;
|
||||||
|
this.set('tenDigitDate', tenDigitDate);
|
||||||
|
|
||||||
|
await render(hbs`<p data-test-date-format>Date: {{date-format tenDigitDate "MM/dd/yyyy"}}</p>`);
|
||||||
|
assert.dom('[data-test-date-format]').includesText('05/23/2021');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,17 +1,7 @@
|
||||||
import { attribute, clickable, fillable, isPresent } from 'ember-cli-page-object';
|
import { clickable, isPresent } from 'ember-cli-page-object';
|
||||||
import { focus, blur } from '@ember/test-helpers';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
wrapperClass: attribute('class', '[data-test-masked-input]'),
|
|
||||||
enterText: fillable('[data-test-textarea]'),
|
|
||||||
textareaIsPresent: isPresent('[data-test-textarea]'),
|
textareaIsPresent: isPresent('[data-test-textarea]'),
|
||||||
inputIsPresent: isPresent('[data-test-input]'),
|
|
||||||
copyButtonIsPresent: isPresent('[data-test-copy-button]'),
|
copyButtonIsPresent: isPresent('[data-test-copy-button]'),
|
||||||
toggleMasked: clickable('[data-test-button]'),
|
toggleMasked: clickable('[data-test-button]'),
|
||||||
async focusField() {
|
|
||||||
return focus('[data-test-textarea]');
|
|
||||||
},
|
|
||||||
async blurField() {
|
|
||||||
return blur('[data-test-textarea]');
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,7 +32,7 @@ type UIConfig struct {
|
||||||
// NewUIConfig creates a new UI config
|
// NewUIConfig creates a new UI config
|
||||||
func NewUIConfig(enabled bool, physicalStorage physical.Backend, barrierStorage logical.Storage) *UIConfig {
|
func NewUIConfig(enabled bool, physicalStorage physical.Backend, barrierStorage logical.Storage) *UIConfig {
|
||||||
defaultHeaders := http.Header{}
|
defaultHeaders := http.Header{}
|
||||||
defaultHeaders.Set("Content-Security-Policy", "default-src 'none'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'unsafe-inline' 'self'; form-action 'none'; frame-ancestors 'none'")
|
defaultHeaders.Set("Content-Security-Policy", "default-src 'none'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'unsafe-inline' 'self'; form-action 'none'; frame-ancestors 'none'; font-src 'self'")
|
||||||
defaultHeaders.Set("Service-Worker-Allowed", "/")
|
defaultHeaders.Set("Service-Worker-Allowed", "/")
|
||||||
|
|
||||||
return &UIConfig{
|
return &UIConfig{
|
||||||
|
|
Loading…
Reference in New Issue