Drilldowns for TSVB / Vega / Timelion (#74848)

* Drilldowns for TSVB / Vega

Closes: #60611

* fix PR comment

* fix PR comments

* add support for Timelion

* rename vis.API.events.brush -> vis.API.events.applyFilter
This commit is contained in:
Alexey Antonov 2020-08-14 15:46:55 +03:00 committed by GitHub
parent 67e28ac8b4
commit f6f59ec261
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 124 additions and 40 deletions

View file

@ -21,8 +21,10 @@ import React from 'react';
import { Sheet } from '../helpers/timelion_request_handler';
import { Panel } from './panel';
import { ExprVisAPIEvents } from '../../../visualizations/public';
interface ChartComponentProp {
applyFilter: ExprVisAPIEvents['applyFilter'];
interval: string;
renderComplete(): void;
seriesList: Sheet;

View file

@ -33,10 +33,12 @@ import {
colors,
Axis,
} from '../helpers/panel_utils';
import { Series, Sheet } from '../helpers/timelion_request_handler';
import { tickFormatters } from '../helpers/tick_formatters';
import { generateTicksProvider } from '../helpers/tick_generator';
import { TimelionVisDependencies } from '../plugin';
import { ExprVisAPIEvents } from '../../../visualizations/public';
interface CrosshairPlot extends jquery.flot.plot {
setCrosshair: (pos: Position) => void;
@ -44,6 +46,7 @@ interface CrosshairPlot extends jquery.flot.plot {
}
interface PanelProps {
applyFilter: ExprVisAPIEvents['applyFilter'];
interval: string;
seriesList: Sheet;
renderComplete(): void;
@ -72,7 +75,7 @@ const DEBOUNCE_DELAY = 50;
// ensure legend is the same height with or without a caption so legend items do not move around
const emptyCaption = '<br>';
function Panel({ interval, seriesList, renderComplete }: PanelProps) {
function Panel({ interval, seriesList, renderComplete, applyFilter }: PanelProps) {
const kibana = useKibana<TimelionVisDependencies>();
const [chart, setChart] = useState(() => cloneDeep(seriesList.list));
const [canvasElem, setCanvasElem] = useState<HTMLDivElement>();
@ -346,12 +349,21 @@ function Panel({ interval, seriesList, renderComplete }: PanelProps) {
const plotSelectedHandler = useCallback(
(event: JQuery.TriggeredEvent, ranges: Ranges) => {
kibana.services.timefilter.setTime({
from: moment(ranges.xaxis.from),
to: moment(ranges.xaxis.to),
applyFilter({
timeFieldName: '*',
filters: [
{
range: {
'*': {
gte: ranges.xaxis.from,
lte: ranges.xaxis.to,
},
},
},
],
});
},
[kibana.services.timefilter]
[applyFilter]
);
useEffect(() => {

View file

@ -38,6 +38,7 @@ function TimelionVisComponent(props: TimelionVisComponentProp) {
return (
<div className="timVis">
<ChartComponent
applyFilter={props.vis.API.events.applyFilter}
seriesList={props.visData.sheet[0]}
renderComplete={props.renderComplete}
interval={props.vis.getState().params.interval}

View file

@ -27,6 +27,8 @@ import { TimelionVisComponent, TimelionVisComponentProp } from './components';
import { TimelionOptions, TimelionOptionsProps } from './timelion_options';
import { TimelionVisDependencies } from './plugin';
import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public';
export const TIMELION_VIS_NAME = 'timelion';
export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) {
@ -63,6 +65,9 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies)
requestHandler: timelionRequestHandler,
responseHandler: 'none',
inspectorAdapters: {},
getSupportedTriggers: () => {
return [VIS_EVENT_TO_TRIGGER.applyFilter];
},
options: {
showIndexSelection: false,
showQueryBar: false,

View file

@ -50,7 +50,7 @@ export class VisEditor extends Component {
visFields: props.visFields,
extractedIndexPatterns: [''],
};
this.onBrush = createBrushHandler(getDataStart().query.timefilter.timefilter);
this.onBrush = createBrushHandler((data) => props.vis.API.events.applyFilter(data));
this.visDataSubject = new Rx.BehaviorSubject(this.props.visData);
this.visData$ = this.visDataSubject.asObservable().pipe(share());

View file

@ -18,28 +18,31 @@
*/
import { createBrushHandler } from './create_brush_handler';
import moment from 'moment';
import { ExprVisAPIEvents } from '../../../../visualizations/public';
describe('brushHandler', () => {
let mockTimefilter;
let onBrush;
let onBrush: ReturnType<typeof createBrushHandler>;
let applyFilter: ExprVisAPIEvents['applyFilter'];
beforeEach(() => {
mockTimefilter = {
time: {},
setTime: function (time) {
this.time = time;
},
};
onBrush = createBrushHandler(mockTimefilter);
applyFilter = jest.fn();
onBrush = createBrushHandler(applyFilter);
});
it('returns brushHandler() that updates timefilter', () => {
const from = '2017-01-01T00:00:00Z';
const to = '2017-01-01T00:10:00Z';
onBrush(from, to);
expect(mockTimefilter.time.from).toEqual(moment(from).toISOString());
expect(mockTimefilter.time.to).toEqual(moment(to).toISOString());
expect(mockTimefilter.time.mode).toEqual('absolute');
test('returns brushHandler() should updates timefilter through vis.API.events.applyFilter', () => {
const gte = '2017-01-01T00:00:00Z';
const lte = '2017-01-01T00:10:00Z';
onBrush(gte, lte);
expect(applyFilter).toHaveBeenCalledWith({
timeFieldName: '*',
filters: [
{
range: { '*': { gte: '2017-01-01T00:00:00Z', lte: '2017-01-01T00:10:00Z' } },
},
],
});
});
});

View file

@ -17,14 +17,23 @@
* under the License.
*/
import moment from 'moment';
import { ExprVisAPIEvents } from '../../../../visualizations/public';
const TIME_MODE = 'absolute';
export const createBrushHandler = (timefilter) => (from, to) => {
timefilter.setTime({
from: moment(from).toISOString(),
to: moment(to).toISOString(),
mode: TIME_MODE,
export const createBrushHandler = (applyFilter: ExprVisAPIEvents['applyFilter']) => (
gte: string,
lte: string
) => {
return applyFilter({
timeFieldName: '*',
filters: [
{
range: {
'*': {
gte,
lte,
},
},
},
],
});
};

View file

@ -25,6 +25,7 @@ import { EditorController } from './application';
// @ts-ignore
import { PANEL_TYPES } from '../common/panel_types';
import { VisEditor } from './application/components/vis_editor_lazy';
import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public';
export const metricsVisDefinition = {
name: 'metrics',
@ -78,6 +79,9 @@ export const metricsVisDefinition = {
showIndexSelection: false,
},
requestHandler: metricsRequestHandler,
getSupportedTriggers: () => {
return [VIS_EVENT_TO_TRIGGER.applyFilter];
},
inspectorAdapters: {},
responseHandler: 'none',
};

View file

@ -27,6 +27,7 @@ import { createVegaRequestHandler } from './vega_request_handler';
import { createVegaVisualization } from './vega_visualization';
import { getDefaultSpec } from './default_spec';
import { createInspectorAdapters } from './vega_inspector';
import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public';
export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependencies) => {
const requestHandler = createVegaRequestHandler(dependencies);
@ -54,6 +55,9 @@ export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependen
showQueryBar: true,
showFilterBar: true,
},
getSupportedTriggers: () => {
return [VIS_EVENT_TO_TRIGGER.applyFilter];
},
stage: 'experimental',
inspectorAdapters: createInspectorAdapters,
};

View file

@ -63,6 +63,7 @@ export class VegaBaseView {
this._parser = opts.vegaParser;
this._serviceSettings = opts.serviceSettings;
this._filterManager = opts.filterManager;
this._applyFilter = opts.applyFilter;
this._timefilter = opts.timefilter;
this._findIndex = opts.findIndex;
this._view = null;
@ -263,7 +264,8 @@ export class VegaBaseView {
async addFilterHandler(query, index) {
const indexId = await this._findIndex(index);
const filter = esFilters.buildQueryFilter(query, indexId);
this._filterManager.addFilters(filter);
this._applyFilter({ filters: [filter] });
}
/**
@ -298,7 +300,22 @@ export class VegaBaseView {
* @param {number|string|Date} end
*/
setTimeFilterHandler(start, end) {
this._timefilter.setTime(VegaBaseView._parseTimeRange(start, end));
const { from, to, mode } = VegaBaseView._parseTimeRange(start, end);
this._applyFilter({
timeFieldName: '*',
filters: [
{
range: {
'*': {
mode,
gte: from,
lte: to,
},
},
},
],
});
}
/**

View file

@ -106,6 +106,7 @@ export const createVegaVisualization = ({ serviceSettings }) =>
const { timefilter } = this.dataPlugin.query.timefilter;
const vegaViewParams = {
parentEl: this._el,
applyFilter: this._vis.API.events.applyFilter,
vegaParser,
serviceSettings,
filterManager,

View file

@ -105,6 +105,11 @@ describe('VegaVisualizations', () => {
vis = {
type: vegaVisType,
API: {
events: {
applyFilter: jest.fn(),
},
},
};
});

View file

@ -17,14 +17,20 @@
* under the License.
*/
import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER } from '../../../../plugins/ui_actions/public';
import {
APPLY_FILTER_TRIGGER,
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
} from '../../../../plugins/ui_actions/public';
export interface VisEventToTrigger {
['applyFilter']: typeof APPLY_FILTER_TRIGGER;
['brush']: typeof SELECT_RANGE_TRIGGER;
['filter']: typeof VALUE_CLICK_TRIGGER;
}
export const VIS_EVENT_TO_TRIGGER: VisEventToTrigger = {
applyFilter: APPLY_FILTER_TRIGGER,
brush: SELECT_RANGE_TRIGGER,
filter: VALUE_CLICK_TRIGGER,
};

View file

@ -310,12 +310,21 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
}
if (!this.input.disableTriggers) {
const triggerId =
event.name === 'brush' ? VIS_EVENT_TO_TRIGGER.brush : VIS_EVENT_TO_TRIGGER.filter;
const context = {
embeddable: this,
data: { timeFieldName: this.vis.data.indexPattern?.timeFieldName!, ...event.data },
};
const triggerId = get(VIS_EVENT_TO_TRIGGER, event.name, VIS_EVENT_TO_TRIGGER.filter);
let context;
if (triggerId === VIS_EVENT_TO_TRIGGER.applyFilter) {
context = {
embeddable: this,
timeFieldName: this.vis.data.indexPattern?.timeFieldName!,
...event.data,
};
} else {
context = {
embeddable: this,
data: { timeFieldName: this.vis.data.indexPattern?.timeFieldName!, ...event.data },
};
}
getUiActions().getTrigger(triggerId).exec(context);
}

View file

@ -43,6 +43,7 @@ export interface ExprVisState {
export interface ExprVisAPIEvents {
filter: (data: any) => void;
brush: (data: any) => void;
applyFilter: (data: any) => void;
}
export interface ExprVisAPI {
@ -83,6 +84,10 @@ export class ExprVis extends EventEmitter {
if (!this.eventsSubject) return;
this.eventsSubject.next({ name: 'brush', data });
},
applyFilter: (data: any) => {
if (!this.eventsSubject) return;
this.eventsSubject.next({ name: 'applyFilter', data });
},
},
};
}

View file

@ -51,5 +51,6 @@ export {
VisSavedObject,
VisResponseValue,
} from './types';
export { ExprVisAPIEvents } from './expressions/vis';
export { VisualizationListItem } from './vis_types/vis_type_alias_registry';
export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants';