mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Lens] Expose events from lens for lens embeddable (#94670)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c6b37dec70
commit
9d8a2f183e
3 changed files with 317 additions and 57 deletions
|
@ -111,7 +111,13 @@ export const App = (props: {
|
|||
defaultIndexPattern: IndexPattern | null;
|
||||
}) => {
|
||||
const [color, setColor] = useState('green');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const LensComponent = props.plugins.lens.EmbeddableComponent;
|
||||
|
||||
const [time, setTime] = useState({
|
||||
from: 'now-5d',
|
||||
to: 'now',
|
||||
});
|
||||
return (
|
||||
<EuiPage>
|
||||
<EuiPageBody style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
|
@ -138,6 +144,7 @@ export const App = (props: {
|
|||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16);
|
||||
|
@ -153,10 +160,7 @@ export const App = (props: {
|
|||
onClick={() => {
|
||||
props.plugins.lens.navigateToPrefilledEditor({
|
||||
id: '',
|
||||
timeRange: {
|
||||
from: 'now-5d',
|
||||
to: 'now',
|
||||
},
|
||||
timeRange: time,
|
||||
attributes: getLensAttributes(props.defaultIndexPattern!, color),
|
||||
});
|
||||
// eslint-disable-next-line no-bitwise
|
||||
|
@ -171,11 +175,23 @@ export const App = (props: {
|
|||
<LensComponent
|
||||
id=""
|
||||
style={{ height: 500 }}
|
||||
timeRange={{
|
||||
from: 'now-5d',
|
||||
to: 'now',
|
||||
}}
|
||||
timeRange={time}
|
||||
attributes={getLensAttributes(props.defaultIndexPattern, color)}
|
||||
onLoad={(val) => {
|
||||
setIsLoading(val);
|
||||
}}
|
||||
onBrushEnd={({ range }) => {
|
||||
setTime({
|
||||
from: new Date(range[0]).toISOString(),
|
||||
to: new Date(range[1]).toISOString(),
|
||||
});
|
||||
}}
|
||||
onFilter={(_data) => {
|
||||
// call back event for on filter event
|
||||
}}
|
||||
onTableRowClick={(_data) => {
|
||||
// call back event for on table row click event
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
@ -679,4 +679,205 @@ describe('embeddable', () => {
|
|||
|
||||
expect(expressionRenderer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onload after rerender and onData$ call ', async () => {
|
||||
const onLoad = jest.fn();
|
||||
|
||||
expressionRenderer = jest.fn(({ onData$ }) => {
|
||||
setTimeout(() => {
|
||||
onData$?.({});
|
||||
}, 10);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const embeddable = new Embeddable(
|
||||
{
|
||||
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
|
||||
attributeService,
|
||||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
ast: {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{ type: 'function', function: 'my', arguments: {} },
|
||||
{ type: 'function', function: 'expression', arguments: {} },
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
}),
|
||||
},
|
||||
({ id: '123', onLoad } as unknown) as LensEmbeddableInput
|
||||
);
|
||||
|
||||
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
|
||||
embeddable.render(mountpoint);
|
||||
|
||||
expect(onLoad).toHaveBeenCalledWith(true);
|
||||
expect(onLoad).toHaveBeenCalledTimes(1);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
// loading should become false
|
||||
expect(onLoad).toHaveBeenCalledTimes(2);
|
||||
expect(onLoad).toHaveBeenNthCalledWith(2, false);
|
||||
|
||||
expect(expressionRenderer).toHaveBeenCalledTimes(1);
|
||||
|
||||
embeddable.updateInput({
|
||||
searchSessionId: 'newSession',
|
||||
});
|
||||
embeddable.reload();
|
||||
|
||||
// loading should become again true
|
||||
expect(onLoad).toHaveBeenCalledTimes(3);
|
||||
expect(onLoad).toHaveBeenNthCalledWith(3, true);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(expressionRenderer).toHaveBeenCalledTimes(2);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
// loading should again become false
|
||||
expect(onLoad).toHaveBeenCalledTimes(4);
|
||||
expect(onLoad).toHaveBeenNthCalledWith(4, false);
|
||||
});
|
||||
|
||||
it('should call onFilter event on filter call ', async () => {
|
||||
const onFilter = jest.fn();
|
||||
|
||||
expressionRenderer = jest.fn(({ onEvent }) => {
|
||||
setTimeout(() => {
|
||||
onEvent?.({ name: 'filter', data: { pings: false } });
|
||||
}, 10);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const embeddable = new Embeddable(
|
||||
{
|
||||
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
|
||||
attributeService,
|
||||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
ast: {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{ type: 'function', function: 'my', arguments: {} },
|
||||
{ type: 'function', function: 'expression', arguments: {} },
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
}),
|
||||
},
|
||||
({ id: '123', onFilter } as unknown) as LensEmbeddableInput
|
||||
);
|
||||
|
||||
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
|
||||
embeddable.render(mountpoint);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
expect(onFilter).toHaveBeenCalledWith({ pings: false });
|
||||
expect(onFilter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onBrush event on brushing', async () => {
|
||||
const onBrushEnd = jest.fn();
|
||||
|
||||
expressionRenderer = jest.fn(({ onEvent }) => {
|
||||
setTimeout(() => {
|
||||
onEvent?.({ name: 'brush', data: { range: [0, 1] } });
|
||||
}, 10);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const embeddable = new Embeddable(
|
||||
{
|
||||
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
|
||||
attributeService,
|
||||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
ast: {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{ type: 'function', function: 'my', arguments: {} },
|
||||
{ type: 'function', function: 'expression', arguments: {} },
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
}),
|
||||
},
|
||||
({ id: '123', onBrushEnd } as unknown) as LensEmbeddableInput
|
||||
);
|
||||
|
||||
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
|
||||
embeddable.render(mountpoint);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
expect(onBrushEnd).toHaveBeenCalledWith({ range: [0, 1] });
|
||||
expect(onBrushEnd).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onTableRowClick event ', async () => {
|
||||
const onTableRowClick = jest.fn();
|
||||
|
||||
expressionRenderer = jest.fn(({ onEvent }) => {
|
||||
setTimeout(() => {
|
||||
onEvent?.({ name: 'tableRowContextMenuClick', data: { name: 'test' } });
|
||||
}, 10);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const embeddable = new Embeddable(
|
||||
{
|
||||
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
|
||||
attributeService,
|
||||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
ast: {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{ type: 'function', function: 'my', arguments: {} },
|
||||
{ type: 'function', function: 'expression', arguments: {} },
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
}),
|
||||
},
|
||||
({ id: '123', onTableRowClick } as unknown) as LensEmbeddableInput
|
||||
);
|
||||
|
||||
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
|
||||
embeddable.render(mountpoint);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
expect(onTableRowClick).toHaveBeenCalledWith({ name: 'test' });
|
||||
expect(onTableRowClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,6 +45,9 @@ import {
|
|||
isLensBrushEvent,
|
||||
isLensFilterEvent,
|
||||
isLensTableRowContextMenuClickEvent,
|
||||
LensBrushEvent,
|
||||
LensFilterEvent,
|
||||
LensTableRowContextMenuEvent,
|
||||
} from '../../types';
|
||||
|
||||
import { IndexPatternsContract } from '../../../../../../src/plugins/data/public';
|
||||
|
@ -63,6 +66,10 @@ interface LensBaseEmbeddableInput extends EmbeddableInput {
|
|||
renderMode?: RenderMode;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
onBrushEnd?: (data: LensBrushEvent['data']) => void;
|
||||
onLoad?: (isLoading: boolean) => void;
|
||||
onFilter?: (data: LensFilterEvent['data']) => void;
|
||||
onTableRowClick?: (data: LensTableRowContextMenuEvent['data']) => void;
|
||||
}
|
||||
|
||||
export type LensByValueInput = {
|
||||
|
@ -103,6 +110,8 @@ export class Embeddable
|
|||
private isInitialized = false;
|
||||
private activeData: Partial<DefaultInspectorAdapters> | undefined;
|
||||
private errors: ErrorMessage[] | undefined;
|
||||
private inputReloadSubscriptions: Subscription[];
|
||||
private isDestroyed?: boolean;
|
||||
|
||||
private externalSearchContext: {
|
||||
timeRange?: TimeRange;
|
||||
|
@ -133,65 +142,76 @@ export class Embeddable
|
|||
|
||||
const input$ = this.getInput$();
|
||||
|
||||
this.inputReloadSubscriptions = [];
|
||||
|
||||
// Lens embeddable does not re-render when embeddable input changes in
|
||||
// general, to improve performance. This line makes sure the Lens embeddable
|
||||
// re-renders when anything in ".dynamicActions" (e.g. drilldowns) changes.
|
||||
input$
|
||||
.pipe(
|
||||
map((input) => input.enhancements?.dynamicActions),
|
||||
distinctUntilChanged((a, b) => isEqual(a, b)),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe((input) => {
|
||||
this.reload();
|
||||
});
|
||||
this.inputReloadSubscriptions.push(
|
||||
input$
|
||||
.pipe(
|
||||
map((input) => input.enhancements?.dynamicActions),
|
||||
distinctUntilChanged((a, b) => isEqual(a, b)),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe((input) => {
|
||||
this.reload();
|
||||
})
|
||||
);
|
||||
|
||||
// Lens embeddable does not re-render when embeddable input changes in
|
||||
// general, to improve performance. This line makes sure the Lens embeddable
|
||||
// re-renders when dashboard view mode switches between "view/edit". This is
|
||||
// needed to see the changes to ".dynamicActions" (e.g. drilldowns) when
|
||||
// dashboard's mode is toggled.
|
||||
input$
|
||||
.pipe(
|
||||
map((input) => input.viewMode),
|
||||
distinctUntilChanged(),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe((input) => {
|
||||
this.reload();
|
||||
});
|
||||
this.inputReloadSubscriptions.push(
|
||||
input$
|
||||
.pipe(
|
||||
map((input) => input.viewMode),
|
||||
distinctUntilChanged(),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe((input) => {
|
||||
this.reload();
|
||||
})
|
||||
);
|
||||
|
||||
// Re-initialize the visualization if either the attributes or the saved object id changes
|
||||
input$
|
||||
.pipe(
|
||||
distinctUntilChanged((a, b) =>
|
||||
isEqual(
|
||||
['attributes' in a && a.attributes, 'savedObjectId' in a && a.savedObjectId],
|
||||
['attributes' in b && b.attributes, 'savedObjectId' in b && b.savedObjectId]
|
||||
)
|
||||
),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe(async (input) => {
|
||||
await this.initializeSavedVis(input);
|
||||
this.reload();
|
||||
});
|
||||
|
||||
this.inputReloadSubscriptions.push(
|
||||
input$
|
||||
.pipe(
|
||||
distinctUntilChanged((a, b) =>
|
||||
isEqual(
|
||||
['attributes' in a && a.attributes, 'savedObjectId' in a && a.savedObjectId],
|
||||
['attributes' in b && b.attributes, 'savedObjectId' in b && b.savedObjectId]
|
||||
)
|
||||
),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe(async (input) => {
|
||||
await this.initializeSavedVis(input);
|
||||
this.reload();
|
||||
})
|
||||
);
|
||||
|
||||
// Update search context and reload on changes related to search
|
||||
this.getUpdated$()
|
||||
.pipe(map(() => this.getInput()))
|
||||
.pipe(
|
||||
distinctUntilChanged((a, b) =>
|
||||
isEqual(
|
||||
[a.filters, a.query, a.timeRange, a.searchSessionId],
|
||||
[b.filters, b.query, b.timeRange, b.searchSessionId]
|
||||
)
|
||||
),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe(async (input) => {
|
||||
this.onContainerStateChanged(input);
|
||||
});
|
||||
this.inputReloadSubscriptions.push(
|
||||
this.getUpdated$()
|
||||
.pipe(map(() => this.getInput()))
|
||||
.pipe(
|
||||
distinctUntilChanged((a, b) =>
|
||||
isEqual(
|
||||
[a.filters, a.query, a.timeRange, a.searchSessionId],
|
||||
[b.filters, b.query, b.timeRange, b.searchSessionId]
|
||||
)
|
||||
),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe(async (input) => {
|
||||
this.onContainerStateChanged(input);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public supportedTriggers() {
|
||||
|
@ -222,7 +242,7 @@ export class Embeddable
|
|||
this.onFatalError(e);
|
||||
return false;
|
||||
});
|
||||
if (!attributes) {
|
||||
if (!attributes || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
this.savedVis = {
|
||||
|
@ -268,6 +288,10 @@ export class Embeddable
|
|||
inspectorAdapters?: Partial<DefaultInspectorAdapters> | undefined
|
||||
) => {
|
||||
this.activeData = inspectorAdapters;
|
||||
if (this.input.onLoad) {
|
||||
// once onData$ is get's called from expression renderer, loading becomes false
|
||||
this.input.onLoad(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -277,9 +301,12 @@ export class Embeddable
|
|||
*/
|
||||
render(domNode: HTMLElement | Element) {
|
||||
this.domNode = domNode;
|
||||
if (!this.savedVis || !this.isInitialized) {
|
||||
if (!this.savedVis || !this.isInitialized || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
if (this.input.onLoad) {
|
||||
this.input.onLoad(true);
|
||||
}
|
||||
const input = this.getInput();
|
||||
render(
|
||||
<ExpressionWrapper
|
||||
|
@ -356,12 +383,19 @@ export class Embeddable
|
|||
data: event.data,
|
||||
embeddable: this,
|
||||
});
|
||||
|
||||
if (this.input.onBrushEnd) {
|
||||
this.input.onBrushEnd(event.data);
|
||||
}
|
||||
}
|
||||
if (isLensFilterEvent(event)) {
|
||||
this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({
|
||||
data: event.data,
|
||||
embeddable: this,
|
||||
});
|
||||
if (this.input.onFilter) {
|
||||
this.input.onFilter(event.data);
|
||||
}
|
||||
}
|
||||
|
||||
if (isLensTableRowContextMenuClickEvent(event)) {
|
||||
|
@ -372,11 +406,14 @@ export class Embeddable
|
|||
},
|
||||
true
|
||||
);
|
||||
if (this.input.onTableRowClick) {
|
||||
this.input.onTableRowClick((event.data as unknown) as LensTableRowContextMenuEvent['data']);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async reload() {
|
||||
if (!this.savedVis || !this.isInitialized) {
|
||||
if (!this.savedVis || !this.isInitialized || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
this.handleContainerStateChanged(this.input);
|
||||
|
@ -445,6 +482,12 @@ export class Embeddable
|
|||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.isDestroyed = true;
|
||||
if (this.inputReloadSubscriptions.length > 0) {
|
||||
this.inputReloadSubscriptions.forEach((reloadSub) => {
|
||||
reloadSub.unsubscribe();
|
||||
});
|
||||
}
|
||||
if (this.domNode) {
|
||||
unmountComponentAtNode(this.domNode);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue