UI: TTL picker cleanup (#18114)

This commit is contained in:
Chelsea Shaw 2022-12-01 09:33:30 -06:00 committed by GitHub
parent 826e87884e
commit 0ea02992b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 810 additions and 916 deletions

3
changelog/18114.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: update TTL picker for consistency
```

View File

@ -41,4 +41,11 @@ export default class ConfigureAwsSecretComponent extends Component {
event.preventDefault();
this.args.saveAWSLease(data);
}
@action
handleTtlChange(name, ttlObj) {
// lease values cannot be undefined, set to 0 to use default
const valueToSet = ttlObj.enabled ? ttlObj.goSafeTimeString : 0;
this.args.model.set(name, valueToSet);
}
}

View File

@ -0,0 +1,12 @@
<div class="field">
<TtlPicker
@label="Wrap response"
@helperTextDisabled="Will not wrap response"
@helperTextEnabled="Will wrap response with a lease of"
@initialEnabled={{true}}
@initialValue="30m"
@onChange={{this.changedValue}}
@changeOnInit={{true}}
data-test-wrap-ttl-picker
/>
</div>

View File

@ -1,49 +1,26 @@
import { assert } from '@ember/debug';
import Component from '@ember/component';
import { set, computed } from '@ember/object';
import hbs from 'htmlbars-inline-precompile';
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default Component.extend({
// passed from outside
onChange: null,
wrapResponse: true,
export default class WrapTtlComponent extends Component {
@tracked
wrapResponse = true;
ttl: '30m',
constructor() {
super(...arguments);
assert('`onChange` handler is a required attr in `' + this.toString() + '`.', this.args.onChange);
}
wrapTTL: computed('wrapResponse', 'ttl', function () {
get wrapTTL() {
const { wrapResponse, ttl } = this;
return wrapResponse ? ttl : null;
}),
}
didRender() {
this._super(...arguments);
this.onChange(this.wrapTTL);
},
init() {
this._super(...arguments);
assert('`onChange` handler is a required attr in `' + this.toString() + '`.', this.onChange);
},
layout: hbs`
<div class="field">
{{ttl-picker2
data-test-wrap-ttl-picker=true
label='Wrap response'
helperTextDisabled='Will not wrap response'
helperTextEnabled='Will wrap response with a lease of'
enableTTL=this.wrapResponse
initialValue=this.ttl
onChange=(action 'changedValue')
}}
</div>
`,
actions: {
changedValue(ttlObj) {
set(this, 'wrapResponse', ttlObj.enabled);
set(this, 'ttl', `${ttlObj.seconds}s`);
this.onChange(this.wrapTTL);
},
},
});
@action
changedValue(ttlObj) {
this.wrapResponse = ttlObj.enabled;
this.ttl = ttlObj.goSafeTimeString;
this.args.onChange(this.wrapTTL);
}
}

View File

@ -13,3 +13,13 @@
// Font comes from npm package: https://www.npmjs.com/package/text-security
// We took the font we wanted and moved it into the ui/fonts folder
@include font-face('text-security-square');
.sr-only {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}

View File

@ -113,7 +113,7 @@
@import './components/tool-tip';
@import './components/transform-edit.scss';
@import './components/transit-card';
@import './components/ttl-picker2';
@import './components/ttl-picker';
@import './components/unseal-warning';
@import './components/ui-wizard';
@import './components/vault-loading';

View File

@ -34,8 +34,18 @@
If you do not supply lease settings, we will use the default values in AWS.
</p>
</div>
<TtlPicker @labelText="Lease" @initialValue={{@model.lease}} @onChange={{action (mut @model.lease)}} />
<TtlPicker @labelText="Maximum Lease" @initialValue={{@model.leaseMax}} @onChange={{action (mut @model.leaseMax)}} />
<TtlPicker
@label="Lease"
@initialValue={{@model.lease}}
@initialEnabled={{@model.lease}}
@onChange={{fn this.handleTtlChange "lease"}}
/>
<TtlPicker
@label="Maximum Lease"
@initialValue={{@model.leaseMax}}
@initialEnabled={{@model.leaseMax}}
@onChange={{fn this.handleTtlChange "leaseMax"}}
/>
<div class="box is-bottomless is-fullwidth">
<button data-test-aws-input="lease-save" type="submit" class="button is-primary">
Save

View File

@ -35,7 +35,7 @@
</div>
</div>
{{else if (eq @attr.options.editType "ttl")}}
<TtlPicker2
<TtlPicker
@initialValue={{or (get @model @attr.name) @attr.options.defaultValue}}
@initialEnabled={{or (get @model @attr.name) false}}
@label={{or @attr.options.label (humanize (dasherize @attr.name))}}

View File

@ -17,7 +17,7 @@
<MessageError @model={{this.config}} @errors={{this.errors}} />
<form {{action "save" this.section on="submit"}} class="box is-shadowless is-marginless is-fullwidth has-slim-padding">
{{#if (eq this.section "crl")}}
<TtlPicker2
<TtlPicker
data-test-input="expiry"
@onChange={{action "handleCrlTtl"}}
@label={{if (get this.config "disable") "CRL building disabled" "CRL building enabled"}}

View File

@ -53,7 +53,7 @@
/>
</div>
</div>
<TtlPicker2
<TtlPicker
@label="Wrap TTL"
@initialValue="30m"
@onChange={{action "updateTtl"}}

View File

@ -7,7 +7,7 @@
<Input id="key-name" @value={{@key.name}} class="input" data-test-transit-key-name={{true}} />
</div>
<div class="field">
<TtlPicker2
<TtlPicker
@initialValue="30d"
@initialEnabled={{false}}
@label="Auto-rotation period"

View File

@ -17,7 +17,7 @@
</div>
</div>
<div class="field">
<TtlPicker2
<TtlPicker
@initialValue={{or @key.autoRotatePeriod "30d"}}
@initialEnabled={{not (eq @key.autoRotatePeriod "0s")}}
@label="Auto-rotation period"

View File

@ -66,7 +66,7 @@
<div class="box is-shadowless" data-test-lease-renew-picker={{true}}>
<h2 class="title is-6">Renew Lease</h2>
<form {{action "renewLease" this.model this.increment on="submit"}}>
<TtlPicker2
<TtlPicker
@label="Increment"
@helperTextEnabled="Lease will expire after"
@helperTextDisabled="Vault will use the default lease duration"

View File

@ -121,7 +121,7 @@
{{! TTL Picker }}
<div class="field">
{{#let (or (get @model this.valuePath) @attr.options.setDefault) as |initialValue|}}
<TtlPicker2
<TtlPicker
data-test-input={{@attr.name}}
@onChange={{this.setAndBroadcastTtl}}
@label={{this.labelString}}

View File

@ -8,7 +8,7 @@
data-test-radio-button="ttl"
/>
<label class="has-left-margin-xs">
<TtlPicker2
<TtlPicker
data-test-input="ttl"
@onChange={{this.setAndBroadcastTtl}}
@label="TTL"

View File

@ -7,8 +7,9 @@
class={{this.inputClasses}}
disabled={{this.disabled}}
data-test-toggle-input={{this.name}}
...attributes
/>
<label data-test-toggle-label={{this.name}} for={{this.safeId}} class="toggle-label">
<label data-test-toggle-label={{this.name}} for={{this.safeId}} class="toggle-label {{if this.hideLabel 'sr-only'}}">
{{#if (has-block)}}
{{yield}}
{{else}}

View File

@ -1,103 +0,0 @@
/**
* @module TtlForm
* TtlForm components are used to enter a Time To Live (TTL) input.
* This component does not include a label and is designed to take
* a time and unit, and pass an object including seconds and
* timestring when those two values are changed.
*
* @example
* ```js
* <TtlForm @onChange={{action handleChange}} @unit="m"/>
* ```
* @param {function} onChange - This function will be called when the user changes the value. An object will be passed in as a parameter with values seconds{number}, timeString{string}
* @param {number} [time] - Time is the value that will be passed into the value input. Can be null/undefined to start if input is required.
* @param {unit} [unit="s"] - This is the unit key which will show by default on the form. Can be one of `s` (seconds), `m` (minutes), `h` (hours), `d` (days)
* @param {number} [recalculationTimeout=5000] - This is the time, in milliseconds, that `recalculateSeconds` will be be true after time is updated
*/
import Ember from 'ember';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { task, timeout } from 'ember-concurrency';
import layout from '../templates/components/ttl-form';
const secondsMap = {
s: 1,
m: 60,
h: 3600,
d: 86400,
};
const convertToSeconds = (time, unit) => {
return time * secondsMap[unit];
};
const convertFromSeconds = (seconds, unit) => {
return seconds / secondsMap[unit];
};
export default Component.extend({
layout,
time: '',
unit: 's',
/* Used internally */
recalculationTimeout: 5000,
recalculateSeconds: false,
errorMessage: null,
unitOptions: computed(function () {
return [
{ label: 'seconds', value: 's' },
{ label: 'minutes', value: 'm' },
{ label: 'hours', value: 'h' },
{ label: 'days', value: 'd' },
];
}),
handleChange() {
const { time, unit, seconds } = this;
const ttl = {
seconds,
timeString: time + unit,
};
this.onChange(ttl);
},
keepSecondsRecalculate(newUnit) {
const newTime = convertFromSeconds(this.seconds, newUnit);
this.setProperties({
time: newTime,
unit: newUnit,
});
},
updateTime: task(function* (newTime) {
this.set('errorMessage', '');
const parsedTime = parseInt(newTime, 10);
if (!newTime) {
this.set('errorMessage', 'This field is required');
return;
} else if (Number.isNaN(parsedTime)) {
this.set('errorMessage', 'Value must be a number');
return;
}
this.set('time', parsedTime);
this.handleChange();
if (Ember.testing) {
return;
}
this.set('recalculateSeconds', true);
yield timeout(this.recalculationTimeout);
this.set('recalculateSeconds', false);
}).restartable(),
seconds: computed('time', 'unit', function () {
return convertToSeconds(this.time, this.unit);
}),
actions: {
updateUnit(newUnit) {
if (this.recalculateSeconds) {
this.set('unit', newUnit);
} else {
this.keepSecondsRecalculate(newUnit);
}
this.handleChange();
},
},
});

View File

@ -0,0 +1,139 @@
<div class="has-bottom-margin-m" ...attributes>
{{#if @hideToggle}}
<fieldset class="field is-grouped is-marginless is-borderless">
{{#if (has-block)}}
{{! Allow label override }}
{{yield}}
{{else}}
<legend>
<span class="ttl-picker-label is-large" data-test-ttl-form-label={{this.label}}>{{this.label}}</span><br />
{{#if this.helperText}}
<div class="sub-text">
<span data-test-ttl-form-subtext>{{this.helperText}}</span>
{{#if @description}}
<ToolTip @verticalPosition="below" as |T|>
<T.Trigger data-test-tooltip-trigger tabindex="-1">
<Icon @name="info" aria-label="description" />
</T.Trigger>
<T.Content @defaultClass="tool-tip">
<div class="box" data-test-hover-copy-tooltip-text>
{{this.description}}
</div>
</T.Content>
</ToolTip>
{{/if}}
</div>
{{/if}}
</legend>
{{/if}}
<div class="control is-marginless" data-test-ttl-inputs>
<label for="time-{{this.elementId}}" class="sr-only">Number of units</label>
<Input
id="time-{{this.elementId}}"
@value={{this.time}}
@type="text"
name="time"
class="input {{if this.errorMessage 'has-error'}}"
oninput={{perform this.updateTime value="target.value"}}
pattern="[0-9]*"
data-test-ttl-value={{this.label}}
/>
</div>
<div class="control">
<label for="unit-{{this.elementId}}" class="sr-only">Unit for TTL</label>
<Select
id="unit-{{this.elementId}}"
@name="ttl-unit"
@options={{this.unitOptions}}
@onChange={{this.updateUnit}}
@selectedValue={{this.unit}}
data-test-ttl-unit={{this.label}}
/>
</div>
{{#if this.errorMessage}}
<div class="columns is-mobile is-variable is-1 ttl-value-error">
<div class="is-narrow message-icon">
<Icon @name="x-square-fill" class="has-text-danger" />
</div>
<div class="has-text-danger">
{{this.errorMessage}}
</div>
</div>
{{/if}}
</fieldset>
{{else}}
<Toggle
@name={{this.label}}
@status="success"
@size="small"
@onChange={{action "toggleEnabled"}}
@checked={{this.enableTTL}}
@hideLabel={{true}}
data-test-ttl-toggle={{this.label}}
>
<fieldset class="field is-grouped is-marginless is-borderless">
{{#if (has-block)}}
{{! Allow label override }}
{{yield}}
{{else}}
<legend>
<span class="ttl-picker-label is-large" data-test-ttl-form-label={{this.label}}>{{this.label}}</span><br />
{{#if this.helperText}}
<div class="sub-text">
<span data-test-ttl-form-subtext>{{this.helperText}}</span>
{{#if @description}}
<ToolTip @verticalPosition="below" as |T|>
<T.Trigger data-test-tooltip-trigger tabindex="-1">
<Icon @name="info" aria-label="description" />
</T.Trigger>
<T.Content @defaultClass="tool-tip">
<div class="box" data-test-hover-copy-tooltip-text>
{{this.description}}
</div>
</T.Content>
</ToolTip>
{{/if}}
</div>
{{/if}}
</legend>
{{/if}}
{{#if (or this.enableTTL @hideToggle)}}
<div class="control is-marginless" data-test-ttl-inputs>
<label for="time-{{this.elementId}}" class="sr-only">Number of units</label>
<Input
id="time-{{this.elementId}}"
@value={{this.time}}
@type="text"
name="time"
class="input {{if this.errorMessage 'has-error'}}"
oninput={{perform this.updateTime value="target.value"}}
pattern="[0-9]*"
data-test-ttl-value={{this.label}}
/>
</div>
<div class="control">
<label for="unit-{{this.elementId}}" class="sr-only">Unit for TTL</label>
<Select
id="unit-{{this.elementId}}"
@name="ttl-unit"
@options={{this.unitOptions}}
@onChange={{this.updateUnit}}
@selectedValue={{this.unit}}
data-test-ttl-unit={{this.label}}
/>
</div>
{{#if this.errorMessage}}
<div class="columns is-mobile is-variable is-1 ttl-value-error">
<div class="is-narrow message-icon">
<Icon @name="x-square-fill" class="has-text-danger" />
</div>
<div class="has-text-danger">
{{this.errorMessage}}
</div>
</div>
{{/if}}
{{/if}}
</fieldset>
</Toggle>
{{/if}}
</div>

View File

@ -1,124 +1,188 @@
import { typeOf } from '@ember/utils';
import EmberError from '@ember/error';
import Component from '@ember/component';
import { set, computed } from '@ember/object';
import Duration from '@icholy/duration';
import layout from '../templates/components/ttl-picker';
const ERROR_MESSAGE = 'TTLs must be specified in whole number increments, please enter a whole number.';
/**
* @module TtlPicker
* `TtlPicker` components are used to set the 'time to live'.
* This version is being deprecated and replaced by `TtlPicker2` which is an automatic-width version that
* automatically recalculates the time value when unit is updated unless time has been changed recently.
* Once all instances of TtlPicker are replaced with TtlPicker2, this component will be removed and
* TtlPicker2 will be renamed to TtlPicker.
* TtlPicker components are used to enable and select duration values such as TTL.
* This component renders a toggle by default, and passes all relevant attributes
* to TtlForm. Please see that component for additional arguments
* - allows TTL to be enabled or disabled
* - recalculates the time when the unit is changed by the user (eg 60s -> 1m)
*
* @example
* ```js
<TtlPicker @labelText="Lease" @initialValue={{lease}} @onChange={{action (mut lease)}} />
* <TtlPicker @onChange={{this.handleChange}} @initialEnabled={{@model.myAttribute}} @initialValue={{@model.myAttribute}}/>
* ```
*
* @param labelClass="" {String} - A CSS class to add to the label.
* @param labelText="TTL" {String} - The text content of the label associated with the widget.
* @param initialValue=null {Number} - The starting value of the TTL;
* @param setDefaultValue=true {Boolean} - If true, the component will trigger onChange on the initial
* render, causing a value to be set.
* @param onChange=Function.prototype{Function} - The function to call when the value of the ttl changes.
* @param outputSeconds=false{Boolean} - If true, the component will trigger onChange with a value
* converted to seconds instead of a Golang duration string.
* @param onChange {Function} - This function will be passed a TTL object, which includes enabled{bool}, seconds{number}, timeString{string}, goSafeTimeString{string}.
* @param initialEnabled=false {Boolean} - Set this value if you want the toggle on when component is mounted
* @param label="Time to live (TTL)" {String} - Label is the main label that lives next to the toggle. Yielded values will replace the label
* @param helperTextEnabled="" {String} - This helper text is shown under the label when the toggle is switched on
* @param helperTextDisabled="" {String} - This helper text is shown under the label when the toggle is switched off
* @param initialValue=null {string} - InitialValue is the duration value which will be shown when the component is loaded. If it can't be parsed, will default to 0.
* @param changeOnInit=false {boolean} - if true, calls the onChange hook when component is initialized
* @param hideToggle=false {Boolean} - set this value if you'd like to hide the toggle and just leverage the input field
*/
export default Component.extend({
layout,
'data-test-component': 'ttl-picker',
attributeBindings: ['data-test-component'],
classNames: 'field',
onChange: () => {},
setDefaultValue: true,
labelText: 'TTL',
labelClass: '',
ouputSeconds: false,
import Component from '@glimmer/component';
import { typeOf } from '@ember/utils';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import Duration from '@icholy/duration';
import { guidFor } from '@ember/object/internals';
import Ember from 'ember';
import { restartableTask, timeout } from 'ember-concurrency';
time: 30,
unit: 'm',
initialValue: null,
errorMessage: null,
unitOptions: computed(function () {
export const secondsMap = {
s: 1,
m: 60,
h: 3600,
d: 86400,
};
const convertToSeconds = (time, unit) => {
return time * secondsMap[unit];
};
const convertFromSeconds = (seconds, unit) => {
return seconds / secondsMap[unit];
};
const goSafeConvertFromSeconds = (seconds, unit) => {
// Go only accepts s, m, or h units
const u = unit === 'd' ? 'h' : unit;
return convertFromSeconds(seconds, u) + u;
};
const largestUnitFromSeconds = (seconds) => {
let unit = 's';
if (seconds === 0) return unit;
// get largest unit with no remainder
if (seconds % secondsMap.d === 0) {
unit = 'd';
} else if (seconds % secondsMap.h === 0) {
unit = 'h';
} else if (seconds % secondsMap.m === 0) {
unit = 'm';
}
return unit;
};
export default class TtlPickerComponent extends Component {
@tracked enableTTL = false;
@tracked recalculateSeconds = false;
@tracked time = ''; // if defaultValue is NOT set, then do not display a defaultValue.
@tracked unit = 's';
@tracked recalculateSeconds = false;
@tracked errorMessage = '';
/* Used internally */
recalculationTimeout = 5000;
elementId = 'ttl-' + guidFor(this);
get label() {
return this.args.label || 'Time to live (TTL)';
}
get helperText() {
return this.enableTTL || this.args.hideToggle
? this.args.helperTextEnabled
: this.args.helperTextDisabled;
}
constructor() {
super(...arguments);
const enable = this.args.initialEnabled;
let setEnable = !!this.args.hideToggle;
if (!!enable || typeOf(enable) === 'boolean') {
// This allows non-boolean values passed in to be evaluated for truthiness
setEnable = !!enable;
}
this.enableTTL = setEnable;
this.initializeTtl();
}
initializeTtl() {
const initialValue = this.args.initialValue;
let seconds = 0;
if (typeof initialValue === 'number') {
// if the passed value is a number, assume unit is seconds
seconds = initialValue;
} else {
try {
seconds = Duration.parse(initialValue).seconds();
} catch (e) {
// if parsing fails leave it empty
return;
}
}
const unit = largestUnitFromSeconds(seconds);
this.time = convertFromSeconds(seconds, unit);
this.unit = unit;
if (this.args.changeOnInit) {
this.handleChange();
}
}
get seconds() {
return convertToSeconds(this.time, this.unit);
}
get unitOptions() {
return [
{ label: 'seconds', value: 's' },
{ label: 'minutes', value: 'm' },
{ label: 'hours', value: 'h' },
{ label: 'days', value: 'd' },
];
}),
}
convertToSeconds(time, unit) {
const toSeconds = {
s: 1,
m: 60,
h: 3600,
};
return time * toSeconds[unit];
},
TTL: computed('outputSeconds', 'time', 'unit', function () {
let { time, unit, outputSeconds } = this;
//convert to hours
if (unit === 'd') {
time = time * 24;
unit = 'h';
keepSecondsRecalculate(newUnit) {
const newTime = convertFromSeconds(this.seconds, newUnit);
if (Number.isInteger(newTime)) {
// Only recalculate if time is whole number
this.time = newTime;
}
const timeString = time + unit;
return outputSeconds ? this.convertToSeconds(time, unit) : timeString;
}),
this.unit = newUnit;
}
didInsertElement() {
this._super(...arguments);
if (this.setDefaultValue === false) {
handleChange() {
const { time, unit, seconds, enableTTL } = this;
const ttl = {
enabled: this.args.hideToggle || enableTTL,
seconds,
timeString: time + unit,
goSafeTimeString: goSafeConvertFromSeconds(seconds, unit),
};
this.args.onChange(ttl);
}
@action
toggleEnabled() {
this.enableTTL = !this.enableTTL;
this.handleChange();
}
@restartableTask
*updateTime(newTime) {
this.errorMessage = '';
const parsedTime = parseInt(newTime, 10);
if (!newTime) {
this.errorMessage = 'This field is required';
return;
} else if (Number.isNaN(parsedTime)) {
this.errorMessage = 'Value must be a number';
return;
}
this.onChange(this.TTL);
},
init() {
this._super(...arguments);
if (!this.onChange) {
throw new EmberError('`onChange` handler is a required attr in `' + this.toString() + '`.');
this.time = parsedTime;
this.handleChange();
if (Ember.testing) {
return;
}
if (this.initialValue != undefined) {
this.parseAndSetTime();
this.recalculateSeconds = true;
yield timeout(this.recalculationTimeout);
this.recalculateSeconds = false;
}
@action
updateUnit(newUnit) {
if (this.recalculateSeconds) {
this.unit = newUnit;
} else {
this.keepSecondsRecalculate(newUnit);
}
},
parseAndSetTime() {
const value = this.initialValue;
let seconds = typeOf(value) === 'number' ? value : 30;
try {
seconds = Duration.parse(value).seconds();
} catch (e) {
// if parsing fails leave as default 30
}
this.set('time', seconds);
this.set('unit', 's');
},
actions: {
changedValue(key, value) {
if (value && key === 'time') {
value = parseInt(value, 10);
if (Number.isNaN(value)) {
this.set('errorMessage', ERROR_MESSAGE);
return;
}
}
this.set('errorMessage', null);
set(this, key, value);
this.onChange(this.TTL);
},
},
});
this.handleChange();
}
}

View File

@ -1,159 +0,0 @@
/**
* @module TtlPicker2
* TtlPicker2 components are used to enable and select 'time to live' values. Use this TtlPicker2 instead of TtlPicker if you:
* - Want the TTL to be enabled or disabled
* - Want to have the time recalculated by default when the unit changes (eg 60s -> 1m)
*
* @example
* ```js
* <TtlPicker2 @onChange={{handleChange}} @time={{defaultTime}} @unit={{defaultUnit}}/>
* ```
* @param onChange {Function} - This function will be passed a TTL object, which includes enabled{bool}, seconds{number}, timeString{string}, goSafeTimeString{string}.
* @param label="Time to live (TTL)" {String} - Label is the main label that lives next to the toggle.
* @param helperTextDisabled="Allow tokens to be used indefinitely" {String} - This helper text is shown under the label when the toggle is switched off
* @param helperTextEnabled="Disable the use of the token after" {String} - This helper text is shown under the label when the toggle is switched on
* @param description="Longer description about this value, what it does, and why it is useful. Shows up in tooltip next to helpertext"
* @param time='' {Number} - The time (in the default units) which will be adjustable by the user of the form
* @param unit="s" {String} - This is the unit key which will show by default on the form. Can be one of `s` (seconds), `m` (minutes), `h` (hours), `d` (days)
* @param recalculationTimeout=5000 {Number} - This is the time, in milliseconds, that `recalculateSeconds` will be be true after time is updated
* @param initialValue=null {String} - This is the value set initially (particularly from a string like '30h')
* @param initialEnabled=null {Boolean} - Set this value if you want the toggle on when component is mounted
* @param changeOnInit=false {Boolean} - set this value if you'd like the passed onChange function to be called on component initialization
* @param hideToggle=false {Boolean} - set this value if you'd like to hide the toggle and just leverage the input field
*/
import { computed } from '@ember/object';
import { typeOf } from '@ember/utils';
import Duration from '@icholy/duration';
import TtlForm from './ttl-form';
import layout from '../templates/components/ttl-picker2';
const secondsMap = {
s: 1,
m: 60,
h: 3600,
d: 86400,
};
const convertFromSeconds = (seconds, unit) => {
return seconds / secondsMap[unit];
};
const goSafeConvertFromSeconds = (seconds, unit) => {
// Go only accepts s, m, or h units
const u = unit === 'd' ? 'h' : unit;
return convertFromSeconds(seconds, u) + u;
};
export default TtlForm.extend({
layout,
enableTTL: false,
label: 'Time to live (TTL)',
helperTextDisabled: 'Allow tokens to be used indefinitely',
helperTextEnabled: 'Disable the use of the token after',
description: '',
time: '', // if defaultValue is NOT set, then do not display a defaultValue.
unit: 's',
initialValue: null,
changeOnInit: false,
hideToggle: false,
init() {
this._super(...arguments);
const value = this.initialValue;
const enable = this.initialEnabled;
const changeOnInit = this.changeOnInit;
// if initial value is unset use params passed in as defaults
// and if no defaultValue is passed in display no time
if (!value && value !== 0) {
return;
}
let time = 30;
let unit = 's';
let setEnable = this.hideToggle || this.enableTTL;
if (!!enable || typeOf(enable) === 'boolean') {
// This allows non-boolean values passed in to be evaluated for truthiness
setEnable = !!enable;
}
if (typeOf(value) === 'number') {
// if the passed value is a number, assume unit is seconds
// then check if the value can be converted into a larger unit
if (value % secondsMap.d === 0) {
unit = 'd';
} else if (value % secondsMap.h === 0) {
unit = 'h';
} else if (value % secondsMap.m === 0) {
unit = 'm';
}
time = convertFromSeconds(value, unit);
} else {
try {
const seconds = Duration.parse(value).seconds();
time = seconds;
// get largest unit with no remainder
if (seconds % secondsMap.d === 0) {
unit = 'd';
} else if (seconds % secondsMap.h === 0) {
unit = 'h';
} else if (seconds % secondsMap.m === 0) {
unit = 'm';
}
if (unit !== 's') {
time = convertFromSeconds(seconds, unit);
}
} catch (e) {
// if parsing fails leave as default 30s
}
}
this.setProperties({
time,
unit,
enableTTL: setEnable,
});
if (changeOnInit) {
this.handleChange();
}
},
unitOptions: computed(function () {
return [
{ label: 'seconds', value: 's' },
{ label: 'minutes', value: 'm' },
{ label: 'hours', value: 'h' },
{ label: 'days', value: 'd' },
];
}),
handleChange() {
const { time, unit, enableTTL, seconds } = this;
const ttl = {
enabled: this.hideToggle || enableTTL,
seconds,
timeString: time + unit,
goSafeTimeString: goSafeConvertFromSeconds(seconds, unit),
};
this.onChange(ttl);
},
helperText: computed(
'enableTTL',
'helperTextDisabled',
'helperTextEnabled',
'helperTextSet',
'helperTextUnset',
'hideToggle',
function () {
return this.enableTTL || this.hideToggle ? this.helperTextEnabled : this.helperTextDisabled;
}
),
recalculateSeconds: false,
actions: {
toggleEnabled() {
this.toggleProperty('enableTTL');
this.handleChange();
},
},
});

View File

@ -1,35 +0,0 @@
{{this.yeild}}
<div class="field is-grouped">
<div class="control">
<input
data-test-ttlform-value
value={{this.time}}
id="time-foobar"
type="text"
name="time"
class="input"
pattern="[0-9]*"
oninput={{perform this.updateTime value="target.value"}}
/>
</div>
<div class="control">
<Select
data-test-ttlform-unit
@name="ttl-unit"
@options={{this.unitOptions}}
@onChange={{action "updateUnit"}}
@selectedValue={{this.unit}}
@isFullwidth={{true}}
/>
</div>
</div>
{{#if this.errorMessage}}
<div class="columns is-mobile is-variable is-1 ttl-value-error">
<div class="is-narrow message-icon">
<Icon @name="x-square-fill" class="has-text-danger" />
</div>
<div class="has-text-danger">
{{this.errorMessage}}
</div>
</div>
{{/if}}

View File

@ -1,25 +0,0 @@
<label for="time-{{this.elementId}}" class="is-label {{this.labelClass}}">{{this.labelText}}</label>
<MessageError @errorMessage={{this.errorMessage}} data-test-ttl-error />
<div class="field is-grouped">
<div class="control is-expanded">
<input
data-test-ttl-value
value={{this.time}}
id="time-{{this.elementId}}"
type="text"
name="time"
class="input"
oninput={{action (action "changedValue" "time") value="target.value"}}
pattern="[0-9]*"
/>
</div>
<div class="control is-expanded">
<Select
@name="ttl-unit"
@options={{this.unitOptions}}
@onChange={{action "changedValue" "unit"}}
@selectedValue={{this.unit.value}}
@isFullwidth={{true}}
/>
</div>
</div>

View File

@ -1,83 +0,0 @@
{{#if this.hideToggle}}
<span class="ttl-picker-label">{{this.label}}</span><br />
<div class="sub-text">
<span>{{this.helperText}}</span>
{{#if this.description}}
<ToolTip @verticalPosition="below" as |T|>
<T.Trigger data-test-tooltip-trigger tabindex="-1">
<Icon @name="info" aria-label="description" />
</T.Trigger>
<T.Content @defaultClass="tool-tip">
<div class="box" data-test-hover-copy-tooltip-text>
{{this.description}}
</div>
</T.Content>
</ToolTip>
{{/if}}
</div>
{{else}}
<Toggle
@name={{this.label}}
@status="success"
@size="small"
@onChange={{action "toggleEnabled"}}
@checked={{this.enableTTL}}
data-test-ttl-toggle
>
<span class="ttl-picker-label is-large">{{this.label}}</span><br />
<div class="description has-text-grey">
<span>{{this.helperText}}</span>
{{#if this.description}}
<ToolTip @verticalPosition="below" as |T|>
<T.Trigger data-test-tooltip-trigger tabindex="-1">
<Icon @name="info" aria-label="description" />
</T.Trigger>
<T.Content @defaultClass="tool-tip">
<div class="box" data-test-hover-copy-tooltip-text>
{{this.description}}
</div>
</T.Content>
</ToolTip>
{{/if}}
</div>
</Toggle>
{{/if}}
{{#if (or this.enableTTL this.hideToggle)}}
<div class={{unless this.hideToggle "ttl-show-picker"}} data-test-ttl-picker-group={{this.label}}>
<div class="field is-grouped is-marginless">
<div class="control is-marginless">
<input
data-test-ttl-value={{this.label}}
value={{this.time}}
id="time-{{this.elementId}}"
type="text"
name="time"
class="input{{if this.errorMessage ' has-error'}}"
oninput={{perform this.updateTime value="target.value"}}
pattern="[0-9]*"
/>
</div>
<div class="control">
<Select
data-test-ttl-unit={{this.label}}
@name="ttl-unit"
@options={{this.unitOptions}}
@onChange={{action "updateUnit"}}
@selectedValue={{this.unit}}
@isFullwidth={{true}}
/>
</div>
</div>
{{#if this.errorMessage}}
<div class="columns is-mobile is-variable is-1 ttl-value-error">
<div class="is-narrow message-icon">
<Icon @name="x-square-fill" class="has-text-danger" />
</div>
<div class="has-text-danger">
{{this.errorMessage}}
</div>
</div>
{{/if}}
</div>
{{yield}}
{{/if}}

View File

@ -1 +0,0 @@
export { default } from 'core/components/ttl-form';

View File

@ -1 +0,0 @@
export { default } from 'core/components/ttl-picker2';

View File

@ -28,7 +28,7 @@
</p>
</div>
<div class="field">
<TtlPicker2
<TtlPicker
@initialValue="30m"
@label="Time to Live (TTL) for generated secondary token"
@helperTextDisabled="If not set, the default value (30 minutes) will be used"

View File

@ -183,6 +183,7 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) {
await fillIn('[data-test-input="maxVersions"]', maxVersion);
await click('[data-test-input="casRequired"]');
await click('[data-test-toggle-label="Automate secret deletion"]');
await fillIn('[data-test-select="ttl-unit"]', 's');
await fillIn('[data-test-ttl-value="Automate secret deletion"]', '1');
await click('[data-test-mount-submit="true"]');

View File

@ -0,0 +1,14 @@
const selectors = {
ttlFormGroup: '[data-test-ttl-inputs]',
toggle: '[data-test-ttl-toggle]',
toggleByLabel: (label) => `[data-test-ttl-toggle="${label}"]`,
label: '[data-test-ttl-form-label]',
subtext: '[data-test-ttl-form-subtext]',
tooltipTrigger: `[data-test-tooltip-trigger]`,
ttlValue: '[data-test-ttl-value]',
ttlUnit: '[data-test-select="ttl-unit"]',
valueInputByLabel: (label) => `[data-test-ttl-value="${label}"]`,
unitInputByLabel: (label) => `[data-test-ttl-unit="${label}"] [data-test-select="ttl-unit"]`,
};
export default selectors;

View File

@ -63,7 +63,7 @@ module('Integration | Component | mfa-method-form', function (hooks) {
assert.expect(3);
this.model.issuer = 'Vault';
this.model.period = '30';
this.model.period = '30s';
this.model.algorithm = 'SHA512';
await render(hbs`

View File

@ -54,22 +54,25 @@ module('Integration | Component | config pki', function (hooks) {
'renders form subtext'
);
assert
.dom('[data-test-toggle-label]')
.hasText('CRL building enabled The CRL will expire after', 'renders enabled field title and subtext');
.dom('[data-test-ttl-form-label="CRL building enabled"]')
.hasText('CRL building enabled', 'renders enabled field title');
assert
.dom('[data-test-ttl-form-subtext]')
.hasText('The CRL will expire after', 'renders enabled field subtext');
assert.dom('[data-test-input="expiry"] input').isChecked('defaults to enabling CRL build');
assert.dom('[data-test-ttl-value="CRL building enabled"]').hasValue('3', 'default value is 3 (72h)');
assert.dom('[data-test-select="ttl-unit"]').hasValue('d', 'default unit value is days');
await click('[data-test-input="expiry"] input');
assert
.dom('[data-test-toggle-label]')
.hasText('CRL building disabled The CRL will not be built.', 'renders disabled text when toggled off');
.dom('[data-test-ttl-form-subtext]')
.hasText('The CRL will not be built.', 'renders disabled text when toggled off');
// assert 'disable' attr on pki-config model updates with toggle
assert.true(this.config.disable, 'when toggled off, sets CRL config to disable=true');
await click('[data-test-input="expiry"] input');
assert
.dom('[data-test-toggle-label]')
.hasText('CRL building enabled The CRL will expire after', 'toggles back to enabled text');
.dom('[data-test-ttl-form-subtext]')
.hasText('The CRL will expire after', 'toggles back to enabled text');
assert.false(this.config.disable, 'CRL config toggles back to disable=false');
});

