mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 20:36:26 +02:00
Ui/ttl form (#9572)
* Add TtlForm and extend TtlPicker2 from new component
This commit is contained in:
parent
f145c66d22
commit
f5f11234af
104
ui/lib/core/addon/components/ttl-form.js
Normal file
104
ui/lib/core/addon/components/ttl-form.js
Normal 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();
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -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();
|
||||
|
||||
40
ui/lib/core/addon/templates/components/ttl-form.hbs
Normal file
40
ui/lib/core/addon/templates/components/ttl-form.hbs
Normal 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}}
|
||||
1
ui/lib/core/app/components/ttl-form.js
Normal file
1
ui/lib/core/app/components/ttl-form.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from 'core/components/ttl-form';
|
||||
29
ui/lib/core/stories/ttl-form.md
Normal file
29
ui/lib/core/stories/ttl-form.md
Normal 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>"s"</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)
|
||||
|
||||
---
|
||||
17
ui/lib/core/stories/ttl-form.stories.js
Normal file
17
ui/lib/core/stories/ttl-form.stories.js
Normal 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 }
|
||||
);
|
||||
49
ui/tests/integration/components/ttl-form-test.js
Normal file
49
ui/tests/integration/components/ttl-form-test.js
Normal 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)');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user