Ui/ttl form (#9572)

* Add TtlForm and extend TtlPicker2 from new component
This commit is contained in:
Chelsea Shaw 2020-07-27 11:02:31 -05:00 committed by GitHub
parent f145c66d22
commit f5f11234af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 243 additions and 49 deletions

View File

@ -0,0 +1,104 @@
/**
* @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() {
let { time, unit, seconds } = this.getProperties('time', 'unit', 'seconds');
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', '');
let parsedTime;
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

@ -21,12 +21,10 @@
* @param changeOnInit=false {Boolean} - set this value if you'd like the passed onChange function to be called on component initialization
*/
import Ember from 'ember';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { task, timeout } from 'ember-concurrency';
import { typeOf } from '@ember/utils';
import Duration from 'Duration.js';
import TtlForm from './ttl-form';
import layout from '../templates/components/ttl-picker2';
const secondsMap = {
@ -36,14 +34,11 @@ const secondsMap = {
d: 86400,
};
const validUnits = ['s', 'm', 'h', 'd'];
const convertToSeconds = (time, unit) => {
return time * secondsMap[unit];
};
const convertFromSeconds = (seconds, unit) => {
return seconds / secondsMap[unit];
};
export default Component.extend({
export default TtlForm.extend({
layout,
enableTTL: false,
label: 'Time to live (TTL)',
@ -52,7 +47,6 @@ export default Component.extend({
description: '',
time: 30,
unit: 's',
recalculationTimeout: 5000,
initialValue: null,
changeOnInit: false,
@ -88,7 +82,6 @@ export default Component.extend({
time = seconds;
}
} catch (e) {
console.error(e);
// if parsing fails leave as default 30s
}
}
@ -121,52 +114,13 @@ export default Component.extend({
};
this.onChange(ttl);
},
updateTime: task(function*(newTime) {
this.set('errorMessage', '');
let parsedTime;
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(),
recalculateTime(newUnit) {
const newTime = convertFromSeconds(this.seconds, newUnit);
this.setProperties({
time: newTime,
unit: newUnit,
});
},
seconds: computed('time', 'unit', function() {
return convertToSeconds(this.time, this.unit);
}),
helperText: computed('enableTTL', 'helperTextUnset', 'helperTextSet', function() {
return this.enableTTL ? this.helperTextEnabled : this.helperTextDisabled;
}),
errorMessage: null,
recalculateSeconds: false,
actions: {
updateUnit(newUnit) {
if (this.recalculateSeconds) {
this.set('unit', newUnit);
} else {
this.recalculateTime(newUnit);
}
this.handleChange();
},
toggleEnabled() {
this.toggleProperty('enableTTL');
this.handleChange();

View File

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

View File

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

View File

@ -0,0 +1,29 @@
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in lib/core/addon/components/ttl-form.js. To make changes, first edit that file and run "yarn gen-story-md ttl-form" to re-generate the content.-->
## 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.
**Params**
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| onChange | <code>function</code> | | 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} |
| [time] | <code>number</code> | | Time is the value that will be passed into the value input. Can be null/undefined to start if input is required. |
| [unit] | <code>unit</code> | <code>&quot;s&quot;</code> | 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) |
| [recalculationTimeout] | <code>number</code> | <code>5000</code> | This is the time, in milliseconds, that `recalculateSeconds` will be be true after time is updated |
**Example**
```js
<TtlForm @onChange={action handleChange} @unit={{m}}/>
```
**See**
- [Uses of TtlForm](https://github.com/hashicorp/vault/search?l=Handlebars&q=TtlForm+OR+ttl-form)
- [TtlForm Source Code](https://github.com/hashicorp/vault/blob/master/ui/lib/core/addon/components/ttl-form.js)
---

View File

@ -0,0 +1,17 @@
import hbs from 'htmlbars-inline-precompile';
import { storiesOf } from '@storybook/ember';
import notes from './ttl-form.md';
storiesOf('TtlForm', module)
.addParameters({ options: { showPanel: true } })
.add(
`TtlForm`,
() => ({
template: hbs`
<h5 class="title is-5">Ttl Form</h5>
<TtlForm/>
`,
context: {},
}),
{ notes }
);

View File

@ -0,0 +1,49 @@
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={{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={{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) {
let changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlForm
@unit="h"
@time="3"
@onChange={{onChange}}
/>
`);
assert.dom('[data-test-select="ttl-unit"]').hasValue('h', 'unit value initially shows as h (hours)');
});
});