View File

@ -33,14 +33,13 @@ module('Integration | Component | radio-select-ttl-or-string', function (hooks)
`,
{ owner: this.engine }
);
assert.dom('[data-test-input="ttl"]').exists('shows the TTL component');
const inputValue = document.querySelector('[data-test-ttl-value="TTL"]').value;
assert.strictEqual(inputValue, '', 'default TTL is empty');
assert.dom('[data-test-ttl-inputs]').exists('shows the TTL component');
assert.dom('[data-test-ttl-value]').hasValue('', 'default TTL is empty');
assert.dom('[data-test-radio-button="ttl"]').isChecked('ttl is selected by default');
});
test('it should set the model properties ttl or notAfter based on the radio button selections', async function (assert) {
assert.expect(8);
assert.expect(7);
await render(
hbs`
<div class="has-top-margin-xxl">
@ -68,23 +67,21 @@ module('Integration | Component | radio-select-ttl-or-string', function (hooks)
'sets the model property notAfter when this value is selected and filled in.'
);
await fillIn('[data-test-ttl-value="TTL"]', ttlDate);
assert.strictEqual(this.model.ttl, '', 'No ttl is set because the radio button was not selected.');
await click('[data-test-radio-button="ttl"]');
assert.strictEqual(
this.model.notAfter,
'',
'The notAfter is cleared on the model because the radio button was selected.'
);
await fillIn('[data-test-ttl-value="TTL"]', ttlDate);
assert.strictEqual(
this.model.ttl,
ttlDate,
'1s',
'The ttl is now saved on the model because the radio button was selected.'
);
await click('[data-test-radio-button="not_after"]');
assert.strictEqual(this.model.ttl, '', 'Both ttl and notAfter are cleared.');
assert.strictEqual(this.model.notAfter, '', 'Both ttl and notAfter are cleared.');
assert.strictEqual(this.model.ttl, '', 'TTL is cleared after radio select.');
assert.strictEqual(this.model.notAfter, '', 'notAfter is cleared after radio select.');
});
});

