Files
awx/awx/ui/client/src/shared/smart-search/queryset.service.js
2018-08-25 14:00:24 -04:00

514 lines
20 KiB
JavaScript

function searchWithoutKey (term, singleSearchParam = null) {
if (singleSearchParam) {
return { [singleSearchParam]: `search=${encodeURIComponent(term)}` };
}
return { search: encodeURIComponent(term) };
}
function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, SmartSearchService) {
return {
// kick off building a model for a specific endpoint
// this is usually a list's basePath
// unified_jobs is the exception, where we need to fetch many subclass OPTIONS and summary_fields
initFieldset(path, name) {
let defer = $q.defer();
defer.resolve(this.getCommonModelOptions(path, name));
return defer.promise;
},
getCommonModelOptions(path, name) {
let resolve, base,
defer = $q.defer();
this.url = path;
resolve = this.options(path)
.then((res) => {
base = res.data.actions.GET;
let relatedSearchFields = res.data.related_search_fields;
defer.resolve({
models: {
[name]: new DjangoSearchModel(name, base, relatedSearchFields)
},
options: res
});
});
return defer.promise;
},
replaceDefaultFlags (value) {
if (value) {
value = value.toString().replace(/__icontains_DEFAULT/g, "__icontains");
value = value.toString().replace(/__search_DEFAULT/g, "__search");
}
return value;
},
replaceEncodedTokens(value) {
return decodeURIComponent(value).replace(/"|'/g, "");
},
encodeTerms(value, key){
key = this.replaceDefaultFlags(key);
value = this.replaceDefaultFlags(value);
var that = this;
if (Array.isArray(value)){
value = _.uniq(_.flattenDeep(value));
let concated = '';
angular.forEach(value, function(item){
if(item && typeof item === 'string') {
item = that.replaceEncodedTokens(item);
}
concated += `${key}=${item}&`;
});
return concated;
}
else {
if(value && typeof value === 'string') {
value = this.replaceEncodedTokens(value);
}
return `${key}=${value}&`;
}
},
// encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL
encodeQueryset(params) {
let queryset;
queryset = _.reduce(params, (result, value, key) => {
return result + this.encodeTerms(value, key);
}, '');
queryset = queryset.substring(0, queryset.length - 1);
return angular.isObject(params) ? `?${queryset}` : '';
},
// like encodeQueryset, but return an actual unstringified API-consumable http param object
encodeQuerysetObject(params) {
return _.reduce(params, (obj, value, key) => {
const encodedKey = this.replaceDefaultFlags(key);
const values = Array.isArray(value) ? value : [value];
obj[encodedKey] = values
.map(value => this.replaceDefaultFlags(value))
.map(value => this.replaceEncodedTokens(value))
.join(',');
return obj;
}, {});
},
// encodes a ui smart-search param to a django-friendly param
// operand:key:comparator:value => {operand__key__comparator: value}
encodeParam({ term, relatedSearchTerm, searchTerm, singleSearchParam }){
// Assumption here is that we have a key and a value so the length
// of the paramParts array will be 2. [0] is the key and [1] the value
let paramParts = SmartSearchService.splitTermIntoParts(term);
let keySplit = paramParts[0].split('.');
let exclude = false;
let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false;
if(keySplit[0].match(/^-/g)) {
exclude = true;
keySplit[0] = keySplit[0].replace(/^-/, '');
}
let paramString = exclude ? "not__" : "";
let valueString = paramParts[1];
if(keySplit.length === 1) {
if(searchTerm && !lessThanGreaterThan) {
if(singleSearchParam) {
paramString += keySplit[0] + '__icontains';
}
else {
paramString += keySplit[0] + '__icontains_DEFAULT';
}
}
else if(relatedSearchTerm) {
if(singleSearchParam) {
paramString += keySplit[0];
}
else {
paramString += keySplit[0] + '__search_DEFAULT';
}
}
else {
paramString += keySplit[0];
}
}
else {
paramString += keySplit.join('__');
}
if(lessThanGreaterThan) {
if(paramParts[1].match(/^>=.*$/)) {
paramString += '__gte';
valueString = valueString.replace(/^(>=)/,"");
}
else if(paramParts[1].match(/^<=.*$/)) {
paramString += '__lte';
valueString = valueString.replace(/^(<=)/,"");
}
else if(paramParts[1].match(/^<.*$/)) {
paramString += '__lt';
valueString = valueString.replace(/^(<)/,"");
}
else if(paramParts[1].match(/^>.*$/)) {
paramString += '__gt';
valueString = valueString.replace(/^(>)/,"");
}
}
if(singleSearchParam) {
return {[singleSearchParam]: paramString + "=" + valueString};
}
else {
return {[paramString] : encodeURIComponent(valueString)};
}
},
// decodes a django queryset param into a ui smart-search tag or set of tags
decodeParam(value, key){
let decodeParamString = function(searchString) {
if(key === 'search') {
// Don't include 'search:' in the search tag
return decodeURIComponent(`${searchString}`);
}
else {
key = key.toString().replace(/__icontains_DEFAULT/g, "");
key = key.toString().replace(/__search_DEFAULT/g, "");
let split = key.split('__');
let decodedParam = searchString;
let exclude = false;
if(key.startsWith('not__')) {
exclude = true;
split = split.splice(1, split.length);
}
if(key.endsWith('__gt')) {
decodedParam = '>' + decodedParam;
split = split.splice(0, split.length-1);
}
else if(key.endsWith('__lt')) {
decodedParam = '<' + decodedParam;
split = split.splice(0, split.length-1);
}
else if(key.endsWith('__gte')) {
decodedParam = '>=' + decodedParam;
split = split.splice(0, split.length-1);
}
else if(key.endsWith('__lte')) {
decodedParam = '<=' + decodedParam;
split = split.splice(0, split.length-1);
}
let uriDecodedParam = decodeURIComponent(decodedParam);
return exclude ? `-${split.join('.')}:${uriDecodedParam}` : `${split.join('.')}:${uriDecodedParam}`;
}
};
if (Array.isArray(value)){
value = _.uniq(_.flattenDeep(value));
return _.map(value, (item) => {
return decodeParamString(item);
});
}
else {
return decodeParamString(value);
}
},
// encodes a django queryset for ui-router's URLMatcherFactory
// {operand__key__comparator: value, } => 'operand:key:comparator:value;...'
// value.isArray expands to:
// {operand__key__comparator: [value1, value2], } => 'operand:key:comparator:value1;operand:key:comparator:value1...'
encodeArr(params) {
let url;
url = _.reduce(params, (result, value, key) => {
return result.concat(encodeUrlString(value, key));
}, []);
return url.join(';');
// {key:'value'} => 'key:value'
// {key: [value1, value2, ...]} => ['key:value1', 'key:value2']
function encodeUrlString(value, key){
if (Array.isArray(value)){
value = _.uniq(_.flattenDeep(value));
return _.map(value, (item) => {
return `${key}:${item}`;
});
}
else {
return `${key}:${value}`;
}
}
},
// decodes a django queryset for ui-router's URLMatcherFactory
// 'operand:key:comparator:value,...' => {operand__key__comparator: value, }
decodeArr(arr) {
let params = {};
if (!arr) {
return params;
}
_.forEach(arr.split(';'), (item) => {
let key = item.split(':')[0],
value = item.split(':')[1];
if(!params[key]){
params[key] = value;
}
else if (Array.isArray(params[key])){
params[key] = _.uniq(_.flattenDeep(params[key]));
params[key].push(value);
}
else {
params[key] = [params[key], value];
}
});
return params;
},
// REST utilities
options(endpoint) {
Rest.setUrl(endpoint);
return Rest.options(endpoint);
},
search(endpoint, params) {
Wait('start');
this.url = `${endpoint}${this.encodeQueryset(params)}`;
Rest.setUrl(this.url);
return Rest.get()
.then(function(response) {
Wait('stop');
if (response
.headers('X-UI-Max-Events') !== null) {
response.data.maxEvents = response.
headers('X-UI-Max-Events');
}
return response;
})
.catch(function(response) {
Wait('stop');
this.error(response.data, response.status);
throw response;
}.bind(this));
},
error(data, status) {
if(data && data.detail){
let error = typeof data.detail === "string" ? data.detail : JSON.parse(data.detail);
if(_.isArray(error)){
data.detail = error[0];
}
}
ProcessErrors($rootScope, data, status, null, {
hdr: 'Error!',
msg: `Invalid search term entered. GET returned: ${status}`
});
},
// Removes state definition defaults and pagination terms
stripDefaultParams(params, defaultParams) {
if (!params) {
return [];
}
if(defaultParams) {
let stripped =_.pickBy(params, (value, key) => {
// setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value
return defaultParams[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaultParams[key] !== null;
});
let strippedCopy = _.cloneDeep(stripped);
if(_.keys(_.pickBy(defaultParams, _.keys(strippedCopy))).length > 0){
for (var key in strippedCopy) {
if (strippedCopy.hasOwnProperty(key)) {
let value = strippedCopy[key];
if(_.isArray(value)){
let index = _.indexOf(value, defaultParams[key]);
value = value.splice(index, 1)[0];
}
}
}
stripped = strippedCopy;
}
return _(strippedCopy).map(this.decodeParam).flatten().value();
}
else {
return _(params).map(this.decodeParam).flatten().value();
}
},
mergeQueryset (queryset, additional, singleSearchParam) {
const space = '%20and%20';
const merged = _.mergeWith({}, queryset, additional, (objectValue, sourceValue, key, object) => {
if (!(object[key] && object[key] !== sourceValue)) {
// // https://lodash.com/docs/3.10.1#each
// If this returns undefined merging is handled by default _.merge algorithm
return undefined;
}
if (_.isArray(object[key])) {
object[key].push(sourceValue);
return object[key];
}
if (singleSearchParam) {
if (!object[key]) {
return sourceValue;
}
const singleSearchParamKeys = object[key].split(space);
if (_.includes(singleSearchParamKeys, sourceValue)) {
return object[key];
}
return `${object[key]}${space}${sourceValue}`;
}
// Start the array of keys
return [object[key], sourceValue];
});
return merged;
},
getSearchInputQueryset (searchInput, isFilterableBaseField = null, isRelatedField = null, isAnsibleFactField = null, singleSearchParam = null) {
// XXX Should find a better approach than passing in the two 'is...Field' callbacks XXX
const space = '%20and%20';
let params = {};
// Remove leading/trailing whitespace if there is any
const terms = (searchInput) ? searchInput.trim() : '';
if (!(terms && terms !== '')) {
return;
}
let splitTerms;
if (singleSearchParam === 'host_filter') {
splitTerms = SmartSearchService.splitFilterIntoTerms(terms);
} else {
splitTerms = SmartSearchService.splitSearchIntoTerms(terms);
}
const combineSameSearches = (a, b) => {
if (!a) {
return undefined;
}
if (_.isArray(a)) {
return a.concat(b);
}
if (singleSearchParam) {
return `${a}${space}${b}`;
}
return [a, b];
};
_.each(splitTerms, term => {
const termParts = SmartSearchService.splitTermIntoParts(term);
let termParams;
if (termParts.length === 1) {
termParams = searchWithoutKey(term, singleSearchParam);
} else if ((isAnsibleFactField && isAnsibleFactField(termParts)) || (isFilterableBaseField && isFilterableBaseField(termParts))) {
termParams = this.encodeParam({ term, singleSearchParam, searchTerm: true });
} else if (isRelatedField && isRelatedField(termParts)) {
termParams = this.encodeParam({ term, singleSearchParam, relatedSearchTerm: true });
} else {
termParams = searchWithoutKey(term, singleSearchParam);
}
params = _.mergeWith(params, termParams, combineSameSearches);
});
return params;
},
removeTermsFromQueryset(queryset, term, isFilterableBaseField, isRelatedField = null, isAnsibleFactField = null, singleSearchParam = null) {
const modifiedQueryset = _.cloneDeep(queryset);
const removeSingleTermFromQueryset = (value, key) => {
const space = '%20and%20';
if (Array.isArray(modifiedQueryset[key])) {
modifiedQueryset[key] = modifiedQueryset[key].filter(item => item !== value);
if (modifiedQueryset[key].length < 1) {
delete modifiedQueryset[key];
}
} else if (singleSearchParam && _.get(modifiedQueryset, singleSearchParam, []).includes(space)) {
const searchParamParts = modifiedQueryset[singleSearchParam].split(space);
// The value side of each paramPart might have been encoded in
// SmartSearch.splitFilterIntoTerms
_.each(searchParamParts, (paramPart, paramPartIndex) => {
searchParamParts[paramPartIndex] = decodeURIComponent(paramPart);
});
const paramPartIndex = searchParamParts.indexOf(decodeURIComponent(value));
if (paramPartIndex !== -1) {
searchParamParts.splice(paramPartIndex, 1);
}
modifiedQueryset[singleSearchParam] = searchParamParts.join(space);
} else {
delete modifiedQueryset[key];
}
};
const termParts = SmartSearchService.splitTermIntoParts(term);
let removed;
if (termParts.length === 1) {
removed = searchWithoutKey(term, singleSearchParam);
} else if ((isAnsibleFactField && isAnsibleFactField(termParts)) || (isFilterableBaseField && isFilterableBaseField(termParts))) {
removed = this.encodeParam({ term, singleSearchParam, searchTerm: true });
} else if (isRelatedField && isRelatedField(termParts)) {
removed = this.encodeParam({ term, singleSearchParam, relatedSearchTerm: true });
} else {
removed = searchWithoutKey(term, singleSearchParam);
}
if (!removed) {
removed = searchWithoutKey(termParts[termParts.length - 1], singleSearchParam);
}
_.each(removed, removeSingleTermFromQueryset);
return modifiedQueryset;
},
createSearchTagsFromQueryset(queryset, defaultParams = null, singleSearchParam = null) {
const space = '%20and%20';
const modifiedQueryset = angular.copy(queryset);
let searchTags = [];
if (singleSearchParam && modifiedQueryset[singleSearchParam]) {
const searchParam = modifiedQueryset[singleSearchParam].split(space);
delete modifiedQueryset[singleSearchParam];
$.each(searchParam, (index, param) => {
const paramParts = decodeURIComponent(param).split(/=(.+)/);
const reconstructedSearchString = this.decodeParam(paramParts[1], paramParts[0]);
searchTags.push(reconstructedSearchString);
});
}
return searchTags.concat(this.stripDefaultParams(modifiedQueryset, defaultParams));
}
};
}
QuerysetService.$inject = [
'$q',
'Rest',
'ProcessErrors',
'$rootScope',
'Wait',
'DjangoSearchModel',
'SmartSearchService',
];
export default QuerysetService;