Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Vega] Implement context filter modification #17586

Merged
merged 6 commits into from Jul 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/core_plugins/vega/public/vega_type.js
Expand Up @@ -60,8 +60,8 @@ VisTypesRegistryProvider.register((Private) => {
responseHandler: 'none',
options: {
showIndexSelection: false,
showQueryBar: false,
showFilterBar: false,
showQueryBar: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what do we actually show the query bar? Since we cannot manipulate the query?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we? My understanding is that this bar allows users to delete and examine the queries when they are developing the vis. I have been using it to test things extensively. I'm not sure about the dashboard impact, but i guess it is similar there.

showFilterBar: true,
},
stage: 'lab',
feedbackMessage: defaultFeedbackMessage,
Expand Down
155 changes: 149 additions & 6 deletions src/core_plugins/vega/public/vega_view/vega_base_view.js
Expand Up @@ -18,14 +18,39 @@
*/

import $ from 'jquery';
import moment from 'moment';
import dateMath from '@kbn/datemath';
import * as vega from 'vega-lib';
import * as vegaLite from 'vega-lite';
import { Utils } from '../data_model/utils';
import { VISUALIZATION_COLORS } from '@elastic/eui';
import { TooltipHandler } from './vega_tooltip';
import { buildQueryFilter } from 'ui/filter_manager/lib';

vega.scheme('elastic', VISUALIZATION_COLORS);

// Vega's extension functions are global. When called,
// we forward execution to the instance-specific handler
// This functions must be declared in the VegaBaseView class
const vegaFunctions = {
kibanaAddFilter: 'addFilterHandler',
kibanaRemoveFilter: 'removeFilterHandler',
kibanaRemoveAllFilters: 'removeAllFiltersHandler',
kibanaSetTimeFilter: 'setTimeFilterHandler',
};

for (const funcName of Object.keys(vegaFunctions)) {
if (!vega.expressionFunction(funcName)) {
vega.expressionFunction(
funcName,
function handlerFwd(...args) {
const view = this.context.dataflow;
view.runAfter(() => view._kibanaView.vegaFunctionsHandler(funcName, ...args));
}
);
}
}

const bypassToken = Symbol();

export function bypassExternalUrlCheck(url) {
Expand All @@ -34,12 +59,17 @@ export function bypassExternalUrlCheck(url) {
}

export class VegaBaseView {
constructor(vegaConfig, editorMode, parentEl, vegaParser, serviceSettings) {
this._vegaConfig = vegaConfig;
this._editorMode = editorMode;
this._$parentEl = $(parentEl);
this._parser = vegaParser;
this._serviceSettings = serviceSettings;
constructor(opts) {
// $rootScope is a temp workaround, see usage below
this._$rootScope = opts.$rootScope;
this._vegaConfig = opts.vegaConfig;
this._editorMode = opts.editorMode;
this._$parentEl = $(opts.parentEl);
this._parser = opts.vegaParser;
this._serviceSettings = opts.serviceSettings;
this._queryfilter = opts.queryfilter;
this._timefilter = opts.timefilter;
this._findIndex = opts.findIndex;
this._view = null;
this._vegaViewConfig = null;
this._$messages = null;
Expand Down Expand Up @@ -175,6 +205,10 @@ export class VegaBaseView {
this._view = view;

if (view) {

// Global vega expression handler uses it to call custom functions
view._kibanaView = this;

if (this._parser.tooltips) {
// position and padding can be specified with
// {config:{kibana:{tooltips: {position: 'top', padding: 15 } }}}
Expand All @@ -188,6 +222,115 @@ export class VegaBaseView {
}
}

/**
* Handle
* @param funcName
* @param args
* @returns {Promise<void>}
*/
async vegaFunctionsHandler(funcName, ...args) {
try {
const handlerFunc = vegaFunctions[funcName];
if (!handlerFunc || !this[handlerFunc]) {
// in case functions don't match the list above
throw new Error(`${funcName}() is not defined for this graph`);
}
await this[handlerFunc](...args);
} catch (err) {
this.onError(err);
}
}

/**
* @param {object} query Elastic Query DSL snippet, as used in the query DSL editor
* @param {string} [index] as defined in Kibana, or default if missing
*/
async addFilterHandler(query, index) {
const indexId = await this._findIndex(index);
const filter = buildQueryFilter(query, indexId);
await this._queryfilter.addFilters(filter);
}

/**
* @param {object} query Elastic Query DSL snippet, as used in the query DSL editor
* @param {string} [index] as defined in Kibana, or default if missing
*/
async removeFilterHandler(query, index) {
const indexId = await this._findIndex(index);
const filter = buildQueryFilter(query, indexId);

// This is a workaround for the https://github.com/elastic/kibana/issues/18863
// Once fixed, replace with a direct call (no await is needed because its not async)
// this._queryfilter.removeFilter(filter);
this._$rootScope.$evalAsync(() => {
try {
this._queryfilter.removeFilter(filter);
} catch (err) {
this.onError(err);
}
});
}

removeAllFiltersHandler() {
this._queryfilter.removeAll();
}

/**
* Update dashboard time filter to the new values
* @param {number|string|Date} start
* @param {number|string|Date} end
*/
setTimeFilterHandler(start, end) {
this._timefilter.setTime(VegaBaseView._parseTimeRange(start, end));
}

/**
* Parse start and end values, determining the mode, and if order should be reversed
* @private
*/
static _parseTimeRange(start, end) {
const absStart = moment(start);
const absEnd = moment(end);
const isValidAbsStart = absStart.isValid();
const isValidAbsEnd = absEnd.isValid();
let mode = 'absolute';
let from;
let to;
let reverse;

if (isValidAbsStart && isValidAbsEnd) {
// Both are valid absolute dates.
from = absStart;
to = absEnd;
reverse = absStart.isAfter(absEnd);
} else {
// Try to parse as relative dates too (absolute dates will also be accepted)
const startDate = dateMath.parse(start);
const endDate = dateMath.parse(end);
if (!startDate || !endDate || !startDate.isValid() || !endDate.isValid()) {
throw new Error(`Error setting time filter: both time values must be either relative or absolute dates. ` +
`start=${JSON.stringify(start)}, end=${JSON.stringify(end)}`);
}
reverse = startDate.isAfter(endDate);
if (isValidAbsStart || isValidAbsEnd) {
// Mixing relative and absolute - treat them as absolute
from = startDate;
to = endDate;
} else {
// Both dates are relative
mode = 'relative';
from = start;
to = end;
}
}

if (reverse) {
[from, to] = [to, from];
}

return { from, to, mode };
}

/**
* Set global debug variable to simplify vega debugging in console. Show info message first time
*/
Expand Down
38 changes: 35 additions & 3 deletions src/core_plugins/vega/public/vega_visualization.js
Expand Up @@ -20,9 +20,12 @@
import { Notifier } from 'ui/notify';
import { VegaView } from './vega_view/vega_view';
import { VegaMapView } from './vega_view/vega_map_view';
import { SavedObjectsClientProvider, findObjectByTitle } from 'ui/saved_objects';

export function VegaVisualizationProvider(vegaConfig, serviceSettings) {
// $rootScope is for the removeFilter() workaround, see vega_view/vega_base_view.js
export function VegaVisualizationProvider(Private, vegaConfig, serviceSettings, $rootScope) {

const savedObjectsClient = Private(SavedObjectsClientProvider);
const notify = new Notifier({ location: 'Vega' });

return class VegaVisualization {
Expand All @@ -31,6 +34,23 @@ export function VegaVisualizationProvider(vegaConfig, serviceSettings) {
this._vis = vis;
}

/**
* Find index pattern by its title, of if not given, gets default
* @param {string} [index]
* @returns {Promise<string>} index id
*/
async findIndex(index) {
let idxObj;
if (index) {
idxObj = await findObjectByTitle(savedObjectsClient, 'index-pattern', index);
if (!idxObj) throw new Error(`Index "${index}" not found`);
} else {
idxObj = await this._vis.API.indexPatterns.getDefault();
if (!idxObj) throw new Error('Unable to find default index');
}
return idxObj.id;
}

/**
*
* @param {VegaParser} visData
Expand Down Expand Up @@ -65,10 +85,22 @@ export function VegaVisualizationProvider(vegaConfig, serviceSettings) {
this._vegaView = null;
}

const vegaViewParams = {
vegaConfig,
editorMode: this._vis.editorMode,
parentEl: this._el,
vegaParser,
serviceSettings,
queryfilter: this._vis.API.queryFilter,
timefilter: this._vis.API.timeFilter,
findIndex: this.findIndex.bind(this),
$rootScope,
};

if (vegaParser.useMap) {
this._vegaView = new VegaMapView(vegaConfig, this._vis.editorMode, this._el, vegaParser, serviceSettings);
this._vegaView = new VegaMapView(vegaViewParams);
} else {
this._vegaView = new VegaView(vegaConfig, this._vis.editorMode, this._el, vegaParser, serviceSettings);
this._vegaView = new VegaView(vegaViewParams);
}
await this._vegaView.init();

Expand Down