View File

@ -1,49 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, fillIn } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
module('Integration | Component | ttl-form', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.changeSpy = sinon.spy();
this.set('onChange', this.changeSpy);
});
test('it shows no initial time and initial unit of s when not time or unit passed in', async function (assert) {
await render(hbs`<TtlForm @onChange={{this.onChange}} />`);
assert.dom('[data-test-ttlform-value]').hasValue('');
assert.dom('[data-test-select="ttl-unit"]').hasValue('s');
});
test('it calls the change fn with the correct values', async function (assert) {
await render(hbs`<TtlForm @onChange={{this.onChange}} @unit="m" />`);
assert.dom('[data-test-select="ttl-unit"]').hasValue('m', 'unit value initially shows m (minutes)');
await fillIn('[data-test-ttlform-value]', '10');
await assert.ok(this.changeSpy.calledOnce, 'it calls the passed onChange');
assert.ok(
this.changeSpy.calledWith({
seconds: 600,
timeString: '10m',
}),
'Passes the default values back to onChange'
);
});
test('it correctly shows initial unit', async function (assert) {
const changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlForm
@unit="h"
@time="3"
@onChange={{this.onChange}}
/>
`);
assert.dom('[data-test-select="ttl-unit"]').hasValue('h', 'unit value initially shows as h (hours)');
});
});

View File

@ -1,30 +1,390 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, fillIn } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import { render, click, fillIn } from '@ember/test-helpers';
import sinon from 'sinon';
import hbs from 'htmlbars-inline-precompile';
import selectors from 'vault/tests/helpers/components/ttl-picker';
module('Integration | Component | ttl picker', function (hooks) {
module('Integration | Component | ttl-picker', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.changeSpy = sinon.spy();
this.set('onChange', this.changeSpy);
this.set('onChange', sinon.spy());
this.set('label', 'Foobar');
});
test('it renders error on non-number input', async function (assert) {
await render(hbs`<TtlPicker @onChange={{this.onChange}} />`);
module('without toggle', function (hooks) {
hooks.beforeEach(function () {
this.set('hideToggle', true);
});
const callCount = this.changeSpy.callCount;
await fillIn('[data-test-ttl-value]', 'foo');
assert.strictEqual(this.changeSpy.callCount, callCount, "it didn't call onChange again");
assert.dom('[data-test-ttl-error]').includesText('Error', 'renders the error box');
await fillIn('[data-test-ttl-value]', '33');
assert.dom('[data-test-ttl-error]').doesNotExist('removes the error box');
test('it shows correct time and value when no initialValue set', async function (assert) {
await render(hbs`<TtlPicker
@label={{this.label}}
@hideToggle={{this.hideToggle}}
@onChange={{this.onChange}} />`);
assert.dom(selectors.ttlFormGroup).exists('TTL Form fields exist');
assert.dom(selectors.ttlValue).hasValue('');
assert.dom(selectors.ttlUnit).hasValue('s');
});
test('it calls the change fn with the correct values', async function (assert) {
const changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker
@label={{this.label}}
@hideToggle={{this.hideToggle}}
@onChange={{this.onChange}}
@initialValue="30m" />
`);
assert.dom(selectors.ttlUnit).hasValue('m', 'unit value shows m (minutes)');
await fillIn(selectors.ttlValue, '10');
await assert.ok(changeSpy.calledOnce, 'it calls the passed onChange');
assert.ok(
changeSpy.calledWithExactly({
enabled: true,
seconds: 600,
timeString: '10m',
goSafeTimeString: '10m',
}),
'Passes the values back to onChange'
);
});
test('it correctly shows initial time and unit', async function (assert) {
await render(hbs`
<TtlPicker
@label={{this.label}}
@hideToggle={{this.hideToggle}}
@initialValue="3h"
@onChange={{this.onChange}}
/>
`);
assert.dom(selectors.ttlUnit).hasValue('h', 'unit value initially shows as h (hours)');
assert.dom(selectors.ttlValue).hasValue('3', 'time value initially shows as 3');
});
test('it fails gracefully when initialValue is not parseable', async function (assert) {
const changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker
@label={{this.label}}
@hideToggle={{this.hideToggle}}
@initialValue="foobar"
@onChange={{this.onChange}}
@changeOnInit={{true}}
/>
`);
assert.dom(selectors.ttlValue).hasValue('', 'time value initially shows as empty');
assert.dom(selectors.ttlUnit).hasValue('s', 'unit value initially shows as s (seconds)');
assert.ok(changeSpy.notCalled, 'onChange is not called on init');
});
test('it recalculates time when unit is changed', async function (assert) {
const changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker
@label={{this.label}}
@hideToggle={{this.hideToggle}}
@initialValue="1h"
@onChange={{this.onChange}}
/>
`);
assert.dom(selectors.ttlUnit).hasValue('h', 'unit value initially shows as h (hours)');
assert.dom(selectors.ttlValue).hasValue('1', 'time value initially shows as 1');
await fillIn(selectors.ttlUnit, 'm');
assert.dom(selectors.ttlUnit).hasValue('m', 'unit value changed to m (minutes)');
assert.dom(selectors.ttlValue).hasValue('60', 'time value recalculates to fit unit');
assert.ok(
changeSpy.calledWithExactly({
enabled: true,
seconds: 3600,
timeString: '60m',
goSafeTimeString: '60m',
}),
'Passes the values back to onChange'
);
});
test('it skips recalculating time when unit is changed if time is not whole number', async function (assert) {
const changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker
@label={{this.label}}
@hideToggle={{this.hideToggle}}
@initialValue="30s"
@onChange={{this.onChange}}
/>
`);
assert.dom(selectors.ttlUnit).hasValue('s', 'unit value starts as s (seconds)');
assert.dom(selectors.ttlValue).hasValue('30', 'time value starts as 30');
await fillIn(selectors.ttlUnit, 'm');
assert.dom(selectors.ttlUnit).hasValue('m', 'unit value changed to m (minutes)');
assert.dom(selectors.ttlValue).hasValue('30', 'time value is still 30');
assert.ok(
changeSpy.calledWithExactly({
enabled: true,
seconds: 1800,
timeString: '30m',
goSafeTimeString: '30m',
}),
'Passes the values back to onChange'
);
});
test('it calls onChange on init when changeOnInit is true', async function (assert) {
const changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker
@label={{this.label}}
@hideToggle={{this.hideToggle}}
@initialValue="10m"
@changeOnInit={{true}}
@onChange={{this.onChange}}
/>
`);
assert.ok(changeSpy.calledOnce, 'it calls the passed onChange when rendered');
assert.ok(
changeSpy.calledWithExactly({
enabled: true,
seconds: 600,
timeString: '10m',
goSafeTimeString: '10m',
}),
'Passes the values back to onChange'
);
});
test('it shows a label when passed', async function (assert) {
this.set('label', 'My Label');
await render(hbs`
<TtlPicker
@label={{this.label}}
@hideToggle={{this.hideToggle}}
@onChange={{this.onChange}}
/>
`);
assert.dom('[data-test-ttl-form-label]').hasText('My Label', 'Renders label correctly');
assert.dom('[data-test-ttl-form-subtext]').doesNotExist('Subtext not rendered');
assert.dom('[data-test-tooltip-trigger]').doesNotExist('Description tooltip not rendered');
});
test('it shows subtext and description when passed', async function (assert) {
this.set('label', 'My Label');
await render(hbs`
<TtlPicker
@label={{this.label}}
@hideToggle={{this.hideToggle}}
@helperTextEnabled="Subtext"
@description="Description"
@onChange={{this.onChange}}
/>
`);
assert.dom('[data-test-ttl-form-label]').hasText('My Label', 'Renders label correctly');
assert.dom('[data-test-ttl-form-subtext]').hasText('Subtext', 'Renders subtext when present');
assert
.dom('[data-test-tooltip-trigger]')
.exists({ count: 1 }, 'Description tooltip icon shows when description present');
});
test('it yields in place of label if block is present', async function (assert) {
this.set('label', 'My Label');
await render(hbs`
<TtlPicker
@label={{this.label}}
@hideToggle={{this.hideToggle}}
@helperTextEnabled="Subtext"
@description="Description"
@onChange={{this.onChange}}
>
<legend data-test-custom>Different Label</legend>
</TtlPicker>
`);
assert.dom('[data-test-custom]').hasText('Different Label', 'custom block is rendered');
assert.dom('[data-test-ttl-form-label]').doesNotExist('Label not rendered');
});
});
test('it shows 30s for invalid duration initialValue input', async function (assert) {
await render(hbs`<TtlPicker @onChange={{this.onChange}} @initialValue="invalid" />`);
assert.dom('[data-test-ttl-value]').hasValue('30', 'sets 30 as the default');
module('with toggle', function () {
test('it has toggle off by default', async function (assert) {
await render(hbs`
<TtlPicker
@label={{this.label}}
@onChange={{this.onChange}}
/>
`);
assert.dom(selectors.toggle).isNotChecked('Toggle is unchecked by default');
assert.dom(selectors.ttlFormGroup).doesNotExist('TTL Form is not rendered');
});
test('it shows time and unit inputs when initialEnabled', async function (assert) {
const changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker
@label={{this.label}}
@onChange={{this.onChange}}
@initialEnabled={{true}}
@changeOnInit={{true}}
/>
`);
assert.dom(selectors.toggle).isChecked('Toggle is checked when initialEnabled is true');
assert.dom(selectors.ttlFormGroup).exists('TTL Form is rendered');
assert.ok(changeSpy.notCalled, 'onChange not called because initialValue not parsed');
});
test('it sets initial value to initialValue', async function (assert) {
await render(hbs`
<TtlPicker
@label={{this.label}}
@onChange={{this.onChange}}
@initialValue="2h"
@initialEnabled={{true}}
/>
`);
assert.dom(selectors.ttlValue).hasValue('2', 'time value is 2');
assert.dom(selectors.ttlUnit).hasValue('h', 'unit is hours');
assert.ok(
this.onChange.notCalled,
'it does not call onChange after render when changeOnInit is not set'
);
});
test('it passes the appropriate data to onChange when toggled on', async function (assert) {
const changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker
@label={{this.label}}
@label="clicktest"
@initialValue="10m"
@onChange={{this.onChange}}
/>
`);
await click(selectors.toggle);
assert.ok(changeSpy.calledOnce, 'it calls the passed onChange');
assert.ok(
changeSpy.calledWith({
enabled: true,
seconds: 600,
timeString: '10m',
goSafeTimeString: '10m',
}),
'Passes the values back to onChange'
);
});
test('inputs reflect initial value when toggled on', async function (assert) {
await render(hbs`
<TtlPicker
@label={{this.label}}
@label="inittest"
@onChange={{this.onChange}}
@initialValue="100m"
/>
`);
assert.dom(selectors.toggle).isNotChecked('Toggle is off');
assert.dom(selectors.ttlFormGroup).doesNotExist('TTL Form not shown on mount');
await click(selectors.toggle);
assert.dom(selectors.ttlValue).hasValue('100', 'time after toggle is 100');
assert.dom(selectors.ttlUnit).hasValue('m', 'Unit is minutes after toggle');
});
test('it is enabled on init if initialEnabled is true', async function (assert) {
await render(hbs`
<TtlPicker
@label={{this.label}}
@label="inittest"
@onChange={{this.onChange}}
@initialValue="100m"
@initialEnabled={{true}}
/>
`);
assert.dom(selectors.toggle).isChecked('Toggle is on');
assert.dom(selectors.ttlFormGroup).exists();
assert.dom(selectors.ttlValue).hasValue('100', 'time is shown on mount');
assert.dom(selectors.ttlUnit).hasValue('m', 'Unit is shown on mount');
await click(selectors.toggle);
assert.dom(selectors.toggle).isNotChecked('Toggle is off');
assert.dom(selectors.ttlFormGroup).doesNotExist('TTL Form no longer shows after toggle');
});
test('it is enabled on init if initialEnabled evals to truthy', async function (assert) {
await render(hbs`
<TtlPicker
@label={{this.label}}
@label="inittest"
@onChange={{this.onChange}}
@initialValue="100m"
@initialEnabled="100m"
/>
`);
assert.dom(selectors.toggle).isChecked('Toggle is enabled');
assert.dom(selectors.ttlValue).hasValue('100', 'time value is shown on mount');
assert.dom(selectors.ttlUnit).hasValue('m', 'Unit matches what is passed in');
});
test('it converts days to go safe time', async function (assert) {
await render(hbs`
<TtlPicker
@label={{this.label}}
@label="clicktest"
@initialValue="2d"
@onChange={{this.onChange}}
/>
`);
await click(selectors.toggle);
assert.ok(this.onChange.calledOnce, 'it calls the passed onChange');
assert.ok(
this.onChange.calledWith({
enabled: true,
seconds: 172800,
timeString: '2d',
goSafeTimeString: '48h',
}),
'Converts day unit to go safe time'
);
});
test('it converts to the largest round unit on init', async function (assert) {
await render(hbs`
<TtlPicker
@label={{this.label}}
@label="convertunits"
@onChange={{this.onChange}}
@initialValue="60000s"
@initialEnabled="true"
/>
`);
assert.dom(selectors.ttlValue).hasValue('1000', 'time value is converted');
assert.dom(selectors.ttlUnit).hasValue('m', 'unit value is m (minutes)');
});
test('it converts to the largest round unit on init when no unit provided', async function (assert) {
await render(hbs`
<TtlPicker
@label={{this.label}}
@label="convertunits"
@onChange={{this.onChange}}
@initialValue={{86400}}
@initialEnabled="true"
/>
`);
assert.dom(selectors.ttlValue).hasValue('1', 'time value is converted');
assert.dom(selectors.ttlUnit).hasValue('d', 'unit value is d (days)');
});
});
});

