Skip to content

Commit

Permalink
[Vega] Implement context filter modification (elastic#17586)
Browse files Browse the repository at this point in the history
* [Vega] Implement context filter modification

elastic#17210

Testing code (click button)

```
{
  "$schema": "https://vega.github.io/schema/vega/v3.json",
  "marks": [
    {
      "name": "myButton",
      "type": "rect",
      "encode": {
        "enter": {
          "xc": {"signal": "width/2"},
          "yc": {"signal": "height/2"},
          "width": {"signal": "width*0.8"},
          "height": {"signal": "height*0.8"},

          "cornerRadius": {"value": 6},
          "strokeWidth": {"value": 10}
        },
        "update": {
          "stroke": {"value": "gray"},
          "fill": {"value": "lightgray"}
        },
        "hover": {"fill": {"value": "gray"}}
      }
    }
  ],
  "signals": [
    {
      "name": "%ADD_FILTER%",
      "on": [
        {
          "events": "@mybutton:click",
          "update": "{field: 'SRC', value: 10, operator: 'IS'}"
        }
      ]
    }
  ]
}
```
  • Loading branch information
nyurik committed Jul 2, 2018
1 parent 4e42c18 commit 00e3f5a
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 11 deletions.
4 changes: 2 additions & 2 deletions src/core_plugins/vega/public/vega_type.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ VisTypesRegistryProvider.register((Private) => {
responseHandler: 'none',
options: {
showIndexSelection: false,
showQueryBar: false,
showFilterBar: false,
showQueryBar: true,
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

0 comments on commit 00e3f5a

Please sign in to comment.