[Lens] Expose active data in some places (#79851)

This commit is contained in:
Joe Reuter 2020-11-06 17:05:34 +01:00 committed by GitHub
parent 5710f6763b
commit 651345b18b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 212 additions and 34 deletions

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element
ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, onData$, reload$, debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element
```

View file

@ -18,6 +18,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams
| [dataAttrs](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.dataattrs.md) | <code>string[]</code> | |
| [debounce](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md) | <code>number</code> | |
| [expression](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.expression.md) | <code>string &#124; ExpressionAstExpression</code> | |
| [onData$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.ondata_.md) | <code>&lt;TData, TInspectorAdapters&gt;(data: TData, adapters?: TInspectorAdapters) =&gt; void</code> | |
| [onEvent](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.onevent.md) | <code>(event: ExpressionRendererEvent) =&gt; void</code> | |
| [padding](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.padding.md) | <code>'xs' &#124; 's' &#124; 'm' &#124; 'l' &#124; 'xl'</code> | |
| [reload$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.reload_.md) | <code>Observable&lt;unknown&gt;</code> | An observable which can be used to re-run the expression without destroying the component |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [ReactExpressionRendererProps](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md) &gt; [onData$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.ondata_.md)
## ReactExpressionRendererProps.onData$ property
<b>Signature:</b>
```typescript
onData$?: <TData, TInspectorAdapters>(data: TData, adapters?: TInspectorAdapters) => void;
```

View file

@ -1057,7 +1057,7 @@ export interface Range {
// Warning: (ae-missing-release-tag) "ReactExpressionRenderer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element;
export const ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, onData$, reload$, debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element;
// Warning: (ae-missing-release-tag) "ReactExpressionRendererProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@ -1072,6 +1072,8 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams {
// (undocumented)
expression: string | ExpressionAstExpression;
// (undocumented)
onData$?: <TData, TInspectorAdapters>(data: TData, adapters?: TInspectorAdapters) => void;
// (undocumented)
onEvent?: (event: ExpressionRendererEvent) => void;
// (undocumented)
padding?: 'xs' | 's' | 'm' | 'l' | 'xl';

View file

@ -195,6 +195,38 @@ describe('ExpressionRenderer', () => {
expect(instance.find('[data-test-subj="custom-error"]')).toHaveLength(0);
});
it('should call onData$ prop on every data$ observable emission in loader', () => {
const dataSubject = new Subject();
const data$ = dataSubject.asObservable().pipe(share());
const newData = {};
const inspectData = {};
const onData$ = jest.fn();
(ExpressionLoader as jest.Mock).mockImplementation(() => {
return {
render$: new Subject(),
data$,
loading$: new Subject(),
events$: new Subject(),
update: jest.fn(),
inspect: jest.fn(() => inspectData),
};
});
mount(<ReactExpressionRenderer expression="" onData$={onData$} />);
expect(onData$).toHaveBeenCalledTimes(0);
act(() => {
dataSubject.next(newData);
});
expect(onData$).toHaveBeenCalledTimes(1);
expect(onData$.mock.calls[0][0]).toBe(newData);
expect(onData$.mock.calls[0][1]).toBe(inspectData);
});
it('should fire onEvent prop on every events$ observable emission in loader', () => {
const dataSubject = new Subject();
const data$ = dataSubject.asObservable().pipe(share());

View file

@ -41,6 +41,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams {
) => React.ReactElement | React.ReactElement[];
padding?: 'xs' | 's' | 'm' | 'l' | 'xl';
onEvent?: (event: ExpressionRendererEvent) => void;
onData$?: <TData, TInspectorAdapters>(data: TData, adapters?: TInspectorAdapters) => void;
/**
* An observable which can be used to re-run the expression without destroying the component
*/
@ -71,6 +72,7 @@ export const ReactExpressionRenderer = ({
renderError,
expression,
onEvent,
onData$,
reload$,
debounce,
...expressionLoaderOptions
@ -135,6 +137,13 @@ export const ReactExpressionRenderer = ({
})
);
}
if (onData$) {
subs.push(
expressionLoaderRef.current.data$.subscribe((newData) => {
onData$(newData, expressionLoaderRef.current?.inspect());
})
);
}
subs.push(
expressionLoaderRef.current.loading$.subscribe(() => {
hasHandledErrorRef.current = false;

View file

@ -93,6 +93,7 @@ export function LayerPanel(
state: props.visualizationState,
frame: props.framePublicAPI,
dateRange: props.framePublicAPI.dateRange,
activeData: props.framePublicAPI.activeData,
};
const datasourceId = datasourcePublicAPI.datasourceId;
const layerDatasourceState = props.datasourceStates[datasourceId].state;
@ -111,6 +112,7 @@ export function LayerPanel(
...layerDatasourceDropProps,
frame: props.framePublicAPI,
dateRange: props.framePublicAPI.dateRange,
activeData: props.framePublicAPI.activeData,
};
const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps);
@ -140,6 +142,7 @@ export function LayerPanel(
nativeProps={{
layerId,
state: layerDatasourceState,
activeData: props.framePublicAPI.activeData,
setState: (updater: unknown) => {
const newState =
typeof updater === 'function' ? updater(layerDatasourceState) : updater;

View file

@ -101,6 +101,7 @@ export function EditorFrame(props: EditorFrameProps) {
const framePublicAPI: FramePublicAPI = {
datasourceLayers,
activeData: state.activeData,
dateRange: props.dateRange,
query: props.query,
filters: props.filters,

View file

@ -6,6 +6,7 @@
import { EditorFrameProps } from './index';
import { Document } from '../../persistence/saved_object_store';
import { TableInspectorAdapter } from '../types';
export interface PreviewState {
visualization: {
@ -21,6 +22,7 @@ export interface EditorFrameState extends PreviewState {
description?: string;
stagedPreview?: PreviewState;
activeDatasourceId: string | null;
activeData?: TableInspectorAdapter;
}
export type Action =
@ -32,6 +34,10 @@ export type Action =
type: 'UPDATE_TITLE';
title: string;
}
| {
type: 'UPDATE_ACTIVE_DATA';
tables: TableInspectorAdapter;
}
| {
type: 'UPDATE_STATE';
// Just for diagnostics, so we can determine what action
@ -139,6 +145,11 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
return { ...state, title: action.title };
case 'UPDATE_STATE':
return action.updater(state);
case 'UPDATE_ACTIVE_DATA':
return {
...state,
activeData: action.tables,
};
case 'UPDATE_LAYER':
return {
...state,

View file

@ -7,6 +7,7 @@
import _ from 'lodash';
import { Ast } from '@kbn/interpreter/common';
import { IconType } from '@elastic/eui/src/components/icon/icon';
import { Datatable } from 'src/plugins/expressions';
import { PaletteOutput } from 'src/plugins/charts/public';
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
import {
@ -50,6 +51,7 @@ export function getSuggestions({
visualizationState,
field,
visualizeTriggerFieldContext,
activeData,
mainPalette,
}: {
datasourceMap: Record<string, Datasource>;
@ -66,6 +68,7 @@ export function getSuggestions({
visualizationState: unknown;
field?: unknown;
visualizeTriggerFieldContext?: VisualizeFieldContext;
activeData?: Record<string, Datatable>;
mainPalette?: PaletteOutput;
}): Suggestion[] {
const datasources = Object.entries(datasourceMap).filter(
@ -87,7 +90,8 @@ export function getSuggestions({
dataSourceSuggestions = datasource.getDatasourceSuggestionsForField(datasourceState, field);
} else {
dataSourceSuggestions = datasource.getDatasourceSuggestionsFromCurrentState(
datasourceState
datasourceState,
activeData
);
}
return dataSourceSuggestions.map((suggestion) => ({ ...suggestion, datasourceId }));

View file

@ -188,6 +188,7 @@ export function SuggestionPanel({
visualizationMap,
activeVisualizationId: currentVisualizationId,
visualizationState: currentVisualizationState,
activeData: frame.activeData,
})
.filter((suggestion) => !suggestion.hide)
.filter(

View file

@ -325,6 +325,7 @@ function getTopSuggestion(
activeVisualizationId: props.visualizationId,
visualizationState: props.visualizationState,
subVisualizationId,
activeData: props.framePublicAPI.activeData,
mainPalette,
});
const suggestions = unfilteredSuggestions.filter((suggestion) => {

View file

@ -253,6 +253,48 @@ describe('workspace_panel', () => {
expect(trigger.exec).toHaveBeenCalledWith({ data: eventData });
});
it('should push add current data table to state on data$ emitting value', () => {
const framePublicAPI = createMockFramePublicAPI();
framePublicAPI.datasourceLayers = {
first: mockDatasource.publicAPIMock,
};
mockDatasource.toExpression.mockReturnValue('datasource');
mockDatasource.getLayers.mockReturnValue(['first']);
const dispatch = jest.fn();
instance = mount(
<WorkspacePanel
activeDatasourceId={'mock'}
datasourceStates={{
mock: {
state: {},
isLoading: false,
},
}}
datasourceMap={{
mock: mockDatasource,
}}
framePublicAPI={framePublicAPI}
activeVisualizationId="vis"
visualizationMap={{
vis: { ...mockVisualization, toExpression: () => 'vis' },
}}
visualizationState={{}}
dispatch={dispatch}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
const onData = expressionRendererMock.mock.calls[0][0].onData$!;
const tableData = { table1: { columns: [], rows: [] } };
onData(undefined, { tables: tableData });
expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_ACTIVE_DATA', tables: tableData });
});
it('should include data fetching for each layer in the expression', () => {
const mockDatasource2 = createMockDatasource('a');
const framePublicAPI = createMockFramePublicAPI();

View file

@ -50,6 +50,7 @@ import {
} from '../../../../../../../src/plugins/data/public';
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
import { DropIllustration } from '../../../assets/drop_illustration';
import { LensInspectorAdapters } from '../../types';
import { getOriginalRequestErrorMessage } from '../../error_helper';
import { validateDatasourceAndVisualization } from '../state_helpers';
@ -296,6 +297,7 @@ export function WorkspacePanel({
expression={expression}
framePublicAPI={framePublicAPI}
timefilter={plugins.data.query.timefilter.timefilter}
dispatch={dispatch}
onEvent={onEvent}
setLocalState={setLocalState}
localState={{ ...localState, configurationValidationError }}
@ -339,11 +341,13 @@ export const InnerVisualizationWrapper = ({
setLocalState,
localState,
ExpressionRendererComponent,
dispatch,
}: {
expression: Ast | null | undefined;
framePublicAPI: FramePublicAPI;
timefilter: TimefilterContract;
onEvent: (event: ExpressionRendererEvent) => void;
dispatch: (action: Action) => void;
setLocalState: (dispatch: (prevState: WorkspaceState) => WorkspaceState) => void;
localState: WorkspaceState & {
configurationValidationError?: Array<{ shortMessage: string; longMessage: string }>;
@ -369,6 +373,18 @@ export const InnerVisualizationWrapper = ({
]
);
const onData$ = useCallback(
(data: unknown, inspectorAdapters?: LensInspectorAdapters) => {
if (inspectorAdapters && inspectorAdapters.tables) {
dispatch({
type: 'UPDATE_ACTIVE_DATA',
tables: inspectorAdapters.tables,
});
}
},
[dispatch]
);
if (localState.configurationValidationError) {
let showExtraErrors = null;
if (localState.configurationValidationError.length > 1) {
@ -455,6 +471,7 @@ export const InnerVisualizationWrapper = ({
searchContext={context}
reload$={autoRefreshFetch$}
onEvent={onEvent}
onData$={onData$}
renderError={(errorMessage?: string | null, error?: ExpressionRenderError | null) => {
const visibleErrorMessage = getOriginalRequestErrorMessage(error) || errorMessage;

View file

@ -6,34 +6,35 @@
import moment from 'moment';
import { mergeTables } from './merge_tables';
import { Datatable } from 'src/plugins/expressions';
import { Datatable, ExecutionContext } from 'src/plugins/expressions';
import { LensInspectorAdapters } from './types';
describe('lens_merge_tables', () => {
const sampleTable1: Datatable = {
type: 'datatable',
columns: [
{ id: 'bucket', name: 'A', meta: { type: 'string' } },
{ id: 'count', name: 'Count', meta: { type: 'number' } },
],
rows: [
{ bucket: 'a', count: 5 },
{ bucket: 'b', count: 10 },
],
};
const sampleTable2: Datatable = {
type: 'datatable',
columns: [
{ id: 'bucket', name: 'C', meta: { type: 'string' } },
{ id: 'avg', name: 'Average', meta: { type: 'number' } },
],
rows: [
{ bucket: 'a', avg: 2.5 },
{ bucket: 'b', avg: 9 },
],
};
it('should produce a row with the nested table as defined', () => {
const sampleTable1: Datatable = {
type: 'datatable',
columns: [
{ id: 'bucket', name: 'A', meta: { type: 'string' } },
{ id: 'count', name: 'Count', meta: { type: 'number' } },
],
rows: [
{ bucket: 'a', count: 5 },
{ bucket: 'b', count: 10 },
],
};
const sampleTable2: Datatable = {
type: 'datatable',
columns: [
{ id: 'bucket', name: 'C', meta: { type: 'string' } },
{ id: 'avg', name: 'Average', meta: { type: 'number' } },
],
rows: [
{ bucket: 'a', avg: 2.5 },
{ bucket: 'b', avg: 9 },
],
};
expect(
mergeTables.fn(
null,
@ -47,6 +48,15 @@ describe('lens_merge_tables', () => {
});
});
it('should store the current tables in the tables inspector', () => {
const adapters: LensInspectorAdapters = { tables: {} };
mergeTables.fn(null, { layerIds: ['first', 'second'], tables: [sampleTable1, sampleTable2] }, {
inspectorAdapters: adapters,
} as ExecutionContext<LensInspectorAdapters>);
expect(adapters.tables!.first).toBe(sampleTable1);
expect(adapters.tables!.second).toBe(sampleTable2);
});
it('should pass the date range along', () => {
expect(
mergeTables.fn(

View file

@ -6,6 +6,7 @@
import { i18n } from '@kbn/i18n';
import {
ExecutionContext,
Datatable,
ExpressionFunctionDefinition,
ExpressionValueSearchContext,
@ -14,6 +15,7 @@ import { search } from '../../../../../src/plugins/data/public';
const { toAbsoluteDates } = search.aggs;
import { LensMultiTable } from '../types';
import { LensInspectorAdapters } from './types';
interface MergeTables {
layerIds: string[];
@ -24,12 +26,14 @@ export const mergeTables: ExpressionFunctionDefinition<
'lens_merge_tables',
ExpressionValueSearchContext | null,
MergeTables,
LensMultiTable
LensMultiTable,
ExecutionContext<LensInspectorAdapters>
> = {
name: 'lens_merge_tables',
type: 'lens_multitable',
help: i18n.translate('xpack.lens.functions.mergeTables.help', {
defaultMessage: 'A helper to merge any number of kibana tables into a single table',
defaultMessage:
'A helper to merge any number of kibana tables into a single table and expose it via inspector adapter',
}),
args: {
layerIds: {
@ -44,10 +48,18 @@ export const mergeTables: ExpressionFunctionDefinition<
},
},
inputTypes: ['kibana_context', 'null'],
fn(input, { layerIds, tables }) {
fn(input, { layerIds, tables }, context) {
if (!context.inspectorAdapters) {
context.inspectorAdapters = {};
}
if (!context.inspectorAdapters.tables) {
context.inspectorAdapters.tables = {};
}
const resultTables: Record<string, Datatable> = {};
tables.forEach((table, index) => {
resultTables[layerIds[index]] = table;
// adapter is always defined at that point because we make sure by the beginning of the function
context.inspectorAdapters.tables![layerIds[index]] = table;
});
return {
type: 'lens_multitable',

View file

@ -0,0 +1,12 @@
/*
* 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 { Datatable } from 'src/plugins/expressions';
export type TableInspectorAdapter = Record<string, Datatable>;
export interface LensInspectorAdapters {
tables?: TableInspectorAdapter;
}

View file

@ -178,7 +178,10 @@ export interface Datasource<T = unknown, P = unknown> {
indexPatternId: string,
fieldName: string
) => Array<DatasourceSuggestion<T>>;
getDatasourceSuggestionsFromCurrentState: (state: T) => Array<DatasourceSuggestion<T>>;
getDatasourceSuggestionsFromCurrentState: (
state: T,
activeData?: Record<string, Datatable>
) => Array<DatasourceSuggestion<T>>;
getPublicAPI: (props: PublicAPIProps<T>) => DatasourcePublicAPI;
getErrorMessages: (state: T) => Array<{ shortMessage: string; longMessage: string }> | undefined;
@ -231,6 +234,7 @@ export type DatasourceDimensionProps<T> = SharedDimensionProps & {
columnId: string;
onRemove?: (accessor: string) => void;
state: T;
activeData?: Record<string, Datatable>;
};
// The only way a visualization has to restrict the query building
@ -249,6 +253,7 @@ export interface DatasourceLayerPanelProps<T> {
layerId: string;
state: T;
setState: StateSetter<T>;
activeData?: Record<string, Datatable>;
}
export interface DraggedOperation {
@ -428,6 +433,12 @@ export interface VisualizationSuggestion<T = unknown> {
export interface FramePublicAPI {
datasourceLayers: Record<string, DatasourcePublicAPI>;
/**
* Data of the chart currently rendered in the preview.
* This data might be not available (e.g. if the chart can't be rendered) or outdated and belonging to another chart.
* If accessing, make sure to check whether expected columns actually exist.
*/
activeData?: Record<string, Datatable>;
dateRange: DateRange;
query: Query;

View file

@ -10744,7 +10744,6 @@
"xpack.lens.fittingFunctionsTitle.lookahead": "次へ",
"xpack.lens.fittingFunctionsTitle.none": "非表示",
"xpack.lens.fittingFunctionsTitle.zero": "ゼロ",
"xpack.lens.functions.mergeTables.help": "いくつかの Kibana 表を 1 つの表に結合するのをアシストします",
"xpack.lens.functions.renameColumns.help": "データベースの列の名前の変更をアシストします",
"xpack.lens.functions.renameColumns.idMap.help": "キーが古い列 ID で値が対応する新しい列 ID となるように JSON エンコーディングされたオブジェクトです。他の列 ID はすべてのそのままです。",
"xpack.lens.includeValueButtonAriaLabel": "{value}を含める",

View file

@ -10757,7 +10757,6 @@
"xpack.lens.fittingFunctionsTitle.lookahead": "下一",
"xpack.lens.fittingFunctionsTitle.none": "隐藏",
"xpack.lens.fittingFunctionsTitle.zero": "零",
"xpack.lens.functions.mergeTables.help": "将任何数目的 kibana 表合并成单个表的助手",
"xpack.lens.functions.renameColumns.help": "用于重命名数据表列的助手",
"xpack.lens.functions.renameColumns.idMap.help": "旧列 ID 为键且相应新列 ID 为值的 JSON 编码对象。所有其他列 ID 都将保留。",
"xpack.lens.includeValueButtonAriaLabel": "包括 {value}",