View File

@ -1,248 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click, fillIn } from '@ember/test-helpers';
import sinon from 'sinon';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | ttl-picker2', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.set('onChange', sinon.spy());
});
test('it renders time and unit inputs when TTL enabled', async function (assert) {
await render(hbs`
<TtlPicker2
@onChange={{this.onChange}}
@enableTTL={{true}}
/>
`);
assert.dom('[data-test-ttl-value]').exists('TTL Picker time input exists');
assert.dom('[data-test-ttl-unit]').exists('TTL Picker unit select exists');
});
test('it does not show time and unit inputs when TTL disabled', async function (assert) {
await render(hbs`
<TtlPicker2
@onChange={{this.onChange}}
@enableTTL={{false}}
/>
`);
assert.dom('[data-test-ttl-value]').doesNotExist('TTL Picker time input exists');
assert.dom('[data-test-ttl-unit]').doesNotExist('TTL Picker unit select exists');
});
test('it passes the appropriate data to onChange when toggled on', async function (assert) {
await render(hbs`
<TtlPicker2
@label="clicktest"
@unit="m"
@time="10"
@onChange={{this.onChange}}
@enableTTL={{false}}
/>
`);
await click('[data-test-toggle-input="clicktest"]');
assert.ok(this.onChange.calledOnce, 'it calls the passed onChange');
assert.ok(
this.onChange.calledWith({
enabled: true,
seconds: 600,
timeString: '10m',
goSafeTimeString: '10m',
}),
'Passes the default values back to onChange'
);
});
test('it keeps seconds value when unit is changed', async function (assert) {
await render(hbs`
<TtlPicker2
@label="clicktest"
@unit="s"
@time="360"
@onChange={{this.onChange}}
@enableTTL={{false}}
/>
`);
await click('[data-test-toggle-input="clicktest"]');
assert.ok(this.onChange.calledOnce, 'it calls the passed onChange');
assert.ok(
this.onChange.calledWith({
enabled: true,
seconds: 360,
timeString: '360s',
goSafeTimeString: '360s',
}),
'Changes enabled to true on click'
);
await fillIn('[data-test-select="ttl-unit"]', 'm');
assert.ok(
this.onChange.calledWith({
enabled: true,
seconds: 360,
timeString: '6m',
goSafeTimeString: '6m',
}),
'Units and time update without changing seconds value'
);
assert.dom('[data-test-ttl-value]').hasValue('6', 'time value shows as 6');
assert.dom('[data-test-select="ttl-unit"]').hasValue('m', 'unit value shows as m (minutes)');
});
test('it recalculates seconds when unit is changed and recalculateSeconds is on', async function (assert) {
await render(hbs`
<TtlPicker2
@label="clicktest"
@unit="s"
@time="120"
@onChange={{this.onChange}}
@enableTTL={{true}}
@recalculateSeconds={{true}}
/>
`);
await fillIn('[data-test-select="ttl-unit"]', 'm');
assert.ok(
this.onChange.calledWith({
enabled: true,
seconds: 7200,
timeString: '120m',
goSafeTimeString: '120m',
}),
'Seconds value is recalculated based on time and unit'
);
});
test('it sets default value to time and unit passed', async function (assert) {
await render(hbs`
<TtlPicker2
@onChange={{this.onChange}}
@initialValue="2h"
@enableTTL={{true}}
@time=4
@unit="d"
/>
`);
assert.dom('[data-test-ttl-value]').hasValue('2', 'time value is 2');
assert.dom('[data-test-select="ttl-unit"]').hasValue('h', 'unit is hours');
assert.ok(this.onChange.notCalled, 'it does not call onChange after render when changeOnInit is not set');
});
test('it is disabled on init if initialEnabled is false', async function (assert) {
await render(hbs`
<TtlPicker2
@label="inittest"
@onChange={{this.onChange}}
@initialValue="100m"
@initialEnabled={{false}}
/>
`);
assert.dom('[data-test-ttl-value]').doesNotExist('Value is not shown on mount');
assert.dom('[data-test-ttl-unit]').doesNotExist('Unit is not shown on mount');
await click('[data-test-toggle-input="inittest"]');
assert.dom('[data-test-ttl-value]').hasValue('100', 'time after toggle is 100');
assert.dom('[data-test-select="ttl-unit"]').hasValue('m', 'Unit is minutes after toggle');
});
test('it is enabled on init if initialEnabled is true', async function (assert) {
await render(hbs`
<TtlPicker2
@label="inittest"
@onChange={{this.onChange}}
@initialValue="100m"
@initialEnabled={{true}}
/>
`);
assert.dom('[data-test-ttl-value]').hasValue('100', 'time is shown on mount');
assert.dom('[data-test-select="ttl-unit"]').hasValue('m', 'Unit is shown on mount');
await click('[data-test-toggle-input="inittest"]');
assert.dom('[data-test-ttl-value]').doesNotExist('Value no longer shows after toggle');
assert.dom('[data-test-ttl-unit]').doesNotExist('Unit no longer shows after toggle');
});
test('it is enabled on init if initialEnabled evals to truthy', async function (assert) {
await render(hbs`
<TtlPicker2
@label="inittest"
@onChange={{this.onChange}}
@initialValue="100m"
@initialEnabled="true"
/>
`);
assert.dom('[data-test-ttl-value]').hasValue('100', 'time value is shown on mount');
assert.dom('[data-test-ttl-unit]').exists('Unit is shown on mount');
assert.dom('[data-test-select="ttl-unit"]').hasValue('m', 'Unit matches what is passed in');
});
test('it calls onChange', async function (assert) {
await render(hbs`
<TtlPicker2
@label="clicktest"
@unit="d"
@time="2"
@onChange={{this.onChange}}
@enableTTL={{false}}
/>
`);
await click('[data-test-toggle-input="clicktest"]');
assert.ok(this.onChange.calledOnce, 'it calls the passed onChange');
assert.ok(
this.onChange.calledWith({
enabled: true,
seconds: 172800,
timeString: '2d',
goSafeTimeString: '48h',
}),
'Converts day unit to go safe time'
);
});
test('it calls onChange on init when rendered if changeOnInit is true', async function (assert) {
await render(hbs`
<TtlPicker2
@label="changeOnInitTest"
@onChange={{this.onChange}}
@initialValue="100m"
@initialEnabled="true"
@changeOnInit={{true}}
/>
`);
assert.ok(
this.onChange.calledWith({
enabled: true,
seconds: 6000,
timeString: '100m',
goSafeTimeString: '100m',
}),
'Seconds value is recalculated based on time and unit'
);
assert.ok(this.onChange.calledOnce, 'it calls the passed onChange after render');
});
test('it converts to the largest round unit on init', async function (assert) {
await render(hbs`
<TtlPicker2
@label="convertunits"
@onChange={{this.onChange}}
@initialValue="60000s"
@initialEnabled="true"
/>
`);
assert.dom('[data-test-ttl-value]').hasValue('1000', 'time value is converted');
assert.dom('[data-test-select="ttl-unit"]').hasValue('m', 'unit value is m (minutes)');
});
test('it converts to the largest round unit on init when no unit provided', async function (assert) {
await render(hbs`
<TtlPicker2
@label="convertunits"
@onChange={{this.onChange}}
@initialValue={{86400}}
@initialEnabled="true"
/>
`);
assert.dom('[data-test-ttl-value]').hasValue('1', 'time value is converted');
assert.dom('[data-test-select="ttl-unit"]').hasValue('d', 'unit value is d (days)');
});
});

