mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Lens] Redux selectors optimization (#107559)
This commit is contained in:
parent
4f7e62fff3
commit
c5499c6592
33 changed files with 874 additions and 1019 deletions
|
@ -13,13 +13,7 @@ import { App } from './app';
|
|||
import { LensAppProps, LensAppServices } from './types';
|
||||
import { EditorFrameInstance, EditorFrameProps } from '../types';
|
||||
import { Document } from '../persistence';
|
||||
import {
|
||||
createMockDatasource,
|
||||
createMockVisualization,
|
||||
DatasourceMock,
|
||||
makeDefaultServices,
|
||||
mountWithProvider,
|
||||
} from '../mocks';
|
||||
import { visualizationMap, datasourceMap, makeDefaultServices, mountWithProvider } from '../mocks';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import {
|
||||
SavedObjectSaveModal,
|
||||
|
@ -71,29 +65,6 @@ describe('Lens App', () => {
|
|||
let defaultDoc: Document;
|
||||
let defaultSavedObjectId: string;
|
||||
|
||||
const mockDatasource: DatasourceMock = createMockDatasource('testDatasource');
|
||||
const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2');
|
||||
const datasourceMap = {
|
||||
testDatasource2: mockDatasource2,
|
||||
testDatasource: mockDatasource,
|
||||
};
|
||||
|
||||
const mockVisualization = {
|
||||
...createMockVisualization(),
|
||||
id: 'testVis',
|
||||
visualizationTypes: [
|
||||
{
|
||||
icon: 'empty',
|
||||
id: 'testVis',
|
||||
label: 'TEST1',
|
||||
groupLabel: 'testVisGroup',
|
||||
},
|
||||
],
|
||||
};
|
||||
const visualizationMap = {
|
||||
testVis: mockVisualization,
|
||||
};
|
||||
|
||||
function createMockFrame(): jest.Mocked<EditorFrameInstance> {
|
||||
return {
|
||||
EditorFrameContainer: jest.fn((props: EditorFrameProps) => <div />),
|
||||
|
@ -1082,11 +1053,12 @@ describe('Lens App', () => {
|
|||
});
|
||||
|
||||
it('updates the state if session id changes from the outside', async () => {
|
||||
const services = makeDefaultServices(sessionIdSubject);
|
||||
const sessionIdS = new Subject<string>();
|
||||
const services = makeDefaultServices(sessionIdS);
|
||||
const { lensStore } = await mountWith({ props: undefined, services });
|
||||
|
||||
act(() => {
|
||||
sessionIdSubject.next('new-session-id');
|
||||
sessionIdS.next('new-session-id');
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
@ -1181,7 +1153,7 @@ describe('Lens App', () => {
|
|||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
datasourceStates: { testDatasource: '' },
|
||||
datasourceStates: { testDatasource: {} },
|
||||
visualization: {},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -29,13 +29,13 @@ import {
|
|||
useLensDispatch,
|
||||
LensAppState,
|
||||
DispatchSetState,
|
||||
selectSavedObjectFormat,
|
||||
} from '../state_management';
|
||||
import {
|
||||
SaveModalContainer,
|
||||
getLastKnownDocWithoutPinnedFilters,
|
||||
runSaveLensVisualization,
|
||||
} from './save_modal_container';
|
||||
import { getSavedObjectFormat } from '../utils';
|
||||
|
||||
export type SaveProps = Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
||||
returnToOrigin: boolean;
|
||||
|
@ -79,11 +79,6 @@ export function App({
|
|||
);
|
||||
|
||||
const {
|
||||
datasourceStates,
|
||||
visualization,
|
||||
filters,
|
||||
query,
|
||||
activeDatasourceId,
|
||||
persistedDoc,
|
||||
isLinkedToOriginatingApp,
|
||||
searchSessionId,
|
||||
|
@ -91,52 +86,20 @@ export function App({
|
|||
isSaveable,
|
||||
} = useLensSelector((state) => state.lens);
|
||||
|
||||
const currentDoc = useLensSelector((state) =>
|
||||
selectSavedObjectFormat(state, datasourceMap, visualizationMap)
|
||||
);
|
||||
|
||||
// Used to show a popover that guides the user towards changing the date range when no data is available.
|
||||
const [indicateNoData, setIndicateNoData] = useState(false);
|
||||
const [isSaveModalVisible, setIsSaveModalVisible] = useState(false);
|
||||
const [lastKnownDoc, setLastKnownDoc] = useState<Document | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const activeVisualization = visualization.activeId && visualizationMap[visualization.activeId];
|
||||
const activeDatasource =
|
||||
activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading
|
||||
? datasourceMap[activeDatasourceId]
|
||||
: undefined;
|
||||
|
||||
if (!activeDatasource || !activeVisualization || !visualization.state) {
|
||||
return;
|
||||
if (currentDoc) {
|
||||
setLastKnownDoc(currentDoc);
|
||||
}
|
||||
setLastKnownDoc(
|
||||
// todo: that should be redux store selector
|
||||
getSavedObjectFormat({
|
||||
activeDatasources: Object.keys(datasourceStates).reduce(
|
||||
(acc, datasourceId) => ({
|
||||
...acc,
|
||||
[datasourceId]: datasourceMap[datasourceId],
|
||||
}),
|
||||
{}
|
||||
),
|
||||
datasourceStates,
|
||||
visualization,
|
||||
filters,
|
||||
query,
|
||||
title: persistedDoc?.title || '',
|
||||
description: persistedDoc?.description,
|
||||
persistedId: persistedDoc?.savedObjectId,
|
||||
})
|
||||
);
|
||||
}, [
|
||||
persistedDoc?.title,
|
||||
persistedDoc?.description,
|
||||
persistedDoc?.savedObjectId,
|
||||
datasourceStates,
|
||||
visualization,
|
||||
filters,
|
||||
query,
|
||||
activeDatasourceId,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
]);
|
||||
}, [currentDoc]);
|
||||
|
||||
const showNoDataPopover = useCallback(() => {
|
||||
setIndicateNoData(true);
|
||||
|
|
|
@ -22,12 +22,6 @@ function mockFrame(): FramePublicAPI {
|
|||
return {
|
||||
...createMockFramePublicAPI(),
|
||||
datasourceLayers: {},
|
||||
query: { query: '', language: 'lucene' },
|
||||
dateRange: {
|
||||
fromDate: 'now-7d',
|
||||
toDate: 'now',
|
||||
},
|
||||
filters: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -24,24 +24,32 @@ import {
|
|||
updateDatasourceState,
|
||||
updateVisualizationState,
|
||||
setToggleFullscreen,
|
||||
useLensSelector,
|
||||
selectVisualization,
|
||||
} from '../../../state_management';
|
||||
|
||||
export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
|
||||
return props.activeVisualization && props.visualizationState ? (
|
||||
<LayerPanels {...props} activeVisualization={props.activeVisualization} />
|
||||
const visualization = useLensSelector(selectVisualization);
|
||||
const activeVisualization = visualization.activeId
|
||||
? props.visualizationMap[visualization.activeId]
|
||||
: null;
|
||||
|
||||
return activeVisualization && visualization.state ? (
|
||||
<LayerPanels {...props} activeVisualization={activeVisualization} />
|
||||
) : null;
|
||||
});
|
||||
|
||||
export function LayerPanels(
|
||||
props: ConfigPanelWrapperProps & {
|
||||
activeDatasourceId: string;
|
||||
activeVisualization: Visualization;
|
||||
}
|
||||
) {
|
||||
const { activeVisualization, visualizationState, activeDatasourceId, datasourceMap } = props;
|
||||
const { activeVisualization, datasourceMap } = props;
|
||||
const { activeDatasourceId, visualization } = useLensSelector((state) => state.lens);
|
||||
|
||||
const dispatchLens = useLensDispatch();
|
||||
|
||||
const layerIds = activeVisualization.getLayerIds(visualizationState);
|
||||
const layerIds = activeVisualization.getLayerIds(visualization.state);
|
||||
const {
|
||||
setNextFocusedId: setNextFocusedLayerId,
|
||||
removeRef: removeLayerRef,
|
||||
|
@ -139,7 +147,7 @@ export function LayerPanels(
|
|||
key={layerId}
|
||||
layerId={layerId}
|
||||
layerIndex={layerIndex}
|
||||
visualizationState={visualizationState}
|
||||
visualizationState={visualization.state}
|
||||
updateVisualization={setVisualizationState}
|
||||
updateDatasource={updateDatasource}
|
||||
updateDatasourceAsync={updateDatasourceAsync}
|
||||
|
@ -187,7 +195,7 @@ export function LayerPanels(
|
|||
/>
|
||||
) : null
|
||||
)}
|
||||
{activeVisualization.appendLayer && visualizationState && (
|
||||
{activeVisualization.appendLayer && visualization.state && (
|
||||
<EuiFlexItem grow={true} className="lnsConfigPanel__addLayerBtnWrapper">
|
||||
<EuiToolTip
|
||||
className="eui-fullWidth"
|
||||
|
@ -220,7 +228,7 @@ export function LayerPanels(
|
|||
activeVisualization,
|
||||
generateId: () => id,
|
||||
trackUiEvent,
|
||||
activeDatasource: datasourceMap[activeDatasourceId],
|
||||
activeDatasource: datasourceMap[activeDatasourceId!],
|
||||
state,
|
||||
}),
|
||||
})
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { Visualization } from '../../../types';
|
||||
import { LayerPanel } from './layer_panel';
|
||||
import { ChildDragDropProvider, DragDrop } from '../../../drag_drop';
|
||||
|
@ -19,6 +18,7 @@ import {
|
|||
createMockFramePublicAPI,
|
||||
createMockDatasource,
|
||||
DatasourceMock,
|
||||
mountWithProvider,
|
||||
} from '../../../mocks';
|
||||
|
||||
jest.mock('../../../id_generator');
|
||||
|
@ -65,15 +65,8 @@ describe('LayerPanel', () => {
|
|||
return {
|
||||
layerId: 'first',
|
||||
activeVisualization: mockVisualization,
|
||||
activeDatasourceId: 'ds1',
|
||||
datasourceMap: {
|
||||
ds1: mockDatasource,
|
||||
},
|
||||
datasourceStates: {
|
||||
ds1: {
|
||||
isLoading: false,
|
||||
state: 'state',
|
||||
},
|
||||
testDatasource: mockDatasource,
|
||||
},
|
||||
visualizationState: 'state',
|
||||
updateVisualization: jest.fn(),
|
||||
|
@ -120,45 +113,49 @@ describe('LayerPanel', () => {
|
|||
};
|
||||
|
||||
mockVisualization.getLayerIds.mockReturnValue(['first']);
|
||||
mockDatasource = createMockDatasource('ds1');
|
||||
mockDatasource = createMockDatasource('testDatasource');
|
||||
});
|
||||
|
||||
describe('layer reset and remove', () => {
|
||||
it('should show the reset button when single layer', () => {
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain(
|
||||
it('should show the reset button when single layer', async () => {
|
||||
const { instance } = await mountWithProvider(<LayerPanel {...getDefaultProps()} />);
|
||||
expect(instance.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain(
|
||||
'Reset layer'
|
||||
);
|
||||
});
|
||||
|
||||
it('should show the delete button when multiple layers', () => {
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} isOnlyLayer={false} />);
|
||||
expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain(
|
||||
it('should show the delete button when multiple layers', async () => {
|
||||
const { instance } = await mountWithProvider(
|
||||
<LayerPanel {...getDefaultProps()} isOnlyLayer={false} />
|
||||
);
|
||||
expect(instance.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain(
|
||||
'Delete layer'
|
||||
);
|
||||
});
|
||||
|
||||
it('should show to reset visualization for visualizations only allowing a single layer', () => {
|
||||
it('should show to reset visualization for visualizations only allowing a single layer', async () => {
|
||||
const layerPanelAttributes = getDefaultProps();
|
||||
delete layerPanelAttributes.activeVisualization.removeLayer;
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain(
|
||||
const { instance } = await mountWithProvider(<LayerPanel {...getDefaultProps()} />);
|
||||
expect(instance.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain(
|
||||
'Reset visualization'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the clear callback', () => {
|
||||
it('should call the clear callback', async () => {
|
||||
const cb = jest.fn();
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} onRemoveLayer={cb} />);
|
||||
const { instance } = await mountWithProvider(
|
||||
<LayerPanel {...getDefaultProps()} onRemoveLayer={cb} />
|
||||
);
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click');
|
||||
instance.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click');
|
||||
});
|
||||
expect(cb).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('single group', () => {
|
||||
it('should render the non-editable state', () => {
|
||||
it('should render the non-editable state', async () => {
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
|
@ -172,12 +169,12 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
const group = component.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]');
|
||||
const { instance } = await mountWithProvider(<LayerPanel {...getDefaultProps()} />);
|
||||
const group = instance.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]');
|
||||
expect(group).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the group with a way to add a new column', () => {
|
||||
it('should render the group with a way to add a new column', async () => {
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
|
@ -191,12 +188,12 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
const group = component.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]');
|
||||
const { instance } = await mountWithProvider(<LayerPanel {...getDefaultProps()} />);
|
||||
const group = instance.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]');
|
||||
expect(group).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the required warning when only one group is configured', () => {
|
||||
it('should render the required warning when only one group is configured', async () => {
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
|
@ -219,16 +216,16 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
const { instance } = await mountWithProvider(<LayerPanel {...getDefaultProps()} />);
|
||||
|
||||
const group = component
|
||||
const group = instance
|
||||
.find(EuiFormRow)
|
||||
.findWhere((e) => e.prop('error')?.props?.children === 'Required dimension');
|
||||
|
||||
expect(group).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the datasource and visualization panels inside the dimension container', () => {
|
||||
it('should render the datasource and visualization panels inside the dimension container', async () => {
|
||||
mockVisualization.getConfiguration.mockReturnValueOnce({
|
||||
groups: [
|
||||
{
|
||||
|
@ -244,18 +241,18 @@ describe('LayerPanel', () => {
|
|||
});
|
||||
mockVisualization.renderDimensionEditor = jest.fn();
|
||||
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
const { instance } = await mountWithProvider(<LayerPanel {...getDefaultProps()} />);
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click');
|
||||
instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click');
|
||||
});
|
||||
component.update();
|
||||
instance.update();
|
||||
|
||||
const group = component.find('DimensionContainer').first();
|
||||
const group = instance.find('DimensionContainer').first();
|
||||
const panel: React.ReactElement = group.prop('panel');
|
||||
expect(panel.props.children).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should not update the visualization if the datasource is incomplete', () => {
|
||||
it('should not update the visualization if the datasource is incomplete', async () => {
|
||||
(generateId as jest.Mock).mockReturnValue(`newid`);
|
||||
const updateAll = jest.fn();
|
||||
const updateDatasourceAsync = jest.fn();
|
||||
|
@ -273,7 +270,7 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const component = mountWithIntl(
|
||||
const { instance } = await mountWithProvider(
|
||||
<LayerPanel
|
||||
{...getDefaultProps()}
|
||||
updateDatasourceAsync={updateDatasourceAsync}
|
||||
|
@ -282,9 +279,9 @@ describe('LayerPanel', () => {
|
|||
);
|
||||
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click');
|
||||
instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click');
|
||||
});
|
||||
component.update();
|
||||
instance.update();
|
||||
|
||||
expect(mockDatasource.renderDimensionEditor).toHaveBeenCalledWith(
|
||||
expect.any(Element),
|
||||
|
@ -319,7 +316,7 @@ describe('LayerPanel', () => {
|
|||
expect(updateAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove the dimension when the datasource marks it as removed', () => {
|
||||
it('should remove the dimension when the datasource marks it as removed', async () => {
|
||||
const updateAll = jest.fn();
|
||||
const updateDatasource = jest.fn();
|
||||
|
||||
|
@ -336,38 +333,42 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const component = mountWithIntl(
|
||||
const { instance } = await mountWithProvider(
|
||||
<LayerPanel
|
||||
{...getDefaultProps()}
|
||||
datasourceStates={{
|
||||
ds1: {
|
||||
isLoading: false,
|
||||
state: {
|
||||
layers: [
|
||||
{
|
||||
indexPatternId: '1',
|
||||
columns: {
|
||||
y: {
|
||||
operationType: 'moving_average',
|
||||
references: ['ref'],
|
||||
},
|
||||
},
|
||||
columnOrder: ['y'],
|
||||
incompleteColumns: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}}
|
||||
updateDatasource={updateDatasource}
|
||||
updateAll={updateAll}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
isLoading: false,
|
||||
state: {
|
||||
layers: [
|
||||
{
|
||||
indexPatternId: '1',
|
||||
columns: {
|
||||
y: {
|
||||
operationType: 'moving_average',
|
||||
references: ['ref'],
|
||||
},
|
||||
},
|
||||
columnOrder: ['y'],
|
||||
incompleteColumns: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click');
|
||||
instance.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click');
|
||||
});
|
||||
component.update();
|
||||
instance.update();
|
||||
|
||||
expect(mockDatasource.renderDimensionEditor).toHaveBeenCalledWith(
|
||||
expect.any(Element),
|
||||
|
@ -399,7 +400,7 @@ describe('LayerPanel', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should keep the DimensionContainer open when configuring a new dimension', () => {
|
||||
it('should keep the DimensionContainer open when configuring a new dimension', async () => {
|
||||
/**
|
||||
* The ID generation system for new dimensions has been messy before, so
|
||||
* this tests that the ID used in the first render is used to keep the container
|
||||
|
@ -436,13 +437,13 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
const { instance } = await mountWithProvider(<LayerPanel {...getDefaultProps()} />);
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click');
|
||||
instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click');
|
||||
});
|
||||
component.update();
|
||||
instance.update();
|
||||
|
||||
expect(component.find('EuiFlyoutHeader').exists()).toBe(true);
|
||||
expect(instance.find('EuiFlyoutHeader').exists()).toBe(true);
|
||||
|
||||
const lastArgs =
|
||||
mockDatasource.renderDimensionEditor.mock.calls[
|
||||
|
@ -459,7 +460,7 @@ describe('LayerPanel', () => {
|
|||
expect(mockVisualization.renderDimensionEditor).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close the DimensionContainer when the active visualization changes', () => {
|
||||
it('should close the DimensionContainer when the active visualization changes', async () => {
|
||||
/**
|
||||
* The ID generation system for new dimensions has been messy before, so
|
||||
* this tests that the ID used in the first render is used to keep the container
|
||||
|
@ -495,21 +496,21 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
const { instance } = await mountWithProvider(<LayerPanel {...getDefaultProps()} />);
|
||||
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click');
|
||||
instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click');
|
||||
});
|
||||
component.update();
|
||||
expect(component.find('EuiFlyoutHeader').exists()).toBe(true);
|
||||
instance.update();
|
||||
expect(instance.find('EuiFlyoutHeader').exists()).toBe(true);
|
||||
act(() => {
|
||||
component.setProps({ activeVisualization: mockVisualization2 });
|
||||
instance.setProps({ activeVisualization: mockVisualization2 });
|
||||
});
|
||||
component.update();
|
||||
expect(component.find('EuiFlyoutHeader').exists()).toBe(false);
|
||||
instance.update();
|
||||
expect(instance.find('EuiFlyoutHeader').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should only update the state on close when needed', () => {
|
||||
it('should only update the state on close when needed', async () => {
|
||||
const updateDatasource = jest.fn();
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
|
@ -524,37 +525,37 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const component = mountWithIntl(
|
||||
const { instance } = await mountWithProvider(
|
||||
<LayerPanel {...getDefaultProps()} updateDatasource={updateDatasource} />
|
||||
);
|
||||
|
||||
// Close without a state update
|
||||
mockDatasource.updateStateOnCloseDimension = jest.fn();
|
||||
component.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click');
|
||||
instance.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click');
|
||||
act(() => {
|
||||
(component.find('DimensionContainer').first().prop('handleClose') as () => void)();
|
||||
(instance.find('DimensionContainer').first().prop('handleClose') as () => void)();
|
||||
});
|
||||
component.update();
|
||||
instance.update();
|
||||
expect(mockDatasource.updateStateOnCloseDimension).toHaveBeenCalled();
|
||||
expect(updateDatasource).not.toHaveBeenCalled();
|
||||
|
||||
// Close with a state update
|
||||
mockDatasource.updateStateOnCloseDimension = jest.fn().mockReturnValue({ newState: true });
|
||||
|
||||
component.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click');
|
||||
instance.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click');
|
||||
act(() => {
|
||||
(component.find('DimensionContainer').first().prop('handleClose') as () => void)();
|
||||
(instance.find('DimensionContainer').first().prop('handleClose') as () => void)();
|
||||
});
|
||||
component.update();
|
||||
instance.update();
|
||||
expect(mockDatasource.updateStateOnCloseDimension).toHaveBeenCalled();
|
||||
expect(updateDatasource).toHaveBeenCalledWith('ds1', { newState: true });
|
||||
expect(updateDatasource).toHaveBeenCalledWith('testDatasource', { newState: true });
|
||||
});
|
||||
});
|
||||
|
||||
// This test is more like an integration test, since the layer panel owns all
|
||||
// the coordination between drag and drop
|
||||
describe('drag and drop behavior', () => {
|
||||
it('should determine if the datasource supports dropping of a field onto empty dimension', () => {
|
||||
it('should determine if the datasource supports dropping of a field onto empty dimension', async () => {
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
|
@ -584,7 +585,7 @@ describe('LayerPanel', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingField}>
|
||||
<LayerPanel {...getDefaultProps()} />
|
||||
</ChildDragDropProvider>
|
||||
|
@ -596,7 +597,7 @@ describe('LayerPanel', () => {
|
|||
})
|
||||
);
|
||||
|
||||
const dragDropElement = component
|
||||
const dragDropElement = instance
|
||||
.find('[data-test-subj="lnsGroup"] DragDrop .lnsDragDrop')
|
||||
.first();
|
||||
|
||||
|
@ -610,7 +611,7 @@ describe('LayerPanel', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should determine if the datasource supports dropping of a field onto a pre-filled dimension', () => {
|
||||
it('should determine if the datasource supports dropping of a field onto a pre-filled dimension', async () => {
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
|
@ -639,7 +640,7 @@ describe('LayerPanel', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingField}>
|
||||
<LayerPanel {...getDefaultProps()} />
|
||||
</ChildDragDropProvider>
|
||||
|
@ -650,10 +651,10 @@ describe('LayerPanel', () => {
|
|||
);
|
||||
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('dropType')
|
||||
instance.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('dropType')
|
||||
).toEqual(undefined);
|
||||
|
||||
const dragDropElement = component
|
||||
const dragDropElement = instance
|
||||
.find('[data-test-subj="lnsGroup"] DragDrop')
|
||||
.first()
|
||||
.find('.lnsLayerPanel__dimension');
|
||||
|
@ -664,7 +665,7 @@ describe('LayerPanel', () => {
|
|||
expect(mockDatasource.onDrop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow drag to move between groups', () => {
|
||||
it('should allow drag to move between groups', async () => {
|
||||
(generateId as jest.Mock).mockReturnValue(`newid`);
|
||||
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
|
@ -705,7 +706,7 @@ describe('LayerPanel', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel {...getDefaultProps()} />
|
||||
</ChildDragDropProvider>
|
||||
|
@ -719,7 +720,7 @@ describe('LayerPanel', () => {
|
|||
|
||||
// Simulate drop on the pre-populated dimension
|
||||
|
||||
const dragDropElement = component
|
||||
const dragDropElement = instance
|
||||
.find('[data-test-subj="lnsGroupB"] DragDrop .lnsDragDrop')
|
||||
.at(0);
|
||||
dragDropElement.simulate('dragOver');
|
||||
|
@ -734,7 +735,7 @@ describe('LayerPanel', () => {
|
|||
|
||||
// Simulate drop on the empty dimension
|
||||
|
||||
const updatedDragDropElement = component
|
||||
const updatedDragDropElement = instance
|
||||
.find('[data-test-subj="lnsGroupB"] DragDrop .lnsDragDrop')
|
||||
.at(2);
|
||||
|
||||
|
@ -749,7 +750,7 @@ describe('LayerPanel', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should reorder when dropping in the same group', () => {
|
||||
it('should reorder when dropping in the same group', async () => {
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
|
@ -775,14 +776,15 @@ describe('LayerPanel', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel {...getDefaultProps()} />
|
||||
</ChildDragDropProvider>,
|
||||
undefined,
|
||||
{ attachTo: container }
|
||||
);
|
||||
act(() => {
|
||||
component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder');
|
||||
instance.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder');
|
||||
});
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -790,7 +792,7 @@ describe('LayerPanel', () => {
|
|||
droppedItem: draggingOperation,
|
||||
})
|
||||
);
|
||||
const secondButton = component
|
||||
const secondButton = instance
|
||||
.find(DragDrop)
|
||||
.at(1)
|
||||
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
|
||||
|
@ -799,7 +801,7 @@ describe('LayerPanel', () => {
|
|||
expect(focusedEl).toEqual(secondButton);
|
||||
});
|
||||
|
||||
it('should copy when dropping on empty slot in the same group', () => {
|
||||
it('should copy when dropping on empty slot in the same group', async () => {
|
||||
(generateId as jest.Mock).mockReturnValue(`newid`);
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
|
@ -826,13 +828,13 @@ describe('LayerPanel', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel {...getDefaultProps()} />
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
act(() => {
|
||||
component.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_compatible');
|
||||
instance.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_compatible');
|
||||
});
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -843,7 +845,7 @@ describe('LayerPanel', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should call onDrop and update visualization when replacing between compatible groups', () => {
|
||||
it('should call onDrop and update visualization when replacing between compatible groups', async () => {
|
||||
const mockVis = {
|
||||
...mockVisualization,
|
||||
removeDimension: jest.fn(),
|
||||
|
@ -881,7 +883,7 @@ describe('LayerPanel', () => {
|
|||
mockDatasource.onDrop.mockReturnValue({ deleted: 'a' });
|
||||
const updateVisualization = jest.fn();
|
||||
|
||||
const component = mountWithIntl(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel
|
||||
{...getDefaultProps()}
|
||||
|
@ -891,7 +893,7 @@ describe('LayerPanel', () => {
|
|||
</ChildDragDropProvider>
|
||||
);
|
||||
act(() => {
|
||||
component.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible');
|
||||
instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible');
|
||||
});
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
|
@ -29,6 +29,12 @@ import { EmptyDimensionButton } from './buttons/empty_dimension_button';
|
|||
import { DimensionButton } from './buttons/dimension_button';
|
||||
import { DraggableDimensionButton } from './buttons/draggable_dimension_button';
|
||||
import { useFocusUpdate } from './use_focus_update';
|
||||
import {
|
||||
useLensSelector,
|
||||
selectIsFullscreenDatasource,
|
||||
selectResolvedDateRange,
|
||||
selectDatasourceStates,
|
||||
} from '../../../state_management';
|
||||
|
||||
const initialActiveDimensionState = {
|
||||
isNew: false,
|
||||
|
@ -51,7 +57,6 @@ export function LayerPanel(
|
|||
onRemoveLayer: () => void;
|
||||
registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void;
|
||||
toggleFullscreen: () => void;
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
) {
|
||||
const [activeDimension, setActiveDimension] = useState<ActiveDimensionState>(
|
||||
|
@ -69,12 +74,14 @@ export function LayerPanel(
|
|||
updateVisualization,
|
||||
updateDatasource,
|
||||
toggleFullscreen,
|
||||
isFullscreen,
|
||||
updateAll,
|
||||
updateDatasourceAsync,
|
||||
visualizationState,
|
||||
} = props;
|
||||
const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId];
|
||||
const dateRange = useLensSelector(selectResolvedDateRange);
|
||||
const datasourceStates = useLensSelector(selectDatasourceStates);
|
||||
const isFullscreen = useLensSelector(selectIsFullscreenDatasource);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveDimension(initialActiveDimensionState);
|
||||
|
@ -90,12 +97,12 @@ export function LayerPanel(
|
|||
layerId,
|
||||
state: props.visualizationState,
|
||||
frame: props.framePublicAPI,
|
||||
dateRange: props.framePublicAPI.dateRange,
|
||||
dateRange,
|
||||
activeData: props.framePublicAPI.activeData,
|
||||
};
|
||||
|
||||
const datasourceId = datasourcePublicAPI.datasourceId;
|
||||
const layerDatasourceState = props.datasourceStates[datasourceId].state;
|
||||
const layerDatasourceState = datasourceStates[datasourceId].state;
|
||||
|
||||
const layerDatasourceDropProps = useMemo(
|
||||
() => ({
|
||||
|
@ -113,8 +120,8 @@ export function LayerPanel(
|
|||
const layerDatasourceConfigProps = {
|
||||
...layerDatasourceDropProps,
|
||||
frame: props.framePublicAPI,
|
||||
dateRange: props.framePublicAPI.dateRange,
|
||||
activeData: props.framePublicAPI.activeData,
|
||||
dateRange,
|
||||
};
|
||||
|
||||
const { groups } = useMemo(
|
||||
|
|
|
@ -11,39 +11,21 @@ import {
|
|||
DatasourceDimensionEditorProps,
|
||||
VisualizationDimensionGroupConfig,
|
||||
DatasourceMap,
|
||||
VisualizationMap,
|
||||
} from '../../../types';
|
||||
export interface ConfigPanelWrapperProps {
|
||||
activeDatasourceId: string;
|
||||
visualizationState: unknown;
|
||||
activeVisualization: Visualization | null;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
datasourceMap: DatasourceMap;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
visualizationMap: VisualizationMap;
|
||||
core: DatasourceDimensionEditorProps['core'];
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
export interface LayerPanelProps {
|
||||
activeDatasourceId: string;
|
||||
visualizationState: unknown;
|
||||
datasourceMap: DatasourceMap;
|
||||
activeVisualization: Visualization;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
core: DatasourceDimensionEditorProps['core'];
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
export interface LayerDatasourceDropProps {
|
||||
|
|
|
@ -10,7 +10,6 @@ import './data_panel_wrapper.scss';
|
|||
import React, { useMemo, memo, useContext, useState, useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { NativeRenderer } from '../../native_renderer';
|
||||
import { DragContext, DragDropIdentifier } from '../../drag_drop';
|
||||
import { StateSetter, DatasourceDataPanelProps, DatasourceMap } from '../../types';
|
||||
|
@ -19,17 +18,16 @@ import {
|
|||
switchDatasource,
|
||||
useLensDispatch,
|
||||
updateDatasourceState,
|
||||
LensState,
|
||||
useLensSelector,
|
||||
setState,
|
||||
selectExecutionContext,
|
||||
selectActiveDatasourceId,
|
||||
selectDatasourceStates,
|
||||
} from '../../state_management';
|
||||
import { initializeDatasources } from './state_helpers';
|
||||
|
||||
interface DataPanelWrapperProps {
|
||||
datasourceState: unknown;
|
||||
datasourceMap: DatasourceMap;
|
||||
activeDatasource: string | null;
|
||||
datasourceIsLoading: boolean;
|
||||
showNoDataPopover: () => void;
|
||||
core: DatasourceDataPanelProps['core'];
|
||||
dropOntoWorkspace: (field: DragDropIdentifier) => void;
|
||||
|
@ -37,35 +35,27 @@ interface DataPanelWrapperProps {
|
|||
plugins: { uiActions: UiActionsStart };
|
||||
}
|
||||
|
||||
const getExternals = createSelector(
|
||||
(state: LensState) => state.lens,
|
||||
({ resolvedDateRange, query, filters, datasourceStates, activeDatasourceId }) => ({
|
||||
dateRange: resolvedDateRange,
|
||||
query,
|
||||
filters,
|
||||
datasourceStates,
|
||||
activeDatasourceId,
|
||||
})
|
||||
);
|
||||
|
||||
export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
||||
const { activeDatasource } = props;
|
||||
const externalContext = useLensSelector(selectExecutionContext);
|
||||
const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
|
||||
const datasourceStates = useLensSelector(selectDatasourceStates);
|
||||
|
||||
const datasourceIsLoading = activeDatasourceId
|
||||
? datasourceStates[activeDatasourceId].isLoading
|
||||
: true;
|
||||
|
||||
const { filters, query, dateRange, datasourceStates, activeDatasourceId } = useLensSelector(
|
||||
getExternals
|
||||
);
|
||||
const dispatchLens = useLensDispatch();
|
||||
const setDatasourceState: StateSetter<unknown> = useMemo(() => {
|
||||
return (updater) => {
|
||||
dispatchLens(
|
||||
updateDatasourceState({
|
||||
updater,
|
||||
datasourceId: activeDatasource!,
|
||||
datasourceId: activeDatasourceId!,
|
||||
clearStagedPreview: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
}, [activeDatasource, dispatchLens]);
|
||||
}, [activeDatasourceId, dispatchLens]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeDatasourceId && datasourceStates[activeDatasourceId].state === null) {
|
||||
|
@ -88,13 +78,11 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
}, [datasourceStates, activeDatasourceId, props.datasourceMap, dispatchLens]);
|
||||
|
||||
const datasourceProps: DatasourceDataPanelProps = {
|
||||
...externalContext,
|
||||
dragDropContext: useContext(DragContext),
|
||||
state: props.datasourceState,
|
||||
state: activeDatasourceId ? datasourceStates[activeDatasourceId].state : null,
|
||||
setState: setDatasourceState,
|
||||
core: props.core,
|
||||
filters,
|
||||
query,
|
||||
dateRange,
|
||||
showNoDataPopover: props.showNoDataPopover,
|
||||
dropOntoWorkspace: props.dropOntoWorkspace,
|
||||
hasSuggestionForField: props.hasSuggestionForField,
|
||||
|
@ -135,7 +123,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
<EuiContextMenuItem
|
||||
key={datasourceId}
|
||||
data-test-subj={`datasource-switch-${datasourceId}`}
|
||||
icon={props.activeDatasource === datasourceId ? 'check' : 'empty'}
|
||||
icon={activeDatasourceId === datasourceId ? 'check' : 'empty'}
|
||||
onClick={() => {
|
||||
setDatasourceSwitcher(false);
|
||||
dispatchLens(switchDatasource({ newDatasourceId: datasourceId }));
|
||||
|
@ -147,10 +135,10 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
/>
|
||||
</EuiPopover>
|
||||
)}
|
||||
{props.activeDatasource && !props.datasourceIsLoading && (
|
||||
{activeDatasourceId && !datasourceIsLoading && (
|
||||
<NativeRenderer
|
||||
className="lnsDataPanelWrapper"
|
||||
render={props.datasourceMap[props.activeDatasource].renderDataPanel}
|
||||
render={props.datasourceMap[activeDatasourceId].renderDataPanel}
|
||||
nativeProps={datasourceProps}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
|
||||
// Tests are executed in a jsdom environment who does not have sizing methods,
|
||||
|
@ -45,8 +45,7 @@ import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/pu
|
|||
import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks';
|
||||
import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks';
|
||||
import { mockDataPlugin, mountWithProvider } from '../../mocks';
|
||||
import { setState, setToggleFullscreen } from '../../state_management';
|
||||
import { FrameLayout } from './frame_layout';
|
||||
import { setState } from '../../state_management';
|
||||
|
||||
function generateSuggestion(state = {}): DatasourceSuggestion {
|
||||
return {
|
||||
|
@ -213,7 +212,7 @@ describe('editor_frame', () => {
|
|||
const props = {
|
||||
...getDefaultProps(),
|
||||
visualizationMap: {
|
||||
testVis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
},
|
||||
datasourceMap: {
|
||||
testDatasource: {
|
||||
|
@ -246,7 +245,7 @@ describe('editor_frame', () => {
|
|||
expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(`
|
||||
"kibana
|
||||
| lens_merge_tables layerIds=\\"first\\" tables={datasource}
|
||||
| vis"
|
||||
| testVis"
|
||||
`);
|
||||
});
|
||||
|
||||
|
@ -263,7 +262,7 @@ describe('editor_frame', () => {
|
|||
const props = {
|
||||
...getDefaultProps(),
|
||||
visualizationMap: {
|
||||
testVis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
},
|
||||
datasourceMap: {
|
||||
testDatasource: {
|
||||
|
@ -368,7 +367,7 @@ describe('editor_frame', () => {
|
|||
},
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "vis",
|
||||
"function": "testVis",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
|
@ -1100,45 +1099,5 @@ describe('editor_frame', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should avoid completely to compute suggestion when in fullscreen mode', async () => {
|
||||
const props = {
|
||||
...getDefaultProps(),
|
||||
initialContext: {
|
||||
indexPatternId: '1',
|
||||
fieldName: 'test',
|
||||
},
|
||||
visualizationMap: {
|
||||
testVis: mockVisualization,
|
||||
},
|
||||
datasourceMap: {
|
||||
testDatasource: mockDatasource,
|
||||
testDatasource2: mockDatasource2,
|
||||
},
|
||||
|
||||
ExpressionRenderer: expressionRendererMock,
|
||||
};
|
||||
|
||||
const { instance: el, lensStore } = await mountWithProvider(<EditorFrame {...props} />, {
|
||||
data: props.plugins.data,
|
||||
});
|
||||
instance = el;
|
||||
|
||||
expect(
|
||||
instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement
|
||||
).not.toBeUndefined();
|
||||
|
||||
lensStore.dispatch(setToggleFullscreen());
|
||||
instance.update();
|
||||
|
||||
expect(instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement).toBe(false);
|
||||
|
||||
lensStore.dispatch(setToggleFullscreen());
|
||||
instance.update();
|
||||
|
||||
expect(
|
||||
instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement
|
||||
).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,21 +5,28 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef, useMemo } from 'react';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
|
||||
import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../types';
|
||||
import { DataPanelWrapper } from './data_panel_wrapper';
|
||||
import { ConfigPanelWrapper } from './config_panel';
|
||||
import { FrameLayout } from './frame_layout';
|
||||
import { SuggestionPanel } from './suggestion_panel';
|
||||
import { SuggestionPanelWrapper } from './suggestion_panel';
|
||||
import { WorkspacePanel } from './workspace_panel';
|
||||
import { DragDropIdentifier, RootDragDropProvider } from '../../drag_drop';
|
||||
import { EditorFrameStartPlugins } from '../service';
|
||||
import { createDatasourceLayers } from './state_helpers';
|
||||
import { getTopSuggestionForField, switchToSuggestion, Suggestion } from './suggestion_helpers';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { useLensSelector, useLensDispatch } from '../../state_management';
|
||||
import {
|
||||
useLensSelector,
|
||||
useLensDispatch,
|
||||
selectAreDatasourcesLoaded,
|
||||
selectFramePublicAPI,
|
||||
selectActiveDatasourceId,
|
||||
selectDatasourceStates,
|
||||
selectVisualization,
|
||||
} from '../../state_management';
|
||||
|
||||
export interface EditorFrameProps {
|
||||
datasourceMap: DatasourceMap;
|
||||
|
@ -31,58 +38,27 @@ export interface EditorFrameProps {
|
|||
}
|
||||
|
||||
export function EditorFrame(props: EditorFrameProps) {
|
||||
const {
|
||||
activeData,
|
||||
resolvedDateRange: dateRange,
|
||||
query,
|
||||
filters,
|
||||
searchSessionId,
|
||||
activeDatasourceId,
|
||||
visualization,
|
||||
datasourceStates,
|
||||
stagedPreview,
|
||||
isFullscreenDatasource,
|
||||
} = useLensSelector((state) => state.lens);
|
||||
|
||||
const { datasourceMap, visualizationMap } = props;
|
||||
const dispatchLens = useLensDispatch();
|
||||
|
||||
const allLoaded = Object.values(datasourceStates).every(({ isLoading }) => isLoading === false);
|
||||
|
||||
const datasourceLayers = React.useMemo(
|
||||
() => createDatasourceLayers(props.datasourceMap, datasourceStates),
|
||||
[props.datasourceMap, datasourceStates]
|
||||
const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
|
||||
const datasourceStates = useLensSelector(selectDatasourceStates);
|
||||
const visualization = useLensSelector(selectVisualization);
|
||||
const allLoaded = useLensSelector(selectAreDatasourcesLoaded);
|
||||
const framePublicAPI: FramePublicAPI = useLensSelector((state) =>
|
||||
selectFramePublicAPI(state, datasourceMap)
|
||||
);
|
||||
|
||||
const framePublicAPI: FramePublicAPI = useMemo(
|
||||
() => ({
|
||||
datasourceLayers,
|
||||
activeData,
|
||||
dateRange,
|
||||
query,
|
||||
filters,
|
||||
searchSessionId,
|
||||
}),
|
||||
[activeData, datasourceLayers, dateRange, query, filters, searchSessionId]
|
||||
);
|
||||
|
||||
// Using a ref to prevent rerenders in the child components while keeping the latest state
|
||||
const getSuggestionForField = useRef<(field: DragDropIdentifier) => Suggestion | undefined>();
|
||||
getSuggestionForField.current = (field: DragDropIdentifier) => {
|
||||
const activeVisualizationId = visualization.activeId;
|
||||
const visualizationState = visualization.state;
|
||||
const { visualizationMap, datasourceMap } = props;
|
||||
|
||||
if (!field || !activeDatasourceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getTopSuggestionForField(
|
||||
datasourceLayers,
|
||||
activeVisualizationId,
|
||||
visualizationMap,
|
||||
visualizationState,
|
||||
datasourceMap[activeDatasourceId],
|
||||
framePublicAPI.datasourceLayers,
|
||||
visualization,
|
||||
datasourceStates,
|
||||
visualizationMap,
|
||||
datasourceMap[activeDatasourceId],
|
||||
field
|
||||
);
|
||||
};
|
||||
|
@ -106,18 +82,12 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
return (
|
||||
<RootDragDropProvider>
|
||||
<FrameLayout
|
||||
isFullscreen={Boolean(isFullscreenDatasource)}
|
||||
dataPanel={
|
||||
<DataPanelWrapper
|
||||
datasourceMap={props.datasourceMap}
|
||||
core={props.core}
|
||||
plugins={props.plugins}
|
||||
datasourceMap={datasourceMap}
|
||||
showNoDataPopover={props.showNoDataPopover}
|
||||
activeDatasource={activeDatasourceId}
|
||||
datasourceState={activeDatasourceId ? datasourceStates[activeDatasourceId].state : null}
|
||||
datasourceIsLoading={
|
||||
activeDatasourceId ? datasourceStates[activeDatasourceId].isLoading : true
|
||||
}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
hasSuggestionForField={hasSuggestionForField}
|
||||
/>
|
||||
|
@ -125,50 +95,33 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
configPanel={
|
||||
allLoaded && (
|
||||
<ConfigPanelWrapper
|
||||
activeVisualization={
|
||||
visualization.activeId ? props.visualizationMap[visualization.activeId] : null
|
||||
}
|
||||
activeDatasourceId={activeDatasourceId!}
|
||||
datasourceMap={props.datasourceMap}
|
||||
datasourceStates={datasourceStates}
|
||||
visualizationState={visualization.state}
|
||||
framePublicAPI={framePublicAPI}
|
||||
core={props.core}
|
||||
isFullscreen={Boolean(isFullscreenDatasource)}
|
||||
datasourceMap={datasourceMap}
|
||||
visualizationMap={visualizationMap}
|
||||
framePublicAPI={framePublicAPI}
|
||||
/>
|
||||
)
|
||||
}
|
||||
workspacePanel={
|
||||
allLoaded && (
|
||||
<WorkspacePanel
|
||||
activeDatasourceId={activeDatasourceId}
|
||||
activeVisualizationId={visualization.activeId}
|
||||
datasourceMap={props.datasourceMap}
|
||||
datasourceStates={datasourceStates}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationState={visualization.state}
|
||||
visualizationMap={props.visualizationMap}
|
||||
isFullscreen={Boolean(isFullscreenDatasource)}
|
||||
ExpressionRenderer={props.ExpressionRenderer}
|
||||
core={props.core}
|
||||
plugins={props.plugins}
|
||||
ExpressionRenderer={props.ExpressionRenderer}
|
||||
datasourceMap={datasourceMap}
|
||||
visualizationMap={visualizationMap}
|
||||
framePublicAPI={framePublicAPI}
|
||||
getSuggestionForField={getSuggestionForField.current}
|
||||
/>
|
||||
)
|
||||
}
|
||||
suggestionsPanel={
|
||||
allLoaded &&
|
||||
!isFullscreenDatasource && (
|
||||
<SuggestionPanel
|
||||
visualizationMap={props.visualizationMap}
|
||||
datasourceMap={props.datasourceMap}
|
||||
allLoaded && (
|
||||
<SuggestionPanelWrapper
|
||||
ExpressionRenderer={props.ExpressionRenderer}
|
||||
stagedPreview={stagedPreview}
|
||||
datasourceMap={datasourceMap}
|
||||
visualizationMap={visualizationMap}
|
||||
frame={framePublicAPI}
|
||||
activeVisualizationId={visualization.activeId}
|
||||
activeDatasourceId={activeDatasourceId}
|
||||
datasourceStates={datasourceStates}
|
||||
visualizationState={visualization.state}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,18 +6,13 @@
|
|||
*/
|
||||
|
||||
import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common';
|
||||
import { DatasourceStates } from '../../state_management';
|
||||
import { Visualization, DatasourcePublicAPI, DatasourceMap } from '../../types';
|
||||
|
||||
export function prependDatasourceExpression(
|
||||
visualizationExpression: Ast | string | null,
|
||||
datasourceMap: DatasourceMap,
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>
|
||||
datasourceStates: DatasourceStates
|
||||
): Ast | null {
|
||||
const datasourceExpressions: Array<[string, Ast | string]> = [];
|
||||
|
||||
|
@ -81,13 +76,7 @@ export function buildExpression({
|
|||
visualization: Visualization | null;
|
||||
visualizationState: unknown;
|
||||
datasourceMap: DatasourceMap;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
datasourceStates: DatasourceStates;
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>;
|
||||
}): Ast | null {
|
||||
if (visualization === null) {
|
||||
|
|
|
@ -11,20 +11,22 @@ import React from 'react';
|
|||
import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { useLensSelector, selectIsFullscreenDatasource } from '../../state_management';
|
||||
|
||||
export interface FrameLayoutProps {
|
||||
dataPanel: React.ReactNode;
|
||||
configPanel?: React.ReactNode;
|
||||
suggestionsPanel?: React.ReactNode;
|
||||
workspacePanel?: React.ReactNode;
|
||||
isFullscreen?: boolean;
|
||||
}
|
||||
|
||||
export function FrameLayout(props: FrameLayoutProps) {
|
||||
const isFullscreen = useLensSelector(selectIsFullscreenDatasource);
|
||||
|
||||
return (
|
||||
<EuiPage
|
||||
className={classNames('lnsFrameLayout', {
|
||||
'lnsFrameLayout-isFullscreen': props.isFullscreen,
|
||||
'lnsFrameLayout-isFullscreen': isFullscreen,
|
||||
})}
|
||||
>
|
||||
<EuiPageBody
|
||||
|
@ -48,7 +50,7 @@ export function FrameLayout(props: FrameLayoutProps) {
|
|||
<section
|
||||
className={classNames('lnsFrameLayout__pageBody', {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'lnsFrameLayout__pageBody-isFullscreen': props.isFullscreen,
|
||||
'lnsFrameLayout__pageBody-isFullscreen': isFullscreen,
|
||||
})}
|
||||
aria-labelledby="workspaceId"
|
||||
>
|
||||
|
@ -65,7 +67,7 @@ export function FrameLayout(props: FrameLayoutProps) {
|
|||
<section
|
||||
className={classNames('lnsFrameLayout__sidebar lnsFrameLayout__sidebar--right', {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'lnsFrameLayout__sidebar-isFullscreen': props.isFullscreen,
|
||||
'lnsFrameLayout__sidebar-isFullscreen': isFullscreen,
|
||||
})}
|
||||
aria-labelledby="configPanel"
|
||||
>
|
||||
|
|
|
@ -28,15 +28,16 @@ import {
|
|||
getMissingIndexPatterns,
|
||||
getMissingVisualizationTypeError,
|
||||
} from '../error_helper';
|
||||
import { DatasourceStates } from '../../state_management';
|
||||
|
||||
export async function initializeDatasources(
|
||||
datasourceMap: DatasourceMap,
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>,
|
||||
datasourceStates: DatasourceStates,
|
||||
references?: SavedObjectReference[],
|
||||
initialContext?: VisualizeFieldContext,
|
||||
options?: InitializationOptions
|
||||
) {
|
||||
const states: Record<string, { isLoading: boolean; state: unknown }> = {};
|
||||
const states: DatasourceStates = {};
|
||||
await Promise.all(
|
||||
Object.entries(datasourceMap).map(([datasourceId, datasource]) => {
|
||||
if (datasourceStates[datasourceId]) {
|
||||
|
@ -57,8 +58,8 @@ export async function initializeDatasources(
|
|||
}
|
||||
|
||||
export const createDatasourceLayers = memoizeOne(function createDatasourceLayers(
|
||||
datasourceMap: DatasourceMap,
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>
|
||||
datasourceStates: DatasourceStates,
|
||||
datasourceMap: DatasourceMap
|
||||
) {
|
||||
const datasourceLayers: Record<string, DatasourcePublicAPI> = {};
|
||||
Object.keys(datasourceMap)
|
||||
|
@ -79,7 +80,7 @@ export const createDatasourceLayers = memoizeOne(function createDatasourceLayers
|
|||
});
|
||||
|
||||
export async function persistedStateToExpression(
|
||||
datasources: Record<string, Datasource>,
|
||||
datasourceMap: DatasourceMap,
|
||||
visualizations: VisualizationMap,
|
||||
doc: Document
|
||||
): Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }> {
|
||||
|
@ -98,7 +99,7 @@ export async function persistedStateToExpression(
|
|||
}
|
||||
const visualization = visualizations[visualizationType!];
|
||||
const datasourceStates = await initializeDatasources(
|
||||
datasources,
|
||||
datasourceMap,
|
||||
Object.fromEntries(
|
||||
Object.entries(persistedDatasourceStates).map(([id, state]) => [
|
||||
id,
|
||||
|
@ -110,7 +111,7 @@ export async function persistedStateToExpression(
|
|||
{ isFullEditor: false }
|
||||
);
|
||||
|
||||
const datasourceLayers = createDatasourceLayers(datasources, datasourceStates);
|
||||
const datasourceLayers = createDatasourceLayers(datasourceStates, datasourceMap);
|
||||
|
||||
const datasourceId = getActiveDatasourceIdFromDoc(doc);
|
||||
if (datasourceId == null) {
|
||||
|
@ -121,7 +122,7 @@ export async function persistedStateToExpression(
|
|||
}
|
||||
|
||||
const indexPatternValidation = validateRequiredIndexPatterns(
|
||||
datasources[datasourceId],
|
||||
datasourceMap[datasourceId],
|
||||
datasourceStates[datasourceId]
|
||||
);
|
||||
|
||||
|
@ -133,7 +134,7 @@ export async function persistedStateToExpression(
|
|||
}
|
||||
|
||||
const validationResult = validateDatasourceAndVisualization(
|
||||
datasources[datasourceId],
|
||||
datasourceMap[datasourceId],
|
||||
datasourceStates[datasourceId].state,
|
||||
visualization,
|
||||
visualizationState,
|
||||
|
@ -146,7 +147,7 @@ export async function persistedStateToExpression(
|
|||
description,
|
||||
visualization,
|
||||
visualizationState,
|
||||
datasourceMap: datasources,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
datasourceLayers,
|
||||
}),
|
||||
|
|
|
@ -9,6 +9,7 @@ import { getSuggestions, getTopSuggestionForField } from './suggestion_helpers';
|
|||
import { createMockVisualization, createMockDatasource, DatasourceMock } from '../../mocks';
|
||||
import { TableSuggestion, DatasourceSuggestion, Visualization } from '../../types';
|
||||
import { PaletteOutput } from 'src/plugins/charts/public';
|
||||
import { DatasourceStates } from '../../state_management';
|
||||
|
||||
const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({
|
||||
state,
|
||||
|
@ -22,13 +23,7 @@ const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSu
|
|||
});
|
||||
|
||||
let datasourceMap: Record<string, DatasourceMock>;
|
||||
let datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
let datasourceStates: DatasourceStates;
|
||||
|
||||
beforeEach(() => {
|
||||
datasourceMap = {
|
||||
|
@ -525,13 +520,10 @@ describe('suggestion helpers', () => {
|
|||
getOperationForColumnId: jest.fn(),
|
||||
},
|
||||
},
|
||||
'vis1',
|
||||
{ activeId: 'vis1', state: {} },
|
||||
{ mockindexpattern: { state: mockDatasourceState, isLoading: false } },
|
||||
{ vis1: mockVisualization1 },
|
||||
{},
|
||||
datasourceMap.mock,
|
||||
{
|
||||
mockindexpattern: { state: mockDatasourceState, isLoading: false },
|
||||
},
|
||||
{ id: 'myfield', humanData: { label: 'myfieldLabel' } },
|
||||
];
|
||||
});
|
||||
|
@ -558,7 +550,7 @@ describe('suggestion helpers', () => {
|
|||
|
||||
it('should return nothing if datasource does not produce suggestions', () => {
|
||||
datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([]);
|
||||
defaultParams[2] = {
|
||||
defaultParams[3] = {
|
||||
vis1: { ...mockVisualization1, getSuggestions: () => [] },
|
||||
vis2: mockVisualization2,
|
||||
};
|
||||
|
@ -567,7 +559,7 @@ describe('suggestion helpers', () => {
|
|||
});
|
||||
|
||||
it('should not consider suggestion from other visualization if there is data', () => {
|
||||
defaultParams[2] = {
|
||||
defaultParams[3] = {
|
||||
vis1: { ...mockVisualization1, getSuggestions: () => [] },
|
||||
vis2: mockVisualization2,
|
||||
};
|
||||
|
@ -593,7 +585,7 @@ describe('suggestion helpers', () => {
|
|||
previewIcon: 'empty',
|
||||
},
|
||||
]);
|
||||
defaultParams[2] = {
|
||||
defaultParams[3] = {
|
||||
vis1: mockVisualization1,
|
||||
vis2: mockVisualization2,
|
||||
vis3: mockVisualization3,
|
||||
|
|
|
@ -22,7 +22,13 @@ import {
|
|||
VisualizationMap,
|
||||
} from '../../types';
|
||||
import { DragDropIdentifier } from '../../drag_drop';
|
||||
import { LensDispatch, selectSuggestion, switchVisualization } from '../../state_management';
|
||||
import {
|
||||
LensDispatch,
|
||||
selectSuggestion,
|
||||
switchVisualization,
|
||||
DatasourceStates,
|
||||
VisualizationState,
|
||||
} from '../../state_management';
|
||||
|
||||
export interface Suggestion {
|
||||
visualizationId: string;
|
||||
|
@ -60,13 +66,7 @@ export function getSuggestions({
|
|||
mainPalette,
|
||||
}: {
|
||||
datasourceMap: DatasourceMap;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
datasourceStates: DatasourceStates;
|
||||
visualizationMap: VisualizationMap;
|
||||
activeVisualizationId: string | null;
|
||||
subVisualizationId?: string;
|
||||
|
@ -143,13 +143,7 @@ export function getVisualizeFieldSuggestions({
|
|||
visualizeTriggerFieldContext,
|
||||
}: {
|
||||
datasourceMap: DatasourceMap;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
datasourceStates: DatasourceStates;
|
||||
visualizationMap: VisualizationMap;
|
||||
activeVisualizationId: string | null;
|
||||
subVisualizationId?: string;
|
||||
|
@ -228,11 +222,10 @@ export function switchToSuggestion(
|
|||
|
||||
export function getTopSuggestionForField(
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>,
|
||||
activeVisualizationId: string | null,
|
||||
visualization: VisualizationState,
|
||||
datasourceStates: DatasourceStates,
|
||||
visualizationMap: Record<string, Visualization<unknown>>,
|
||||
visualizationState: unknown,
|
||||
datasource: Datasource,
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>,
|
||||
field: DragDropIdentifier
|
||||
) {
|
||||
const hasData = Object.values(datasourceLayers).some(
|
||||
|
@ -240,20 +233,20 @@ export function getTopSuggestionForField(
|
|||
);
|
||||
|
||||
const mainPalette =
|
||||
activeVisualizationId && visualizationMap[activeVisualizationId]?.getMainPalette
|
||||
? visualizationMap[activeVisualizationId].getMainPalette?.(visualizationState)
|
||||
visualization.activeId && visualizationMap[visualization.activeId]?.getMainPalette
|
||||
? visualizationMap[visualization.activeId].getMainPalette?.(visualization.state)
|
||||
: undefined;
|
||||
const suggestions = getSuggestions({
|
||||
datasourceMap: { [datasource.id]: datasource },
|
||||
datasourceStates,
|
||||
visualizationMap:
|
||||
hasData && activeVisualizationId
|
||||
? { [activeVisualizationId]: visualizationMap[activeVisualizationId] }
|
||||
hasData && visualization.activeId
|
||||
? { [visualization.activeId]: visualizationMap[visualization.activeId] }
|
||||
: visualizationMap,
|
||||
activeVisualizationId,
|
||||
visualizationState,
|
||||
activeVisualizationId: visualization.activeId,
|
||||
visualizationState: visualization.state,
|
||||
field,
|
||||
mainPalette,
|
||||
});
|
||||
return suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0];
|
||||
return suggestions.find((s) => s.visualizationId === visualization.activeId) || suggestions[0];
|
||||
}
|
||||
|
|
|
@ -16,12 +16,12 @@ import {
|
|||
} from '../../mocks';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
|
||||
import { esFilters, IFieldType, IndexPattern } from '../../../../../../src/plugins/data/public';
|
||||
import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel';
|
||||
import { SuggestionPanel, SuggestionPanelProps, SuggestionPanelWrapper } from './suggestion_panel';
|
||||
import { getSuggestions, Suggestion } from './suggestion_helpers';
|
||||
import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui';
|
||||
import { LensIconChartDatatable } from '../../assets/chart_datatable';
|
||||
import { mountWithProvider } from '../../mocks';
|
||||
import { LensAppState, PreviewState, setState, setToggleFullscreen } from '../../state_management';
|
||||
|
||||
jest.mock('./suggestion_helpers');
|
||||
|
||||
|
@ -38,6 +38,8 @@ describe('suggestion_panel', () => {
|
|||
|
||||
let defaultProps: SuggestionPanelProps;
|
||||
|
||||
let preloadedState: Partial<LensAppState>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockVisualization = createMockVisualization();
|
||||
mockDatasource = createMockDatasource('a');
|
||||
|
@ -49,7 +51,7 @@ describe('suggestion_panel', () => {
|
|||
previewIcon: 'empty',
|
||||
score: 0.5,
|
||||
visualizationState: suggestion1State,
|
||||
visualizationId: 'vis',
|
||||
visualizationId: 'testVis',
|
||||
title: 'Suggestion1',
|
||||
keptLayerIds: ['a'],
|
||||
},
|
||||
|
@ -58,36 +60,58 @@ describe('suggestion_panel', () => {
|
|||
previewIcon: 'empty',
|
||||
score: 0.5,
|
||||
visualizationState: suggestion2State,
|
||||
visualizationId: 'vis',
|
||||
visualizationId: 'testVis',
|
||||
title: 'Suggestion2',
|
||||
keptLayerIds: ['a'],
|
||||
},
|
||||
] as Suggestion[]);
|
||||
|
||||
defaultProps = {
|
||||
activeDatasourceId: 'mock',
|
||||
datasourceMap: {
|
||||
mock: mockDatasource,
|
||||
},
|
||||
preloadedState = {
|
||||
datasourceStates: {
|
||||
mock: {
|
||||
testDatasource: {
|
||||
isLoading: false,
|
||||
state: {},
|
||||
state: '',
|
||||
},
|
||||
},
|
||||
activeVisualizationId: 'vis',
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
},
|
||||
activeDatasourceId: 'testDatasource',
|
||||
};
|
||||
|
||||
defaultProps = {
|
||||
datasourceMap: {
|
||||
testDatasource: mockDatasource,
|
||||
},
|
||||
visualizationMap: {
|
||||
vis: mockVisualization,
|
||||
testVis: mockVisualization,
|
||||
vis2: createMockVisualization(),
|
||||
},
|
||||
visualizationState: {},
|
||||
ExpressionRenderer: expressionRendererMock,
|
||||
frame: createMockFramePublicAPI(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should avoid completely to render SuggestionPanel when in fullscreen mode', async () => {
|
||||
const { instance, lensStore } = await mountWithProvider(
|
||||
<SuggestionPanelWrapper {...defaultProps} />
|
||||
);
|
||||
expect(instance.find(SuggestionPanel).exists()).toBe(true);
|
||||
|
||||
lensStore.dispatch(setToggleFullscreen());
|
||||
instance.update();
|
||||
expect(instance.find(SuggestionPanel).exists()).toBe(false);
|
||||
|
||||
lensStore.dispatch(setToggleFullscreen());
|
||||
instance.update();
|
||||
expect(instance.find(SuggestionPanel).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should list passed in suggestions', async () => {
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />, {
|
||||
preloadedState,
|
||||
});
|
||||
|
||||
expect(
|
||||
instance
|
||||
|
@ -98,62 +122,56 @@ describe('suggestion_panel', () => {
|
|||
});
|
||||
|
||||
describe('uncommitted suggestions', () => {
|
||||
let suggestionState: Pick<
|
||||
SuggestionPanelProps,
|
||||
'datasourceStates' | 'activeVisualizationId' | 'visualizationState'
|
||||
>;
|
||||
let stagedPreview: SuggestionPanelProps['stagedPreview'];
|
||||
let suggestionState: Pick<LensAppState, 'datasourceStates' | 'visualization'>;
|
||||
let stagedPreview: PreviewState;
|
||||
beforeEach(() => {
|
||||
suggestionState = {
|
||||
datasourceStates: {
|
||||
mock: {
|
||||
testDatasource: {
|
||||
isLoading: false,
|
||||
state: {},
|
||||
state: '',
|
||||
},
|
||||
},
|
||||
activeVisualizationId: 'vis2',
|
||||
visualizationState: {},
|
||||
visualization: {
|
||||
activeId: 'vis2',
|
||||
state: {},
|
||||
},
|
||||
};
|
||||
|
||||
stagedPreview = {
|
||||
datasourceStates: defaultProps.datasourceStates,
|
||||
visualization: {
|
||||
state: defaultProps.visualizationState,
|
||||
activeId: defaultProps.activeVisualizationId,
|
||||
},
|
||||
datasourceStates: preloadedState.datasourceStates!,
|
||||
visualization: preloadedState.visualization!,
|
||||
};
|
||||
});
|
||||
|
||||
it('should not update suggestions if current state is moved to staged preview', async () => {
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
const { instance, lensStore } = await mountWithProvider(
|
||||
<SuggestionPanel {...defaultProps} />,
|
||||
{ preloadedState }
|
||||
);
|
||||
getSuggestionsMock.mockClear();
|
||||
instance.setProps({
|
||||
stagedPreview,
|
||||
...suggestionState,
|
||||
});
|
||||
lensStore.dispatch(setState({ stagedPreview }));
|
||||
instance.update();
|
||||
expect(getSuggestionsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update suggestions if staged preview is removed', async () => {
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
const { instance, lensStore } = await mountWithProvider(
|
||||
<SuggestionPanel {...defaultProps} />,
|
||||
{ preloadedState }
|
||||
);
|
||||
getSuggestionsMock.mockClear();
|
||||
instance.setProps({
|
||||
stagedPreview,
|
||||
...suggestionState,
|
||||
});
|
||||
lensStore.dispatch(setState({ stagedPreview, ...suggestionState }));
|
||||
instance.update();
|
||||
instance.setProps({
|
||||
stagedPreview: undefined,
|
||||
...suggestionState,
|
||||
});
|
||||
lensStore.dispatch(setState({ stagedPreview: undefined, ...suggestionState }));
|
||||
instance.update();
|
||||
expect(getSuggestionsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should highlight currently active suggestion', async () => {
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />, {
|
||||
preloadedState,
|
||||
});
|
||||
act(() => {
|
||||
instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
|
||||
});
|
||||
|
@ -189,12 +207,14 @@ describe('suggestion_panel', () => {
|
|||
});
|
||||
|
||||
it('should dispatch visualization switch action if suggestion is clicked', async () => {
|
||||
const { instance, lensStore } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
const { instance, lensStore } = await mountWithProvider(<SuggestionPanel {...defaultProps} />, {
|
||||
preloadedState,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
instance.find('button[data-test-subj="lnsSuggestion"]').at(1).simulate('click');
|
||||
});
|
||||
instance.update();
|
||||
// instance.update();
|
||||
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -203,7 +223,7 @@ describe('suggestion_panel', () => {
|
|||
datasourceId: undefined,
|
||||
datasourceState: {},
|
||||
initialState: { suggestion1: true },
|
||||
newVisualizationId: 'vis',
|
||||
newVisualizationId: 'testVis',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -217,7 +237,7 @@ describe('suggestion_panel', () => {
|
|||
previewIcon: LensIconChartDatatable,
|
||||
score: 0.5,
|
||||
visualizationState: suggestion1State,
|
||||
visualizationId: 'vis',
|
||||
visualizationId: 'testVis',
|
||||
title: 'Suggestion1',
|
||||
},
|
||||
{
|
||||
|
@ -225,7 +245,7 @@ describe('suggestion_panel', () => {
|
|||
previewIcon: 'empty',
|
||||
score: 0.5,
|
||||
visualizationState: suggestion2State,
|
||||
visualizationId: 'vis',
|
||||
visualizationId: 'testVis',
|
||||
title: 'Suggestion2',
|
||||
previewExpression: 'test | expression',
|
||||
},
|
||||
|
@ -239,7 +259,9 @@ describe('suggestion_panel', () => {
|
|||
|
||||
mockDatasource.toExpression.mockReturnValue('datasource_expression');
|
||||
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />, {
|
||||
preloadedState,
|
||||
});
|
||||
|
||||
expect(instance.find(EuiIcon)).toHaveLength(1);
|
||||
expect(instance.find(EuiIcon).prop('type')).toEqual(LensIconChartDatatable);
|
||||
|
@ -252,17 +274,20 @@ describe('suggestion_panel', () => {
|
|||
indexPatterns: {},
|
||||
};
|
||||
mockDatasource.checkIntegrity.mockReturnValue(['a']);
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
|
||||
const newPreloadedState = {
|
||||
...preloadedState,
|
||||
datasourceStates: {
|
||||
mock: {
|
||||
...defaultProps.datasourceStates.mock,
|
||||
testDatasource: {
|
||||
...preloadedState.datasourceStates!.testDatasource,
|
||||
state: missingIndexPatternsState,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...newProps} />);
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />, {
|
||||
preloadedState: newPreloadedState,
|
||||
});
|
||||
expect(instance.html()).toEqual(null);
|
||||
});
|
||||
|
||||
|
@ -274,7 +299,7 @@ describe('suggestion_panel', () => {
|
|||
previewIcon: 'empty',
|
||||
score: 0.5,
|
||||
visualizationState: suggestion1State,
|
||||
visualizationId: 'vis',
|
||||
visualizationId: 'testVis',
|
||||
title: 'Suggestion1',
|
||||
},
|
||||
{
|
||||
|
@ -282,7 +307,7 @@ describe('suggestion_panel', () => {
|
|||
previewIcon: 'empty',
|
||||
score: 0.5,
|
||||
visualizationState: suggestion2State,
|
||||
visualizationId: 'vis',
|
||||
visualizationId: 'testVis',
|
||||
title: 'Suggestion2',
|
||||
},
|
||||
] as Suggestion[]);
|
||||
|
@ -291,18 +316,7 @@ describe('suggestion_panel', () => {
|
|||
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression');
|
||||
mockDatasource.toExpression.mockReturnValue('datasource_expression');
|
||||
|
||||
const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern;
|
||||
const field = ({ name: 'myfield' } as unknown) as IFieldType;
|
||||
|
||||
mountWithProvider(
|
||||
<SuggestionPanel
|
||||
{...defaultProps}
|
||||
frame={{
|
||||
...createMockFramePublicAPI(),
|
||||
filters: [esFilters.buildExistsFilter(field, indexPattern)],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
mountWithProvider(<SuggestionPanel {...defaultProps} frame={createMockFramePublicAPI()} />);
|
||||
|
||||
expect(expressionRendererMock).toHaveBeenCalledTimes(1);
|
||||
const passedExpression = (expressionRendererMock as jest.Mock).mock.calls[0][0].expression;
|
||||
|
|
|
@ -42,30 +42,28 @@ import { prependDatasourceExpression } from './expression_helpers';
|
|||
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';
|
||||
import { getMissingIndexPattern, validateDatasourceAndVisualization } from './state_helpers';
|
||||
import {
|
||||
PreviewState,
|
||||
rollbackSuggestion,
|
||||
selectExecutionContextSearch,
|
||||
submitSuggestion,
|
||||
useLensDispatch,
|
||||
useLensSelector,
|
||||
selectCurrentVisualization,
|
||||
selectCurrentDatasourceStates,
|
||||
DatasourceStates,
|
||||
selectIsFullscreenDatasource,
|
||||
selectSearchSessionId,
|
||||
selectActiveDatasourceId,
|
||||
selectActiveData,
|
||||
selectDatasourceStates,
|
||||
} from '../../state_management';
|
||||
|
||||
const MAX_SUGGESTIONS_DISPLAYED = 5;
|
||||
|
||||
export interface SuggestionPanelProps {
|
||||
activeDatasourceId: string | null;
|
||||
datasourceMap: DatasourceMap;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
activeVisualizationId: string | null;
|
||||
visualizationMap: VisualizationMap;
|
||||
visualizationState: unknown;
|
||||
ExpressionRenderer: ReactExpressionRendererType;
|
||||
frame: FramePublicAPI;
|
||||
stagedPreview?: PreviewState;
|
||||
}
|
||||
|
||||
const PreviewRenderer = ({
|
||||
|
@ -173,128 +171,114 @@ const SuggestionPreview = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const SuggestionPanelWrapper = (props: SuggestionPanelProps) => {
|
||||
const isFullscreenDatasource = useLensSelector(selectIsFullscreenDatasource);
|
||||
return isFullscreenDatasource ? null : <SuggestionPanel {...props} />;
|
||||
};
|
||||
|
||||
export function SuggestionPanel({
|
||||
activeDatasourceId,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
activeVisualizationId,
|
||||
visualizationMap,
|
||||
visualizationState,
|
||||
frame,
|
||||
ExpressionRenderer: ExpressionRendererComponent,
|
||||
stagedPreview,
|
||||
}: SuggestionPanelProps) {
|
||||
const dispatchLens = useLensDispatch();
|
||||
|
||||
const currentDatasourceStates = stagedPreview ? stagedPreview.datasourceStates : datasourceStates;
|
||||
const currentVisualizationState = stagedPreview
|
||||
? stagedPreview.visualization.state
|
||||
: visualizationState;
|
||||
const currentVisualizationId = stagedPreview
|
||||
? stagedPreview.visualization.activeId
|
||||
: activeVisualizationId;
|
||||
const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
|
||||
const activeData = useLensSelector(selectActiveData);
|
||||
const datasourceStates = useLensSelector(selectDatasourceStates);
|
||||
const existsStagedPreview = useLensSelector((state) => Boolean(state.lens.stagedPreview));
|
||||
const currentVisualization = useLensSelector(selectCurrentVisualization);
|
||||
const currentDatasourceStates = useLensSelector(selectCurrentDatasourceStates);
|
||||
|
||||
const missingIndexPatterns = getMissingIndexPattern(
|
||||
activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
|
||||
activeDatasourceId ? datasourceStates[activeDatasourceId] : null
|
||||
);
|
||||
const { suggestions, currentStateExpression, currentStateError } = useMemo(
|
||||
() => {
|
||||
const newSuggestions = missingIndexPatterns.length
|
||||
? []
|
||||
: getSuggestions({
|
||||
datasourceMap,
|
||||
datasourceStates: currentDatasourceStates,
|
||||
visualizationMap,
|
||||
activeVisualizationId: currentVisualizationId,
|
||||
visualizationState: currentVisualizationState,
|
||||
activeData: frame.activeData,
|
||||
})
|
||||
.filter(
|
||||
({
|
||||
hide,
|
||||
visualizationId,
|
||||
visualizationState: suggestionVisualizationState,
|
||||
datasourceState: suggestionDatasourceState,
|
||||
datasourceId: suggetionDatasourceId,
|
||||
}) => {
|
||||
return (
|
||||
!hide &&
|
||||
validateDatasourceAndVisualization(
|
||||
suggetionDatasourceId ? datasourceMap[suggetionDatasourceId] : null,
|
||||
suggestionDatasourceState,
|
||||
visualizationMap[visualizationId],
|
||||
suggestionVisualizationState,
|
||||
frame
|
||||
) == null
|
||||
);
|
||||
}
|
||||
)
|
||||
.slice(0, MAX_SUGGESTIONS_DISPLAYED)
|
||||
.map((suggestion) => ({
|
||||
...suggestion,
|
||||
previewExpression: preparePreviewExpression(
|
||||
suggestion,
|
||||
visualizationMap[suggestion.visualizationId],
|
||||
datasourceMap,
|
||||
currentDatasourceStates,
|
||||
frame
|
||||
),
|
||||
}));
|
||||
|
||||
const validationErrors = validateDatasourceAndVisualization(
|
||||
activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
|
||||
activeDatasourceId && currentDatasourceStates[activeDatasourceId]?.state,
|
||||
currentVisualizationId ? visualizationMap[currentVisualizationId] : null,
|
||||
currentVisualizationState,
|
||||
frame
|
||||
);
|
||||
|
||||
const newStateExpression =
|
||||
currentVisualizationState && currentVisualizationId && !validationErrors
|
||||
? preparePreviewExpression(
|
||||
{ visualizationState: currentVisualizationState },
|
||||
visualizationMap[currentVisualizationId],
|
||||
const { suggestions, currentStateExpression, currentStateError } = useMemo(() => {
|
||||
const newSuggestions = missingIndexPatterns.length
|
||||
? []
|
||||
: getSuggestions({
|
||||
datasourceMap,
|
||||
datasourceStates: currentDatasourceStates,
|
||||
visualizationMap,
|
||||
activeVisualizationId: currentVisualization.activeId,
|
||||
visualizationState: currentVisualization.state,
|
||||
activeData,
|
||||
})
|
||||
.filter(
|
||||
({
|
||||
hide,
|
||||
visualizationId,
|
||||
visualizationState: suggestionVisualizationState,
|
||||
datasourceState: suggestionDatasourceState,
|
||||
datasourceId: suggetionDatasourceId,
|
||||
}) => {
|
||||
return (
|
||||
!hide &&
|
||||
validateDatasourceAndVisualization(
|
||||
suggetionDatasourceId ? datasourceMap[suggetionDatasourceId] : null,
|
||||
suggestionDatasourceState,
|
||||
visualizationMap[visualizationId],
|
||||
suggestionVisualizationState,
|
||||
frame
|
||||
) == null
|
||||
);
|
||||
}
|
||||
)
|
||||
.slice(0, MAX_SUGGESTIONS_DISPLAYED)
|
||||
.map((suggestion) => ({
|
||||
...suggestion,
|
||||
previewExpression: preparePreviewExpression(
|
||||
suggestion,
|
||||
visualizationMap[suggestion.visualizationId],
|
||||
datasourceMap,
|
||||
currentDatasourceStates,
|
||||
frame
|
||||
)
|
||||
: undefined;
|
||||
),
|
||||
}));
|
||||
|
||||
return {
|
||||
suggestions: newSuggestions,
|
||||
currentStateExpression: newStateExpression,
|
||||
currentStateError: validationErrors,
|
||||
};
|
||||
},
|
||||
const validationErrors = validateDatasourceAndVisualization(
|
||||
activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
|
||||
activeDatasourceId && currentDatasourceStates[activeDatasourceId]?.state,
|
||||
currentVisualization.activeId ? visualizationMap[currentVisualization.activeId] : null,
|
||||
currentVisualization.state,
|
||||
frame
|
||||
);
|
||||
|
||||
const newStateExpression =
|
||||
currentVisualization.state && currentVisualization.activeId && !validationErrors
|
||||
? preparePreviewExpression(
|
||||
{ visualizationState: currentVisualization.state },
|
||||
visualizationMap[currentVisualization.activeId],
|
||||
datasourceMap,
|
||||
currentDatasourceStates,
|
||||
frame
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
suggestions: newSuggestions,
|
||||
currentStateExpression: newStateExpression,
|
||||
currentStateError: validationErrors,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
currentDatasourceStates,
|
||||
currentVisualizationState,
|
||||
currentVisualizationId,
|
||||
activeDatasourceId,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
]
|
||||
);
|
||||
}, [
|
||||
currentDatasourceStates,
|
||||
currentVisualization.state,
|
||||
currentVisualization.activeId,
|
||||
activeDatasourceId,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
]);
|
||||
|
||||
const context: ExecutionContextSearch = useMemo(
|
||||
() => ({
|
||||
query: frame.query,
|
||||
timeRange: {
|
||||
from: frame.dateRange.fromDate,
|
||||
to: frame.dateRange.toDate,
|
||||
},
|
||||
filters: frame.filters,
|
||||
}),
|
||||
[frame.query, frame.dateRange.fromDate, frame.dateRange.toDate, frame.filters]
|
||||
);
|
||||
const context: ExecutionContextSearch = useLensSelector(selectExecutionContextSearch);
|
||||
const searchSessionId = useLensSelector(selectSearchSessionId);
|
||||
|
||||
const contextRef = useRef<ExecutionContextSearch>(context);
|
||||
contextRef.current = context;
|
||||
|
||||
const sessionIdRef = useRef<string>(frame.searchSessionId);
|
||||
sessionIdRef.current = frame.searchSessionId;
|
||||
const sessionIdRef = useRef<string>(searchSessionId);
|
||||
sessionIdRef.current = searchSessionId;
|
||||
|
||||
const AutoRefreshExpressionRenderer = useMemo(() => {
|
||||
return (props: ReactExpressionRendererProps) => (
|
||||
|
@ -312,11 +296,11 @@ export function SuggestionPanel({
|
|||
// if the staged preview is overwritten by a suggestion,
|
||||
// reset the selected index to "current visualization" because
|
||||
// we are not in transient suggestion state anymore
|
||||
if (!stagedPreview && lastSelectedSuggestion !== -1) {
|
||||
if (!existsStagedPreview && lastSelectedSuggestion !== -1) {
|
||||
setLastSelectedSuggestion(-1);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [stagedPreview]);
|
||||
}, [existsStagedPreview]);
|
||||
|
||||
if (!activeDatasourceId) {
|
||||
return null;
|
||||
|
@ -347,7 +331,7 @@ export function SuggestionPanel({
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{stagedPreview && (
|
||||
{existsStagedPreview && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.lens.suggestion.refreshSuggestionTooltip', {
|
||||
|
@ -373,14 +357,15 @@ export function SuggestionPanel({
|
|||
</EuiFlexGroup>
|
||||
|
||||
<div className="lnsSuggestionPanel__suggestions">
|
||||
{currentVisualizationId && (
|
||||
{currentVisualization.activeId && (
|
||||
<SuggestionPreview
|
||||
preview={{
|
||||
error: currentStateError != null,
|
||||
expression: currentStateExpression,
|
||||
icon:
|
||||
visualizationMap[currentVisualizationId].getDescription(currentVisualizationState)
|
||||
.icon || 'empty',
|
||||
visualizationMap[currentVisualization.activeId].getDescription(
|
||||
currentVisualization.state
|
||||
).icon || 'empty',
|
||||
title: i18n.translate('xpack.lens.suggestions.currentVisLabel', {
|
||||
defaultMessage: 'Current visualization',
|
||||
}),
|
||||
|
@ -436,10 +421,7 @@ function getPreviewExpression(
|
|||
return null;
|
||||
}
|
||||
|
||||
const suggestionFrameApi: FramePublicAPI = {
|
||||
...frame,
|
||||
datasourceLayers: { ...frame.datasourceLayers },
|
||||
};
|
||||
const suggestionFrameApi: FramePublicAPI = { datasourceLayers: { ...frame.datasourceLayers } };
|
||||
|
||||
// use current frame api and patch apis for changed datasource layers
|
||||
if (
|
||||
|
@ -473,8 +455,8 @@ function getPreviewExpression(
|
|||
function preparePreviewExpression(
|
||||
visualizableState: VisualizableState,
|
||||
visualization: Visualization,
|
||||
datasourceMap: Record<string, Datasource<unknown, unknown>>,
|
||||
datasourceStates: Record<string, { isLoading: boolean; state: unknown }>,
|
||||
datasourceMap: DatasourceMap,
|
||||
datasourceStates: DatasourceStates,
|
||||
framePublicAPI: FramePublicAPI
|
||||
) {
|
||||
const suggestionDatasourceId = visualizableState.datasourceId;
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
createMockVisualization,
|
||||
createMockFramePublicAPI,
|
||||
createMockDatasource,
|
||||
mockDatasourceStates,
|
||||
} from '../../../mocks';
|
||||
import { mountWithProvider } from '../../../mocks';
|
||||
|
||||
|
@ -163,15 +164,6 @@ describe('chart_switch', () => {
|
|||
};
|
||||
}
|
||||
|
||||
function mockDatasourceStates() {
|
||||
return {
|
||||
testDatasource: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function showFlyout(instance: ReactWrapper) {
|
||||
instance.find('[data-test-subj="lnsChartSwitchPopover"]').first().simulate('click');
|
||||
}
|
||||
|
|
|
@ -35,6 +35,11 @@ import {
|
|||
updateVisualizationState,
|
||||
useLensDispatch,
|
||||
useLensSelector,
|
||||
VisualizationState,
|
||||
DatasourceStates,
|
||||
selectActiveDatasourceId,
|
||||
selectVisualization,
|
||||
selectDatasourceStates,
|
||||
} from '../../../state_management';
|
||||
import { generateId } from '../../../id_generator/id_generator';
|
||||
|
||||
|
@ -111,9 +116,9 @@ function getCurrentVisualizationId(
|
|||
export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
||||
const [flyoutOpen, setFlyoutOpen] = useState<boolean>(false);
|
||||
const dispatchLens = useLensDispatch();
|
||||
const activeDatasourceId = useLensSelector((state) => state.lens.activeDatasourceId);
|
||||
const visualization = useLensSelector((state) => state.lens.visualization);
|
||||
const datasourceStates = useLensSelector((state) => state.lens.datasourceStates);
|
||||
const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
|
||||
const visualization = useLensSelector(selectVisualization);
|
||||
const datasourceStates = useLensSelector(selectDatasourceStates);
|
||||
|
||||
function removeLayers(layerIds: string[]) {
|
||||
const activeVisualization =
|
||||
|
@ -498,11 +503,8 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
function getTopSuggestion(
|
||||
props: Props,
|
||||
visualizationId: string,
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>,
|
||||
visualization: {
|
||||
activeId: string | null;
|
||||
state: unknown;
|
||||
},
|
||||
datasourceStates: DatasourceStates,
|
||||
visualization: VisualizationState,
|
||||
newVisualization: Visualization<unknown>,
|
||||
subVisualizationId?: string
|
||||
): Suggestion | undefined {
|
||||
|
|
|
@ -33,6 +33,7 @@ import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/publ
|
|||
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';
|
||||
import { LensRootStore, setState } from '../../../state_management';
|
||||
|
||||
const defaultPermissions: Record<string, Record<string, boolean | Record<string, boolean>>> = {
|
||||
navLinks: { management: true },
|
||||
|
@ -49,12 +50,8 @@ function createCoreStartWithPermissions(newCapabilities = defaultPermissions) {
|
|||
}
|
||||
|
||||
const defaultProps = {
|
||||
activeDatasourceId: 'mock',
|
||||
datasourceStates: {},
|
||||
datasourceMap: {},
|
||||
framePublicAPI: createMockFramePublicAPI(),
|
||||
activeVisualizationId: 'vis',
|
||||
visualizationState: {},
|
||||
ExpressionRenderer: createExpressionRendererMock(),
|
||||
core: createCoreStartWithPermissions(),
|
||||
plugins: {
|
||||
|
@ -62,7 +59,6 @@ const defaultProps = {
|
|||
data: mockDataPlugin(),
|
||||
},
|
||||
getSuggestionForField: () => undefined,
|
||||
isFullscreen: false,
|
||||
toggleFullscreen: jest.fn(),
|
||||
};
|
||||
|
||||
|
@ -84,7 +80,7 @@ describe('workspace_panel', () => {
|
|||
uiActionsMock.getTrigger.mockReturnValue(trigger);
|
||||
mockVisualization = createMockVisualization();
|
||||
mockVisualization2 = createMockVisualization();
|
||||
mockDatasource = createMockDatasource('a');
|
||||
mockDatasource = createMockDatasource('testDatasource');
|
||||
expressionRendererMock = createExpressionRendererMock();
|
||||
});
|
||||
|
||||
|
@ -96,14 +92,16 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
activeVisualizationId={null}
|
||||
visualizationMap={{
|
||||
vis: mockVisualization,
|
||||
testVis: mockVisualization,
|
||||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
{
|
||||
data: defaultProps.plugins.data,
|
||||
preloadedState: { visualization: { activeId: null, state: {} }, datasourceStates: {} },
|
||||
}
|
||||
);
|
||||
instance = mounted.instance;
|
||||
expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2);
|
||||
|
@ -115,11 +113,11 @@ describe('workspace_panel', () => {
|
|||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => null },
|
||||
testVis: { ...mockVisualization, toExpression: () => null },
|
||||
}}
|
||||
/>,
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
{ data: defaultProps.plugins.data, preloadedState: { datasourceStates: {} } }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -132,11 +130,11 @@ describe('workspace_panel', () => {
|
|||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
/>,
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
{ data: defaultProps.plugins.data, preloadedState: { datasourceStates: {} } }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -155,18 +153,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
|
@ -178,7 +170,7 @@ describe('workspace_panel', () => {
|
|||
expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(`
|
||||
"kibana
|
||||
| lens_merge_tables layerIds=\\"first\\" tables={datasource}
|
||||
| vis"
|
||||
| testVis"
|
||||
`);
|
||||
});
|
||||
|
||||
|
@ -194,18 +186,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...props}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
plugins={{ ...props.plugins, uiActions: uiActionsMock }}
|
||||
|
@ -235,18 +221,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
|
@ -283,28 +263,32 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
mock2: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
mock2: mockDatasource2,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
{
|
||||
data: defaultProps.plugins.data,
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
mock2: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -364,18 +348,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
|
@ -418,18 +396,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
|
@ -470,23 +442,27 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
// define a layer with an indexpattern not available
|
||||
state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
/>,
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
{
|
||||
data: defaultProps.plugins.data,
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
// define a layer with an indexpattern not available
|
||||
state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -504,20 +480,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
// define a layer with an indexpattern not available
|
||||
state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
activeVisualizationId="vis"
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
// Use cannot navigate to the management page
|
||||
core={createCoreStartWithPermissions({
|
||||
|
@ -526,7 +494,18 @@ describe('workspace_panel', () => {
|
|||
})}
|
||||
/>,
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
{
|
||||
data: defaultProps.plugins.data,
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
// define a layer with an indexpattern not available
|
||||
state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -545,19 +524,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
// define a layer with an indexpattern not available
|
||||
state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
// user can go to management, but indexPatterns management is not accessible
|
||||
core={createCoreStartWithPermissions({
|
||||
|
@ -566,7 +538,18 @@ describe('workspace_panel', () => {
|
|||
})}
|
||||
/>,
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
{
|
||||
data: defaultProps.plugins.data,
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
// define a layer with an indexpattern not available
|
||||
state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -588,18 +571,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
/>,
|
||||
|
||||
|
@ -617,7 +594,7 @@ describe('workspace_panel', () => {
|
|||
mockVisualization.getErrorMessages.mockReturnValue([
|
||||
{ shortMessage: 'Some error happened', longMessage: 'Some long description happened' },
|
||||
]);
|
||||
mockVisualization.toExpression.mockReturnValue('vis');
|
||||
mockVisualization.toExpression.mockReturnValue('testVis');
|
||||
const framePublicAPI = createMockFramePublicAPI();
|
||||
framePublicAPI.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
|
@ -626,18 +603,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: mockVisualization,
|
||||
testVis: mockVisualization,
|
||||
}}
|
||||
/>,
|
||||
|
||||
|
@ -657,7 +628,7 @@ describe('workspace_panel', () => {
|
|||
mockVisualization.getErrorMessages.mockReturnValue([
|
||||
{ shortMessage: 'Some error happened', longMessage: 'Some long description happened' },
|
||||
]);
|
||||
mockVisualization.toExpression.mockReturnValue('vis');
|
||||
mockVisualization.toExpression.mockReturnValue('testVis');
|
||||
const framePublicAPI = createMockFramePublicAPI();
|
||||
framePublicAPI.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
|
@ -666,18 +637,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: mockVisualization,
|
||||
testVis: mockVisualization,
|
||||
}}
|
||||
/>,
|
||||
|
||||
|
@ -703,18 +668,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
/>,
|
||||
|
||||
|
@ -738,18 +697,12 @@ describe('workspace_panel', () => {
|
|||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
|
@ -775,23 +728,17 @@ describe('workspace_panel', () => {
|
|||
framePublicAPI.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
};
|
||||
|
||||
let lensStore: LensRootStore;
|
||||
await act(async () => {
|
||||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
|
@ -799,6 +746,7 @@ describe('workspace_panel', () => {
|
|||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
lensStore = mounted.lensStore;
|
||||
});
|
||||
|
||||
instance.update();
|
||||
|
@ -809,7 +757,14 @@ describe('workspace_panel', () => {
|
|||
return <span />;
|
||||
});
|
||||
|
||||
instance.setProps({ visualizationState: {} });
|
||||
lensStore!.dispatch(
|
||||
setState({
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
},
|
||||
})
|
||||
);
|
||||
instance.update();
|
||||
|
||||
expect(expressionRendererMock).toHaveBeenCalledTimes(2);
|
||||
|
@ -843,18 +798,12 @@ describe('workspace_panel', () => {
|
|||
>
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={frame}
|
||||
visualizationMap={{
|
||||
vis: mockVisualization,
|
||||
testVis: mockVisualization,
|
||||
vis2: mockVisualization2,
|
||||
}}
|
||||
getSuggestionForField={mockGetSuggestionForField}
|
||||
|
@ -867,9 +816,9 @@ describe('workspace_panel', () => {
|
|||
|
||||
it('should immediately transition if exactly one suggestion is returned', async () => {
|
||||
mockGetSuggestionForField.mockReturnValue({
|
||||
visualizationId: 'vis',
|
||||
visualizationId: 'testVis',
|
||||
datasourceState: {},
|
||||
datasourceId: 'mock',
|
||||
datasourceId: 'testDatasource',
|
||||
visualizationState: {},
|
||||
});
|
||||
const { lensStore } = await initComponent();
|
||||
|
@ -879,19 +828,19 @@ describe('workspace_panel', () => {
|
|||
expect(lensStore.dispatch).toHaveBeenCalledWith({
|
||||
type: 'lens/switchVisualization',
|
||||
payload: {
|
||||
newVisualizationId: 'vis',
|
||||
newVisualizationId: 'testVis',
|
||||
initialState: {},
|
||||
datasourceState: {},
|
||||
datasourceId: 'mock',
|
||||
datasourceId: 'testDatasource',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to drop if there are suggestions', async () => {
|
||||
mockGetSuggestionForField.mockReturnValue({
|
||||
visualizationId: 'vis',
|
||||
visualizationId: 'testVis',
|
||||
datasourceState: {},
|
||||
datasourceId: 'mock',
|
||||
datasourceId: 'testDatasource',
|
||||
visualizationState: {},
|
||||
});
|
||||
await initComponent();
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
isLensEditEvent,
|
||||
VisualizationMap,
|
||||
DatasourceMap,
|
||||
DatasourceFixAction,
|
||||
} from '../../../types';
|
||||
import { DragDrop, DragContext, DragDropIdentifier } from '../../../drag_drop';
|
||||
import { Suggestion, switchToSuggestion } from '../suggestion_helpers';
|
||||
|
@ -58,34 +59,30 @@ import {
|
|||
updateVisualizationState,
|
||||
updateDatasourceState,
|
||||
setSaveable,
|
||||
useLensSelector,
|
||||
selectExecutionContext,
|
||||
selectIsFullscreenDatasource,
|
||||
selectVisualization,
|
||||
selectDatasourceStates,
|
||||
selectActiveDatasourceId,
|
||||
selectSearchSessionId,
|
||||
} from '../../../state_management';
|
||||
|
||||
export interface WorkspacePanelProps {
|
||||
activeVisualizationId: string | null;
|
||||
visualizationMap: VisualizationMap;
|
||||
visualizationState: unknown;
|
||||
activeDatasourceId: string | null;
|
||||
datasourceMap: DatasourceMap;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
state: unknown;
|
||||
isLoading: boolean;
|
||||
}
|
||||
>;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
ExpressionRenderer: ReactExpressionRendererType;
|
||||
core: CoreStart;
|
||||
plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart };
|
||||
getSuggestionForField: (field: DragDropIdentifier) => Suggestion | undefined;
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
interface WorkspaceState {
|
||||
expressionBuildError?: Array<{
|
||||
shortMessage: string;
|
||||
longMessage: string;
|
||||
fixAction?: { label: string; newState: (framePublicAPI: FramePublicAPI) => Promise<unknown> };
|
||||
fixAction?: DatasourceFixAction<unknown>;
|
||||
}>;
|
||||
expandError: boolean;
|
||||
}
|
||||
|
@ -120,29 +117,30 @@ export const WorkspacePanel = React.memo(function WorkspacePanel(props: Workspac
|
|||
|
||||
// Exported for testing purposes only.
|
||||
export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
||||
activeDatasourceId,
|
||||
activeVisualizationId,
|
||||
visualizationMap,
|
||||
visualizationState,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
framePublicAPI,
|
||||
visualizationMap,
|
||||
datasourceMap,
|
||||
core,
|
||||
plugins,
|
||||
ExpressionRenderer: ExpressionRendererComponent,
|
||||
suggestionForDraggedField,
|
||||
isFullscreen,
|
||||
}: Omit<WorkspacePanelProps, 'getSuggestionForField'> & {
|
||||
suggestionForDraggedField: Suggestion | undefined;
|
||||
}) {
|
||||
const dispatchLens = useLensDispatch();
|
||||
const isFullscreen = useLensSelector(selectIsFullscreenDatasource);
|
||||
const visualization = useLensSelector(selectVisualization);
|
||||
const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
|
||||
const datasourceStates = useLensSelector(selectDatasourceStates);
|
||||
|
||||
const { datasourceLayers } = framePublicAPI;
|
||||
const [localState, setLocalState] = useState<WorkspaceState>({
|
||||
expressionBuildError: undefined,
|
||||
expandError: false,
|
||||
});
|
||||
|
||||
const activeVisualization = activeVisualizationId
|
||||
? visualizationMap[activeVisualizationId]
|
||||
const activeVisualization = visualization.activeId
|
||||
? visualizationMap[visualization.activeId]
|
||||
: null;
|
||||
|
||||
const missingIndexPatterns = getMissingIndexPattern(
|
||||
|
@ -175,64 +173,60 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
|
||||
activeDatasourceId && datasourceStates[activeDatasourceId]?.state,
|
||||
activeVisualization,
|
||||
visualizationState,
|
||||
visualization.state,
|
||||
framePublicAPI
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[activeVisualization, visualizationState, activeDatasourceId, datasourceMap, datasourceStates]
|
||||
[activeVisualization, visualization.state, activeDatasourceId, datasourceMap, datasourceStates]
|
||||
);
|
||||
|
||||
const expression = useMemo(
|
||||
() => {
|
||||
if (!configurationValidationError?.length && !missingRefsErrors.length) {
|
||||
try {
|
||||
const ast = buildExpression({
|
||||
visualization: activeVisualization,
|
||||
visualizationState,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
datasourceLayers: framePublicAPI.datasourceLayers,
|
||||
});
|
||||
const expression = useMemo(() => {
|
||||
if (!configurationValidationError?.length && !missingRefsErrors.length) {
|
||||
try {
|
||||
const ast = buildExpression({
|
||||
visualization: activeVisualization,
|
||||
visualizationState: visualization.state,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
datasourceLayers,
|
||||
});
|
||||
|
||||
if (ast) {
|
||||
// expression has to be turned into a string for dirty checking - if the ast is rebuilt,
|
||||
// turning it into a string will make sure the expression renderer only re-renders if the
|
||||
// expression actually changed.
|
||||
return toExpression(ast);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
const buildMessages = activeVisualization?.getErrorMessages(visualizationState);
|
||||
const defaultMessage = {
|
||||
shortMessage: i18n.translate('xpack.lens.editorFrame.buildExpressionError', {
|
||||
defaultMessage: 'An unexpected error occurred while preparing the chart',
|
||||
}),
|
||||
longMessage: e.toString(),
|
||||
};
|
||||
// Most likely an error in the expression provided by a datasource or visualization
|
||||
setLocalState((s) => ({
|
||||
...s,
|
||||
expressionBuildError: buildMessages ?? [defaultMessage],
|
||||
}));
|
||||
if (ast) {
|
||||
// expression has to be turned into a string for dirty checking - if the ast is rebuilt,
|
||||
// turning it into a string will make sure the expression renderer only re-renders if the
|
||||
// expression actually changed.
|
||||
return toExpression(ast);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
const buildMessages = activeVisualization?.getErrorMessages(visualization.state);
|
||||
const defaultMessage = {
|
||||
shortMessage: i18n.translate('xpack.lens.editorFrame.buildExpressionError', {
|
||||
defaultMessage: 'An unexpected error occurred while preparing the chart',
|
||||
}),
|
||||
longMessage: e.toString(),
|
||||
};
|
||||
// Most likely an error in the expression provided by a datasource or visualization
|
||||
setLocalState((s) => ({
|
||||
...s,
|
||||
expressionBuildError: buildMessages ?? [defaultMessage],
|
||||
}));
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
activeVisualization,
|
||||
visualizationState,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
framePublicAPI.dateRange,
|
||||
framePublicAPI.query,
|
||||
framePublicAPI.filters,
|
||||
]
|
||||
);
|
||||
}
|
||||
}, [
|
||||
activeVisualization,
|
||||
visualization.state,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
datasourceLayers,
|
||||
configurationValidationError?.length,
|
||||
missingRefsErrors.length,
|
||||
]);
|
||||
|
||||
const expressionExists = Boolean(expression);
|
||||
const hasLoaded = Boolean(
|
||||
activeVisualization && visualizationState && datasourceMap && datasourceStates
|
||||
activeVisualization && visualization.state && datasourceMap && datasourceStates
|
||||
);
|
||||
useEffect(() => {
|
||||
if (hasLoaded) {
|
||||
|
@ -392,8 +386,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
return (
|
||||
<WorkspacePanelWrapper
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationState={visualizationState}
|
||||
visualizationId={activeVisualizationId}
|
||||
visualizationState={visualization.state}
|
||||
visualizationId={visualization.activeId}
|
||||
datasourceStates={datasourceStates}
|
||||
datasourceMap={datasourceMap}
|
||||
visualizationMap={visualizationMap}
|
||||
|
@ -424,7 +418,7 @@ export const VisualizationWrapper = ({
|
|||
configurationValidationError?: Array<{
|
||||
shortMessage: string;
|
||||
longMessage: string;
|
||||
fixAction?: { label: string; newState: (framePublicAPI: FramePublicAPI) => Promise<unknown> };
|
||||
fixAction?: DatasourceFixAction<unknown>;
|
||||
}>;
|
||||
missingRefsErrors?: Array<{ shortMessage: string; longMessage: string }>;
|
||||
};
|
||||
|
@ -432,22 +426,19 @@ export const VisualizationWrapper = ({
|
|||
application: ApplicationStart;
|
||||
activeDatasourceId: string | null;
|
||||
}) => {
|
||||
const context: ExecutionContextSearch = useMemo(
|
||||
const context = useLensSelector(selectExecutionContext);
|
||||
const searchContext: ExecutionContextSearch = useMemo(
|
||||
() => ({
|
||||
query: framePublicAPI.query,
|
||||
query: context.query,
|
||||
timeRange: {
|
||||
from: framePublicAPI.dateRange.fromDate,
|
||||
to: framePublicAPI.dateRange.toDate,
|
||||
from: context.dateRange.fromDate,
|
||||
to: context.dateRange.toDate,
|
||||
},
|
||||
filters: framePublicAPI.filters,
|
||||
filters: context.filters,
|
||||
}),
|
||||
[
|
||||
framePublicAPI.query,
|
||||
framePublicAPI.dateRange.fromDate,
|
||||
framePublicAPI.dateRange.toDate,
|
||||
framePublicAPI.filters,
|
||||
]
|
||||
[context]
|
||||
);
|
||||
const searchSessionId = useLensSelector(selectSearchSessionId);
|
||||
|
||||
const dispatchLens = useLensDispatch();
|
||||
|
||||
|
@ -465,9 +456,7 @@ export const VisualizationWrapper = ({
|
|||
| {
|
||||
shortMessage: string;
|
||||
longMessage: string;
|
||||
fixAction?:
|
||||
| { label: string; newState: (framePublicAPI: FramePublicAPI) => Promise<unknown> }
|
||||
| undefined;
|
||||
fixAction?: DatasourceFixAction<unknown>;
|
||||
}
|
||||
| undefined
|
||||
) {
|
||||
|
@ -480,7 +469,10 @@ export const VisualizationWrapper = ({
|
|||
data-test-subj="errorFixAction"
|
||||
onClick={async () => {
|
||||
trackUiEvent('error_fix_action');
|
||||
const newState = await validationError.fixAction?.newState(framePublicAPI);
|
||||
const newState = await validationError.fixAction?.newState({
|
||||
...framePublicAPI,
|
||||
...context,
|
||||
});
|
||||
dispatchLens(
|
||||
updateDatasourceState({
|
||||
updater: newState,
|
||||
|
@ -638,8 +630,8 @@ export const VisualizationWrapper = ({
|
|||
className="lnsExpressionRenderer__component"
|
||||
padding="m"
|
||||
expression={expression!}
|
||||
searchContext={context}
|
||||
searchSessionId={framePublicAPI.searchSessionId}
|
||||
searchContext={searchContext}
|
||||
searchSessionId={searchSessionId}
|
||||
onEvent={onEvent}
|
||||
onData$={onData$}
|
||||
renderMode="edit"
|
||||
|
|
|
@ -14,23 +14,22 @@ import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../../types'
|
|||
import { NativeRenderer } from '../../../native_renderer';
|
||||
import { ChartSwitch } from './chart_switch';
|
||||
import { WarningsPopover } from './warnings_popover';
|
||||
import { useLensDispatch, updateVisualizationState } from '../../../state_management';
|
||||
import {
|
||||
useLensDispatch,
|
||||
updateVisualizationState,
|
||||
DatasourceStates,
|
||||
VisualizationState,
|
||||
} from '../../../state_management';
|
||||
import { WorkspaceTitle } from './title';
|
||||
|
||||
export interface WorkspacePanelWrapperProps {
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
framePublicAPI: FramePublicAPI;
|
||||
visualizationState: unknown;
|
||||
visualizationState: VisualizationState['state'];
|
||||
visualizationMap: VisualizationMap;
|
||||
visualizationId: string | null;
|
||||
datasourceMap: DatasourceMap;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
datasourceStates: DatasourceStates;
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ import {
|
|||
FormulaIndexPatternColumn,
|
||||
} from './formula';
|
||||
import { lastValueOperation, LastValueIndexPatternColumn } from './last_value';
|
||||
import { FramePublicAPI, OperationMetadata } from '../../../types';
|
||||
import { FrameDatasourceAPI, OperationMetadata } from '../../../types';
|
||||
import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types';
|
||||
import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types';
|
||||
import { DateRange } from '../../../../common';
|
||||
|
@ -280,7 +280,7 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn> {
|
|||
label: string;
|
||||
newState: (
|
||||
core: CoreStart,
|
||||
frame: FramePublicAPI,
|
||||
frame: FrameDatasourceAPI,
|
||||
layerId: string
|
||||
) => Promise<IndexPatternLayer>;
|
||||
};
|
||||
|
@ -437,7 +437,7 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> {
|
|||
label: string;
|
||||
newState: (
|
||||
core: CoreStart,
|
||||
frame: FramePublicAPI,
|
||||
frame: FrameDatasourceAPI,
|
||||
layerId: string
|
||||
) => Promise<IndexPatternLayer>;
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ import { FieldStatsResponse } from '../../../../../common';
|
|||
import { AggFunctionsMapping, esQuery } from '../../../../../../../../src/plugins/data/public';
|
||||
import { buildExpressionFunction } from '../../../../../../../../src/plugins/expressions/public';
|
||||
import { updateColumnParam, isReferenced } from '../../layer_helpers';
|
||||
import { DataType, FramePublicAPI } from '../../../../types';
|
||||
import { DataType, FrameDatasourceAPI } from '../../../../types';
|
||||
import { FiltersIndexPatternColumn, OperationDefinition, operationDefinitionMap } from '../index';
|
||||
import { FieldBasedIndexPatternColumn } from '../column_types';
|
||||
import { ValuesInput } from './values_input';
|
||||
|
@ -77,7 +77,7 @@ function getDisallowedTermsMessage(
|
|||
label: i18n.translate('xpack.lens.indexPattern.termsWithMultipleShiftsFixActionLabel', {
|
||||
defaultMessage: 'Use filters',
|
||||
}),
|
||||
newState: async (core: CoreStart, frame: FramePublicAPI, layerId: string) => {
|
||||
newState: async (core: CoreStart, frame: FrameDatasourceAPI, layerId: string) => {
|
||||
const currentColumn = layer.columns[columnId] as TermsIndexPatternColumn;
|
||||
const fieldName = currentColumn.sourceField;
|
||||
const activeDataFieldNameMatch =
|
||||
|
|
|
@ -22,7 +22,7 @@ import { ValuesInput } from './values_input';
|
|||
import type { TermsIndexPatternColumn } from '.';
|
||||
import { termsOperation } from '../index';
|
||||
import { IndexPattern, IndexPatternLayer } from '../../../types';
|
||||
import { FramePublicAPI } from '../../../../types';
|
||||
import { FrameDatasourceAPI } from '../../../../types';
|
||||
|
||||
const uiSettingsMock = {} as IUiSettingsClient;
|
||||
|
||||
|
@ -1110,7 +1110,7 @@ describe('terms', () => {
|
|||
fromDate: '2020',
|
||||
toDate: '2021',
|
||||
},
|
||||
} as unknown) as FramePublicAPI,
|
||||
} as unknown) as FrameDatasourceAPI,
|
||||
'first'
|
||||
);
|
||||
expect(newLayer.columns.col1).toEqual(
|
||||
|
|
|
@ -9,7 +9,8 @@ import { partition, mapValues, pickBy } from 'lodash';
|
|||
import { CoreStart } from 'kibana/public';
|
||||
import { Query } from 'src/plugins/data/common';
|
||||
import type {
|
||||
FramePublicAPI,
|
||||
DatasourceFixAction,
|
||||
FrameDatasourceAPI,
|
||||
OperationMetadata,
|
||||
VisualizationDimensionGroupConfig,
|
||||
} from '../../types';
|
||||
|
@ -1249,10 +1250,7 @@ export function getErrorMessages(
|
|||
| string
|
||||
| {
|
||||
message: string;
|
||||
fixAction?: {
|
||||
label: string;
|
||||
newState: (frame: FramePublicAPI) => Promise<IndexPatternPrivateState>;
|
||||
};
|
||||
fixAction?: DatasourceFixAction<IndexPatternPrivateState>;
|
||||
}
|
||||
>
|
||||
| undefined {
|
||||
|
@ -1284,7 +1282,7 @@ export function getErrorMessages(
|
|||
fixAction: errorMessage.fixAction
|
||||
? {
|
||||
...errorMessage.fixAction,
|
||||
newState: async (frame: FramePublicAPI) => ({
|
||||
newState: async (frame: FrameDatasourceAPI) => ({
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
|
@ -1300,10 +1298,7 @@ export function getErrorMessages(
|
|||
| string
|
||||
| {
|
||||
message: string;
|
||||
fixAction?: {
|
||||
label: string;
|
||||
newState: (framePublicAPI: FramePublicAPI) => Promise<IndexPatternPrivateState>;
|
||||
};
|
||||
fixAction?: DatasourceFixAction<IndexPatternPrivateState>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
|
|
@ -39,7 +39,22 @@ import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/publ
|
|||
import { makeConfigureStore, LensAppState, LensState } from './state_management/index';
|
||||
import { getResolvedDateRange } from './utils';
|
||||
import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks';
|
||||
import { DatasourcePublicAPI, Datasource, Visualization, FramePublicAPI } from './types';
|
||||
import {
|
||||
DatasourcePublicAPI,
|
||||
Datasource,
|
||||
Visualization,
|
||||
FramePublicAPI,
|
||||
FrameDatasourceAPI,
|
||||
} from './types';
|
||||
|
||||
export function mockDatasourceStates() {
|
||||
return {
|
||||
testDatasource: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockVisualization(): jest.Mocked<Visualization> {
|
||||
return {
|
||||
|
@ -83,9 +98,9 @@ export function createMockVisualization(): jest.Mocked<Visualization> {
|
|||
};
|
||||
}
|
||||
|
||||
const visualizationMap = {
|
||||
vis: createMockVisualization(),
|
||||
vis2: createMockVisualization(),
|
||||
export const visualizationMap = {
|
||||
testVis: createMockVisualization(),
|
||||
testVis2: createMockVisualization(),
|
||||
};
|
||||
|
||||
export type DatasourceMock = jest.Mocked<Datasource> & {
|
||||
|
@ -134,7 +149,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
|
||||
const mockDatasource: DatasourceMock = createMockDatasource('testDatasource');
|
||||
const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2');
|
||||
const datasourceMap = {
|
||||
export const datasourceMap = {
|
||||
testDatasource2: mockDatasource2,
|
||||
testDatasource: mockDatasource,
|
||||
};
|
||||
|
@ -148,12 +163,18 @@ export function createExpressionRendererMock(): jest.Mock<
|
|||
|
||||
export type FrameMock = jest.Mocked<FramePublicAPI>;
|
||||
export function createMockFramePublicAPI(): FrameMock {
|
||||
return {
|
||||
datasourceLayers: {},
|
||||
};
|
||||
}
|
||||
|
||||
export type FrameDatasourceMock = jest.Mocked<FrameDatasourceAPI>;
|
||||
export function createMockFrameDatasourceAPI(): FrameDatasourceMock {
|
||||
return {
|
||||
datasourceLayers: {},
|
||||
dateRange: { fromDate: 'now-7d', toDate: 'now' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
searchSessionId: 'sessionId',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -393,12 +414,7 @@ export const defaultState = {
|
|||
state: {},
|
||||
activeId: 'testVis',
|
||||
},
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
isLoading: false,
|
||||
state: '',
|
||||
},
|
||||
},
|
||||
datasourceStates: mockDatasourceStates(),
|
||||
};
|
||||
|
||||
export function makeLensStore({
|
||||
|
|
|
@ -14,6 +14,7 @@ import { optimizingMiddleware } from './optimizing_middleware';
|
|||
import { LensState, LensStoreDeps } from './types';
|
||||
import { initMiddleware } from './init_middleware';
|
||||
export * from './types';
|
||||
export * from './selectors';
|
||||
|
||||
export const reducer = {
|
||||
lens: lensSlice.reducer,
|
||||
|
|
153
x-pack/plugins/lens/public/state_management/selectors.ts
Normal file
153
x-pack/plugins/lens/public/state_management/selectors.ts
Normal file
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { SavedObjectReference } from 'kibana/server';
|
||||
import { LensState } from './types';
|
||||
import { extractFilterReferences } from '../persistence';
|
||||
import { Datasource, DatasourceMap, VisualizationMap } from '../types';
|
||||
import { createDatasourceLayers } from '../editor_frame_service/editor_frame';
|
||||
|
||||
export const selectPersistedDoc = (state: LensState) => state.lens.persistedDoc;
|
||||
export const selectQuery = (state: LensState) => state.lens.query;
|
||||
export const selectSearchSessionId = (state: LensState) => state.lens.searchSessionId;
|
||||
export const selectFilters = (state: LensState) => state.lens.filters;
|
||||
export const selectResolvedDateRange = (state: LensState) => state.lens.resolvedDateRange;
|
||||
export const selectVisualization = (state: LensState) => state.lens.visualization;
|
||||
export const selectStagedPreview = (state: LensState) => state.lens.stagedPreview;
|
||||
export const selectDatasourceStates = (state: LensState) => state.lens.datasourceStates;
|
||||
export const selectActiveDatasourceId = (state: LensState) => state.lens.activeDatasourceId;
|
||||
export const selectActiveData = (state: LensState) => state.lens.activeData;
|
||||
export const selectIsFullscreenDatasource = (state: LensState) =>
|
||||
Boolean(state.lens.isFullscreenDatasource);
|
||||
|
||||
export const selectExecutionContext = createSelector(
|
||||
[selectQuery, selectFilters, selectResolvedDateRange],
|
||||
(query, filters, dateRange) => ({
|
||||
dateRange,
|
||||
query,
|
||||
filters,
|
||||
})
|
||||
);
|
||||
|
||||
export const selectExecutionContextSearch = createSelector(selectExecutionContext, (res) => ({
|
||||
query: res.query,
|
||||
timeRange: {
|
||||
from: res.dateRange.fromDate,
|
||||
to: res.dateRange.toDate,
|
||||
},
|
||||
filters: res.filters,
|
||||
}));
|
||||
|
||||
const selectDatasourceMap = (state: LensState, datasourceMap: DatasourceMap) => datasourceMap;
|
||||
|
||||
const selectVisualizationMap = (
|
||||
state: LensState,
|
||||
datasourceMap: DatasourceMap,
|
||||
visualizationMap: VisualizationMap
|
||||
) => visualizationMap;
|
||||
|
||||
export const selectSavedObjectFormat = createSelector(
|
||||
[
|
||||
selectPersistedDoc,
|
||||
selectVisualization,
|
||||
selectDatasourceStates,
|
||||
selectQuery,
|
||||
selectFilters,
|
||||
selectActiveDatasourceId,
|
||||
selectDatasourceMap,
|
||||
selectVisualizationMap,
|
||||
],
|
||||
(
|
||||
persistedDoc,
|
||||
visualization,
|
||||
datasourceStates,
|
||||
query,
|
||||
filters,
|
||||
activeDatasourceId,
|
||||
datasourceMap,
|
||||
visualizationMap
|
||||
) => {
|
||||
const activeVisualization =
|
||||
visualization.state && visualization.activeId && visualizationMap[visualization.activeId];
|
||||
const activeDatasource =
|
||||
datasourceStates && activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading
|
||||
? datasourceMap[activeDatasourceId]
|
||||
: undefined;
|
||||
|
||||
if (!activeDatasource || !activeVisualization) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeDatasources: Record<string, Datasource> = Object.keys(datasourceStates).reduce(
|
||||
(acc, datasourceId) => ({
|
||||
...acc,
|
||||
[datasourceId]: datasourceMap[datasourceId],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
const persistibleDatasourceStates: Record<string, unknown> = {};
|
||||
const references: SavedObjectReference[] = [];
|
||||
Object.entries(activeDatasources).forEach(([id, datasource]) => {
|
||||
const { state: persistableState, savedObjectReferences } = datasource.getPersistableState(
|
||||
datasourceStates[id].state
|
||||
);
|
||||
persistibleDatasourceStates[id] = persistableState;
|
||||
references.push(...savedObjectReferences);
|
||||
});
|
||||
|
||||
const { persistableFilters, references: filterReferences } = extractFilterReferences(filters);
|
||||
|
||||
references.push(...filterReferences);
|
||||
|
||||
return {
|
||||
savedObjectId: persistedDoc?.savedObjectId,
|
||||
title: persistedDoc?.title || '',
|
||||
description: persistedDoc?.description,
|
||||
visualizationType: visualization.activeId,
|
||||
type: 'lens',
|
||||
references,
|
||||
state: {
|
||||
visualization: visualization.state,
|
||||
query,
|
||||
filters: persistableFilters,
|
||||
datasourceStates: persistibleDatasourceStates,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const selectCurrentVisualization = createSelector(
|
||||
[selectVisualization, selectStagedPreview],
|
||||
(visualization, stagedPreview) => (stagedPreview ? stagedPreview.visualization : visualization)
|
||||
);
|
||||
|
||||
export const selectCurrentDatasourceStates = createSelector(
|
||||
[selectDatasourceStates, selectStagedPreview],
|
||||
(datasourceStates, stagedPreview) =>
|
||||
stagedPreview ? stagedPreview.datasourceStates : datasourceStates
|
||||
);
|
||||
|
||||
export const selectAreDatasourcesLoaded = createSelector(
|
||||
selectDatasourceStates,
|
||||
(datasourceStates) =>
|
||||
Object.values(datasourceStates).every(({ isLoading }) => isLoading === false)
|
||||
);
|
||||
|
||||
export const selectDatasourceLayers = createSelector(
|
||||
[selectDatasourceStates, selectDatasourceMap],
|
||||
(datasourceStates, datasourceMap) => createDatasourceLayers(datasourceStates, datasourceMap)
|
||||
);
|
||||
|
||||
export const selectFramePublicAPI = createSelector(
|
||||
[selectDatasourceStates, selectActiveData, selectDatasourceMap],
|
||||
(datasourceStates, activeData, datasourceMap) => ({
|
||||
datasourceLayers: createDatasourceLayers(datasourceStates, datasourceMap),
|
||||
activeData,
|
||||
})
|
||||
);
|
|
@ -10,7 +10,10 @@ import moment from 'moment';
|
|||
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
|
||||
import { setState, LensDispatch } from '.';
|
||||
import { LensAppState } from './types';
|
||||
import { getResolvedDateRange, containsDynamicMath, TIME_LAG_PERCENTAGE_LIMIT } from '../utils';
|
||||
import { getResolvedDateRange, containsDynamicMath } from '../utils';
|
||||
|
||||
const TIME_LAG_PERCENTAGE_LIMIT = 0.02;
|
||||
const TIME_LAG_MIN_LIMIT = 10000; // for a small timerange to avoid infinite data refresh timelag minimum is TIME_LAG_ABSOLUTE ms
|
||||
|
||||
/**
|
||||
* checks if TIME_LAG_PERCENTAGE_LIMIT passed to renew searchSessionId
|
||||
|
@ -48,8 +51,8 @@ function updateTimeRange(data: DataPublicPluginStart, dispatch: LensDispatch) {
|
|||
// calculate lag of managed "now" for date math
|
||||
const nowDiff = Date.now() - data.nowProvider.get().valueOf();
|
||||
|
||||
// if the lag is signifcant, start a new session to clear the cache
|
||||
if (nowDiff > timeRangeLength * TIME_LAG_PERCENTAGE_LIMIT) {
|
||||
// if the lag is significant, start a new session to clear the cache
|
||||
if (nowDiff > Math.max(timeRangeLength * TIME_LAG_PERCENTAGE_LIMIT, TIME_LAG_MIN_LIMIT)) {
|
||||
dispatch(
|
||||
setState({
|
||||
searchSessionId: data.search.session.start(),
|
||||
|
|
|
@ -15,12 +15,15 @@ import { DateRange } from '../../common';
|
|||
import { LensAppServices } from '../app_plugin/types';
|
||||
import { DatasourceMap, VisualizationMap } from '../types';
|
||||
|
||||
export interface VisualizationState {
|
||||
activeId: string | null;
|
||||
state: unknown;
|
||||
}
|
||||
|
||||
export type DatasourceStates = Record<string, { state: unknown; isLoading: boolean }>;
|
||||
export interface PreviewState {
|
||||
visualization: {
|
||||
activeId: string | null;
|
||||
state: unknown;
|
||||
};
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>;
|
||||
visualization: VisualizationState;
|
||||
datasourceStates: DatasourceStates;
|
||||
}
|
||||
export interface EditorFrameState extends PreviewState {
|
||||
activeDatasourceId: string | null;
|
||||
|
|
|
@ -256,6 +256,11 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
getWarningMessages?: (state: T, frame: FramePublicAPI) => React.ReactNode[] | undefined;
|
||||
}
|
||||
|
||||
export interface DatasourceFixAction<T> {
|
||||
label: string;
|
||||
newState: (frame: FrameDatasourceAPI) => Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource
|
||||
*/
|
||||
|
@ -516,10 +521,11 @@ export interface FramePublicAPI {
|
|||
* If accessing, make sure to check whether expected columns actually exist.
|
||||
*/
|
||||
activeData?: Record<string, Datatable>;
|
||||
}
|
||||
export interface FrameDatasourceAPI extends FramePublicAPI {
|
||||
dateRange: DateRange;
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
searchSessionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,11 +10,10 @@ import { IndexPattern, IndexPatternsContract, TimefilterContract } from 'src/plu
|
|||
import { IUiSettingsClient } from 'kibana/public';
|
||||
import moment from 'moment-timezone';
|
||||
import { SavedObjectReference } from 'kibana/public';
|
||||
import { Filter, Query } from 'src/plugins/data/public';
|
||||
import { uniq } from 'lodash';
|
||||
import { Document } from './persistence/saved_object_store';
|
||||
import { Datasource, DatasourceMap } from './types';
|
||||
import { extractFilterReferences } from './persistence';
|
||||
import { DatasourceStates } from './state_management';
|
||||
|
||||
export function getVisualizeGeoFieldMessage(fieldType: string) {
|
||||
return i18n.translate('xpack.lens.visualizeGeoFieldMessage', {
|
||||
|
@ -36,8 +35,6 @@ export function containsDynamicMath(dateMathString: string) {
|
|||
return dateMathString.includes('now');
|
||||
}
|
||||
|
||||
export const TIME_LAG_PERCENTAGE_LIMIT = 0.02;
|
||||
|
||||
export function getTimeZone(uiSettings: IUiSettingsClient) {
|
||||
const configuredTimeZone = uiSettings.get('dateFormat:tz');
|
||||
if (configuredTimeZone === 'Browser') {
|
||||
|
@ -59,66 +56,12 @@ export const getInitialDatasourceId = (datasourceMap: DatasourceMap, doc?: Docum
|
|||
return (doc && getActiveDatasourceIdFromDoc(doc)) || Object.keys(datasourceMap)[0] || null;
|
||||
};
|
||||
|
||||
export interface GetIndexPatternsObjects {
|
||||
activeDatasources: Record<string, Datasource>;
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>;
|
||||
visualization: {
|
||||
activeId: string | null;
|
||||
state: unknown;
|
||||
};
|
||||
filters: Filter[];
|
||||
query: Query;
|
||||
title: string;
|
||||
description?: string;
|
||||
persistedId?: string;
|
||||
}
|
||||
|
||||
export function getSavedObjectFormat({
|
||||
activeDatasources,
|
||||
datasourceStates,
|
||||
visualization,
|
||||
filters,
|
||||
query,
|
||||
title,
|
||||
description,
|
||||
persistedId,
|
||||
}: GetIndexPatternsObjects): Document {
|
||||
const persistibleDatasourceStates: Record<string, unknown> = {};
|
||||
const references: SavedObjectReference[] = [];
|
||||
Object.entries(activeDatasources).forEach(([id, datasource]) => {
|
||||
const { state: persistableState, savedObjectReferences } = datasource.getPersistableState(
|
||||
datasourceStates[id].state
|
||||
);
|
||||
persistibleDatasourceStates[id] = persistableState;
|
||||
references.push(...savedObjectReferences);
|
||||
});
|
||||
|
||||
const { persistableFilters, references: filterReferences } = extractFilterReferences(filters);
|
||||
|
||||
references.push(...filterReferences);
|
||||
|
||||
return {
|
||||
savedObjectId: persistedId,
|
||||
title,
|
||||
description,
|
||||
type: 'lens',
|
||||
visualizationType: visualization.activeId,
|
||||
state: {
|
||||
datasourceStates: persistibleDatasourceStates,
|
||||
visualization: visualization.state,
|
||||
query,
|
||||
filters: persistableFilters,
|
||||
},
|
||||
references,
|
||||
};
|
||||
}
|
||||
|
||||
export function getIndexPatternsIds({
|
||||
activeDatasources,
|
||||
datasourceStates,
|
||||
}: {
|
||||
activeDatasources: Record<string, Datasource>;
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>;
|
||||
datasourceStates: DatasourceStates;
|
||||
}): string[] {
|
||||
const references: SavedObjectReference[] = [];
|
||||
Object.entries(activeDatasources).forEach(([id, datasource]) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue