mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 03:58:01 +00:00
Search select (#5851)
This commit is contained in:
@@ -30,6 +30,8 @@ export default ApplicationAdapter.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
query(store, type) {
|
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 },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
95
ui/app/components/search-select.js
Normal file
95
ui/app/components/search-select.js
Normal file
@@ -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() {
|
setType() {
|
||||||
const list = this.get('inputList');
|
const list = this.inputList;
|
||||||
if (!list) {
|
if (!list) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -91,9 +91,7 @@ export default Component.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
toVal() {
|
toVal() {
|
||||||
const inputs = this.get('inputList')
|
const inputs = this.inputList.filter(x => x.value).mapBy('value');
|
||||||
.filter(x => x.value)
|
|
||||||
.mapBy('value');
|
|
||||||
if (this.get('format') === 'string') {
|
if (this.get('format') === 'string') {
|
||||||
return inputs.join(',');
|
return inputs.join(',');
|
||||||
}
|
}
|
||||||
@@ -101,8 +99,8 @@ export default Component.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
toList() {
|
toList() {
|
||||||
let input = this.get('inputValue') || [];
|
let input = this.inputValue || [];
|
||||||
const inputList = this.get('inputList');
|
const inputList = this.inputList;
|
||||||
if (typeof input === 'string') {
|
if (typeof input === 'string') {
|
||||||
input = input.split(',');
|
input = input.split(',');
|
||||||
}
|
}
|
||||||
@@ -111,22 +109,22 @@ export default Component.extend({
|
|||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
inputChanged(idx, val) {
|
inputChanged(idx, val) {
|
||||||
const inputObj = this.get('inputList').objectAt(idx);
|
const inputObj = this.inputList.objectAt(idx);
|
||||||
const onChange = this.get('onChange');
|
const onChange = this.onChange;
|
||||||
set(inputObj, 'value', val);
|
set(inputObj, 'value', val);
|
||||||
onChange(this.toVal());
|
onChange(this.toVal());
|
||||||
},
|
},
|
||||||
|
|
||||||
addInput() {
|
addInput() {
|
||||||
const inputList = this.get('inputList');
|
const inputList = this.inputList;
|
||||||
if (inputList.get('lastObject.value') !== '') {
|
if (inputList.get('lastObject.value') !== '') {
|
||||||
inputList.pushObject({ value: '' });
|
inputList.pushObject({ value: '' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeInput(idx) {
|
removeInput(idx) {
|
||||||
const onChange = this.get('onChange');
|
const onChange = this.onChange;
|
||||||
const inputs = this.get('inputList');
|
const inputs = this.inputList;
|
||||||
inputs.removeObject(inputs.objectAt(idx));
|
inputs.removeObject(inputs.objectAt(idx));
|
||||||
onChange(this.toVal());
|
onChange(this.toVal());
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ export default IdentityModel.extend({
|
|||||||
editType: 'kv',
|
editType: 'kv',
|
||||||
}),
|
}),
|
||||||
policies: attr({
|
policies: attr({
|
||||||
editType: 'stringArray',
|
label: 'Policies',
|
||||||
|
editType: 'searchSelect',
|
||||||
|
fallbackComponent: 'string-list',
|
||||||
|
models: ['policy/acl', 'policy/rgp'],
|
||||||
}),
|
}),
|
||||||
creationTime: attr('string', {
|
creationTime: attr('string', {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
|||||||
@@ -36,19 +36,28 @@ export default IdentityModel.extend({
|
|||||||
editType: 'kv',
|
editType: 'kv',
|
||||||
}),
|
}),
|
||||||
policies: attr({
|
policies: attr({
|
||||||
editType: 'stringArray',
|
label: 'Policies',
|
||||||
|
editType: 'searchSelect',
|
||||||
|
fallbackComponent: 'string-list',
|
||||||
|
models: ['policy/acl', 'policy/rgp'],
|
||||||
}),
|
}),
|
||||||
memberGroupIds: attr({
|
memberGroupIds: attr({
|
||||||
label: 'Member Group IDs',
|
label: 'Member Group IDs',
|
||||||
editType: 'stringArray',
|
editType: 'searchSelect',
|
||||||
|
fallbackComponent: 'string-list',
|
||||||
|
models: ['identity/group'],
|
||||||
}),
|
}),
|
||||||
parentGroupIds: attr({
|
parentGroupIds: attr({
|
||||||
label: 'Parent Group IDs',
|
label: 'Parent Group IDs',
|
||||||
editType: 'stringArray',
|
editType: 'searchSelect',
|
||||||
|
fallbackComponent: 'string-list',
|
||||||
|
models: ['identity/group'],
|
||||||
}),
|
}),
|
||||||
memberEntityIds: attr({
|
memberEntityIds: attr({
|
||||||
label: 'Member Entity IDs',
|
label: 'Member Entity IDs',
|
||||||
editType: 'stringArray',
|
editType: 'searchSelect',
|
||||||
|
fallbackComponent: 'string-list',
|
||||||
|
models: ['identity/entity'],
|
||||||
}),
|
}),
|
||||||
hasMembers: computed(
|
hasMembers: computed(
|
||||||
'memberEntityIds',
|
'memberEntityIds',
|
||||||
|
|||||||
@@ -4,23 +4,14 @@ import ApplicationSerializer from '../application';
|
|||||||
export default ApplicationSerializer.extend({
|
export default ApplicationSerializer.extend({
|
||||||
normalizeItems(payload) {
|
normalizeItems(payload) {
|
||||||
if (payload.data.keys && Array.isArray(payload.data.keys)) {
|
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);
|
assign(payload, payload.data);
|
||||||
delete payload.data;
|
delete payload.data;
|
||||||
return payload;
|
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 'ember-basic-dropdown';
|
||||||
@import "./core";
|
@import 'ember-power-select';
|
||||||
|
@import './core';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.popup-menu-content {
|
.popup-menu-content,
|
||||||
|
.ember-power-select-options {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
margin: -2px 0 0 0;
|
margin: -2px 0 0 0;
|
||||||
|
|
||||||
@@ -23,8 +24,15 @@
|
|||||||
.menu {
|
.menu {
|
||||||
padding: $size-11 0;
|
padding: $size-11 0;
|
||||||
|
|
||||||
|
small code {
|
||||||
|
margin-left: $spacing-xs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button.link,
|
button.link,
|
||||||
a,
|
a,
|
||||||
|
.ember-power-select-option,
|
||||||
|
.ember-power-select-option[aria-current='true'],
|
||||||
.menu-item {
|
.menu-item {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -40,13 +48,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.link,
|
button.link,
|
||||||
|
.ember-power-select-option,
|
||||||
|
.ember-power-select-option[aria-current='true'],
|
||||||
a {
|
a {
|
||||||
|
background-color: $white;
|
||||||
color: $menu-item-color;
|
color: $menu-item-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $menu-item-hover-background-color;
|
background-color: $menu-item-hover-background-color;
|
||||||
color: $menu-item-hover-color;
|
color: $menu-item-hover-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background-color: $menu-item-active-background-color;
|
||||||
|
color: $menu-item-active-color;
|
||||||
|
}
|
||||||
|
|
||||||
&.is-destroy {
|
&.is-destroy {
|
||||||
color: $red;
|
color: $red;
|
||||||
|
|
||||||
@@ -66,11 +83,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
small code {
|
|
||||||
margin-left: $spacing-xs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-label {
|
.menu-label {
|
||||||
color: $grey-dark;
|
color: $grey-dark;
|
||||||
font-size: $size-9;
|
font-size: $size-9;
|
||||||
|
|||||||
124
ui/app/styles/components/search-select.scss
Normal file
124
ui/app/styles/components/search-select.scss
Normal file
@@ -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/popup-menu';
|
||||||
@import './components/radial-progress';
|
@import './components/radial-progress';
|
||||||
@import './components/role-item';
|
@import './components/role-item';
|
||||||
|
@import './components/search-select';
|
||||||
@import './components/secret-control-bar';
|
@import './components/secret-control-bar';
|
||||||
@import './components/shamir-progress';
|
@import './components/shamir-progress';
|
||||||
@import './components/sidebar';
|
@import './components/sidebar';
|
||||||
|
|||||||
@@ -184,8 +184,11 @@ label {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select:not(.is-multiple)::after,
|
.select:not(.is-multiple) {
|
||||||
.select:not(.is-multiple)::before {
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select:not(.is-multiple)::after {
|
||||||
border-color: $black;
|
border-color: $black;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
.title:not(:last-child),
|
.title:not(:last-child),
|
||||||
.subtitle:not(:last-child) {
|
.subtitle:not(:last-child) {
|
||||||
|
display: block;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,3 +12,7 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-section .title {
|
||||||
|
margin-bottom: $spacing-s;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
{{#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">
|
<label for="{{attr.name}}" class="is-label">
|
||||||
{{labelString}}
|
{{labelString}}
|
||||||
{{#if attr.options.helpText}}
|
{{#if attr.options.helpText}}
|
||||||
@@ -11,42 +16,69 @@
|
|||||||
</label>
|
</label>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{#if attr.options.possibleValues}}
|
{{#if attr.options.possibleValues}}
|
||||||
<div class="control is-expanded" >
|
<div class="control is-expanded">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
<select
|
<select
|
||||||
name="{{attr.name}}"
|
name="{{attr.name}}"
|
||||||
id="{{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}}
|
data-test-input={{attr.name}}
|
||||||
>
|
>
|
||||||
{{#each (path-or-array attr.options.possibleValues model) as |val|}}
|
{{#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}}
|
{{or val.displayName val}}
|
||||||
</option>
|
</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="b-checkbox">
|
||||||
<input type="checkbox"
|
<input
|
||||||
|
type="checkbox"
|
||||||
id="{{attr.name}}"
|
id="{{attr.name}}"
|
||||||
class="styled"
|
class="styled"
|
||||||
checked={{eq (get model valuePath) attr.options.trueValue}}
|
checked={{eq (get model valuePath) attr.options.trueValue}}
|
||||||
onchange={{action (action "setAndBroadcastBool" valuePath attr.options.trueValue attr.options.falseValue) value="target.checked"}}
|
onchange={{action
|
||||||
|
(action
|
||||||
|
"setAndBroadcastBool"
|
||||||
|
valuePath
|
||||||
|
attr.options.trueValue
|
||||||
|
attr.options.falseValue
|
||||||
|
)
|
||||||
|
value="target.checked"
|
||||||
|
}}
|
||||||
data-test-input={{attr.name}}
|
data-test-input={{attr.name}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label for="{{attr.name}}" class="is-label">
|
<label for="{{attr.name}}" class="is-label">
|
||||||
{{labelString}}
|
{{labelString}}
|
||||||
{{#if attr.options.helpText}}
|
{{#if attr.options.helpText}}
|
||||||
{{#info-tooltip}}
|
{{#info-tooltip}}{{attr.options.helpText}}{{/info-tooltip}}
|
||||||
{{attr.options.helpText}}
|
|
||||||
{{/info-tooltip}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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
|
{{mount-accessor-select
|
||||||
name=attr.name
|
name=attr.name
|
||||||
label=labelString
|
label=labelString
|
||||||
@@ -55,7 +87,7 @@
|
|||||||
value=(get model valuePath)
|
value=(get model valuePath)
|
||||||
onChange=(action "setAndBroadcast" valuePath)
|
onChange=(action "setAndBroadcast" valuePath)
|
||||||
}}
|
}}
|
||||||
{{else if (eq attr.options.editType 'kv')}}
|
{{else if (eq attr.options.editType "kv")}}
|
||||||
{{kv-object-editor
|
{{kv-object-editor
|
||||||
value=(get model valuePath)
|
value=(get model valuePath)
|
||||||
onChange=(action "setAndBroadcast" valuePath)
|
onChange=(action "setAndBroadcast" valuePath)
|
||||||
@@ -63,15 +95,15 @@
|
|||||||
warning=attr.options.warning
|
warning=attr.options.warning
|
||||||
helpText=attr.options.helpText
|
helpText=attr.options.helpText
|
||||||
}}
|
}}
|
||||||
{{else if (eq attr.options.editType 'file')}}
|
{{else if (eq attr.options.editType "file")}}
|
||||||
{{text-file
|
{{text-file
|
||||||
index=''
|
index=""
|
||||||
file=file
|
file=file
|
||||||
onChange=(action 'setFile')
|
onChange=(action "setFile")
|
||||||
warning=attr.options.warning
|
warning=attr.options.warning
|
||||||
label=labelString
|
label=labelString
|
||||||
}}
|
}}
|
||||||
{{else if (eq attr.options.editType 'ttl')}}
|
{{else if (eq attr.options.editType "ttl")}}
|
||||||
{{ttl-picker
|
{{ttl-picker
|
||||||
data-test-input=attr.name
|
data-test-input=attr.name
|
||||||
initialValue=(or (get model valuePath) attr.options.defaultValue)
|
initialValue=(or (get model valuePath) attr.options.defaultValue)
|
||||||
@@ -80,7 +112,7 @@
|
|||||||
setDefaultValue=(or attr.options.setDefault false)
|
setDefaultValue=(or attr.options.setDefault false)
|
||||||
onChange=(action (action "setAndBroadcast" valuePath))
|
onChange=(action (action "setAndBroadcast" valuePath))
|
||||||
}}
|
}}
|
||||||
{{else if (eq attr.options.editType 'stringArray')}}
|
{{else if (eq attr.options.editType "stringArray")}}
|
||||||
{{string-list
|
{{string-list
|
||||||
label=labelString
|
label=labelString
|
||||||
warning=attr.options.warning
|
warning=attr.options.warning
|
||||||
@@ -96,16 +128,18 @@
|
|||||||
/>
|
/>
|
||||||
{{else if (or (eq attr.type 'number') (eq attr.type 'string'))}}
|
{{else if (or (eq attr.type 'number') (eq attr.type 'string'))}}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{{#if (eq attr.options.editType 'textarea')}}
|
{{#if (eq attr.options.editType "textarea")}}
|
||||||
<textarea
|
<textarea
|
||||||
data-test-input={{attr.name}}
|
data-test-input={{attr.name}}
|
||||||
id={{attr.name}}
|
id={{attr.name}}
|
||||||
value={{or (get model valuePath) attr.options.defaultValue}}
|
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"
|
class="textarea"
|
||||||
></textarea>
|
></textarea>
|
||||||
{{else if (eq attr.options.editType 'json')}}
|
{{else if (eq attr.options.editType "json")}}
|
||||||
|
|
||||||
<label for="{{attr.name}}" class="is-label">
|
<label for="{{attr.name}}" class="is-label">
|
||||||
{{labelString}}
|
{{labelString}}
|
||||||
{{#if attr.options.helpText}}
|
{{#if attr.options.helpText}}
|
||||||
@@ -117,8 +151,10 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</label>
|
</label>
|
||||||
{{json-editor
|
{{json-editor
|
||||||
value=(if (get model valuePath) (stringify (jsonify (get model valuePath))))
|
value=(if
|
||||||
valueUpdated=(action "codemirrorUpdated" attr.name 'string')
|
(get model valuePath) (stringify (jsonify (get model valuePath)))
|
||||||
|
)
|
||||||
|
valueUpdated=(action "codemirrorUpdated" attr.name "string")
|
||||||
}}
|
}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<input
|
<input
|
||||||
@@ -126,38 +162,50 @@
|
|||||||
id={{attr.name}}
|
id={{attr.name}}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
value={{or (get model valuePath) attr.options.defaultValue}}
|
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"
|
class="input"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{{#if attr.options.validationAttr}}
|
{{#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
|
<AlertInline
|
||||||
@type="danger"
|
@type="danger"
|
||||||
@message={{attr.options.invalidMessage}}
|
@message={{attr.options.invalidMessage}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{else if (eq attr.type 'boolean')}}
|
{{else if (eq attr.type "boolean")}}
|
||||||
<div class="b-checkbox">
|
<div class="b-checkbox">
|
||||||
<input type="checkbox"
|
<input
|
||||||
|
type="checkbox"
|
||||||
id="{{attr.name}}"
|
id="{{attr.name}}"
|
||||||
class="styled"
|
class="styled"
|
||||||
checked={{get model attr.name}}
|
checked={{get model attr.name}}
|
||||||
onchange={{action (action "setAndBroadcast" valuePath) value="target.checked"}}
|
onchange={{action
|
||||||
|
(action "setAndBroadcast" valuePath)
|
||||||
|
value="target.checked"
|
||||||
|
}}
|
||||||
data-test-input={{attr.name}}
|
data-test-input={{attr.name}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label for="{{attr.name}}" class="is-label">
|
<label for="{{attr.name}}" class="is-label">
|
||||||
{{labelString}}
|
{{labelString}}
|
||||||
{{#if attr.options.helpText}}
|
{{#if attr.options.helpText}}
|
||||||
{{#info-tooltip}}
|
{{#info-tooltip}}{{attr.options.helpText}}{{/info-tooltip}}
|
||||||
{{attr.options.helpText}}
|
|
||||||
{{/info-tooltip}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{{else if (eq attr.type 'object')}}
|
{{else if (eq attr.type "object")}}
|
||||||
{{json-editor
|
{{json-editor
|
||||||
value=(if (get model valuePath) (stringify (get model valuePath)) emptyData)
|
value=(if (get model valuePath) (stringify (get model valuePath)) emptyData)
|
||||||
valueUpdated=(action "codemirrorUpdated" attr.name false)
|
valueUpdated=(action "codemirrorUpdated" attr.name false)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{#if label}}
|
{{#if label}}
|
||||||
<label class="title is-5" data-test-kv-label="true">
|
<label class="title is-4" data-test-kv-label="true">
|
||||||
{{label}}
|
{{label}}
|
||||||
{{#if helpText}}
|
{{#if helpText}}
|
||||||
{{#info-tooltip}}
|
{{#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>
|
||||||
58
ui/app/templates/components/search-select.hbs
Normal file
58
ui/app/templates/components/search-select.hbs
Normal file
@@ -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">
|
<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"}}
|
{{#link-to "vault.cluster.logout" class="button link"}}
|
||||||
Reauthenticate
|
Reauthenticate
|
||||||
{{/link-to}}
|
{{/link-to}}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="level-right is-flex is-paddingless is-marginless">
|
<div class="level-right is-flex is-paddingless is-marginless">
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
{{#popup-menu name="auth-backend-nav" contentClass="is-wide"}}
|
{{#popup-menu name="auth-backend-nav"}}
|
||||||
<nav class="menu">
|
<nav class="menu">
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ module.exports = function(defaults) {
|
|||||||
devtool: 'inline-source-map',
|
devtool: 'inline-source-map',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'ember-test-selectors': {
|
||||||
|
strip: isProd,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app.import('vendor/string-includes.js');
|
app.import('vendor/string-includes.js');
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
"ember-load-initializers": "^1.1.0",
|
"ember-load-initializers": "^1.1.0",
|
||||||
"ember-maybe-import-regenerator": "^0.1.6",
|
"ember-maybe-import-regenerator": "^0.1.6",
|
||||||
"ember-maybe-in-element": "^0.1.3",
|
"ember-maybe-in-element": "^0.1.3",
|
||||||
|
"ember-power-select": "^2.0.12",
|
||||||
"ember-radio-button": "^1.1.1",
|
"ember-radio-button": "^1.1.1",
|
||||||
"ember-resolver": "^5.0.1",
|
"ember-resolver": "^5.0.1",
|
||||||
"ember-responsive": "^3.0.0-beta.3",
|
"ember-responsive": "^3.0.0-beta.3",
|
||||||
|
|||||||
@@ -12,14 +12,6 @@ const component = create(formFields);
|
|||||||
module('Integration | Component | form field', function(hooks) {
|
module('Integration | Component | form field', function(hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
hooks.beforeEach(function() {
|
|
||||||
component.setContext(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
hooks.afterEach(function() {
|
|
||||||
component.removeContext();
|
|
||||||
});
|
|
||||||
|
|
||||||
const createAttr = (name, type, options) => {
|
const createAttr = (name, type, options) => {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
|
|||||||
175
ui/tests/integration/components/search-select-test.js
Normal file
175
ui/tests/integration/components/search-select-test.js
Normal file
@@ -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 {
|
export default {
|
||||||
hasStringList: isPresent('[data-test-component=string-list]'),
|
hasStringList: isPresent('[data-test-component=string-list]'),
|
||||||
|
hasSearchSelect: isPresent('[data-test-component=search-select]'),
|
||||||
hasTextFile: isPresent('[data-test-component=text-file]'),
|
hasTextFile: isPresent('[data-test-component=text-file]'),
|
||||||
hasTTLPicker: isPresent('[data-test-component=ttl-picker]'),
|
hasTTLPicker: isPresent('[data-test-component=ttl-picker]'),
|
||||||
hasJSONEditor: isPresent('[data-test-component=json-editor]'),
|
hasJSONEditor: isPresent('[data-test-component=json-editor]'),
|
||||||
|
|||||||
15
ui/tests/pages/components/search-select.js
Normal file
15
ui/tests/pages/components/search-select.js
Normal file
@@ -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-cli-babel "^6.8.2"
|
||||||
ember-maybe-import-regenerator "^0.1.5"
|
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:
|
ember-copy@1.0.0, ember-copy@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ember-copy/-/ember-copy-1.0.0.tgz#426554ba6cf65920f31d24d0a3ca2cb1be16e4aa"
|
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"
|
broccoli-funnel "^1.1.0"
|
||||||
ember-cli-babel "^6.6.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:
|
ember-qunit@^3.3.2:
|
||||||
version "3.4.1"
|
version "3.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/ember-qunit/-/ember-qunit-3.4.1.tgz#204a2d39a5d44d494c56bf17cf3fd12f06210359"
|
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-babel "^6.8.2"
|
||||||
ember-cli-version-checker "^2.0.0"
|
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"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-2.1.0.tgz#d4dab4eee7945aa2388126485977baeb33ca0798"
|
resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-2.1.0.tgz#d4dab4eee7945aa2388126485977baeb33ca0798"
|
||||||
integrity sha512-BQlU8aTNl1XHKTYZ243r66yqtR9JU7XKWQcmMA+vkqfkE/c9WWQ9hQZM8YABihCmbyxzzZsngvldokmeX5GhAw==
|
integrity sha512-BQlU8aTNl1XHKTYZ243r66yqtR9JU7XKWQcmMA+vkqfkE/c9WWQ9hQZM8YABihCmbyxzzZsngvldokmeX5GhAw==
|
||||||
|
|||||||
Reference in New Issue
Block a user