View File

@ -1,4 +1,5 @@
import { module, test } from 'qunit';
import Sinon from 'sinon';
import { setupRenderingTest } from 'ember-qunit';
import { render, click, fillIn } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
@ -7,38 +8,37 @@ import waitForError from 'vault/tests/helpers/wait-for-error';
module('Integration | Component | wrap ttl', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.lastOnChangeCall = null;
this.set('onChange', (val) => {
this.lastOnChangeCall = val;
});
});
test('it requires `onChange`', async function (assert) {
const promise = waitForError();
render(hbs`{{wrap-ttl}}`);
render(hbs`<WrapTtl />`);
const err = await promise;
assert.ok(err.message.includes('`onChange` handler is a required attr in'), 'asserts without onChange');
});
test('it renders', async function (assert) {
await render(hbs`{{wrap-ttl onChange=(action this.onChange)}}`);
assert.strictEqual(this.lastOnChangeCall, '30m', 'calls onChange with 30m default on first render');
assert.dom('label[for="toggle-Wrapresponse"] .ttl-picker-label').hasText('Wrap response');
const changeSpy = Sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`<WrapTtl @onChange={{this.onChange}} />`);
assert.ok(changeSpy.calledWithExactly('30m'), 'calls onChange with 30m default on render');
assert.dom('[data-test-ttl-form-label]').hasText('Wrap response');
});
test('it nulls out value when you uncheck wrapResponse', async function (assert) {
await render(hbs`{{wrap-ttl onChange=(action this.onChange)}}`);
await click('[data-test-toggle-label="Wrap response"]');
assert.strictEqual(this.lastOnChangeCall, null, 'calls onChange with null');
const changeSpy = Sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`<WrapTtl @onChange={{this.onChange}} />`);
await click('[data-test-ttl-form-label]');
assert.ok(changeSpy.calledWithExactly(null), 'calls onChange with null');
});
test('it sends value changes to onChange handler', async function (assert) {
await render(hbs`{{wrap-ttl onChange=(action this.onChange)}}`);
const changeSpy = Sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`<WrapTtl @onChange={{this.onChange}} />`);
// for testing purposes we need to input unit first because it keeps seconds value
await fillIn('[data-test-select="ttl-unit"]', 'h');
assert.strictEqual(this.lastOnChangeCall, '1800s', 'calls onChange correctly on time input');
await fillIn('[data-test-ttl-value="Wrap response"]', '20');
assert.strictEqual(this.lastOnChangeCall, '72000s', 'calls onChange correctly on unit change');
assert.ok(changeSpy.calledWithExactly('30h'), 'calls onChange correctly on time input');
await fillIn('[data-test-ttl-value]', '20');
assert.ok(changeSpy.calledWithExactly('20h'), 'calls onChange correctly on unit change');
});
});