Ui/ttl picker component (#8648)

TtlPicker2 Addon Component added
This commit is contained in:
Chelsea Shaw 2020-04-06 13:18:19 -05:00 committed by GitHub
parent 52f43020f0
commit 59f186d1b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 450 additions and 17 deletions

View File

@ -1,5 +1,6 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Vault UI](#vault-ui)
@ -64,7 +65,7 @@ long-form version of the npm script:
### Code Generators
Make use of the many generators for code, try `ember help generate` for more details
Make use of the many generators for code, try `ember help generate` for more details. If you're using a component that can be widely-used, consider making it an `addon` component instead (see [this PR](https://github.com/hashicorp/vault/pull/6629) for more details)
### Running Tests
@ -110,6 +111,7 @@ setting `VAULT_UI` environment variable.
## Vault Storybook
The Vault UI uses Storybook to catalog all of its components. Below are details for running and contributing to Storybook.
### Storybook Commands at a Glance
| Command | Description |
@ -151,6 +153,7 @@ Each component in `vault/ui/app/components` should have a corresponding `[compon
* @param {String} [closedLabel=More options] - The message to display when the toggle is closed.
*/
````
Note that placing a param inside brackets (e.g. `[closedLabel=More options]` indicates it is optional and has a default value of `'More options'`.)
2. Generate a new story with `ember generate story [name-of-component]`

View File

@ -25,7 +25,7 @@ export default Component.extend({
size: 'normal',
status: 'normal',
safeId: computed('name', function() {
return `toggle-${this.name}`;
return `toggle-${this.name.replace(/\W/g, '')}`;
}),
inputClasses: computed('size', 'status', function() {
const sizeClass = `is-${this.size}`;

View File

@ -0,0 +1,17 @@
.ttl-show-picker {
padding: 0.5rem 0 1.6rem 2.4rem;
}
.ttl-picker-label {
font-weight: bold;
}
input.has-error,
input.has-error:focus,
input.has-error:hover {
border-color: $red-dark;
}
.ttl-value-error {
margin-top: 0.3em;
}

View File

@ -91,6 +91,7 @@
@import './components/toolbar';
@import './components/tool-tip';
@import './components/transit-card';
@import './components/ttl-picker2';
@import './components/unseal-warning';
@import './components/ui-wizard';
@import './components/vault-loading';

View File

@ -25,7 +25,6 @@
.toggle[type='checkbox'] + label {
position: relative;
display: inline-block;
font-size: 1rem;
padding-left: 3.5rem;
cursor: pointer;
}
@ -65,8 +64,7 @@
.toggle[type='checkbox'] {
&.is-small {
+ label {
font-size: $size-8;
font-weight: bold;
font-size: $size-7;
padding-left: $size-8 * 2.5;
margin: 0 0.25rem;
&::before {

View File

@ -8,4 +8,10 @@
disabled=disabled
data-test-toggle-input=name
}}
<label data-test-toggle-label={{name}} for={{safeId}}>{{yield}}</label>
<label data-test-toggle-label={{name}} for={{safeId}}>
{{#if (has-block)}}
{{yield}}
{{else}}
{{name}}
{{/if}}
</label>

View File

@ -9,7 +9,11 @@ const ERROR_MESSAGE = 'TTLs must be specified in whole number increments, please
/**
* @module TtlPicker
* `TtlPicker` components are used to expand and collapse content with a toggle.
* `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.
*
* @example
* ```js

View File

@ -0,0 +1,117 @@
/**
* @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}.
* @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 time=30 {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
*/
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-picker2';
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,
enableTTL: false,
label: 'Time to live (TTL)',
helperTextDisabled: 'Allow tokens to be used indefinitely',
helperTextEnabled: 'Disable the use of the token after',
time: 30,
unit: 'm',
recalculationTimeout: 5000,
unitOptions: computed(function() {
return [
{ label: 'seconds', value: 's' },
{ label: 'minutes', value: 'm' },
{ label: 'hours', value: 'h' },
{ label: 'days', value: 'd' },
];
}),
TTL: computed('enableTTL', 'seconds', function() {
let { time, unit, enableTTL, seconds } = this.getProperties('time', 'unit', 'enableTTL', 'seconds');
return {
enabled: enableTTL,
seconds,
timeString: time + unit,
};
}),
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.onChange(this.TTL);
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.onChange(this.TTL);
},
toggleEnabled() {
this.toggleProperty('enableTTL');
this.onChange(this.TTL);
},
},
});

View File

@ -0,0 +1,55 @@
<Toggle
@name={{label}}
@status="success"
@size="small"
@onChange={{action 'toggleEnabled'}}
@checked={{enableTTL}}
data-test-ttl-toggle
>
<span class="ttl-picker-label is-large">{{label}}</span><br/>
<span class="has-text-grey">{{helperText}}</span>
</Toggle>
{{#if enableTTL}}
<div class="ttl-show-picker">
<div class="field is-grouped is-marginless">
<div class="control is-marginless">
<input
data-test-ttl-value
value={{time}}
id="time-{{elementId}}"
type="text"
name="time"
class="input{{if errorMessage " has-error"}}"
oninput={{perform updateTime value="target.value"}}
pattern="[0-9]*"
/>
</div>
<div class="control">
<Select
data-test-ttl-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}}
</div>
{{yield}}
{{/if}}

View File

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

View File

@ -1,8 +1,13 @@
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in lib/core/addon/components/ttl-picker.js. To make changes, first edit that file and run "yarn gen-story-md ttl-picker" to re-generate the content.-->
## TtlPicker
`TtlPicker` components are used to expand and collapse content with a toggle.
`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.
**Params**
| Param | Type | Default | Description |
| --- | --- | --- | --- |
@ -21,7 +26,7 @@
**See**
- [Uses of TtlPicker](https://github.com/hashicorp/vault/search?l=Handlebars&q=TtlPicker)
- [TtlPicker Source Code](https://github.com/hashicorp/vault/blob/master/ui/app/components/ttl-picker.js)
- [Uses of TtlPicker](https://github.com/hashicorp/vault/search?l=Handlebars&q=TtlPicker+OR+ttl-picker)
- [TtlPicker Source Code](https://github.com/hashicorp/vault/blob/master/ui/lib/core/addon/components/ttl-picker.js)
---

View File

@ -2,7 +2,7 @@ import hbs from 'htmlbars-inline-precompile';
import { storiesOf } from '@storybook/ember';
import notes from './ttl-picker.md';
storiesOf('TtlPicker/', module)
storiesOf('TTL/TtlPicker/', module)
.addParameters({ options: { showPanel: false } })
.add(
`TtlPicker`,

View File

@ -0,0 +1,33 @@
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in lib/core/addon/components/ttl-picker2.js. To make changes, first edit that file and run "yarn gen-story-md ttl-picker2" to re-generate the content.-->
## 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)
**Params**
| Param | Type | Default | Description |
| -------------------- | --------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| onChange | <code>function</code> | | This function will be passed a TTL object, which includes enabled{bool}, seconds{number}, timeString{string}. |
| label | <code>String</code> | <code>&quot;Time to live (TTL)"</code> | Label is the main label that lives next to the toggle. |
| helperTextDisabled | <code>String</code> | <code>&quot;Allow tokens to be used indefinitely"</code> | This helper text is shown under the label when the toggle is switched off |
| helperTextEnabled | <code>String</code> | <code>&quot;Disable the use of the token after" </code> | This helper text is shown under the label when the toggle is switched on |
| time | <code>Number</code> | <code>30</code> | The time (in the default units) which will be adjustable by the user of the form |
| unit | <code>String</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
<TtlPicker2 @onChange={{handleChange}} @time={{defaultTime}} @unit={{defaultUnit}}/>
```
**See**
- [Uses of TtlPicker2](https://github.com/hashicorp/vault/search?l=Handlebars&q=TtlPicker2+OR+ttl-picker2)
- [TtlPicker2 Source Code](https://github.com/hashicorp/vault/blob/master/ui/lib/core/addon/components/ttl-picker2.js)
---

View File

@ -0,0 +1,59 @@
import hbs from 'htmlbars-inline-precompile';
import { storiesOf } from '@storybook/ember';
import notes from './ttl-picker2.md';
import { withKnobs, text, boolean, select } from '@storybook/addon-knobs';
storiesOf('TTL/TtlPicker2/', module)
.addParameters({ options: { showPanel: true } })
.addDecorator(withKnobs())
.add(
`TtlPicker2|single`,
() => ({
template: hbs`
<h5 class="title is-5">Ttl Picker2</h5>
<TtlPicker2
@enableTTL={{enableTTL}}
@unit={{unit}}
@time={{time}}
@label={{label}}
@helperTextDisabled={{helperTextDisabled}}
@helperTextEnabled={{helperTextEnabled}}
@onChange={{onChange}}
/>
`,
context: {
enableTTL: boolean('enableTTL', true),
unit: select('unit', ['s', 'm', 'h', 'd'], 'm'),
time: text('time', '40'),
label: text('label', 'Main label of TTL'),
helperTextDisabled: text('helperTextDisabled', 'This helper text displays when TTL is disabled'),
helperTextEnabled: text('helperTextEnabled', 'Enabling TTL will show this text instead'),
onChange: ttl => {
console.log('onChange fired', ttl);
},
},
}),
{ notes }
)
.add(
`TtlPicker2|nested`,
() => ({
template: hbs`
<h5 class="title is-5">Ttl Picker2</h5>
<TtlPicker2 @onChange={{onChange}}>
<TtlPicker2
@onChange={{onChange}}
@label="Maximum time to live (Max TTL)"
@helperTextDisabled="Allow tokens to be renewed indefinitely"
@unit="h"
/>
</TtlPicker2>
`,
context: {
onChange: ttl => {
console.log('onChange fired', ttl);
},
},
}),
{ notes }
);

View File

@ -12,7 +12,7 @@ let handler = (data, e) => {
module('Integration | Component | toggle', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
test('it renders with name as default label', async function(assert) {
this.set('handler', sinon.spy(handler));
await render(hbs`<Toggle
@ -20,7 +20,7 @@ module('Integration | Component | toggle', function(hooks) {
@name="thing"
/>`);
assert.equal(findAll('label')[0].textContent.trim(), '');
assert.equal(findAll('label')[0].textContent.trim(), 'thing');
await render(hbs`
<Toggle
@ -47,4 +47,20 @@ module('Integration | Component | toggle', function(hooks) {
`);
assert.dom('.toggle.is-small').exists('toggle has is-small class');
});
test('it sets the id of the input correctly', async function(assert) {
this.set('handler', sinon.spy(handler));
await render(hbs`
<Toggle
@onChange={{handler}}
@name="my toggle #_has we!rd chars"
>
Label
</Toggle>
`);
assert.dom('#toggle-mytoggle_haswerdchars').exists('input has correct ID');
assert
.dom('label')
.hasAttribute('for', 'toggle-mytoggle_haswerdchars', 'label has correct for attribute');
});
});

View File

@ -0,0 +1,118 @@
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);
test('it renders time and unit inputs when TTL enabled', async function(assert) {
let changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker2
@onChange={{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) {
let changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker2
@onChange={{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) {
let changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker2
@label="clicktest"
@unit="m"
@time="10"
@onChange={{onChange}}
@enableTTL={{false}}
/>
`);
await click('[data-test-toggle-input="clicktest"]');
assert.ok(changeSpy.calledOnce, 'it calls the passed onChange');
assert.ok(
changeSpy.calledWith({
enabled: true,
seconds: 600,
timeString: '10m',
}),
'Passes the default values back to onChange'
);
});
test('it keeps seconds value when unit is changed', async function(assert) {
let changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker2
@label="clicktest"
@unit="s"
@time="360"
@onChange={{onChange}}
@enableTTL={{false}}
/>
`);
await click('[data-test-toggle-input="clicktest"]');
assert.ok(changeSpy.calledOnce, 'it calls the passed onChange');
assert.ok(
changeSpy.calledWith({
enabled: true,
seconds: 360,
timeString: '360s',
}),
'Changes enabled to true on click'
);
await fillIn('[data-test-select="ttl-unit"]', 'm');
assert.ok(
changeSpy.calledWith({
enabled: true,
seconds: 360,
timeString: '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) {
let changeSpy = sinon.spy();
this.set('onChange', changeSpy);
await render(hbs`
<TtlPicker2
@label="clicktest"
@unit="s"
@time="120"
@onChange={{onChange}}
@enableTTL={{true}}
@recalculateSeconds={{true}}
/>
`);
await fillIn('[data-test-select="ttl-unit"]', 'm');
assert.ok(
changeSpy.calledWith({
enabled: true,
seconds: 7200,
timeString: '120m',
}),
'Seconds value is recalculated based on time and unit'
);
});
});