Search select (#5851)
This commit is contained in:
parent
c67ef8be09
commit
c84c8e01b2
|
@ -30,6 +30,8 @@ export default ApplicationAdapter.extend({
|
|||
},
|
||||
|
||||
query(store, type) {
|
||||
return this.ajax(this.buildURL(type.modelName), 'GET', { data: { list: true } });
|
||||
return this.ajax(this.buildURL(type.modelName), 'GET', {
|
||||
data: { list: true },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import Component from '@ember/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default Component.extend({
|
||||
'data-test-component': 'search-select',
|
||||
classNames: ['field', 'search-select'],
|
||||
store: service(),
|
||||
|
||||
/*
|
||||
* @public
|
||||
* @param Function
|
||||
*
|
||||
* Function called when any of the inputs change
|
||||
* accepts a single param `value`
|
||||
*
|
||||
*/
|
||||
onChange: () => {},
|
||||
|
||||
/*
|
||||
* @public
|
||||
* @param String | Array
|
||||
* A comma-separated string or an array of strings.
|
||||
* Defaults to an empty array.
|
||||
*
|
||||
*/
|
||||
inputValue: computed(function() {
|
||||
return [];
|
||||
}),
|
||||
selectedOptions: null, //list of selected options
|
||||
options: null, //all possible options
|
||||
shouldUseFallback: false,
|
||||
shouldRenderName: false,
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('selectedOptions', this.inputValue || []);
|
||||
},
|
||||
fetchOptions: task(function*() {
|
||||
for (let modelType of this.models) {
|
||||
if (modelType.includes('identity')) {
|
||||
this.set('shouldRenderName', true);
|
||||
}
|
||||
try {
|
||||
let options = yield this.store.query(modelType, {});
|
||||
options = options.toArray().map(option => {
|
||||
option.searchText = `${option.name} ${option.id}`;
|
||||
return option;
|
||||
});
|
||||
let formattedOptions = this.selectedOptions.map(option => {
|
||||
let matchingOption = options.findBy('id', option);
|
||||
options.removeObject(matchingOption);
|
||||
return { id: option, name: matchingOption.name, searchText: matchingOption.searchText };
|
||||
});
|
||||
this.set('selectedOptions', formattedOptions);
|
||||
if (this.options) {
|
||||
options = this.options.concat(options);
|
||||
}
|
||||
this.set('options', options);
|
||||
} catch (err) {
|
||||
if (err.httpStatus === 404) {
|
||||
//leave options alone, it's okay
|
||||
return;
|
||||
}
|
||||
if (err.httpStatus === 403) {
|
||||
this.set('shouldUseFallback', true);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}).on('didInsertElement'),
|
||||
handleChange() {
|
||||
if (this.selectedOptions.length && typeof this.selectedOptions.firstObject === 'object') {
|
||||
this.onChange(Array.from(this.selectedOptions, option => option.id));
|
||||
} else {
|
||||
this.onChange(this.selectedOptions);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
onChange(val) {
|
||||
this.onChange(val);
|
||||
},
|
||||
selectOption(option) {
|
||||
this.selectedOptions.pushObject(option);
|
||||
this.options.removeObject(option);
|
||||
this.handleChange();
|
||||
},
|
||||
discardSelection(selected) {
|
||||
this.selectedOptions.removeObject(selected);
|
||||
this.options.pushObject(selected);
|
||||
this.handleChange();
|
||||
},
|
||||
},
|
||||
});
|
|
@ -83,7 +83,7 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
setType() {
|
||||
const list = this.get('inputList');
|
||||
const list = this.inputList;
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
|
@ -91,9 +91,7 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
toVal() {
|
||||
const inputs = this.get('inputList')
|
||||
.filter(x => x.value)
|
||||
.mapBy('value');
|
||||
const inputs = this.inputList.filter(x => x.value).mapBy('value');
|
||||
if (this.get('format') === 'string') {
|
||||
return inputs.join(',');
|
||||
}
|
||||
|
@ -101,8 +99,8 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
toList() {
|
||||
let input = this.get('inputValue') || [];
|
||||
const inputList = this.get('inputList');
|
||||
let input = this.inputValue || [];
|
||||
const inputList = this.inputList;
|
||||
if (typeof input === 'string') {
|
||||
input = input.split(',');
|
||||
}
|
||||
|
@ -111,22 +109,22 @@ export default Component.extend({
|
|||
|
||||
actions: {
|
||||
inputChanged(idx, val) {
|
||||
const inputObj = this.get('inputList').objectAt(idx);
|
||||
const onChange = this.get('onChange');
|
||||
const inputObj = this.inputList.objectAt(idx);
|
||||
const onChange = this.onChange;
|
||||
set(inputObj, 'value', val);
|
||||
onChange(this.toVal());
|
||||
},
|
||||
|
||||
addInput() {
|
||||
const inputList = this.get('inputList');
|
||||
const inputList = this.inputList;
|
||||
if (inputList.get('lastObject.value') !== '') {
|
||||
inputList.pushObject({ value: '' });
|
||||
}
|
||||
},
|
||||
|
||||
removeInput(idx) {
|
||||
const onChange = this.get('onChange');
|
||||
const inputs = this.get('inputList');
|
||||
const onChange = this.onChange;
|
||||
const inputs = this.inputList;
|
||||
inputs.removeObject(inputs.objectAt(idx));
|
||||
onChange(this.toVal());
|
||||
},
|
||||
|
|
|
@ -22,7 +22,10 @@ export default IdentityModel.extend({
|
|||
editType: 'kv',
|
||||
}),
|
||||
policies: attr({
|
||||
editType: 'stringArray',
|
||||
label: 'Policies',
|
||||
editType: 'searchSelect',
|
||||
fallbackComponent: 'string-list',
|
||||
models: ['policy/acl', 'policy/rgp'],
|
||||
}),
|
||||
creationTime: attr('string', {
|
||||
readOnly: true,
|
||||
|
|
|
@ -36,19 +36,28 @@ export default IdentityModel.extend({
|
|||
editType: 'kv',
|
||||
}),
|
||||
policies: attr({
|
||||
editType: 'stringArray',
|
||||
label: 'Policies',
|
||||
editType: 'searchSelect',
|
||||
fallbackComponent: 'string-list',
|
||||
models: ['policy/acl', 'policy/rgp'],
|
||||
}),
|
||||
memberGroupIds: attr({
|
||||
label: 'Member Group IDs',
|
||||
editType: 'stringArray',
|
||||
editType: 'searchSelect',
|
||||
fallbackComponent: 'string-list',
|
||||
models: ['identity/group'],
|
||||
}),
|
||||
parentGroupIds: attr({
|
||||
label: 'Parent Group IDs',
|
||||
editType: 'stringArray',
|
||||
editType: 'searchSelect',
|
||||
fallbackComponent: 'string-list',
|
||||
models: ['identity/group'],
|
||||
}),
|
||||
memberEntityIds: attr({
|
||||
label: 'Member Entity IDs',
|
||||
editType: 'stringArray',
|
||||
editType: 'searchSelect',
|
||||
fallbackComponent: 'string-list',
|
||||
models: ['identity/entity'],
|
||||
}),
|
||||
hasMembers: computed(
|
||||
'memberEntityIds',
|
||||
|
|
|
@ -4,23 +4,14 @@ import ApplicationSerializer from '../application';
|
|||
export default ApplicationSerializer.extend({
|
||||
normalizeItems(payload) {
|
||||
if (payload.data.keys && Array.isArray(payload.data.keys)) {
|
||||
return payload.data.keys;
|
||||
return payload.data.keys.map(key => {
|
||||
let model = payload.data.key_info[key];
|
||||
model.id = key;
|
||||
return model;
|
||||
});
|
||||
}
|
||||
assign(payload, payload.data);
|
||||
delete payload.data;
|
||||
return payload;
|
||||
},
|
||||
|
||||
extractLazyPaginatedData(payload) {
|
||||
let list;
|
||||
list = payload.data.keys.map(key => {
|
||||
let model = payload.data.key_info[key];
|
||||
model.id = key;
|
||||
return model;
|
||||
});
|
||||
delete payload.data.key_info;
|
||||
return list.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
@import "ember-basic-dropdown";
|
||||
@import "./core";
|
||||
@import 'ember-basic-dropdown';
|
||||
@import 'ember-power-select';
|
||||
@import './core';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.popup-menu-content {
|
||||
.popup-menu-content,
|
||||
.ember-power-select-options {
|
||||
border-radius: 2px;
|
||||
margin: -2px 0 0 0;
|
||||
|
||||
|
@ -23,54 +24,65 @@
|
|||
.menu {
|
||||
padding: $size-11 0;
|
||||
|
||||
button.link,
|
||||
a,
|
||||
.menu-item {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
display: block;
|
||||
height: auto;
|
||||
font-size: $size-7;
|
||||
font-weight: $font-weight-semibold;
|
||||
padding: $size-9 $size-8;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button.link,
|
||||
a {
|
||||
color: $menu-item-color;
|
||||
&:hover {
|
||||
background-color: $menu-item-hover-background-color;
|
||||
color: $menu-item-hover-color;
|
||||
}
|
||||
|
||||
&.is-destroy {
|
||||
color: $red;
|
||||
|
||||
&:hover {
|
||||
background-color: $red;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
small code {
|
||||
margin-left: $spacing-xs;
|
||||
}
|
||||
}
|
||||
|
||||
button.link,
|
||||
a,
|
||||
.ember-power-select-option,
|
||||
.ember-power-select-option[aria-current='true'],
|
||||
.menu-item {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
display: block;
|
||||
height: auto;
|
||||
font-size: $size-7;
|
||||
font-weight: $font-weight-semibold;
|
||||
padding: $size-9 $size-8;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button.link,
|
||||
.ember-power-select-option,
|
||||
.ember-power-select-option[aria-current='true'],
|
||||
a {
|
||||
background-color: $white;
|
||||
color: $menu-item-color;
|
||||
|
||||
&:hover {
|
||||
background-color: $menu-item-hover-background-color;
|
||||
color: $menu-item-hover-color;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: $menu-item-active-background-color;
|
||||
color: $menu-item-active-color;
|
||||
}
|
||||
|
||||
&.is-destroy {
|
||||
color: $red;
|
||||
|
||||
&:hover {
|
||||
background-color: $red;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
color: $grey-dark;
|
||||
font-size: $size-9;
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
.ember-power-select-dropdown {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
overflow: visible;
|
||||
|
||||
&.ember-power-select-dropdown.ember-basic-dropdown-content--below {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ember-power-select-trigger {
|
||||
border: 0;
|
||||
border-radius: $radius;
|
||||
padding-right: 0;
|
||||
|
||||
&--active {
|
||||
outline-width: 3px;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.ember-power-select-trigger:focus,
|
||||
.ember-power-select-trigger--active {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ember-power-select-status-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ember-power-select-search {
|
||||
left: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translateY(-100%);
|
||||
z-index: -1;
|
||||
|
||||
&::after {
|
||||
background: $white;
|
||||
bottom: $spacing-xxs;
|
||||
content: '';
|
||||
left: $spacing-xxs + $spacing-l;
|
||||
position: absolute;
|
||||
right: $spacing-xxs;
|
||||
top: $spacing-xxs;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.ember-power-select-search-input {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: $spacing-xxs $spacing-s;
|
||||
padding-left: $spacing-xxs + $spacing-l;
|
||||
}
|
||||
|
||||
.ember-power-select-options {
|
||||
background: $white;
|
||||
border: $base-border;
|
||||
box-shadow: $box-shadow-middle;
|
||||
margin: -4px $spacing-xs 0;
|
||||
padding: $spacing-xxs 0;
|
||||
|
||||
.ember-power-select-option,
|
||||
.ember-power-select-option[aria-current='true'] {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ember-power-select-option[aria-current='true'] {
|
||||
@extend .ember-power-select-option:hover;
|
||||
}
|
||||
|
||||
.ember-power-select-option--no-matches-message {
|
||||
color: $grey;
|
||||
font-size: $size-8;
|
||||
font-weight: $font-weight-semibold;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: transparent;
|
||||
color: $grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-select-list-item {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 0 $spacing-m;
|
||||
padding: $spacing-xxs;
|
||||
justify-content: space-between;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: $light-border;
|
||||
}
|
||||
|
||||
.control .button {
|
||||
color: $grey-light;
|
||||
min-width: auto;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-select-list-key {
|
||||
color: $grey;
|
||||
font-size: $size-8;
|
||||
}
|
||||
|
||||
.ember-power-select-dropdown.ember-basic-dropdown-content {
|
||||
animation: none;
|
||||
|
||||
.ember-power-select-options {
|
||||
animation: drop-fade-above 0.15s;
|
||||
}
|
||||
}
|
|
@ -69,6 +69,7 @@
|
|||
@import './components/popup-menu';
|
||||
@import './components/radial-progress';
|
||||
@import './components/role-item';
|
||||
@import './components/search-select';
|
||||
@import './components/secret-control-bar';
|
||||
@import './components/shamir-progress';
|
||||
@import './components/sidebar';
|
||||
|
|
|
@ -184,8 +184,11 @@ label {
|
|||
}
|
||||
}
|
||||
|
||||
.select:not(.is-multiple)::after,
|
||||
.select:not(.is-multiple)::before {
|
||||
.select:not(.is-multiple) {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.select:not(.is-multiple)::after {
|
||||
border-color: $black;
|
||||
border-width: 2px;
|
||||
margin-top: 0;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.title:not(:last-child),
|
||||
.subtitle:not(:last-child) {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
@ -11,3 +12,7 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section .title {
|
||||
margin-bottom: $spacing-s;
|
||||
}
|
||||
|
|
|
@ -1,52 +1,84 @@
|
|||
{{#unless (or (and attr.options.editType (not-eq attr.options.editType "textarea")) (eq attr.type "boolean"))}}
|
||||
{{#unless
|
||||
(or
|
||||
(and attr.options.editType (not-eq attr.options.editType "textarea"))
|
||||
(eq attr.type "boolean")
|
||||
)
|
||||
}}
|
||||
<label for="{{attr.name}}" class="is-label">
|
||||
{{labelString}}
|
||||
{{#if attr.options.helpText}}
|
||||
{{#info-tooltip}}
|
||||
<span data-test-help-text>
|
||||
{{attr.options.helpText}}
|
||||
</span>
|
||||
<span data-test-help-text>
|
||||
{{attr.options.helpText}}
|
||||
</span>
|
||||
{{/info-tooltip}}
|
||||
{{/if}}
|
||||
</label>
|
||||
{{/unless}}
|
||||
{{#if attr.options.possibleValues}}
|
||||
<div class="control is-expanded" >
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
name="{{attr.name}}"
|
||||
id="{{attr.name}}"
|
||||
onchange={{action (action "setAndBroadcast" valuePath) value="target.value"}}
|
||||
onchange={{action
|
||||
(action "setAndBroadcast" valuePath)
|
||||
value="target.value"
|
||||
}}
|
||||
data-test-input={{attr.name}}
|
||||
>
|
||||
{{#each (path-or-array attr.options.possibleValues model) as |val|}}
|
||||
<option selected={{eq (get model valuePath) (or val.value val)}} value={{or val.value val}}>
|
||||
<option
|
||||
selected={{eq (get model valuePath) (or val.value val)}}
|
||||
value={{or val.value val}}
|
||||
>
|
||||
{{or val.displayName val}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{{else if (and (eq attr.type 'string') (eq attr.options.editType 'boolean'))}}
|
||||
{{else if (and (eq attr.type "string") (eq attr.options.editType "boolean"))}}
|
||||
<div class="b-checkbox">
|
||||
<input type="checkbox"
|
||||
id="{{attr.name}}"
|
||||
class="styled"
|
||||
checked={{eq (get model valuePath) attr.options.trueValue}}
|
||||
onchange={{action (action "setAndBroadcastBool" valuePath attr.options.trueValue attr.options.falseValue) value="target.checked"}}
|
||||
data-test-input={{attr.name}}
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="{{attr.name}}"
|
||||
class="styled"
|
||||
checked={{eq (get model valuePath) attr.options.trueValue}}
|
||||
onchange={{action
|
||||
(action
|
||||
"setAndBroadcastBool"
|
||||
valuePath
|
||||
attr.options.trueValue
|
||||
attr.options.falseValue
|
||||
)
|
||||
value="target.checked"
|
||||
}}
|
||||
data-test-input={{attr.name}}
|
||||
/>
|
||||
|
||||
<label for="{{attr.name}}" class="is-label">
|
||||
{{labelString}}
|
||||
{{#if attr.options.helpText}}
|
||||
{{#info-tooltip}}
|
||||
{{attr.options.helpText}}
|
||||
{{/info-tooltip}}
|
||||
{{#info-tooltip}}{{attr.options.helpText}}{{/info-tooltip}}
|
||||
{{/if}}
|
||||
</label>
|
||||
</div>
|
||||
{{else if (eq attr.options.editType "searchSelect")}}
|
||||
<div class="form-section">
|
||||
<SearchSelect
|
||||
@id={{attr.name}}
|
||||
@models={{attr.options.models}}
|
||||
@onChange={{action (action "setAndBroadcast" valuePath)}}
|
||||
@inputValue={{get model valuePath}}
|
||||
@helpText={{attr.options.helpText}}
|
||||
@warning={{attr.options.warning}}
|
||||
@label={{labelString}}
|
||||
@fallbackComponent={{attr.options.fallbackComponent}}
|
||||
/>
|
||||
|
||||
{{else if (eq attr.options.editType 'mountAccessor')}}
|
||||
</div>
|
||||
{{else if (eq attr.options.editType "mountAccessor")}}
|
||||
{{mount-accessor-select
|
||||
name=attr.name
|
||||
label=labelString
|
||||
|
@ -54,8 +86,8 @@
|
|||
helpText=attr.options.helpText
|
||||
value=(get model valuePath)
|
||||
onChange=(action "setAndBroadcast" valuePath)
|
||||
}}
|
||||
{{else if (eq attr.options.editType 'kv')}}
|
||||
}}
|
||||
{{else if (eq attr.options.editType "kv")}}
|
||||
{{kv-object-editor
|
||||
value=(get model valuePath)
|
||||
onChange=(action "setAndBroadcast" valuePath)
|
||||
|
@ -63,15 +95,15 @@
|
|||
warning=attr.options.warning
|
||||
helpText=attr.options.helpText
|
||||
}}
|
||||
{{else if (eq attr.options.editType 'file')}}
|
||||
{{else if (eq attr.options.editType "file")}}
|
||||
{{text-file
|
||||
index=''
|
||||
index=""
|
||||
file=file
|
||||
onChange=(action 'setFile')
|
||||
onChange=(action "setFile")
|
||||
warning=attr.options.warning
|
||||
label=labelString
|
||||
}}
|
||||
{{else if (eq attr.options.editType 'ttl')}}
|
||||
{{else if (eq attr.options.editType "ttl")}}
|
||||
{{ttl-picker
|
||||
data-test-input=attr.name
|
||||
initialValue=(or (get model valuePath) attr.options.defaultValue)
|
||||
|
@ -80,7 +112,7 @@
|
|||
setDefaultValue=(or attr.options.setDefault false)
|
||||
onChange=(action (action "setAndBroadcast" valuePath))
|
||||
}}
|
||||
{{else if (eq attr.options.editType 'stringArray')}}
|
||||
{{else if (eq attr.options.editType "stringArray")}}
|
||||
{{string-list
|
||||
label=labelString
|
||||
warning=attr.options.warning
|
||||
|
@ -96,29 +128,33 @@
|
|||
/>
|
||||
{{else if (or (eq attr.type 'number') (eq attr.type 'string'))}}
|
||||
<div class="control">
|
||||
{{#if (eq attr.options.editType 'textarea')}}
|
||||
{{#if (eq attr.options.editType "textarea")}}
|
||||
<textarea
|
||||
data-test-input={{attr.name}}
|
||||
id={{attr.name}}
|
||||
value={{or (get model valuePath) attr.options.defaultValue}}
|
||||
oninput={{action (action "setAndBroadcast" valuePath) value="target.value"}}
|
||||
oninput={{action
|
||||
(action "setAndBroadcast" valuePath)
|
||||
value="target.value"
|
||||
}}
|
||||
class="textarea"
|
||||
></textarea>
|
||||
{{else if (eq attr.options.editType 'json')}}
|
||||
|
||||
></textarea>
|
||||
{{else if (eq attr.options.editType "json")}}
|
||||
<label for="{{attr.name}}" class="is-label">
|
||||
{{labelString}}
|
||||
{{#if attr.options.helpText}}
|
||||
{{#info-tooltip}}
|
||||
<span data-test-help-text>
|
||||
{{attr.options.helpText}}
|
||||
</span>
|
||||
<span data-test-help-text>
|
||||
{{attr.options.helpText}}
|
||||
</span>
|
||||
{{/info-tooltip}}
|
||||
{{/if}}
|
||||
</label>
|
||||
{{json-editor
|
||||
value=(if (get model valuePath) (stringify (jsonify (get model valuePath))))
|
||||
valueUpdated=(action "codemirrorUpdated" attr.name 'string')
|
||||
value=(if
|
||||
(get model valuePath) (stringify (jsonify (get model valuePath)))
|
||||
)
|
||||
valueUpdated=(action "codemirrorUpdated" attr.name "string")
|
||||
}}
|
||||
{{else}}
|
||||
<input
|
||||
|
@ -126,40 +162,52 @@
|
|||
id={{attr.name}}
|
||||
autocomplete="off"
|
||||
value={{or (get model valuePath) attr.options.defaultValue}}
|
||||
oninput={{action (action "setAndBroadcast" valuePath) value="target.value"}}
|
||||
oninput={{action
|
||||
(action "setAndBroadcast" valuePath)
|
||||
value="target.value"
|
||||
}}
|
||||
class="input"
|
||||
/>
|
||||
/>
|
||||
|
||||
{{#if attr.options.validationAttr}}
|
||||
{{#if (and (get model valuePath) (not (get model attr.options.validationAttr)))}}
|
||||
{{#if
|
||||
(and
|
||||
(get model valuePath) (not (get model attr.options.validationAttr))
|
||||
)
|
||||
}}
|
||||
<AlertInline
|
||||
@type="danger"
|
||||
@message={{attr.options.invalidMessage}}
|
||||
/>
|
||||
/>
|
||||
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else if (eq attr.type 'boolean')}}
|
||||
{{else if (eq attr.type "boolean")}}
|
||||
<div class="b-checkbox">
|
||||
<input type="checkbox"
|
||||
id="{{attr.name}}"
|
||||
class="styled"
|
||||
checked={{get model attr.name}}
|
||||
onchange={{action (action "setAndBroadcast" valuePath) value="target.checked"}}
|
||||
data-test-input={{attr.name}}
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="{{attr.name}}"
|
||||
class="styled"
|
||||
checked={{get model attr.name}}
|
||||
onchange={{action
|
||||
(action "setAndBroadcast" valuePath)
|
||||
value="target.checked"
|
||||
}}
|
||||
data-test-input={{attr.name}}
|
||||
/>
|
||||
|
||||
<label for="{{attr.name}}" class="is-label">
|
||||
{{labelString}}
|
||||
{{#if attr.options.helpText}}
|
||||
{{#info-tooltip}}
|
||||
{{attr.options.helpText}}
|
||||
{{/info-tooltip}}
|
||||
{{#info-tooltip}}{{attr.options.helpText}}{{/info-tooltip}}
|
||||
{{/if}}
|
||||
</label>
|
||||
</div>
|
||||
{{else if (eq attr.type 'object')}}
|
||||
{{else if (eq attr.type "object")}}
|
||||
{{json-editor
|
||||
value=(if (get model valuePath) (stringify (get model valuePath)) emptyData)
|
||||
valueUpdated=(action "codemirrorUpdated" attr.name false)
|
||||
}}
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -1,5 +1,5 @@
|
|||
{{#if label}}
|
||||
<label class="title is-5" data-test-kv-label="true">
|
||||
<label class="title is-4" data-test-kv-label="true">
|
||||
{{label}}
|
||||
{{#if helpText}}
|
||||
{{#info-tooltip}}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<div class="field">
|
||||
<p class="control has-icons-left has-icons-right">
|
||||
<span class="input has-text-grey-light">Search</span>
|
||||
{{i-con glyph="ios-search-strong" class="is-left has-text-grey-light" size=16}}
|
||||
</p>
|
||||
</div>
|
|
@ -0,0 +1,58 @@
|
|||
{{#if shouldUseFallback}}
|
||||
{{component
|
||||
fallbackComponent
|
||||
label=label
|
||||
onChange=(action "onChange")
|
||||
inputValue=inputValue
|
||||
warning=warning
|
||||
helpText=helpText
|
||||
}}
|
||||
{{else}}
|
||||
<label for="{{name}}" class="title is-4" data-test-field-label>
|
||||
{{label}}
|
||||
{{#if helpText}}
|
||||
{{#info-tooltip}}{{helpText}}{{/info-tooltip}}
|
||||
{{/if}}
|
||||
</label>
|
||||
{{#power-select
|
||||
options=options
|
||||
onchange=(action "selectOption")
|
||||
placeholderComponent="search-select-placeholder"
|
||||
renderInPlace=true
|
||||
searchField="searchText"
|
||||
verticalPosition="below" as |option|
|
||||
}}
|
||||
{{#if shouldRenderName}}
|
||||
{{option.name}}
|
||||
<small class="search-select-list-key" data-test-smaller-id="true">
|
||||
{{option.id}}
|
||||
</small>
|
||||
{{else}}
|
||||
{{option.id}}
|
||||
{{/if}}
|
||||
{{/power-select}}
|
||||
<ul class="search-select-list">
|
||||
{{#each selectedOptions as |selected|}}
|
||||
<li class="search-select-list-item" data-test-selected-option="true">
|
||||
{{#if shouldRenderName}}
|
||||
{{selected.name}}
|
||||
<small class="search-select-list-key" data-test-smaller-id="true">
|
||||
{{selected.id}}
|
||||
</small>
|
||||
{{else}}
|
||||
{{selected.id}}
|
||||
{{/if}}
|
||||
<div class="control">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-ghost"
|
||||
data-test-selected-list-button="delete"
|
||||
{{action "discardSelection" selected}}
|
||||
>
|
||||
{{i-con size=16 glyph="trash-a" excludeIconClass=true}}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
|
@ -1,6 +1,11 @@
|
|||
{{#if (and expirationDate (is-after (now interval=1000) expirationDate)) }}
|
||||
{{#if (and expirationDate (is-after (now interval=1000) expirationDate))}}
|
||||
<div class="token-expire-warning">
|
||||
<AlertBanner @type="danger" @message="Your auth token expired on {{date-format expirationDate 'MMMM Do YYYY, h:mm:ss a'}}. You will need to re-authenticate.">
|
||||
<AlertBanner
|
||||
@type="danger"
|
||||
@message="Your auth token expired on
|
||||
{{date-format expirationDate "MMMM Do YYYY, h:mm:ss a"}}
|
||||
. You will need to re-authenticate."
|
||||
>
|
||||
{{#link-to "vault.cluster.logout" class="button link"}}
|
||||
Reauthenticate
|
||||
{{/link-to}}
|
||||
|
@ -8,4 +13,4 @@
|
|||
</div>
|
||||
{{else}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -41,7 +41,7 @@
|
|||
</div>
|
||||
<div class="level-right is-flex is-paddingless is-marginless">
|
||||
<div class="level-item">
|
||||
{{#popup-menu name="auth-backend-nav" contentClass="is-wide"}}
|
||||
{{#popup-menu name="auth-backend-nav"}}
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
|
|
|
@ -42,6 +42,9 @@ module.exports = function(defaults) {
|
|||
devtool: 'inline-source-map',
|
||||
},
|
||||
},
|
||||
'ember-test-selectors': {
|
||||
strip: isProd,
|
||||
},
|
||||
});
|
||||
|
||||
app.import('vendor/string-includes.js');
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
"ember-load-initializers": "^1.1.0",
|
||||
"ember-maybe-import-regenerator": "^0.1.6",
|
||||
"ember-maybe-in-element": "^0.1.3",
|
||||
"ember-power-select": "^2.0.12",
|
||||
"ember-radio-button": "^1.1.1",
|
||||
"ember-resolver": "^5.0.1",
|
||||
"ember-responsive": "^3.0.0-beta.3",
|
||||
|
|
|
@ -12,14 +12,6 @@ const component = create(formFields);
|
|||
module('Integration | Component | form field', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
component.setContext(this);
|
||||
});
|
||||
|
||||
hooks.afterEach(function() {
|
||||
component.removeContext();
|
||||
});
|
||||
|
||||
const createAttr = (name, type, options) => {
|
||||
return {
|
||||
name,
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
import { module, test, skip } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import { typeInSearch, clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import Service from '@ember/service';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { run } from '@ember/runloop';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import searchSelect from '../../pages/components/search-select';
|
||||
|
||||
const component = create(searchSelect);
|
||||
|
||||
const storeService = Service.extend({
|
||||
query(modelType) {
|
||||
return new Promise((resolve, reject) => {
|
||||
switch (modelType) {
|
||||
case 'policy/acl':
|
||||
resolve([{ id: '1', name: '1' }, { id: '2', name: '2' }, { id: '3', name: '3' }]);
|
||||
break;
|
||||
case 'policy/rgp':
|
||||
reject({ httpStatus: 403, message: 'permission denied' });
|
||||
break;
|
||||
case 'identity/entity':
|
||||
resolve([{ id: '7', name: 'seven' }, { id: '8', name: 'eight' }, { id: '9', name: 'nine' }]);
|
||||
break;
|
||||
case 'server/error':
|
||||
var error = new Error('internal server error');
|
||||
error.httpStatus = 500;
|
||||
reject(error);
|
||||
break;
|
||||
default:
|
||||
reject({ httpStatus: 404, message: 'not found' });
|
||||
break;
|
||||
}
|
||||
reject({ httpStatus: 404, message: 'not found' });
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
module('Integration | Component | search select', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
run(() => {
|
||||
this.owner.unregister('service:store');
|
||||
this.owner.register('service:store', storeService);
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
const models = ['policy/acl'];
|
||||
this.set('models', models);
|
||||
this.set('onChange', sinon.spy());
|
||||
await render(hbs`{{search-select label="foo" models=models onChange=onChange}}`);
|
||||
assert.ok(component.hasLabel, 'it renders the label');
|
||||
assert.equal(component.labelText, 'foo', 'the label text is correct');
|
||||
assert.ok(component.hasTrigger, 'it renders the power select trigger');
|
||||
assert.equal(component.selectedOptions.length, 0, 'there are no selected options');
|
||||
});
|
||||
|
||||
test('it shows options when trigger is clicked', async function(assert) {
|
||||
const models = ['policy/acl'];
|
||||
this.set('models', models);
|
||||
this.set('onChange', sinon.spy());
|
||||
await render(hbs`{{search-select label="foo" models=models onChange=onChange}}`);
|
||||
await clickTrigger();
|
||||
assert.equal(component.options.length, 3, 'shows all options');
|
||||
assert.equal(component.options[0].text, component.selectedOptionText, 'first object in list is focused');
|
||||
});
|
||||
|
||||
test('it filters options when text is entered', async function(assert) {
|
||||
const models = ['identity/entity'];
|
||||
this.set('models', models);
|
||||
this.set('onChange', sinon.spy());
|
||||
await render(hbs`{{search-select label="foo" models=models onChange=onChange}}`);
|
||||
await clickTrigger();
|
||||
assert.equal(component.options.length, 3, 'shows all options');
|
||||
await typeInSearch('n');
|
||||
assert.equal(component.options.length, 2, 'shows two options');
|
||||
});
|
||||
|
||||
test('it moves option from drop down to list when clicked', async function(assert) {
|
||||
const models = ['identity/entity'];
|
||||
this.set('models', models);
|
||||
this.set('onChange', sinon.spy());
|
||||
await render(hbs`{{search-select label="foo" models=models onChange=onChange}}`);
|
||||
await clickTrigger();
|
||||
assert.equal(component.options.length, 3, 'shows all options');
|
||||
await component.selectOption();
|
||||
assert.equal(component.selectedOptions.length, 1, 'there is 1 selected option');
|
||||
assert.ok(this.onChange.calledOnce);
|
||||
assert.ok(this.onChange.calledWith(['7']));
|
||||
await clickTrigger();
|
||||
assert.equal(component.options.length, 2, 'shows two options');
|
||||
});
|
||||
|
||||
test('it pre-populates list with passed in selectedOptions', async function(assert) {
|
||||
const models = ['identity/entity'];
|
||||
this.set('models', models);
|
||||
this.set('onChange', sinon.spy());
|
||||
this.set('inputValue', ['8']);
|
||||
await render(hbs`{{search-select label="foo" inputValue=inputValue models=models onChange=onChange}}`);
|
||||
assert.equal(component.selectedOptions.length, 1, 'there is 1 selected option');
|
||||
await clickTrigger();
|
||||
assert.equal(component.options.length, 2, 'shows two options');
|
||||
});
|
||||
|
||||
test('it adds discarded list items back into select', async function(assert) {
|
||||
const models = ['identity/entity'];
|
||||
this.set('models', models);
|
||||
this.set('onChange', sinon.spy());
|
||||
this.set('inputValue', ['8']);
|
||||
await render(hbs`{{search-select label="foo" inputValue=inputValue models=models onChange=onChange}}`);
|
||||
assert.equal(component.selectedOptions.length, 1, 'there is 1 selected option');
|
||||
await component.deleteButtons[0].click();
|
||||
assert.equal(component.selectedOptions.length, 0, 'there are no selected options');
|
||||
assert.ok(this.onChange.calledOnce);
|
||||
assert.ok(this.onChange.calledWith([]));
|
||||
await clickTrigger();
|
||||
assert.equal(component.options.length, 3, 'shows all options');
|
||||
});
|
||||
|
||||
test('it uses fallback component if endpoint 403s', async function(assert) {
|
||||
const models = ['policy/rgp'];
|
||||
this.set('models', models);
|
||||
this.set('onChange', sinon.spy());
|
||||
await render(
|
||||
hbs`{{search-select label="foo" inputValue=inputValue models=models fallbackComponent="string-list" onChange=onChange}}`
|
||||
);
|
||||
assert.ok(component.hasStringList);
|
||||
});
|
||||
|
||||
test('it shows no results if endpoint 404s', async function(assert) {
|
||||
const models = ['test'];
|
||||
this.set('models', models);
|
||||
this.set('onChange', sinon.spy());
|
||||
await render(
|
||||
hbs`{{search-select label="foo" inputValue=inputValue models=models fallbackComponent="string-list" onChange=onChange}}`
|
||||
);
|
||||
await clickTrigger();
|
||||
assert.equal(component.options.length, 1, 'has the disabled no results option');
|
||||
assert.equal(component.options[0].text, 'No results found', 'text of option shows No results found');
|
||||
});
|
||||
|
||||
test('it shows both name and smaller id for identity endpoints', async function(assert) {
|
||||
const models = ['identity/entity'];
|
||||
this.set('models', models);
|
||||
this.set('onChange', sinon.spy());
|
||||
await render(hbs`{{search-select label="foo" inputValue=inputValue models=models onChange=onChange}}`);
|
||||
await clickTrigger();
|
||||
assert.equal(component.options.length, 3, 'shows all options');
|
||||
assert.equal(component.smallOptionIds.length, 3, 'shows the smaller id text and the name');
|
||||
});
|
||||
|
||||
test('it does not show name and smaller id for non-identity endpoints', async function(assert) {
|
||||
const models = ['policy/acl'];
|
||||
this.set('models', models);
|
||||
this.set('onChange', sinon.spy());
|
||||
await render(hbs`{{search-select label="foo" inputValue=inputValue models=models onChange=onChange}}`);
|
||||
await clickTrigger();
|
||||
assert.equal(component.options.length, 3, 'shows all options');
|
||||
assert.equal(component.smallOptionIds.length, 0, 'only shows the regular sized id');
|
||||
});
|
||||
|
||||
skip('it throws an error if endpoint 500s', async function(assert) {
|
||||
const models = ['server/error'];
|
||||
this.set('models', models);
|
||||
this.set('onChange', sinon.spy());
|
||||
assert.throws(
|
||||
await render(hbs`{{search-select label="foo" inputValue=inputValue models=models onChange=onChange}}`)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -12,6 +12,7 @@ import {
|
|||
|
||||
export default {
|
||||
hasStringList: isPresent('[data-test-component=string-list]'),
|
||||
hasSearchSelect: isPresent('[data-test-component=search-select]'),
|
||||
hasTextFile: isPresent('[data-test-component=text-file]'),
|
||||
hasTTLPicker: isPresent('[data-test-component=ttl-picker]'),
|
||||
hasJSONEditor: isPresent('[data-test-component=json-editor]'),
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { isPresent, collection, text, clickable } from 'ember-cli-page-object';
|
||||
|
||||
export default {
|
||||
hasSearchSelect: isPresent('[data-test-component=search-select]'),
|
||||
hasTrigger: isPresent('.ember-power-select-trigger'),
|
||||
hasLabel: isPresent('[data-test-field-label]'),
|
||||
labelText: text('[data-test-field-label]'),
|
||||
options: collection('.ember-power-select-option'),
|
||||
selectedOptions: collection('[data-test-selected-option]'),
|
||||
deleteButtons: collection('[data-test-selected-list-button="delete"]'),
|
||||
selectedOptionText: text('[aria-current=true]'),
|
||||
selectOption: clickable('[aria-current=true]'),
|
||||
hasStringList: isPresent('[data-test-string-list-input]'),
|
||||
smallOptionIds: collection('[data-test-smaller-id]'),
|
||||
};
|
30
ui/yarn.lock
30
ui/yarn.lock
|
@ -4999,6 +4999,15 @@ ember-concurrency@^0.8.14:
|
|||
ember-cli-babel "^6.8.2"
|
||||
ember-maybe-import-regenerator "^0.1.5"
|
||||
|
||||
ember-concurrency@^0.8.19:
|
||||
version "0.8.22"
|
||||
resolved "https://registry.yarnpkg.com/ember-concurrency/-/ember-concurrency-0.8.22.tgz#900e870aae486e1f5fcb168bbb4efba02561a5ad"
|
||||
integrity sha512-njLqyjMxBf8fapIV8WPyG2gFbSCIQgMpk33uSs5Ih7HsfFAz60KwVo9sMVPBYAJwQmF8jFPm7Ph+mjkiQXHmmA==
|
||||
dependencies:
|
||||
babel-core "^6.24.1"
|
||||
ember-cli-babel "^6.8.2"
|
||||
ember-maybe-import-regenerator "^0.1.5"
|
||||
|
||||
ember-copy@1.0.0, ember-copy@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-copy/-/ember-copy-1.0.0.tgz#426554ba6cf65920f31d24d0a3ca2cb1be16e4aa"
|
||||
|
@ -5120,6 +5129,18 @@ ember-native-dom-helpers@^0.5.3:
|
|||
broccoli-funnel "^1.1.0"
|
||||
ember-cli-babel "^6.6.0"
|
||||
|
||||
ember-power-select@^2.0.12:
|
||||
version "2.0.12"
|
||||
resolved "https://registry.yarnpkg.com/ember-power-select/-/ember-power-select-2.0.12.tgz#24ee54333b9dab482e6da2e2e31966c40cdc445c"
|
||||
integrity sha512-81I850ypsDqik2djv2hbDeV7yFjcxmjkkQk+TADoM+MVo4W5FzRsl87MiX1RK35ZADYxG1RTjVBcCrtKWuMEEg==
|
||||
dependencies:
|
||||
ember-basic-dropdown "^1.0.0"
|
||||
ember-cli-babel "^6.16.0"
|
||||
ember-cli-htmlbars "^3.0.0"
|
||||
ember-concurrency "^0.8.19"
|
||||
ember-text-measurer "^0.4.0"
|
||||
ember-truth-helpers "^2.0.0"
|
||||
|
||||
ember-qunit@^3.3.2:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-qunit/-/ember-qunit-3.4.1.tgz#204a2d39a5d44d494c56bf17cf3fd12f06210359"
|
||||
|
@ -5241,7 +5262,14 @@ ember-test-selectors@^1.0.0:
|
|||
ember-cli-babel "^6.8.2"
|
||||
ember-cli-version-checker "^2.0.0"
|
||||
|
||||
ember-truth-helpers@^2.1.0:
|
||||
ember-text-measurer@^0.4.0:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-text-measurer/-/ember-text-measurer-0.4.1.tgz#30ababa7100b2ffb86f8c37fe2b4b56d2592c626"
|
||||
integrity sha512-fwRoaGF0pemMyJ5S/j5FYt7PZva8MyZA33DwkL1LLAR9T5hQyatSbZaLl4D1a5Kv8kDXX0tTFZT9xCPPKahYTg==
|
||||
dependencies:
|
||||
ember-cli-babel "^6.8.2"
|
||||
|
||||
ember-truth-helpers@^2.0.0, ember-truth-helpers@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-2.1.0.tgz#d4dab4eee7945aa2388126485977baeb33ca0798"
|
||||
integrity sha512-BQlU8aTNl1XHKTYZ243r66yqtR9JU7XKWQcmMA+vkqfkE/c9WWQ9hQZM8YABihCmbyxzzZsngvldokmeX5GhAw==
|
||||
|
|
Loading…
Reference in New Issue