Lens drilldowns (#65675)

This commit is contained in:
Joe Reuter 2020-05-15 12:01:27 +02:00 committed by GitHub
parent f7a89601c7
commit 52070091f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 521 additions and 298 deletions

View file

@ -53,16 +53,16 @@ export function selectRangeAction(
});
},
isCompatible,
execute: async ({ timeFieldName, data }: SelectRangeActionContext) => {
if (!(await isCompatible({ timeFieldName, data }))) {
execute: async ({ data }: SelectRangeActionContext) => {
if (!(await isCompatible({ data }))) {
throw new IncompatibleActionError();
}
const selectedFilters = await createFiltersFromRangeSelectAction(data);
if (timeFieldName) {
if (data.timeFieldName) {
const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter(
timeFieldName,
data.timeFieldName,
selectedFilters
);
filterManager.addFilters(restOfFilters);

View file

@ -57,12 +57,12 @@ export function valueClickAction(
});
},
isCompatible,
execute: async (context: ValueClickActionContext) => {
if (!(await isCompatible(context))) {
execute: async ({ data }: ValueClickActionContext) => {
if (!(await isCompatible({ data }))) {
throw new IncompatibleActionError();
}
const filters: Filter[] = await createFiltersFromValueClickAction(context.data);
const filters: Filter[] = await createFiltersFromValueClickAction(data);
let selectedFilters = filters;
@ -98,9 +98,9 @@ export function valueClickAction(
selectedFilters = await filterSelectionPromise;
}
if (context.timeFieldName) {
if (data.timeFieldName) {
const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter(
context.timeFieldName,
data.timeFieldName,
selectedFilters
);
filterManager.addFilters(restOfFilters);

View file

@ -27,7 +27,6 @@ export interface EmbeddableContext {
export interface ValueClickTriggerContext<T extends IEmbeddable = IEmbeddable> {
embeddable?: T;
timeFieldName?: string;
data: {
data: Array<{
table: Pick<KibanaDatatable, 'rows' | 'columns'>;
@ -35,6 +34,7 @@ export interface ValueClickTriggerContext<T extends IEmbeddable = IEmbeddable> {
row: number;
value: any;
}>;
timeFieldName?: string;
negate?: boolean;
};
}
@ -45,11 +45,11 @@ export const isValueClickTriggerContext = (
export interface RangeSelectTriggerContext<T extends IEmbeddable = IEmbeddable> {
embeddable?: T;
timeFieldName?: string;
data: {
table: KibanaDatatable;
column: number;
range: number[];
timeFieldName?: string;
};
}

View file

@ -264,8 +264,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
event.name === 'brush' ? VIS_EVENT_TO_TRIGGER.brush : VIS_EVENT_TO_TRIGGER.filter;
const context = {
embeddable: this,
timeFieldName: this.vis.data.indexPattern!.timeFieldName!,
data: event.data,
data: { timeFieldName: this.vis.data.indexPattern?.timeFieldName!, ...event.data },
};
getUiActions()

View file

@ -27,9 +27,6 @@ import { getDocumentationLinks } from './lib/documentation_links';
import { HelpMenu } from './components/help_menu/help_menu';
import { createStore, destroyStore } from './store';
import { VALUE_CLICK_TRIGGER, ActionByType } from '../../../../src/plugins/ui_actions/public';
/* eslint-disable */
import { ACTION_VALUE_CLICK } from '../../../../src/plugins/data/public/actions/value_click_action';
/* eslint-enable */
import { init as initStatsReporter } from './lib/ui_metric';
@ -45,16 +42,6 @@ import './style/index.scss';
const { ReadOnlyBadge: strings } = CapabilitiesStrings;
let restoreAction: ActionByType<any> | undefined;
const emptyAction = {
id: 'empty-action',
type: '',
getDisplayName: () => 'empty action',
getIconType: () => undefined,
isCompatible: async () => true,
execute: async () => undefined,
} as ActionByType<any>;
export const renderApp = (
coreStart: CoreStart,
plugins: CanvasStartDeps,
@ -134,17 +121,6 @@ export const initializeCanvas = async (
},
});
// TODO: We need this to disable the filtering modal from popping up in lens embeds until
// they honor the disableTriggers parameter
const action = startPlugins.uiActions.getAction(ACTION_VALUE_CLICK);
if (action) {
restoreAction = action;
startPlugins.uiActions.detachAction(VALUE_CLICK_TRIGGER, action.id);
startPlugins.uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, emptyAction);
}
if (setupPlugins.usageCollection) {
initStatsReporter(setupPlugins.usageCollection.reportUiStats);
}
@ -158,12 +134,6 @@ export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDe
resetInterpreter();
destroyStore();
startPlugins.uiActions.detachAction(VALUE_CLICK_TRIGGER, emptyAction.id);
if (restoreAction) {
startPlugins.uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, restoreAction);
restoreAction = undefined;
}
coreStart.chrome.setBadge(undefined);
coreStart.chrome.setHelpExtension(undefined);

View file

@ -42,14 +42,6 @@ export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLY
if (!supportedTriggers || !supportedTriggers.length) return false;
if (context.embeddable.getRoot().type !== 'dashboard') return false;
/**
* Temporarily disable drilldowns for Lens as Lens embeddable does not have
* `.embeddable` field on VALUE_CLICK_TRIGGER context.
*
* @todo Remove this condition once Lens adds `.embeddable` to field to context.
*/
if (context.embeddable.type === 'lens') return false;
return supportedTriggers.indexOf('VALUE_CLICK_TRIGGER') > -1;
}

View file

@ -134,10 +134,12 @@ describe('.execute() & getHref', () => {
};
const context = ({
data: useRangeEvent
? ({ range: {} } as RangeSelectTriggerContext['data'])
: ({ data: [] } as ValueClickTriggerContext['data']),
timeFieldName: 'order_date',
data: {
...(useRangeEvent
? ({ range: {} } as RangeSelectTriggerContext['data'])
: ({ data: [] } as ValueClickTriggerContext['data'])),
timeFieldName: 'order_date',
},
embeddable: {
getInput: () => ({
filters: [],

View file

@ -127,9 +127,9 @@ export class DashboardToDashboardDrilldown
}
})();
if (context.timeFieldName) {
if (context.data.timeFieldName) {
const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter(
context.timeFieldName,
context.data.timeFieldName,
filtersFromEvent
);
filtersFromEvent = restOfFilters;

View file

@ -9,10 +9,9 @@
"expressions",
"navigation",
"kibanaLegacy",
"uiActions",
"visualizations",
"dashboard"
],
"optionalPlugins": ["embeddable", "usageCollection", "taskManager"],
"optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions"],
"configPath": ["xpack", "lens"]
}

View file

@ -13,7 +13,7 @@ import { DatatableProps } from './expression';
import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks';
import { IFieldFormat } from '../../../../../src/plugins/data/public';
import { IAggType } from 'src/plugins/data/public';
const executeTriggerActions = jest.fn();
const onClickValue = jest.fn();
function sampleArgs() {
const data: LensMultiTable = {
@ -66,7 +66,7 @@ describe('datatable_expression', () => {
data={data}
args={args}
formatFactory={x => x as IFieldFormat}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
getType={jest.fn()}
/>
)
@ -87,7 +87,7 @@ describe('datatable_expression', () => {
}}
args={args}
formatFactory={x => x as IFieldFormat}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
getType={jest.fn(() => ({ type: 'buckets' } as IAggType))}
/>
);
@ -97,18 +97,16 @@ describe('datatable_expression', () => {
.first()
.simulate('click');
expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', {
data: {
data: [
{
column: 0,
row: 0,
table: data.tables.l1,
value: 10110,
},
],
negate: true,
},
expect(onClickValue).toHaveBeenCalledWith({
data: [
{
column: 0,
row: 0,
table: data.tables.l1,
value: 10110,
},
],
negate: true,
timeFieldName: undefined,
});
});
@ -127,7 +125,7 @@ describe('datatable_expression', () => {
}}
args={args}
formatFactory={x => x as IFieldFormat}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
getType={jest.fn(() => ({ type: 'buckets' } as IAggType))}
/>
);
@ -137,18 +135,16 @@ describe('datatable_expression', () => {
.at(3)
.simulate('click');
expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', {
data: {
data: [
{
column: 1,
row: 0,
table: data.tables.l1,
value: 1588024800000,
},
],
negate: false,
},
expect(onClickValue).toHaveBeenCalledWith({
data: [
{
column: 1,
row: 0,
table: data.tables.l1,
value: 1588024800000,
},
],
negate: false,
timeFieldName: 'b',
});
});

View file

@ -10,17 +10,17 @@ import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { EuiBasicTable, EuiFlexGroup, EuiButtonIcon, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { IAggType } from 'src/plugins/data/public';
import { FormatFactory, LensMultiTable } from '../types';
import {
FormatFactory,
ILensInterpreterRenderHandlers,
LensFilterEvent,
LensMultiTable,
} from '../types';
import {
ExpressionFunctionDefinition,
ExpressionRenderDefinition,
IInterpreterRenderHandlers,
} from '../../../../../src/plugins/expressions/public';
import { VisualizationContainer } from '../visualization_container';
import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
import { getExecuteTriggerActions } from '../services';
export interface DatatableColumns {
columnIds: string[];
}
@ -37,7 +37,7 @@ export interface DatatableProps {
type DatatableRenderProps = DatatableProps & {
formatFactory: FormatFactory;
executeTriggerActions: UiActionsStart['executeTriggerActions'];
onClickValue: (data: LensFilterEvent['data']) => void;
getType: (name: string) => IAggType;
};
@ -125,17 +125,19 @@ export const getDatatableRenderer = (dependencies: {
render: async (
domNode: Element,
config: DatatableProps,
handlers: IInterpreterRenderHandlers
handlers: ILensInterpreterRenderHandlers
) => {
const resolvedFormatFactory = await dependencies.formatFactory;
const executeTriggerActions = getExecuteTriggerActions();
const resolvedGetType = await dependencies.getType;
const onClickValue = (data: LensFilterEvent['data']) => {
handlers.event({ name: 'filter', data });
};
ReactDOM.render(
<I18nProvider>
<DatatableComponent
{...config}
formatFactory={resolvedFormatFactory}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
getType={resolvedGetType}
/>
</I18nProvider>,
@ -162,21 +164,19 @@ export function DatatableComponent(props: DatatableRenderProps) {
const timeFieldName = negate && isDateHistogram ? undefined : col?.meta?.aggConfigParams?.field;
const rowIndex = firstTable.rows.findIndex(row => row[field] === value);
const context: ValueClickTriggerContext = {
data: {
negate,
data: [
{
row: rowIndex,
column: colIndex,
value,
table: firstTable,
},
],
},
const data: LensFilterEvent['data'] = {
negate,
data: [
{
row: rowIndex,
column: colIndex,
value,
table: firstTable,
},
],
timeFieldName,
};
props.executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context);
props.onClickValue(data);
};
return (

View file

@ -4,12 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, CoreStart } from 'kibana/public';
import { CoreSetup } from 'kibana/public';
import { datatableVisualization } from './visualization';
import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public';
import { datatable, datatableColumns, getDatatableRenderer } from './expression';
import { EditorFrameSetup, FormatFactory } from '../types';
import { setExecuteTriggerActions } from '../services';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
@ -42,7 +41,4 @@ export class DatatableVisualization {
);
editorFrame.registerVisualization(datatableVisualization);
}
start(core: CoreStart, { uiActions }: DatatableVisualizationPluginStartPlugins) {
setExecuteTriggerActions(uiActions.executeTriggerActions);
}
}

View file

@ -21,6 +21,9 @@ import {
import { ReactExpressionRendererType } from 'src/plugins/expressions/public';
import { DragDrop } from '../../drag_drop';
import { FrameLayout } from './frame_layout';
import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks';
function generateSuggestion(state = {}): DatasourceSuggestion {
return {
@ -48,6 +51,11 @@ function getDefaultProps() {
query: { query: '', language: 'lucene' },
filters: [],
core: coreMock.createSetup(),
plugins: {
uiActions: uiActionsPluginMock.createStartContract(),
data: dataPluginMock.createStartContract(),
expressions: expressionsPluginMock.createStartContract(),
},
};
}

View file

@ -26,6 +26,7 @@ import { getSavedObjectFormat } from './save';
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
import { generateId } from '../../id_generator';
import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public';
import { EditorFrameStartPlugins } from '../service';
export interface EditorFrameProps {
doc?: Document;
@ -36,6 +37,7 @@ export interface EditorFrameProps {
ExpressionRenderer: ReactExpressionRendererType;
onError: (e: { message: string }) => void;
core: CoreSetup | CoreStart;
plugins: EditorFrameStartPlugins;
dateRange: {
fromDate: string;
toDate: string;
@ -285,6 +287,7 @@ export function EditorFrame(props: EditorFrameProps) {
dispatch={dispatch}
ExpressionRenderer={props.ExpressionRenderer}
core={props.core}
plugins={props.plugins}
/>
</WorkspacePanelWrapper>
)

View file

@ -9,6 +9,9 @@ import { EditorFrameProps } from './index';
import { Datasource, Visualization } from '../../types';
import { createExpressionRendererMock } from '../mocks';
import { coreMock } from 'src/core/public/mocks';
import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks';
describe('editor_frame state management', () => {
describe('initialization', () => {
@ -24,6 +27,11 @@ describe('editor_frame state management', () => {
ExpressionRenderer: createExpressionRendererMock(),
onChange: jest.fn(),
core: coreMock.createSetup(),
plugins: {
uiActions: uiActionsPluginMock.createStartContract(),
data: dataPluginMock.createStartContract(),
expressions: expressionsPluginMock.createStartContract(),
},
dateRange: { fromDate: 'now-7d', toDate: 'now' },
query: { query: '', language: 'lucene' },
filters: [],

View file

@ -22,6 +22,10 @@ import { DragDrop, ChildDragDropProvider } from '../../drag_drop';
import { Ast } from '@kbn/interpreter/common';
import { coreMock } from 'src/core/public/mocks';
import { esFilters, IFieldType, IIndexPattern } from '../../../../../../src/plugins/data/public';
import { TriggerId, UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks';
import { TriggerContract } from '../../../../../../src/plugins/ui_actions/public/triggers';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable';
describe('workspace_panel', () => {
let mockVisualization: jest.Mocked<Visualization>;
@ -29,10 +33,15 @@ describe('workspace_panel', () => {
let mockDatasource: DatasourceMock;
let expressionRendererMock: jest.Mock<React.ReactElement, [ReactExpressionRendererProps]>;
let uiActionsMock: jest.Mocked<UiActionsStart>;
let trigger: jest.Mocked<TriggerContract<TriggerId>>;
let instance: ReactWrapper<WorkspacePanelProps>;
beforeEach(() => {
trigger = ({ exec: jest.fn() } as unknown) as jest.Mocked<TriggerContract<TriggerId>>;
uiActionsMock = uiActionsPluginMock.createStartContract();
uiActionsMock.getTrigger.mockReturnValue(trigger);
mockVisualization = createMockVisualization();
mockVisualization2 = createMockVisualization();
@ -60,6 +69,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
);
@ -82,6 +92,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
);
@ -104,6 +115,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
);
@ -140,6 +152,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
);
@ -198,6 +211,48 @@ describe('workspace_panel', () => {
`);
});
it('should execute a trigger on expression event', () => {
const framePublicAPI = createMockFramePublicAPI();
framePublicAPI.datasourceLayers = {
first: mockDatasource.publicAPIMock,
};
mockDatasource.toExpression.mockReturnValue('datasource');
mockDatasource.getLayers.mockReturnValue(['first']);
instance = mount(
<InnerWorkspacePanel
activeDatasourceId={'mock'}
datasourceStates={{
mock: {
state: {},
isLoading: false,
},
}}
datasourceMap={{
mock: mockDatasource,
}}
framePublicAPI={framePublicAPI}
activeVisualizationId="vis"
visualizationMap={{
vis: { ...mockVisualization, toExpression: () => 'vis' },
}}
visualizationState={{}}
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
);
const onEvent = expressionRendererMock.mock.calls[0][0].onEvent!;
const eventData = {};
onEvent({ name: 'brush', data: eventData });
expect(uiActionsMock.getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush);
expect(trigger.exec).toHaveBeenCalledWith({ data: eventData });
});
it('should include data fetching for each layer in the expression', () => {
const mockDatasource2 = createMockDatasource('a');
const framePublicAPI = createMockFramePublicAPI();
@ -237,6 +292,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
);
@ -316,6 +372,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
);
});
@ -370,6 +427,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
);
});
@ -424,6 +482,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
);
@ -461,6 +520,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
);
});
@ -504,6 +564,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
);
});
@ -559,6 +620,7 @@ describe('workspace_panel', () => {
dispatch={mockDispatch}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
/>
</ChildDragDropProvider>
);

View file

@ -17,14 +17,25 @@ import {
EuiButtonEmpty,
} from '@elastic/eui';
import { CoreStart, CoreSetup } from 'kibana/public';
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
import {
ExpressionRendererEvent,
ReactExpressionRendererType,
} from '../../../../../../src/plugins/expressions/public';
import { Action } from './state_management';
import { Datasource, Visualization, FramePublicAPI } from '../../types';
import {
Datasource,
Visualization,
FramePublicAPI,
isLensBrushEvent,
isLensFilterEvent,
} from '../../types';
import { DragDrop, DragContext } from '../../drag_drop';
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
import { buildExpression } from './expression_helpers';
import { debouncedComponent } from '../../debounced_component';
import { trackUiEvent } from '../../lens_ui_telemetry';
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public';
export interface WorkspacePanelProps {
activeVisualizationId: string | null;
@ -43,6 +54,7 @@ export interface WorkspacePanelProps {
dispatch: (action: Action) => void;
ExpressionRenderer: ReactExpressionRendererType;
core: CoreStart | CoreSetup;
plugins: { uiActions?: UiActionsStart };
}
export const WorkspacePanel = debouncedComponent(InnerWorkspacePanel);
@ -58,6 +70,7 @@ export function InnerWorkspacePanel({
framePublicAPI,
dispatch,
core,
plugins,
ExpressionRenderer: ExpressionRendererComponent,
}: WorkspacePanelProps) {
const IS_DARK_THEME = core.uiSettings.get('theme:darkMode');
@ -211,6 +224,22 @@ export function InnerWorkspacePanel({
className="lnsExpressionRenderer__component"
padding="m"
expression={expression!}
onEvent={(event: ExpressionRendererEvent) => {
if (!plugins.uiActions) {
// ui actions not available, not handling event...
return;
}
if (isLensBrushEvent(event)) {
plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({
data: event.data,
});
}
if (isLensFilterEvent(event)) {
plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({
data: event.data,
});
}
}}
renderError={(errorMessage?: string | null) => {
return (
<EuiFlexGroup direction="column" alignItems="center">

View file

@ -10,6 +10,7 @@ import { ReactExpressionRendererProps } from 'src/plugins/expressions/public';
import { Query, TimeRange, Filter, TimefilterContract } from 'src/plugins/data/public';
import { Document } from '../../persistence';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable';
jest.mock('../../../../../../src/plugins/inspector/public/', () => ({
isAvailable: false,
@ -34,10 +35,14 @@ const savedVis: Document = {
describe('embeddable', () => {
let mountpoint: HTMLDivElement;
let expressionRenderer: jest.Mock<null, [ReactExpressionRendererProps]>;
let getTrigger: jest.Mock;
let trigger: { exec: jest.Mock };
beforeEach(() => {
mountpoint = document.createElement('div');
expressionRenderer = jest.fn(_props => null);
trigger = { exec: jest.fn() };
getTrigger = jest.fn(() => trigger);
});
afterEach(() => {
@ -48,6 +53,7 @@ describe('embeddable', () => {
const embeddable = new Embeddable(
dataPluginMock.createSetupContract().query.timefilter.timefilter,
expressionRenderer,
getTrigger,
{
editPath: '',
editUrl: '',
@ -70,6 +76,7 @@ describe('embeddable', () => {
const embeddable = new Embeddable(
dataPluginMock.createSetupContract().query.timefilter.timefilter,
expressionRenderer,
getTrigger,
{
editPath: '',
editUrl: '',
@ -97,6 +104,7 @@ describe('embeddable', () => {
const embeddable = new Embeddable(
dataPluginMock.createSetupContract().query.timefilter.timefilter,
expressionRenderer,
getTrigger,
{
editPath: '',
editUrl: '',
@ -114,6 +122,32 @@ describe('embeddable', () => {
});
});
it('should execute trigger on event from expression renderer', () => {
const embeddable = new Embeddable(
dataPluginMock.createSetupContract().query.timefilter.timefilter,
expressionRenderer,
getTrigger,
{
editPath: '',
editUrl: '',
editable: true,
savedVis,
},
{ id: '123' }
);
embeddable.render(mountpoint);
const onEvent = expressionRenderer.mock.calls[0][0].onEvent!;
const eventData = {};
onEvent({ name: 'brush', data: eventData });
expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush);
expect(trigger.exec).toHaveBeenCalledWith(
expect.objectContaining({ data: eventData, embeddable: expect.anything() })
);
});
it('should not re-render if only change is in disabled filter', () => {
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
const query: Query = { language: 'kquery', query: '' };
@ -122,6 +156,7 @@ describe('embeddable', () => {
const embeddable = new Embeddable(
dataPluginMock.createSetupContract().query.timefilter.timefilter,
expressionRenderer,
getTrigger,
{
editPath: '',
editUrl: '',
@ -154,6 +189,7 @@ describe('embeddable', () => {
const embeddable = new Embeddable(
timefilter,
expressionRenderer,
getTrigger,
{
editPath: '',
editUrl: '',

View file

@ -8,25 +8,30 @@ import _ from 'lodash';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import {
Query,
TimeRange,
Filter,
IIndexPattern,
Query,
TimefilterContract,
TimeRange,
} from 'src/plugins/data/public';
import { Subscription } from 'rxjs';
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
import {
ExpressionRendererEvent,
ReactExpressionRendererType,
} from '../../../../../../src/plugins/expressions/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public';
import {
Embeddable as AbstractEmbeddable,
EmbeddableInput,
EmbeddableOutput,
IContainer,
EmbeddableInput,
} from '../../../../../../src/plugins/embeddable/public';
import { Document, DOC_TYPE } from '../../persistence';
import { DOC_TYPE, Document } from '../../persistence';
import { ExpressionWrapper } from './expression_wrapper';
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
import { isLensBrushEvent, isLensFilterEvent } from '../../types';
export interface LensEmbeddableConfiguration {
savedVis: Document;
@ -50,6 +55,7 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
type = DOC_TYPE;
private expressionRenderer: ReactExpressionRendererType;
private getTrigger: UiActionsStart['getTrigger'] | undefined;
private savedVis: Document;
private domNode: HTMLElement | Element | undefined;
private subscription: Subscription;
@ -65,6 +71,7 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
constructor(
timefilter: TimefilterContract,
expressionRenderer: ReactExpressionRendererType,
getTrigger: UiActionsStart['getTrigger'] | undefined,
{ savedVis, editPath, editUrl, editable, indexPatterns }: LensEmbeddableConfiguration,
initialInput: LensEmbeddableInput,
parent?: IContainer
@ -86,6 +93,7 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
parent
);
this.getTrigger = getTrigger;
this.expressionRenderer = expressionRenderer;
this.savedVis = savedVis;
this.subscription = this.getInput$().subscribe(input => this.onContainerStateChanged(input));
@ -100,6 +108,9 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
switch (this.savedVis.visualizationType) {
case 'lnsXY':
return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush];
case 'lnsDatatable':
case 'lnsPie':
return [VIS_EVENT_TO_TRIGGER.filter];
case 'lnsMetric':
default:
return [];
@ -140,11 +151,30 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
ExpressionRenderer={this.expressionRenderer}
expression={this.savedVis.expression}
context={this.currentContext}
handleEvent={this.handleEvent}
/>,
domNode
);
}
handleEvent = (event: ExpressionRendererEvent) => {
if (!this.getTrigger || this.input.disableTriggers) {
return;
}
if (isLensBrushEvent(event)) {
this.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({
data: event.data,
embeddable: this,
});
}
if (isLensFilterEvent(event)) {
this.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({
data: event.data,
embeddable: this,
});
}
};
destroy() {
super.destroy();
if (this.domNode) {

View file

@ -26,6 +26,7 @@ import {
import { Embeddable } from './embeddable';
import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence';
import { getEditPath } from '../../../common';
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
interface StartServices {
timefilter: TimefilterContract;
@ -34,6 +35,7 @@ interface StartServices {
savedObjectsClient: SavedObjectsClientContract;
expressionRenderer: ReactExpressionRendererType;
indexPatternService: IndexPatternsContract;
uiActions?: UiActionsStart;
}
export class EmbeddableFactory implements EmbeddableFactoryDefinition {
@ -74,6 +76,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
indexPatternService,
timefilter,
expressionRenderer,
uiActions,
} = await this.getStartServices();
const store = new SavedObjectIndexStore(savedObjectsClient);
const savedVis = await store.load(savedObjectId);
@ -99,6 +102,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
return new Embeddable(
timefilter,
expressionRenderer,
uiActions?.getTrigger,
{
savedVis,
editPath: getEditPath(savedObjectId),

View file

@ -9,7 +9,10 @@ import { I18nProvider } from '@kbn/i18n/react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui';
import { TimeRange, Filter, Query } from 'src/plugins/data/public';
import { ReactExpressionRendererType } from 'src/plugins/expressions/public';
import {
ExpressionRendererEvent,
ReactExpressionRendererType,
} from 'src/plugins/expressions/public';
export interface ExpressionWrapperProps {
ExpressionRenderer: ReactExpressionRendererType;
@ -20,12 +23,14 @@ export interface ExpressionWrapperProps {
filters?: Filter[];
lastReloadRequestTime?: number;
};
handleEvent: (event: ExpressionRendererEvent) => void;
}
export function ExpressionWrapper({
ExpressionRenderer: ExpressionRendererComponent,
expression,
context,
handleEvent,
}: ExpressionWrapperProps) {
return (
<I18nProvider>
@ -51,6 +56,7 @@ export function ExpressionWrapper({
expression={expression}
searchContext={{ ...context }}
renderError={error => <div data-test-subj="expression-renderer-error">{error}</div>}
onEvent={handleEvent}
/>
</div>
)}

View file

@ -26,6 +26,7 @@ import { mergeTables } from './merge_tables';
import { formatColumn } from './format_column';
import { EmbeddableFactory } from './embeddable/embeddable_factory';
import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
export interface EditorFrameSetupPlugins {
data: DataPublicPluginSetup;
@ -37,6 +38,7 @@ export interface EditorFrameStartPlugins {
data: DataPublicPluginStart;
embeddable?: EmbeddableStart;
expressions: ExpressionsStart;
uiActions?: UiActionsStart;
}
async function collectAsyncDefinitions<T extends { id: string }>(
@ -73,6 +75,7 @@ export class EditorFrameService {
timefilter: deps.data.query.timefilter.timefilter,
expressionRenderer: deps.expressions.ReactExpressionRenderer,
indexPatternService: deps.data.indexPatterns,
uiActions: deps.uiActions,
};
};
@ -116,6 +119,7 @@ export class EditorFrameService {
(doc && doc.visualizationType) || firstVisualizationId || null
}
core={core}
plugins={plugins}
ExpressionRenderer={plugins.expressions.ReactExpressionRenderer}
doc={doc}
dateRange={dateRange}

View file

@ -5,13 +5,12 @@
*/
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { CoreSetup, CoreStart } from 'src/core/public';
import { CoreSetup } from 'src/core/public';
import { ExpressionsSetup } from 'src/plugins/expressions/public';
import { pieVisualization } from './pie_visualization';
import { pie, getPieRenderer } from './register_expression';
import { EditorFrameSetup, FormatFactory } from '../types';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
import { setExecuteTriggerActions } from '../services';
export interface PieVisualizationPluginSetupPlugins {
editorFrame: EditorFrameSetup;
@ -44,10 +43,4 @@ export class PieVisualization {
editorFrame.registerVisualization(pieVisualization);
}
start(core: CoreStart, { uiActions }: PieVisualizationPluginStartPlugins) {
setExecuteTriggerActions(uiActions.executeTriggerActions);
}
stop() {}
}

View file

@ -14,9 +14,8 @@ import {
ExpressionRenderDefinition,
ExpressionFunctionDefinition,
} from 'src/plugins/expressions/public';
import { LensMultiTable, FormatFactory } from '../types';
import { LensMultiTable, FormatFactory, LensFilterEvent } from '../types';
import { PieExpressionProps, PieExpressionArgs } from './types';
import { getExecuteTriggerActions } from '../services';
import { PieComponent } from './render_function';
export interface PieRender {
@ -109,7 +108,9 @@ export const getPieRenderer = (dependencies: {
config: PieExpressionProps,
handlers: IInterpreterRenderHandlers
) => {
const executeTriggerActions = getExecuteTriggerActions();
const onClickValue = (data: LensFilterEvent['data']) => {
handlers.event({ name: 'filter', data });
};
const formatFactory = await dependencies.formatFactory;
ReactDOM.render(
<I18nProvider>
@ -117,7 +118,7 @@ export const getPieRenderer = (dependencies: {
{...config}
{...dependencies}
formatFactory={formatFactory}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
isDarkMode={dependencies.isDarkMode}
/>
</I18nProvider>,

View file

@ -5,7 +5,7 @@
*/
import React from 'react';
import { Settings } from '@elastic/charts';
import { SeriesIdentifier, Settings } from '@elastic/charts';
import { shallow } from 'enzyme';
import { LensMultiTable } from '../types';
import { PieComponent } from './render_function';
@ -59,7 +59,7 @@ describe('PieVisualization component', () => {
formatFactory: getFormatSpy,
isDarkMode: false,
chartTheme: {},
executeTriggerActions: jest.fn(),
onClickValue: jest.fn(),
};
}
@ -111,6 +111,58 @@ describe('PieVisualization component', () => {
expect(component.find(Settings).prop('legendMaxDepth')).toBeUndefined();
});
test('it calls filter callback with the given context', () => {
const defaultArgs = getDefaultArgs();
const component = shallow(<PieComponent args={{ ...args }} {...defaultArgs} />);
component
.find(Settings)
.first()
.prop('onElementClick')!([[[{ groupByRollup: 6, value: 6 }], {} as SeriesIdentifier]]);
expect(defaultArgs.onClickValue.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"data": Array [
Object {
"column": 0,
"row": 0,
"table": Object {
"columns": Array [
Object {
"id": "a",
"name": "a",
},
Object {
"id": "b",
"name": "b",
},
Object {
"id": "c",
"name": "c",
},
],
"rows": Array [
Object {
"a": 6,
"b": 2,
"c": "I",
"d": "Row 1",
},
Object {
"a": 1,
"b": 5,
"c": "J",
"d": "Row 2",
},
],
"type": "kibana_datatable",
},
"value": 6,
},
],
}
`);
});
test('it shows emptyPlaceholder for undefined grouped data', () => {
const defaultData = getDefaultArgs().data;
const emptyData: LensMultiTable = {

View file

@ -24,12 +24,10 @@ import {
RecursivePartial,
LayerValue,
} from '@elastic/charts';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
import { FormatFactory } from '../types';
import { FormatFactory, LensFilterEvent } from '../types';
import { VisualizationContainer } from '../visualization_container';
import { CHART_NAMES, DEFAULT_PERCENT_DECIMALS } from './constants';
import { ColumnGroups, PieExpressionProps } from './types';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
import { getSliceValueWithFallback, getFilterContext } from './render_helpers';
import { EmptyPlaceholder } from '../shared_components';
import './visualization.scss';
@ -43,13 +41,13 @@ export function PieComponent(
formatFactory: FormatFactory;
chartTheme: Exclude<PartialTheme, undefined>;
isDarkMode: boolean;
executeTriggerActions: UiActionsStart['executeTriggerActions'];
onClickValue: (data: LensFilterEvent['data']) => void;
}
) {
const [firstTable] = Object.values(props.data.tables);
const formatters: Record<string, ReturnType<FormatFactory>> = {};
const { chartTheme, isDarkMode, executeTriggerActions } = props;
const { chartTheme, isDarkMode, onClickValue } = props;
const {
shape,
groups,
@ -246,7 +244,7 @@ export function PieComponent(
firstTable
);
executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context);
onClickValue(context);
}}
/>
<Partition

View file

@ -96,16 +96,14 @@ describe('render helpers', () => {
],
};
expect(getFilterContext([{ groupByRollup: 'Test', value: 100 }], ['a'], table)).toEqual({
data: {
data: [
{
row: 1,
column: 0,
value: 'Test',
table,
},
],
},
data: [
{
row: 1,
column: 0,
value: 'Test',
table,
},
],
});
});
@ -124,16 +122,14 @@ describe('render helpers', () => {
],
};
expect(getFilterContext([{ groupByRollup: 'Test', value: 100 }], ['a', 'b'], table)).toEqual({
data: {
data: [
{
row: 1,
column: 0,
value: 'Test',
table,
},
],
},
data: [
{
row: 1,
column: 0,
value: 'Test',
table,
},
],
});
});
@ -161,22 +157,20 @@ describe('render helpers', () => {
table
)
).toEqual({
data: {
data: [
{
row: 1,
column: 0,
value: 'Test',
table,
},
{
row: 1,
column: 1,
value: 'Two',
table,
},
],
},
data: [
{
row: 1,
column: 0,
value: 'Test',
table,
},
{
row: 1,
column: 1,
value: 'Two',
table,
},
],
});
});
});

View file

@ -6,8 +6,8 @@
import { Datum, LayerValue } from '@elastic/charts';
import { KibanaDatatable, KibanaDatatableColumn } from 'src/plugins/expressions/public';
import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public';
import { ColumnGroups } from './types';
import { LensFilterEvent } from '../types';
export function getSliceValueWithFallback(
d: Datum,
@ -28,7 +28,7 @@ export function getFilterContext(
clickedLayers: LayerValue[],
layerColumnIds: string[],
table: KibanaDatatable
): ValueClickTriggerContext {
): LensFilterEvent['data'] {
const matchingIndex = table.rows.findIndex(row =>
clickedLayers.every((layer, index) => {
const columnId = layerColumnIds[index];
@ -37,13 +37,11 @@ export function getFilterContext(
);
return {
data: {
data: clickedLayers.map((clickedLayer, index) => ({
column: table.columns.findIndex(col => col.id === layerColumnIds[index]),
row: matchingIndex,
value: clickedLayer.groupByRollup,
table,
})),
},
data: clickedLayers.map((clickedLayer, index) => ({
column: table.columns.findIndex(col => col.id === layerColumnIds[index]),
row: matchingIndex,
value: clickedLayer.groupByRollup,
table,
})),
};
}

View file

@ -103,9 +103,6 @@ export class LensPlugin {
start(core: CoreStart, startDependencies: LensPluginStartDependencies) {
this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance;
this.xyVisualization.start(core, startDependencies);
this.datatableVisualization.start(core, startDependencies);
this.pieVisualization.start(core, startDependencies);
}
stop() {

View file

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { createGetterSetter } from '../../../../src/plugins/kibana_utils/public';
import { UiActionsStart } from '../../../../src/plugins/ui_actions/public';
export const [getExecuteTriggerActions, setExecuteTriggerActions] = createGetterSetter<
UiActionsStart['executeTriggerActions']
>('executeTriggerActions');

View file

@ -7,11 +7,21 @@
import { Ast } from '@kbn/interpreter/common';
import { IconType } from '@elastic/eui/src/components/icon/icon';
import { CoreSetup } from 'kibana/public';
import { KibanaDatatable, SerializedFieldFormat } from '../../../../src/plugins/expressions/public';
import {
ExpressionRendererEvent,
IInterpreterRenderHandlers,
KibanaDatatable,
SerializedFieldFormat,
} from '../../../../src/plugins/expressions/public';
import { DragContextState } from './drag_drop';
import { Document } from './persistence';
import { DateRange } from '../common';
import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public';
import {
SELECT_RANGE_TRIGGER,
TriggerContext,
VALUE_CLICK_TRIGGER,
} from '../../../../src/plugins/ui_actions/public';
export type ErrorCallback = (e: { message: string }) => void;
@ -467,3 +477,29 @@ export interface Visualization<T = unknown, P = unknown> {
*/
toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null;
}
export interface LensFilterEvent {
name: 'filter';
data: TriggerContext<typeof VALUE_CLICK_TRIGGER>['data'];
}
export interface LensBrushEvent {
name: 'brush';
data: TriggerContext<typeof SELECT_RANGE_TRIGGER>['data'];
}
export function isLensFilterEvent(event: ExpressionRendererEvent): event is LensFilterEvent {
return event.name === 'filter';
}
export function isLensBrushEvent(event: ExpressionRendererEvent): event is LensBrushEvent {
return event.name === 'brush';
}
/**
* Expression renderer handlers specifically for lens renderers. This is a narrowed down
* version of the general render handlers, specifying supported event types. If this type is
* used, dispatched events will be handled correctly.
*/
export interface ILensInterpreterRenderHandlers extends IInterpreterRenderHandlers {
event: (event: LensFilterEvent | LensBrushEvent) => void;
}

View file

@ -5,15 +5,13 @@
*/
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { CoreSetup, IUiSettingsClient, CoreStart } from 'kibana/public';
import { CoreSetup, IUiSettingsClient } from 'kibana/public';
import moment from 'moment-timezone';
import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public';
import { xyVisualization } from './xy_visualization';
import { xyChart, getXyChartRenderer } from './xy_expression';
import { legendConfig, xConfig, layerConfig } from './types';
import { EditorFrameSetup, FormatFactory } from '../types';
import { setExecuteTriggerActions } from '../services';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
export interface XyVisualizationPluginSetupPlugins {
expressions: ExpressionsSetup;
@ -21,10 +19,6 @@ export interface XyVisualizationPluginSetupPlugins {
editorFrame: EditorFrameSetup;
}
interface XyVisualizationPluginStartPlugins {
uiActions: UiActionsStart;
}
function getTimeZone(uiSettings: IUiSettingsClient) {
const configuredTimeZone = uiSettings.get('dateFormat:tz');
if (configuredTimeZone === 'Browser') {
@ -59,7 +53,4 @@ export class XyVisualization {
editorFrame.registerVisualization(xyVisualization);
}
start(core: CoreStart, { uiActions }: XyVisualizationPluginStartPlugins) {
setExecuteTriggerActions(uiActions.executeTriggerActions);
}
}

View file

@ -25,7 +25,8 @@ import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerArgs } from './ty
import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
const executeTriggerActions = jest.fn();
const onClickValue = jest.fn();
const onSelectRange = jest.fn();
const dateHistogramData: LensMultiTable = {
type: 'lens_multitable',
@ -296,7 +297,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -344,7 +346,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(`
@ -379,7 +382,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -415,7 +419,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -452,7 +457,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -496,7 +502,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -530,7 +537,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(Settings).prop('xDomain')).toBeUndefined();
@ -546,7 +554,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -563,7 +572,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -580,7 +590,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -602,7 +613,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -611,12 +623,10 @@ describe('xy_expression', () => {
.first()
.prop('onBrushEnd')!({ x: [1585757732783, 1585758880838] });
expect(executeTriggerActions).toHaveBeenCalledWith('SELECT_RANGE_TRIGGER', {
data: {
column: 0,
table: dateHistogramData.tables.timeLayer,
range: [1585757732783, 1585758880838],
},
expect(onSelectRange).toHaveBeenCalledWith({
column: 0,
table: dateHistogramData.tables.timeLayer,
range: [1585757732783, 1585758880838],
timeFieldName: 'order_date',
});
});
@ -656,7 +666,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -665,23 +676,21 @@ describe('xy_expression', () => {
.first()
.prop('onElementClick')!([[geometry, series as XYChartSeriesIdentifier]]);
expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', {
data: {
data: [
{
column: 1,
row: 1,
table: data.tables.first,
value: 5,
},
{
column: 1,
row: 0,
table: data.tables.first,
value: 2,
},
],
},
expect(onClickValue).toHaveBeenCalledWith({
data: [
{
column: 1,
row: 1,
table: data.tables.first,
value: 5,
},
{
column: 1,
row: 0,
table: data.tables.first,
value: 2,
},
],
});
});
@ -695,7 +704,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -713,7 +723,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -734,7 +745,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -753,7 +765,8 @@ describe('xy_expression', () => {
timeZone="CEST"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(LineSeries).prop('timeZone')).toEqual('CEST');
@ -771,7 +784,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true);
@ -796,7 +810,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true);
@ -815,7 +830,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(false);
@ -876,7 +892,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
};
@ -1071,7 +1088,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Ordinal);
@ -1088,7 +1106,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(LineSeries).prop('yScaleType')).toEqual(ScaleType.Sqrt);
@ -1105,7 +1124,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -1123,7 +1143,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -1141,7 +1162,8 @@ describe('xy_expression', () => {
chartTheme={{}}
histogramBarTarget={50}
timeZone="UTC"
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(getFormatSpy).toHaveBeenCalledWith({
@ -1161,7 +1183,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -1248,7 +1271,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -1302,7 +1326,8 @@ describe('xy_expression', () => {
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);

View file

@ -21,24 +21,22 @@ import {
} from '@elastic/charts';
import { I18nProvider } from '@kbn/i18n/react';
import {
IInterpreterRenderHandlers,
ExpressionRenderDefinition,
ExpressionFunctionDefinition,
ExpressionRenderDefinition,
ExpressionValueSearchContext,
} from 'src/plugins/expressions/public';
import { IconType } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
ValueClickTriggerContext,
RangeSelectTriggerContext,
} from '../../../../../src/plugins/embeddable/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
import { LensMultiTable, FormatFactory } from '../types';
LensMultiTable,
FormatFactory,
ILensInterpreterRenderHandlers,
LensFilterEvent,
LensBrushEvent,
} from '../types';
import { XYArgs, SeriesType, visualizationTypes } from './types';
import { VisualizationContainer } from '../visualization_container';
import { isHorizontalChart } from './state_helpers';
import { getExecuteTriggerActions } from '../services';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
import { parseInterval } from '../../../../../src/plugins/data/common';
import { EmptyPlaceholder } from '../shared_components';
@ -63,7 +61,8 @@ type XYChartRenderProps = XYChartProps & {
formatFactory: FormatFactory;
timeZone: string;
histogramBarTarget: number;
executeTriggerActions: UiActionsStart['executeTriggerActions'];
onClickValue: (data: LensFilterEvent['data']) => void;
onSelectRange: (data: LensBrushEvent['data']) => void;
};
export const xyChart: ExpressionFunctionDefinition<
@ -125,9 +124,18 @@ export const getXyChartRenderer = (dependencies: {
}),
validate: () => undefined,
reuseDomNode: true,
render: async (domNode: Element, config: XYChartProps, handlers: IInterpreterRenderHandlers) => {
const executeTriggerActions = getExecuteTriggerActions();
render: async (
domNode: Element,
config: XYChartProps,
handlers: ILensInterpreterRenderHandlers
) => {
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
const onClickValue = (data: LensFilterEvent['data']) => {
handlers.event({ name: 'filter', data });
};
const onSelectRange = (data: LensBrushEvent['data']) => {
handlers.event({ name: 'brush', data });
};
const formatFactory = await dependencies.formatFactory;
ReactDOM.render(
<I18nProvider>
@ -137,7 +145,8 @@ export const getXyChartRenderer = (dependencies: {
chartTheme={dependencies.chartTheme}
timeZone={dependencies.timeZone}
histogramBarTarget={dependencies.histogramBarTarget}
executeTriggerActions={executeTriggerActions}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
</I18nProvider>,
domNode,
@ -177,7 +186,8 @@ export function XYChart({
timeZone,
chartTheme,
histogramBarTarget,
executeTriggerActions,
onClickValue,
onSelectRange,
}: XYChartRenderProps) {
const { legend, layers } = args;
@ -287,15 +297,13 @@ export function XYChart({
);
const timeFieldName = table.columns[xAxisColumnIndex]?.meta?.aggConfigParams?.field;
const context: RangeSelectTriggerContext = {
data: {
range: [min, max],
table,
column: xAxisColumnIndex,
},
const context: LensBrushEvent['data'] = {
range: [min, max],
table,
column: xAxisColumnIndex,
timeFieldName,
};
executeTriggerActions(VIS_EVENT_TO_TRIGGER.brush, context);
onSelectRange(context);
}}
onElementClick={([[geometry, series]]) => {
// for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue
@ -337,18 +345,16 @@ export function XYChart({
?.aggConfigParams?.field;
const timeFieldName = xDomain && xAxisFieldName;
const context: ValueClickTriggerContext = {
data: {
data: points.map(point => ({
row: point.row,
column: point.column,
value: point.value,
table,
})),
},
const context: LensFilterEvent['data'] = {
data: points.map(point => ({
row: point.row,
column: point.column,
value: point.value,
table,
})),
timeFieldName,
};
executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context);
onClickValue(context);
}}
/>