Search select (#5851)

This commit is contained in:
madalynrose 2018-12-10 11:44:37 -05:00 committed by GitHub
parent c67ef8be09
commit c84c8e01b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 722 additions and 146 deletions

View File

@ -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 },
});
},
});

View 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();
},
},
});

View File

@ -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());
},

View File

@ -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,

View File

@ -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',

View File

@ -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);
});
},
});

View File

@ -1,2 +1,3 @@
@import "ember-basic-dropdown";
@import "./core";
@import 'ember-basic-dropdown';
@import 'ember-power-select';
@import './core';

View File

@ -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;

View 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;
}
}

View File

@ -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';

View File

@ -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;

View File

@ -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;
}

View File

@ -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}}

View File

@ -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}}

View File

@ -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>

View 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}}

View File

@ -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}}

View File

@ -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>

View File

@ -42,6 +42,9 @@ module.exports = function(defaults) {
devtool: 'inline-source-map',
},
},
'ember-test-selectors': {
strip: isProd,
},
});
app.import('vendor/string-includes.js');

View File

@ -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",

View File

@ -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,

View 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}}`)
);
});
});

View File

@ -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]'),

View 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]'),
};

View File

@ -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==