mirror of https://github.com/rancher/ui.git
302 lines
8.0 KiB
JavaScript
302 lines
8.0 KiB
JavaScript
import { inject as service } from '@ember/service';
|
|
import C from 'ui/utils/constants';
|
|
import { get, set, computed, observer } from '@ember/object';
|
|
import { task, timeout } from 'ember-concurrency';
|
|
import { isBlank } from '@ember/utils';
|
|
import SearchableSelect from '../searchable-select/component';
|
|
import { alias } from '@ember/object/computed';
|
|
import { later } from '@ember/runloop';
|
|
import { on } from '@ember/object/evented';
|
|
import { isAlternate, isMore, isRange } from 'ui/utils/platform';
|
|
|
|
const DEBOUNCE_MS = 250;
|
|
export default SearchableSelect.extend({
|
|
classNames: 'principal-search',
|
|
globalStore: service(),
|
|
errors: null,
|
|
content: alias('filteredPrincipals'),
|
|
value: alias('filter'),
|
|
_principals: null,
|
|
useLabel: null,
|
|
showDropdownArrow: false,
|
|
|
|
clientSideFiltering: false,
|
|
loading: false,
|
|
focused: false,
|
|
selectExactOnBlur: true,
|
|
includeLocal: true,
|
|
|
|
init() {
|
|
this._super(...arguments);
|
|
set(this,'allUsers', get(this,'globalStore').all('user'));
|
|
},
|
|
|
|
didInsertElement() {
|
|
//Explicitly not calling super here to not show until there's content this._super(...arguments);
|
|
|
|
this.$('input').on('focus', () => {
|
|
if (this.isDestroyed || this.isDestroying) {
|
|
return;
|
|
}
|
|
|
|
set(this, 'focused', true);
|
|
const term = get(this,'value');
|
|
if ( term ) {
|
|
set(this, '_principals', []);
|
|
get(this, 'search').perform(term);
|
|
this.send('show');
|
|
}
|
|
});
|
|
|
|
this.$('input').on('blur', () => {
|
|
later(() => {
|
|
if (this.isDestroyed || this.isDestroying) {
|
|
return;
|
|
}
|
|
|
|
set(this, 'focused', false);
|
|
if ( get(this, 'selectExactOnBlur') ) {
|
|
this.scheduleSend();
|
|
}
|
|
|
|
this.send('hide');
|
|
}, 250);
|
|
});
|
|
},
|
|
|
|
scheduleSend() {
|
|
if ( get(this, 'loading') ) {
|
|
set(this, 'sendExactAfterSearch', true);
|
|
} else {
|
|
set(this, 'sendExactAfterSearch', false);
|
|
this.sendSelectExact();
|
|
}
|
|
},
|
|
|
|
sendSelectExact() {
|
|
const value = get(this,'value');
|
|
const match = get(this,'filteredPrincipals').findBy('label', value);
|
|
|
|
let principal = null;
|
|
if ( match ) {
|
|
principal = match.principal;
|
|
} else {
|
|
set(this, 'value', '');
|
|
}
|
|
|
|
this.sendAction('selectExact', principal);
|
|
this.send('hide');
|
|
},
|
|
|
|
sendAfterLoad: false,
|
|
|
|
filteredPrincipals: computed('_principals.@each.{id,state}', function() {
|
|
return ( get(this, '_principals') || [] ).map(( principal ) =>{
|
|
// console.log({label: get(principal, 'displayName') || get(principal, 'loginName') || get(principal, 'name'), value: get(principal, 'id'), provider: get(principal, 'provider'),});
|
|
return {
|
|
label: get(principal, 'displayName') || get(principal, 'loginName') || get(principal, 'name'),
|
|
value: get(principal, 'id'),
|
|
provider: get(principal, 'provider'),
|
|
type: get(principal, 'principalType'),
|
|
principal: principal,
|
|
};
|
|
});
|
|
}),
|
|
|
|
externalChanged: on('init', observer('external', function(){
|
|
let principal = get(this, 'external');
|
|
|
|
if (principal) {
|
|
// for update TODO
|
|
// if (!get(this, 'globalStore').hasRecordFor(principal.type, principal.id)) {
|
|
// get(this, 'globalStore')._add('principal', principal);
|
|
// }
|
|
this.set('readOnly', true);
|
|
this.set('optionValuePath', 'label');
|
|
this.setSelect({
|
|
label: get(principal, 'displayName') || get(principal, 'loginName') || get(principal, 'name'),
|
|
value: get(principal, 'id'),
|
|
provider: get(principal, 'provider'),
|
|
type: get(principal, 'principalType')
|
|
});
|
|
}
|
|
})),
|
|
|
|
metas: computed(function() {
|
|
return Object.keys(C.KEY).map(k => C.KEY[k]);
|
|
}),
|
|
|
|
actions: {
|
|
search(term, e) {
|
|
const kc = e.keyCode;
|
|
|
|
this.send('show');
|
|
|
|
if ( kc === C.KEY.CR || kc === C.KEY.LF ) {
|
|
this.scheduleSend();
|
|
return;
|
|
}
|
|
|
|
var isAlpha = (k) => {
|
|
return !get(this, 'metas').includes(k)
|
|
&& !isAlternate(k)
|
|
&& !isRange(k)
|
|
&& !isMore(k);
|
|
}
|
|
if (isAlpha(kc)) {
|
|
set(this, 'principal', null);
|
|
this.sendAction('add');
|
|
get(this, 'search').perform(term);
|
|
}
|
|
},
|
|
|
|
show() {
|
|
if (this.get('showOptions') === true) {
|
|
return;
|
|
}
|
|
const toBottom = $('body').height() - $(this.$()[0]).offset().top - 60;
|
|
this.set('maxHeight', toBottom < get(this,'maxHeight') ? toBottom : get(this,'maxHeight'));
|
|
this.set('showOptions', true);
|
|
},
|
|
|
|
hide() {
|
|
set(this, 'filter', get(this, 'displayLabel'))
|
|
this.set('showOptions', false);
|
|
this.set('$activeTarget', null);
|
|
},
|
|
},
|
|
|
|
displayLabel: computed('value', 'prompt', 'interContent.[]', function () {
|
|
const value = this.get('value');
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
const vp = this.get('optionValuePath');
|
|
const lp = this.get('optionLabelPath');
|
|
const selectedItem = this.get('interContent').filterBy(vp, value).get('firstObject');
|
|
|
|
if (selectedItem) {
|
|
let label = get(selectedItem, lp);
|
|
if (this.get('localizedLabel')) {
|
|
label = this.get('intl').t(label);
|
|
}
|
|
return label;
|
|
}
|
|
return value;
|
|
}),
|
|
|
|
setSelect(item) {
|
|
const gp = this.get('optionGroupPath');
|
|
const vp = this.get('optionValuePath');
|
|
|
|
this.set('value', get(item, vp));
|
|
if (gp && get(item, gp)) {
|
|
this.set('group', get(item, gp));
|
|
}
|
|
|
|
this.set('filter', this.get('displayLabel'));
|
|
|
|
set(this, 'principal', item);
|
|
this.sendAction('add');
|
|
this.send('hide');
|
|
},
|
|
|
|
showMessage: computed('filtered.[]','value', function() {
|
|
if ( !get(this,'value') ) {
|
|
return false;
|
|
}
|
|
|
|
return get(this, 'filtered.length') === 0;
|
|
}),
|
|
|
|
search: task(function * (term) {
|
|
if (isBlank(term)) {
|
|
set(this, '_principals', []);
|
|
set(this, 'loading', false);
|
|
return;
|
|
}
|
|
|
|
// Pause here for DEBOUNCE_MS milliseconds. Because this
|
|
// task is `restartable`, if the principal starts typing again,
|
|
// the current search will be canceled at this point and
|
|
// start over from the beginning. This is the
|
|
// ember-concurrency way of debouncing a task.
|
|
|
|
set(this, 'loading', true);
|
|
|
|
yield timeout(DEBOUNCE_MS);
|
|
|
|
let xhr = yield this.get('goSearch').perform(term);
|
|
return xhr;
|
|
}).restartable(),
|
|
|
|
|
|
goSearch: task(function * (term) {
|
|
const globalStore = get(this, 'globalStore');
|
|
let promise;
|
|
|
|
promise = globalStore.rawRequest({
|
|
url: 'principals?action=search',
|
|
method: 'POST',
|
|
data: {
|
|
name: term,
|
|
}
|
|
}).then((xhr) => {
|
|
let neu = [];
|
|
if ( xhr.status !== 204 ) {
|
|
if ( xhr.body && typeof xhr.body === 'object' && xhr.body.data ) {
|
|
neu = xhr.body.data;
|
|
}
|
|
}
|
|
|
|
if ( get(this, 'includeLocal') ) {
|
|
let normalizedTerm = term.toLowerCase().trim();
|
|
let foundIds = {};
|
|
neu.forEach((x) => {
|
|
foundIds[x.id] = true;
|
|
})
|
|
|
|
let local = get(this,'allUsers');
|
|
local = local.filter((x) => {
|
|
if ( (x.name||'').toLowerCase().trim().startsWith(normalizedTerm) ||
|
|
(x.username||'').toLowerCase().trim().startsWith(normalizedTerm) )
|
|
{
|
|
for ( let i = 0 ; i < x.principalIds.length ; i++ ) {
|
|
if ( foundIds[ x.principalIds[i] ] ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
const globalStore = get(this, 'globalStore');
|
|
local = local.map((x) => {
|
|
return globalStore.getById('principal', x.principalIds[0]);
|
|
});
|
|
local = local.filter((x) => !!x);
|
|
neu.addObjects(local);
|
|
}
|
|
|
|
set(this, '_principals', neu);
|
|
|
|
return xhr;
|
|
}).catch((xhr) => {
|
|
set(this, 'errors', [`${xhr.status}: ${xhr.statusText}`]);
|
|
return xhr;
|
|
}).finally(() => {
|
|
set(this, 'loading', false);
|
|
if ( get(this,'sendExactAfterSearch') ) {
|
|
this.scheduleSend();
|
|
}
|
|
});
|
|
|
|
let result = yield promise;
|
|
return result;
|
|
}),
|
|
});
|