Validate queries and allow for custom JSON queries. Closes #168. Closes #203

This commit is contained in:
Rashid Khan 2014-07-24 16:47:08 -07:00
parent af9800bcc9
commit 52e873c783
11 changed files with 173 additions and 32 deletions

2
.gitignore vendored
View file

@ -1,7 +1,7 @@
.DS_Store
node_modules
src/bower_components
**/styles/*.css
**/*.css
trash
build
target

View file

@ -5,11 +5,19 @@
{{dash.title}}
</span>
<form class="fill inline-form" ng-submit="filterResults()">
<input placeholder="Filter..." type="text" class="form-control" ng-model="state.query">
<button class="btn btn-default" type="submit">
<span class="fa fa-search"></span>
</button>
<form class="fill inline-form" ng-submit="filterResults()" name="queryInput">
<div class="input-group"
ng-class="queryInput.$invalid ? 'has-error' : ''">
<input query-input
placeholder="Filter..."
type="text"
class="form-control"
ng-model="state.query">
<button class="btn btn-default" type="submit"
ng-disabled="queryInput.$invalid">
<span class="fa fa-search"></span>
</button>
</div>
</form>
<div class="button-group">

View file

@ -85,11 +85,7 @@ define(function (require) {
function updateQueryOnRootSource() {
if ($state.query) {
dash.searchSource.set('filter', {
query: {
query_string: {
query: $state.query
}
}
query: $state.query
});
} else {
dash.searchSource.set('filter', null);

View file

@ -15,6 +15,7 @@ define(function (require) {
require('filters/moment');
require('components/courier/courier');
require('components/index_patterns/index_patterns');
require('components/query_input/query_input');
require('components/state_management/app_state');
require('services/timefilter');
@ -70,7 +71,7 @@ define(function (require) {
var defaultFormat = courier.indexPatterns.fieldFormats.defaultByType.string;
var stateDefaults = {
query: initialQuery ? initialQuery.query_string.query : '',
query: initialQuery || '',
columns: ['_source'],
index: config.get('defaultIndex'),
interval: 'auto'
@ -291,11 +292,7 @@ define(function (require) {
}
return sort;
})
.query(!$state.query ? null : {
query_string: {
query: $state.query
}
});
.query(!$state.query ? null : $state.query);
// get the current indexPattern
var indexPattern = $scope.searchSource.get('index');

View file

@ -1,9 +1,18 @@
<div ng-controller="discover">
<navbar>
<form class="fill inline-form" ng-submit="fetch()">
<input placeholder="Search..." type="text" class="form-control" ng-model="state.query">
<button type="submit"><span class="fa fa-search"></span></button>
<button type="button" ng-click="resetQuery()"><span class="fa fa-ban"></span></button>
<form class="fill inline-form" ng-submit="fetch()" name="discoverSearch">
<div class="input-group"
ng-class="discoverSearch.$invalid ? 'has-error' : ''">
<input query-input="searchSource"
ng-model="state.query"
placeholder="Search..."
type="text"
class="form-control">
<button type="submit"
ng-disabled="discoverSearch.$invalid">
<span class="fa fa-search"></span></button>
<button type="button" ng-click="resetQuery()"><span class="fa fa-ban"></span></button>
</div>
</form>
<div class="button-group">

View file

@ -228,7 +228,7 @@ define(function (require) {
delete vis.savedSearchId;
var q = vis.searchSource.get('query');
$state.query = _.isObject(q) ? q.query_string.query : q;
$state.query = q;
var parent = vis.searchSource.parent();
// we will copy over all state minus the "aggs"
@ -249,7 +249,7 @@ define(function (require) {
delete $state.query;
} else {
var q = $state.query || vis.searchSource.get('query');
$state.query = _.isObject(q) ? q.query_string.query : q;
$state.query = q;
}
// init

View file

@ -17,13 +17,19 @@
<i class="fa fa-chain-broken"></i> Unlinked!
</div>
<form ng-submit="doVisualize()" class="inline-form">
<input
placeholder="Search..."
type="text"
class="form-control"
ng-model="state.query">
<button class="btn btn-default" type="submit"><span class="fa fa-search"></span></button>
<form ng-submit="doVisualize()" class="inline-form" name="queryInput">
<div class="input-group"
ng-class="queryInput.$invalid ? 'has-error' : ''">
<input query-input="vis.searchSource"
placeholder="Search..."
type="text"
class="form-control"
ng-model="state.query">
<button class="btn btn-default" type="submit"
ng-disabled="queryInput.$invalid">
<span class="fa fa-search"></span>
</button>
</div>
</form>
</div>

View file

@ -0,0 +1,112 @@
define(function (require) {
var _ = require('lodash');
var $ = require('jquery');
require('css!components/query_input/query_input.css');
require('modules')
.get('kibana')
.directive('queryInput', function (es, $compile, timefilter, configFile) {
return {
restrict: 'A',
require: 'ngModel',
scope: {
'ngModel': '=',
'queryInput': '=?',
},
link: function ($scope, elem, attr, ngModel) {
// track request so we can abort it if needed
var request = {};
var errorElem = $('<i class="fa fa-ban input-query-error"></i>').hide();
var init = function () {
elem.after(errorElem);
validater($scope.ngModel);
};
var validater = function (query) {
var index, type;
var error = function (resp) {
ngModel.$setValidity('queryInput', false);
errorElem.attr('tooltip', resp.explanations && resp.explanations[0] ?
resp.explanations[0].error : undefined);
// Compile is needed for the tooltip
$compile(errorElem)($scope);
errorElem.show();
return undefined;
};
var success = function (resp) {
if (resp.valid) {
ngModel.$setValidity('queryInput', true);
errorElem.hide();
return query;
} else {
return error(resp);
}
};
if ($scope.queryInput) {
index = $scope.queryInput.get('index').toIndexList();
} else {
index = configFile.kibanaIndex;
type = '__kibanaQueryValidator';
}
if (request.abort) request.abort();
request = es.indices.validateQuery({
index: index,
type: type,
explain: true,
ignoreUnavailable: true,
body: {
query: query || { match_all: {} }
}
}).then(success, error);
};
var debouncedValidator = _.debounce(validater, 300);
// What should I make with the input from the user?
var fromUser = function (text) {
try {
return JSON.parse(text);
} catch (e) {
return {
query_string: {
query: text || '*'
}
};
}
};
// How should I present the data back to the user in the input field?
var toUser = function (text) {
if (_.isString(text)) return text;
if (_.isObject(text)) {
if (text.query_string) return text.query_string.query;
return JSON.stringify(text);
}
return undefined;
};
ngModel.$parsers.push(fromUser);
ngModel.$formatters.push(toUser);
// Use a model watch instead of parser/formatter. Debounced anyway. Parsers require the
// user to actually enter input, which may not happen if the back button is clicked
$scope.$watch('ngModel', debouncedValidator);
init();
}
};
});
});

View file

@ -0,0 +1,11 @@
@import (reference) "../../styles/_bootstrap.less";
@import (reference) "../../styles/theme/_theme.less";
@import (reference) "../../styles/_variables.less";
i.query-input-error {
position: absolute;
margin-left: -25px;
color: @brand-danger;
margin-top: 10px;
z-index: 5;
}

View file

@ -76,7 +76,8 @@ navbar {
// horizontal group of buttons/form elements
.button-group,
.inline-form {
.inline-form .input-group {
margin-bottom: 0px;
.display(flex);
> * {
@ -90,7 +91,7 @@ navbar {
}
}
.inline-form {
.inline-form .input-group {
input {
height: auto;
}

View file

@ -3,6 +3,7 @@ var bc = require('path').join(__dirname, '../../src/bower_components');
module.exports = {
src: {
src: [
'<%= src %>/kibana/components/*/*.less',
'<%= src %>/kibana/apps/dashboard/styles/main.less',
'<%= src %>/kibana/apps/discover/styles/main.less',
'<%= src %>/kibana/apps/settings/styles/main.less',